square / blueprint Goto Github PK
View Code? Open in Web Editor NEWDeclarative UI construction for iOS, written in Swift
Home Page: https://square.github.io/Blueprint/
License: Apache License 2.0
Declarative UI construction for iOS, written in Swift
Home Page: https://square.github.io/Blueprint/
License: Apache License 2.0
We ought to be able to use the same behavior in both BlueprintUI
and BlueprintUICommonControls
, but ideally without publicly polluting FloatingPoint
.
let column = Column { column in
column.verticalUnderflow = .spaceEvenly
column.add(child: Element1())
column.add(child: Element2(){
}.box(clipsContent: true).constrainedTo(width: .absolute(343), height: .absolute(48))
)}.scrollable(.fittingHeight){
$0.alwaysBounceVertical = true
$0.contentInset = UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16)
}
Here no flexible space added between two childs: Element1() and Element2() but column.verticalUnderflow = .spaceEvenly
ViewDescription
should provide a way to run a closure when the view appears or disappears. Could possibly be a property on the VisibilityTransition
.
SwiftUI provides onAppear
(which runs before appearance animations) and onDisappear
(which runs after). We may want to provide separate hooks for the start & end of animations.
It should specify that the z-ordering of children is back-most to front-most. (Or if that is not guaranteed, that should be documented instead.)
I need to verify some things, but at the very least there are some conditions where the decoration is laid out smaller than it's correct size.
It would be handy to have an element similar to SwiftUI's GeometryReader.
This would allow for layout-time sizing behaviors to be written ad hoc without having to build a Layout implementation.
I've prototyped a potential implementation of an environmental context like SwiftUI's @Environment
functionality. I'd like to get some feedback on this approach before opening a PR.
Like SwiftUI, it features an extensible, strongly-typed dictionary, where keys are types, and each key has a default value. Environment modifications in outer elements cascade to the contained elements at layout time without the two having any knowledge of each other.
Unlike SwiftUI, it does not use property wrappers, and is less magical. Instead there is a new protocol ContextElement
that works just like ProxyElement
, but instead of an elementRepresentation
property it has a function:
public protocol ContextElement: Element {
func elementRepresentation(in environment: Environment) -> Element
}
To accomplish this I added a new ContentStorage
implementation for ElementContent
that holds a closure, and defers building its child elements until layout time. By deferring until layout time, we can use the layout pass to cascade an Environment
instance through the entire tree.
This strategy requires building children twice: once for measuring and once for layout. Leaf nodes might be built numerous times if there are elements upstream in the tree that measure their children for layout.
Because the environment is required to build children, and building children is required for measuring, this change affects all Measurable
s. I tried two implementations.
https://github.com/square/Blueprint/tree/watt/environment-api-measurable
This branch changes Measurable
and adds an Environment
argument.
https://github.com/square/Blueprint/tree/watt/environment-api-nonmeasurable
This branch does not change Measurable
. Some types simply no longer conform. ElementContent
has a function like this instead:
func measurable(in environment: Environment) -> Measurable
The second approach had a smaller impact, but I could see doing it either way.
This commit contains an example use in the sample app.
Currently, the update process for elements to view in BlueprintView
is asynchronous.
Specifically, when element
is set, there is a call to setNeedsViewHierachyUpdate
which sets a flag that an update must happen, then a call to setNeedsLayout
where the update will happen on the next layout pass.
This has the side effect of potentially losing events if the previous closure bound to an element/view (like a text field, for instance) changes or is invalidated after being received.
UIKit will queue the events, so only send one per runloop pass, however there is a gap between the first being handled and the closure being updated (since it does not updated until the next layout pass has completed). Using Blueprint with Workflows can easily produce this with very fast input to text fields (eg: with a KIF test, but can be reproduced with a keyboard). Since the sink (event handler) in workflows is only valid for a single event in a single render pass, the behavior seen is a crash (or would be dropped events if it was not asserting) because of the gap in updates.
The naive "fix" for this would be to change BlueprintView
's didSet on element
to update the hierarchy, ie:
/// The root element that is displayed within the view.
public var element: Element? {
didSet {
setNeedsViewHierarchyUpdate()
+ // Immediately update the hierarchy when element is set, instead of waiting for the layout pass
+ updateViewHierarchyIfNeeded()
}
}
This is the naive fix, as blueprint should not support reentrant updates, so will likely need a bit of exploration to determine a "safe" way to make this update be synchronous.
And example view controller that reproduces what the behavior would be when used with Workflows: (a sink that invalidates after every update):
import UIKit
import BlueprintUI
import BlueprintUICommonControls
public final class SinkBackedBlueprintViewController: UIViewController {
private class Sink<Value> {
var valid = true
var onEvent: (Value) -> Void
init(onEvent: @escaping (Value) -> Void) {
self.onEvent = onEvent
}
func send(event: Value) {
if !valid {
fatalError("Old sink")
}
self.onEvent(event)
invalidate()
}
func invalidate() {
valid = false
}
}
private let blueprintView: BlueprintView
private var text: String = ""
private var sink: Sink<String>
public init() {
self.blueprintView = BlueprintView(frame: .zero)
self.sink = Sink(onEvent: { _ in })
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(blueprintView)
update(text: "")
}
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
blueprintView.frame = view.bounds
}
func update(text: String) {
self.text = text
generate()
}
func generate() {
var textField = TextField(text: text)
let sink = Sink<String>(onEvent: { [weak self] updated in
self?.update(text: updated)
})
textField.onChange = { [sink] updated in
sink.send(event: updated)
}
let label = AccessibilityElement(label: "email", value: nil, hint: nil, traits: [], wrapping: textField)
blueprintView.element = Column { col in
col.horizontalAlignment = .fill
col.minimumVerticalSpacing = 8.0
col.add(child: Box(backgroundColor: .green, cornerStyle: Box.CornerStyle.square, wrapping: nil))
col.add(
child: Box(
backgroundColor: .red,
cornerStyle: Box.CornerStyle.square,
wrapping: label))
col.add(child: Box(backgroundColor: .green, cornerStyle: Box.CornerStyle.square, wrapping: nil))
}
}
}
Because the consumer is interacting with elements, rather than the views backing them, view recycling could be done for the backing views with no changes in the public API. Each time an element needed a view, it could request it from a factory that doles out recycled views rather than creating its own.
The big questions are how to reclaim views and how to reset them for later use. The factory could maintain its own strong reference to all recyclable views, and then in the deinit method of an element, the element could notify the recycler that the view is now available to be reclaimed. The resetting part is less trivial but still doable.
There are a couple of related problems with the AttributedLabel
element (and by extension, Label
).
Measurement is wrong when numberOfLines
is set to a value other than 0
.
The current measurement behavior uses NSAttributedString.boundingRect(with:options:context:)
, which has no affordance for line limits.
Rounding assumes the main screen's scale by default, and will be wrong if rendered on a different screen.
This one is definitely an edge case, but we should at least consider solving this if it's feasible.
We solved problem 1 internally by using a static prototype instance of UILabel
. During measuring we apply the view description to the label and then call sizeThatFits
. This solution operates on the assumption that measuring is always done on the main thread, so two labels cannot be measured concurrently.
This does not solve problem 2: if UILabel
is not in a window, it returns results rounded to the main screen's scale.
TextKit measuring methods allow us to do line limits, but in experimentation I have found that TextKit measuring doesn't match UILabel
. In fact, it's quite difficult (maybe impossible, without knowing its internals) to accurately reproduce its measuring behavior across every combination of line break mode and line limit.
It's important to get those values right โ if they're off slightly, it can cause text to be truncated in places where it should have fit. If we want to use TextKit we may have to switch to UITextView
or do our own string rendering to ensure it matches our measuring.
We could also potentially switch between using a UILabel
for single-line text and a UITextView
for multi-line text, or offer these as separate elements.
Since TextKit does not automatically do any rounding, we'd be free to do this ourselves (perhaps by passing the screen scale down through the Environment).
As mentioned above, we could side-step UIKit entirely and use TextKit/CoreText to measure and then render strings.
This seems like overkill but it's an option.
The doc mentioned auto ui update for data change, but didn't see this in tutorial.
To avoid fuzziness caused by views that are not on pixel boundaries (generally due to rounding), we should snaps edges to pixel bounds. Ideally, a generalized solution that does not require individual elements to do their own explicit rounding. AutoLayout and SwiftUI have such features.
Blueprint and SwiftUI both tackle a similar kind of problem, so for developers outside Square, it would be helpful to understand how they differ โ for example, Blueprint's support for deploying to older versions of iOS.
Create (or upstream) a coordinate space reading element and an "anchor" type that holds the last emitted coordinate space. These can be used together to imperatively read an element's position on screen after layout.
Hi there!
A colleague linked me this repo and I've been browsing the source code. Just some questions:
Looking forward to see how this library evolves ๐ Awesome job
It causes unnecessary rendering of the element even if we push another view controller.
This leads to the app freeze if there are lots elements in element....
Stack layout currently measures its children only once, using the stack's entire bounds as a constraint. These basis sizes are then used for the cross axis sizes, while the layout axis sizes are adjusted according to the child priorities.
For elements like a Label
with text wrapping, where the height depends on the width, this can result in undesirable cross axis measurements.
Here's an example:
struct TestElement: ProxyElement {
var elementRepresentation: Element {
return Column { column in
column.verticalUnderflow = .justifyToCenter
column.horizontalAlignment = .fill
column.add(child: row(withMargin: 0))
column.add(child: row(withMargin: 50))
}
}
func row(withMargin margin: CGFloat) -> Element {
return Row { row in
row.horizontalOverflow = .condenseProportionally
row.add(
growPriority: 0,
shrinkPriority: 0,
child: Spacer(size: CGSize(width: margin, height: 0)))
row.add(
growPriority: 1,
shrinkPriority: 1,
child: Label(text: "This is a long label for testing. It takes up 2 lines if there are no margins, but needs 3 if we add some margins."))
row.add(
growPriority: 0,
shrinkPriority: 0,
child: Spacer(size: CGSize(width: margin, height: 0)))
}
}
}
In the second instance, the label should ideally be wrapped onto 3 lines. However, its height measurement is the same as the first instance, and does not take into account the adjusted width. It ends up truncated.
To fix this, I think we should re-measure children after determining the layout axis, and use those measurements for the cross axis sizes, rather than the basis sizes.
Many Blueprint elements that wrap a single child have an initializer defined like this:
WrapperElement(wrapping: Element, param1: X, param2: Y)
Where the wrapped element is the first parameter. Unfortunately, this ordering does not lend itself to inline-nested Element compositions, because it pushes the domain-related parameters away from the type they're associated with.
Consider the following element construction:
var constrainedInsetRow: Element {
return ConstrainedSize(
wrapping: Inset(
wrapping: Row { row in
row.verticalAlignment = .center
for leadingElement in self.leadingElements {
row.add(growPriority: 0, shrinkPriority: 0, child: leadingElement)
}
if let contentElement = self.contentElement {
row.add(child: contentElement)
}
for trailingElement in self.trailingElements {
row.add(growPriority: 0, shrinkPriority: 0, child: trailingElement)
}
},
uniformInset: 8
),
height: .atLeast(44)
)
}
The uniformInset
parameter is pushed 16 lines away from the Inset
it is associated with, and height
is 19 lines away from ConstrainedSize
.
If we move the wrapping
parameters to the end of the argument list, it looks like this:
var constrainedInsetRow: Element {
return ConstrainedSize(
height: .atLeast(44),
wrapping: Inset(
uniformInset: 8,
wrapping: Row { row in
row.verticalAlignment = .center
for leadingElement in self.leadingElements {
row.add(growPriority: 0, shrinkPriority: 0, child: leadingElement)
}
if let contentElement = self.contentElement {
row.add(child: contentElement)
}
for trailingElement in self.trailingElements {
row.add(growPriority: 0, shrinkPriority: 0, child: trailingElement)
}
}
)
)
}
Now it is immediately clear that height
goes with ConstrainedSize
and uniformInset
goes with Inset
, without really affecting the clarity of the nesting either.
Can we change the order of the arguments on ConstrainedSize
, Inset
, etc. to consistently have the wrapped element be last?
I tested on the swift package that the iOS platform can support at least iOS 13.0
SwiftUI has a concept of secondary views, which are attached to a parent view but do not play a part in layout. Handy for e.g. putting a badge on an icon without changing the layout of the icon.
This can almost be replicated in Blueprint with Overlays, but really needs custom layout in order to let the secondary element take any size or position without affecting the parent element. Blueprint should provide a dedicated element for this pattern.
Instead of
Inset(
uniformInset: 24.0,
wrapping: label)
we should make the tutorials use the trailing modifier syntax:
label.inset(uniform: 24)
(came up in #130)
This repo needs a contribution guide containing info like:
Travis or similar
We should add profiling signposts to the render pass to aid in performance optimization.
add event .onDisplay, .onEndDisplay
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.