Applying the State Pattern in Swift

Vel Yanchina
3 min readDec 26, 2016

Like a 🤘🏼⭐️

It is common for apps to support multiple states.

What this post suggests is an approach which avoids large conditional statements by applying the State Pattern.

Let’s look at some sample code. It implements a navigation bar that updates to reflect user’s authentication state.

State Machine

//1
fileprivate protocol Statelike {
var stateMachine: StateMachine { get }
func logIn()
func logOut()
}
//2
extension Statelike {
func logIn() {}
func logOut() {}
}
//3
fileprivate struct LoggedIn: Statelike {
var stateMachine: StateMachine

func logOut() {
stateMachine.setState(LoggedOut(stateMachine: stateMachine))
stateMachine.loggedOut?()
}
}
//4
fileprivate struct LoggedOut: Statelike {
var stateMachine: StateMachine

func logIn() {
stateMachine.setState(LoggedIn(stateMachine: stateMachine))
stateMachine.loggedIn?()
}
}
//5
class StateMachine {
typealias StateHandler = () -> ()
var loggedIn: StateHandler?
var loggedOut: StateHandler?

private lazy var state: Statelike = {
return LoggedOut(stateMachine: self)
}()

@objc func logIn() {
state.logIn()
}

@objc func logOut() {
state.logOut()
}

fileprivate func setState(_ state: Statelike) {
self.state = state
}
}
  1. Define an interface that each state must implement.
  2. The default protocol implementation is empty. This extension pretty much makes all methods optional.
  3. LoggedIn is a stateless implementation (struct) of the Statelike protocol which represents logged in state. 😵 All it actually does is to update the state of the StateMachine and invoke the block responsible for handling the transition. It could, of course, make a network request, etc.
  4. LoggedOut is analogous to LoggedIn.
  5. Finally the StateMachine provides a mechanism for triggering logIn()/logOut() behavior on it’s internal state.

Statefulness

Now that I’ve created this awesome machinery 🤖, let’s see how to make use of it.

//1
protocol Stateful {
func loggedIn()
func loggedOut()
}
//2
extension UINavigationItem: Stateful {
func loggedIn() {
rightBarButtonItem = UIBarButtonItem(title: "Logout", style: .plain, target: nil, action: nil)
}

func loggedOut() {
rightBarButtonItem = UIBarButtonItem(title: "Login", style: .plain, target: nil, action: nil)
}
}
//3
extension UIViewController: Stateful {
func loggedIn() {
navigationItem.loggedIn()
}

func loggedOut() {
navigationItem.loggedOut()
}
}
  1. The requirements for every Stateful object are defined.
  2. I’ve turned every NavigationItem into a Stateful object. Crusty would be proud of me! 😎 The protocol implementation simply sets a rightBarButtonItem appropriate for the user’s authentication state.
  3. What a Stateful UIViewController does by default is to invoke the respective methods on its navigation item.

The rest is easy.


extension UIBarButtonItem {
func addTarget(_ target: AnyObject?, action: Selector){
self.target = target
self.action = action
}
}
class ViewController: UIViewController {
//1
var stateMachine = StateMachine()

override func viewDidLoad() {
super.viewDidLoad()

//3
stateMachine.loggedIn = { self.loggedIn() }
stateMachine.loggedOut = { self.loggedOut() }
//4
stateMachine.logIn()
}
}
//2
extension ViewController {
override func loggedIn() {
super.loggedIn()
self.navigationItem.rightBarButtonItem?.addTarget(self.stateMachine, action: #selector(StateMachine.logOut))
}

override func loggedOut() {
super.loggedOut()
self.navigationItem.rightBarButtonItem?.addTarget(self.stateMachine, action: #selector(StateMachine.logIn))
}
}
  1. An instance of the StateMachine is needed.
  2. I’ve extended ViewController to override the default Stateful protocol implementation. The rightBarButtonItem now has the stateMachine as its target and triggers the correct action.
  3. A handler for a state I am interested in simply invokes the appropriate Stateful protocol method.
  4. Calling logIn gets the stateMachine going. 🚀

I realize this seems like an overkill for the use case above. After all, there are only two states and one item to update.

I also believe this solution increases maintainability and testability by separating logic in a neat way. And hey it allows us to easily support multiple states without a single conditional statement. How awesome is that! 😎

A Gist is available. Thanks for reading! 🤗

--

--