unknownUser
unknownUser

Reputation: 13

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

Upvotes: 1

Views: 745

Answers (1)

lorem ipsum
lorem ipsum

Reputation: 29614

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

Upvotes: 1

Related Questions