Redux Meets Swift: How to Store and Structure Data

5.0 / 5.0
Article rating

We continue our series of articles dedicated to the work with Redux libraries in Swift. This time we describe how to store and restore an app’s state when working with Redux.

If you want to use Redux libraries for building iOS app interfaces, the first thing you need to know is how to store data locally and restore it. When developers work with other libraries, they typically use CoreData or a Realm Mobile Database container for data storage. They also store encryption keys and access tokens in Keychain and save binaries to the file system. Storing data in Redux is similar.

How to structure data in the Redux store?

This is a common question for all developers using Redux. The answer in most cases depends on how you plan to interact with the data: Are you planning to frequently iterate over the store data like a list of rows? Or will you require fast access to individual items?

There are a lot of different approaches; however, they usually contain some tradeoffs between access time and ease of iteration. In this article, we present our own approach that addresses the problem of storing data in Redux.

We suggest you start with the following steps:

  • Store data locally
  • Structure the StatePersistence attribute
  • Map models from the state to the database and back

 

Step 1: Store data locally

First of all, the main idea is to store the state locally after every update and restore it when the app starts. For this, you’ll need some basic entities:

– Persistence itself with functions to store and restore the state
– Action to restore the state
– Middleware to handle that action

Persistence will also be subscribed to Store and will handle every state update in the function newState(_ state: State).

protocol StatePersistence {
    func storeState(_ state: State?)
    func restoreState() -> State?
}
 
extension StatePersistence: Subscriber {
    func newState(_ state: State) {
        self.storeState(state)
    }
}
struct InitStoreAction: Action {
    var restoredState: State?
}
 
import Foundation
import RealmSwift
 
class DatabaseMiddleware: Middleware {
 
    let persistence = AppStatePersistence()
 
    // MARK: Middleware
 
    func handle(action: Action) -> Action {
        if var action = action as? InitStoreAction {
            action.restoredState = persistence.restoreState()
            return action
        }
        return action
    }
}

In most cases, this part will look similar to the pattern we’ve shown, regardless of how you decide to implement this function.

Step 2: Structure the StatePersistence attribute

Because the state of the app has a tree-like structure, it’s better to structure all methods within one piece of code in the same manner. In our previous article, we restructured reducers in a similar way to simplify our work. This time, let’s restructure StatePersistence:

 
struct AppState: State {
    var sessionState: SessionState?
    var userState: UserState?
    var feedState: FeedState?
}
 
struct AppStatePersistence: StatePersistence {
 
    let sessionPersistence = SessionStatePersistence()
    let userPersistence = UserStatePersistence()
    let feedPersistence = FeedStatePersistence()
 
    func storeState(_ state: State?) {
        if let state = state as? AppState {
            sessionPersistence.storeState(state.sessionState)
            userPersistence.storeState(state.userState)
            feedPersistence.storeState(state.feedState)
        }
    }
 
    func restoreState() -> State? {
        var state = AppState()
        state.sessionState = sessionPersistence.restoreState()
        state.userState = userPersistence.restoreState()
        state.feedState = feedPersistence.restoreState()
        return state
    }
}
 
struct SessionState: State {
    var authToken: String
}
  
struct SessionStatePersistence: StatePersistence {
 
    let keychain = Keychain.shared()
   func storeState(_ state: State?) {
        guard let state = state as? SessionState else { return }
        keychain.store(string: state.authToken, for: "authToken")
    }
 
    func restoreState() -> State? {
        guard let authToken = keychain.stringForKey("authToken") else { return nil }
        return SessionState(authToken: authToken)
    }
}
struct UserState: State {
    var profile: UserProfile
}
struct UserStatePersistence: StatePersistence {
 
    let database = RealmDatabase.shared()
 
    func storeState(_ state: State?) {
        guard let state = state as? UserState else {
            database.currentUserProfile = nil
            return
        }
        database.currentUserProfile = state.profile
    }
 
    func restoreState() -> State? {
        guard let profile = database.currentUserProfile else { return nil }
        return state = UserState(profile: profile)
    }
}

struct FeedState: State {
    var items: [FeedItem]?
    var loadingStatus: LoadingStatus = .none
    var loadingError: Error?
 
    init(items: [FeedItem]?, loadingStatus: LoadingStatus = .none, loadingError: Error? = nil)
}
struct FeedStatePersistence: StatePersistence {
 
    let database = RealmDatabase.shared()
 
    func storeState(_ state: State?) {
        if let state = state as? FeedState {
            if state.loadingStatus == .success {
                database.feedItems = state.items
            }
        }
        else {
            database.feedItems = []
        }
    }
 
    func restoreState() -> State? {
        return state = FeedState(items: database.feedItems)
    }
}

