Skip to content

Instantly share code, notes, and snippets.

@trszdev
Last active April 15, 2022 20:15
Show Gist options
  • Save trszdev/1c86091165adbb148bd6810069c10db0 to your computer and use it in GitHub Desktop.
Save trszdev/1c86091165adbb148bd6810069c10db0 to your computer and use it in GitHub Desktop.
ViewModel behind protocol (2 approaches)
import SwiftUI
import Combine
// Protocol 'NonConstrainedViewModel' can only be used as a generic constraint
// because it has Self or associated type requirements
protocol NonConstrainedViewModel: ObservableObject {
var state: Int { get set }
var statePublished: Published<Int> { get }
var statePublisher: Published<Int>.Publisher { get }
func increase()
}
struct State: Equatable {
struct Value: Equatable {
var value = 2.0
}
var value = Value()
}
final class NonConstrainedViewModelImpl: NonConstrainedViewModel {
@Published var state = 1
var statePublished: Published<Int> { _state }
var statePublisher: Published<Int>.Publisher { $state }
func increase() {
state += 1
}
}
protocol ViewModel {
var state: State { get set }
func increase()
}
@dynamicMemberLookup
final class AnyObservableObject<Object>: ObservableObject {
@dynamicMemberLookup
final class Bind {
private var object: Object
init(object: Object) {
self.object = object
}
subscript<T>(dynamicMember keyPath: WritableKeyPath<Object, T>) -> Binding<T> {
.init {
self.object[keyPath: keyPath]
} set: {
self.object[keyPath: keyPath] = $0
}
}
}
init(object: Object, objectWillChange: AnyPublisher<Void, Never>) {
self.object = object
self.bind = Bind(object: object)
self.objectWillChange = objectWillChange
}
let object: Object
let bind: Bind
let objectWillChange: AnyPublisher<Void, Never>
subscript<T>(dynamicMember keyPath: KeyPath<Object, T>) -> T {
object[keyPath: keyPath]
}
}
postfix operator %
postfix func %<Object>(lhs: AnyObservableObject<Object>) -> AnyObservableObject<Object>.Bind {
lhs.bind
}
extension ObservableObject where ObjectWillChangePublisher.Output == Void {
func eraseTo<ViewModel>(_ viewModelType: ViewModel.Type) -> AnyObservableObject<ViewModel> {
let selfVM = self as! ViewModel
return AnyObservableObject(object: selfVM, objectWillChange: objectWillChange.eraseToAnyPublisher())
}
}
final class ViewModelImpl: ObservableObject, ViewModel {
@Published var state = State()
func increase() {
state.value.value += 2
}
}
struct SampleView: View {
@ObservedObject var viewModel: AnyObservableObject<ViewModel>
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
Slider(value: viewModel%.state.value.value)
Button {
viewModel.object.increase()
} label: {
Text(verbatim: "\(viewModel.state.value.value)")
}
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text(verbatim: "dismiss")
}
}
.onChange(of: viewModel.state) { state in
print("New state: \(state)")
}
}
static var hostingVC: UIViewController {
let vm = ViewModelImpl()
let view = SampleView(viewModel: vm.eraseTo(ViewModel.self))
return UIHostingController(rootView: view)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment