Last active
October 16, 2016 14:32
-
-
Save andersio/4988f9ee44d65f27cdbd0076a89d6a21 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| import ReactiveSwift | |
| import enum Result.NoError | |
| /// Bidirectional Binding Operator: `<~>` | |
| precedencegroup BidirectionalBinding { | |
| higherThan: AssignmentPrecedence | |
| } | |
| infix operator <~>: BidirectionalBinding | |
| /// Describes a transform for an Editor to bridge presented values in UI | |
| /// with persisted value in the model layer. | |
| public protocol EditorTransform { | |
| associatedtype Presented | |
| associatedtype Persisted: Equatable | |
| static func convert(persisted value: Persisted) -> Presented | |
| static func convert(presented value: Presented) -> Persisted | |
| } | |
| /// Defines how an Editor should merge values shall any conflict arises. | |
| public protocol EditorMergePolicy { | |
| associatedtype Persisted | |
| static var stopsPersistedAfterFirstEdit: Bool { get } | |
| static func merge(cached value: Persisted, with proposedValue: Persisted) -> Persisted | |
| } | |
| /// A merge policy that always prefers user edits. | |
| public enum PreferUserEditMergePolicy<Value>: EditorMergePolicy { | |
| public static var stopsPersistedAfterFirstEdit: Bool { | |
| return true | |
| } | |
| public static func merge(cached value: Value, with proposedValue: Value) -> Value { | |
| return value | |
| } | |
| } | |
| /// Represents a subject of a control that can be bond with an Editor. | |
| public struct ControlSubject<Value> { | |
| public let signal: Signal<Value, NoError> | |
| public let value: BindingTarget<Value> | |
| public let isEnabled: BindingTarget<Bool> | |
| public let duration: Signal<(), NoError> | |
| public init(signal: Signal<Value, NoError>, value: BindingTarget<Value>, isEnabled: BindingTarget<Bool>, duration: Signal<(), NoError>) { | |
| self.signal = signal | |
| self.value = value | |
| self.isEnabled = isEnabled | |
| self.duration = duration | |
| } | |
| } | |
| /// Manages a two-way binding between a mutable entity and a ControlSubject. | |
| public class Editor<Transform: EditorTransform> { | |
| /// The latest cached value of the Editor. | |
| /// | |
| /// - important: This is eventually consistent with the persisted source, and | |
| /// strongly consistent with the bound control. | |
| public var value: Transform.Persisted { | |
| return _value | |
| } | |
| /// The underlying storage of `value`. | |
| private var _value: Transform.Persisted! | |
| /// Whether a user initiated edit has ever been caught. | |
| public private(set) var hasBeenEdited: Bool = false | |
| /// Whether the Editor should drop non distinct user changes. | |
| private var dropsNonDistinctUserChanges = true | |
| /// Whether the changes from the persisted source should be ignored. | |
| private var ignoresPersistedValue = false | |
| /// Whether the changes from the persisted change should be ignored after the | |
| /// user has made a very first edit. | |
| private let stopsPersistedAfterFirstEdit: Bool | |
| /// An action to merge the cached value and the persisted change. | |
| private let merge: (Transform.Persisted, Transform.Persisted) -> Transform.Persisted | |
| /// An action to write the value back to the persisted source. | |
| private let _writeback: (Transform.Persisted) -> Void | |
| /// A scheduler to serialize changes from the persisted source and the | |
| /// writebacks from the editor. | |
| private let scheduler: UIScheduler | |
| /// A producer that controls whether the control should be enabled. | |
| private let enabledProducer: SignalProducer<Bool, NoError> | |
| /// The ControlSubject being bound to `self`. | |
| private var subject: ControlSubject<Transform.Presented>? { | |
| didSet { | |
| guard let subject = subject else { return } | |
| let controlDisposable = CompositeDisposable() | |
| subject.duration.take(first: 1).observe { _ in controlDisposable.dispose() } | |
| controlDisposable += { [weak self] in self?.subject = nil } | |
| controlDisposable += subject.isEnabled <~ enabledProducer | |
| controlDisposable += subject.signal.observeValues { userValue in | |
| let newValue = Transform.convert(presented: userValue) | |
| if self.dropsNonDistinctUserChanges && newValue == self._value { | |
| return | |
| } | |
| if !self.hasBeenEdited { | |
| self.hasBeenEdited = true | |
| } | |
| self._value = newValue | |
| self.writeback(async: false) | |
| } | |
| subject.value.consume(Transform.convert(persisted: _value)) | |
| self.controlDisposable = controlDisposable | |
| } | |
| } | |
| /// The disposable of the current ControlSubject. | |
| private var controlDisposable: CompositeDisposable? | |
| /// The disposable of the persisted source. | |
| private var persistedDisposable: CompositeDisposable | |
| /// Initialize an Editor that manages two-way bindings with the supplied | |
| /// persisted source (`persistedValues` and `writeback`). | |
| /// | |
| /// - important: You may supply a `writeback` action that would cause values | |
| /// being sent upon `persistedValues`. The editor would track | |
| /// and ignore these values at best effort to avoid deadlocks and | |
| /// feedback loops. | |
| /// | |
| /// - parameters: | |
| /// - persistedValues: The initial value and subsequent changes over time. | |
| /// - writeback: An action to writeback changes. | |
| /// - mergePolicy: The desired merge policy for the Editor. | |
| /// - enabledProducer: A producer which specifies whether the Editor is | |
| /// enabled. | |
| public init<MergePolicy: EditorMergePolicy>( | |
| persistedValues: SignalProducer<Transform.Persisted, NoError>, | |
| writeback setter: @escaping (Transform.Persisted) -> Void, | |
| mergePolicy: MergePolicy.Type, | |
| enabledIf enabledProducer: SignalProducer<Bool, NoError>, | |
| dropsNonDistinctUserChanges: Bool = true | |
| ) where MergePolicy.Persisted == Transform.Persisted { | |
| self.dropsNonDistinctUserChanges = dropsNonDistinctUserChanges | |
| stopsPersistedAfterFirstEdit = mergePolicy.stopsPersistedAfterFirstEdit | |
| merge = mergePolicy.merge(cached:with:) | |
| _writeback = setter | |
| self.enabledProducer = enabledProducer | |
| persistedDisposable = CompositeDisposable() | |
| scheduler = UIScheduler() | |
| persistedDisposable += persistedValues | |
| .observe(on: scheduler) | |
| .startWithValues { value in | |
| if self.ignoresPersistedValue { | |
| return | |
| } | |
| if self.hasBeenEdited { | |
| if self.stopsPersistedAfterFirstEdit { | |
| let mergedValue = self.merge(self._value, value) | |
| self._value = mergedValue | |
| } | |
| // The writeback needs to be asynchronous, since the `sendLock` of | |
| // `persistedValues` is held by this scope. | |
| self.writeback(async: true) | |
| } else { | |
| self._value = value | |
| } | |
| self.subject?.value.consume(Transform.convert(persisted: value)) | |
| } | |
| assert(_value != nil) | |
| } | |
| /// Write back the cached value to the persisted source. | |
| /// | |
| /// - parameters: | |
| /// - async: Whether the writeback should be performed asynchronously. | |
| private func writeback(async: Bool = false) { | |
| ignoresPersistedValue = true | |
| if async { | |
| scheduler.schedule { | |
| self._writeback(self._value) | |
| // Since the writeback is delivered to `scheduler` asynchronously, we | |
| // enqueue a block to flip the ignoring flag after the writeback. | |
| self.scheduler.schedule { | |
| self.ignoresPersistedValue = false | |
| } | |
| } | |
| } else { | |
| self._writeback(self._value) | |
| self.ignoresPersistedValue = false | |
| } | |
| } | |
| /// Bind a ControlSubject with an Editor. | |
| public static func <~> (left: Editor<Transform>, right: ControlSubject<Transform.Presented>) { | |
| left.subject = right | |
| } | |
| } |
This file contains hidden or 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
| import AppKit | |
| class TweetCell: NSTableCellView { | |
| func setViewModel(_ viewModel: TweetViewModel) { | |
| let subject = ControlSubject(signal: textField!.reactive.continuousStringValues, | |
| value: textField!.reactive.stringValue, | |
| isEnabled: textField!.reactive.isEnabled, | |
| duration: reactive.trigger(for: #selector(self.prepareForReuse))) | |
| viewModel.content <~> subject! | |
| } | |
| } |
This file contains hidden or 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
| import MantleData | |
| import ReactiveSwift | |
| public enum NopTransform<Value>: EditorTransform { | |
| public static func convert(persisted value: Value) -> Value { | |
| return value | |
| } | |
| public static func convert(presented value: Value) -> Value { | |
| return value | |
| } | |
| } | |
| public struct TweetViewModel { | |
| let content: Editor<NopTransform<String>> | |
| internal init(_ tweet: Tweet) { | |
| content = Editor(persistedValues: content.producer(forKeyPath: #keyPath(Entry.flow)), | |
| writeback: { entry.flow = $0 }, | |
| mergePolicy: PreferUserEditMergePolicy.self, | |
| enabledIf: SignalProducer(value: true)) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment