Skip to content

Instantly share code, notes, and snippets.

@icanswiftabit
Created June 3, 2023 22:31
Show Gist options
  • Save icanswiftabit/573197fb63f83884078babcb68a1a0f9 to your computer and use it in GitHub Desktop.
Save icanswiftabit/573197fb63f83884078babcb68a1a0f9 to your computer and use it in GitHub Desktop.
import SwiftUI
struct ContentState {
var count: Int
}
enum ContentInput {
case increment
case push
}
protocol ContentDelegate: AnyObject {
func showNewView()
}
class ContentViewModel: ViewModel {
@MainActor @Published var state: ContentState
weak var delegate: ContentDelegate?
@MainActor
init(state: ContentState) {
self.state = state
}
@MainActor
func trigger(_ input: ContentInput) async {
switch input {
case .increment:
state.count += 1
case .push:
delegate?.showNewView()
}
}
}
struct ContentView: View {
@StateObject var viewModel: AnyViewModel<ContentState, ContentInput>
var body: some View {
VStack {
Text("Count")
Button("increment") { viewModel.trigger(.increment) }
Button("push") { viewModel.trigger(.push) }
}
.padding()
}
}
import Combine
import Foundation
import SwiftUI
public protocol ViewModel: ObservableObject where ObjectWillChangePublisher.Output == Void {
associatedtype State
associatedtype Input
var state: State { get }
func trigger(_ input: Input) async
}
public extension ViewModel {
func erase() -> AnyViewModel<State, Input> {
AnyViewModel(self)
}
}
extension AnyViewModel: Identifiable where State: Identifiable {
public var id: State.ID {
state.id
}
}
@dynamicMemberLookup
public final class AnyViewModel<State, Input>: ViewModel {
// MARK: Stored properties
private let wrappedObjectWillChange: () -> AnyPublisher<Void, Never>
private let wrappedState: () -> State
private let wrappedTrigger: (Input) async -> Void
// MARK: Computed properties
public var objectWillChange: AnyPublisher<Void, Never> {
wrappedObjectWillChange()
}
public var state: State {
wrappedState()
}
// MARK: Methods
public func trigger(_ input: Input) async {
await wrappedTrigger(input)
}
@discardableResult
public func trigger(_ input: Input) -> Task<Void, Never> {
Task(priority: .userInitiated) { [weak self] in
await self?.trigger(input)
}
}
public subscript<Value>(dynamicMember keyPath: KeyPath<State, Value>) -> Value {
state[keyPath: keyPath]
}
// MARK: Initialization
public init<V: ViewModel>(_ viewModel: V) where V.State == State, V.Input == Input {
self.wrappedObjectWillChange = { viewModel.objectWillChange.eraseToAnyPublisher() }
self.wrappedState = { viewModel.state }
self.wrappedTrigger = viewModel.trigger
}
public func binding<Value>(_ stateValue: KeyPath<State, Value>, with inputCase: @escaping (Value) -> Input) -> Binding<Value> {
Binding<Value>(
get: { self.state[keyPath: stateValue] },
set: { self.trigger(inputCase($0)) }
)
}
init<V: ViewModel>(
_ viewModel: V,
stateMap: @escaping (V.State) -> State,
triggerMap: @escaping (Input) -> V.Input
) {
self.wrappedObjectWillChange = { viewModel.objectWillChange.eraseToAnyPublisher() }
self.wrappedState = { stateMap(viewModel.state) }
self.wrappedTrigger = { await viewModel.trigger(triggerMap($0)) }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment