Last active
July 2, 2018 14:35
-
-
Save shawnthroop/d6be9b81a40a27c44a19e631290a52ed to your computer and use it in GitHub Desktop.
An elaboration of Swift Talk S01E63 (Mutable Shared Structs). Provides a simpler base class for read-only properties.
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
// swift 4.1 | |
import Foundation | |
import Sting | |
struct Account: Equatable { | |
var id: Int | |
} | |
struct Accounts { | |
private var content: MutableVariable<[Account]> | |
private var disposables = [Any]() | |
init(accounts: [Account]) { | |
self.content = MutableVariable(accounts) | |
disposables.append(content[\.first].addChangeObserver { newValue, oldValue in | |
print("Current Account: \(String(describing: newValue?.id)), old: \(String(describing: oldValue?.id))") | |
}) | |
} | |
init(_ accounts: Account...) { | |
self.init(accounts: accounts) | |
} | |
var current: Account? { | |
return content.value.first | |
} | |
mutating func prepend(_ account: Account) { | |
content.value.insert(account, at: content.value.startIndex) | |
} | |
mutating func remove(_ account: Account) { | |
if let i = content.value.index(of: account) { | |
content.value.remove(at: i) | |
} | |
} | |
} | |
let a0 = Account(id: 0) | |
let a1 = Account(id: 1) | |
let a2 = Account(id: 2) | |
var accounts = Accounts(a0, a1) | |
accounts.prepend(a2) | |
// Current Account: Optional(2), old: Optional(0) | |
accounts.remove(a2) | |
// Current Account: Optional(0), old: Optional(2) | |
accounts.remove(a1) // nothing is triggered! 👍🏼 | |
accounts.remove(a0) | |
// Current Account: nil, old: Optional(0) |
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
// swift 4.1 | |
import Foundation | |
extension DispatchQueue { | |
public static func concurrent(_ label: String) -> DispatchQueue { | |
return DispatchQueue(label: label, attributes: .concurrent) | |
} | |
} |
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
// swift 4.1 | |
public final class Disposable { | |
private let dispose: () -> Void | |
public init(_ dispose: @escaping () -> Void) { | |
self.dispose = dispose | |
} | |
deinit { | |
dispose() | |
} | |
} |
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
// swift 4.1 | |
import Foundation | |
public class MutableVariable<T>: Variable<T> { | |
internal typealias Setter = (T) -> Void | |
private let _setter: Setter | |
public convenience init(_ initial: T, isThreadSafe threadSafe: Bool = false) { | |
self.init(initial, queue: threadSafe == true ? .concurrent("com.shawnthroop.sting.MutableVariable.queue") : nil) | |
} | |
private convenience init(_ initial: T, queue: DispatchQueue?) { | |
var (observers, keys) = MutableVariable.observersAndKeys() | |
var value: T = initial { | |
didSet { | |
for observer in observers.values { | |
observer(value, oldValue) | |
} | |
} | |
} | |
self.init(queue: queue, get: { value }, set: { newValue in value = newValue }) { observer in | |
let key = keys.next()! | |
observers[key] = observer | |
return Disposable { | |
observers[key] = nil | |
} | |
} | |
} | |
internal init(queue: DispatchQueue?, get: @escaping Getter, set: @escaping Setter, addObserver: @escaping AddObserver) { | |
if let queue = queue { | |
_setter = { newValue in | |
queue.async(flags: .barrier) { | |
set(newValue) | |
} | |
} | |
} else { | |
_setter = set | |
} | |
super.init(queue: queue, get: get, addObserver: addObserver) | |
} | |
public override var value: T { | |
get { | |
return super.value | |
} | |
set { | |
_setter(newValue) | |
} | |
} | |
public func performBatchMutation(_ mutation: (inout T) -> Void) { | |
mutation(&value) | |
} | |
public subscript<V>(keyPath: WritableKeyPath<T,V>) -> MutableVariable<V> { | |
return MutableVariable<V>(queue: nil, get: { self.value[keyPath: keyPath] }, set: { newValue in | |
self.value[keyPath: keyPath] = newValue | |
}, addObserver: { observer in | |
self.addObserver { newValue, oldValue in | |
observer(newValue[keyPath: keyPath], oldValue[keyPath: keyPath]) | |
} | |
}) | |
} | |
} | |
extension MutableVariable where T: MutableCollection { | |
public subscript(index: T.Index) -> MutableVariable<T.Element> { | |
return MutableVariable<T.Element>(queue: nil, get: { self.value[index] }, set: { newValue in self.value[index] = newValue }) { observer in | |
self.addObserver { newValue, oldValue in | |
observer(newValue[index], oldValue[index]) | |
} | |
} | |
} | |
} |
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
// swift 4.1 | |
import Foundation | |
/// A reference type that allows observing value types. | |
/// | |
/// - Note: | |
/// Orignal concept from [Swift Talk - S01E63: Mutable Shared Structs](https://talk.objc.io/episodes/S01E63-mutable-shared-structs-part-2) | |
public class Variable<T> { | |
public typealias Observer = (_ newValue: T, _ oldValue: T) -> Void | |
public typealias ObservationPredicate = (_ newValue: T, _ oldValue: T) -> Bool | |
internal typealias Getter = () -> T | |
internal typealias AddObserver = (@escaping Observer) -> Disposable | |
private let _getter: Getter | |
private let _addObserver: AddObserver | |
public convenience init(_ initial: T, isThreadSafe threadSafe: Bool = false) { | |
self.init(initial, queue: threadSafe == true ? .concurrent("com.shawnthroop.sting.Variable.queue") : nil) | |
} | |
private convenience init(_ initial: T, queue: DispatchQueue?) { | |
var (observers, keys) = Variable.observersAndKeys() | |
var value: T = initial { | |
didSet { | |
for observer in observers.values { | |
observer(value, oldValue) | |
} | |
} | |
} | |
self.init(queue: queue, get: { value }) { observer in | |
let key = keys.next()! | |
observers[key] = observer | |
return Disposable { | |
observers[key] = nil | |
} | |
} | |
} | |
internal init(queue: DispatchQueue?, get: @escaping Getter, addObserver: @escaping AddObserver) { | |
if let queue = queue { | |
_getter = { | |
return queue.sync { get() } | |
} | |
_addObserver = { observer in | |
return queue.sync { addObserver(observer) } | |
} | |
} else { | |
_getter = get | |
_addObserver = addObserver | |
} | |
} | |
public var value: T { | |
get { | |
return _getter() | |
} | |
} | |
public func addObserver(_ observer: @escaping Observer) -> Disposable { | |
return _addObserver(observer) | |
} | |
public func addObserver(where predicate: @escaping ObservationPredicate, observer: @escaping Observer) -> Disposable { | |
return addObserver { newValue, oldValue in | |
guard predicate(newValue, oldValue) else { | |
return | |
} | |
observer(newValue, oldValue) | |
} | |
} | |
public subscript<V>(keyPath: KeyPath<T,V>) -> Variable<V> { | |
return Variable<V>(queue: nil, get: { self.value[keyPath: keyPath] }) { observer in | |
self.addObserver { newValue, oldValue in | |
observer(newValue[keyPath: keyPath], oldValue[keyPath: keyPath]) | |
} | |
} | |
} | |
} | |
extension Variable where T: Equatable { | |
public func addChangeObserver(_ observer: @escaping Observer) -> Disposable { | |
return addObserver(where: !=, observer: observer) | |
} | |
} | |
internal extension Variable { | |
static func observersAndKeys() -> (observers: [Int: Observer], keys: CountablePartialRangeFrom<Int>.Iterator) { | |
return ([Int: Observer](), (0...).makeIterator()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment