Last active
May 15, 2020 11:09
-
-
Save chosa91/af185d4168f00944bf9c61f36345cb00 to your computer and use it in GitHub Desktop.
Backward compatible reactive ObservableObject
This file contains 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 Foundation | |
import UIKit | |
// Source: https://www.swiftbysundell.com/articles/published-properties-in-swift/ | |
// MARK: - List | |
struct List<Value> { | |
private(set) var firstNode: Node? | |
private(set) var lastNode: Node? | |
} | |
extension List { | |
class Node { | |
var value: Value | |
fileprivate(set) weak var previous: Node? | |
fileprivate(set) var next: Node? | |
init(value: Value) { | |
self.value = value | |
} | |
} | |
@discardableResult | |
mutating func append(_ value: Value) -> Node { | |
let node = Node(value: value) | |
node.previous = lastNode | |
lastNode?.next = node | |
lastNode = node | |
if firstNode == nil { | |
firstNode = node | |
} | |
return node | |
} | |
mutating func remove(_ node: Node) { | |
node.previous?.next = node.next | |
node.next?.previous = node.previous | |
// Using "triple-equals" we can compare two class | |
// instances by identity, rather than by value: | |
if firstNode === node { | |
firstNode = node.next | |
} | |
if lastNode === node { | |
lastNode = node.previous | |
} | |
// Completely disconnect the node by removing its | |
// sibling references: | |
node.next = nil | |
node.previous = nil | |
} | |
} | |
extension List: Sequence { | |
func makeIterator() -> AnyIterator<Value> { | |
var node = firstNode | |
return AnyIterator { | |
// Iterate through all of our nodes by continuously | |
// moving to the next one and extract its value: | |
let value = node?.value | |
node = node?.next | |
return value | |
} | |
} | |
} | |
// MARK: - MutableReference | |
class Reference<Value> { | |
fileprivate(set) var value: Value | |
init(value: Value) { | |
self.value = value | |
} | |
} | |
class MutableReference<Value>: Reference<Value> { | |
func update(with value: Value) { | |
self.value = value | |
} | |
} | |
// MARK: - Cancellable | |
class Cancellable { | |
private var closure: (() -> Void)? | |
init(closure: @escaping () -> Void) { | |
self.closure = closure | |
} | |
deinit { | |
cancel() | |
} | |
func cancel() { | |
closure?() | |
closure = nil | |
} | |
} | |
// MARK: - Published | |
@propertyWrapper | |
struct Published<Value> { | |
var projectedValue: Published { self } | |
var wrappedValue: Value { | |
didSet { | |
valueDidChange() | |
} | |
} | |
private var observations = MutableReference( | |
value: List<(Value) -> Void>() | |
) | |
init(wrappedValue: Value) { | |
self.wrappedValue = wrappedValue | |
} | |
} | |
private extension Published { | |
func valueDidChange() { | |
for closure in observations.value { | |
closure(wrappedValue) | |
} | |
} | |
func observe(_ closure: @escaping (Value) -> Void) -> Cancellable { | |
// To further mimmic Combine's behaviors, we'll call | |
// each observation closure as soon as it's attached to | |
// our property: | |
closure(wrappedValue) | |
let node = observations.value.append(closure) | |
return Cancellable { [weak observations] in | |
observations?.value.remove(node) | |
} | |
} | |
} | |
// MARK: - TEST | |
struct User { | |
let name: String | |
} | |
class ProfileViewModel: ObservableObject { | |
enum State { | |
case isLoading | |
case failed(Error) | |
case loaded(User) | |
} | |
// Simply marking a property with the @Published property wrapper | |
// is enough to make the system emit observable events whenever | |
// a new value was assigned to it. | |
@Published var state = State.isLoading | |
} | |
class ProfileViewController: UIViewController { | |
private let viewModel: ProfileViewModel | |
private var cancellable: Cancellable? | |
init(viewModel: ProfileViewModel) { | |
self.viewModel = viewModel | |
super.init(nibName: nil, bundle: nil) | |
} | |
required init?(coder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
cancellable = viewModel.$state.observe { [weak self] value in | |
self?.render(value) | |
} | |
} | |
private func render(_ state: ProfileViewModel.State) { | |
print("in:", state) | |
} | |
} | |
// MARK: - Mock | |
enum ProfileError: Error { | |
case notFound | |
} | |
var model = ProfileViewModel() | |
let sut = ProfileViewController(viewModel: model) | |
sut.viewDidLoad() | |
var anotherObserver: Cancellable? = model.$state.observe { state in | |
print("out:", state) | |
} | |
model.state = .failed(ProfileError.notFound) | |
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { | |
model.state = .loaded(User(name: "Bob")) | |
anotherObserver = nil | |
} | |
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { | |
model.state = .loaded(User(name: "Bob Marley")) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment