Skip to content

Instantly share code, notes, and snippets.

@shaps80
Last active June 19, 2018 21:50
Show Gist options
  • Select an option

  • Save shaps80/3cf7b91fd7e65fc58d1e6f4991ffe47e to your computer and use it in GitHub Desktop.

Select an option

Save shaps80/3cf7b91fd7e65fc58d1e6f4991ffe47e to your computer and use it in GitHub Desktop.
A simple workspace builder using simple UIStackView's and a clean DSL for describing the layout.
//
// ViewController.swift
// Workspace
//
// Created by Shaps Benkau on 12/27/2016.
// Copyright (c) 2016 Snippex Ltd. All rights reserved.
//
import UIKit
class DemoViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let content = PaneViewController(title: "Content")
content.view.backgroundColor = UIColor(white: 0.106, alpha: 1)
let preview = PaneViewController(title: "Preview")
preview.view.backgroundColor = UIColor(white: 0.086, alpha: 1)
let timeline = PaneViewController(title: "Timeline")
timeline.view.backgroundColor = UIColor(white: 0.106, alpha: 1)
let toolbar = PaneViewController(title: "Toolbar") // 0.122
toolbar.view.backgroundColor = UIColor(white: 0.2, alpha: 1)
let workspace = WorkspaceController(nibName: nil, bundle: nil)
addChildViewController(workspace)
view.addSubview(workspace.view)
workspace.didMove(toParentViewController:self)
workspace.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
/* This is where we define the workspace
----- -------------
| | | |
| C | | Preview |
| o | |------------
| n | | Toolbar |
| t | |------------
| e | | |
| n | | Timeline |
| t | | |
| | | |
----- -------------
*/
let rightPane = WorkspacePane(axis: .vertical)
.add(child: .controller(preview), size: .fixed(length: 320))
.add(child: .controller(toolbar), size: .fixed(length: 44))
.add(child: .controller(timeline), size: .fixed(length: 100), priority: UILayoutPriorityDefaultLow)
let mainPane = WorkspacePane(axis: .horizontal)
.add(child: .controller(content), size: .fixed(length: 320))
.add(child: .pane(rightPane), size: .flexible)
workspace.invalidate(with: mainPane)
// To demonstrate animation -- we can hide the left hand view initially
content.view.isHidden = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Now we can un-hide the view and watch everything animate nicely ;)
UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 1, options: .beginFromCurrentState, animations: {
content.view.isHidden = false
}, completion: nil)
}
}
}
final class PaneViewController: UIViewController {
init(title: String) {
super.init(nibName: nil, bundle: nil)
self.title = title
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = title
label.textColor = .white
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
//
// WorkspaceController.swift
// Workspace
//
// Created by Shaps Benkau on 27/12/2016.
// Copyright © 2016 Snippex Ltd. All rights reserved.
//
import UIKit
public class WorkspaceController: UIViewController {
var viewControllers: [UIViewController] = []
public func invalidate(with pane: WorkspacePane) {
for controller in viewControllers {
controller.willMove(toParentViewController: nil)
controller.view.removeFromSuperview()
controller.removeFromParentViewController()
}
viewControllers.removeAll()
view.subviews.forEach {
$0.removeFromSuperview()
}
pane.view.frame = view.bounds
pane.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.backgroundColor = .black
view.addSubview(pane.view)
pane.invalidate(in: self)
}
}
//
// WorkspacePane.swift
// Workspace
//
// Created by Shaps Benkau on 01/01/2017.
// Copyright © 2017 Snippex Ltd. All rights reserved.
//
import UIKit
public final class WorkspacePane {
public enum Axis {
case horizontal
case vertical
}
public enum SizeMode {
case flexible
case fixed(length: CGFloat)
}
public enum Child {
case controller(UIViewController)
case pane(WorkspacePane)
}
public var view: UIView {
return stackView
}
// MARK: Public
public init(axis: Axis) {
let stackView = UIStackView()
stackView.axis = axis == .horizontal ? .horizontal : .vertical
stackView.alignment = .fill
stackView.distribution = .fill
stackView.spacing = 2
self.stackView = stackView
self.axis = axis
}
@discardableResult public func add(child: Child, size mode: SizeMode, priority: UILayoutPriority = UILayoutPriorityDefaultHigh) -> WorkspacePane {
children.append((child, mode, priority))
return self
}
// MARK: Internal
func invalidate(in workspace: WorkspaceController) {
let maxLength = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
for (child, mode, priority) in children {
switch child {
case .controller(let controller):
addChild(controller: controller, to: workspace)
applyConstraint(to: controller.view, maxLength: maxLength, size: mode, priority: priority)
case .pane(let pane):
addChild(pane: pane, to: workspace)
applyConstraint(to: pane.view, maxLength: maxLength, size: mode, priority: priority)
}
}
}
// MARK: Private
private let axis: Axis
private let stackView: UIStackView
private var children: [(Child, SizeMode, UILayoutPriority)] = []
private func sizeForMode(_ mode: SizeMode, maxLength: CGFloat) -> CGSize {
switch mode {
case .fixed(let length):
return CGSize(width: axis == .horizontal ? length : maxLength,
height: axis == .vertical ? length : maxLength)
default:
return CGSize(width: maxLength, height: maxLength)
}
}
private func addChild(pane: WorkspacePane, to workspace: WorkspaceController) {
stackView.addArrangedSubview(pane.view)
pane.invalidate(in: workspace)
}
private func addChild(controller: UIViewController, to workspace: WorkspaceController) {
guard !workspace.viewControllers.contains(controller) else { fatalError("This controller already exists in the Workspace") }
workspace.addChildViewController(controller)
stackView.addArrangedSubview(controller.view)
controller.didMove(toParentViewController:workspace)
workspace.viewControllers.append(controller)
}
private func applyConstraint(to view: UIView, maxLength: CGFloat, size mode: SizeMode, priority: UILayoutPriority) {
let size = sizeForMode(mode, maxLength: maxLength)
if axis == .horizontal && size.width != maxLength {
let constraint = view.widthAnchor.constraint(equalToConstant: size.width)
// we lower the constraint so that it doesn't conflict with the UIStackView's constraints
constraint.priority = max(1, priority - 1)
NSLayoutConstraint.activate([ constraint ])
}
if axis == .vertical && size.height != maxLength {
let constraint = view.heightAnchor.constraint(equalToConstant: size.height)
// we lower the constraint so that it doesn't conflict with the UIStackView's constraints
constraint.priority = max(1, priority - 1)
NSLayoutConstraint.activate([ constraint ])
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment