Application development on SwiftUI. Part 1: Dataflow and Redux

Application development on SwiftUI. Part 1: Dataflow and Redux

After participating in the State of the Union session at WWDC 2019, I decided to study SwiftUI in detail. I have spent a lot of time working with it and have now started to develop 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 movies, as well as collecting them in a collection using TMDB API. I have always loved films and even created a company working in this area, though a long time ago. It was difficult to call the company cool, but the application - yes!

We remind you: for all readers of "Habr" - a discount of 10 rubles when enrolling in any Skillbox course using the "Habr" promotional code.

Skillbox recommends: Educational online course "Profession Java Developer".

So what does MovieSwiftUI do?

  • Interacts with API - this is what almost any modern application does.
  • Loads asynchronous data on request and parses JSON into a Swift model using codable.
  • Shows images loaded on demand and caches them.
  • This app for iOS, iPadOS, and macOS provides the best user experience for those OS users.
  • 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 flow here is unidirectional. It can be fully cached, restored and overwritten.
  • The application uses the basic components of SwiftUI, TabbedView, SegmentedControl, NavigationView, Form, Modal, etc. It also provides custom views, gestures, UI/UX.

Application development on SwiftUI. Part 1: Dataflow and Redux
In fact, the animation is smooth, the GIF turned out to be a little twitchy

Working on the app has given me a lot of experience and overall it's a positive experience. I was able to write a fully functional application, in September I will improve it and put it on the AppStore, simultaneously with the release of iOS 13.

Redux, BindableObject and EnvironmentObject

Application development on SwiftUI. Part 1: Dataflow and Redux

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

I have never once regretted choosing Redux as my dataflow architecture for building a SwiftUI app. The trickiest part of using Redux in a UIKit app is dealing with the store and getting and retrieving data and mapping it to 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 thing to worry about with SwiftUI - if you plan on using Redux - is the store, states, and reducers. Interaction with the store is completely taken over by SwiftUI thanks to @EnvironmentObject. So store starts with BindableObject.

I have created a simple Swift package, SwiftUIFlux, which provides basic usage of Redux. In my case it is part of MovieSwiftUI. I also wrote a step by step tutorialto help you use this component.

How does it work?

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

Every time you fire an action, you fire the reducer. It will evaluate actions according to the current state of the application. Further, 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 when its value has changed using the willChange property provided by PassthroughSubject. This is because the BindableObject must provide a 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 render cycle, SwiftUI will help render the body of the views according to the state change.

Actually, this is all - the heart and magic of SwiftUI. Now in any view that is subscribed to the 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<AppState>
    
    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())
    }
}

The Store is injected as an EnvironmentObject at application startup and is then available to any view using @EnvironmentObject. There is no performance penalty because derived properties are quickly retrieved or computed from application state.

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

And this is actually done with just one line, with the help of which the views are connected to the state. If you have worked with ReSwift on iOS or even connect with React, you will understand what the magic of SwiftUI 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<AppState>
 
    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 row in the list to display a normal iOS swipe to delete. So when the user taps the delete button, it triggers the corresponding action and removes the movie from the list.

Well, since the list property is derived from the BindableObject state and injected as an EnvironmentObject, SwiftUI updates the list because ForEach is bound to the movies computed 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 dispatch an action and return a new state, as said above.

I won't go into details just yet - how does SwiftUI actually know what to display. In order to understand this more deeply, it is worth view WWDC session on data flow in SwiftUI. It also explains in detail why and when to use State, @Binding, ObjectBinding and EnvironmentObject.

Skillbox recommends:

Source: habr.com

Add a comment