Skip to content

Commit

Permalink
[swift-helpers] Move Swift helpers to dreimultiplatform
Browse files Browse the repository at this point in the history
  • Loading branch information
lailabecker committed Aug 21, 2024
1 parent a1b4830 commit 45b5627
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plugins {
id("org.jetbrains.kotlin.multiplatform")
id("maven-publish")
id("signing")
id("co.touchlab.skie") version "0.8.4"
}
apply plugin: "org.jlleitschuh.gradle.ktlint"
apply plugin: "org.jetbrains.dokka"
Expand Down
39 changes: 39 additions & 0 deletions src/iosMain/swift/Dispatch.swift
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)
}
}
}
48 changes: 48 additions & 0 deletions src/iosMain/swift/NavigationReduxMapper.swift
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)
}
}
}
17 changes: 17 additions & 0 deletions src/iosMain/swift/ObservableStore.swift
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
}
}
140 changes: 140 additions & 0 deletions src/iosMain/swift/ReduxState.swift
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
}
}
}
}
39 changes: 39 additions & 0 deletions src/iosMain/swift/ReduxStateSequence.swift
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)
}
}
}

0 comments on commit 45b5627

Please sign in to comment.