Created
May 25, 2020 19:44
-
-
Save noahsark769/7759e47c0d5753b0f56eeff89ae9f0c8 to your computer and use it in GitHub Desktop.
ConstraintSystem
This file contains hidden or 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
// 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 | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
C'est magnifique!