Building Reactive Apps with Realm: Episode 5

In this five-episode series, we’re going to have a look at how Realm facilitates building reactive applications from the ground up. In the fifth and final episode, we’ll use Inboxly’s sample project one last time, and learn how we can use a message-driven architecture to keep our project organized and easily scalable.

You can find the other episodes here:


 

Introduction (0:00)

Hi, and welcome back to Building Reactive Apps with Realm. In this series, we’ve been exploring the ways Realm facilitates writing reactive code. We’ve seen examples of the Realm API helping you with responsiveness, resiliency, and in the last episode, we explored elasticity. This time, we’re going to have a look at sending messages.

Sending messages (0:29)

Having a message driven in-app architecture allows you to separate the concerns of your classes more easily. The view controllers only control views, and does not know about your data design, or networking, and so forth. Keeping components decoupled also allows you to keep growing your app without experiencing architecture scale issues.

In this series, we’ve played with a sample project, called Inboxly. Let’s have a look at how new messages are being sent in this app.

// USER
Messages: List<Message>
Outgoing: List<Message>

// MESSAGE
Text: String
Outgoing: Boolean
Favorite: Boolean

The user Realm object has two list properties. One is called Messages, and contains a list of all the messages the user sees on their feed screen. There is also a second list, Outgoing, which contains all the messages that this user has written and are to be sent out. Please note that if the same message exists on both lists, that doesn’t mean that the message object is duplicated. Simply both lists contain a pointer to the same message object.

When the user writes a new message, the newly created message object is inserted in both Messages, and Outgoing.

  • The messages list is what drives the UI, which shows the messages in the feed table view.
  • The ongoing list, on the other hand, is a message queue, which the app network controller observes and consumes messages from, whenever new messages are inserted.

After that, it takes the messages and sends them to the simulated server API, in the sample project.

Since the message queue is persistent in Realm, that also automatically takes care of the cases when the network goes down, or your app is being queued by the OS. Since the data is persisted on disk first, and then the network controller tries to send it, the next time your app starts or when the network comes back, the network controller will try to resend the messages again.

Let’s have a quick look at how a simplistic implementation of this message queue might work.

Adding a Message to the Queue (2:32)

First, let’s have a look at the data controller in the app:

func addMessage(message: String) {

        let realm = try! Realm(configuration: RealmConfig.Main.configuration)
        let user = realm.objects(User).first!

    }

}

There is a method called addMessage that takes a string. It creates a new message and sends it off to the simulated server API. I have already fetched my Realm and the user object. What I want to do next is create a new message and store it in the outgoing list and in the messages list of my user account.

This creates a message object, so it returns a message object that is fully configured for the current user:

func addMessage(message: String) {

        let realm = try! Realm(configuration: RealmConfig.Main.configuration)
        let user = realm.objects(User).first!

        if let outgoingMessage = try? user.createMessage(message) {

        }
    }

What I want to do in here is open a write transaction with my Realm, and add this message to both lists of my user:

func addMessage(message: String) {

        let realm = try! Realm(configuration: RealmConfig.Main.configuration)
        let user = realm.objects(User).first!

        if let outgoingMessage = try? user.createMessage(message) {

            try! realm.write {
                user.messages.insert(outgoingMessage, atIndex: 0)
                user.outgoing.append(outgoingMessage)
            }

        }
    }

I insert the new message at the first position in my Messages list so that this message will appear at the top of my feed table view. And I add it at the end of Outgoing, so that the potential earlier messages that are still to be sent were going to be sent first, and this new message is going to be sent last.

If I run the app right now, I can see this is already doing something. I can tap on the write message icon and type, “I just added some code in my project,” then we will see the message appear in the feed table.

The message is shown a little bit differently because the feed controller knows how to display messages that are to be sent, being sent, and ones that are already sent. Our message is never going to be sent, because we don’t have this part of the code yet, so I’m going to switch to the NetworkingController, and also add this part.

Observing for New Messages (4:55)

import Foundation
import RealmSwift

class NetworkingController {

    private let api: InboxlyAPI
    private let user: User
    private var subscription: NotificationToken?
    
