1

I'm trying to show different views (with the same base) depending on an enum value but depending on how to "inspect" the enum the behavior changes. This is the code (I'm using a "useSwitch" variable to be able to alternate between both behaviors)

import SwiftUI

enum ViewType: CaseIterable {
    case type1
    case type2
    
    var text: String {
        switch self {
        case .type1:
            return "Type 1"
        case .type2:
            return "Type 2"
        }
    }
}

final class BaseVM: ObservableObject {
    let type: ViewType
    
    @Published var requestingData = false
    
    init(type: ViewType) {
        self.type = type
    }
    
    @MainActor func getData() async {
        requestingData = true
        
        try! await Task.sleep(nanoseconds: 1_000_000_000)
        
        requestingData = false
    }
}

struct BaseView: View {
    @StateObject var  vm: BaseVM
    
    var body: some View {
        Group {
            if vm.requestingData {
                ProgressView("Getting data for \(vm.type.text)")
            } else {
                Text("\(vm.type.text)")
            }
        }
        .onAppear {
            Task {
                await vm.getData()
            }
        }
    }
}

struct TestZStackView: View {
    private let types = ViewType.allCases
    @State var currentType: ViewType = .type1
    
    private var useSwitch = true
    
    var body: some View {
        VStack {
            if useSwitch {
                Group {
                    switch currentType {
                    case .type1:
                        BaseView(vm: BaseVM(type: currentType))
                    case .type2:
                        BaseView(vm: BaseVM(type: currentType))
                    }
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else {
                BaseView(vm: BaseVM(type: currentType))
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
            Spacer()
            Picker("", selection: $currentType) {
                ForEach(types, id: \.self) {
                    Text($0.text)
                }
            }
            .pickerStyle(.segmented)
            .padding(.top, 20)
        }
        .padding()
    }
}

struct TestZStackView_Previews: PreviewProvider {
    static var previews: some View {
        TestZStackView()
    }
}

I don't understand why using a switch (useSwitch == true) refreshes the view but using the constructor passing the enum as parameter (useSwitch = false) doesn't refresh the view... It can't detect that the currentType has changed if used as parameter instead of checking it using a switch?

1 Answer 1

1

This is all about identity. If you need more information I would recommend watching WWDC Demystify SwiftUI.

If your @State var triggers when changing the Picker the TestZStackView rebuilds itself. When hitting the if/else clause there are two possibilities:

  • private var useSwitch = true. So it checks the currentType and builds the appropriate BaseView. These differ from each other in their id, so a new View gets build and you get what you expect.

  • the second case is less intuitive. I really recommend watching that WWDC session mentioned earlier. If private var useSwitch = false there is no switch statement and SwiftUI tries to find out if your BaseView has changed and needs to rerender. For SwiftUI your BaseView hasn´t changed even if you provided a new BaseVM. It does notify only changes on depending properties or structs (or @Published in ObservableObject).

In your case @StateObject var vm: BaseVM is the culprit. But removing @StateObject will create the new View but you loose the ObservableObject functionality.

Solution here would be to restructure your code. Use only one BaseVm instance that holds your state and pass that on into the environment.

E.g.:

final class BaseVM: ObservableObject {
    // create a published var here
    @Published var type: ViewType = .type1
    @Published var requestingData = false

    @MainActor func getData() async {
        requestingData = true
        
        try! await Task.sleep(nanoseconds: 1_000_000_000)
        
        requestingData = false
    }
}

struct BaseView: View {
    // receive the viewmodel from the environment
    @EnvironmentObject private var vm: BaseVM
    
    var body: some View {
        Group {
            if vm.requestingData {
                ProgressView("Getting data for \(vm.type.text)")
            } else {
                Text("\(vm.type.text)")
            }
        }
        // change this also because the view will not apear multiple times it
        // will just change depending on the type value
        .onChange(of: vm.type) { newValue in
            Task{
                await vm.getData()
            }
        }.onAppear{
            Task{
                await vm.getData()
            }
        }
    }
}

struct TestZStackView: View {
    private let types = ViewType.allCases
    @StateObject private var viewmodel = BaseVM()
    
    private var useSwitch = false
    
    var body: some View {
        VStack {
            if useSwitch {
                //this group doesn´t really make sense but just for demonstration
                Group {
                    switch viewmodel.type {
                    case .type1:
                        BaseView()
                            .environmentObject(viewmodel)
                    case .type2:
                        BaseView()
                            .environmentObject(viewmodel)
                    }
                }
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            } else {
                BaseView()
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .environmentObject(viewmodel)
            }
            Spacer()
            Picker("", selection: $viewmodel.type) {
                ForEach(types, id: \.self) {
                    Text($0.text)
                }
            }
            .pickerStyle(.segmented)
            .padding(.top, 20)
        }
        .padding()
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Great answer, very useful. Thank you. I've been watching the WWDC video (specifically the identity part as you mentioned) and I've checked that adding ".id(currentType)" modifier to the "BaseView(vm: BaseVM(type: currentType))" view of my code fixes it (maybe not the optimum solution but helped me to understand the identity concept)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.