How to append an array inside an array? - arrays

I have an array of working hours, and within that array, there is another array of shifts. I would like to remove all instances in the shifts array where weekday is equal to "Monday"
struct Shift {
let id: String = UUID().uuidString
var weekday: String
var startTime: String
var endTime: String
var error: Bool
}
struct WorkingHours: Identifiable {
let id: String = UUID().uuidString
var availability: [Shift]
}
class AvailabilityManager: ObservableObject {
#Published var workingHours: [WorkingHours] = []
}
In my view:
#EnvironmentObject var availabilityManager: AvailabilityManager
self.availabilityManager.workingHours.removeAll(where: {$0.availability.weekday == "Monday"})
However, it says: "Value of type '[Shift]' has no member 'weekday'"
Any help is appreciated :)

Change
self.availabilityManager.workingHours.removeAll(where: {$0.availability.weekday == "Monday"})
To
self.availabilityManager.workingHours.removeAll(where: {$0.availability.contains(where: {$0.weekday == "Monday"})})
More shorthand method
self.availabilityManager.workingHours.removeAll { $0.availability.contains { $0.weekday == "Monday" } }

Add the following function to WorkingHours
mutating func removeShift(weekDay: String) {
availability = availability.filter { $0.weekday != weekDay }
}
and call it like
workingHour.removeShift(weekDay: "Monday")
If you have an array of WorkinHours you can call the method using map for instance
workingHoursArray = workingHoursArray.map {
var workingHours = $0
workingHours.removeShift(weekDay: "Monday")
return workingHours
}

Related

Swift Struct in Array of Structs not updating to new values

