Skip to content

Instantly share code, notes, and snippets.

@noahsark769
Created May 25, 2020 19:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save noahsark769/7759e47c0d5753b0f56eeff89ae9f0c8 to your computer and use it in GitHub Desktop.
Save noahsark769/7759e47c0d5753b0f56eeff89ae9f0c8 to your computer and use it in GitHub Desktop.
ConstraintSystem
// The following is a code sample based on https://twitter.com/noahsark769/status/1264681181435420672?s=20
// It's not really done, but putting it here for the benefit of the community. Maybe at some point soon I'll open source
// this into a proper library.
//
// What it does: Defines a ConstraintSystem SwiftUI view which allows you to specify autolayout constraints between views.
//
// Caveats:
// - Only works for AppKit/NSView/NSViewRepresentable, not UIKit yet
// - Only works on the first render (update(nsView) implementation is empty)
// - The constraint identifiers must be strings, it would be nice to make them generic over some type that is Hashable,
// like .tag()
// - ConstraintSystem takes an array, it would be nice to make it use function builders, and might let us eliminate the
// Constraint type, which is just an enum for either a horizontal or vertical constraint
// - Only does edge pinning, no pinning to exact widths (don't even know if that makes sense for this)
// - Only does equalTo, no greaterThanOrEqualTo etc
// - No constraint priorities at this time
// - Multiline SwiftUI Text() views don't expand the height in the way I'd like them to currently
// - If you put a ConstraintSystem inside a ZStack whose frame is bigger than the bounding rectangle of the ConstraintSystem,
// it doesn't align correctly, and I'm not sure why
import Foundation
import SwiftUI
import AppKit
struct ConstraintIdentifiedView: View {
let anyView: AnyView
let identifier: String
var body: some View {
self.anyView
}
}
extension View {
func constraintIdentifier(identifier: String) -> ConstraintIdentifiedView {
return ConstraintIdentifiedView(anyView: AnyView(self), identifier: identifier)
}
}
extension Sequence {
func toDictionary<Key>(_ by: (Element) -> Key) -> Dictionary<Key, Element> {
return Dictionary(grouping: self, by: by).compactMapValues({ $0.first })
}
}
protocol Edge {
func createConstraint(to toEdge: Self, of toView: NSView, from fromView: NSView) -> NSLayoutConstraint
static func constraintFrom(axisConstraint: AxisConstraint<Self>) -> Constraint
}
enum HorizontalEdge: Edge {
case trailing
case leading
func createConstraint(to toEdge: HorizontalEdge, of toView: NSView, from fromView: NSView) -> NSLayoutConstraint {
let toAnchor: NSLayoutXAxisAnchor
let fromAnchor: NSLayoutXAxisAnchor
switch toEdge {
case .trailing: toAnchor = toView.trailingAnchor
case .leading: toAnchor = toView.leadingAnchor
}
switch self {
case .trailing: fromAnchor = fromView.trailingAnchor
case .leading: fromAnchor = fromView.leadingAnchor
}
return toAnchor.constraint(equalTo: fromAnchor)
}
static func constraintFrom(axisConstraint: AxisConstraint<Self>) -> Constraint {
return .horizontal(axisConstraint)
}
}
enum VerticalEdge: Edge {
case top
case bottom
func createConstraint(to toEdge: VerticalEdge, of toView: NSView, from fromView: NSView) -> NSLayoutConstraint {
let toAnchor: NSLayoutYAxisAnchor
let fromAnchor: NSLayoutYAxisAnchor
switch toEdge {
case .top: toAnchor = toView.topAnchor
case .bottom: toAnchor = toView.bottomAnchor
}
switch self {
case .top: fromAnchor = fromView.topAnchor
case .bottom: fromAnchor = fromView.bottomAnchor
}
return toAnchor.constraint(equalTo: fromAnchor)
}
static func constraintFrom(axisConstraint: AxisConstraint<Self>) -> Constraint {
return .vertical(axisConstraint)
}
}
struct AxisConstraint<EdgeType: Edge> {
let fromIdentifier: String
let fromEdge: EdgeType
let toIdentifier: String
let toEdge: EdgeType
func layoutConstraint(withMapping mapping: [String: ConstrainedView]) -> NSLayoutConstraint {
guard let toView = mapping[toIdentifier], let fromView = mapping[fromIdentifier] else {
fatalError("WHAT????")
}
return fromEdge.createConstraint(to: toEdge, of: toView, from: fromView)
}
func to(_ other: Self) -> Constraint {
return EdgeType.constraintFrom(axisConstraint: self)
}
}
enum Constraint {
case horizontal(AxisConstraint<HorizontalEdge>)
case vertical(AxisConstraint<VerticalEdge>)
func layoutConstraint(withMapping mapping: [String: ConstrainedView]) -> NSLayoutConstraint {
switch self {
case let .horizontal(axis): return axis.layoutConstraint(withMapping: mapping)
case let .vertical(axis): return axis.layoutConstraint(withMapping: mapping)
}
}
}
extension Sequence where Element == Constraint {
func layoutConstraints(withMapping mapping: [String: ConstrainedView]) -> [NSLayoutConstraint] {
return self.map { constraint in
constraint.layoutConstraint(withMapping: mapping)
}
}
}
struct IdentifiedEdge<EdgeType: Edge> {
let identifier: String
let edge: EdgeType
func to(_ other: IdentifiedEdge<EdgeType>) -> Constraint {
return EdgeType.constraintFrom(axisConstraint: AxisConstraint(fromIdentifier: self.identifier, fromEdge: self.edge, toIdentifier: other.identifier, toEdge: other.edge))
}
}
final class ConstraintSystemView: NSView {
private var constrainedViews: [ConstrainedView] = []
func addConstrainedView(_ view: ConstrainedView) {
view.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(view)
self.constrainedViews.append(view)
}
func applyConstraints(_ constraints: [Constraint]) {
let viewsById = constrainedViews.toDictionary { $0.constraintIdentifier }
let layoutConstraints = constraints.layoutConstraints(withMapping: viewsById)
for layoutConstraint in layoutConstraints {
layoutConstraint.isActive = true
}
}
func removeAllConstrainedViews() {
for subview in self.subviews {
subview.removeFromSuperview()
}
constrainedViews.removeAll()
}
}
final class ConstrainedView: NSView {
let constraintIdentifier: String
init(constraintIdentifier: String) {
self.constraintIdentifier = constraintIdentifier
super.init(frame: .zero)
// self.wantsLayer = true
// self.layer?.backgroundColor = NSColor.blue.cgColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension String {
var leading: IdentifiedEdge<HorizontalEdge> {
return IdentifiedEdge(identifier: self, edge: .leading)
}
var trailing: IdentifiedEdge<HorizontalEdge> {
return IdentifiedEdge(identifier: self, edge: .trailing)
}
var top: IdentifiedEdge<VerticalEdge> {
return IdentifiedEdge(identifier: self, edge: .top)
}
var bottom: IdentifiedEdge<VerticalEdge> {
return IdentifiedEdge(identifier: self, edge: .bottom)
}
}
extension NSLayoutConstraint {
func at(_ priority: NSLayoutConstraint.Priority) -> NSLayoutConstraint {
self.priority = priority
return self
}
}
struct ConstraintSystem: NSViewRepresentable {
let content: [ConstraintIdentifiedView]
let constraints: [Constraint]
func makeNSView(context: Context) -> ConstraintSystemView {
let view = ConstraintSystemView()
view.translatesAutoresizingMaskIntoConstraints = false
self.addViews(to: view)
view.applyConstraints(constraints)
return view
}
func updateNSView(_ nsView: ConstraintSystemView, context: Context) {
// self.addViews(to: nsView)
//// nsView.removeAllConstraints()
// nsView.applyConstraints(constraints)
}
private func addViews(to view: ConstraintSystemView) {
view.removeAllConstrainedViews()
for contentView in content {
let constrainedView = ConstrainedView(constraintIdentifier: contentView.identifier)
let hostingView = NSHostingView(rootView: contentView)
hostingView.translatesAutoresizingMaskIntoConstraints = false
constrainedView.addSubview(hostingView)
constrainedView.leadingAnchor.constraint(equalTo: hostingView.leadingAnchor).isActive = true
constrainedView.trailingAnchor.constraint(equalTo: hostingView.trailingAnchor).isActive = true
constrainedView.topAnchor.constraint(equalTo: hostingView.topAnchor).isActive = true
constrainedView.bottomAnchor.constraint(equalTo: hostingView.bottomAnchor).isActive = true
view.addConstrainedView(constrainedView)
view.leadingAnchor.constraint(lessThanOrEqualTo: constrainedView.leadingAnchor).isActive = true
view.trailingAnchor.constraint(greaterThanOrEqualTo: constrainedView.trailingAnchor).isActive = true
view.topAnchor.constraint(lessThanOrEqualTo: constrainedView.topAnchor).isActive = true
view.bottomAnchor.constraint(greaterThanOrEqualTo: constrainedView.bottomAnchor).isActive = true
view.leadingAnchor.constraint(equalTo: constrainedView.leadingAnchor).at(.defaultHigh).isActive = true
view.trailingAnchor.constraint(equalTo: constrainedView.trailingAnchor).at(.defaultHigh).isActive = true
view.topAnchor.constraint(equalTo: constrainedView.topAnchor).at(.defaultHigh).isActive = true
view.bottomAnchor.constraint(equalTo: constrainedView.bottomAnchor).at(.defaultHigh).isActive = true
}
}
}
@vmanot
Copy link

vmanot commented May 25, 2020

C'est magnifique!

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