Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwiftUI integration #2

Open
andersio opened this issue May 11, 2020 · 1 comment
Open

SwiftUI integration #2

andersio opened this issue May 11, 2020 · 1 comment
Labels
enhancement New feature or request

Comments

@andersio
Copy link
Member

andersio commented May 11, 2020

Loop may offer in-built SwiftUI integration on Apple platforms without creating an extra "LoopUI" package.

Loop only needs to provide standard data funnels to connect feedback loops with SwiftUI, and has no involvement in UI concerns except for the two characters in import SwiftUI. Everything else is SwiftUI's declarative UI realm (data to virtual DOM, vDOM to native view, etc). That is in contrast to our UIKit sibling ReactiveCocoa, which is a heavy UI-focused library that (1) attempted to solve AppKit/UIKit reactive programming at a micro level (KVO-like property bindings), and (2) wraps parts of Objective-C dynamism into friendly Swift APIs.

Since SwiftUI is shipped with the system, no friction is incurred for dependency management on users' end. All these collectively makes it perfect for Loop to offer straightly the said utilities.

Such integration should be excluded from Linux builds.

Concepts

Concepts are built upon the implicit behavior of DynamicProperty, which serves as a clue for SwiftUI runtime to look inside the property wrapper, so as to pick up embedded special wrappers like @State, @ObservedObject and @Environment.

This enables us to build custom property wrappers that provide simple dev experience, while hiding the heavy lifting of bridging feedback loops to SwiftUI world.

Direct state binding like @ObservedObject:

Bound view pseudo code
typealias WeatherStore = Store<WeatherState, WeatherAction>

struct WeatherView: View {
    @WeatherStore.Binding
    var state: WeatherState

    init(store: WeatherStore) {
      _state = store.binding()
    }

    var body: some Body {
        VStack {
            Spacer()

            Text("Current temperature: \(state.temperature)")
                + Text(" \(state.unit)").font(.system(size: 10.0)).baselineOffset(7.0)
            Spacer()
            Button()
                action: { self.$state.perform(.refresh) }
                label: { Text("Refresh 🔄") }

            Spacer()
    }
}

SwiftUI environment injected state bindings

Note: Unlike @State and @ObservedObject, it hasn't been tested whether @EnvironmentObject would work.

Bound view pseudo code
typealias WeatherStore = Store<WeatherState, WeatherAction>

struct WeatherView: View {
    @WeatherStore.EnvironmentBinding
    var state: WeatherStore.State

    var body: some Body {
        VStack {
            Spacer()

            Text("Current temperature: \(state.temperature)")
                + Text(" \(state.unit)").font(.system(size: 10.0)).baselineOffset(7.0)
            Spacer()
            Button()
                action: { self.$state.perform(.refresh) }
                label: { Text("Refresh 🔄") }

            Spacer()
    }
}
Parent view pseudo code
typealias WeatherStore = Store<WeatherState, WeatherAction>
let weatherStore = WeatherStore()

struct ContentView: View {
    var body: some Body {
        WeatherView()
            .environmentObject(weatherStore)
    }
}

Partial store

Already supported via Store.view(value:event:). e.g. injecting via @EnvironmentBinding a partial store that exposes only state & events related to radar images.

typealias WeatherStore = Store<WeatherState, WeatherAction>
let weatherStore = WeatherStore()

struct ContentView: View {
    var body: some Body {
        RadarView()
            .environmentObject(
                weatherStore.view
                    value: \WeatherState.radarImages
                    event: WeatherAction.radar
            )
    }
}
@sergdort
Copy link
Contributor

sergdort commented Jun 2, 2020

I'm wondering if we need something similar to TCA IfLetStore

public struct IfLetBinding<State, Action, IfContent: View, ElseContent: View>: View {
    public let binding: LoopBinding<State?, Action>
    public let ifContent: (LoopBinding<State, Action>) -> IfContent
    public let elseContent: () -> ElseContent

    public init(
        _ binding: LoopBinding<State?, Action>,
        then ifContent: @escaping (LoopBinding<State, Action>) -> IfContent,
        else elseContent: @escaping @autoclosure () -> ElseContent
    ) {
        self.binding = binding
        self.ifContent = ifContent
        self.elseContent = elseContent
    }

    public var body: some View {
        Group<_ConditionalContent<IfContent, ElseContent>> {
            if let state = binding.wrappedValue {
                return ViewBuilder.buildEither(
                    first: self.ifContent(
                        self.binding.scoped(
                            to: { $0 ?? state },
                            event: { $0 }
                        )
                    )
                )
            } else {
                return ViewBuilder.buildEither(second: self.elseContent())
            }
        }
    }
}

I've been playing with it here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants