Description
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))
}
}
}
Activity