This is my data structure
struct SPPWorkout: Codable {
static let setKey = "Sets"
static let exerciseID = "id"
var id: Double? = 0.0
var duration: String?
var calories: Int?
var date: String?
var exercises: [ExerciseSet]
[...]
}
struct ExerciseSet: Codable {
let id: String
let name: String
var reps: Int
var weight: Double
[...]
}
extension ExerciseSet: Equatable {
static func ==(lhs: ExerciseSet, rhs: ExerciseSet) -> Bool {
return lhs.id == rhs.id
}
}
and in a SwiftUI view I'm trying to modify an ExerciseSet from user input
#State private var sppWorkout: SPPWorkout!
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ?? 0.0
self.editingIndex = nil
})
}
The issue is here
sppWorkout.exercises[editingIndex].reps = Int(reps) ?? 0
sppWorkout.exercises[editingIndex].weight = Double(weight) ??
and I've tried in all ways to update it, both from the view and with a func in SPPWorkout. I've also tried to replace the object at index
var newSet = ExerciseSet(id: [...], newValues)
self.exercises[editingIndex] = newSet
but in no way it wants to update. I'm sure that somewhere it creates a copy that it edits but I have no idea why and how to set the new values.
Edit: if I try to delete something, it's fine
sppWorkout.exercises.removeAll(where: { $0 == sppWorkout.exercises[index]})
Edit 2:
It passes the guard statement and it does not change the values in the array.
Edit 3:
At the suggestion below from Jared, I've copied the existing array into a new one, set the new values then tried to assign the new one over to the original one but still, it does not overwrite.
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
print(sppWorkout.exercises)
guard let editingIndex = editingIndex else { return }
var copyOfTheArray = sppWorkout.exercises
copyOfTheArray[editingIndex].reps = Int(reps) ?? 0
copyOfTheArray[editingIndex].weight = Double(weight) ?? 0.0
//Copy of the array is updated correctly, it has the new values
sppWorkout.exercises = copyOfTheArray
//Original array doesn't get overwritten. It still has old values
self.editingIndex = nil
Edit 4: I've managed to make progress by extracting the model into a view model and updating the values there. Now the values get updated in sppWorkout, but even though I call objectWillChange.send(), the UI Update doesn't trigger.
full code:
class WorkoutDetailsViewModel: ObservableObject {
var workoutID: String!
#Published var sppWorkout: SPPWorkout!
func setupData(with workoutID: String) {
sppWorkout = FileIOManager.readWorkout(with: workoutID)
}
func update(_ index: Int, newReps: Int, newWeight: Double) {
let oldOne = sppWorkout.exercises[index]
let update = ExerciseSet(id: oldOne.id, name: oldOne.name, reps: newReps, weight: newWeight)
sppWorkout.exercises[index] = update
self.objectWillChange.send()
}
}
struct WorkoutDetailsView: View {
var workoutID: String!
#StateObject private var viewModel = WorkoutDetailsViewModel()
var workout: HKWorkout
var dateFormatter: DateFormatter
#State private var offset = 0
#State private var isShowingOverlay = false
#State private var editingIndex: Int?
#EnvironmentObject var settingsManager: SettingsManager
#Environment(\.dismiss) private var dismiss
var body: some View {
if viewModel.sppWorkout != nil {
VStack {
ListWorkoutItem(workout: workout, dateFormatter: dateFormatter)
.padding([.leading, .trailing], 10.0)
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
let index = viewModel.sppWorkout.exercises.firstIndex(of: exercise) ?? 0
DetailListSetItem(exerciseSet: viewModel.sppWorkout.exercises[index], set: index + 1)
.environmentObject(settingsManager)
.swipeActions {
Button(role: .destructive, action: {
viewModel.sppWorkout.exercises.removeAll(where: { $0 == viewModel.sppWorkout.exercises[index]})
} ) {
Label("Delete", systemImage: "trash")
}
Button(role: .none, action: {
isShowingOverlay = true
editingIndex = index
} ) {
Label("Edit", systemImage: "pencil")
}.tint(.blue)
}
}
.padding([.leading, .trailing], -30)
//iOS 16 .scrollContentBackground(.hidden)
}
.overlay(alignment: .bottom, content: {
editOverlay
.animation(.easeInOut (duration: 0.5), value: isShowingOverlay)
})
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button(action : {
do {
try FileIOManager.write(viewModel.sppWorkout, toDocumentNamed: "\(viewModel.sppWorkout.id ?? 0).json")
} catch {
Debugger.log(error: error.localizedDescription)
}
dismiss()
}){
Image(systemName: "arrow.left")
})
} else {
Text("No workout details found")
.italic()
.fontWeight(.bold)
.font(.system(size: 35))
.onAppear(perform: {
viewModel.setupData(with: workoutID)
})
}
}
#ViewBuilder private var editOverlay: some View {
if isShowingOverlay {
ZStack {
Button {
isShowingOverlay = false
} label: {
Color.clear
}
.edgesIgnoringSafeArea(.all)
VStack{
Spacer()
EditSetPopup(isShowingOverlay: $isShowingOverlay,
update: { reps, weight in
guard let editingIndex = editingIndex else { return }
print(viewModel.sppWorkout.exercises)
print("dupa aia:\n")
viewModel.update(editingIndex, newReps: Int(reps) ?? 0, newWeight: Double(weight) ?? 0.0)
print(viewModel.sppWorkout.exercises)
self.editingIndex = nil
})
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color("popupBackground"),
lineWidth: 3)
)
}
}
}
}
}
So I got a very good explanation on reddit on what causes the problem. Thank you u/neddy-seagoon if you are reading this.
The explanation
. I believe that updating an array will not trigger a state update. The only thing that will, with an array, is if the count changes. So
sppWorkout.exercises[index].reps = newReps
will not cause a trigger. This is not changing viewModel.sppWorkout.exercises.indices
So all I had to to was modify my List from
List(viewModel.sppWorkout.exercises, id: \.id)
to
List(viewModel.sppWorkout.exercises, id: \.hashValue)
as this triggers the list update because the hashValue does change when updating the properties of the entries in the list.
For the line
List(viewModel.sppWorkout.exercises, id: \.id) { exercise in
Replace with
List(viewModel.sppWorkout.exercises, id: \.self) { exercise in

Remove Object From Array with Specific Requirement

I have a custom object Shift
struct Shift: Identifiable, Encodable, Equatable {
var id: String = UUID().uuidString
var weekday: String
var startTime: Date
var endTime: Date
}
And an array of Shift objects:
#Published var shifts: [Shift] = []
I would like to remove the last item in the array where the value of weekday is equal to "Monday"
I tried this but it throws an error saying Value of type 'Array<Shift>.Index' (aka 'Int') has no member 'removeLast'
if let index = shifts.firstIndex(where: {$0.weekday == "Monday"}) {
index.removeLast()
}
Any help is appreciated :)
No, you want the last index and you want to remove the item from the shifts array rather than from the index
if let index = shifts.lastIndex(where: {$0.weekday == "Monday"}) {
shifts.remove(at: index)
}

How can I create a function to search for an ID, locate and change one field of this record?

Model:
enum TaskType: Int, Codable {
case upcoming = 0
case inProgress
case testing
case completed
var title: String {
switch self {
case .upcoming:
return "Upcoming"
case .inProgress:
return "In Progress"
case .testing:
return "Testing"
case .completed:
return "Completed"
}
}
}
struct TasksModel: Encodable, Decodable {
var upcomingArray: [TaskInfo]
var inProgressArray: [TaskInfo]
var testingArray: [TaskInfo]
var completedArray: [TaskInfo]
}
struct TaskInfo: Codable, Equatable, Identifiable {
var id: String
var title: String
var description: String
var taskStatus: TaskType
var taskDate = Date()
}
VM:
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel
self.tasksArray = TasksModel.init(upcomingArray: [], inProgressArray: [], testingArray: [], completedArray: [])
}
So now that I could locate the record with received taskID and change the taskStatus, I need also to move the record from upcomingArray to inProgressArray. This is what I’m trying:
func inProgressSetTask(taskID: String) {
#StateObject var viewModel = HomeVM()
if let index = viewModel.tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
// Update task status
viewModel.tasksArray.upcomingArray[index].taskStatus = .inProgress
// Need to remove from upcomingArray and append into inProgressArray
viewModel.tasksArray.upcomingArray.remove(at: index)
var lastIndex = viewModel.tasksArray.inProgressArray.last
viewModel.tasksArray.inProgressArray[lastIndex].append()
viewModel.save()
// End
} else {
…
Updating taskStatus above working fine but remove from one array into another is not.
This code above will repeat for each array after else. Appreciate any help.
you could try the following example code to achieve what you want:
(note, you should have #StateObject var viewModel = HomeVM() outside of the func inProgressSetTask(taskID: String) {...}
or pass it in as a parameter)
EDIT-1: moving the function with all arrays into HomeVM and assuming id are unique.
func inProgressSetTask(taskID: String) {
print("InProgress Set ID# \(taskID)")
// with index, using `firstIndex`
if let index = viewModel.tasksArray.inProgressArray.firstIndex(where: {$0.id == taskID}) {
// do something with the index
viewModel.tasksArray.inProgressArray[index].title = "xxx"
}
// with TaskInfo, using `first`
if var taskInfo = viewModel.tasksArray.inProgressArray.first(where: {$0.id == taskID}) {
// do something with the taskInfo
taskInfo.title = "xxx"
}
}
With all arrays of TaskInfo, use the function setTaskFromAll(...) in HomeVM. For example: viewModel.setTaskFromAll(taskID: "1")
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel = TasksModel.init(upcomingArray: [], inProgressArray: [], testingArray: [], completedArray: [])
func setTaskFromAll(taskID: String) {
if let index = tasksArray.inProgressArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.inProgressArray[index].title = "inProgress"
} else {
if let index = tasksArray.completedArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.completedArray[index].title = "completed"
} else {
if let index = tasksArray.testingArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.testingArray[index].title = "testing"
} else {
if let index = tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
tasksArray.upcomingArray[index].title = "upcoming"
}
}
}
}
}
}
EDIT-2:
However, since you already have the "TaskType" of each array in the TaskInfo struct, why not remove TasksModel
and use a single array of TaskInfo in your HomeVM. Like this:
class HomeVM: ObservableObject {
#Published var tasksArray: [TaskInfo] = [
TaskInfo(id: "1", title: "title1", description: "description1", taskStatus: .upcoming),
TaskInfo(id: "2", title: "title2", description: "description2", taskStatus: .inProgress)
// ....
]
func setTask(taskID: String, to taskType: TaskType) {
if let index = tasksArray.firstIndex(where: {$0.id == taskID}) {
tasksArray[index].taskStatus = taskType
}
}
func getAllTaskInfo(_ oftype: TaskType) -> [TaskInfo] {
return tasksArray.filter{$0.taskStatus == oftype}
}
}
and use it like this: viewModel.setTask(taskID: "1", to: .testing) and viewModel.getAllTaskInfo(.inProgress)
EDIT-3: to remove from one array and append to another, using your TasksModel scheme, use this:
class HomeVM: ObservableObject {
#Published var tasksArray: TasksModel = TasksModel(upcomingArray: [
TaskInfo(id: "1", title: "title1", description: "description1", taskStatus: .upcoming),
TaskInfo(id: "2", title: "title2", description: "description2", taskStatus: .upcoming)
], inProgressArray: [
TaskInfo(id: "3", title: "title3", description: "description3", taskStatus: .inProgress),
TaskInfo(id: "4", title: "title4", description: "description4", taskStatus: .inProgress)
], testingArray: [], completedArray: [])
func inProgressSetTask(taskID: String) {
if let index = tasksArray.upcomingArray.firstIndex(where: {$0.id == taskID}) {
// update task status
tasksArray.upcomingArray[index].taskStatus = .inProgress
// get the upcomingArray taskInfo
let taskInfo = tasksArray.upcomingArray[index]
// remove from upcomingArray
tasksArray.upcomingArray.remove(at: index)
// append to inProgressArray
tasksArray.inProgressArray.append(taskInfo)
} else {
// ...
}
}
}
Use it like this: viewModel.inProgressSetTask(taskID: "1")
As you can plainly see, you are much better-off with the EDIT-2, you are repeating/duplicating things in EDIT-3 for no reason. There is no need for separate arrays for the different TaskType, you already have this info in the TaskInfo var taskStatus: TaskType. With EDIT-2, use viewModel.getAllTaskInfo(.inProgress) to get all TaskInfo of a particular type, just like it would be if you used a separate array.
You are attempting to compare a String to a TaskInfo, because the elements of an inProgressArray are of type TaskInfo. What you need to do is drill into the array and get to the .id. That is simple to do. In the .first(where:), you simply pass a closure of $0.id == taskID.
if let index = viewModel.tasksArray.inProgressArray.first(where: { $0.id == taskID } ) {

swiftUI array.first(where: not working with struct/class array

I am trying to find a string within an array using this code;
var userArray = [UserItem]()
var foundUser: String {
guard let findUser = userArray.first(where: { $0 == item.name }) else { return "Not found" }
return findUser
}
But I am getting the following error message;
"Cannot convert value of type '(String) -> Bool' to expected argument type '(UserList) throws -> Bool'"
So I tried adding a standard array;
var userArray = ["1", "Gale Dyer", "3", "4"]
and got rid of the error and the result I intended.
I assume it is because my struct or class does not conform to String but I am not sure how I fix this as adding ", String" doesn't seem to be the answer.
For reference here is the other data;
struct UserItem: Codable, Identifiable {
var id: String
var isActive: Bool
var name: String
var age: Int
var company: String
var email: String
var address: String
var about: String
// var registered: Date
var tags: [String]
var friends: [Friend]
}
struct Friend: Codable {
var id: String
var name: String
}
class UserList: ObservableObject {
#Published var items = [UserItem]()
{
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {
UserDefaults.standard.set(encoded, forKey: "Items")
}
}
}
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([UserItem].self, from: items) {
self.items = decoded
return
}
}
self.items = []
}
}
Thank you in advance
You have to check with same type in the closure of first(where:) method. Here's the fix.
var foundUser: String {
guard let findUser = userArray.first(where: { $0.name == item.name })?.name else { return "Not found" }
return findUser
}

