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.24k stars 1.42k forks source link

Crash in 1.5.2 - Unwrapping in IfLetStore, line 88 #2632

Closed jaanussiim closed 9 months ago

jaanussiim commented 9 months ago

Description

Noticed a crash when upgrading from 1.5.1 to 1.5.2

Checklist

Expected behavior

No response

Actual behavior

No response

Steps to reproduce

import ComposableArchitecture
import SwiftUI

struct ContentView: View {
    var body: some View {
        FeatureView(
            store: Store(
                initialState: Feature.State(),
                reducer: Feature.init
            )
        )
    }
}

#Preview {
    ContentView()
}

public struct FeatureView: View {
    let store: StoreOf<Feature>

    public var body: some View {
        ZStack {
            Button(action: { store.send(.tappedShowOverlay) }) {
                Text("Show overlay")
            }
            IfLetStore(
                store.scope(state: \.$destination.overlay, action: \.destination.overlay),
                then: OverlayView.init(store:)
            )
        }
    }
}

@Reducer
public struct Feature {
    public struct State {
        @PresentationState var destination: Destination.State?
    }

    public enum Action {
        case tappedShowOverlay

        case destination(PresentationAction<Destination.Action>)
    }

    public var body: some ReducerOf<Self> {
        Reduce {
            state, action in

            switch action {
            case .destination(.presented(.overlay(.delegate(let action)))):
                switch action {
                case .dismiss:
                    state.destination = nil
                    return .none
                }

            case .tappedShowOverlay:
                state.destination = .overlay(Overlay.State())
                return .none

            case .destination:
                return .none
            }
        }
        ._printChanges()
        .ifLet(\.$destination, action: \.destination) {
            Destination()
        }
    }

    @Reducer
    public struct Destination {
        public enum State {
            case overlay(Overlay.State)
        }

        public enum Action {
            case overlay(Overlay.Action)
        }

        public var body: some ReducerOf<Self> {
            Scope(state: \.overlay, action: \.overlay) {
                Overlay()
            }
        }
    }
}

public struct OverlayView: View {
    let store: StoreOf<Overlay>

    public var body: some View {
        Color.blue
            .overlay(content: { Text("Tap me") })
            .onTapGesture {
                store.send(.tapped)
            }
    }
}

@Reducer
public struct Overlay {
    public struct State {

    }

    public enum Action {
        case tapped

        case delegate(Delegate)

        public enum Delegate {
            case dismiss
        }
    }

    public var body: some ReducerOf<Self> {
        Reduce {
            state, action in

            switch action {
            case .tapped:
                return Effect.send(.delegate(.dismiss))

            case .delegate:
                return .none
            }
        }
    }
}

The Composable Architecture version information

1.5.2

Destination operating system

iOS17

Xcode version information

Version 15.1 (15C65)

Swift Compiler version information

swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
larryonoff commented 9 months ago

I have the same issue also.

mbrandonw commented 9 months ago

Hi @jaanussiim, thank you very much for the simple reproducible example. That was very helpful.

I've been able to verify the crash, and I was able to reduce the example a little bit:

struct ParentView: View {
  let store = Store(initialState: Parent.State()) {
    Parent()
  }
  var body: some View {
    Form {
      IfLetStore(
        store.scope(state: \.$child, action: \.child),
        then: ChildView.init(store:),
        else: {
          Button(action: { store.send(.show) }) {
            Text("Show")
          }
        }
      )
    }
  }
}
@Reducer
struct Parent {
  struct State {
    @PresentationState var child: Child.State?
  }
  enum Action {
    case child(PresentationAction<Child.Action>)
    case show
  }
  var body: some ReducerOf<Self> {
    Reduce { state, action in
      switch action {
      case .child(.presented(.dismiss)):
        state.child = nil
        return .none
      case .child:
        return .none
      case .show:
        state.child = Child.State()
        return .none
      }
    }
    .ifLet(\.$child, action: \.child) {
      Child()
    }
  }
}
struct ChildView: View {
  let store: StoreOf<Child>
  var body: some View {
    Button("Dismiss") { store.send(.dismiss) }
  }
}
@Reducer
struct Child {
  struct State {}
  enum Action { case dismiss }
  var body: some ReducerOf<Self> { EmptyReducer() }
}

I've got a fix for the crash, but want to get some tests into place and want to discuss with Stephen too. For now I suggest pinning to 1.5.1 to avoid the problem.

mbrandonw commented 9 months ago

@larryonoff @jaanussiim Can y'all try pointing to this branch and see if it fixes the problem for you?

https://github.com/pointfreeco/swift-composable-architecture/compare/store-cache-crash-fix

jaanussiim commented 9 months ago

The location where I found the crash is no longer crashing when using branch store-cache-crash-fix

larryonoff commented 9 months ago

@mbrandonw thanks for the fix! The crash flew away.