Sign-In With Apple in TCA — Part 2

A simple UI and some TCA

Sam McGarry
7 min readJan 8, 2023

General approach

As my team at Playground has been adapting TCA, we’ve naturally been trying to identify best practices with the framework. We’ve adapted a few ideas thus far, some of which will show up in this tutorial.

To provide some clarity, I’ll go through them briefly before we get started with our UI (but feel free to skip of course).

Delegation

Traditionally in TCA, communication between features is done by having a parent feature observe a child feature’s actions directly. An alternative approach that my team recently adapted is to only rely on custom delegate actions for communication.

This allows us to communicate with parent TCA features without exposing the actions that are only relevant to child features (hence avoiding unforeseen side effects later on, especially in larger applications). I originally got the idea from an article called TCA Action Boundaries by Krzysztof Zabłocki, which I highly recommend checking out.

Naming Techniques

Another thing you may notice is underscores in the names of some TCA actions we’ll be defining. This is something my team has been doing to identify actions as “internal”, meaning only the child feature itself should care about it. This is another idea we got from Krzysztof’s article that I mentioned above.

Reducer Protocol

Each TCA feature we build will conform to the new Reducer Protocol. This means it will have a State, Action, and Body.

Features

For our app, we’ll need three features:

  • SignIn — to be shown when the user is signed out.
  • Main — to be shown when the user is signed in.
  • Root — to host and integrate the SignIn and Main features.

Let’s start with the star of the show; the SignIn feature.

Let’s build some UI

SignIn TCA

The main action we’ll need in the SignIn feature is .signInWithAppleButtonTapped, which will take a TaskResult<AppleAuthenticationResponse> as an argument. This action will be called whenever the sign-in button is tapped.

The SignIn feature will also need a Delegate, and within it a .didSignIn action so we can notify when the UI needs to be updated after a successful sign-in.

In the reducer, we can handle the .success and .failure cases of the result. If the result is successful, we’ll want to save the userID we get back from our response using the KeychainItem helper we already set up (so we can check if the user is already signed in on launch). Then we can call our .didSignIn delegate action we just defined to notify for a UI update.

If the sign-in attempt fails, ideally we would show some sort of an alert in the UI with the error message. I’m choosing not to cover that here, as it will make this article longer than it already it is. So for now, we’ll just return .none in the event of a failure.

SignInView

The only component we’ll need in the SignInView is the SignInWithAppleButton I mentioned earlier. We aren’t requesting any additional info from the user (e.g. email address, name, etc.), so we won’t be adjusting the ASAuthorizationAppleIDRequest in the onRequest closure.

In the onCompletion handler, we’ll need to handle the .failure and .success cases for the result. Regardless of the result, we’ll be sending the .signInWithAppleButtonTapped action.

If the result is a .success, we’ll need to unwrap the optional ASAuthorizationCredential that we get from the ASAuthorization object. If the credential is nil, we can throw the .missingAppleIDCredential error we defined earlier.

Here we are basically throwing the error into the abyss since we aren’t showing anything on the UI side, but we are demonstrating how we can use TaskResult to pass an error back to TCA.

Assuming we do have a credential object, we can create an AppleAuthenticationResponse using the unique userID we get, and pass that response back to TCA.

If the result is a .failure, we’ll simply relay the error we get back from AuthenticationServices back to TCA.

Again, the error is ending up in the abyss in this implementation. I’ll leave you to handle the error if you wish :)

Main TCA

Like the SignIn feature, our Main feature will only have one button, this time for signing out. So we’ll need a .signOutButtonTapped action to call when that button is tapped, and it won’t need to take any arguments.

We’ll also need a Delegate once again, this time with a .didSignOut action to notify when a UI update is needed after the user signs out. I’m sure you can start to notice a pattern emerging.

When the .signOutButtonTapped action is called, we’ll want to delete any stored userID using the KeychainItem helper again. Once that is done, we’ll notify that a UI update is needed by returning the .didSignOut delegate action.

MainView

All we’ll need in the MainView is the sign-out button, which again will call the .signOutButtonTapped action we just defined. We can also give the view a little navigation title to make it look a bit more friendly.

Root Reducer