It’s important to keep the hierarchy of all StatePersistence attributes up to date. This won’t be hard if you’re familiar with how to add reducers. The tree-like form keeps our code organized and allows us to modify the structure even for a large and complex state by using different storages for different data. For instance, we can store user profiles and feeds in Realm and keep session data in Keychain.

Step 3: Map models from the state to the database and back

The main idea is that the state and database structures don’t have to reflect each other. It’s not necessary since they have different purposes. We can create a schema for data in the database in a traditional way, with all relationships between entities, while the state stays hierarchical. If you still need to connect entities in the state the same way as in the database, you can use identities and sets of functions to navigate entities instead of linking them directly, as you would do in CoreData.

There’s also no need to follow one single data model throughout the app. For instance, take a look at the MVVM (Model–View–ViewModel) architectural pattern. There’s always a data model to operate and store in Model, and its many representations are provided by ViewModels for easy use in Views. This is a good example of going away from a single form of data in a whole app, which many developers keep using even though it’s not too useful.

Let’s take a look at how to store user profiles using the Realm Database:

 var currentUserProfile: UserProfile? {
        get {
            guard let dbProfile = realm.objects(RealmUserProfile.self).filter("current == true").first else { return nil }
            var profile = UserProfile(id: dbProfile.id, firstName: dbProfile.firstName, secondName: dbProfile.secondName, email: dbProfile.email)
            return profile
        }
 
        set {
            if let newValue = newValue {
                var profile = RealmUserProfile()
                profile.id = newValue.id
                profile.email = newValue.email
                profile.firstName = newValue.firstName
                profile.secondName = newValue.secondName
                profile.current = true
 
                try! realm.write {
                    realm.add(profile, update: .modified)
                }
            }
            else {
                guard let profile = realm.objects(RealmUserProfile.self).filter("current == true").first else { return }
 
                try! realm.write {
                    realm.delete(profile)
                }
            }
        }
    }
 

… and sync feed items.

var feedItems: [FeedItem] {
        get {
            guard let dbItems = realm.objects(RealmFeedItem.self).toArray() else { return [] }
            var items: [FeedItem] = []
            dbItems.forEach {
                var item = FeedItem(id: $0.id, content: $0.content)
                items.append(item)
            }
            return items
        }
 
        set {
            var items: [String : FeedItem] = [:]
            for item in newValue {
             items[item.id] = item
            }
            let dbItems = realm.objects(RealmFeedItem.self).toArray()
            var toDelete: [RealmFeedItem] = []
            for item in dbItems {
                if items[item.id] == nil {
                    toDelete.append(item)
                }
            }
            try! realm.write {
                for item in newValue {
                    realm.create(RealmFeedItem.self, value: ["id": item.id, "content": item.content], update: .modified)
                }
                realm.delete(toDelete)
            }
        }
    }
    

Here are the database models themselves:

class RealmUserProfile: Object {
    @objc dynamic var id: String = ""
    @objc dynamic var firstName: String?
    @objc dynamic var secondName: String?
    @objc dynamic var email: String = ""
    @objc dynamic var current: Bool = false
 
    override static func primaryKey() -> String? {
        return "id"
    }
}
class RealmFeedItem {
    @objc dynamic var id: String
    @objc dynamic var content: String
 
    override static func primaryKey() -> String? {
        return "id"
    }
}

It’s neither simple nor complicated. Obviously, code grows fast as we develop an app. With Redux, at least there still is a possibility to keep data separated and organized naturally even if the code becomes complex.

The worst thing about the above code is that it will be executed every time the state is updated. This won’t seem like a problem at the start, but as the state keeps growing, the number of updates per second will increase too. Without proper optimization, continuous data processing caused by updates will lead to the device overheating.

Another way to work with data in Redux is to make states conform to the Encodable and Decodable protocols.

In order to remedy the situation, you should make states equatable and/or versionable to anything you want and only save the applied changes. You could also save the state explicitly based on certain actions, though it’s a slippery slope if you don’t know when to choose the right moment to store the state, and especially if you don’t know the exact order of all actions in a multithreaded environment. However, it might be a working solution in some cases.

Another way to work with data in Redux is to make states conform to the Encodable and Decodable protocols. This way you’ll be able to serialize states to JavaScript Object Notations (JSONs) or binaries or make your own implementation of Encoder and Decoder working with Realm, for example. All the same, you can set up CodeGen to generate a mapping between the state and database using a scheme written in JSON or Ruby.

Wrapping up

As you can see, there’s more than one approach to dealing with data storage while coding with Redux. No one has managed to find the ideal method yet, as there are always certain shortfalls when it comes to access time and ease of iteration.

The main purpose of proper state management applied to the frontend is to help us maintain the clarity, elegance, and consistency of our data. Redux helps us keep complex things simple while providing top-notch results.

iOS development services
Do you need a team of professional developers you can lean on? We are at your disposal

Rate the article!

🌕 Cool!
🌖 Good
🌗 So-so
🌘 Meh
🌑 …