Reputation: 65
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
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