Skip to content

Instantly share code, notes, and snippets.

@chosa91
Last active May 15, 2020 11:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chosa91/af185d4168f00944bf9c61f36345cb00 to your computer and use it in GitHub Desktop.
Save chosa91/af185d4168f00944bf9c61f36345cb00 to your computer and use it in GitHub Desktop.
Backward compatible reactive ObservableObject
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