Skip to content

Instantly share code, notes, and snippets.

@owenv
Created November 11, 2016 02:05
Show Gist options
  • Save owenv/56017d4702464fe33e5a79bfb1315c4d to your computer and use it in GitHub Desktop.
Save owenv/56017d4702464fe33e5a79bfb1315c4d to your computer and use it in GitHub Desktop.
Composable Layout Objects in Swift
//
// 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))
}
}
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
}
}
//
// 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
}
}
//
// 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)
}
}
//
// 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")
}
}
@owenv
Copy link
Author

owenv commented Nov 11, 2016

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

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