Dealing with system calls

Assuming that I have a store with the following actions available:

enum MyActions {
  case start
  case step1Success(Response)
  case step1Failure(Error)
  case step2Success(Outcome)
  case step2Failure(Error)
}

From a consumer point of view, I am exposing this:

enum State {
   case success(Outcome)
   case loading
   case failure(Error)
}

Now I want to start a request at this point, along the lines of:

func application(_ application: UIApplication, 
                 performFetchWithCompletionHandler completionHandler:
                 @escaping (UIBackgroundFetchResult) -> Void) {


    myStore.publisher.sink { state in 
       /// Do something with the state
    }

    myStore.send(.start)

}

The problem here is that I am not interested in the end result, but rather the step1's Response.

I have considered passing a function to the Environment and calling it when the step1 is triggered inside the reducer, but I feel there's a more elegant approach for this problem.

Rui - sorry, I meant to get back to you about this yesterday.

I think the right approach here is to try and push this behaviour into your domain logic (i.e. your reducer and effects if necessary). I treat App/Scene delegate callbacks as inputs into the system just like user inputs - just fire actions into your store from these hooks as needed.

I don't know exactly what your state struct looks like but I would try and model the idea of an in-progress background task. You could wrap the background fetch completion handler in a type of your own for this but I don't think it's necessary.

For example, you could have this on your state:

typealias BackgroundFetchCompletionHandler = (UIBackgroundFetchResult) -> Void

struct MyState {
  /// Represents an in-progress background fetch
  var backgroundFetchCompletionHandler: BackgroundFetchCompletionHandler?
}

enum MyAction {
  ...
  case backgroundFetchStarted(BackgroundFetchCompletionHandler)
}

let reducer = Reducer<MyState, MyAction, MyEnvironment> { state, action, environment
  switch action {
    case let .backgroundFetchStarted(completionHandler):
      state.backgroundFetchCompletionHandler = completionHandler
    
    case let .step1Success(response):
      /// do something with step 1 response
      if let completionHandler = state.backgroundFetchCompletionHandler {
        state.backgroundFetchCompletionHandler = nil
        return .fireAndForget { completionHandler(.newData) } // etc.
      }
  }
}

Now, in your app delegate you can fire off two actions:

func application(
  _ application: UIApplication, 
  performFetchWithCompletionHandler completionHandler:
  @escaping (UIBackgroundFetchResult) -> Void
) {
  store.send(.backgroundFetchStarted(completionHandler))
  store.send(.start)
}

Alternatively, you could return an effect from .backgroundFetchStarted to trigger .start automatically rather than explicitly sending the action from the app delegate (you could return Effect(value: .start) but I don't know if that's considered idiomatic TCA or not.

The nice thing about this is that can now test this behaviour much more easily (and control any dependencies if effects are involved).

Does this help?

1 Like

Rui pointed out on Twitter that storing a function in your state would break automatic Equatable conformance which is a good point. So returning to my idea of wrapping in a custom type, this would probably be the way to go:

struct BackgroundTask: Equatable {
  var completionHandler: (UIBackgroundFetchResult) -> Void

  // custom equatable conformance to just return true
}

I really like this approach:

  store.send(.backgroundFetchStarted(completionHandler))
  store.send(.start)

:heart:

I was under the impression that utilizing the environment was the standard way to do this. What makes it unelegant?

My lack of taste. On a serious tone, I don't mind either way, on the state or environment.

Standard way to do what though? It’s not exactly clear what Rui wants to do but if it’s something like a side effect then it should just be an Effect.

I’m not really sure how the environment helps here because the background completion handler will likely be received after you’ve initialised your store and it’s environment.

This is why I suggest treating this as another event that triggers an action and passing in the completion handler that way. The fact you are in the middle of processing a background fetch seems like part of your application state to me. The manual equatable conformance is a little bit annoying but bearable I think.

Thought a bit more about this topic.

I am not too sure how this would be passed to the environment, because it's a completion handler and the only way to pass it somewhere is via viewStore.send(). At the reducer stage, environment is a constant, unless I move the Environment from a struct to a class, which I guess could work, but given that I see the completion handler (for a background task) a part of the state, I am more inclined to have it as part of the state. Regarding Equatable, I am using this: Function.swift · GitHub

Bridging these types of APIs to TCA is a little tricky. It seems like it should be in the environment because it's definitely side-effecty, but there's no clear way to get it into the environment since you only have access when the UIApplication delegate method is called. So then it seems like it should be passed in with the action, but then that ruins equatability which creates friction with testing.

Another way is to have this fully driven off of state. That is, hold an optional UIBackgroundFetchResult in state (luckily it's Equatable):

struct AppState: Equatable {
  var backgroundFetchResult: UIBackgroundFetchResult?
  ...
}

And then in the delegate you can subscribe to when backgroundFetchResult flips to a non-nil value and use that as the opportunity to invoke the completion handler:

func application(
  _ application: UIApplication, 
  performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
  self.viewStore.send(.performFetch)
  self.backgroundFetchCancellable = self.viewStore.publisher.backgroundFetchResult
    .compactMap { $0 }
    .prefix(1)
    .sink(receiveValue: completionHandler)
}

It's a bit of a bummer to put this logic in the view, but we find it to have the least number of downsides compared to shoehorning functions into the state/action/environment.

Edit: I didn't explain what the reducer would do with this action. It could be roughly something like this:

case .performFetch:
  state.backgroundFetchResult = nil
  return environment.download()
    .map(AppAction.downloadResponse)

case .downloadResponse(.sucess):
  state.backgroundFetchResult = .newData
  return .none

case .downloadResponse(.failed):
  state.backgroundFetchResult = .failed
  return .none
5 Likes
Terms of Service

Privacy Policy

Cookie Policy