Created
June 3, 2023 22:31
-
-
Save icanswiftabit/573197fb63f83884078babcb68a1a0f9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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