Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events can be dropped between updates #13

Open
davidapgar opened this issue Jun 27, 2019 · 0 comments
Open

Events can be dropped between updates #13

davidapgar opened this issue Jun 27, 2019 · 0 comments
Assignees

Comments

@davidapgar
Copy link

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))
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant