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.35k stars 1.44k forks source link

NavigationLinkStore misbehaves with multi-level navigation #2296

Closed sadikcoban closed 1 year ago

sadikcoban commented 1 year ago

Description

I am working on a SwiftUI project whose minimum iOS version is iOS 14 and I am using NavigationLinkStore for navigation purposes. However, when I do multi level navigation from a root screen, the second navigation pops itself automatically. Here is the code example:

https://gist.github.com/sadikcoban/56566b0c2970184a151f7d4edcdb5aa7

import ComposableArchitecture
import SwiftUI

struct RootFeature: Reducer {
  struct State: Equatable {
    @PresentationState var child: ChildFeature.State?
  }
  enum Action: Equatable {
    case child(PresentationAction<ChildFeature.Action>)
    case itemTapped
  }

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .child:
        return .none
      case .itemTapped:
        state.child = .init()
        return.none
      }
    }
    .ifLet(\.$child, action: /Action.child){
      ChildFeature()
    }
  }
}

struct RootView: View {
  let store: StoreOf<RootFeature>
  var body: some View {
    WithViewStore(self.store, observe: { $0 }) { viewStore in
      VStack(alignment: .center) {
        NavigationLinkStore(self.store.scope(state: \.$child, action: RootFeature.Action.child)) {
          viewStore.send(.itemTapped)
        } destination: { childStore in
          ChildView(store: childStore)
        } label: {
          Text("Tap to see first level child")
        }

      }
      .padding()
    }
  }
}

struct ChildFeature: Reducer {
  struct State: Equatable {
    @PresentationState var secondLevelChildState: SecondLevelchildFeature.State?
  }
  enum Action: Equatable {
    case secondLevelChild(PresentationAction<SecondLevelchildFeature.Action>)
    case itemTapped
  }

  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .secondLevelChild:
        return .none
      case .itemTapped:
        state.secondLevelChildState = .init()
        return .none
      }
    }
    .ifLet(\.$secondLevelChildState, action: /Action.secondLevelChild) {
      SecondLevelchildFeature()
    }
  }
}

struct ChildView: View {
  let store: StoreOf<ChildFeature>
  var body: some View {
    WithViewStore(self.store, observe: { $0 }) { viewStore in
      NavigationLinkStore(self.store.scope(state: \.$secondLevelChildState, action: ChildFeature.Action.secondLevelChild), onTap: {
        viewStore.send(.itemTapped)
      }, destination: { secondLevelChildStore in
        SecondLevelChildView(store: secondLevelChildStore)
      }, label: {
        Text("Tap to see second level child feature")
      })
      .navigationTitle("First Level Child")

    }
  }
}

struct SecondLevelchildFeature: Reducer {
  struct State: Equatable { }
  enum Action: Equatable { }
  func reduce(into state: inout State, action: Action) -> Effect<Action> {

  }
}

struct SecondLevelChildView: View {
  let store: StoreOf<SecondLevelchildFeature>
  var body: some View {
    Text("End")
      .navigationTitle("Second Level Child")
  }
}

When the "Tap to see second level child feature" is tapped from ChildView, the SecondLevelChildView appears and disappears automatically, because of the change of ChildFeature state i think. How could I solve this problem?

Checklist

Expected behavior

No response

Actual behavior

No response

Steps to reproduce

No response

The Composable Architecture version information

prerelease/1.0

Destination operating system

iOS 14

Xcode version information

Xcode 14.3

Swift Compiler version information

swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
stephencelis commented 1 year ago

Unfortunately I believe this is a vanilla SwiftUI bug in navigation links that folks have had to work around since iOS 13. In particular, deep linking multiple layers requires staging the deep linking over time using async delays per layer, and you must also use a .stack navigation style for the binding to write correctly and avoid those pops.

I’m going to convert this to a discussion since it doesn’t seem to be a library bug.