Thursday, 5 May 2016

Type-safe styles in Swift

TL;DR - By leveraging generics it's possible to create type-safe state values for the different states of a view
Views in a application will be styled and branded and looking slightly differently depending on the state they are in. For example, for a button that is disabled the label may be greyed out and the shadow removed. Sometimes even the font could change when the state changes. We can leverage generics in Swift to encapsulate this concept.
The first thing to do is to create a generic struct that will encapsulate the values for the different states. Note that this struct will be specialised with anything relevant for a state, e.g. UIColorUIFont, etc. In the following struct I've only defined 3 states, normalhighlighted and disabled but there could as many or as few as it's needed since they are optional.
struct StateValue<T> {
  
  private let normal: T?
  private let highlighted: T?
  private let disabled: T?
  
  init(normal: T? = nil, highlighted: T? = nil, disabled: T? = nil) {
    self.normal = normal
    self.highlighted = highlighted
    self.disabled = disabled
  }
  
}
To be able to improve how the StateValue struct is used I created an enum that represents some of the states a view could be in. This list matches the previous states thought it doesn't necessarily have to. Again, it's certainly not an exhaustive list, it only covers the relevant states for this example.
enum ViewState {
  case Normal
  case Highlighted
  case Disabled
}
We can then extend the StateValue so that we can access the value for a specific ViewState. I have used a subscript for this because it provides a cleared interface than a function.
extension StateValue {
  
  subscript (state: ViewState) -> T? {
    switch state {
    case .Normal:
      return normal
    case .Highlighted:
      return highlighted
    case .Disabled:
      return disabled
    }
  }
  
}
Now that all the building blocks are defined we can use them to extend any views we would want. For example, UIButton could be extended to have a method that will set the colour of the title for all states.
extension UIButton {
  
  func new_setTitleColor(stateValue: StateValue<UIColor>) {
    setTitleColor(stateValue[.Normal], forState: .Normal)
    setTitleColor(stateValue[.Highlighted], forState: .Highlighted)
    setTitleColor(stateValue[.Disabled], forState: .Disabled)
  }
  
}
Putting all this together into a playground and using the new interactive playgrounds for the button it's possible to show this in action.
import UIKit
import XCPlayground

class Target : NSObject {
  func action() {
    print("Button pressed!")
  }
}

let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
containerView.backgroundColor = .whiteColor()
XCPlaygroundPage.currentPage.liveView = containerView
let target = Target()

let button = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
button.backgroundColor = .lightGrayColor()
button.setTitle("Some button", forState: .Normal)
button.addTarget(target, action: #selector(Target.action), forControlEvents: .TouchUpInside)
containerView.addSubview(button)

let buttonState: StateValue<UIColor> = StateValue(normal: .purpleColor(), highlighted: .greenColor(), disabled: .redColor())
button.new_setTitleColor(buttonState)
// button.enabled = false // This would make the button red
The previous example is one of the ways the StateValue can be used. Equally this could be used for encapsulating the themes of different types in your application. This is a lot clearer with an example. Let's say we have an Vehicle enum:
enum Vehicle {
  case Car
  case Airplane
}
Let's say in our application the button for a Car looks different than the button from a Airplane and they are both displayed in some form of dynamic view (e.g. a UITableView of many Cars and Airplanes). To style each of the buttons of the cell we could extend Vehicle using a new Stylable protocol and define the StateValue for each of the Vehicle. Note that the Stylable protocol is not strictly needed, it's just a way to have named extensions that are clear to read and make our code more generic and reusable.
protocol Stylable {
  var buttonColor: StateValue<UIColor> { get }
}

extension Vehicle : Stylable {
  var buttonColor: StateValue<UIColor>  {
    switch self {
    case .Car:
      return StateValue(normal: .yellowColor(), highlighted: .blueColor(), disabled: .grayColor())
    case .Airplane:
      return StateValue(normal: .purpleColor(), highlighted: .greenColor(), disabled: .redColor())
    }
  }
}
The buttonColor on a Vehicle could be used in the following way (Bare in mind this is quick example to show the concept in action). 
let vehicleButton = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))

func setTitleColorForStylableItem(stylable: Stylable) {
  vehicleButton.new_setTitleColor(stylable.buttonColor)
}

let car = Vehicle.Car
setTitleColorForStylableItem(car)
All in all, if you have a lot of styling in your app for the various states and types of object using a generic StateValue struct is a great way to encapsulate all this logic. One of the advantages is that because it's generic it can be used for anything relevant to your style as mentioned originally; UIColors, UIFonts, UIImage, etc. I believe this is a great pattern that can be extended in way that will suit your needs very easily.

0 comments:

Post a Comment