‘Functional’ Swift #6: View styling

Andy Bennett
5 min readMar 28, 2018

--

What else can we do? Lets think about another common pattern we see in our code …

class MyViewController: UIViewController
{
let myBtn = UIButton()
let otherBtn = UIButton()
override func viewDidLoad()
{
super.viewDidLoad()
myBtn.backgroundColor = .white
myBtn.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
myBtn.isUserInteractionEnabled = false
otherBtn.backgroundColor = .blue
otherBtn.frame = CGRect(x: 120, y: 10, width: 100, height: 100)
otherBtn.isUserInteractionEnabled = true
}
}

Particularly in codebases that don’t use nibs or storyboards, this is very common, but it seems very awkward that the initial styling of the views (in this case the 2 buttons) is disconnected from their initialisation.

We could do something like this:-

class MyViewController: UIViewController
{
let myBtn = { () -> UIButton in
let button = UIButton()
button.backgroundColor = .white
button.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
button.isUserInteractionEnabled = false
return button
}()

let otherBtn = { () -> UIButton in
let button = UIButton()
button.backgroundColor = .blue
button.frame = CGRect(x: 120, y: 10, width: 100, height: 100)
button.isUserInteractionEnabled = true
return button
}()
override func viewDidLoad()
{
super.viewDidLoad()
}
}

… but then the top of the file gets loaded down with all of that styling code. How about this?

class MyViewController: UIViewController
{
let myBtn = UIButton().configureMyButton()
let otherBtn = UIButton().configureOtherButton()
override func viewDidLoad()
{
super.viewDidLoad()
}
}
fileprivate extension UIButton
{
func configureMyButton()
{
self.backgroundColor = .white
self.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
self.isUserInteractionEnabled = false
}
func configureOtherButton()
{
self.backgroundColor = .blue
self.frame = CGRect(x: 120, y: 10, width: 100, height: 100)
self.isUserInteractionEnabled = true
}
}

This is much better IMO, because the styling is kept encapsulated, and the view controller code is kept clean … but all of these specific properties could be applied to any UIView subclass, so is there a way of expressing this with more reusability? Plus, if I have the same buttons across a few view controllers, then I’ll have to make the extension internal or public, and that feels like polluting the namespace. Obviously we could subclass UIButton to do the same thing, but if those styles could be applied to for example a UILabel and UIButton, subclassing is no use.

What about protocols?

class MyViewController: UIViewController
{
let myBtn = UIButton().configureStyle1()
let otherBtn = UIButton().configureStyle2()
override func viewDidLoad()
{
super.viewDidLoad()
}
}
protocol StyleConfigurable {}extension StyleConfigurable where Self: UIView
{
func configureStyle1() -> UIView
{
self.backgroundColor = .white
self.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
self.isUserInteractionEnabled = false
return self
}
func configureStyle2() -> UIView
{
self.backgroundColor = .blue
self.frame = CGRect(x: 120, y: 10, width: 100, height: 100)
self.isUserInteractionEnabled = true
return self
}
}
extension UIView: StyleConfigurable {}

There’s something to be said for this approach; the StyleConfigurable protocol is being applied to any UIView or subclass; or could be extended with specific functions only for certain subclasses. One thing I don’t love is that you have to explicitly return self at the end of the configure functions.

How about this then …

struct ViewStyle
{
static func style1(_ view: UIView)
{
view.backgroundColor = .white
view.frame = CGRect(x: 10, y: 10, width: 100, height: 100)
view.isUserInteractionEnabled = false
}
static func style2(_ view: UIView)
{
view.backgroundColor = .blue
view.frame = CGRect(x: 120, y: 10, width: 100, height: 100)
view.isUserInteractionEnabled = true
}
}
extension UIView
{
func apply(_ fnc: (UIView) -> Void) -> UIView
{
fnc(self); return self
}
}
class MyViewController: UIViewController
{
let myBtn = UIButton().apply(ViewStyle.style1)
let otherBtn = UIButton().apply(ViewStyle.style2)
override func viewDidLoad()
{
super.viewDidLoad()
}
}

Okay, now that’s getting somewhere; the styles are broken out into a reusable format, and they’re applied to the buttons on initialisation. We can also break the ViewStyles struct and UIView extension out into separate files.

Hold on … apply … that sounds familiar! Previously we had defined a custom apply operator |>

func |> <A, B>(a: A, f: (A) -> B) -> B
{
return f(a)
}

How about we wrap our styling functions in a little helper, then they’d fit that form:-

func applyable<T>(_ fnc: @escaping (T) -> Void) -> (T) -> T
{
return { fnc($0); return $0 }
}

Now we can do this:-

class MyViewController: UIViewController
{
let myBtn = UIButton() |> applyable(ViewStyle.style1)
let otherBtn = UIButton() |> applyable(ViewStyle.style2)
override func viewDidLoad()
{
super.viewDidLoad()
}
}

This might seem like minor change, but now with the applyable function, we’ve made it generic over any type. Can we go further?

We could break out the individual style functions (CGRect details omitted for clarity) using the >>> function composition operator we defined before:-

struct ViewStyle
{
typealias Styling = (UIView) -> UIView
static let style1: Styling
= applyable { $0.backgroundColor = .white }
>>> applyable { $0.frame = CGRect(......) }
>>> applyable { $0.isUserInteractionEnabled = false }
static let style2: Styling
= applyable { $0.backgroundColor = .blue }
>>> applyable { $0.frame = CGRect(......) }
>>> applyable { $0.isUserInteractionEnabled = true }
}
class MyViewController: UIViewController
{
let myBtn = UIButton() |> ViewStyle.style1
let otherBtn = UIButton() |> ViewStyle.style2
override func viewDidLoad()
{
super.viewDidLoad()
}
}

So, now our view controller is unaware of the applyable function, and is purely using the same apply operator |> as we’ve used before. We could also define the styles as functions that take additional parameters …

struct ViewStyle
{
typealias Styling = (UIView) -> UIView
static func style(color: UIColor,
frame: CGRect,
enabled: Bool) -> Styling
{
return {
$0 |> applyable { $0.backgroundColor = color}
>>> applyable { $0.frame = frame }
>>> applyable { $0.isUserInteractionEnabled = enabled }
}
}
static let style2: Styling {
return style(color: .blue, frame: ....)
}
}
class MyViewController: UIViewController
{
let myBtn = UIButton() |> ViewStyle.style(color: .white, frame: ...)
let otherBtn = UIButton() |> ViewStyle.style2
override func viewDidLoad()
{
super.viewDidLoad()
}
}

I’ve shown a couple of alternatives here —passing the parameters directly or creating a simple wrapper that does the passing, and returns the style.

Of course, this is just one approach to this whole area; we could combine the ViewStyle struct with a protocol based apply for instance.

Gist for complete code for Xcode 9.3 playground

--

--