问题
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
}
}
}
回答1:
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
来源:https://stackoverflow.com/questions/64062153/view-presented-twice-in-swiftui-on-ios-14