Saving User Data to Array inside Struct - arrays

I'd like to properly save inputted user data / variables inside an array which is inside a struct but unable to get it to work.
Currently, I have a few structs:
struct Household: Codable {
let id = UUID()
var region: String
var householdSize: Int = 1
var receivingBenefits: [String]
var energyCrisis: Bool
var utilityProviders: [String]
var residenceType: String
var propertyTaxPastDue, homeNeedsRepairs, filedPreviousYearTaxReturn, heatingSystemNeedsRepairs: Bool
var atRiskOfHomelessness: Bool
var receivedMaximumBenefit: ReceivedMaximumBenefit
var personDetails: [PersonDetail]
var incomes: [Income]
var assets: [Asset]
enum CodingKeys: String, CodingKey {
case region
case householdSize = "household_size"
case receivingBenefits = "receiving_benefits"
case energyCrisis = "energy_crisis"
case utilityProviders = "utility_providers"
case residenceType = "residence_type"
case propertyTaxPastDue = "property_tax_past_due"
case homeNeedsRepairs = "home_needs_repairs"
case filedPreviousYearTaxReturn = "filed_previous_year_tax_return"
case heatingSystemNeedsRepairs = "heating_system_needs_repairs"
case atRiskOfHomelessness = "at_risk_of_homelessness"
case receivedMaximumBenefit = "received_maximum_benefit"
case personDetails = "person_details"
case incomes, assets
}
}
struct PersonDetail: Codable, Identifiable {
let id = UUID() // <-- here
var age: Int = 18
var maritalStatus: String = ""
var minimumEmploymentOverExtendedPeriod: Bool
var workStatus: String = ""
var pregnant: Bool
var attendingSchool: Bool = false
var disabled: Bool
enum CodingKeys: String, CodingKey {
case age
case maritalStatus = "marital_status"
case minimumEmploymentOverExtendedPeriod = "minimum_employment_over_extended_period"
case workStatus = "work_status"
case pregnant
case attendingSchool = "attending_school"
case disabled
}
}
class Base: ObservableObject, Codable {
#Published var household: Household
enum CodingKeys: String, CodingKey {
case household = "household"
}
}
Now, I can easily bind a Textfield, toggle or Picker to anything under the Household struct for example below which I believe is easily connected via household in the Base() class..
HStack() {
Image(systemName:"wrench.and.screwdriver.fill")
.frame(width: 15, height: 15)
Toggle(isOn: $eligBase.household.homeNeedsRepairs) {
Text("Need Home Repairs?")
.font(.system(size: 15))
}.tint(.blue)
}
However, I'm unable to connect anything in the array`[PersonDetail], which I included the struct.
For example, If I wanted to connected a Toggle for the disabled variable in `PersonDetail', I get an error, here is my disabled toggle:
Toggle(isOn: $eligBase.household.personDetails.disabled) {
Text("Disabled")
.font(.system(size: 15))
......
}
I receive an error stating:
Value of type 'Binding<[PersonDetail]>' has no dynamic member
'disabled' using key path from root type '[PersonDetail]'
Any ideas how I can connect Toggles, Textfield, Picker to a variable in an array which is in a struct?

You can use a List for your array of PersonDetail, such as in this example code:
EDIT:
struct ContentView: View {
#StateObject var eligBase = Base()
var body: some View {
List {
let _ = print("\n---> \(eligBase.household.personDetails.count) \n")
Section("About You") {
ForEach($eligBase.household.personDetails) { $person in
HStack {
Text("Age")
Spacer()
Picker("", selection: $person.age) {
ForEach(0...100, id: \.self) {
Text("\($0)")
}
}.pickerStyle(.menu)
}
HStack {
Text("Marital Status")
Spacer()
Picker("", selection: $person.maritalStatus) {
ForEach(0...3, id: \.self) {
Text("\($0)")
}
}.pickerStyle(.menu)
}
}
}
}
.onAppear {
eligBase.household = Household(region: "xyz", householdSize: 0, receivingBenefits: [], energyCrisis: false, utilityProviders: [], residenceType: "", propertyTaxPastDue: false, homeNeedsRepairs: false, filedPreviousYearTaxReturn: false, heatingSystemNeedsRepairs: false, atRiskOfHomelessness: false, receivedMaximumBenefit: ReceivedMaximumBenefit(cip: false), personDetails: [PersonDetail(age: 33, maritalStatus: "single", minimumEmploymentOverExtendedPeriod: true, workStatus: "ok", pregnant: false, attendingSchool: false, disabled: false), PersonDetail(age: 44, maritalStatus: "maried", minimumEmploymentOverExtendedPeriod: true, workStatus: "ok", pregnant: false, attendingSchool: false, disabled: true)], incomes: [], assets: [])
}
}

You are trying to bind the control to the whole array of PersonDetail, not an individual entry within the array.
For example, if you always wanted to use the first personDetail instance in the array:
Toggle(isOn: $eligBase.household.personDetails.first!.disabled) {
Text("Disabled")
.font(.system(size: 15))
In a real solution you'd probably want to safely unwrap whatever PersonDetail instance you want in the array, and cleanly handle the array being empty (as the forced unwrap in my example would crash if the array was empty).

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

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 } ) {

How to unite two different structures into one array and sort it? Swift

I'm building a feed, where I want to unite two different structures and sort the feed by date, here's how I've tried. It shows mistakes in View. It writes either "compiler unable to type check in reasonable time", either something else that connects to the ForEach loop.
Maybe you have an idea what can be an issue? Or, if there's other way to build and sort the feed?
EDIT: The following error: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
class FeedViewModel: ObservableObject {
#Published var feed: [FeedCommonContent] = []
// download(ed) articles
self.feed.append(Article(id: doc.document.documentID, title: title, pic: pic, time: time.dateValue(), user: user))
// download(ed) tutorials
self.feed.append(Tutorial(id: doc.document.documentID, title: title, steps: [steps], notes: notes, pic: pic, time: time.dateValue(), user: user))
// sort the feed
self.feed.sort { (p1, p2) -> Bool in
return p1.time > p2.time
}
}
protocol FeedCommonContent {
// Define anything in common between objects
var time: Date { get set }
var id: String { get set }
var feedIdentity: FeedIdentity.RawValue { get } // Article, or Tutorial; or - { get set }
}
enum FeedIdentity: String {
// case Article, Tutorial
case Article = "Article"
case Tutorial = "Tutorial"
}
struct Article: Identifiable, FeedCommonContent {
var feedIdentity = FeedIdentity.Article.rawValue
var id: String = UUID().uuidString
var title: String
var pic: String
var time: Date
var user: User
}
struct Tutorial: Identifiable, FeedCommonContent {
var feedIdentity = FeedIdentity.Tutorial.rawValue
var id: String = UUID().uuidString
var titleImage: String
var name: String
var user: User
var inventory: [String]
var steps: [String]
var notes: String
var time: Date
var warnings: String
}
struct FeedView: View {
#StateObject var feedData = FeedViewModel()
var body: some View {
ScrollView {
ForEach(feedData.feed, id: \.self.id) { item in
// also tried type checking:
// if item is Article // or, if let article = item as? Article
if item.feedIdentity == FeedIdentity.Article.rawValue {
NavigationLink(destination: ArticleView(article: item, articleData: feedData)) {
ArticleUnitView(article: item, feedData: feedData)
}
} else if item.feedIdentity == FeedIdentity.Tutorial.rawValue { // if item is Tutorial
NavigationLink(destination: TutorialView(tutorial: item, feedData: feedData)) {
TutorialUnitView(tutorial: item, feedData: feedData)
}
}
}
}
When I did an array only of one of data structures, all worked. (i.e. either var articles: [Article] = [], or var tutorials: [Tutorial] = [] etc..)
this is the (test) code I used to show how to "sort" your feed by date, and display it in a View:
EDIT, using "Joakim Danielson" enum suggestion:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
FeedView()
}
}
}
class FeedViewModel: ObservableObject {
#Published var feed: [FeedCommonContent] = []
// for testing
init() {
self.feed.append(Article(title: "article title", pic: "pic1", time: Date(), user: User()))
self.feed.append(Tutorial(titleImage: "title1", name: "tut1", user: User(), inventory: [], steps: [], notes: "note1", time: Date(), warnings: ""))
sortFeed()
}
func sortFeed() {
feed.sort { $0.time > $1.time }
}
}
protocol FeedCommonContent {
// Define anything in common between objects
var time: Date { get set }
var id: String { get set }
var feedIdentity: FeedIdentity { get } // Article, or Tutorial; or - { get set }
}
enum FeedIdentity: String {
// case Article, Tutorial
case Article
case Tutorial
}
struct User: Identifiable {
var id: String = UUID().uuidString
var name: String = "user"
}
struct Article: Identifiable, FeedCommonContent {
let feedIdentity = FeedIdentity.Article
var id: String = UUID().uuidString
var title: String
var pic: String
var time: Date
var user: User
}
struct Tutorial: Identifiable, FeedCommonContent {
let feedIdentity = FeedIdentity.Tutorial
var id: String = UUID().uuidString
var titleImage: String
var name: String
var user: User
var inventory: [String]
var steps: [String]
var notes: String
var time: Date
var warnings: String
}
struct FeedView: View {
#StateObject var feedData = FeedViewModel()
var body: some View {
ScrollView {
ForEach(feedData.feed, id: \.id) { item in
// switch item.feedIdentity {
// case FeedIdentity.Article:
// NavigationLink(destination: ArticleView(article: item, articleData: feedData)) {
// ArticleUnitView(article: item, feedData: feedData)
// }
// case FeedIdentity.Tutorial:
// NavigationLink(destination: TutorialView(tutorial: item, feedData: feedData)) {
// TutorialUnitView(tutorial: item, feedData: feedData)
// }
// }
// for testing
switch item.feedIdentity {
case FeedIdentity.Article:
NavigationLink(destination: Text(item.feedIdentity.rawValue + " " + item.id)) {
Text(item.feedIdentity.rawValue)
}
case FeedIdentity.Tutorial:
NavigationLink(destination: Text(item.feedIdentity.rawValue + " " + item.id)) {
Text(item.feedIdentity.rawValue)
}
}
}
}
}

SwiftUI MVVM how to loop through data and store them inside array

I am using MVVM in my SwiftUI project and after I request to fetch data from the API, I want to loop through the data and store some of them inside an array, however it returns error, what is the correct method to do this?
Here is my code and data struct
MenuDetailView
struct MenuDetailView: View {
#ObservedObject var viewModel = MenuDetailViewModel()
var body: some View {
ForEach(self.viewModel.variantGroup, id: \.self) { vg in
/*** I Need to loop and store vg.variantGroupName into array viewModel.variantChosen, how? ***/
self.viewModel.variantChosen.append(vg.VariantGroupName)
// This always return error:
// Type '()' cannot conform to 'View'; only struct/enum/class types can conform to protocols
VStack {
HStack {
Text(vg.variantGroupName)
Text(String(self.viewModel.arrVariantChoosen[0]))
}
VStack {
ForEach(vg.variant, id: \.self) { v in
Text(v.variantName)
}
}
}
}
}
}
}
MenuDetailViewModel.swift
class MenuDetailViewModel: ObservableObject, MenuDetailService {
var apiSession: APIService
#Published var detaildata: MenuDetailData?
#Published var variantGroup = [MenuDetailVariantGroup]()
#Published var variantChosen: Array<String> = []
var cancellables = Set<AnyCancellable>()
init(apiSession: APIService = APISession()) {
self.apiSession = apiSession
}
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
}
cancellables.insert(cancellable)
}
}
MenuDetailData.swift
struct MenuDetailData: Codable, Identifiable {
let id = UUID()
let idMenu: String
let menuName: String
let variantGroup: [MenuDetailVariantGroup]
}
MenuDetailVariantGroup.swift
struct MenuDetailVariantGroup: Codable, Identifiable, Hashable {
let id = UUID()
let variantGroupName: String
let variant: [MenuDetailVariant]
let limit: Int
}
MenuDetailVariant.swift
struct MenuDetailVariant: Codable, Identifiable, Hashable {
let id = UUID()
let variantName: String
}
Thank you all in advance
You can not add this inside the ForEach. In SwiftUI, ForEach is a view. It accepts view data, it's not the same as Array.forEach.
You need to do it inside the view model. Like this
func getMenuDetail() {
let cancellable = self.getMenuDetail()
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print("Handle error: \(error)")
case .finished:
break
}
}) { (detail) in
self.detaildata = detail.data
self.variantGroup = detail.data.variantGroup
self.variantChosen = self.variantGroup.map{$0.variantGroupName} //<--here
}
cancellables.insert(cancellable)
}
Remove this from ForEach
self.viewModel.variantChosen.append(vg.VariantGroupName)

Find if a different array has the same element and open an Alert

I know from the question it looks like something that has been already answered on this website before, but please read until the end, because I can't find the answer.
So, I have an Array that contains values of TagsModel:
import SwiftUI
import Combine
class DataManager : Equatable, Identifiable, ObservableObject {
static let shared = DataManager()
#Published var storageTags : [TagsModel] = []
typealias StorageTags = [TagsModel]
//The rest of the code
}
And the TagsModel:
import SwiftUI
import Combine
class TagsModel : Codable, Identifiable, Equatable, ObservableObject {
var id = UUID()
var tagName : String
var value : [ValueModel] = []
init(tagName: String) {
self.tagName = tagName
}
static func == (lhs: TagsModel, rhs: TagsModel) -> Bool {
return lhs.id.uuidString == rhs.id.uuidString
}
}
If you need it, the ValueModel is:
import SwiftUI
import Combine
class ValueModel : Codable, Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var name : String
var notes : String?
var expires : Date?
init(name: String, notes: String?, expires: Date?) {
self.name = name
self.notes = notes
self.expires = expires
}
}
Now what I would like to do is let the user add the elements of type ValueModel to the array Value of each TagsModel (which in English means I would like users to be able to add values inside their belonging tags). I can do all this, but I would like to add a check: if any other TagsModel contains the value that the user is trying to add, show an Alert (since every value can have only one tag). This Alert should be asking the user whether he/she wants to add that value to the selected tag and remove it from the other one, or cancel the action.
What I managed to do up to now is this:
import SwiftUI
struct SelectValuesForTagsView: View {
#ObservedObject var dm: DataManager
var tm: TagsModel
#Binding var showSheetSelectValuesForTagsView : Bool
#State var showAlertValueAlreadyInTag = false
#Environment(\.presentationMode) var mode: Binding<PresentationMode>
#GestureState private var dragOffset = CGSize.zero
#Environment(\.colorScheme) var colorScheme
var body: some View {
NavigationView {
Form {
ForEach(dm.storageValues) { valuesOfForEach in
HStack {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Image(systemName: "checkmark.circle.fill")
.frame(width: 22, height: 22)
.foregroundColor(.green)
} else {
Image(systemName: "circle")
.frame(width: 22, height: 22)
.foregroundColor(colorScheme == .dark ? .white : .black)
}
Button(action: {
if !tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
tm.value.append(valuesOfForEach)
dm.save()
} else {
guard let indexValue = tm.value.firstIndex(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) else { return }
tm.value.remove(at: indexValue)
dm.save()
}
}, label: {
if tm.value.contains(where: { $0.id.uuidString == valuesOfForEach.id.uuidString
}) {
Text(valuesOfForEach.name)
.foregroundColor(.blue)
} else {
Text(valuesOfForEach.name)
.foregroundColor(.yellow)
}
})
}
}
}
.navigationBarTitle(Text("Add Values"), displayMode: .automatic)
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing: saveButton)
}
}
var saveButton: some View {
Button(action: {
let tag : TagsModel
if dm.storageTags.contains(where: {$0.value == tm.value}) {
showAlertValueAlreadyInTag = true
//The problem is here, that the Alert ALWAYS shows up, even though there's only this Tag containing that value
}
else {
dm.save()
showSheetSelectValuesForTagsView = false
}
}, label: {
Text("Save")
.foregroundColor(.blue)
}).alert(isPresented: $showAlertValueAlreadyInTag, content: { alertValueAlreadyInTag })
}
var alertValueAlreadyInTag : Alert {
Alert(title: Text("Attention!"), message: Text("Every value can only be assigned to one tag. One or more values you selected are already into another tag. Would you like to substitute the belonging tag for these values?"), primaryButton: Alert.Button.default(Text("Yes, substitute"), action: {
dm.save()
showAlertValueAlreadyInTag = false
showSheetSelectValuesForTagsView = false
}), secondaryButton: Alert.Button.default(Text("Cancel"), action: {
showAlertValueAlreadyInTag = false
}))
}
}
How can I check if another tag inside the storageTags has the already the same value inside of its value Array?

Resources