Now to bring it all together, we need to set up a Root feature that will host the SignIn and Main features we just set up. As the root feature of the app (hence the name), it will be in charge of determining whether the user is signed in or not and showing the correct view to the user.

This feature is more complicated than the ones we’ve already built, so I’ll cover it by going through the State, Action and then Reducer.

Root State

Since our app can be boiled down to two child states (SignIn and Main), and the app can only show one of them at a time, this is a perfect opportunity to use TCA’s enumerated State. Our cases can be called .signIn (taking SignIn.State as an argument), and .main (taking Main.State as an argument).

We can also set .signIn as the default state in the initializer, as if we don’t know anything about the user’s authorization status, it is safer to assume they still need to sign-in.

public enum State: Equatable {
case signIn(SignIn.State)
case main(Main.State)

public init() {
self = .signIn(SignIn.State())
}
}

Root Action

In order to observe the delegate actions we defined earlier, the Root feature will need a .main and .signIn action, taking SignIn.Action and Main.Action as arguments.

It will also need an ._onAppear action to trigger the authentication check on launch, and an _checkAuthenticationStatus action to be called when we get a response from that status check.

public enum Action: Equatable {
case signIn(SignIn.Action)
case main(Main.Action)

case _onAppear
case _checkAuthenticationStatusResponse(AppleAuthenticationStatus)
}

Root Reducer

The first thing you’ll notice above the reducer code is where we access the AuthenticationClient we’ll be using (the live implementation we defined in the part 1 of this article) with the slick @Dependency wrapper.

Next, we’ll be using the Scope() feature of TCA to scope our SignIn and Main features by passing the previously declared child states and actions, and telling TCA which reducer we’ll want to use for each child state and action. This enables us to observe the delegate actions for each scoped feature.

Scope(
state: /State.signIn,
action: /Action.signIn
) {
SignIn()
}

Lastly, we’ll use TCA’s Reduce closure to handle the main actions for the Root feature, as well as the delegate actions for our child features. This is where everything we’ve done so far really comes together.

  • The ._onAppear action will return a .task effect, which will in turn call the ._checkAuthenticationStatus action with the response of our authentication status check. To get the response, we’ll call the checkAppleAuthenticationStatus() method on our AuthenticationClient, and pass in the stored userID that we query using the KeychainItem helper.
  • Depending on the user’s authentication status that is returned in the ._checkAuthenticationStatusResponse action, we’ll set the app state accordingly.
  • Then we just need to handle the delegate actions for .didSignIn and .didSignOut, to trigger changes of state (and by extension the UI) when the user signs in or out.
@Dependency(\.authenticationClient) var authenticationClient

public var body: some ReducerProtocol<State, Action> {
Scope(
state: /State.signIn,
action: /Action.signIn
) {
SignIn()
}
Scope(
state: /State.main,
action: /Action.main
) {
Main()
}
Reduce { state, action in
switch action {
case ._onAppear:
return .task {
._checkAuthenticationStatusResponse(
await authenticationClient.checkAppleAuthenticationStatus(KeychainItem.currentUserIdentifier)
)
}
case let ._checkAuthenticationStatusResponse(status):
switch status {
case .signedIn:
state = .main(Main.State())
case .signedOut:
state = .signIn(SignIn.State())
}
return .none
case let .signIn(action):
switch action {
case .delegate(.signInSuccess):
state = .main(Main.State())
default:
break
}
return .none
case let .main(action):
switch action {
case .delegate(.didSignOut):
state = .signIn(SignIn.State())
default:
break
}
return .none
}
}
}

RootView

After all that set up for the Root feature, the accompanying view is refreshingly simple. We’ll be using TCA’s SwitchStore with the CaseLet view wrapper, that will show the SignInView when we set the app’s state to .signIn, and the MainView when it is set to .main.

The only action we’ll be explicitly calling from this view is ._onAppear, so we can kick off the authentication status check.

SignInWithAppleTCAApp

Finally, all that is needed now is to call the RootView in our App struct (and wrap it in a NavigationView so our navigation titles show up 😉).

And we’re done! Hit run on the project and you should be able to sign-in with Apple, and then sign back out again 🎉.

Now that we’re done building the app, we can dive into testing in part 3.

--

--