Created
November 11, 2016 02:05
-
-
Save owenv/56017d4702464fe33e5a79bfb1315c4d to your computer and use it in GitHub Desktop.
Composable Layout Objects in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// CustomLayout.swift | |
// LayoutKit | |
// | |
// Created by Owen Voorhees on 10/6/16. | |
// Copyright © 2016 Owen Voorhees. All rights reserved. | |
// | |
//Lays out children using a custom set of constraints | |
public class CustomLayout: Layout { | |
public var root: Constrainable = LayoutGuide() | |
public var children: [Constrainable] = [] | |
private var constraintGenerator: (Constrainable, [Constrainable])->(Set<NSLayoutConstraint>) | |
private var constraints: Set<NSLayoutConstraint> = [] | |
public init(children: [Constrainable], constraintGenerator: @escaping (Constrainable, [Constrainable])->(Set<NSLayoutConstraint>)) { | |
self.children = children | |
self.constraintGenerator = constraintGenerator | |
} | |
public func layoutChildren() { | |
NSLayoutConstraint.deactivate(Array(constraints)) | |
let newConstraints = constraintGenerator(self, children) | |
constraints.formIntersection(newConstraints) | |
constraints.formUnion(newConstraints) | |
constraints.forEach({$0.isActive = true}) | |
NSLayoutConstraint.activate(Array(constraints)) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ViewController: UIViewController { | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
let v1 = UIView(color: .red) | |
let v2 = UIView(color: .green) | |
let v3 = UIView(color: .blue) | |
let v4 = UIView(color: .purple) | |
let padding = PaddedLayout(child: StackLayout(children: [v2, PaddedLayout(child: v3, padding: 50), v4], axis: .vertical), padding: 30.0) | |
let layout = CustomLayout(children: [v1, padding], constraintGenerator: { | |
parent, items in | |
let v1 = items[0] | |
let padded = items[1] | |
return [ | |
v1.leftAnchor.constraint(equalTo: parent.leftAnchor), | |
v1.topAnchor.constraint(equalTo: parent.topAnchor), | |
v1.bottomAnchor.constraint(equalTo: parent.bottomAnchor), | |
v1.widthAnchor.constraint(equalTo: parent.widthAnchor, multiplier: 0.5), | |
padded.leftAnchor.constraint(equalTo: v1.rightAnchor), | |
padded.rightAnchor.constraint(equalTo: parent.rightAnchor), | |
padded.topAnchor.constraint(equalTo: parent.topAnchor), | |
padded.bottomAnchor.constraint(equalTo: parent.bottomAnchor) | |
] | |
}) | |
layout.fill(view: view) | |
} | |
} | |
public extension View { | |
convenience init(color: Color) { | |
self.init() | |
#if os(macOS) | |
wantsLayer = true | |
layer.backgroundColor = color.cgColor | |
#elseif os(iOS) | |
backgroundColor = color | |
#endif | |
} | |
func addBorder() { | |
#if os(macOS) | |
wantsLayer = true | |
#endif | |
layer.borderWidth = 4.0 | |
layer.borderColor = UIColor.black.cgColor | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Layout.swift | |
// LayoutKit | |
// | |
// Created by Owen Voorhees on 10/3/16. | |
// Copyright © 2016 Owen Voorhees. All rights reserved. | |
// | |
#if os(macOS) | |
import AppKit | |
public typealias View = NSView | |
public typealias Color = NSColor | |
public typealias LayoutGuide = NSLayoutGuide | |
#elseif os(iOS) | |
import UIKit | |
public typealias View = UIView | |
public typealias Color = UIColor | |
public typealias LayoutGuide = UILayoutGuide | |
#endif | |
public protocol Constrainable { //Anything with autolayout anchors that can be added to a view (views, layout guides) | |
var bottomAnchor: NSLayoutYAxisAnchor {get} | |
var centerXAnchor: NSLayoutXAxisAnchor {get} | |
var centerYAnchor: NSLayoutYAxisAnchor {get} | |
var heightAnchor: NSLayoutDimension {get} | |
var leadingAnchor: NSLayoutXAxisAnchor {get} | |
var leftAnchor: NSLayoutXAxisAnchor {get} | |
var rightAnchor: NSLayoutXAxisAnchor {get} | |
var topAnchor: NSLayoutYAxisAnchor {get} | |
var trailingAnchor: NSLayoutXAxisAnchor {get} | |
var widthAnchor: NSLayoutDimension {get} | |
func addTo(view: View) | |
func removeFromContainer() | |
} | |
extension View: Constrainable { | |
public func addTo(view: View) { | |
view.addSubview(self) | |
} | |
public func removeFromContainer() { | |
removeFromSuperview() | |
} | |
} | |
extension LayoutGuide: Constrainable { | |
public func addTo(view: View) { | |
view.addLayoutGuide(self) | |
} | |
public func removeFromContainer() { | |
owningView?.removeLayoutGuide(self) | |
} | |
} | |
public protocol Layout: Constrainable { | |
var root: Constrainable {get} //Can usually be a layout guide to reduce the number of container views in the hierarchy, but also supports using stack view, etc. as the basis for composable layouts | |
var children: [Constrainable] {get} | |
func layoutChildren() | |
} | |
extension Layout { | |
public var bottomAnchor: NSLayoutYAxisAnchor {return root.bottomAnchor} | |
public var centerXAnchor: NSLayoutXAxisAnchor {return root.centerXAnchor} | |
public var centerYAnchor: NSLayoutYAxisAnchor {return root.centerYAnchor} | |
public var heightAnchor: NSLayoutDimension {return root.heightAnchor} | |
public var leadingAnchor: NSLayoutXAxisAnchor {return root.leadingAnchor} | |
public var leftAnchor: NSLayoutXAxisAnchor {return root.leftAnchor} | |
public var rightAnchor: NSLayoutXAxisAnchor {return root.rightAnchor} | |
public var topAnchor: NSLayoutYAxisAnchor {return root.topAnchor} | |
public var trailingAnchor: NSLayoutXAxisAnchor {return root.trailingAnchor} | |
public var widthAnchor: NSLayoutDimension {return root.widthAnchor} | |
public func addTo(view: View) { | |
root.addTo(view: view) | |
if let r = root as? UIView { | |
r.translatesAutoresizingMaskIntoConstraints = false | |
} | |
for child in children { | |
if let v = child as? View { | |
v.translatesAutoresizingMaskIntoConstraints = false | |
} | |
if !(root is View) { | |
child.addTo(view: view) | |
} else { | |
child.addTo(view: root as! View) | |
} | |
} | |
layout() | |
} | |
public func removeFromContainer() { | |
root.removeFromContainer() | |
} | |
public func layout() { //recursively layout children | |
layoutChildren() | |
for child in children { | |
if let clayout = child as? Layout { | |
clayout.layout() | |
} | |
} | |
} | |
public func fill(view: View) { //add layout to view (usually top level) | |
self.addTo(view: view) | |
view.topAnchor.constraint(equalTo: self.topAnchor).isActive = true | |
view.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true | |
view.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true | |
view.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PaddedLayout.swift | |
// LayoutKit | |
// | |
// Created by Owen Voorhees on 10/6/16. | |
// Copyright © 2016 Owen Voorhees. All rights reserved. | |
// | |
//Lays out a single child padded on four sides | |
public class PaddedLayout: Layout { | |
public let root: Constrainable = UILayoutGuide() | |
public let children: [Constrainable] | |
private let padding: CGFloat | |
private let constraints: [NSLayoutConstraint] | |
public init(child: Constrainable, padding: CGFloat) { | |
children = [child] | |
self.padding = padding | |
constraints = [ | |
child.leftAnchor.constraint(equalTo: root.leftAnchor, constant: padding), | |
child.rightAnchor.constraint(equalTo: root.rightAnchor, constant: -padding), | |
child.topAnchor.constraint(equalTo: root.topAnchor, constant: padding), | |
child.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -padding) | |
] | |
} | |
public func layoutChildren() { | |
NSLayoutConstraint.activate(constraints) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// StackLayout.swift | |
// LayoutKit | |
// | |
// Created by Owen Voorhees on 10/7/16. | |
// Copyright © 2016 Owen Voorhees. All rights reserved. | |
// | |
//Lays out children using a stack view | |
//TODO: Not compatible w/ OS X | |
public class StackLayout: Layout { | |
public let root: Constrainable = UIStackView() | |
public var children: [Constrainable] = [] | |
private var stackView: UIStackView { | |
return root as! UIStackView | |
} | |
public init(children: [Constrainable], axis: UILayoutConstraintAxis, alignment: UIStackViewAlignment = .fill, spacing: CGFloat = 0.0, distribution: UIStackViewDistribution = .fillEqually) { | |
stackView.axis = axis | |
stackView.alignment = alignment | |
stackView.spacing = spacing | |
stackView.distribution = distribution | |
for child in children { | |
if let c = child as? UIView { | |
stackView.addArrangedSubview(c) | |
} else { | |
let container = ConstrainableContainerView(constrainable: child) | |
stackView.addArrangedSubview(container) | |
} | |
} | |
} | |
public func layoutChildren() {} //unnecesary b/c static layout managed by stack view | |
} | |
public class ConstrainableContainerView: UIView { | |
public let constrainable: Constrainable | |
public init(constrainable: Constrainable) { | |
self.constrainable = constrainable | |
super.init(frame: CGRect.zero) | |
constrainable.addTo(view: self) | |
constrainable.leftAnchor.constraint(equalTo: leftAnchor).isActive = true | |
constrainable.rightAnchor.constraint(equalTo: rightAnchor).isActive = true | |
constrainable.topAnchor.constraint(equalTo: topAnchor).isActive = true | |
constrainable.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true | |
} | |
required public init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Layout.swift - Protocols and extensions to allow composition
PaddedLayout.swift, StackLayout.swift, CustomLayout.swift - Example Layout objects
ExampleViewController.swift - A basic demonstration of a more complex layout created by composing the above objects