swiftuistateobservedobjectenvironmentobject

Saving state across views


I would like to implement a multi view questionnaire for the user to enter details about themselves. Therefore, I need a persistent storage about what the user picks preferably in an object that I can pass through different views which each mutate a detail about the user object.

The process:

  1. Choose name - button with selected name turns darkblue
  2. Choose favourite sports
  3. Go back to name and choose different name (expecting selected name already being darkbleue still)

Video of issue:

enter image description here

My approach: I have already tried using stateobjects in order to pass the state down like in React but that didn't work.

This is the code as of now:


struct Names: View {
    private let names = ["xyz", "zfx", "abc", "def", "ghij", "klm"]
    var body: some View {
        NavigationView{
            VStack{
                Text("Choose your name")
                    .font(.largeTitle)
                    .foregroundColor(Color("LightBlue"))
                    .padding(.bottom, 40)
                    .padding(.top, 40)
                VStack(spacing: 20){
                    ForEach(names, id: \.self){ name in
                        CustomButton(name: name)
                    }
                }
            }
        }
    }
}

struct CustomButton: View {
    @State private var clicked = false
    let name:String
    
    init(name:String){
        self.name = name
    }
    
    var body: some View {
        Button{
            self.clicked.toggle()
        }label: {
            NavigationLink(destination: ChooseSports()){
                Text(self.name)
                    .font(.subheadline)
                    .frame(width: 250, height: 60)
                    .background(self.clicked ? Color("DarkBlue") : .white)
                    .foregroundColor(self.clicked ? .white : Color("DarkBlue"))
                    .cornerRadius(50)
                    .overlay(
                        RoundedRectangle(cornerRadius: 50)
                            .stroke(Color("DarkBlue"), lineWidth: 2)
                    )
            }
        }
    }
}


struct CustomButton2: View {
    @State private var clicked = false
    let name:String
    
    init(name:String){
        self.name = name
    }
    
    var body: some View {
        Button{
            self.clicked.toggle()
        }label: {
            Text(self.name)
                .font(.subheadline)
                .frame(width: 250, height: 60)
                .background(self.clicked ? Color("DarkBlue") : .white)
                .foregroundColor(self.clicked ? .white : Color("DarkBlue"))
                .cornerRadius(50)
                .overlay(
                    RoundedRectangle(cornerRadius: 50)
                        .stroke(Color("DarkBlue"), lineWidth: 2)
                )
        }
    }
}


struct ChooseSports: View {
    private let sports = ["tennis", "football", "golf", "basketball", "squash", "badminton", "swimming", "skiing"]
    
    var body: some View {
        ScrollView{
            VStack{
                Text("Choose your favourite sports")
                    .font(.largeTitle)
                    .foregroundColor(Color("LightBlue"))
                    .padding(.bottom, 40)
                    .padding(.top, 40)
                VStack(spacing: 20){
                    ForEach(sports, id: \.self){ sport in
                        CustomButton2(name:sport)
                    }
                }
            }
        }
    }
}

Expected process:

  1. Choose name - button turns darkblue and view switches to favourite sports view
  2. Once navigated to the sports view, navigate back to name view
  3. Name view should have name previously selected name still darkblue (this does not happen)
  4. Data from name view should be retrieved in the next view

