Application development on SwiftUI. Part 1: data flow and Redux

Original author: Thomas Ricouard
  • Transfer


After participating in the State of the Union session at WWDC 2019, I decided to study SwiftUI in detail. I spent a lot of time working with him and now I started developing a real application that can be useful to a wide range of users.

I called it MovieSwiftUI - this is an app for finding new and old films, as well as collecting them into a collection using the TMDB API . I always loved films and even created a company working in this area, though for a long time. It was hard to call the company cool, but the application - yes!

We remind you: for all readers of “Habr” - a discount of 10,000 rubles when registering for any Skillbox course using the “Habr” promo code.

Skillbox recommends: The on-line educational course "Profession Java-developer" .

So what does MovieSwiftUI do?

  • Interacts with the API - this is what almost any modern application does.
  • Loads asynchronous request data and parses JSON in the Swift model using Codable .
  • Shows images downloaded on demand and caches them.
  • This app for iOS, iPadOS, and macOS provides the best UX for users of these OSs.
  • The user can generate data, create their own movie lists. The application saves and restores user data.
  • Views, components and models are clearly separated using the Redux pattern. The data stream is unidirectional here. It can be fully cached, restored and overwritten.
  • The application uses the basic components SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. It also provides custom views, gestures, UI / UX.


In fact, the animation is smooth, the gif turned out to be a little twitchy.

Work on the application gave me a lot of experience, and overall it is a positive experience. I was able to write a fully functional application, in September I will improve it and put it in the AppStore, simultaneously with the release of iOS 13.

Redux, BindableObject, and EnvironmentObject




I have been working with Redux for about two years now, so I know this relatively well. In particular, I use it in the frontend for the React website, as well as for developing native iOS (Swift) and Android (Kotlin) applications.

I have never regretted choosing Redux as the data flow architecture for building an application on SwiftUI. The most difficult moments when using Redux in the UIKit application are working with the store, as well as getting and retrieving data and comparing it with your views / components. To do this, I had to create a kind of connector library (on ReSwift and ReKotlin). Works well, but quite a lot of code. Unfortunately, it is (not yet) open source.

Good news! The only things to worry about with SwiftUI - if you plan to use Redux - are store, states and reducers. The SwiftUI takes over the interaction with the store thanks to @EnvironmentObject. So, store starts with BindableObject.

I created a simple Swift package, SwiftUIFlux , which provides the basic use of Redux. In my case, this is part of MovieSwiftUI. I also wrote a step-by-step tutorial to help you use this component.

How it works?

final public class Store: BindableObject {
    public let willChange = PassthroughSubject()
    private(set) public var state: State
    private func _dispatch(action: Action) {
        willChange.send()
        state = reducer(state, action)
    }
}

Each time you start an action, you activate the gearbox. He will evaluate the actions in accordance with the current state of the application. Then it will return a new modified state in accordance with the type of action and data.

Well, since store is a BindableObject, it will notify SwiftUI of a change in its value using the willChange property provided by PassthroughSubject. This is because BindableObject must provide PublisherType, but the protocol implementation is responsible for managing it. All in all, this is a very powerful tool from Apple. Accordingly, in the next rendering cycle, SwiftUI will help to display the body of representations in accordance with the state change.

Actually, this is all - the heart and magic of SwiftUI. Now, in any view that is subscribed to a state, the view will be displayed according to what data is received from the state and what has changed.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let controller = UIHostingController(rootView: HomeView().environmentObject(store))
            window.rootViewController = controller
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}
struct CustomListCoverRow : View {
    @EnvironmentObject var store: Store
    let movieId: Int
    var movie: Movie! {
        return store.state.moviesState.movies[movieId]
    }
    var body: some View {
        HStack(alignment: .center, spacing: 0) {
            Image(movie.poster)
        }.listRowInsets(EdgeInsets())
    }
}

Store is implemented as an EnvironmentObject when the application starts, and then available in any view using @EnvironmentObject. Performance is not reduced because derived properties are quickly retrieved or calculated from the application state.

The code above changes the image if the poster for the movie changes.

And this is actually done in just one line, with the help of which the views are connected to the state. If you worked with ReSwift on iOS or even connect with React, you will understand what SwiftUI magic is.

And now you can try to activate the action and publish a new state. Here is a more complex example.

struct CustomListDetail : View {
    @EnvironmentObject var store: Store
    let listId: Int
    var list: CustomList {
        store.state.moviesState.customLists[listId]!
    }
    var movies: [Int] {
        list.movies.sortedMoviesIds(by: .byReleaseDate, state: store.state)
    }
    var body: some View {
        List {
            ForEach(movies) { movie in
                NavigationLink(destination: MovieDetail(movieId: movie).environmentObject(self.store)) {
                    MovieRow(movieId: movie, displayListImage: false)
                }
            }.onDelete { (index) in
               self.store.dispatch(action: MoviesActions.RemoveMovieFromCustomList(list: self.listId, movie: self.movies[index.first!]))
            }
        }
    }
}

In the code above, I use the .onDelete action from SwiftUI for each IP. This allows the line in the list to display the usual iOS swipe for deletion. Therefore, when the user touches the delete button, he starts the corresponding action and removes the movie from the list.

Well, since the list property is derived from the state of BindableObject and is implemented as an EnvironmentObject, SwiftUI updates the list, because ForEach is associated with the calculated movie property.

Here is part of the MoviesState reducer:

func moviesStateReducer(state: MoviesState, action: Action) -> MoviesState {
    var state = state
    switch action {
    // other actions.
    case let action as MoviesActions.AddMovieToCustomList:
        state.customLists[action.list]?.movies.append(action.movie)
    case let action as MoviesActions.RemoveMovieFromCustomList:
        state.customLists[action.list]?.movies.removeAll{ $0 == action.movie }
    default:
        break
    }
    return state
}

The reducer is executed when you submit the action and return a new state, as mentioned above.

I will not go into details for now - how does SwiftUI really know what to display. To understand this more deeply, you should look at the WWDC session on data flow in SwiftUI. It also explains in detail why and when to use State , @Binding, ObjectBinding, and EnvironmentObject.

Skillbox recommends:


Also popular now: