Issue #824

Use a custom NavigationLink with EmptyView as the background, this failable initializer accepts Binding of optional value. This works well as the destination are made lazily.

extension NavigationLink where Label == EmptyView {
    init?<Value>(
        _ binding: Binding<Value?>,
        @ViewBuilder destination: (Value) -> Destination
    ) {
        guard let value = binding.wrappedValue else {
            return nil
        }

        let isActive = Binding(
            get: { true },
            set: { newValue in if !newValue { binding.wrappedValue = nil } }
        )

        self.init(destination: destination(value), isActive: isActive, label: EmptyView.init)
    }
}

extension View {
    @ViewBuilder
    func navigate<Value, Destination: View>(
        using binding: Binding<Value?>,
        @ViewBuilder destination: (Value) -> Destination
    ) -> some View {
        background(NavigationLink(binding, destination: destination))
    }
}

Then in our OnboardView, we can check the destination and make according View

final class OnboardViewModel: ObservableObject {
    enum Destination {
        case email
        case confirm
        case avatar
    }

    @Published var destination: Destination? = .email

    func goTo(destination: Destination) {
        self.destination = destination
    }
}

struct OnboardView: View {
    @StateObject private var viewModel = OnboardViewModel()

    var body: some View {
        NavigationView {
            OnboardEmailView(viewModel: viewModel)
                .navigate(using: $viewModel.destination, destination: makeView)
        }
    }

    @ViewBuilder
    private func makeView(destination: OnboardViewModel.Destination) -> some View {
        switch destination {
        case .email:
            OnboardEmailView(viewModel: viewModel)
        case .confirm:
            OnboardConfirmView(viewModel: viewModel)
        case .avatar:
            OnboardAvatarView(viewModel: viewModel)
        }
    }
}

Reference