pointfreeco / swift-composable-architecture

A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
https://www.pointfree.co/collections/composable-architecture
MIT License
12.22k stars 1.42k forks source link

NavigationStack inside sheet appends first destination without transition on iOS 16 #3231

Closed KirillZholnerovich closed 2 months ago

KirillZholnerovich commented 2 months ago

Description

It works well on iOS 17, but does not work correctly on iOS16

I've created an example project to reproduce this bug

Screen A ` @Reducer public struct ScreenAFeature { @ObservableState public struct State: Equatable { @Presents public var screenB: ScreenBFeature.State? = nil public init() {} }

public enum Action {
    case screenB(PresentationAction<ScreenBFeature.Action>)
    case showButtonTapped
}

public init() {}

public var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .screenB:
            return .none

        case .showButtonTapped:
            state.screenB = .init()
            return .none
        }
    }
    .ifLet(\.$screenB, action: \.screenB) {
        ScreenBFeature()
    }
}

}

public struct ScreenAView: View { @Perception.Bindable var store: StoreOf

public init(store: StoreOf<ScreenAFeature>) {
    self.store = store
}

public var body: some View {
    WithPerceptionTracking {
        VStack {
            Text("Screen A")
            Button("Show B") {
                self.store.send(.showButtonTapped)
            }
        }
        .sheet(
            item: self.$store.scope(
                state: \.screenB,
                action: \.screenB
            )
        ) { store in
            ScreenBView(store: store)
        }
    }
}

} Screen B @Reducer public struct ScreenBFeature { @ObservableState public struct State: Equatable { public var path: StackState = StackState()

    public init() {}
}

public enum Action {
    case path(StackAction<Path.State, Path.Action>)
    case toCButtonTapped
}

@Reducer(state: .equatable)
public enum Path {
    case screenC(ScreenCFeature)
}

public init() {}

public var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .path:
            return .none

        case .toCButtonTapped:
            state.path.append(.screenC(.init()))
            return .none
        }
    }
    .forEach(\.path, action: \.path)
}

}

public struct ScreenBView: View { @Perception.Bindable public var store: StoreOf

public init(store: StoreOf<ScreenBFeature>) {
    self.store = store
}

public var body: some View {
    WithPerceptionTracking {
        NavigationStack(path: self.$store.scope(state: \.path, action: \.path)) {
            VStack {
                Text("Screen B")

                NavigationLink(state: ScreenBFeature.Path.State.screenC(.init())) {
                    Text("To Screen C")
                }
            }
        } destination: { store in
            WithPerceptionTracking {
                switch store.case {
                case .screenC(let store):
                    ScreenCView(store: store)
                }
            }
        }
    }
}

} `

Screen C ` @Reducer public struct ScreenCFeature { @ObservableState public struct State: Equatable { public init() {} }

public enum Action {}

public init() {}

public var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        default: return .none
        }
    }
}

}

public struct ScreenCView: View { public let store: StoreOf

public init(store: StoreOf<ScreenCFeature>) {
    self.store = store
}

public var body: some View {
    WithPerceptionTracking {
        Text("Screen C")
    }
}

} `

Behaviour on iOS 16

https://github.com/pointfreeco/swift-composable-architecture/assets/14135206/52e151c1-515c-422e-a4dc-7ad171f32c75

Behaviour on iOS 17

https://github.com/pointfreeco/swift-composable-architecture/assets/14135206/30ec6a8d-44c4-4f3a-85eb-c48651cc58c3

Checklist

Expected behavior

Appearing works with transition.

Actual behavior

Appearing works without transition.

Steps to reproduce

  1. Tap "Show B" button on ScreenAView.
  2. Tap "To Screen C" button on ScreenBView.

The Composable Architecture version information

1.11.2

Destination operating system

iOS 16

Xcode version information

15.4

Swift Compiler version information

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0
mbrandonw commented 2 months ago

Hi @KirillZholnerovich, this is a bug in vanilla SwiftUI and there’s nothing we can do to fix it in the library.

I am going to convert this to a discussion since it’s not an issue with the library. Feel free to continue the conversation over there.