1

I am trying to use Combine to route SwiftUI views, and it sort of works, but has some unwanted behaviour that I hope someone can help me with. I made an example project which only contains two files, ViewRouter.swift and ViewController.swift that just contains a @IBSegueAction to return a UIHostingController:

The class ViewRouter is a ViewModel for the SwiftUI Views that contains a @ViewBuilder property. When the SwiftUI views calls the didFinish() function, the step property is set to the next View.

class ViewRouter: ObservableObject {
    enum Step { case test1, test2, test3, test4 }
    @Published var step: Step = .test1
    @ViewBuilder var nextStepView: some View {
        switch step {
        case .test1:
            Test1(router: self)
        case .test2:
            Test2(router: self)
        case .test3:
            Test3(router: self)
        case .test4:
            Test4(router: self)
        }
    }
    
    func didFinish() {
        switch step {
        case .test1:
            step = .test2
        case .test2:
            step = .test3
        case .test3:
            step = .test4
        case .test4:
            break
        }
    }
}

This works perfect for Test1, when tapping the Next link, the Test1 view is animated to the left, to reveal Test2. When tapping Next in Test2 however, the Test2 is animated to the left to reveal Test3, but then immediately it animates back to the right to reveal the same Test3 view. Exactly the same happens when tapping Next in Test3.

And tapping the back button always ends up in Test1 view.

All four views are the same, apart from the text in Text:

struct Test1: View {
    @ObservedObject var router: ViewRouter
    @State var nextView = false

    var body: some View {
        VStack {
            NavigationLink(destination: router.nextStepView, isActive: $nextView) {
                EmptyView()
            }

            Text("Test1")
            
            Button(action: {
                nextView = true
                router.didFinish()
            }, label: {
                Text("Next")
            })
        }
    }
}
Ivan C Myrvold
  • 680
  • 7
  • 23

1 Answers1

0

I have made some small changes to the ViewRouter class to make this work. I came to the conclusion that all SwiftUI views need their own ViewRouter instance. Using the same shared instance of ViewRouter for all views results in the unwanted behaviour. I have introduced a nextStep optional property that is set to the current step when a view calls the didFinish function, to make sure that the router returns correct nextStepView if called a second or third etc times.

I have pushed an update to the GitHub repo for the example Xcode project.

The modified class now looks like this:

class ViewRouter: ObservableObject {
    enum Step { case test1, test2, test3, test4 }
    @Published var step: Step
    private var nextStep: Step?
    @ViewBuilder var nextStepView: some View {
        switch step {
        case .test1:
            Test1(router: self)
        case .test2:
            Test2(router: ViewRouter(step: .test2))
        case .test3:
            Test3(router: ViewRouter(step: .test3))
        case .test4:
            Test4(router: ViewRouter(step: .test4))
        }
    }
    
    init(step: Step) {
        self.step = step
    }
    
    func didFinish() {
        switch step {
        case .test1:
            if let next = nextStep {
                step = next
            } else {
                step = .test2
                nextStep = .test2
            }
        case .test2:
            if let next = nextStep {
                step = next
            } else {
                step = .test3
                nextStep = .test3
            }
        case .test3:
            if let next = nextStep {
                step = next
            } else {
                step = .test4
                nextStep = .test4
            }
        case .test4:
            break
        }
    }
    
}
Ivan C Myrvold
  • 680
  • 7
  • 23