Skip to content

Instantly share code, notes, and snippets.

@alexpiezo
Last active June 15, 2022 04:42
Show Gist options
  • Save alexpiezo/5b4b3a9ffbd32a724b7196a1711e6ff1 to your computer and use it in GitHub Desktop.
Save alexpiezo/5b4b3a9ffbd32a724b7196a1711e6ff1 to your computer and use it in GitHub Desktop.
import Combine
import SwiftUI
@dynamicMemberLookup public protocol ViewModelBinding {
associatedtype State
associatedtype Input
var state: State { get }
mutating func bind(with input: PassthroughSubject<Input, Never>)
}
public extension ViewModelBinding {
subscript<Subject>(dynamicMember keyPath: KeyPath<State, Subject>) -> Subject {
state[keyPath: keyPath]
}
}
@propertyWrapper @frozen public struct ViewModel<ObjectType: ViewModelBinding>: DynamicProperty where ObjectType.State: ObservableObject {
@dynamicMemberLookup public struct Wrapper {
var wrappedObject: ObjectType
private var actionSubject: PassthroughSubject<ObjectType.Input, Never>
init(wrappedObject: ObjectType) {
actionSubject = PassthroughSubject<ObjectType.Input, Never>()
self.wrappedObject = wrappedObject
self.wrappedObject.bind(with: actionSubject)
}
subscript<Subject>(dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType.State, Subject>) -> Binding<Subject> {
Binding {
wrappedObject.state[keyPath: keyPath]
} set: { newValue in
wrappedObject.state[keyPath: keyPath] = newValue
}
}
public func trigger(_ input: ObjectType.Input) {
actionSubject.send(input)
}
}
private var _wrapper: Wrapper
private var _object: ObservedObject<ObjectType.State>
public var wrappedValue: ObjectType {
get { _wrapper.wrappedObject }
set { _wrapper.wrappedObject = newValue }
}
public var projectedValue: Wrapper {
_wrapper
}
public init(wrappedValue: ObjectType) {
_wrapper = Wrapper(wrappedObject: wrappedValue)
_object = ObservedObject(initialValue: wrappedValue.state)
}
}
// MARK: Implementation
class SimpleState: ObservableObject {
@Published var text = "1"
@Published var text2 = "2"
}
enum SimpleInput {
case action
}
class SimpleViewModel: ViewModelBinding {
var state = SimpleState()
private var cancellable = Set<AnyCancellable>()
func bind(with input: PassthroughSubject<SimpleInput, Never>) {
state.$text.sink {
print($0)
}.store(in: &cancellable)
input
.filter { $0 == .action }
.sink { _ in
self.state.text2 = "3"
}.store(in: &cancellable)
}
}
struct ContentView: View {
@ViewModel var vm = SimpleViewModel()
var body: some View {
VStack {
Text("Text1 \(vm.text)")
Text("Text2 \(vm.text2)")
TextField("", text: $vm.text)
Button("Save") {
$vm.trigger(.action)
}
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding(30)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment