IfLet without publisher

I would like to be able to go from a Store<State?, Action> to a Store<State, Action>? without having to setup an ifLet publisher when scoping an IdentifiedArray with an ID.

import ComposableArchitecture

public extension Store {
   func ifLet<Wrapped>() -> Store<Wrapped, Action>? where State == Wrapped?, Wrapped: Equatable {
    guard let state = ViewStore(self).state else { return nil }    
    return self.scope(state: { $0 ?? state })
  }
}

Is there a better way to do this?

There was a discussion about "synchronous" ifLet here: UIKit Synchronous IfLet by heyltsjay · Pull Request #472 · pointfreeco/swift-composable-architecture · GitHub

Outside of SwiftUI (which maintains its own subscriptions automatically), it's probably not super safe to observe a store without a subscription, since you may be working with old state or may miss notifications of state updates. I'm not sure how you're using the above ifLet, but can you envision a case in which the upstream store's state goes nil and then you might be left with a phantom store that always returns the cached state value? Or are you being safe about dismissing the view/controller that holds onto this store when it goes nil?

Thanks for your reply. This is the main use case:

itemsIDs points to items.ids (IdentifiedArray)

inside a ASCollectionController:

    viewStore.publisher.ids
      .sink { [weak self] ids in
        guard let self = self else { return }
        
        defer {
          if let context = self.batchContext {
            context.completeBatchFetching(true)
            self.batchContext = nil
          }
        }
              
        let changeset = StagedChangeset(source: self.itemIDs, target: ids)
        
        self.node.reload(using: changeset) { data in
          self.itemIDs = data
        }
      }
      .store(in: &cancellables)
  public func collectionNode(
    _ collectionNode: ASCollectionNode,
    nodeBlockForItemAt indexPath: IndexPath
  ) -> ASCellNodeBlock {
    // We MUST NOT access indexPath within our node block. The block
    // will run asynchronously and indexPath may not be valid outside
    // this scope.
    
    let id = itemIDs[indexPath.row]
    
    let store = store.scope(
      state: { $0.items[id: id] },
      action: { PropAction.item(id: id, action: $0) }
    )
    return store.cellNodeBlock(presenter: self)
  }
public extension Store {
  func cellNodeBlock(presenter: UIViewController? = nil) -> ASCellNodeBlock
  where State == PropState?, Action == PropAction {
    guard let store = self.ifLet() else { return { .init() } }
    
    switch ViewStore(store).type {
    case .banner, .bannerWithOverlay:
      return { BannerNode(store: store, presenter: presenter) }
    default:
      return { PropNode(store: store, presenter: presenter) }
    }
  }

The store is scoped right away for every cell node in the collection.

If the store is nil it means the item was removed from the array and the cellNode will disappear shortly.

if the store is nil when we create the node block, an empty cell node would be displayed but it will also be removed from the collection:

        let changeset = StagedChangeset(source: self.itemIDs, target: ids)
        
        self.node.reload(using: changeset) { data in
          self.itemIDs = data
        }
Terms of Service

Privacy Policy

Cookie Policy