Multicast on Swift 3 and MVVM-C

I was working on an iOS project written entirely on Swift 3, and it came to a point in the project where writting a Mutlicast class seemed like the right solution. There are some examples out there for other swift versions but nothing for Swift 3 and using MVVM-C. So I decided to write a solution and now I’m sharing this with you.

Few things before we continue. In the particular project I was working on I decided that it was a good approach to have a way to broadcast to different parts of my app when something changed in the a Model. This way each ViewController added to a UITabBarController will get notified about this changes and then update the app accordingly. The final result will allow for one ViewModel to communicate to all the other ViewModels that a change happen (due to a network update, or user input) in our Model. Also I’m using the MVVM-C pattern. As you will see next I will take this approach and try to show in detail how I implemented my solution. I’m aiming to provide a full solution and examples of how to use this Multicast approach.

First of all I will show how I implemented my Multicast class, if you whish to continue reading this article I will simply provide examples of how I used it in my app.

MulticastDelegate

A few things to mention about MulticastDelegate class. This is where we keep an array of multicast delegates of type Weak. This is important because we want to avoid any memory cycles in our app thus in our Weak class we have a weak reference to some AnyObject that we want to keep in our delegates array.

Also notice the use of generics in MulticastDelegate and AnyObject in our Weak class. We want to keep our implementation as flexible as possible in order for us to reuse this implementation as we wish in the future. Reusing code is always a plus, and we avoid duplicating code. This is a great feature in Swift.

ModelDelegate Protocol

As I mentioned eariler the use of generics is important in our implementation in order to have a flexible solution and avoid duplication. Now we have to define some type that we are going to pass to the array in our MulticastDelegate. This will give us type safety and needed structure in order for us to know what is possible to do between all the ViewModels.

This is the protocol that each ViewModel is going to extend in order to communicate and broadcast to other ViewModels any changes in the Model. Such changes could be due to network updates or user ipunts.

Model

Now it is time to reveal our Model. This is our class where we keep the data. Also any changes that occur to this data will then invoke all ModelDelegates (in our case these are ViewModels), and use any of the functions defined in our ModelDelegate Protocol to communicate such changes.

Now we can see how everything is coming together. This PlacesModel is keeping track of Place objects and a CLLocation object.

var delegates = MulticastDelegate<PlacesModelDelegate>()

This is where we specify the type that our delegates variable will accept. Later in the article I will show how our ViewModels will extend PlacesModelDelegate protocol that appears here.

Our ViewModels are going to update this Model due to some action done by the user, or some network update requested by the app.

var places: [Place] = [] { 
didSet {
delegates.invoke { [weak self]
vm in if let places = self?.places {
vm.didChange(places: places)
}
}
}
}

This is an example of what the ViewModels are updating and how we use broadcast to all ViewModels that something happen.

There are a few things happening here. When a ViewModel updates places array we use didSet to call all PlacesModelDelegates. We pass a closure to our function invoke that specifies what action to take according to our protocol that we defined earlier. In this case we want to inform all PlacesModelDelegates (ViewModels) that a change happened in places array. We call didChange(places:) on each PlacesModelDelegate and we pass the new Place array.

Similarly we apply the same logic to the other data in our Model. Plus our Model do other data related functions which I am skipping here for clarity.

ViewModel

Now it is time for ViewModel to shine. Each UIViewController (our View in MVVM) that we create will have a reference to a ViewModel. Thus any actions taken by the user or network requests logic will start by the user on the View or it will start in the ViewModel itself. Ultimately the ViewModel will be in charge to talk to our Model and View.

I will now show you how our ViewModel was implemented. I will use one ViewModel as an example but you can imagine that it will be a similar approach for the other ViewModels. You can implement as many ViewModels as you need in your project and as many UIViewControllers.

First of all we have to see that our ViewModel is extending PlacesModelDelegate. Extensions are another great feature in Swift. I allows us to have a more clean and organized code.

init(placesModel: PlacesModel) { 
self.placesModel = placesModel
super.init()
placesModel.delegates.add(self)
//initialize more objects here…
}

We made sure we initialize our Model earlier and we pass a reference to it to our ViewModel. Also in this init is where we add our ViewModel to our delegates array (MulticastDelegate<PlacesModelDelegate>) in the Model. This is done by calling:

placesModel.delegates.add(self)

Now I will explain one scenario where you can see how to use Multicast.

The scenario is the following: The user taps on a marker on the map and we want to either show a bottom card with details about this location or dismiss the bottom card.

If you read again our ViewModel you can see that I left a function called tappedOn.

func tappedOn(placeId id: Int) {
findAndShow(placeId: id)
broadcastSelectedItem(id: id)
}

This function is called by our View (UIViewController) and it happens when the user taps on a marker on the map. This will do some work in order to find the necessary data but we want to focus that here is also where we start to modify our Model when we call broadcastSelectedItem.

func broadcastSelectedItem(id: Int?) {
placesModel.selectedItem = id
}

The app handles many other tap scenarios but eventually each tap action will update the model and call this function above. This is where we update the model. It is as simple as calling the respective property in our object and pass the new value.

This will invoke all our ViewModels that were added to our PlacesModel and each ViewModel will then call the function didSelectItem.

The block of code that does this in PlacesModel is the following:

var selectedItem: Int? {
didSet {
delegates.invoke { [weak self] vm in
vm.didSelectItem(self?.selectedItem)
}
}
}

And the function that our MapViewModel implements looks like this:

func didSelectItem(_ id: Int?) {
guard let placeId = id else {
viewDelegate?.hideBottomCard()
return
}
findAndShow(placeId: placeId)
guard let index = placesModel.findArrayIndex(by: placeId),
let placeCoordinate = placesModel.getCoordinates(of: placeId) else
{
return
}
viewDelegate?.setMarker(at: index)
viewDelegate?.moveCamera(to: placeCoordinate, animate: true)
}

There is a very important detail you need to have in mind. Because when we call broadcastSelectedItem() it will call ALL delegates including the calling delegate. In this case MapViewModel delegate. Thus didSelectItem in our ViewModel will be called we have to restrain any UI updates to this didSelectItem function. Otherwise if you start updating the UI earlier you might get unexpected behaviors because didSelectItem will be called as well in this current ViewModel. So be careful where you put your UI updates and other logic.

When we call broadcastSelectedItem each ViewModel will receive a call on didSelectItem. Each ViewModel implements its own didSelectItem which will modify their respective View (UIViewController) as they see fit.

Conclusion

Multicast is a simple way to enable communication between ViewModels and the benefits show when you have data updates that need to be notified across your app by multiple ViewModels. I hope you enjoyed this explanation and also find it useful for your projects.