Skip to content

Commit

Permalink
Add basic SwiftUIPresenter logic
Browse files Browse the repository at this point in the history
  • Loading branch information
rickclephas committed May 26, 2023
1 parent 521d989 commit b23b7ef
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 66 deletions.
23 changes: 23 additions & 0 deletions circuit-swiftui/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (C) 2022 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
plugins {
kotlin("multiplatform")
alias(libs.plugins.compose)
}

kotlin {
// region KMP Targets
ios()
iosSimulatorArm64()
// endregion

sourceSets {
commonMain {
dependencies {
api(projects.circuitRuntimePresenter)
api(libs.kmmviewmodel.core)
implementation(libs.molecule.runtime)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.slack.circuit.swiftui

import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionClock
import com.rickclephas.kmm.viewmodel.KMMViewModel
import com.slack.circuit.runtime.CircuitUiState
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.presenter.presenterOf

public class SwiftUIPresenter<UiState: CircuitUiState> internal constructor(
private val presenter: Presenter<UiState>
): KMMViewModel() {

private val stateFlow = viewModelScope.launchMolecule(RecompositionClock.Immediate) {
presenter.present()
}

public val state: UiState get() = stateFlow.value
}

public fun <UiState : CircuitUiState> swiftUIPresenterOf(
body: @Composable (Navigator) -> UiState
): SwiftUIPresenter<UiState> {
return SwiftUIPresenter(presenterOf { body(Navigator.NoOp) })
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.slack.circuit.swiftui

import androidx.compose.runtime.Composable
import app.cash.molecule.RecompositionClock
import app.cash.molecule.launchMolecule
import com.rickclephas.kmm.viewmodel.MutableStateFlow
import com.rickclephas.kmm.viewmodel.ViewModelScope
import com.rickclephas.kmm.viewmodel.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

/**
* Identical to the molecule implementation, but with a KMM-ViewModel [MutableStateFlow].
* https://github.com/cashapp/molecule/blob/c902f7f60022911bf0cc6940cf86f3ff07c76591/molecule-runtime/src/commonMain/kotlin/app/cash/molecule/molecule.kt#L102
*/
internal fun <T> ViewModelScope.launchMolecule(
clock: RecompositionClock,
body: @Composable () -> T,
): StateFlow<T> {
var flow: MutableStateFlow<T>? = null
coroutineScope.launchMolecule(
clock = clock,
emitter = { value ->
val outputFlow = flow
if (outputFlow != null) {
outputFlow.value = value
} else {
flow = MutableStateFlow(this, value)
}
},
body = body,
)
return flow!!
}
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ kotlin.mpp.stability.nowarn=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.mpp.androidGradlePluginCompatibility.nowarn=true

kotlin.mpp.enableCInteropCommonization=true

# Enable Gradle configuration caching
# TODO disabled because of compose/kotlin multiplatform
org.gradle.configuration-cache=false
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ detekt = "1.23.0"
dokka = "1.8.10"
eithernet = "1.4.0"
jdk = "19"
kmmviewmodel = "1.0.0-ALPHA-9"
kotlin = "1.8.20"
kotlinpoet = "1.13.2"
kotlinx-coroutines = "1.7.1"
Expand Down Expand Up @@ -172,6 +173,7 @@ eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet
jline = "org.jline:jline:3.23.0"
jsoup = "org.jsoup:jsoup:1.16.1"
junit = "junit:junit:4.13.2"
kmmviewmodel-core = { module = "com.rickclephas.kmm:kmm-viewmodel-core", version.ref = "kmmviewmodel"}
kotlinx-immutable = "org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5"
kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet"}
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet"}
Expand Down
28 changes: 27 additions & 1 deletion samples/counter/apps/Counter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 52;
objects = {

/* Begin PBXBuildFile section */
058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; };
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
1DE43CE32A210A1400EB0E36 /* KMMViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE43CE22A210A1400EB0E36 /* KMMViewModel.swift */; };
1DE43CE52A21281B00EB0E36 /* KMMViewModelState in Frameworks */ = {isa = PBXBuildFile; productRef = 1DE43CE42A21281B00EB0E36 /* KMMViewModelState */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
602DC854301CF43C8E7B0F6D /* Pods_Counter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27D2DFDA1A8E1896075A1701 /* Pods_Counter.framework */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
Expand All @@ -30,6 +32,8 @@
/* Begin PBXFileReference section */
058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
1DE43CE12A2109E500EB0E36 /* KMM-ViewModel */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "KMM-ViewModel"; path = "../../../../../Rick Clephas/KMM-ViewModel"; sourceTree = "<group>"; };
1DE43CE22A210A1400EB0E36 /* KMMViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMMViewModel.swift; sourceTree = "<group>"; };
2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
27D2DFDA1A8E1896075A1701 /* Pods_Counter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Counter.framework; sourceTree = BUILT_PRODUCTS_DIR; };
35C96B8C8A190485ECDD3046 /* Pods-Counter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Counter.debug.xcconfig"; path = "Target Support Files/Pods-Counter/Pods-Counter.debug.xcconfig"; sourceTree = "<group>"; };
Expand All @@ -45,6 +49,7 @@
buildActionMask = 2147483647;
files = (
602DC854301CF43C8E7B0F6D /* Pods_Counter.framework in Frameworks */,
1DE43CE52A21281B00EB0E36 /* KMMViewModelState in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -68,9 +73,18 @@
path = "Preview Content";
sourceTree = "<group>";
};
1DE43CE02A2109E500EB0E36 /* Packages */ = {
isa = PBXGroup;
children = (
1DE43CE12A2109E500EB0E36 /* KMM-ViewModel */,
);
name = Packages;
sourceTree = "<group>";
};
7555FF72242A565900829871 = {
isa = PBXGroup;
children = (
1DE43CE02A2109E500EB0E36 /* Packages */,
7555FF7D242A565900829871 /* Counter */,
7555FF7C242A565900829871 /* Products */,
7555FFB0242A642200829871 /* Frameworks */,
Expand All @@ -94,6 +108,7 @@
7555FF8C242A565B00829871 /* Info.plist */,
2152FB032600AC8F00CF470E /* iOSApp.swift */,
058557D7273AAEEB004C7B11 /* Preview Content */,
1DE43CE22A210A1400EB0E36 /* KMMViewModel.swift */,
);
path = Counter;
sourceTree = "<group>";
Expand Down Expand Up @@ -125,6 +140,9 @@
dependencies = (
);
name = Counter;
packageProductDependencies = (
1DE43CE42A21281B00EB0E36 /* KMMViewModelState */,
);
productName = Counter;
productReference = 7555FF7B242A565900829871 /* Counter.app */;
productType = "com.apple.product-type.application";
Expand Down Expand Up @@ -221,6 +239,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1DE43CE32A210A1400EB0E36 /* KMMViewModel.swift in Sources */,
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
7555FF83242A565900829871 /* ContentView.swift in Sources */,
);
Expand Down Expand Up @@ -425,6 +444,13 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCSwiftPackageProductDependency section */
1DE43CE42A21281B00EB0E36 /* KMMViewModelState */ = {
isa = XCSwiftPackageProductDependency;
productName = KMMViewModelState;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 7555FF73242A565900829871 /* Project object */;
}
30 changes: 5 additions & 25 deletions samples/counter/apps/Counter/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import SwiftUI
import counter
import KMMViewModelState

struct ContentView: View {
@ObservedObject var presenter = SwiftCounterPresenter()
@ObservedViewModelState var state = PresenterFactory.shared.counterPresenter()

var body: some View {
NavigationView {
VStack(alignment: .center) {
Text("Count \(presenter.state?.count ?? 0)")
Text("Count \(state.count)")
.font(.system(size: 36))
HStack(spacing: 10) {
Button(action: {
presenter.state?.eventSink(CounterScreenEventDecrement.shared)
state.eventSink(CounterScreenEventDecrement.shared)
}) {
Text("-")
.font(.system(size: 36, weight: .black, design: .monospaced))
Expand All @@ -20,7 +21,7 @@ struct ContentView: View {
.foregroundColor(.white)
.background(Color.blue)
Button(action: {
presenter.state?.eventSink(CounterScreenEventIncrement.shared)
state.eventSink(CounterScreenEventIncrement.shared)
}) {
Text("+")
.font(.system(size: 36, weight: .black, design: .monospaced))
Expand All @@ -35,27 +36,6 @@ struct ContentView: View {
}
}

// TODO we hide all this behind the Circuit UI interface somehow? Then we can pass it state only
@MainActor
class SwiftCounterPresenter: BasePresenter<CounterScreenState> {
init() {
// TODO why can't swift infer these generics?
super.init(
delegate: SwiftSupportKt.asSwiftPresenter(SwiftSupportKt.doNewCounterPresenter())
as! SwiftPresenter<CounterScreenState>)
}
}

class BasePresenter<T: AnyObject>: ObservableObject {
@Published var state: T? = nil

init(delegate: SwiftPresenter<T>) {
delegate.subscribe { state in
self.state = state
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
Expand Down
19 changes: 19 additions & 0 deletions samples/counter/apps/Counter/KMMViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// KMMViewModel.swift
// Counter
//
// Created by Rick Clephas on 26/05/2023.
// Copyright © 2023 orgName. All rights reserved.
//

import KMMViewModelCore
import KMMViewModelState
import counter

extension Kmm_viewmodel_coreKMMViewModel: KMMViewModel { }

extension ObservedViewModelState {
init(wrappedValue: ViewModel) where ViewModel: Circuit_swiftuiSwiftUIPresenter<State> {
self.init(wrappedValue: wrappedValue, \.state)
}
}
6 changes: 5 additions & 1 deletion samples/counter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ kotlin {
}
}
maybeCreate("commonTest").apply { dependencies { implementation(libs.kotlin.test) } }
val iosMain by sourceSets.getting
val iosMain by sourceSets.getting {
dependencies {
implementation(projects.circuitSwiftui)
}
}
val iosSimulatorArm64Main by sourceSets.getting
// Set up dependencies between the source sets
iosSimulatorArm64Main.dependsOn(iosMain)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.slack.circuit.sample.counter

import com.slack.circuit.swiftui.swiftUIPresenterOf

object PresenterFactory {
fun counterPresenter() = swiftUIPresenterOf { CounterPresenter(it) }
fun primePresenter(number: Int) = swiftUIPresenterOf { PrimePresenter(it, number) }
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ include(
":circuit-runtime",
":circuit-runtime-presenter",
":circuit-runtime-ui",
":circuit-swiftui",
":circuit-test",
":samples:counter",
":samples:counter:apps",
Expand Down

0 comments on commit b23b7ef

Please sign in to comment.