Skip to content

Instantly share code, notes, and snippets.

@felginep
Created January 18, 2019 09:05
Show Gist options
  • Star 64 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save felginep/0148b40e26b19d07e81c2e1e4f2ff3d2 to your computer and use it in GitHub Desktop.
Save felginep/0148b40e26b19d07e81c2e1e4f2ff3d2 to your computer and use it in GitHub Desktop.
import Foundation
import UIKit
struct ViewStyle<T> {
let style: (T) -> Void
}
let filled = ViewStyle<UIButton> {
$0.setTitleColor(.white, for: .normal)
$0.backgroundColor = .red
}
let rounded = ViewStyle<UIButton> {
$0.layer.cornerRadius = 4.0
}
extension ViewStyle {
func compose(with style: ViewStyle<T>) -> ViewStyle<T> {
return ViewStyle<T> {
self.style($0)
style.style($0)
}
}
}
let roundedAndFilled = rounded.compose(with: filled)
extension ViewStyle where T: UIButton {
static var filled: ViewStyle<UIButton> {
return ViewStyle<UIButton> {
$0.setTitleColor(.white, for: .normal)
$0.backgroundColor = .red
}
}
static var rounded: ViewStyle<UIButton> {
return ViewStyle<UIButton> {
$0.layer.cornerRadius = 4.0
}
}
static var roundedAndFilled: ViewStyle<UIButton> {
return rounded.compose(with: filled)
}
}
func style<T>(_ object: T, with style: ViewStyle<T>) {
style.style(object)
}
protocol Stylable {
init()
}
extension UIView: Stylable {}
extension Stylable {
init(style: ViewStyle<Self>) {
self.init()
apply(style)
}
func apply(_ style: ViewStyle<Self>) {
style.style(self)
}
}
let button = UIButton(style: .roundedAndFilled)
button.setTitle("My Button", for: .normal)
button.sizeToFit()
button
@rbresjer
Copy link

rbresjer commented Mar 2, 2019

Nice idea!

If you would return ‘self’ from the ‘style:’ method, wouldn’t that make composing easier? I’m on mobile now so can’t verify, but e.g.:

‘‘‘
struct ViewStyle {
let style: (T) -> T
}

extension Stylable {
init(style: ViewStyle) {
self.init()
apply(style)
}
func apply(_ style: ViewStyle) -> Self {
return style.style(self)
}
}
// Then you could do e.g.:
button
.apply(.rounded)
.apply(.filled)
‘‘‘

That would remove the need for the compose method, as well as the need for predefined composed styles.

@erickva
Copy link

erickva commented Mar 4, 2019

This is a very good idea @rbresjer, we would also have to return inside the static property, something like:

static var rounded: ViewStyle<UIButton> {
        return ViewStyle<UIButton> {
            $0.layer.cornerRadius = 4.0
            return $0
        }
    }

static var fullyRounded: ViewStyle<UIButton> {
        return ViewStyle<UIButton> {
            $0.layer.cornerRadius = $0.frame.height / 2
            return $0
        }
    }

I would also add a ready method without any return inside the Stylable extension, or something on these lines so it will stop complaining that the return is not being used:

extension Stylable {
    
    func apply(_ style: ViewStyle<Self>) -> Self {
        return style.style(self)
    }
    
    func ready() {}
}

So you can do:

class ViewController: UIViewController {

    @IBOutlet weak var button: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        button
            .apply(.filled)
            .apply(.fullyRounded)
            .ready()   
    }
}

@joamafer
Copy link

joamafer commented Mar 4, 2019

@erickva I think you can use @discardableResult for that. In that way you wouldn't need to add a ready function, which doesn't add any value.

Also, there's no need to import both Foundation and UIKit. UIKit is enough. Read this for more info:
https://stackoverflow.com/a/30181218

@cpatterson-lilly
Copy link

cpatterson-lilly commented Mar 4, 2019

What if I want to parameterize the ViewStyle<UIButton>.rounded style? Couldn't functions be used as well?

static func rounded(_ radius: Double) -> ViewStyle<UIButton> {
    return ViewStyle<UIButton> {
        $0.layer.cornerRadius = radius
        return $0
    }
}

Resulting in:

class ViewController: UIViewController {

    @IBOutlet weak var button: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        button
            .apply(.filled)
            .apply(.rounded(10.0))
    }
}

@IanKeen
Copy link

IanKeen commented Mar 5, 2019

I've built something similar in the past:
https://gist.github.com/IanKeen/ac051da0eeeaefcae9cbb8c1e4bb9c1b

You might like the styleAll variant which uses the appearance proxy

@seanrucker
Copy link

seanrucker commented Mar 8, 2019

How could you allow composing ViewStyles for UIView subclasses with ViewStyles for UIView?

extension ViewStyle {
    static var buttonStyle: ViewStyle<UIButton> {
        return ViewStyle<UIButton> {
            $0.titleLabel?.font = UIFont.systemFont(ofSize: 15)
        }
    }
    
    static var roundedAndFilledButton: ViewStyle<UIButton> {
        // This doesn't work because 'roundedAndFilled' returns a 'ViewStyle<UIView>' and buttonStyle is expecting 'ViewStyle<UIButton>'
        return buttonStyle.compose(with: .roundedAndFilled)
    }
}

@ConEliopoulos
Copy link

I like the idea of overloading the + operator

extension ViewStyle {

    static func +(left: ViewStyle<T>, right: ViewStyle<T>) -> ViewStyle<T> {
        return ViewStyle<T> {
            left.style($0)
            right.style($0)
            return $0
        }
    }
}

So then you can do

let button = UIButton(style: rounded + filled)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment