0

I'm working on a SwiftUI project, and I've run into a couple of issues I can't seem to solve. Here's a brief overview of my code:

protocol FlowViewModel: ObservableObject {
    var myFakeData: Int { get }
    var shouldNavigate: Bool { get set }
    func getNextViewModel() -> FlowViewModel
}

final class Step2VM: FlowViewModel {
    @Published var shouldNavigate: Bool = false
    
    var myFakeData: Int {
        return 1
    }
    
    func getNextViewModel() -> FlowViewModel {
        return Step3VM()
    }
}

final class Step1VM: FlowViewModel {
    @Published var shouldNavigate: Bool = false

    var myFakeData: Int {
        return 0
    }
    
    func getNextViewModel() -> FlowViewModel {
        return Step2VM()
    }
}

struct MainView<ViewModel>: View where ViewModel: FlowViewModel {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        NavigationView {
            StepView(viewModel: viewModel)
        }
    }
}

struct StepView<ViewModel>: View where ViewModel: FlowViewModel {
    @ObservedObject var viewModel: ViewModel
    
    var body: some View {
        VStack {
            NavigationLink(
                destination: StepView(viewModel: viewModel.getNextViewModel()),
                isActive: $viewModel.shouldNavigate,
                label: { EmptyView() }
            )

            Text("\(viewModel.myFakeData)")
        }
    }
}

I'm having these errors:

Inside my protocol:

Use of protocol 'FlowViewModel' as a type must be written 'any FlowViewModel'

As you can see I am attempting to reuse StepView for all the views and just passing in a new view model each time to populate the current data.

Is there a better approach to this?

DrewG23
  • 427
  • 3
  • 11

1 Answers1

1

Starting off with your last question is there a better approach, you could have each ViewModel return the "Next View" with the model in it, by initialising the NextView model and passing in to a ViewBuilder function and use that as the navigation link. eg.

@ViewBuilder func nextView() -> some View {
    StepView(viewModel: step2VM)
}

This could create a lot of other issues with View Types in the protocol though. Have used this with varying degrees of success with generics in the past.

Working with your original code and getting it to work a rather long answer here. To avoid having to use "any FlowViewModel" , which can get complicated / frustrating with unknown types , you can use an associated type in your protocol to get access to the actual type of the next model. Trying to use "any FlowViewModel" also seems to run in to problems in the view as the any can not conform to ObservableObject, it needs a concrete type conformance.

protocol FlowViewModel: ObservableObject {
    associatedtype NextModel: FlowViewModel
    var myFakeData: Int { get }
    var shouldNavigate: Bool { get set }
    func getNextViewModel() -> NextModel?
}

This does cause another slight issue. At some stage there will no more view model to return so in my code example I have made the alias NextModel optional and the model at the end of the chain will return nil. This needs to be unwrapped in the view. I have added this to the view so it only adds the link if there is a nextViewModel.

There is some mildly complex generics in the view where you also need to link the generic types of the current model and the next model. I have done this here by having a var that is set at initialisation. This is purely to confirm/set in the displayed view what the concrete type of the next view model is in the chain, so it can be called in the navigation link. Generic types need to become actual/concrete types when generic items are called, and these need to be determined ahead of time.

struct StepView<T: FlowViewModel >: View {
    @ObservedObject var viewModel: T
    var nextViewModel: T.NextModel?
    
    var body: some View {
        VStack {
            if let nextModel = nextViewModel {
                NavigationLink(
                    destination: StepView<T.NextModel>(viewModel: nextModel, nextViewModel: nextModel.getNextViewModel()),
                    isActive: $viewModel.shouldNavigate,
                    label: { EmptyView() }
                )
            }

            Text("\(viewModel.myFakeData)")
        }
    }
}

And then pass the nextModel in from the MainView.

var body: some View {
        NavigationView {
            StepView(viewModel: viewModel , nextViewModel: viewModel.getNextViewModel())
        }
    }

Finally you need to set concrete types as the return of each of the FlowView Model, with the end of the chain returning nil.

final class Step1VM: FlowViewModel {
    @Published var shouldNavigate: Bool = false

    var myFakeData: Int { return 0 }
    
    func getNextViewModel() -> Step2VM? {
        return Step2VM()
    }
}

final class Step2VM: FlowViewModel {
    @Published var shouldNavigate: Bool = false
    
    var myFakeData: Int { return 1 }
    
    func getNextViewModel() -> Step2VM? {
        return nil
    }
}

This all compiles ok for me.

Hongtron
  • 217
  • 6