简体   繁体   中英

View presented twice in SwiftUI on iOS 14

I need to dynamically present views based on a model so the decision of which view to present and the creation of the views needs to be external to the SwiftUI views. This is working well on iOS 13 but on iOS 14 the first view presents again instead of the second view, although the second view does get initialised as expected the first view is pushed onto the navigation stack a second time.

It seems like a iOS 14 bug but perhaps I'm doing something wrong so asking here before filing a bug report with Apple.

This is simplified version of what I'm trying to do, this code works in iOS 13 but not iOS 14.


/// Defines the interface for a Screen that is presented dynamically using the Coordinator & ScreenProvider
protocol Screen {
    
    var coordinator: Coordinator { get }
    var pushNext: Bool { get set }
    var sheetNext: Bool { get set }
    
    func screenIsComplete() throws
}


/// Creates Screens on request
struct ScreenProvider {

    /// Creates a Screen when receiving a `screenId` & wraps it in an AnyView
    func screen(for screenId: String, coordinator: Coordinator) -> AnyView {
        
        print("Getting screen(for screenId: \(screenId))")
        
        switch screenId {
        case "View1":
            return AnyView(View1(coordinator: coordinator))
        case "View2":
            return AnyView(View2(coordinator: coordinator))
        case "View3":
            return AnyView(View3(coordinator: coordinator))
        case "View4":
            return AnyView(View4(coordinator: coordinator))
        default:
            fatalError()
        }
    }
}

/// What should the current Screen do after it has completed
enum ScreenPresentationAction {
    case pushNext
    case sheetNext
    case doNothing
}

/// Determines what action should be taken after a screen has completed and serves the next Screen to the current Screen 
final class Coordinator {
    
    let screenProvider: ScreenProvider
    private var screenIndex: Int = 1
    
    init(screenProvider: ScreenProvider) {
        self.screenProvider = screenProvider
    }
    
    /// What should the current Screen do after it has completed
    func nextActionAfterScreenCompletion() throws -> ScreenPresentationAction {
        if screenIndex < 4 {
            screenIndex += 1
        }
        if screenIndex == 4 {
            return .sheetNext
        }
        return .pushNext
    }
    
    /// Get the next screen to be presented
    func nextScreen() -> some View {
    
        let screenId = "View\(screenIndex)"
        print("Getting nextScreen() for index \(screenIndex)")
        return screenProvider.screen(for: screenId, coordinator: self)
    }
}

There are four almost identical Views:


struct View1: View, Screen {
    
    //------------------------------------
    // MARK: Screen
    //------------------------------------
    var coordinator: Coordinator
    @State var pushNext: Bool = false
    @State var sheetNext: Bool = false

    //------------------------------------
    // MARK: Properties
    //------------------------------------
    
    // # Body
    var body: some View {
        
        VStack {
            
            Spacer()
            Text("View 1")
            Spacer()
            Button(action: {
                 try! self.screenIsComplete()
            }) {
                Text("Next")
            }
            Spacer()
            NavigationLink(destination: self.coordinator.nextScreen(), isActive: self.$pushNext) {
                EmptyView()
            }
        }
        .sheet(isPresented: self.$sheetNext) {
            self.coordinator.nextScreen()
        }
    }
}

extension View1 {
    
    func screenIsComplete() throws {
        
        let action = try coordinator.nextActionAfterScreenCompletion()
        switch action {
        case .pushNext:
            self.pushNext = true
        case .sheetNext:
            self.sheetNext = true
        case .doNothing:
            break
        }
    }
}

You need to defer building navigation link destination view (otherwise it is created right on first refresh and you see your effect).

Tested with Xcode 12 / iOS 14

NavigationLink(destination: DeferView { self.coordinator.nextScreen() }, // << here !!
  isActive: self.$pushNext) {
    EmptyView()
}

The DeferView is taken from other my solution

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM