A minimal implementation of State and Binding with Swift

One of my most favorite thing about SwiftUI is neither the declarative API nor the mystical fancy syntax but rather the philosophy of one directional flow of data and event. I’ve been trying to put that philosophy into practice for quite a while now with whatever framework I use.

The core concepts of the philosophy are basically thinking about who owns the data and who handles the events. And once you’ve figured out these decisions then the rest of the plumbing can be done with whatever tools you have handy. Could be RxSwift, Combine, completion handlers, notification center or even KVO. The cherry on the top could be fancy syntactic sugar with @propertyWrapper or @dynamicMemberLookup.

From SwiftUI I like the idea of using @State and @Binding to give a clear hint on who owns the data and who is just borrowing it. With that in mind I came up with this minimal State and Binding constructs:

@dynamicMemberLookup
class Variable<T> {
    var value: T {
        get { sub.value }
        set { sub.value = newValue }
    }
    
    var stream: AnyPublisher<T, Never> {
        return sub.eraseToAnyPublisher()
    }
    
    subscript<P>(dynamicMember keyPath: WritableKeyPath<T, P>) -> P {
        get { sub.value[keyPath: keyPath] }
        set { sub.value[keyPath: keyPath] = newValue }
    }
    
    fileprivate let sub: CurrentValueSubject<T, Never>
    
    init(_ sub: CurrentValueSubject<T, Never>) {
        self.sub = sub
    }
}

class State<T>: Variable<T> {
    var binding: Binding<T> {
        return Binding(self)
    }

    init(_ value: T) {
        super.init(CurrentValueSubject(value))
    }
}

class Binding<T>: Variable<T> {
    init(_ state: State<T>) {
        super.init(state.sub)
    }
}

The idea is that the State can be initialized with the data to indicate that it owns the data, while Binding can not. So the only way to create a Binding object is from a State. All the common functionality like reading and writing data is moved to a shared base class Variable which apart from exposing the value also provides a stream that be used to listen to changes and also supports dynamicMemberLookup to provide a shortcut to accessing properties of the wrapped type.

Here’s an example to illustrate the usage:

struct Story {
    var text: String?
}
class ViewController: UIViewController {
    // owns the data
    let story = State(Story())

    let label = UILabel(frame: .zero)
    var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        playground.run()
        view.addSubview(label)
        label.backgroundColor = .white
        label.textAlignment = .center
        view.backgroundColor = .gray

        // update the word count at every keystroke
        story.stream
            .map(\.text)
            .map { $0?.count ?? 0}
            .map { "Words: \($0)" }
            .assign(to: \.title, on: self)
            .store(in: &cancellables)
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit,
                                                            target: self,
                                                            action: #selector(onEdit))
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        label.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width - 40,
                                                         height: 100))
        label.center = view.center
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // update the content whenever view is displayed
        // notice that we're not using:
        //      label.text = story.value.text
        label.text = story.text
    }
    
    @objc func onEdit() {
        let textEditVwCtrl = TextEditViewController(story: story.binding)
        navigationController?.pushViewController(textEditVwCtrl, animated: true)
    }
}
class TextEditViewController: UIViewController, UITextViewDelegate {
    // borrows the data with a read-write access
    private let story: Binding<Story>

    init(story: Binding<Story>) {
        self.story = story
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    let textVw = UITextView(frame: .zero)

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(textVw)
        textVw.text = story.text
        textVw.delegate = self
        title = "Story Editor"
        view.backgroundColor = .white
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        textVw.frame = view.bounds
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        textVw.becomeFirstResponder()
    }

    func textViewDidChange(_ textView: UITextView) {
        // notice that we're not using:
        //      story.value.text = textView.text
        story.text = textView.text
    }
}

I’ve a feeling the syntax could be made even fancier with the help of @propertyWrapper to look something like what SwiftUI does with @State var story = Story() and @Binding var storyCopy = $story but maybe I’ll leave that as the task for the reader.