siiulan
siiulan

Reputation: 65

How to mutate the variable that passed from other views

I am new to the SwiftUI, I try to create an app, it has a list of goals and above the list, there is an add button to add a goal and display it on the list. Currently, I am having trouble adding the goal instance into the goals(array of goals), in the create view, I try to append a new instance of Goal to the goals that I created in another view. And it gives me an error message: Cannot use mutating member on immutable value: 'self' is immutable on the line goals.append(Goal(...)) Does anyone know how to fix it? here is my code! Thank you so much!

struct ContentView: View {
    var goals: [Goal] = []
    
    var body: some View {
        TabView{
            VStack{
                Text("You have")
                Text("0")
                Text("tasks to do")
            }.tabItem { Text("Home")}
            MyScroll(1..<100).tabItem { Text("My Goals") }
        }
    }
}

struct MyScroll: View {
    var numRange: Range<Int>
    var goals: [Goal]
    
    init (_ r:Range<Int>) {
        numRange = r
        goals = []
    }
    
    var body: some View {
        NavigationView{
            VStack{
                NavigationLink(destination: AddView(goals:self.goals)){
                    Image(systemName: "folder.badge.plus")
                }
                List(goals) { goal in
                    HStack(alignment: .center){
                        Text(goal.name)
                    }
                }
            }
        }.navigationTitle(Text("1111"))
    }
}

struct AddView: View {
    var goals:[Goal]
    @State var types = ["study", "workout", "hobby", "habbit"]
    @State private var selected = false
    @State var selection = Set<String>()
    @State var goalName: String = ""
    @State var goalType: String = ""
    @State var isLongTerm: Bool = false
    @State var progress: [Progress] = []
    
    var body: some View {
        VStack{
            Text("Create your goal")
            // type in name
            HStack{
                TextField("Name", text: $goalName)
            }.padding()
            // choose type: a selection list
            HStack{
                List(types, id: \.self, selection: $selection) {
                    Text($0)
                }
                .navigationBarItems(trailing: EditButton())
            }.padding()
            // toggle if it is a logn term goal
            HStack{
                Toggle(isOn: $selected) {
                    Text("Is your goal Long Term (no end date)")
                }.padding()
            }.padding()
            Button(action: {
                addGoal(goalName, goalType, isLongTerm, progress)
            }, label: {
                /*@START_MENU_TOKEN@*/Text("Button")/*@END_MENU_TOKEN@*/
            })
        }
    }
        
    // function that add the goal instance to the goals
    mutating func addGoal( _ t:String, _ n:String, _ iLT: Bool, _ p: [Progress]){
        let item: Goal = Goal(t,n,iLT,[])
        goals.append(item)
    }
}

The Goal is just a structure that I created for storing information:

import Foundation

// This is the structure for each goal when it is created
struct Goal: Identifiable {
    var id: UUID
    var type: String // type of goals
    var name: String // the custom name of the goal
    var isLongTerm: Bool // if goal is a long term goal (no deadline)
    var progress: [Progress] // an array of progress for each day
    
    init(_ t:String, _ n:String, _ iLT: Bool, _ p: [Progress]) {
        id = UUID()
        type = t
        name = n
        isLongTerm = iLT
        progress = p
    }
}

Upvotes: 0

Views: 734

Answers (1)

jnpdx
jnpdx

Reputation: 52645

One way to to this is by using a @Binding to hold @State in a parent view and pass it down through the view hierarchy, letting the children send data back up.

(One caveat is that sending a Binding through many views looks like it may have unexpected results in the current version of SwiftUI, but one or two levels seems to be fine. Another option is using an ObservableObject with a @Published property that gets passed between views)

Note how the ContentView owns the [Goal] and then the subsequent child views get it as a @Binding -- the $ symbol is used to pass that Binding through the parameters:

struct Goal: Identifiable {
    var id: UUID
    var type: String // type of goals
    var name: String // the custom name of the goal
    var isLongTerm: Bool // if goal is a long term goal (no deadline)
    var progress: [Progress] // an array of progress for each day
    
    init(_ t:String, _ n:String, _ iLT: Bool, _ p: [Progress]) {
            id = UUID()
            type = t
            name = n
            isLongTerm = iLT
            progress = p
        }
}

struct ContentView: View {
    @State var goals: [Goal] = []

    var body: some View {
        TabView{
            VStack{
                Text("You have")
                Text("\(goals.count)")
                Text("tasks to do")
            }.tabItem { Text("Home")}
            MyScroll(numRange: 1..<100, goals: $goals).tabItem { Text("My Goals") }
        }
    }
}

struct MyScroll: View {
    var numRange: Range<Int>
    @Binding var goals: [Goal]
    
    var body: some View {
        NavigationView{
            VStack{
                NavigationLink(destination: AddView(goals:$goals)){
                    Image(systemName: "folder.badge.plus")
                }
                List(goals) { goal in
                    HStack(alignment: .center){
                        Text(goal.name)
                    }
                }
            }
        }.navigationTitle(Text("1111"))
    }
}

struct AddView: View {
    @Binding var goals:[Goal]
    @State var types = ["study", "workout", "hobby", "habbit"]
    @State private var selected = false
    @State var selection = Set<String>()
    @State var goalName: String = ""
    @State var goalType: String = ""
    @State var isLongTerm: Bool = false
    @State var progress: [Progress] = []
    
    var body: some View {
        VStack{
            Text("Create your goal")
            // type in name
            HStack{
                TextField("Name", text: $goalName)
            }.padding()
            // choose type: a selection list
            HStack{
                List(types, id: \.self, selection: $selection) {
                    Text($0)
                }
                .navigationBarItems(trailing: EditButton())
            }.padding()
            // toggle if it is a logn term goal
            HStack{
                Toggle(isOn: $selected) {
                    Text("Is your goal Long Term (no end date)")
                }.padding()
            }.padding()
            Button(action: {
                addGoal(goalType, goalName, isLongTerm, progress)
            }, label: {
                /*@START_MENU_TOKEN@*/Text("Button")/*@END_MENU_TOKEN@*/
            })
        }
    }
        
    // function that add the goal instance to the goals
    func addGoal( _ t:String, _ n:String, _ iLT: Bool, _ p: [Progress]){
        let item: Goal = Goal(t,n,iLT,[])
        goals.append(item)
    }
}

Your addGoal function no longer has to be mutating, since it's not actually mutating its own state any more (which doesn't work in SwiftUI anyway).

As a side note, I'd be cautious about writing your initializers and functions like you're doing with the _ unnamed parameters -- I found one in your original code where you meant to be passing the name of the goal but instead were passing the type for that parameter, and because all of the parameters were/are unnamed, there's no warning about it.

Upvotes: 1

Related Questions