-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[swift-helpers] Move Swift helpers to dreimultiplatform
- Loading branch information
1 parent
a1b4830
commit 45b5627
Showing
6 changed files
with
284 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// | ||
// Dispatch.swift | ||
// Barryvox | ||
// | ||
// Created by Laila Becker on 01.11.22. | ||
// Copyright © 2022 dreipol GmbH. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
public struct Dispatcher { | ||
public typealias Thunk = ((@escaping (Any) -> Any, @escaping () -> ApplicationState, Any?) -> Any) | ||
|
||
private let dispatch: (Any) -> Void | ||
|
||
fileprivate init(dispatch: @escaping (Any) -> Void) { | ||
self.dispatch = dispatch | ||
} | ||
|
||
public func callAsFunction(_ action: Any) { | ||
dispatch(action) | ||
} | ||
|
||
public func callAsFunction(_ thunk: @escaping Thunk) { | ||
dispatch(ThunksKt.createThunkAction(thunk: thunk)) | ||
} | ||
} | ||
|
||
@propertyWrapper public struct Dispatch: DynamicProperty { | ||
@EnvironmentObject private var observableStore: ObservableStore | ||
|
||
public init() {} | ||
|
||
public var wrappedValue: Dispatcher { | ||
return Dispatcher { action in | ||
_ = observableStore.store.dispatch(action) | ||
} | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// | ||
// NavigationReduxMapper.swift | ||
// Barryvox | ||
// | ||
// Created by Laila Becker on 02.11.22. | ||
// Copyright © 2022 dreipol GmbH. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
private extension Collection where Index: Strideable, Index.Stride: SignedInteger { | ||
func slidingPairs() -> some RandomAccessCollection<(Element, Element)> { | ||
(startIndex ..< endIndex) | ||
.map { i in | ||
(self[i], self[index(after: i)]) | ||
} | ||
} | ||
} | ||
|
||
public struct NavigationReduxMapper { | ||
private static func getDestination<Current, Destination>(state: ApplicationState, | ||
current: Current.Type, | ||
destination: Destination.Type) -> Destination? | ||
where Current: Screen, Destination: Screen { | ||
state.navigationState.screens.slidingPairs().last { (from, to) in | ||
from is Current && to is Destination | ||
}?.1 as? Destination | ||
} | ||
|
||
public static func from<Current, Destination>(_ current: Current.Type, to destination: Destination.Type) -> ReduxMapper<Bool, Bool> | ||
where Current: Screen, Destination: Screen { | ||
ReduxMapper { state in | ||
getDestination(state: state, current: Current.self, destination: Destination.self) != nil | ||
} action: { dispatch, state, newValue in | ||
if !newValue && state.navigationState.screens.last is Destination { | ||
dispatch(NavigationAction.Back()) | ||
} | ||
} | ||
} | ||
|
||
public static func destinationInfo<Current, Destination>(from current: Current.Type, | ||
to destination: Destination.Type) -> ReduxStateGetter<Destination?, Destination?> | ||
where Current: Screen, Destination: Screen { | ||
ReduxStateGetter { state in | ||
getDestination(state: state, current: Current.self, destination: Destination.self) | ||
} | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// | ||
// ObservableStore.swift | ||
// Multiplatform Redux Sample | ||
// | ||
// Created by Samuel Bichsel on 30.10.20. | ||
// Copyright © 2020 dreipol GmbH. All rights reserved. | ||
// | ||
|
||
import Foundation | ||
|
||
public final class ObservableStore: ObservableObject { | ||
let store: TypedStore | ||
|
||
public init(store: TypedStore) { | ||
self.store = store | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// | ||
// ReduxState.swift | ||
// Barryvox | ||
// | ||
// Created by Laila Becker on 01.11.22. | ||
// Copyright © 2022 dreipol GmbH. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
public protocol ReduxGetter { | ||
associatedtype Value: Equatable | ||
associatedtype SwiftValue | ||
|
||
var mapper: (ApplicationState) -> Value { get } | ||
var swiftMapper: (Value) -> SwiftValue { get } | ||
} | ||
|
||
public struct ReduxMapper<Value: Equatable, SwiftValue>: ReduxGetter { | ||
public var mapper: (ApplicationState) -> Value | ||
public var swiftMapper: (Value) -> SwiftValue | ||
fileprivate var action: (_ dispatch: Dispatcher, _ state: ApplicationState, _ newValue: SwiftValue) -> Void | ||
|
||
public init(mapper: @escaping (ApplicationState) -> Value, | ||
swiftMapper: @escaping (Value) -> SwiftValue, | ||
action: @escaping (_: Dispatcher, _: ApplicationState, _: SwiftValue) -> Void) { | ||
self.mapper = mapper | ||
self.swiftMapper = swiftMapper | ||
self.action = action | ||
} | ||
} | ||
|
||
public extension ReduxMapper where Value == SwiftValue { | ||
init(mapper: @escaping (ApplicationState) -> Value, action: @escaping (_: Dispatcher, _: ApplicationState, _: Value) -> Void) { | ||
self.init(mapper: mapper, swiftMapper: { $0 }, action: action) | ||
} | ||
} | ||
|
||
public struct ReduxStateGetter<Value: Equatable, SwiftValue>: ReduxGetter { | ||
public var mapper: (ApplicationState) -> Value | ||
public var swiftMapper: (Value) -> SwiftValue | ||
} | ||
|
||
public extension ReduxStateGetter where Value == SwiftValue { | ||
init(mapper: @escaping (ApplicationState) -> Value) { | ||
self.init(mapper: mapper, swiftMapper: { $0 }) | ||
} | ||
} | ||
|
||
// MARK: - Property wrappers | ||
|
||
@propertyWrapper public struct ReduxState<Value: Equatable, SwiftValue>: SubscribedProperty { | ||
typealias Getter = ReduxMapper<Value, SwiftValue> | ||
|
||
private var mapper: ReduxMapper<Value, SwiftValue> | ||
fileprivate var getter: ReduxMapper<Value, SwiftValue> { mapper } | ||
|
||
@EnvironmentObject fileprivate var observableStore: ObservableStore | ||
@Dispatch private var dispatch: Dispatcher | ||
|
||
@State fileprivate var currentValue: Value? | ||
fileprivate let subscriptionHolder: SubscriptionHolder = .init() | ||
|
||
public init(_ mapper: ReduxMapper<Value, SwiftValue>) { | ||
self.mapper = mapper | ||
} | ||
|
||
public var wrappedValue: SwiftValue { | ||
get { | ||
mapper.swiftMapper(currentValue ?? mapper.mapper(observableStore.store.applicationState)) | ||
} | ||
nonmutating set { | ||
mapper.action(dispatch, observableStore.store.applicationState, newValue) | ||
} | ||
} | ||
|
||
public var projectedValue: Binding<SwiftValue> { | ||
Binding { | ||
wrappedValue | ||
} set: { newValue in | ||
wrappedValue = newValue | ||
} | ||
} | ||
} | ||
|
||
@propertyWrapper public struct GetReduxState<Getter: ReduxGetter>: SubscribedProperty { | ||
fileprivate var getter: Getter | ||
|
||
@EnvironmentObject fileprivate var observableStore: ObservableStore | ||
|
||
@State fileprivate var currentValue: Getter.Value? | ||
fileprivate let subscriptionHolder: SubscriptionHolder = .init() | ||
|
||
public init(_ getter: Getter) { | ||
self.getter = getter | ||
} | ||
|
||
public var wrappedValue: Getter.SwiftValue { | ||
getter.swiftMapper(currentValue ?? getter.mapper(observableStore.store.applicationState)) | ||
} | ||
} | ||
|
||
private class SubscriptionHolder { | ||
var subscription: (() -> KotlinUnit)? | ||
|
||
func subscribe(to store: TypedStore, receive: @escaping (ApplicationState) -> Void) { | ||
guard subscription == nil else { | ||
return | ||
} | ||
|
||
subscription = store.subscribe { | ||
receive(store.applicationState) | ||
return KotlinUnit() | ||
} | ||
} | ||
|
||
deinit { | ||
_ = subscription?() | ||
} | ||
} | ||
|
||
private protocol SubscribedProperty: DynamicProperty { | ||
associatedtype Getter: ReduxGetter | ||
|
||
var getter: Getter { get } | ||
var observableStore: ObservableStore { get } | ||
var currentValue: Getter.Value? { get nonmutating set } | ||
var subscriptionHolder: SubscriptionHolder { get } | ||
} | ||
|
||
private extension SubscribedProperty { | ||
public func update() { | ||
subscriptionHolder.subscribe(to: observableStore.store) { newState in | ||
let newValue = getter.mapper(newState) | ||
if newValue != currentValue { | ||
currentValue = newValue | ||
} | ||
} | ||
} | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// | ||
// ReduxStateSequence.swift | ||
// Vaduz | ||
// | ||
// Created by Laila Becker on 19.06.2024. | ||
// Copyright © 2024 dreipol GmbH. All rights reserved. | ||
// | ||
|
||
import SwiftUI | ||
|
||
@propertyWrapper | ||
public struct ReduxStateSequence<Getter: ReduxGetter>: DynamicProperty { | ||
nonisolated private let getter: Getter | ||
|
||
@EnvironmentObject private var observableStore: ObservableStore | ||
|
||
public init(_ getter: Getter) { | ||
self.getter = getter | ||
} | ||
|
||
// TODO: prefer this version once iOS 17 support is dropped | ||
// @available(iOS 18, *) | ||
// var wrappedValue: some AsyncSequence<Getter.SwiftValue, Never> { | ||
public var wrappedValue: AsyncMapSequence<SkieSwiftOptionalFlow<Any>, Getter.SwiftValue?> { | ||
observableStore.store | ||
.flowOf { | ||
// swiftlint:disable:next force_cast | ||
let state = $0 as! ApplicationState | ||
return getter.mapper(state) | ||
} | ||
.map { | ||
guard let kotlinValue = $0 as? Getter.Value else { | ||
return nil | ||
} | ||
|
||
return getter.swiftMapper(kotlinValue) | ||
} | ||
} | ||
} |