Solution

  • State is a source of truth that lives as long as the View lives. When you go to the next CustomButton the old one gets redrawn/recreated.

    What you need some kind of continuity.

    You can achieve that by putting everything that goes together into a struct/Model

    struct NameModel{
        var name: String
        var clicked: Bool = false
    }
    

    Then then the value of clicked will be able to survive when Names redraws the body

    struct NamesView: View {
        
        @State private var names:[NameModel] = [.init(name: "xyz"), .init(name: "zfx"), .init(name: "abc"), .init(name: "def"), .init(name: "ghij"), .init(name: "klm")]
        
        var body: some View {
            NavigationView{
                VStack{
                    Text("Choose your name")
                        .font(.largeTitle)
                        .foregroundColor(Color.blue)
                        .padding(.bottom, 40)
                        .padding(.top, 40)
                    VStack(spacing: 20){
                        ForEach($names, id: \.name){ $model in
                            CustomButton(model: $model)
                        }
                    }
                }
            }
        }
    }
    
    struct CustomButton: View {
        @Binding var model : NameModel
        
        var body: some View {
            NavigationLink(destination: ChooseSports()
                .onAppear(){
                    model.clicked = true
                }){
                    Text(model.name)
                        .font(.subheadline)
                        .frame(width: 250, height: 60)
                        .background(model.clicked ? Color.blue : .white)
                        .foregroundColor(model.clicked ? .white : Color.blue)
                        .cornerRadius(50)
                        .overlay(
                            RoundedRectangle(cornerRadius: 50)
                                .stroke(Color.blue, lineWidth: 2)
                        )
                }
        }
    }
    

    But since you have sports that are specific to a name you may watt to adjust to something like the code below.

    import SwiftUI
    
    struct NameModel{
        var name: String
        var favoriteTeams: [String]? //Will only be nil of sports has not been visited
    }
    
    
    struct NamesView: View {
        
        @State private var names:[NameModel] = [.init(name: "xyz"), .init(name: "zfx"), .init(name: "abc"), .init(name: "def"), .init(name: "ghij"), .init(name: "klm")]
        
        var body: some View {
            NavigationView{
                VStack{
                    Text("Choose your name")
                        .font(.largeTitle)
                        .foregroundColor(Color.blue)
                        .padding(.bottom, 40)
                        .padding(.top, 40)
                    VStack(spacing: 20){
                        ForEach($names, id: \.name){ $model in
                            CustomButton(model: $model)
                        }
                    }
                }
            }
        }
    }
    
    struct CustomButton: View {
        @Binding var model : NameModel
        
        var body: some View {
            NavigationLink(destination: ChooseSports(model: $model)
                .onAppear(){
                    if model.favoriteTeams == nil{ //Change to empty to symbolize that the user has not selected any teams
                        model.favoriteTeams = []
                    }
                }){
                    Text(model.name)
                        .font(.subheadline)
                        .frame(width: 250, height: 60)
                        .background(model.favoriteTeams != nil ? Color.blue : .white)
                        .foregroundColor(model.favoriteTeams != nil ? .white : Color.blue)
                        .cornerRadius(50)
                        .overlay(
                            RoundedRectangle(cornerRadius: 50)
                                .stroke(Color.blue, lineWidth: 2)
                        )
                }
        }
    }
    
    
    struct CustomButton2: View {
        @Binding var model : NameModel
        let name: String
        var body: some View {
            Button{
                //Select or deselect based on contents of array
                if let idx = model.favoriteTeams?.firstIndex(where: { str in
                    str == name
                }){
                    model.favoriteTeams?.remove(at: idx)
                }else{
                    if model.favoriteTeams == nil{
                        model.favoriteTeams = []
                    }
                    model.favoriteTeams?.append(name)
                }
            }label: {
                Text(self.name)
                    .font(.subheadline)
                    .frame(width: 250, height: 60)
                    .background(model.favoriteTeams?.contains(name) ?? false ? Color.blue : .white)
                    .foregroundColor(model.favoriteTeams?.contains(name) ?? false ? .white : Color.blue)
                    .cornerRadius(50)
                    .overlay(
                        RoundedRectangle(cornerRadius: 50)
                            .stroke(Color.blue, lineWidth: 2)
                    )
            }
        }
    }
    
    
    struct ChooseSports: View {
        @Binding var model : NameModel
        private let sports = ["tennis", "football", "golf", "basketball", "squash", "badminton", "swimming", "skiing"]
        
        var body: some View {
            ScrollView{
                VStack{
                    Text("Choose your favourite sports")
                        .font(.largeTitle)
                        .foregroundColor(Color.blue)
                        .padding(.bottom, 40)
                        .padding(.top, 40)
                    VStack(spacing: 20){
                        ForEach(sports, id: \.self){ sport in
                            CustomButton2(model: $model, name:sport)
                        }
                    }
                }
            }
        }
    }
    
    
    
    struct NamesView_Previews: PreviewProvider {
        static var previews: some View {
            NamesView()
        }
    }