    init(api: InboxlyAPI) {
        self.api = api

        let realm = try! Realm(configuration: RealmConfig.Main.configuration)
        user = realm.objects(User.self).first!
    }
    
    func sendMessage(message: Message) {
        self.api.postMessage(message) {[weak self] success, messageId in 
            if let realm = try? Realm(configuration: RealmConfig.Main.configuration),
               let message = realm.objectForPrimaryKey(Message.self, key: messageId),
               let user = self?.user
            {
                print("sent \(message.id) from user \(user.name)")
            }
        }
    }

}

NetworkingController is configured with a little bit of code to fetch Realm and the user. I’m going to add a notification block inside init to observe for newly inserted messages that will then invoke the sendMessage method and will send it off to the simulated API.

Just like in previous episodes, I’m going to switch over the changes parameter. In this case, I’m only going to be interested in the update case. And whenever there is inserted messages, I’m going to send them off to the send message method:

init(api: InboxlyAPI) {
        self.api = api

        let realm = try! Realm(configuration: RealmConfig.Main.configuration)
        user = realm.objects(User.self).first!

        subscription = user.outgoing.addNotificationBlock {[weak self] changes in
            switch changes {
            case .Update(let outgoing, _, let insertions, _):
                for message in insertions.map ({ outgoing[$0] }) {
                    self?.sendMessage(message)
                }
            default: break
            }
        }
    }

This is a very naive implementation to demonstrate the general structure of the messaging queue. In here, we’ll be listening for the .Update state of the outgoing message queue. Whenever there is newly inserted messages, we grab the insertions, which is an array of indexes. We are turning those indexes to the actual objects from the outgoing list object, and then we send them one by one to sendMessage. sendMessage will then try to call the API, and send them off to the simulated server.

Consuming Messages from the Queue (6:49)

Sending the messages is not the end of the story. We also need to consume the message object from the outgoing message queue, so that they are not being consumed once more:

func sendMessage(message: Message) {
    self.api.postMessage(message) {[weak self] success, messageId in 
        if let realm = try? Realm(configuration: RealmConfig.Main.configuration),
           let message = realm.objectForPrimaryKey(Message.self, key: messageId),
           let user = self?.user
        {
            print("sent \(message.id) from user \(user.name)")
        }
    }
}

Inside of sendMessage, instead of just printing, we are going to grab the index of the object that was passed, and remove it from the outgoing queue:

func sendMessage(message: Message) {
    self.api.postMessage(message) {[weak self] success, messageId in 
        if let realm = try? Realm(configuration: RealmConfig.Main.configuration),
           let message = realm.objectForPrimaryKey(Message.self, key: messageId),
           let user = self?.user
           let index = user.outgoing.indexOf(message)
        {
            print("sent \(message.id) from user \(user.name)")
        }
    }
}

Now, I have the index of the message, and I’m going to update the message object, and remove it from outgoing:

func sendMessage(message: Message) {

        self.api.postMessage(message) {[weak self] success, messageId in

            if let realm = try? Realm(configuration: RealmConfig.Main.configuration),
                let message = realm.objectForPrimaryKey(Message.self, key: messageId),
                let user = self?.user,
                let index = user.outgoing.indexOf(message)
            {
                print("sent \(message.id) from user \(user.name)")
                try! realm.write {
                    message.outgoing = false
                    user.outgoing.removeAtIndex(index)
                }
            }
        }

    }

This example is a very simple implementation of a message queue. We updated the message so that it will update the UI. It will show the messages as being sent already, and this removes it from outgoing. And our work is done.

Conclusion (7:52)

If we test our app one more time, we’ll see how we can insert a message and whenever the job is done, you can see the UI being updated. That’s pretty much everything for this episode, and I hope that you enjoyed this video series!

Resources


Marin Todorov

Marin Todorov

Marin Todorov is an independent iOS consultant and publisher. He’s co-author on the book "RxSwift: Reactive programming with Swift" the author of “iOS Animations by Tutorials”. He's part of Realm and raywenderlich.com. Besides crafting code, Marin also enjoys blogging, writing books, teaching, and speaking. He sometimes open sources his code. He walked the way to Santiago.