Effortless Deep Linking with TCA
In this story, I want to demonstrate deep linking with The Composable Architecture known as TCA. Before starting to implement, I suggest reading these stories to become familiar with the Deep linking idea and how I’m going to do it. and of course, you need to be familiar with TCA because my focus is to implement DeepLink not explain TCA. The Point-Free teams had a lot of videos and documentation about TCA and it’s better to learn it from them.
Implementation
Apart from the app, to handle a new deep link, I need a MainDeepLinkReducer
to handle all deep links at first.
@Reducer
struct MainDeepLinkReducer {
struct State: Equatable {
var home = HomeReducer.State()
var profile = ProfileReducer.State()
}
enum Action {
case deepLink(URL)
case home(HomeReducer.Action)
case profile(ProfileReducer.Action)
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case let .deepLink(deepLink):
return .send(.home(.deepLink(.handle(deepLink))))
case let .home(.deepLink(.delegate(.next(deepLink)))):
return .send(.profile(.deepLink(.handle(deepLink))))
case .profile(.deepLink(.delegate(.next))):
return .none
case .home, .profile:
return .none
}
}
Scope(state: \.home, action: \.home) {
HomeReducer()
}
Scope(state: \.profile, action: \.profile) {
ProfileReducer()
}
}
}
For each tab or feature that my app has, I created another Reducer
. HomeReducer
and ProfileReducer
are two features that I have in this demo app. Each feature has its own DeepLinkReducer
, HomeDeepLinkReducer
for Home and ProfileDeepLinkReducer
for Profile.
In the MainDeepLinkReducer
body, There is a deepLink
action, in this action I pass the input url to my first feature. if the feature can handle the new url, it will sendhandled
action and that’s it. if not, it will send next
action and I pass the url to the next feature.
I have implemented handling only for the ProfileReducer
for demonstration purposes. Let’s see how it works.
@Reducer
public struct ProfileReducer {
public struct State: Equatable {
var deepLink = ProfileDeepLinkReducer.State()
public init() {}
}
public enum Action {
case deepLink(ProfileDeepLinkReducer.Action)
}
public init() {}
public var body: some ReducerOf<Self> {
Scope(state: \.deepLink, action: \.deepLink) {
ProfileDeepLinkReducer()
}
}
}
I kept ProfileReducer
as simple as possible, it just define the ProfileDeepLinkReducer
.
The magic is happening in the ProfileDeepLinkReducer
.
@Reducer
public struct ProfileDeepLinkReducer {
public struct State: Equatable {
@PresentationState var destination: ProfileDeepLinkDestinationReducer.State?
}
public enum Action {
case handle(URL)
case delegate(DeepLinkDelegate)
case destination(PresentationAction<ProfileDeepLinkDestinationReducer.Action>)
}
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case let .handle(deepLink):
guard let destination = handle(deepLink: deepLink) else {
return .send(.delegate(.next(deepLink)))
}
state.destination = destination
return .send(.delegate(.handled(deepLink)))
case .delegate, .destination:
return .none
}
}
.ifLet(\.$destination, action: \.destination) {
ProfileDeepLinkDestinationReducer()
}
}
private func handle(deepLink: URL) -> ProfileDeepLinkDestinationReducer.State? {
deepLink.absoluteString.contains("account") ? .account(.init()) : nil
}
}
As you can see here, I have a destination that contains all possible destinations for deep links in the Profile section. in the body, I try to handle the new deep link and if it’s handled, I will set the destination with the proper value and send handled
action to the parent.
In the UI part, I utilize TCA and display a sheet whenever the destination receives a value.
WithViewStore(store, observe: { $0 }) { viewStore in
VStack {
Image(.background2)
.resizable()
.aspectRatio(contentMode: .fill)
}
.padding()
.sheet(
store: store.scope(
state: \.deepLink.$destination.account,
action: \.deepLink.destination.account
)
) { store in
Text("Account view")
}
}
I can say that with TCA, the implementation is way easier. you can easily adopt the implementation in my previous story with TCA.
Conclusion
In conclusion, leveraging The Composable Architecture (TCA) for deep linking proves to be a game-changer. The clean, modular structure facilitates an intuitive implementation, streamlining the handling of deep links across different features. With TCA, the complexity of deep linking becomes a manageable and elegant aspect of app development, emphasizing the power of thoughtful architecture.
You can find the source code here on my GitHub.