Skip to content

Events can be dropped between updates #13

Open
@davidapgar

Description

@davidapgar

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

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions