forked from square/Blueprint
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0644699
Showing
153 changed files
with
9,731 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# OS | ||
.DS_Store | ||
|
||
# cocoapods-generate | ||
gen/ | ||
|
||
# Xcode | ||
xcuserdata/ | ||
|
||
# Cocoapods | ||
|
||
Pods/ | ||
*.xcworkspace |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
Pod::Spec.new do |s| | ||
s.name = 'BlueprintUI' | ||
s.version = '0.1.0' | ||
s.summary = 'Swift library for declarative UI construction' | ||
s.homepage = 'https://www.github.com/square/blueprint' | ||
s.license = 'Apache License, Version 2.0' | ||
s.author = 'Square' | ||
s.source = { :git => 'https://github.com/square/blueprint.git', :tag => s.version } | ||
|
||
s.swift_version = '4.2' | ||
|
||
s.ios.deployment_target = '9.3' | ||
|
||
s.source_files = 'BlueprintUI/Sources/**/*.swift' | ||
|
||
s.test_spec 'Tests' do |test_spec| | ||
test_spec.source_files = 'BlueprintUI/Tests/**/*.swift' | ||
test_spec.framework = 'XCTest' | ||
end | ||
|
||
end |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Blueprint | ||
===== | ||
|
||
Declarative UI 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
import UIKit | ||
|
||
/// A view that is responsible for displaying an `Element` hierarchy. | ||
/// | ||
/// A view controller that renders content via Blueprint might look something | ||
/// like this: | ||
/// | ||
/// ``` | ||
/// final class HelloWorldViewController: UIViewController { | ||
/// | ||
/// private var blueprintView = BlueprintView(element: nil) | ||
/// | ||
/// override func viewDidLoad() { | ||
/// super.viewDidLoad() | ||
/// | ||
/// let rootElement = Label(text: "Hello, world!") | ||
/// blueprintView.element = rootElement | ||
/// view.addSubview(blueprintView) | ||
/// } | ||
/// | ||
/// override func viewDidLayoutSubviews() { | ||
/// super.viewDidLayoutSubviews() | ||
/// blueprintView.frame = view.bounds | ||
/// } | ||
/// | ||
/// } | ||
/// ``` | ||
public final class BlueprintView: UIView { | ||
|
||
private var needsViewHierarchyUpdate: Bool = true | ||
private var hasUpdatedViewHierarchy: Bool = false | ||
private var lastViewHierarchyUpdateBounds: CGRect = .zero | ||
|
||
/// Used to detect reentrant updates | ||
private var isInsideUpdate: Bool = false | ||
|
||
private let rootController: NativeViewController | ||
|
||
/// The root element that is displayed within the view. | ||
public var element: Element? { | ||
didSet { | ||
setNeedsViewHierarchyUpdate() | ||
} | ||
} | ||
|
||
/// Instantiates a view with the given element | ||
/// | ||
/// - parameter element: The root element that will be displayed in the view. | ||
public required init(element: Element?) { | ||
|
||
self.element = element | ||
|
||
rootController = NativeViewController( | ||
node: NativeViewNode( | ||
content: UIView.describe() { _ in }, | ||
layoutAttributes: LayoutAttributes(), | ||
children: [])) | ||
|
||
super.init(frame: CGRect.zero) | ||
|
||
self.backgroundColor = .white | ||
addSubview(rootController.view) | ||
} | ||
|
||
public override convenience init(frame: CGRect) { | ||
self.init(element: nil) | ||
self.frame = frame | ||
} | ||
|
||
@available(*, unavailable) | ||
required public init?(coder aDecoder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
/// Forwarded to the `measure(in:)` implementation of the root element. | ||
override public func sizeThatFits(_ size: CGSize) -> CGSize { | ||
guard let element = element else { return .zero } | ||
let constraint: SizeConstraint | ||
if size == .zero { | ||
constraint = SizeConstraint(width: .unconstrained, height: .unconstrained) | ||
} else { | ||
constraint = SizeConstraint(size) | ||
} | ||
return element.content.measure(in: constraint) | ||
} | ||
|
||
override public func layoutSubviews() { | ||
super.layoutSubviews() | ||
performUpdate() | ||
} | ||
|
||
private func performUpdate() { | ||
updateViewHierarchyIfNeeded() | ||
} | ||
|
||
private func setNeedsViewHierarchyUpdate() { | ||
guard !needsViewHierarchyUpdate else { return } | ||
needsViewHierarchyUpdate = true | ||
|
||
/// We currently rely on CA's layout pass to actually perform a hierarchy update. | ||
setNeedsLayout() | ||
} | ||
|
||
private func updateViewHierarchyIfNeeded() { | ||
guard needsViewHierarchyUpdate || bounds != lastViewHierarchyUpdateBounds else { return } | ||
|
||
assert(!isInsideUpdate, "Reentrant updates are not supported in BlueprintView. Ensure that view events from within the hierarchy are not synchronously triggering additional updates.") | ||
isInsideUpdate = true | ||
|
||
needsViewHierarchyUpdate = false | ||
lastViewHierarchyUpdateBounds = bounds | ||
|
||
/// Grab view descriptions | ||
let viewNodes = element? | ||
.layout(frame: bounds) | ||
.resolve() ?? [] | ||
|
||
rootController.view.frame = bounds | ||
|
||
let rootNode = NativeViewNode( | ||
content: UIView.describe() { _ in }, | ||
layoutAttributes: LayoutAttributes(frame: bounds), | ||
children: viewNodes) | ||
|
||
rootController.update(node: rootNode, appearanceTransitionsEnabled: hasUpdatedViewHierarchy) | ||
hasUpdatedViewHierarchy = true | ||
|
||
isInsideUpdate = false | ||
} | ||
|
||
var currentNativeViewControllers: [(path: ElementPath, node: NativeViewController)] { | ||
|
||
/// Perform an update if needed so that the node hierarchy is fully populated. | ||
updateViewHierarchyIfNeeded() | ||
|
||
/// rootViewNode always contains a simple UIView – its children represent the | ||
/// views that are actually generated by the root element. | ||
return rootController.children | ||
} | ||
|
||
} | ||
|
||
extension BlueprintView { | ||
|
||
final class NativeViewController { | ||
|
||
private var viewDescription: ViewDescription | ||
|
||
private var layoutAttributes: LayoutAttributes | ||
|
||
private (set) var children: [(ElementPath, NativeViewController)] | ||
|
||
let view: UIView | ||
|
||
init(node: NativeViewNode) { | ||
self.viewDescription = node.viewDescription | ||
self.layoutAttributes = node.layoutAttributes | ||
self.children = [] | ||
self.view = node.viewDescription.build() | ||
update(node: node, appearanceTransitionsEnabled: false) | ||
} | ||
|
||
fileprivate func canUpdateFrom(node: NativeViewNode) -> Bool { | ||
return node.viewDescription.viewType == type(of: view) | ||
} | ||
|
||
fileprivate func update(node: NativeViewNode, appearanceTransitionsEnabled: Bool) { | ||
|
||
assert(node.viewDescription.viewType == type(of: view)) | ||
|
||
viewDescription = node.viewDescription | ||
layoutAttributes = node.layoutAttributes | ||
|
||
viewDescription.apply(to: view) | ||
|
||
var oldChildren: [ElementPath: NativeViewController] = [:] | ||
oldChildren.reserveCapacity(children.count) | ||
|
||
for (path, childController) in children { | ||
oldChildren[path] = childController | ||
} | ||
|
||
var newChildren: [(path: ElementPath, node: NativeViewController)] = [] | ||
newChildren.reserveCapacity(node.children.count) | ||
|
||
var usedKeys: Set<ElementPath> = [] | ||
usedKeys.reserveCapacity(node.children.count) | ||
|
||
for (path, child) in node.children { | ||
|
||
guard usedKeys.contains(path) == false else { | ||
fatalError("Duplicate view identifier") | ||
} | ||
usedKeys.insert(path) | ||
|
||
if let controller = oldChildren[path], controller.canUpdateFrom(node: child) { | ||
|
||
oldChildren.removeValue(forKey: path) | ||
newChildren.append((path: path, node: controller)) | ||
|
||
let layoutTransition: LayoutTransition | ||
|
||
if child.layoutAttributes != controller.layoutAttributes { | ||
layoutTransition = child.viewDescription.layoutTransition | ||
} else { | ||
layoutTransition = .inherited | ||
} | ||
layoutTransition.perform { | ||
child.layoutAttributes.apply(to: controller.view) | ||
controller.update(node: child, appearanceTransitionsEnabled: true) | ||
} | ||
} else { | ||
let controller = NativeViewController(node: child) | ||
newChildren.append((path: path, node: controller)) | ||
|
||
UIView.performWithoutAnimation { | ||
child.layoutAttributes.apply(to: controller.view) | ||
} | ||
|
||
let contentView = node.viewDescription.contentView(in: view) | ||
contentView.addSubview(controller.view) | ||
|
||
controller.update(node: child, appearanceTransitionsEnabled: false) | ||
|
||
if appearanceTransitionsEnabled { | ||
child.viewDescription.appearingTransition?.performAppearing(view: controller.view, layoutAttributes: child.layoutAttributes, completion: {}) | ||
} | ||
} | ||
} | ||
|
||
for controller in oldChildren.values { | ||
if let transition = controller.viewDescription.disappearingTransition { | ||
transition.performDisappearing(view: controller.view, layoutAttributes: controller.layoutAttributes, completion: { | ||
controller.view.removeFromSuperview() | ||
}) | ||
} else { | ||
controller.view.removeFromSuperview() | ||
} | ||
} | ||
|
||
children = newChildren | ||
} | ||
|
||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/// Conforming types represent a rectangular content area in a two-dimensional | ||
/// layout space. | ||
/// | ||
/// *** | ||
/// | ||
/// The ultimate purpose of an element is to provide visual content. This can be | ||
/// done in two ways: | ||
/// | ||
/// - By providing a view description (`ViewDescription`). | ||
/// | ||
/// - By providing child elements that will be displayed recursively within | ||
/// the local coordinate space. | ||
/// | ||
/// *** | ||
/// | ||
/// A custom element might look something like this: | ||
/// | ||
/// ``` | ||
/// struct MyElement: Element { | ||
/// | ||
/// var backgroundColor: UIColor = .red | ||
/// | ||
/// // Returns a single child element. | ||
/// var content: ElementContent { | ||
/// return ElementContent(child: Label(text: "😂")) | ||
/// } | ||
/// | ||
/// // Providing a view description means that this element will be | ||
/// // backed by a UIView instance when displayed in a `BlueprintView`. | ||
/// func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? { | ||
/// return UIView.describe { config in | ||
/// config.bind(backgroundColor, to: \.backgrouncColor) | ||
/// } | ||
/// } | ||
/// | ||
/// } | ||
/// ``` | ||
/// | ||
public protocol Element { | ||
|
||
/// Returns the content of this element. | ||
/// | ||
/// Elements generally fall into two types: | ||
/// - Leaf elements, or elements that have no children. These elements commonly have an intrinsic size, or some | ||
/// content that can be measured. Leaf elements typically instantiate their content with | ||
/// `ElementContent(measurable:)` or similar. | ||
/// - Container elements: these element have one or more children, which are arranged by a layout implementation. | ||
/// Container elements typically use methods like `ElementContent(layout:configure:)` to instantiate | ||
/// their content. | ||
var content: ElementContent { get } | ||
|
||
/// Returns an (optional) description of the view that should back this element. | ||
/// | ||
/// In Blueprint, elements that are displayed using a live `UIView` instance are referred to as "view-backed". | ||
/// Elements become view-backed by returning a `ViewDescription` value from this method. | ||
/// | ||
/// - Parameter bounds: The bounds of this element after layout is complete. | ||
/// - Parameter subtreeExtent: A rectangle in the local coordinate space that contains any children. | ||
/// `subtreeExtent` will be nil if there are no children. | ||
/// | ||
/// - Returns: An optional `ViewDescription`. | ||
func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? | ||
|
||
} |
Oops, something went wrong.