Redux-like state container in SwiftUI: Connectors.
Tags: swift, ios, mobile, programming
Published: 2023-03-18 09:36:39

In the past month, I gained a deep appreciation for the benefits of having a single source of truth and a state container to manage the entire [application's state in a single place](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-derived-stores-2d62). I've already implemented this strategy in a few of my [previous apps](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-container-views-a35) and intend to continue employing it in all future projects.

- [Basics](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-basics-single-source-of-truth-2lpc)
- [Reducer and Actions](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-basics-reducer-and-actions-flg)
- [Unidirectional flow](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-basics-unidirectional-flow-3f2p)
- [Side effects](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-side-effects-3l23)
- [Usage](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-usage-k1i)
- [State normalization](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-state-normalization-108o)
- [State composition](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-state-composition-57e5)
- [Reducer composition](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-reducer-composition-3j0m)
- [Derived stores](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-derived-stores-2d62)
- [Container Views](https://dev.to/sergeyleschev/redux-like-state-container-in-swiftui-container-views-a35)


```swift
import Foundation

struct AppState: Equatable {
    var showsById: [Ids: Show] = [:]
    var seasons: [Ids: Season] = [:]
    var episodes: [Ids: Episode] = [:]
    var showImages: [Ids: FanartImages] = [:]
    var watchedHistory: [Ids] = []
}

enum AppAction: Equatable {
    case markAsWatched(episode: Ids, watched: Bool)
}

import SwiftUI
import KingfisherSwiftUI

struct HistoryView: View {
    @ObservedObject var store: Store<AppState, AppAction>

    var body: some View {
        LazyVGrid(columns: [.init(), .init()]) {
            ForEach(store.state.watchedHistory, id: \.self) { id in
                posterView(for: id)
            }
        }
    }

    private func posterView(for id: Ids) -> some View {
        let image = store.state.showImages[id]?.tvPosters?.first?.url
        let episode = store.state.episodes[id]
        let title = episode?.title ?? ""
        let date = episode?.firstAired ?? Date()

        return VStack {
            image.map {
                KFImage($0)
            }

            Text(title)
            Text(verbatim: DateFormatter.shortDate.string(for: date))
                .foregroundColor(.secondary)
                .font(.subheadline)
        }
    }
}

```

The main issue here is the formatting logic that lives inside the view. We can’t verify it using unit tests. Another problem is SwiftUI previews. We have to provide the entire store with the whole app state to render a single screen. And we can’t keep the view in a separated Swift Package because it depends on the whole app state.

We can improve the case a little bit by using derived stores that provide only the app state’s needed part. But we still need to keep the formatting logic somewhere outside of the view.

Let me introduce another component that lives in between the whole app store and the dedicated view. The primary responsibility of this component is the transformation of the app state to the view state. I call it Connector, and it is Redux inspired component.

```swift
protocol Connector {
    associatedtype State
    associatedtype Action
    associatedtype ViewState: Equatable
    associatedtype ViewAction: Equatable

    func connect(state: State) -> ViewState
    func connect(action: ViewAction) -> Action
}

extension Store {
    func connect<C: Connector>(
        using connector: C
    ) -> Store<C.ViewState, C.ViewAction> where C.State == State, C.Action == Action {
        derived(
            deriveState: connector.connect(state: ),
            embedAction: connector.connect(action: )
        )
    }
}
```

`Connector` is a simple protocol that defines two functions. The first one transforms the whole app state into the view state, and the second one converts view actions into app actions. Let’s refactor our view by introducing view state and view actions.

```swift
extension HistoryView {
    struct State: Equatable {
        let posters: [Poster]

        struct Poster: Hashable {
            let ids: Ids
            let imageURL: URL?
            let title: String
            let subtitle: String
        }
    }

    enum Action: Equatable {
        case markAsWatched(episode: Ids)
    }

    typealias ViewModel = Store<State, Action>
}
```

We create an entirely different model for our view that holds the only needed data. The view state here is a direct mapping of the view representation and its model. The view action enum is the only action that available for this particular view. You eliminate the accidents where you call unrelated actions. Finally, your view is fully independent, which allows you to extract it into a separated Swift Package.

```swift
import KingfisherSwiftUI
import SwiftUI

struct HistoryView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        LazyVGrid(columns: [.init(), .init()]) {
            ForEach(viewModel.state.posters, id: \.title) { poster in
                VStack {
                    poster.imageURL.map {
                        KFImage($0)
                    }
                    Text(poster.title)
                    Text(poster.subtitle)
                }.onTapGesture {
                    viewModel.send(.markAsWatched(episode: poster.ids))
                }
            }
        }
    }
}
```

Another benefit here is the **simple view**. It doesn’t do anything. The view displays the formatted data and sends actions. You can quickly write as many SwiftUI previews as you need to cover all the different cases like loading, empty, etc.

```swift
extension Store {
    static func stub(with state: State) -> Store {
        Store(
            initialState: state,
            reducer: .init { _, _, _ in Empty().eraseToAnyPublisher() },
            environment: ()
        )
    }
}

struct HistoryView_Previews: PreviewProvider {
    static var previews: some View {
        HistoryView(
            viewModel: .stub(
                with: .init(
                    posters: [
                        .init(
                            ids: Ids(trakt: 1),
                            imageURL: URL(
                                staticString: "https://domain.com/image.jpg"
                            ),
                            title: "Film",
                            subtitle: "Science"
                        )
                    ]
                )
            )
        )
    }
}
```
It’s time to create the particular connector type, which we will use to bind the app state to the view state.

```swift
enum Connectors {}

extension Connectors {
    struct WatchedHistoryConnector: Connector {
        func connect(state: AppState) -> HistoryView.State {
            .init(
                posters: state.watchedHistory.compactMap { ids in
                    let episode = state.episodes[ids]
                    return HistoryView.State.Poster(
                        ids: ids,
                        imageURL: state.showImages[ids]?.tvPosters?.first?.url,
                        title: episode?.title ?? "",
                        subtitle: DateFormatter.shortDate.string(for: episode?.firstAired) ?? ""
                    )
                }
            )
        }

        func connect(action: HistoryView.Action) -> AppAction {
            switch action {
            case let .markAsWatched(episode):
                return AppAction.markAsWatched(episode: episode, watched: true)
            }
        }
    }
}
```

As you can see in the example above, `WatchedHistoryConnector` is a simple value type that we can quickly test using unit testing. Now, we should take a look at how we can use our connector types. Usually, I have container or flow views that connect views to the store.

```swift
import SwiftUI

struct RootContainerView: View {
    @EnvironmentObject var store: Store<AppState, AppAction>

    var body: some View {
        HistoryView(
            viewModel: store.connect(using: Connectors.WatchedHistoryConnector())
        )
    }
}
```


---

**Contacts**
I have a clear focus on time-to-market and don't prioritize technical debt. And I took part in the Pre-Sale/RFX activity as a System Architect, assessment efforts for Mobile (iOS-Swift, Android-Kotlin), Frontend (React-TypeScript) and Backend (NodeJS-.NET-PHP-Kafka-SQL-NoSQL). And I also formed the work of Pre-Sale as a CTO from Opportunity to Proposal via knowledge transfer to Successful Delivery.

🛩️ #startups #management #cto #swift #typescript #database
📧 Email: sergey.leschev@gmail.com
👋 LinkedIn: https://linkedin.com/in/sergeyleschev/
👋 LeetCode: https://leetcode.com/sergeyleschev/
👋 Twitter: https://twitter.com/sergeyleschev
👋 Github: https://github.com/sergeyleschev
🌎 Website: https://sergeyleschev.github.io
🌎 Reddit: https://reddit.com/user/sergeyleschev
🌎 Quora: https://quora.com/sergey-leschev
🌎 Medium: https://medium.com/@sergeyleschev