Understanding and Implementing State Changes in iOS

Category Product engineering

Today morning I opened the Apple’s music app to soothe my ears with some songs. But of course, as an iOS developer, I was more drawn to examine the functionality of the app instead. What I found was the subtle ways in which the app handles state changes in iOS.

Content, Loading, Error and Empty states

So, it drew me to find the best ways in which we could handle network states in our applications.
Let’s say we’re building an app which manages four states, namely:
1. Content: The data is presented to the user
2. Loading: The data is being loaded over network
3. Error: Error encountered while loading data over network
4. Empty: No data available to be displayed to the user
The questions which arises are: Is my code reusable? Do I follow the DRY principle? What if I have different methods to manage states in different classes? Is code refactoring easy?
If your answer is ‘No’, you can refer to the ‘The Solution’ or else have your spaghetti.

The Solution

At WWDC 2015, Apple introduced Protocol-Oriented Programming. With it came the most powerful feature: Protocol Extensions.
Wait, what did you say? Now, what the hell is protocol extension. If you are new to the concept, please refer to the links:
https://www.raizlabs.com/dev/2015/06/protocol-extensions-swift-2-0/
http://machinethink.net/blog/mixins-and-traits-in-swift-2.0/
http://cutting.io/posts/stateful-mixins-in-swift/

Let’s start with Protocol

We have a protocol named ViewStateProtocol:

 protocol ViewStateProtocol: class {
 var stateManager: StateManager? { get }
  
 var loadingView: UIView? { get }
 var errorView: UIView? { get }
 var emptyView: UIView? { get }
  
 var errorMessage: String? { get set }
 func addView(withState state: StatesType)
 }

Let’s talk about it. We have a state manager class instance (class which manages adding and removing views), loading, error and empty views, an error message and a method declaration addView which takes States Type as a parameter(an enum containing the various states)

 enum StatesType: String {
 case error = "error"
 case empty = "empty"
 case loading = "loading"
 case none = "none"
 }

Now, let’s have some magic with protocol extensions.
First, we created a single instance of State manager class which takes care of adding and removing views. Then, we create loading, error and empty view objects.

 extension ViewStateProtocol where Self: UIViewController {
  
 // State manager class to remove/add views
 var stateManager: StateManager? {
 return StateManager.sharedInstance
 }
  
 // Loading view
 var loadingView: UIView? {
 return LoadingView(frame: UIScreen.main.bounds)
 }
  
 // Error View
 var errorView: UIView? {
 return ErrorState(frame: UIScreen.main.bounds)
 }
  
 // Empty view
 var emptyView: UIView? {
 return EmptyStateView(frame: UIScreen.main.bounds)
 }
 }

Awesome! But, we still need to add these views to our view controller’s view. So, how to do that? Not a problem, we have state managers to our rescue. State Manager class will take care of adding these views.

 extension ViewStateProtocol where Self: UIViewController {
 ..........
 // Manages and adds different views on the basis of the state
 func addView(withState state: StatesType) {
  
 // error state, empty state & loading state
 switch state {
 case .loading:
 // calls state manager to add a laoding view
 stateManager?.addView(loadingView!, forState: StatesType.loading.rawValue, superview: view)
 case .error:
 // calls state manager to add an error view
 stateManager?.addView(errorView!, forState: StatesType.error.rawValue, superview: view)
 case .empty:
 // calls state manager to add an empty view
 stateManager?.addView(emptyView!, forState: StatesType.empty.rawValue, superview: view)
 default:
 // removes all the views for managing states
 removeAllViews()
 }
 }
 }

Yay! We did it. But wait, where’s our State Manager class. Let’s have a look at it too…

 class StateManager {
  
 static let sharedInstance = StateManager()
 var viewStore: [String: UIView] = [:]
  
 // Associates a view for the given state
 public func addView(_ view: UIView, forState state: String, superview: UIView) {
 viewStore[state] = view
 superview.addSubview(view)
 }
  
 // Remove all views
 public func removeAllViews() {
 for (_, view) in self.viewStore {
 view.removeFromSuperview()
 viewStore = [:]
 }
 }
 }

So, we’ve the default implementation for all the views. But how do we implement it? Ever wondered?

Implementation of protocol

Our view controller is going to implement the ViewStateProtocol and call the methods whenever it needs to display the views.

 class StateViewController: UIViewController {}
  
 extension StateViewController: ViewStateProtocol {
 @objc func handleTap(_ sender: UIView) {
 // for showing the loader
 addView(withState: .loading)
  
 // for showing the error message
 addView(withState: .error)
  
 // for showing the empty results message label
 addView(withState: .empty)
  
 // for removing all the views
 removeAllViews()
 }
 }

Any view controller that is concerned with managing network related states can implement the ViewStateProtocol and reuse all the code. As simple as this ?

Wrap up

Protocol Extensions allow us to have mixin like pattern. Its advantageous for code reusability and maintainability.

This github repository has a demo application

Ready to embark on a transformative journey? Connect with our experts and fuel your growth today!