1

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
koen
  • 5,383
  • 7
  • 50
  • 89

1 Answers1

1

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()
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • Thank you for your solution! Is there also a possibility to do this with classes instead of structs? – unknownUser Mar 19 '23 at 13:36
  • @unknownUser yes, with [`ObservableObject`s](https://developer.apple.com/documentation/combine/observableobject) that are wrapped accordingly https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app – lorem ipsum Mar 19 '23 at 13:38