Understanding and Implementing State Changes in iOS
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 statesSo, 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 user2. Loading: The data is being loaded over network3. Error: Error encountered while loading data over network4. Empty: No data available to be displayed to the userThe 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 SolutionAt 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 ProtocolWe 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) }view rawViewStateProtocol.swift hosted with ❤ by GitHubLet’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" }view rawStatesType.swift hosted with ❤ by GitHubNow, 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) } }view rawViewStateProtocolExtension.swift hosted with ❤ by GitHubAwesome! 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() } } }view rawViewStateProtocolExtension_AddViews.swift hosted with ❤ by GitHubYay! 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 = [:] } } }view rawStateManager.swift hosted with ❤ by GitHubSo, we’ve the default implementation for all the views. But how do we implement it? Ever wondered?Implementation of protocolOur 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() } }view rawStateViewController.swift hosted with ❤ by GitHubAny view controller that is concerned with managing network related states can implement the ViewStateProtocol and reuse all the code. As simple as this 🙂Wrap upProtocol Extensions allow us to have mixin like pattern. Its advantageous for code reusability and maintainability.This github repository has a demo application
Learn More >