Swift initialise empty array to store different structs later

I have a couple of different types of structs (Promo & Event). I'd like to create an empty array which gets populated with an array of each type depending on the outcome of an if statement.
So something like this:
var dataArray:[Any] = [] // see options I've tried below
if myTest == true {
dataArray = [Promo, Promo, Promo]
} else {
dataArray = [Event, Event, Event]
}
I have tried using:
1. var dataArray: [Any] = []
2. var dataArray: [AnyObject] = []
3. var dataArray: [Any] = [Any]()
4. var dataArray: [AnyObject] = [AnyObject]()
but when I try to store an array of Promo Structs in dataArray I get an error Cannot assign value of type '[Promo]' to type '[Any]' etc.
So, how do I initialise an array so that it can store a variety of (unknown) Structs. Or how do I modify my Structs to enable them to be stored in an array?
I'm really struggling to see what I'm doing wrong so any pointers would be v. helpful.
Here are my Structs:
Promo.swift
import Foundation
struct Promo {
// initialise the stored properties for use later
let promoId : Int
let date : NSDate
let title: String
let body: String
let busName : String
let busId : Int
let categoryId: Int
let featured: Bool
// a universal init() method which has optional parameters
init(promoId: Int,
date: NSDate,
title: String,
body: String,
busName: String,
busId: Int,
categoryId: Int,
featured: Bool
){
self.promoId = promoId
self.date = date
self.title = title
self.body = body
self.busName = busName
self.busId = busId
self.categoryId = categoryId
self.featured = featured
}
}
// allow us to compare structs
extension Promo: Equatable {}
func ==(lhs: Promo, rhs: Promo) -> Bool {
return lhs.promoId == rhs.promoId
&& lhs.date == rhs.date
&& lhs.title == rhs.title
&& lhs.body == rhs.body
&& lhs.busName == rhs.busName
&& lhs.busId == rhs.busId
&& lhs.categoryId == rhs.categoryId
&& lhs.featured == rhs.featured
}
Event.swift
import Foundation
struct Event {
// initialise the stored properties for use later
let eventId : Int
let date : NSDate
let title: String
let body: String
let busName : String
let busId : Int
let categoryId: Int
// a universal init() method which has optional parameters
init(eventId: Int,
date: NSDate,
title: String,
body: String,
busName: String,
busId: Int,
categoryId: Int
){
self.eventId = eventId
self.date = date
self.title = title
self.body = body
self.busName = busName
self.busId = busId
self.categoryId = categoryId
}
}
This may not be exactly what you intended, but you can make this a bit cleaner by using classes instead of structs. It appears that a 'Promo' is just an 'Event' with one extra data member (featured)... If that's the case, then you can rename the Promo.promoId field Promo.eventId, and then make it a subclass of Event. Like this:
class Promo : Event {
let featured: Bool
// a universal init() method which has optional parameters
init(eventId: Int,
date: NSDate,
title: String,
body: String,
busName: String,
busId: Int,
categoryId: Int,
featured: Bool
){
self.featured = featured
super.init(eventId: eventId, date: date, title: title, body: body, busName: busName, busId: busId, categoryId: categoryId)
}
}
Then just create the data array like this:
var dataArray = [Event]()
if myTest == true {
dataArray = [promo1, promo2, promo3]
} else {
dataArray = [event1, event2, event3]
}
To use the featured member for a Promo you'll still need to cast like this:
if let thisPromo = dataArray[0] as? Promo {
print(thisPromo.featured)
}
If you are trying to assign to dataArray from [Promo] or [Event] arrays, you could map:
var dataArray:[Any] = []
var promoArray:[Promo] = [Promo(), Promo(), Promo()]
var eventArray:[Event] = [Event(), Event(),Event()]
if myTest == true {
dataArray = promoArray.map { $0 as Any }
} else {
dataArray = eventArray.map { $0 as Any }
}
Or create new Any arrays:
if myTest == true {
dataArray = Array<Any>(arrayLiteral: promoArray)
} else {
dataArray = Array<Any>(arrayLiteral: eventArray)
}

Resources