diff --git a/README.md b/README.md index 2c51222d..c6ec8472 100755 --- a/README.md +++ b/README.md @@ -40,18 +40,18 @@ allprojects { project build.gradle ```groovy dependencies { - commonMainApi("dev.icerock.moko:mvvm-core:0.12.0") // only ViewModel, EventsDispatcher, Dispatchers.UI - commonMainApi("dev.icerock.moko:mvvm-livedata:0.12.0") // api mvvm-core, LiveData and extensions - commonMainApi("dev.icerock.moko:mvvm-state:0.12.0") // api mvvm-livedata, ResourceState class and extensions - commonMainApi("dev.icerock.moko:mvvm-livedata-resources:0.12.0") // api mvvm-core, moko-resources, extensions for LiveData with moko-resources + commonMainApi("dev.icerock.moko:mvvm-core:0.13.0") // only ViewModel, EventsDispatcher, Dispatchers.UI + commonMainApi("dev.icerock.moko:mvvm-livedata:0.13.0") // api mvvm-core, LiveData and extensions + commonMainApi("dev.icerock.moko:mvvm-state:0.13.0") // api mvvm-livedata, ResourceState class and extensions + commonMainApi("dev.icerock.moko:mvvm-livedata-resources:0.13.0") // api mvvm-core, moko-resources, extensions for LiveData with moko-resources - androidMainApi("dev.icerock.moko:mvvm-livedata-material:0.12.0") // api mvvm-livedata, Material library android extensions - androidMainApi("dev.icerock.moko:mvvm-livedata-glide:0.12.0") // api mvvm-livedata, Glide library android extensions - androidMainApi("dev.icerock.moko:mvvm-livedata-swiperefresh:0.12.0") // api mvvm-livedata, SwipeRefreshLayout library android extensions - androidMainApi("dev.icerock.moko:mvvm-databinding:0.12.0") // api mvvm-livedata, DataBinding support for Android - androidMainApi("dev.icerock.moko:mvvm-viewbinding:0.12.0") // api mvvm-livedata, ViewBinding support for Android + androidMainApi("dev.icerock.moko:mvvm-livedata-material:0.13.0") // api mvvm-livedata, Material library android extensions + androidMainApi("dev.icerock.moko:mvvm-livedata-glide:0.13.0") // api mvvm-livedata, Glide library android extensions + androidMainApi("dev.icerock.moko:mvvm-livedata-swiperefresh:0.13.0") // api mvvm-livedata, SwipeRefreshLayout library android extensions + androidMainApi("dev.icerock.moko:mvvm-databinding:0.13.0") // api mvvm-livedata, DataBinding support for Android + androidMainApi("dev.icerock.moko:mvvm-viewbinding:0.13.0") // api mvvm-livedata, ViewBinding support for Android - commonTestImplementation("dev.icerock.moko:mvvm-test:0.12.0") // test utilities + commonTestImplementation("dev.icerock.moko:mvvm-test:0.13.0") // test utilities } ``` @@ -61,10 +61,10 @@ kotlin { // export correct artifact to use all classes of library directly from Swift targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java).all { binaries.withType(org.jetbrains.kotlin.gradle.plugin.mpp.Framework::class.java).all { - export("dev.icerock.moko:mvvm-core:0.12.0") - export("dev.icerock.moko:mvvm-livedata:0.12.0") - export("dev.icerock.moko:mvvm-livedata-resources:0.12.0") - export("dev.icerock.moko:mvvm-state:0.12.0") + export("dev.icerock.moko:mvvm-core:0.13.0") + export("dev.icerock.moko:mvvm-livedata:0.13.0") + export("dev.icerock.moko:mvvm-livedata-resources:0.13.0") + export("dev.icerock.moko:mvvm-state:0.13.0") } } } @@ -75,6 +75,14 @@ kotlin { For iOS we recommend use [moko-kswift](https://github.com/icerockdev/moko-kswift) with extensions generation enabled. All `LiveData` to `UIView` bindings is extensions for UI elements. +### SwiftUI additions + +To use MOKO MVVM with SwiftUI set name of your kotlin framework to `MultiPlatformLibrary` and add +dependency to CocoaPods: +```ruby +pod 'mokoMvvmFlowSwiftUI', :podspec => 'https://raw.githubusercontent.com/icerockdev/moko-mvvm/release/0.13.0/mokoMvvmFlowSwiftUI.podspec' +``` + ## Documentation Documentation generated by Dokka and available at https://icerockdev.github.io/moko-mvvm/ @@ -498,7 +506,8 @@ Please see more examples in the [sample directory](sample). - The [mvvm-databinding directory](mvvm-databinding) contains DataBinding support code for Android; - The [mvvm-viewbinding directory](mvvm-viewbinding) contains ViewBinding support code for Android; - The [mvvm-test directory](mvvm-test) contains the test utilities; -- In [sample directory](sample) contains sample apps for Android and iOS; plus the mpp-library connected to the apps. +- In [sample directory](sample) contains sample apps for Android and iOS; plus the mpp-library connected to the apps; +- In [sample-declarative-ui directory](sample-declarative-ui) contains sample apps with Jetpack Compose and SwiftUI. ## Contributing All development (both new features and bug fixes) is performed in the `develop` branch. This way `master` always contains the sources of the most recently released version. Please send PRs with bug fixes to the `develop` branch. Documentation fixes in the markdown files are an exception to this rule. They are updated directly in `master`. diff --git a/build.gradle.kts b/build.gradle.kts index 57d7a424..b51b62f2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ buildscript { dependencies { classpath(":mvvm-build-logic") classpath(libs.kswiftGradlePlugin) + classpath(libs.composeJetBrainsGradlePlugin) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11de7849..48d1eab7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,19 +4,26 @@ androidLifecycleVersion = "2.2.0" coroutinesVersion = "1.6.0-native-mt" mokoResourcesVersion = "0.18.0" mokoTestVersion = "0.6.1" -mokoMvvmVersion = "0.12.0" +mokoMvvmVersion = "0.13.0" mokoKSwiftVersion = "0.4.0" +composeVersion = "1.1.1" +composeJetBrainsVersion = "1.1.1" [libraries] # android appCompat = { module = "androidx.appcompat:appcompat", version = "1.2.0" } material = { module = "com.google.android.material:material", version = "1.2.1" } lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" } +lifecycleKtx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidLifecycleVersion" } androidViewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidLifecycleVersion" } androidLiveData = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidLifecycleVersion" } glide = { module = "com.github.bumptech.glide:glide", version = "4.11.0" } swipeRefresh = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version = "1.1.0" } +# compose +composeFoundation = { module = "androidx.compose.foundation:foundation", version.ref = "composeVersion" } +composeLiveData = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "composeVersion" } + # coroutines coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } coroutinesTest = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesVersion" } @@ -37,6 +44,7 @@ mobileMultiplatformGradlePlugin = { module = "dev.icerock:mobile-multiplatform", androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.0.4" } detektGradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.19.0" } kswiftGradlePlugin = { module = "dev.icerock.moko:kswift-gradle-plugin", version.ref = "mokoKSwiftVersion" } +composeJetBrainsGradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "composeJetBrainsVersion" } [plugins] nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version = "1.1.0" } diff --git a/mokoMvvmFlowSwiftUI.podspec b/mokoMvvmFlowSwiftUI.podspec new file mode 100644 index 00000000..fde1c1b6 --- /dev/null +++ b/mokoMvvmFlowSwiftUI.podspec @@ -0,0 +1,19 @@ +Pod::Spec.new do |s| + s.name = 'mokoMvvmFlowSwiftUI' + s.version = '0.13.0' + s.summary = 'MOKO MVVM SwiftUI additions for Flow' + s.description = 'some description here' + s.homepage = 'localhost' + s.license = { :type => 'Apache 2' } + s.authors = 'IceRock Development' + s.source = { + :http => "https://repo1.maven.org/maven2/dev/icerock/moko/mvvm-flow-swiftui/#{s.version}/mvvm-flow-swiftui-#{s.version}.zip", + :type => "zip" + } + + s.platform = :ios + s.ios.deployment_target = '13.0' + s.ios.vendored_framework = 'mokoMvvmFlowSwiftUI.xcframework' + + s.requires_arc = true +end \ No newline at end of file diff --git a/mvvm-core/src/androidMain/kotlin/dev/icerock/moko/mvvm/ViewModelFactory.kt b/mvvm-core/src/androidMain/kotlin/dev/icerock/moko/mvvm/ViewModelFactory.kt index 3c98bb8d..35154f11 100644 --- a/mvvm-core/src/androidMain/kotlin/dev/icerock/moko/mvvm/ViewModelFactory.kt +++ b/mvvm-core/src/androidMain/kotlin/dev/icerock/moko/mvvm/ViewModelFactory.kt @@ -24,7 +24,7 @@ inline fun ViewModelStoreOwner.getViewModel( ViewModelFactory { viewModelBlock() } ).get(T::class.java) -inline fun ViewModelStoreOwner.createViewModelFactory( +inline fun createViewModelFactory( noinline viewModelBlock: () -> T ): ViewModelFactory = ViewModelFactory { viewModelBlock() diff --git a/mvvm-flow-compose/build.gradle.kts b/mvvm-flow-compose/build.gradle.kts new file mode 100644 index 00000000..c748d268 --- /dev/null +++ b/mvvm-flow-compose/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("detekt-convention") + id("org.jetbrains.compose") + id("javadoc-stub-convention") + id("publication-convention") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +kotlin { + jvm() + js(IR) { + browser() + } + + sourceSets { + commonMain { + dependencies { + api(projects.mvvmFlow) + + api(compose.runtime) + } + } + } +} diff --git a/mvvm-flow-compose/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/compose/FlowActions.kt b/mvvm-flow-compose/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/compose/FlowActions.kt new file mode 100644 index 00000000..eb2518e8 --- /dev/null +++ b/mvvm-flow-compose/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/compose/FlowActions.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +@Composable +fun Flow.observeAsActions(onEach: (T) -> Unit) { + val flow = this + LaunchedEffect(key1 = flow) { + flow.onEach(onEach).collect() + } +} diff --git a/mvvm-flow/apple/build.gradle.kts b/mvvm-flow/apple/build.gradle.kts new file mode 100644 index 00000000..ba78e552 --- /dev/null +++ b/mvvm-flow/apple/build.gradle.kts @@ -0,0 +1,98 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework +import org.jetbrains.kotlin.konan.target.HostManager + +plugins { + id("detekt-convention") + id("org.jetbrains.kotlin.multiplatform") + id("dev.icerock.mobile.multiplatform.apple-framework") + id("publication-convention") +} + +kotlin { + iosArm64() + iosX64() + iosSimulatorArm64() + macosX64() + + val xcf = XCFramework("MultiPlatformLibrary") + targets.withType() + .configureEach { + binaries.withType().configureEach { + xcf.add(this) + } + } +} + +dependencies { + commonMainImplementation(libs.coroutines) + + commonMainApi(libs.mokoResources) + commonMainApi(projects.mvvmFlow) + commonMainApi(projects.mvvmCore) +} + +framework { + export(projects.mvvmCore) + export(projects.mvvmFlow) + export(libs.mokoResources) +} + +val swiftXCFrameworkProject = File(projectDir, "xcode/mokoMvvmFlow.xcodeproj") +val swiftXCFrameworkOutput = File(projectDir, "xcode/xcframework") +val swiftXCFramework = File(swiftXCFrameworkOutput, "mokoMvvmFlowSwiftUI.xcframework") +val swiftXCFrameworkArchive = File(swiftXCFrameworkOutput, "mokoMvvmFlowSwiftUI.xcframework.zip") + +val compileTask = tasks.create("compileMokoFlowSwiftUIXCFramework", Exec::class) { + group = "xcode" + + commandLine = listOf( + "xcodebuild", + "-scheme", + "mokoMvvmFlowSwiftUI", + "-project", + swiftXCFrameworkProject.absolutePath, + "build" + ) + + dependsOn("assembleMultiPlatformLibraryDebugXCFramework") + + onlyIf { HostManager.hostIsMac } +} + +val archiveTask = tasks.create("archiveMokoFlowSwiftUIXCFramework", Zip::class) { + group = "xcode" + + from(swiftXCFramework) + into(swiftXCFramework.name) + + archiveFileName.set(swiftXCFrameworkArchive.name) + destinationDirectory.set(swiftXCFrameworkOutput) + + dependsOn(compileTask) + + onlyIf { HostManager.hostIsMac } +} + +val publicationName = "swiftuiAdditions" + +publishing { + publications { + create(publicationName) { + artifactId = "mvvm-flow-swiftui" + + artifact(archiveTask.archiveFile) { + extension = "zip" + } + + pom { + packaging = "zip" + } + } + } +} + +tasks.withType() + .configureEach { + if (publication.name != publicationName) enabled = false + else onlyIf { HostManager.hostIsMac } + } diff --git a/mvvm-flow/apple/src/commonMain/kotlin/Greeting.kt b/mvvm-flow/apple/src/commonMain/kotlin/Greeting.kt new file mode 100644 index 00000000..a7658b42 --- /dev/null +++ b/mvvm-flow/apple/src/commonMain/kotlin/Greeting.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.apple + +// just for compile this module +fun helloWorld() = println("hello world") diff --git a/mvvm-flow/apple/xcode/.gitignore b/mvvm-flow/apple/xcode/.gitignore new file mode 100644 index 00000000..e3995a97 --- /dev/null +++ b/mvvm-flow/apple/xcode/.gitignore @@ -0,0 +1 @@ +xcframework/ \ No newline at end of file diff --git a/mvvm-flow/apple/xcode/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/mvvm-flow/apple/xcode/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/mvvm-flow/apple/xcode/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mvvm-flow/apple/xcode/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/mvvm-flow/apple/xcode/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..63efdb0a --- /dev/null +++ b/mvvm-flow/apple/xcode/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,143 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mvvm-flow/apple/xcode/Shared/Assets.xcassets/Contents.json b/mvvm-flow/apple/xcode/Shared/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/mvvm-flow/apple/xcode/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mvvm-flow/apple/xcode/Shared/ContentView.swift b/mvvm-flow/apple/xcode/Shared/ContentView.swift new file mode 100644 index 00000000..269f2404 --- /dev/null +++ b/mvvm-flow/apple/xcode/Shared/ContentView.swift @@ -0,0 +1,21 @@ +// +// ContentView.swift +// Shared +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + Text("Hello, world!") + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/mvvm-flow/apple/xcode/Shared/mokoMvvmFlowApp.swift b/mvvm-flow/apple/xcode/Shared/mokoMvvmFlowApp.swift new file mode 100644 index 00000000..f846f10c --- /dev/null +++ b/mvvm-flow/apple/xcode/Shared/mokoMvvmFlowApp.swift @@ -0,0 +1,17 @@ +// +// mokoMvvmFlowApp.swift +// Shared +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +import SwiftUI + +@main +struct mokoMvvmFlowApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/mvvm-flow/apple/xcode/macOS/macOS.entitlements b/mvvm-flow/apple/xcode/macOS/macOS.entitlements new file mode 100644 index 00000000..f2ef3ae0 --- /dev/null +++ b/mvvm-flow/apple/xcode/macOS/macOS.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.pbxproj b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.pbxproj new file mode 100644 index 00000000..1b90c6c7 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.pbxproj @@ -0,0 +1,910 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXAggregateTarget section */ + 22BDE72D281BD39900259368 /* mokoMvvmFlowSwiftUI */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 22BDE72E281BD39900259368 /* Build configuration list for PBXAggregateTarget "mokoMvvmFlowSwiftUI" */; + buildPhases = ( + 22BDE745281BD9FB00259368 /* build XCFramework */, + ); + dependencies = ( + 22BDE732281BD3A600259368 /* PBXTargetDependency */, + 22BDE734281BD3A600259368 /* PBXTargetDependency */, + ); + name = mokoMvvmFlowSwiftUI; + productName = mokoMvvmFlowSwiftUI; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 22BDE710281BD34C00259368 /* mokoMvvmFlowSwiftUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 22BDE702281BD34C00259368 /* mokoMvvmFlowSwiftUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 22BDE713281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE700281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework */; }; + 22BDE714281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE700281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 22BDE727281BD37100259368 /* mokoMvvmFlowSwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE721281BD37100259368 /* mokoMvvmFlowSwiftUI.framework */; }; + 22BDE728281BD37100259368 /* mokoMvvmFlowSwiftUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE721281BD37100259368 /* mokoMvvmFlowSwiftUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 22BDE737281BD55400259368 /* MultiPlatformLibrary.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE735281BD54E00259368 /* MultiPlatformLibrary.xcframework */; }; + 22BDE738281BD55500259368 /* MultiPlatformLibrary.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE735281BD54E00259368 /* MultiPlatformLibrary.xcframework */; }; + 22BDE73A281BD56700259368 /* CFlowExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE739281BD56700259368 /* CFlowExt.swift */; }; + 22BDE73B281BD56A00259368 /* CFlowExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE739281BD56700259368 /* CFlowExt.swift */; }; + 22BDE73D281BD67C00259368 /* ViewModelObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE73C281BD67C00259368 /* ViewModelObservable.swift */; }; + 22BDE73E281BD67C00259368 /* ViewModelObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE73C281BD67C00259368 /* ViewModelObservable.swift */; }; + 22BDE740281BD6A300259368 /* ViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE73F281BD6A300259368 /* ViewModelState.swift */; }; + 22BDE741281BD6A300259368 /* ViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE73F281BD6A300259368 /* ViewModelState.swift */; }; + 22BDE743281BD6C600259368 /* ViewModelBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE742281BD6C600259368 /* ViewModelBinding.swift */; }; + 22BDE744281BD6C600259368 /* ViewModelBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE742281BD6C600259368 /* ViewModelBinding.swift */; }; + 22D5AD5C281BD2F700008F57 /* mokoMvvmFlowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D5AD4C281BD2F600008F57 /* mokoMvvmFlowApp.swift */; }; + 22D5AD5D281BD2F700008F57 /* mokoMvvmFlowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D5AD4C281BD2F600008F57 /* mokoMvvmFlowApp.swift */; }; + 22D5AD5E281BD2F700008F57 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D5AD4D281BD2F600008F57 /* ContentView.swift */; }; + 22D5AD5F281BD2F700008F57 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D5AD4D281BD2F600008F57 /* ContentView.swift */; }; + 22D5AD60281BD2F700008F57 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22D5AD4E281BD2F700008F57 /* Assets.xcassets */; }; + 22D5AD61281BD2F700008F57 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22D5AD4E281BD2F700008F57 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 22BDE711281BD34C00259368 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 22D5AD47281BD2F600008F57 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 22BDE6FF281BD34C00259368; + remoteInfo = mokoMvvmFlowSwiftUI; + }; + 22BDE725281BD37100259368 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 22D5AD47281BD2F600008F57 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 22BDE720281BD37100259368; + remoteInfo = mokoMvvmFlowSwiftUI; + }; + 22BDE731281BD3A600259368 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 22D5AD47281BD2F600008F57 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 22BDE6FF281BD34C00259368; + remoteInfo = "mokoMvvmFlowSwiftUI (iOS)"; + }; + 22BDE733281BD3A600259368 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 22D5AD47281BD2F600008F57 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 22BDE720281BD37100259368; + remoteInfo = "mokoMvvmFlowSwiftUI (macOS)"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 22BDE715281BD34C00259368 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 22BDE714281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + 22BDE72C281BD37100259368 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 22BDE728281BD37100259368 /* mokoMvvmFlowSwiftUI.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 22BDE700281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = mokoMvvmFlowSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 22BDE702281BD34C00259368 /* mokoMvvmFlowSwiftUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mokoMvvmFlowSwiftUI.h; sourceTree = ""; }; + 22BDE721281BD37100259368 /* mokoMvvmFlowSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = mokoMvvmFlowSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 22BDE735281BD54E00259368 /* MultiPlatformLibrary.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MultiPlatformLibrary.xcframework; path = ../build/XCFrameworks/debug/MultiPlatformLibrary.xcframework; sourceTree = ""; }; + 22BDE739281BD56700259368 /* CFlowExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CFlowExt.swift; sourceTree = ""; }; + 22BDE73C281BD67C00259368 /* ViewModelObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelObservable.swift; sourceTree = ""; }; + 22BDE73F281BD6A300259368 /* ViewModelState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelState.swift; sourceTree = ""; }; + 22BDE742281BD6C600259368 /* ViewModelBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelBinding.swift; sourceTree = ""; }; + 22D5AD4C281BD2F600008F57 /* mokoMvvmFlowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mokoMvvmFlowApp.swift; sourceTree = ""; }; + 22D5AD4D281BD2F600008F57 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 22D5AD4E281BD2F700008F57 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 22D5AD53281BD2F700008F57 /* mokoMvvmFlow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mokoMvvmFlow.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 22D5AD59281BD2F700008F57 /* mokoMvvmFlow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mokoMvvmFlow.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 22D5AD5B281BD2F700008F57 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 22BDE6FD281BD34C00259368 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE737281BD55400259368 /* MultiPlatformLibrary.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22BDE71E281BD37100259368 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE738281BD55500259368 /* MultiPlatformLibrary.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22D5AD50281BD2F700008F57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE713281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22D5AD56281BD2F700008F57 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE727281BD37100259368 /* mokoMvvmFlowSwiftUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 22BDE701281BD34C00259368 /* mokoMvvmFlowSwiftUI */ = { + isa = PBXGroup; + children = ( + 22BDE702281BD34C00259368 /* mokoMvvmFlowSwiftUI.h */, + 22BDE739281BD56700259368 /* CFlowExt.swift */, + 22BDE73C281BD67C00259368 /* ViewModelObservable.swift */, + 22BDE73F281BD6A300259368 /* ViewModelState.swift */, + 22BDE742281BD6C600259368 /* ViewModelBinding.swift */, + ); + path = mokoMvvmFlowSwiftUI; + sourceTree = ""; + }; + 22D5AD46281BD2F600008F57 = { + isa = PBXGroup; + children = ( + 22BDE735281BD54E00259368 /* MultiPlatformLibrary.xcframework */, + 22D5AD4B281BD2F600008F57 /* Shared */, + 22D5AD5A281BD2F700008F57 /* macOS */, + 22BDE701281BD34C00259368 /* mokoMvvmFlowSwiftUI */, + 22D5AD54281BD2F700008F57 /* Products */, + ); + sourceTree = ""; + }; + 22D5AD4B281BD2F600008F57 /* Shared */ = { + isa = PBXGroup; + children = ( + 22D5AD4C281BD2F600008F57 /* mokoMvvmFlowApp.swift */, + 22D5AD4D281BD2F600008F57 /* ContentView.swift */, + 22D5AD4E281BD2F700008F57 /* Assets.xcassets */, + ); + path = Shared; + sourceTree = ""; + }; + 22D5AD54281BD2F700008F57 /* Products */ = { + isa = PBXGroup; + children = ( + 22D5AD53281BD2F700008F57 /* mokoMvvmFlow.app */, + 22D5AD59281BD2F700008F57 /* mokoMvvmFlow.app */, + 22BDE700281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework */, + 22BDE721281BD37100259368 /* mokoMvvmFlowSwiftUI.framework */, + ); + name = Products; + sourceTree = ""; + }; + 22D5AD5A281BD2F700008F57 /* macOS */ = { + isa = PBXGroup; + children = ( + 22D5AD5B281BD2F700008F57 /* macOS.entitlements */, + ); + path = macOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 22BDE6FB281BD34C00259368 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE710281BD34C00259368 /* mokoMvvmFlowSwiftUI.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22BDE71C281BD37100259368 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 22BDE6FF281BD34C00259368 /* mokoMvvmFlowSwiftUI (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 22BDE71A281BD34C00259368 /* Build configuration list for PBXNativeTarget "mokoMvvmFlowSwiftUI (iOS)" */; + buildPhases = ( + 22BDE6FB281BD34C00259368 /* Headers */, + 22BDE6FC281BD34C00259368 /* Sources */, + 22BDE6FD281BD34C00259368 /* Frameworks */, + 22BDE6FE281BD34C00259368 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "mokoMvvmFlowSwiftUI (iOS)"; + productName = mokoMvvmFlowSwiftUI; + productReference = 22BDE700281BD34C00259368 /* mokoMvvmFlowSwiftUI.framework */; + productType = "com.apple.product-type.framework"; + }; + 22BDE720281BD37100259368 /* mokoMvvmFlowSwiftUI (macOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 22BDE729281BD37100259368 /* Build configuration list for PBXNativeTarget "mokoMvvmFlowSwiftUI (macOS)" */; + buildPhases = ( + 22BDE71C281BD37100259368 /* Headers */, + 22BDE71D281BD37100259368 /* Sources */, + 22BDE71E281BD37100259368 /* Frameworks */, + 22BDE71F281BD37100259368 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "mokoMvvmFlowSwiftUI (macOS)"; + productName = mokoMvvmFlowSwiftUI; + productReference = 22BDE721281BD37100259368 /* mokoMvvmFlowSwiftUI.framework */; + productType = "com.apple.product-type.framework"; + }; + 22D5AD52281BD2F700008F57 /* mokoMvvmFlow (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 22D5AD64281BD2F700008F57 /* Build configuration list for PBXNativeTarget "mokoMvvmFlow (iOS)" */; + buildPhases = ( + 22D5AD4F281BD2F700008F57 /* Sources */, + 22D5AD50281BD2F700008F57 /* Frameworks */, + 22D5AD51281BD2F700008F57 /* Resources */, + 22BDE715281BD34C00259368 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 22BDE712281BD34C00259368 /* PBXTargetDependency */, + ); + name = "mokoMvvmFlow (iOS)"; + productName = "mokoMvvmFlow (iOS)"; + productReference = 22D5AD53281BD2F700008F57 /* mokoMvvmFlow.app */; + productType = "com.apple.product-type.application"; + }; + 22D5AD58281BD2F700008F57 /* mokoMvvmFlow (macOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 22D5AD67281BD2F700008F57 /* Build configuration list for PBXNativeTarget "mokoMvvmFlow (macOS)" */; + buildPhases = ( + 22D5AD55281BD2F700008F57 /* Sources */, + 22D5AD56281BD2F700008F57 /* Frameworks */, + 22D5AD57281BD2F700008F57 /* Resources */, + 22BDE72C281BD37100259368 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 22BDE726281BD37100259368 /* PBXTargetDependency */, + ); + name = "mokoMvvmFlow (macOS)"; + productName = "mokoMvvmFlow (macOS)"; + productReference = 22D5AD59281BD2F700008F57 /* mokoMvvmFlow.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 22D5AD47281BD2F600008F57 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1330; + LastUpgradeCheck = 1330; + TargetAttributes = { + 22BDE6FF281BD34C00259368 = { + CreatedOnToolsVersion = 13.3.1; + LastSwiftMigration = 1330; + }; + 22BDE720281BD37100259368 = { + CreatedOnToolsVersion = 13.3.1; + }; + 22BDE72D281BD39900259368 = { + CreatedOnToolsVersion = 13.3.1; + }; + 22D5AD52281BD2F700008F57 = { + CreatedOnToolsVersion = 13.3.1; + }; + 22D5AD58281BD2F700008F57 = { + CreatedOnToolsVersion = 13.3.1; + }; + }; + }; + buildConfigurationList = 22D5AD4A281BD2F600008F57 /* Build configuration list for PBXProject "mokoMvvmFlow" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 22D5AD46281BD2F600008F57; + productRefGroup = 22D5AD54281BD2F700008F57 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 22D5AD52281BD2F700008F57 /* mokoMvvmFlow (iOS) */, + 22D5AD58281BD2F700008F57 /* mokoMvvmFlow (macOS) */, + 22BDE6FF281BD34C00259368 /* mokoMvvmFlowSwiftUI (iOS) */, + 22BDE720281BD37100259368 /* mokoMvvmFlowSwiftUI (macOS) */, + 22BDE72D281BD39900259368 /* mokoMvvmFlowSwiftUI */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 22BDE6FE281BD34C00259368 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22BDE71F281BD37100259368 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22D5AD51281BD2F700008F57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22D5AD60281BD2F700008F57 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22D5AD57281BD2F700008F57 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22D5AD61281BD2F700008F57 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 22BDE745281BD9FB00259368 /* build XCFramework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "build XCFramework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "SCHEME_NAME_IOS=\"mokoMvvmFlowSwiftUI-iOS\"\nSCHEME_NAME_MACOS=\"mokoMvvmFlowSwiftUI-macOS\"\nFRAMEWORK_NAME=\"mokoMvvmFlowSwiftUI\"\nSIMULATOR_ARCHIVE_PATH=\"${BUILD_DIR}/${CONFIGURATION}/${FRAMEWORK_NAME}-simulator\"\nDEVICE_ARCHIVE_PATH=\"${BUILD_DIR}/${CONFIGURATION}/${FRAMEWORK_NAME}-device\"\nOUTPUT_DIC=\"./xcframework/\"\n\n# Simulator xcarchieve\nxcodebuild archive \\\n -scheme ${SCHEME_NAME_IOS} \\\n -archivePath \"${SIMULATOR_ARCHIVE_PATH}/ios\" \\\n -sdk iphonesimulator \\\n SKIP_INSTALL=NO\n# Device xcarchieve\nxcodebuild archive \\\n -scheme ${SCHEME_NAME_IOS} \\\n -archivePath \"${DEVICE_ARCHIVE_PATH}/ios\" \\\n -sdk iphoneos \\\n SKIP_INSTALL=NO\n\n# Device xcarchieve\nxcodebuild archive \\\n -scheme ${SCHEME_NAME_MACOS} \\\n -archivePath \"${DEVICE_ARCHIVE_PATH}/macos\" \\\n SKIP_INSTALL=NO\n\n# Clean up old output directory\nrm -rf \"${OUTPUT_DIC}\"\n# Create xcframwork combine of all frameworks\nxcodebuild -create-xcframework \\\n -framework ${SIMULATOR_ARCHIVE_PATH}/ios.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework \\\n -framework ${DEVICE_ARCHIVE_PATH}/ios.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework \\\n -framework ${DEVICE_ARCHIVE_PATH}/macos.xcarchive/Products/Library/Frameworks/${FRAMEWORK_NAME}.framework \\\n -output ${OUTPUT_DIC}/${FRAMEWORK_NAME}.xcframework\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 22BDE6FC281BD34C00259368 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE743281BD6C600259368 /* ViewModelBinding.swift in Sources */, + 22BDE73A281BD56700259368 /* CFlowExt.swift in Sources */, + 22BDE73D281BD67C00259368 /* ViewModelObservable.swift in Sources */, + 22BDE740281BD6A300259368 /* ViewModelState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22BDE71D281BD37100259368 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22BDE744281BD6C600259368 /* ViewModelBinding.swift in Sources */, + 22BDE73B281BD56A00259368 /* CFlowExt.swift in Sources */, + 22BDE73E281BD67C00259368 /* ViewModelObservable.swift in Sources */, + 22BDE741281BD6A300259368 /* ViewModelState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22D5AD4F281BD2F700008F57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22D5AD5E281BD2F700008F57 /* ContentView.swift in Sources */, + 22D5AD5C281BD2F700008F57 /* mokoMvvmFlowApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 22D5AD55281BD2F700008F57 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22D5AD5F281BD2F700008F57 /* ContentView.swift in Sources */, + 22D5AD5D281BD2F700008F57 /* mokoMvvmFlowApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 22BDE712281BD34C00259368 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 22BDE6FF281BD34C00259368 /* mokoMvvmFlowSwiftUI (iOS) */; + targetProxy = 22BDE711281BD34C00259368 /* PBXContainerItemProxy */; + }; + 22BDE726281BD37100259368 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 22BDE720281BD37100259368 /* mokoMvvmFlowSwiftUI (macOS) */; + targetProxy = 22BDE725281BD37100259368 /* PBXContainerItemProxy */; + }; + 22BDE732281BD3A600259368 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 22BDE6FF281BD34C00259368 /* mokoMvvmFlowSwiftUI (iOS) */; + targetProxy = 22BDE731281BD3A600259368 /* PBXContainerItemProxy */; + }; + 22BDE734281BD3A600259368 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 22BDE720281BD37100259368 /* mokoMvvmFlowSwiftUI (macOS) */; + targetProxy = 22BDE733281BD3A600259368 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 22BDE716281BD34C00259368 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlowSwiftUI; + PRODUCT_NAME = mokoMvvmFlowSwiftUI; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 22BDE717281BD34C00259368 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlowSwiftUI; + PRODUCT_NAME = mokoMvvmFlowSwiftUI; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 22BDE72A281BD37100259368 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + EXCLUDED_ARCHS = arm64; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlowSwiftUI; + PRODUCT_NAME = mokoMvvmFlowSwiftUI; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 22BDE72B281BD37100259368 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + EXCLUDED_ARCHS = arm64; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlowSwiftUI; + PRODUCT_NAME = mokoMvvmFlowSwiftUI; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 22BDE72F281BD39900259368 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 22BDE730281BD39900259368 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 22D5AD62281BD2F700008F57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 22D5AD63281BD2F700008F57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 22D5AD65281BD2F700008F57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlow; + PRODUCT_NAME = mokoMvvmFlow; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 22D5AD66281BD2F700008F57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlow; + PRODUCT_NAME = mokoMvvmFlow; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 22D5AD68281BD2F700008F57 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlow; + PRODUCT_NAME = mokoMvvmFlow; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 22D5AD69281BD2F700008F57 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.icerock.moko.mokoMvvmFlow; + PRODUCT_NAME = mokoMvvmFlow; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 22BDE71A281BD34C00259368 /* Build configuration list for PBXNativeTarget "mokoMvvmFlowSwiftUI (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 22BDE716281BD34C00259368 /* Debug */, + 22BDE717281BD34C00259368 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 22BDE729281BD37100259368 /* Build configuration list for PBXNativeTarget "mokoMvvmFlowSwiftUI (macOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 22BDE72A281BD37100259368 /* Debug */, + 22BDE72B281BD37100259368 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 22BDE72E281BD39900259368 /* Build configuration list for PBXAggregateTarget "mokoMvvmFlowSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 22BDE72F281BD39900259368 /* Debug */, + 22BDE730281BD39900259368 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 22D5AD4A281BD2F600008F57 /* Build configuration list for PBXProject "mokoMvvmFlow" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 22D5AD62281BD2F700008F57 /* Debug */, + 22D5AD63281BD2F700008F57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 22D5AD64281BD2F700008F57 /* Build configuration list for PBXNativeTarget "mokoMvvmFlow (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 22D5AD65281BD2F700008F57 /* Debug */, + 22D5AD66281BD2F700008F57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 22D5AD67281BD2F700008F57 /* Build configuration list for PBXNativeTarget "mokoMvvmFlow (macOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 22D5AD68281BD2F700008F57 /* Debug */, + 22D5AD69281BD2F700008F57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 22D5AD47281BD2F600008F57 /* Project object */; +} diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlow-iOS.xcscheme b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlow-iOS.xcscheme new file mode 100644 index 00000000..5384222e --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlow-iOS.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlow-macOS.xcscheme b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlow-macOS.xcscheme new file mode 100644 index 00000000..27d53292 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlow-macOS.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI-iOS.xcscheme b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI-iOS.xcscheme new file mode 100644 index 00000000..f600e5e4 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI-iOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI-macOS.xcscheme b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI-macOS.xcscheme new file mode 100644 index 00000000..a0b0d158 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI-macOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI.xcscheme b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI.xcscheme new file mode 100644 index 00000000..9507f017 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlow.xcodeproj/xcshareddata/xcschemes/mokoMvvmFlowSwiftUI.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/CFlowExt.swift b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/CFlowExt.swift new file mode 100644 index 00000000..62306de6 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/CFlowExt.swift @@ -0,0 +1,46 @@ +// +// CFlowExt.swift +// mokoMvvmFlowSwiftUI (iOS) +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +import MultiPlatformLibrary +import Combine + +public func createPublisher(_ cFlow: CFlow) -> AnyPublisher { + return CFlowPublisher(cFlow: cFlow).eraseToAnyPublisher() +} + +private struct CFlowPublisher: Publisher { + + typealias Output = Output + typealias Failure = Never + + let cFlow: CFlow + + func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { + subscriber.receive(subscription: CFlowSubscription(flow: cFlow, subscriber: subscriber)) + } +} + +private class CFlowSubscription: Subscription where S.Input == Output, S.Failure == Never { + + private let disposable: DisposableHandle + private let subscriber: S + + init(flow: CFlow, subscriber: S) { + self.subscriber = subscriber + self.disposable = flow.subscribe { value in + let _ = subscriber.receive(value!) + } + } + + func request(_ demand: Subscribers.Demand) { } + + func cancel() { + DispatchQueue.main.async { + self.disposable.dispose() + } + } +} diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelBinding.swift b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelBinding.swift new file mode 100644 index 00000000..9feccf94 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelBinding.swift @@ -0,0 +1,92 @@ +// +// CFlowExt.swift +// mokoMvvmFlowSwiftUI (iOS) +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +import MultiPlatformLibrary +import SwiftUI +import Combine + +public extension ObservableObject where Self: ViewModel { + + func binding( + _ flowKey: KeyPath>, + equals: @escaping (T?, T?) -> Bool, + getMapper: @escaping (T) -> R, + setMapper: @escaping (R) -> T + ) -> Binding { + let stateFlow: CMutableStateFlow = self[keyPath: flowKey] + var lastValue: T? = stateFlow.value + + var disposable: DisposableHandle? = nil + + disposable = stateFlow.subscribe(onCollect: { value in + if !equals(lastValue, value) { + lastValue = value + self.objectWillChange.send() + disposable?.dispose() + } + }) + + return Binding( + get: { getMapper(stateFlow.value!) }, + set: { stateFlow.value = setMapper($0) } + ) + } + + func binding(_ flowKey: KeyPath>) -> Binding { + return binding( + flowKey, + equals: { $0 == $1 }, + getMapper: { $0 as String }, + setMapper: { $0 as NSString } + ) + } + + func binding(_ flowKey: KeyPath>) -> Binding { + return binding( + flowKey, + equals: { $0?.boolValue == $1?.boolValue }, + getMapper: { $0.boolValue }, + setMapper: { KotlinBoolean(bool: $0) } + ) + } + + func binding(_ flowKey: KeyPath>) -> Binding { + return binding( + flowKey, + equals: { $0?.intValue == $1?.intValue }, + getMapper: { $0.intValue }, + setMapper: { KotlinInt(int: Int32($0)) } + ) + } + + func binding(_ flowKey: KeyPath>) -> Binding { + return binding( + flowKey, + equals: { $0?.int64Value == $1?.int64Value }, + getMapper: { $0.int64Value }, + setMapper: { KotlinLong(longLong: $0) } + ) + } + + func binding(_ flowKey: KeyPath>) -> Binding { + return binding( + flowKey, + equals: { $0?.floatValue == $1?.floatValue }, + getMapper: { $0.floatValue }, + setMapper: { KotlinFloat(float: $0) } + ) + } + + func binding(_ flowKey: KeyPath>) -> Binding { + return binding( + flowKey, + equals: { $0?.doubleValue == $1?.doubleValue }, + getMapper: { $0.doubleValue }, + setMapper: { KotlinDouble(double: $0) } + ) + } +} diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelObservable.swift b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelObservable.swift new file mode 100644 index 00000000..61e07e77 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelObservable.swift @@ -0,0 +1,13 @@ +// +// CFlowExt.swift +// mokoMvvmFlowSwiftUI (iOS) +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +import MultiPlatformLibrary +import SwiftUI + +extension ViewModel: ObservableObject { + +} diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelState.swift b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelState.swift new file mode 100644 index 00000000..eea06bb9 --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/ViewModelState.swift @@ -0,0 +1,90 @@ +// +// CFlowExt.swift +// mokoMvvmFlowSwiftUI (iOS) +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +import MultiPlatformLibrary +import SwiftUI +import Combine + +public extension ObservableObject where Self: ViewModel { + + func state( + _ flowKey: KeyPath>, + equals: @escaping (T?, T?) -> Bool, + mapper: @escaping (T) -> R + ) -> R { + let stateFlow: CStateFlow = self[keyPath: flowKey] + var lastValue: T? = stateFlow.value + + var disposable: DisposableHandle? = nil + + disposable = stateFlow.subscribe(onCollect: { value in + if !equals(lastValue, value) { + lastValue = value + self.objectWillChange.send() + disposable?.dispose() + } + }) + + return mapper(stateFlow.value!) + } + + func state(_ flowKey: KeyPath>) -> Bool { + return state( + flowKey, + equals: { $0?.boolValue == $1?.boolValue }, + mapper: { $0.boolValue } + ) + } + + func state(_ flowKey: KeyPath>) -> Double { + return state( + flowKey, + equals: { $0?.doubleValue == $1?.doubleValue }, + mapper: { $0.doubleValue } + ) + } + + func state(_ flowKey: KeyPath>) -> Float { + return state( + flowKey, + equals: { $0?.floatValue == $1?.floatValue }, + mapper: { $0.floatValue } + ) + } + + func state(_ flowKey: KeyPath>) -> Int { + return state( + flowKey, + equals: { $0?.intValue == $1?.intValue }, + mapper: { $0.intValue } + ) + } + + func state(_ flowKey: KeyPath>) -> Int64 { + return state( + flowKey, + equals: { $0?.int64Value == $1?.int64Value }, + mapper: { $0.int64Value } + ) + } + + func state(_ flowKey: KeyPath>) -> String { + return state( + flowKey, + equals: { $0 == $1 }, + mapper: { $0 as String } + ) + } + + func state(_ flowKey: KeyPath>) -> Array { + return state( + flowKey, + equals: { $0 === $1 }, + mapper: { $0 as! Array } + ) + } +} diff --git a/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/mokoMvvmFlowSwiftUI.h b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/mokoMvvmFlowSwiftUI.h new file mode 100644 index 00000000..eb70b88d --- /dev/null +++ b/mvvm-flow/apple/xcode/mokoMvvmFlowSwiftUI/mokoMvvmFlowSwiftUI.h @@ -0,0 +1,18 @@ +// +// mokoMvvmFlowSwiftUI.h +// mokoMvvmFlowSwiftUI +// +// Created by Aleksey Mikhailov on 29.04.2022. +// + +#import + +//! Project version number for mokoMvvmFlowSwiftUI. +FOUNDATION_EXPORT double mokoMvvmFlowSwiftUIVersionNumber; + +//! Project version string for mokoMvvmFlowSwiftUI. +FOUNDATION_EXPORT const unsigned char mokoMvvmFlowSwiftUIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/mvvm-flow/build.gradle.kts b/mvvm-flow/build.gradle.kts new file mode 100644 index 00000000..1cc3edb7 --- /dev/null +++ b/mvvm-flow/build.gradle.kts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("kmp-library-convention") + id("detekt-convention") + id("publication-convention") +} + +dependencies { + commonMainImplementation(libs.mokoKSwift) + commonMainApi(projects.mvvmCore) + + androidMainImplementation(libs.lifecycleKtx) + + commonTestApi(libs.mokoTest) + commonTestApi(projects.mvvmTest) +} diff --git a/mvvm-flow/src/androidMain/AndroidManifest.xml b/mvvm-flow/src/androidMain/AndroidManifest.xml new file mode 100755 index 00000000..a57b4319 --- /dev/null +++ b/mvvm-flow/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt new file mode 100644 index 00000000..82da3120 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.Flow + +actual class CFlow actual constructor( + private val flow: Flow +) : Flow by flow diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt new file mode 100644 index 00000000..85cb2630 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.MutableStateFlow + +actual class CMutableStateFlow actual constructor( + private val flow: MutableStateFlow +) : MutableStateFlow by flow diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt new file mode 100644 index 00000000..095ec005 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.StateFlow + +actual open class CStateFlow actual constructor( + private val flow: StateFlow +) : StateFlow by flow diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt new file mode 100644 index 00000000..ef9a2a46 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach + +fun StateFlow.bind( + lifecycleOwner: LifecycleOwner, + observer: (T) -> Unit +): DisposableHandle { + val self: StateFlow = this + val job: Job = lifecycleOwner.lifecycleScope.launchWhenStarted { + self.onEach { observer(it) }.collect() + } + return DisposableHandle { job.cancel() } +} diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/CompoundButtonBindings.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/CompoundButtonBindings.kt new file mode 100644 index 00000000..9c4d0048 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/CompoundButtonBindings.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import android.widget.CompoundButton +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +fun CompoundButton.bindChecked( + lifecycleOwner: LifecycleOwner, + flow: StateFlow +): DisposableHandle { + return flow.bind(lifecycleOwner) { this.isChecked = it } +} + +fun CompoundButton.bindCheckedTwoWay( + lifecycleOwner: LifecycleOwner, + flow: MutableStateFlow +): DisposableHandle { + val readDisposable: DisposableHandle = flow.bind(lifecycleOwner) { value -> + if (this.isChecked == value) return@bind + + this.isChecked = value + } + + val checkListener = CompoundButton.OnCheckedChangeListener { _, isChecked -> + if (flow.value == isChecked) return@OnCheckedChangeListener + + flow.value = isChecked + } + setOnCheckedChangeListener(checkListener) + + return DisposableHandle { + readDisposable.dispose() + setOnCheckedChangeListener(null) + } +} diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/EditTextBindings.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/EditTextBindings.kt new file mode 100644 index 00000000..97282643 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/EditTextBindings.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import android.text.Editable +import android.text.TextWatcher +import android.widget.EditText +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.MutableStateFlow + +fun EditText.bindTextTwoWay( + lifecycleOwner: LifecycleOwner, + flow: MutableStateFlow +): DisposableHandle { + val readDisposable: DisposableHandle = flow.bind(lifecycleOwner) { value -> + if (this.text.toString() == value) return@bind + + this.setText(value) + } + + val watcher = object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val str = s.toString() + if (str == flow.value) return + + flow.value = str + } + + override fun afterTextChanged(s: Editable?) = Unit + } + this.addTextChangedListener(watcher) + + return DisposableHandle { + readDisposable.dispose() + this.removeTextChangedListener(watcher) + } +} diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/TextViewBindings.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/TextViewBindings.kt new file mode 100644 index 00000000..befaef11 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/TextViewBindings.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import android.widget.TextView +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.StateFlow + +fun TextView.bindText( + lifecycleOwner: LifecycleOwner, + flow: StateFlow +): DisposableHandle { + return flow.bind(lifecycleOwner) { this.text = it } +} diff --git a/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/ViewBindings.kt b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/ViewBindings.kt new file mode 100644 index 00000000..668e31e5 --- /dev/null +++ b/mvvm-flow/src/androidMain/kotlin/dev/icerock/moko/mvvm/flow/binding/ViewBindings.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import android.view.View +import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.StateFlow + +fun View.bindVisibleOrGone( + lifecycleOwner: LifecycleOwner, + flow: StateFlow +): DisposableHandle { + return flow.bind(lifecycleOwner) { value -> + this.visibility = if (value) View.VISIBLE else View.GONE + } +} + +fun View.bindVisibleOrInvisible( + lifecycleOwner: LifecycleOwner, + flow: StateFlow +): DisposableHandle { + return flow.bind(lifecycleOwner) { value -> + this.visibility = if (value) View.VISIBLE else View.INVISIBLE + } +} + +fun View.bindEnabled( + lifecycleOwner: LifecycleOwner, + flow: StateFlow +): DisposableHandle { + return flow.bind(lifecycleOwner) { this.isEnabled = it } +} diff --git a/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt b/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt new file mode 100644 index 00000000..d5c19fa5 --- /dev/null +++ b/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.Flow + +expect class CFlow(flow: Flow) : Flow + +fun Flow.cFlow(): CFlow = CFlow(this) diff --git a/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt b/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt new file mode 100644 index 00000000..d184e253 --- /dev/null +++ b/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.MutableStateFlow + +expect class CMutableStateFlow(flow: MutableStateFlow) : MutableStateFlow + +fun MutableStateFlow.cMutableStateFlow(): CMutableStateFlow = CMutableStateFlow(this) diff --git a/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt b/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt new file mode 100644 index 00000000..4f62f5d4 --- /dev/null +++ b/mvvm-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.StateFlow + +expect open class CStateFlow(flow: StateFlow) : StateFlow + +fun StateFlow.cStateFlow(): CStateFlow = CStateFlow(this) diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt new file mode 100644 index 00000000..711cd8b1 --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/BindingBase.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.kswift.KSwiftExclude +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import kotlin.native.ref.WeakReference + +@KSwiftExclude +fun V.bind( + flow: CStateFlow, + setter: V.(T) -> Unit +): DisposableHandle { + val weakView: WeakReference = WeakReference(this) + + return flow.subscribe { value -> + val strongView: V = weakView.get() ?: return@subscribe + strongView.setter(value) + } +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/NSNotificationCenterExt.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/NSNotificationCenterExt.kt new file mode 100644 index 00000000..d7d1a9b3 --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/NSNotificationCenterExt.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.kswift.KSwiftExclude +import dev.icerock.moko.mvvm.flow.DisposableHandle +import kotlinx.cinterop.ExportObjCClass +import kotlinx.cinterop.ObjCAction +import kotlinx.cinterop.cstr +import platform.Foundation.NSNotification +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSNotificationName +import platform.Foundation.NSSelectorFromString +import platform.darwin.NSObject +import platform.objc.OBJC_ASSOCIATION_RETAIN +import platform.objc.objc_setAssociatedObject + +@KSwiftExclude +fun NSNotificationCenter.setEventHandler( + notification: NSNotificationName, + ref: T, + lambda: T.() -> Unit +): DisposableHandle { + val lambdaTarget = NotificationLambdaTarget(lambda) + + addObserver( + observer = lambdaTarget, + selector = NSSelectorFromString("action:"), + name = notification, + `object` = ref + ) + + objc_setAssociatedObject( + `object` = ref, + key = "notification$notification".cstr, + value = lambdaTarget, + policy = OBJC_ASSOCIATION_RETAIN + ) + + return DisposableHandle { + removeObserver(lambdaTarget) + // TODO remove associated object too, when it will be available in kotlin + } +} + +@ExportObjCClass +private class NotificationLambdaTarget( + val lambda: T.() -> Unit +) : NSObject() { + + @ObjCAction + fun action(notification: NSNotification) { + @Suppress("UNCHECKED_CAST") + val ref = notification.`object` as T + lambda(ref) + } +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIButtonBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIButtonBindings.kt new file mode 100644 index 00000000..7a48624b --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIButtonBindings.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import platform.UIKit.UIButton +import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIImage + +fun UIButton.bindTitle(flow: CStateFlow): DisposableHandle { + return bind(flow) { value -> + setTitle(value, forState = UIControlStateNormal) + } +} + +fun UIButton.bindImage( + flow: CStateFlow, + trueImage: UIImage, + falseImage: UIImage +): DisposableHandle { + return bind(flow) { value -> + val image = when (value) { + true -> trueImage + false -> falseImage + } + + setImage(image, forState = UIControlStateNormal) + } +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIControlBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIControlBindings.kt new file mode 100644 index 00000000..f2d007ce --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIControlBindings.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CMutableStateFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import dev.icerock.moko.mvvm.flow.plus +import platform.UIKit.UIControl +import platform.UIKit.UIControlEventEditingDidBegin +import platform.UIKit.UIControlEventEditingDidEnd +import platform.UIKit.UIControlEventEditingDidEndOnExit + +fun UIControl.bindEnabled( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { this.enabled = it } +} + +fun UIControl.bindFocusTwoWay(flow: CMutableStateFlow): DisposableHandle { + val readCloseable = bindFocus(flow) + + val handler: UIControl.() -> Unit = { + val focused = isFocused() + if (flow.value != focused) flow.value = focused + } + + val beginCloseable = setEventHandler(UIControlEventEditingDidBegin, handler) + val endCloseable = setEventHandler(UIControlEventEditingDidEnd, handler) + val endOnExitCloseable = setEventHandler(UIControlEventEditingDidEndOnExit, handler) + + return readCloseable + beginCloseable + endCloseable + endOnExitCloseable +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIControlExt.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIControlExt.kt new file mode 100644 index 00000000..8604e554 --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIControlExt.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.kswift.KSwiftExclude +import dev.icerock.moko.mvvm.flow.DisposableHandle +import kotlinx.cinterop.ExportObjCClass +import kotlinx.cinterop.ObjCAction +import kotlinx.cinterop.cstr +import platform.Foundation.NSSelectorFromString +import platform.UIKit.UIControl +import platform.UIKit.UIControlEvents +import platform.darwin.NSObject +import platform.objc.OBJC_ASSOCIATION_RETAIN +import platform.objc.objc_setAssociatedObject + +@KSwiftExclude +fun T.setEventHandler( + event: UIControlEvents, + lambda: T.() -> Unit +): DisposableHandle { + val lambdaTarget = ControlLambdaTarget(lambda) + val action = NSSelectorFromString("action:") + + addTarget( + target = lambdaTarget, + action = action, + forControlEvents = event + ) + + objc_setAssociatedObject( + `object` = this, + key = "event$event".cstr, + value = lambdaTarget, + policy = OBJC_ASSOCIATION_RETAIN + ) + + return DisposableHandle { + removeTarget(target = lambdaTarget, action = action, forControlEvents = event) + // TODO remove associated object too, when it will be available in kotlin + } +} + +@ExportObjCClass +private class ControlLambdaTarget( + private val lambda: T.() -> Unit +) : NSObject() { + + @ObjCAction + fun action(sender: UIControl) { + @Suppress("UNCHECKED_CAST") + lambda(sender as T) + } +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UILabelBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UILabelBindings.kt new file mode 100644 index 00000000..f313b568 --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UILabelBindings.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import platform.UIKit.UILabel + +fun UILabel.bindText( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { this.text = it } +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UISwitchBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UISwitchBindings.kt new file mode 100644 index 00000000..d801d76e --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UISwitchBindings.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CMutableStateFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import dev.icerock.moko.mvvm.flow.plus +import platform.UIKit.UIControlEventValueChanged +import platform.UIKit.UISwitch + +fun UISwitch.bindSwitchOn( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { this.on = it } +} + +fun UISwitch.bindSwitchOnTwoWay( + flow: CMutableStateFlow +): DisposableHandle { + val readCloseable = bindSwitchOn(flow) + + val writeCloseable = setEventHandler(UIControlEventValueChanged) { + if (flow.value == on) return@setEventHandler + + flow.value = on + } + + return readCloseable + writeCloseable +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UITextFieldBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UITextFieldBindings.kt new file mode 100644 index 00000000..235035e4 --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UITextFieldBindings.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CMutableStateFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import dev.icerock.moko.mvvm.flow.plus +import platform.UIKit.UIControlEventEditingChanged +import platform.UIKit.UITextField + +fun UITextField.bindText( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { value -> + if (this.text == value) return@bind + + this.text = value + } +} + +fun UITextField.bindTextTwoWay( + flow: CMutableStateFlow +): DisposableHandle { + val readCloseable = bindText(flow) + + val writeCloseable = setEventHandler(UIControlEventEditingChanged) { + val newText = this.text.orEmpty() + + if (flow.value == newText) return@setEventHandler + + flow.value = newText + } + + return readCloseable + writeCloseable +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UITextViewBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UITextViewBindings.kt new file mode 100644 index 00000000..c65f8211 --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UITextViewBindings.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CMutableStateFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import dev.icerock.moko.mvvm.flow.plus +import platform.Foundation.NSNotificationCenter +import platform.UIKit.UITextView +import platform.UIKit.UITextViewTextDidBeginEditingNotification +import platform.UIKit.UITextViewTextDidChangeNotification +import platform.UIKit.UITextViewTextDidEndEditingNotification + +fun UITextView.bindText( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { value -> + if (this.text == value) return@bind + + this.text = value.orEmpty() + } +} + +fun UITextView.bindTextTwoWay( + flow: CMutableStateFlow +): DisposableHandle { + val readCloseable = bindText(flow) + + val writeCloseable = NSNotificationCenter.defaultCenter.setEventHandler( + notification = UITextViewTextDidChangeNotification, + ref = this + ) { + val newText = this.text + + if (flow.value == newText) return@setEventHandler + + flow.value = newText + } + + return readCloseable + writeCloseable +} + +fun UITextView.bindFocusTwoWay( + flow: CMutableStateFlow +): DisposableHandle { + val readCloseable = bindFocus(flow) + + val handler: UITextView.() -> Unit = { + val focused = isFocused() + + if (flow.value != focused) flow.value = focused + } + + val beginCloseable = NSNotificationCenter.defaultCenter.setEventHandler( + notification = UITextViewTextDidBeginEditingNotification, + ref = this, + lambda = handler + ) + val endCloseable = NSNotificationCenter.defaultCenter.setEventHandler( + notification = UITextViewTextDidEndEditingNotification, + ref = this, + lambda = handler + ) + + return readCloseable + beginCloseable + endCloseable +} diff --git a/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIViewBindings.kt b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIViewBindings.kt new file mode 100644 index 00000000..f663adbb --- /dev/null +++ b/mvvm-flow/src/iosMain/kotlin/dev/icerock/moko/mvvm/flow/binding/UIViewBindings.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow.binding + +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.DisposableHandle +import platform.UIKit.UIColor +import platform.UIKit.UIView +import platform.UIKit.backgroundColor +import platform.UIKit.hidden + +fun UIView.bindBackgroundColor( + flow: CStateFlow, + trueColor: UIColor, + falseColor: UIColor +): DisposableHandle { + return bind(flow) { value -> + val color = when (value) { + true -> trueColor + false -> falseColor + } + + backgroundColor = color + } +} + +fun UIView.bindHidden( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { value -> + hidden = value + } +} + +fun UIView.bindFocus( + flow: CStateFlow +): DisposableHandle { + return bind(flow) { value -> + if (value) { + becomeFirstResponder() + } else { + if (nextResponder?.canBecomeFirstResponder == true) { + nextResponder?.becomeFirstResponder() + } else { + resignFirstResponder() + } + } + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UIButtonBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UIButtonBindingsTests.kt new file mode 100644 index 00000000..ea611536 --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UIButtonBindingsTests.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindImage +import dev.icerock.moko.mvvm.flow.binding.bindTitle +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UIButton +import platform.UIKit.UIImage +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UIButtonBindingsTests { + + private lateinit var destination: UIButton + + @BeforeTest + fun setup() { + destination = UIButton(frame = CGRectZero.readValue()) + } + + @Test + fun `nonnullable string title`() { + val source: MutableStateFlow = MutableStateFlow("init") + destination.bindTitle(source.cStateFlow()) + assertEquals( + expected = "init", + actual = destination.currentTitle + ) + source.value = "second" + assertEquals( + expected = "second", + actual = destination.currentTitle + ) + } + + @Test + fun `nullable string title`() { + val source: MutableStateFlow = MutableStateFlow(null) + destination.bindTitle(source.cStateFlow()) + assertEquals( + expected = null, + actual = destination.currentTitle + ) + source.value = "value" + assertEquals( + expected = "value", + actual = destination.currentTitle + ) + } + + @Test + fun `bool image`() { + val source: MutableStateFlow = MutableStateFlow(false) + val trueImage = UIImage() + val falseImage = UIImage() + destination.bindImage( + flow = source.cStateFlow(), + trueImage = trueImage, + falseImage = falseImage + ) + assertEquals( + expected = falseImage, + actual = destination.currentImage + ) + source.value = true + assertEquals( + expected = trueImage, + actual = destination.currentImage + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UIControlBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UIControlBindingsTests.kt new file mode 100644 index 00000000..a6805fd3 --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UIControlBindingsTests.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindEnabled +import dev.icerock.moko.mvvm.flow.binding.bindFocusTwoWay +import dev.icerock.moko.mvvm.flow.cMutableStateFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.Foundation.NSDate +import platform.Foundation.NSRunLoop +import platform.Foundation.date +import platform.Foundation.runUntilDate +import platform.UIKit.UITextField +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UIControlBindingsTests { + + private lateinit var destination: UITextField + + @BeforeTest + fun setup() { + destination = UITextField(frame = CGRectZero.readValue()) + } + + @Test + fun `bool enabled`() { + val source: MutableStateFlow = MutableStateFlow(false) + destination.bindEnabled(source.cStateFlow()) + assertEquals( + expected = false, + actual = destination.enabled + ) + source.value = true + assertEquals( + expected = true, + actual = destination.enabled + ) + } + + // disabled while not found way to sync becomeFirstResponder logic with current thread +// @Test + fun `bool two way focused`() { + val source: MutableStateFlow = MutableStateFlow(false) + destination.bindFocusTwoWay(source.cMutableStateFlow()) + assertEquals( + expected = false, + actual = destination.focused + ) + + source.value = true + NSRunLoop.currentRunLoop.runUntilDate(NSDate.date()) + assertEquals( + expected = true, + actual = destination.focused + ) + + destination.resignFirstResponder() + NSRunLoop.currentRunLoop.runUntilDate(NSDate.date()) + assertEquals( + expected = false, + actual = destination.focused + ) + assertEquals( + expected = false, + actual = source.value + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UILabelBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UILabelBindingsTests.kt new file mode 100644 index 00000000..ae97d336 --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UILabelBindingsTests.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindText +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UILabel +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UILabelBindingsTests { + + private lateinit var destination: UILabel + + @BeforeTest + fun setup() { + destination = UILabel(frame = CGRectZero.readValue()) + } + + @Test + fun `nonnullable string text`() { + val source: MutableStateFlow = MutableStateFlow("init") + destination.bindText(source.cStateFlow()) + assertEquals( + expected = "init", + actual = destination.text + ) + source.value = "second" + assertEquals( + expected = "second", + actual = destination.text + ) + } + + @Test + fun `nullable string text`() { + val source: MutableStateFlow = MutableStateFlow(null) + destination.bindText(source.cStateFlow()) + assertEquals( + expected = null, + actual = destination.text + ) + source.value = "value" + assertEquals( + expected = "value", + actual = destination.text + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UIResponderBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UIResponderBindingsTests.kt new file mode 100644 index 00000000..ead53500 --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UIResponderBindingsTests.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindFocus +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.Foundation.NSDate +import platform.Foundation.NSRunLoop +import platform.Foundation.date +import platform.Foundation.runUntilDate +import platform.UIKit.UITextField +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.assertEquals + +@Ignore +class UIResponderBindingsTests { + + private lateinit var destination: UITextField + + @BeforeTest + fun setup() { + destination = UITextField(frame = CGRectZero.readValue()) + } + + // disabled while not found way to sync becomeFirstResponder logic with current thread +// @Test + fun `bool focused`() { + val source: MutableStateFlow = MutableStateFlow(false) + destination.bindFocus(source.cStateFlow()) + assertEquals( + expected = false, + actual = destination.focused + ) + + source.value = true + NSRunLoop.currentRunLoop.runUntilDate(NSDate.date()) + assertEquals( + expected = true, + actual = destination.focused + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UISwitchBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UISwitchBindingsTests.kt new file mode 100644 index 00000000..f9d5252d --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UISwitchBindingsTests.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindSwitchOn +import dev.icerock.moko.mvvm.flow.binding.bindSwitchOnTwoWay +import dev.icerock.moko.mvvm.flow.cMutableStateFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.Foundation.NSDate +import platform.Foundation.NSRunLoop +import platform.Foundation.date +import platform.Foundation.runUntilDate +import platform.UIKit.UISwitch +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UISwitchBindingsTests { + + private lateinit var destination: UISwitch + + @BeforeTest + fun setup() { + destination = UISwitch(frame = CGRectZero.readValue()) + } + + @Test + fun `bool switch on`() { + val source: MutableStateFlow = MutableStateFlow(false) + destination.bindSwitchOn(source.cStateFlow()) + assertEquals( + expected = false, + actual = destination.isOn() + ) + source.value = true + assertEquals( + expected = true, + actual = destination.isOn() + ) + } + + // disabled while not found way to sync setOn logic with current thread +// @Test + fun `bool two way switch on`() { + val source: MutableStateFlow = MutableStateFlow(false) + destination.bindSwitchOnTwoWay(source.cMutableStateFlow()) + assertEquals( + expected = false, + actual = destination.isOn() + ) + + source.value = true + assertEquals( + expected = true, + actual = destination.isOn() + ) + + destination.setOn(false, animated = false) + NSRunLoop.currentRunLoop.runUntilDate(NSDate.date()) + assertEquals( + expected = false, + actual = source.value + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UITextFieldBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UITextFieldBindingsTests.kt new file mode 100644 index 00000000..66801adf --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UITextFieldBindingsTests.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindText +import dev.icerock.moko.mvvm.flow.binding.bindTextTwoWay +import dev.icerock.moko.mvvm.flow.cMutableStateFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UITextField +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UITextFieldBindingsTests { + + private lateinit var destination: UITextField + + @BeforeTest + fun setup() { + destination = UITextField(frame = CGRectZero.readValue()) + } + + @Test + fun `nonnullable string text`() { + val source: MutableStateFlow = MutableStateFlow("init") + destination.bindText(source.cStateFlow()) + assertEquals( + expected = "init", + actual = destination.text + ) + source.value = "second" + assertEquals( + expected = "second", + actual = destination.text + ) + } + + // can't now set uitextfield value as user do +// @Test + fun `nonnullable two way string text`() { + val source: MutableStateFlow = MutableStateFlow("init") + destination.bindTextTwoWay(source.cMutableStateFlow()) + assertEquals( + expected = "init", + actual = destination.text + ) + + source.value = "second" + assertEquals( + expected = "second", + actual = destination.text + ) + + destination.text = "third" + assertEquals( + expected = "third", + actual = source.value + ) + } + + @Test + fun `nullable string text`() { + val source: MutableStateFlow = MutableStateFlow(null) + destination.bindText(source.cStateFlow()) + assertEquals( + expected = "", + actual = destination.text + ) + source.value = "value" + assertEquals( + expected = "value", + actual = destination.text + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UITextViewBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UITextViewBindingsTests.kt new file mode 100644 index 00000000..fe640579 --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UITextViewBindingsTests.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindText +import dev.icerock.moko.mvvm.flow.binding.bindTextTwoWay +import dev.icerock.moko.mvvm.flow.cMutableStateFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UITextView +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UITextViewBindingsTests { + + private lateinit var destination: UITextView + + @BeforeTest + fun setup() { + destination = UITextView(frame = CGRectZero.readValue()) + } + + @Test + fun `nonnullable string text`() { + val source: MutableStateFlow = MutableStateFlow("init") + destination.bindText(source.cStateFlow()) + assertEquals( + expected = "init", + actual = destination.text + ) + source.value = "second" + assertEquals( + expected = "second", + actual = destination.text + ) + } + + // can't now set uitextview value as user do +// @Test + fun `nonnullable two way string text`() { + val source: MutableStateFlow = MutableStateFlow("init") + destination.bindTextTwoWay(source.cMutableStateFlow()) + assertEquals( + expected = "init", + actual = destination.text + ) + + source.value = "second" + assertEquals( + expected = "second", + actual = destination.text + ) + + destination.text = "third" + assertEquals( + expected = "third", + actual = source.value + ) + } + + @Test + fun `nullable string text`() { + val source: MutableStateFlow = MutableStateFlow(null) + destination.bindText(source.cStateFlow()) + assertEquals( + expected = "", + actual = destination.text + ) + source.value = "value" + assertEquals( + expected = "value", + actual = destination.text + ) + } +} diff --git a/mvvm-flow/src/iosTest/kotlin/UIViewBindingsTests.kt b/mvvm-flow/src/iosTest/kotlin/UIViewBindingsTests.kt new file mode 100644 index 00000000..7e4ea3bd --- /dev/null +++ b/mvvm-flow/src/iosTest/kotlin/UIViewBindingsTests.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import dev.icerock.moko.mvvm.flow.binding.bindBackgroundColor +import dev.icerock.moko.mvvm.flow.binding.bindHidden +import dev.icerock.moko.mvvm.flow.cStateFlow +import kotlinx.cinterop.readValue +import kotlinx.coroutines.flow.MutableStateFlow +import platform.CoreGraphics.CGRectZero +import platform.UIKit.UIColor +import platform.UIKit.UIView +import platform.UIKit.backgroundColor +import platform.UIKit.isHidden +import kotlin.test.BeforeTest +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +@Ignore +class UIViewBindingsTests { + + private lateinit var destination: UIView + + @BeforeTest + fun setup() { + destination = UIView(frame = CGRectZero.readValue()) + } + + @Test + fun `bool hidden`() { + val source: MutableStateFlow = MutableStateFlow(false) + destination.bindHidden(source.cStateFlow()) + assertEquals( + expected = false, + actual = destination.isHidden() + ) + source.value = true + assertEquals( + expected = true, + actual = destination.isHidden() + ) + } + + @Test + fun `bool color`() { + val source: MutableStateFlow = MutableStateFlow(false) + val trueColor = UIColor.blueColor + val falseColor = UIColor.redColor + destination.bindBackgroundColor( + flow = source.cStateFlow(), + trueColor = trueColor, + falseColor = falseColor + ) + assertEquals( + expected = falseColor, + actual = destination.backgroundColor + ) + source.value = true + assertEquals( + expected = trueColor, + actual = destination.backgroundColor + ) + } +} diff --git a/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt new file mode 100644 index 00000000..00eca1de --- /dev/null +++ b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CFlow.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +actual open class CFlow actual constructor( + private val flow: Flow +) : Flow by flow { + + fun subscribe( + coroutineScope: CoroutineScope, + dispatcher: CoroutineDispatcher, + onCollect: (T) -> Unit + ): DisposableHandle { + val job: Job = coroutineScope.launch(dispatcher) { + flow.onEach { onCollect(it) }.collect() + } + return DisposableHandle { + job.cancel() + } + } + + fun subscribe(onCollect: (T) -> Unit): DisposableHandle { + @Suppress("OPT_IN_USAGE") + return subscribe( + coroutineScope = GlobalScope, + dispatcher = Dispatchers.Main, + onCollect = onCollect + ) + } +} diff --git a/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt new file mode 100644 index 00000000..b3f60631 --- /dev/null +++ b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CMutableStateFlow.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +actual class CMutableStateFlow actual constructor( + private val flow: MutableStateFlow +) : CStateFlow(flow), MutableStateFlow { + + override var value: T + get() = super.value + set(value) { + flow.value = value + } + + override val subscriptionCount: StateFlow = flow.subscriptionCount + + override suspend fun emit(value: T) = flow.emit(value) + + @ExperimentalCoroutinesApi + override fun resetReplayCache() = flow.resetReplayCache() + + override fun tryEmit(value: T): Boolean = flow.tryEmit(value) + + override fun compareAndSet(expect: T, update: T): Boolean = flow.compareAndSet(expect, update) +} diff --git a/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt new file mode 100644 index 00000000..bda2d595 --- /dev/null +++ b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/CStateFlow.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow + +actual open class CStateFlow actual constructor( + private val flow: StateFlow +) : CFlow(flow), StateFlow { + override val replayCache: List get() = flow.replayCache + + override suspend fun collect(collector: FlowCollector): Nothing = flow.collect(collector) + + override val value: T get() = flow.value +} diff --git a/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/DisposableHandle.kt b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/DisposableHandle.kt new file mode 100644 index 00000000..f449ebfb --- /dev/null +++ b/mvvm-flow/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/flow/DisposableHandle.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +// just to not export all coroutines api to swift side +interface DisposableHandle : kotlinx.coroutines.DisposableHandle + +fun DisposableHandle(block: () -> Unit): DisposableHandle { + return object : DisposableHandle { + override fun dispose() { + block() + } + } +} + +operator fun DisposableHandle.plus(other: DisposableHandle): DisposableHandle { + return DisposableHandle { + this.dispose() + other.dispose() + } +} diff --git a/mvvm-livedata-compose/build.gradle.kts b/mvvm-livedata-compose/build.gradle.kts new file mode 100644 index 00000000..86bd90c2 --- /dev/null +++ b/mvvm-livedata-compose/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.multiplatform") + id("android-base-convention") + id("dev.icerock.mobile.multiplatform.android-manifest") + id("detekt-convention") + id("org.jetbrains.compose") + id("javadoc-stub-convention") + id("publication-convention") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(11)) + } +} + +kotlin { + android() + jvm() + js(IR) { + browser() + } + + sourceSets { + val commonMain by getting { + dependencies { + api(projects.mvvmLivedata) + + api(compose.runtime) + } + } + val androidMain by getting { + dependencies { + api(libs.composeLiveData) + } + } + + val nonAndroidMain by creating { + dependsOn(commonMain) + } + val jvmMain by getting { + dependsOn(nonAndroidMain) + } + val jsMain by getting { + dependsOn(nonAndroidMain) + } + } +} diff --git a/mvvm-livedata-compose/src/androidMain/AndroidManifest.xml b/mvvm-livedata-compose/src/androidMain/AndroidManifest.xml new file mode 100755 index 00000000..513479d4 --- /dev/null +++ b/mvvm-livedata-compose/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mvvm-livedata-compose/src/androidMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt b/mvvm-livedata-compose/src/androidMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt new file mode 100644 index 00000000..e7ad0a9c --- /dev/null +++ b/mvvm-livedata-compose/src/androidMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.livedata.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.livedata.observeAsState +import dev.icerock.moko.mvvm.livedata.LiveData + +@Composable +actual fun LiveData.observeAsState(): State { + return ld().observeAsState(initial = this.value) +} diff --git a/mvvm-livedata-compose/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt b/mvvm-livedata-compose/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt new file mode 100644 index 00000000..92997503 --- /dev/null +++ b/mvvm-livedata-compose/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.livedata.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import dev.icerock.moko.mvvm.livedata.LiveData + +@Composable +expect fun LiveData.observeAsState(): State diff --git a/mvvm-livedata-compose/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt b/mvvm-livedata-compose/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt new file mode 100644 index 00000000..256468ee --- /dev/null +++ b/mvvm-livedata-compose/src/nonAndroidMain/kotlin/dev/icerock/moko/mvvm/livedata/compose/LiveDataState.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.livedata.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import dev.icerock.moko.mvvm.livedata.LiveData + +@Composable +actual fun LiveData.observeAsState(): State { + val self: LiveData = this + val state = remember { mutableStateOf(self.value) } + DisposableEffect(self) { + val observer: (T) -> Unit = { state.value = it } + self.addObserver(observer) + onDispose { removeObserver(observer) } + } + return state +} diff --git a/sample-declarative-ui/.gitignore b/sample-declarative-ui/.gitignore new file mode 100644 index 00000000..bf68fbac --- /dev/null +++ b/sample-declarative-ui/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +.idea +.DS_Store +/build +*/build +/captures +.externalNativeBuild +.cxx +local.properties \ No newline at end of file diff --git a/sample-declarative-ui/.idea/copyright/IceRock.xml b/sample-declarative-ui/.idea/copyright/IceRock.xml new file mode 100644 index 00000000..0aa66dcf --- /dev/null +++ b/sample-declarative-ui/.idea/copyright/IceRock.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/sample-declarative-ui/.idea/copyright/profiles_settings.xml b/sample-declarative-ui/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..ed93011f --- /dev/null +++ b/sample-declarative-ui/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/sample-declarative-ui/androidApp/build.gradle.kts b/sample-declarative-ui/androidApp/build.gradle.kts new file mode 100644 index 00000000..a4fcc477 --- /dev/null +++ b/sample-declarative-ui/androidApp/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("com.android.application") + kotlin("android") +} + +val composeVersion = "1.1.1" + +android { + compileSdk = 32 + buildFeatures { + compose = true + } + defaultConfig { + applicationId = "dev.icerock.moko.mvvm.sample.declarativeui.android" + minSdk = 21 + targetSdk = 32 + versionCode = 1 + versionName = "1.0" + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + composeOptions { + kotlinCompilerExtensionVersion = composeVersion + } +} + +dependencies { + implementation(project(":shared")) + + implementation("androidx.compose.ui:ui:$composeVersion") + // Tooling support (Previews, etc.) + implementation("androidx.compose.ui:ui-tooling:$composeVersion") + // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) + implementation("androidx.compose.foundation:foundation:$composeVersion") + // Material Design + implementation("androidx.compose.material:material:$composeVersion") + // Material design icons + implementation("androidx.compose.material:material-icons-core:$composeVersion") + // Integration with observables + implementation("androidx.compose.runtime:runtime-livedata:$composeVersion") + + implementation("androidx.activity:activity-compose:1.4.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1") + implementation("androidx.navigation:navigation-compose:2.4.1") +} \ No newline at end of file diff --git a/sample-declarative-ui/androidApp/src/main/AndroidManifest.xml b/sample-declarative-ui/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 00000000..03e3a34d --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/BookListUI.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/BookListUI.kt new file mode 100644 index 00000000..74d564a5 --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/BookListUI.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.android + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Card +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.ListItem +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Warning +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import dev.icerock.moko.mvvm.createViewModelFactory +import dev.icerock.moko.mvvm.flow.compose.observeAsActions +import dev.icerock.moko.mvvm.sample.declarativeui.BookListViewModel +import dev.icerock.moko.resources.compose.localized +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc + +@Composable +fun BookListScreen( + viewModel: BookListViewModel = viewModel( + factory = createViewModelFactory { BookListViewModel().start() } + ), + onOpenBook: (Int) -> Unit = {} +) { + val state: BookListViewModel.State by viewModel.state.collectAsState() + val context: Context = LocalContext.current + + viewModel.actions.observeAsActions { it.handleAction(context, onOpenBook) } + + when (@Suppress("NAME_SHADOWING") val state = state) { + is BookListViewModel.State.Empty -> EmptyState(message = state.message) + is BookListViewModel.State.Error -> ErrorState(message = state.message) + BookListViewModel.State.Loading -> LoadingState() + is BookListViewModel.State.Success -> SuccessState(items = state.items) + } +} + +private fun BookListViewModel.Action.handleAction( + context: Context, + onOpenBook: (Int) -> Unit +) { + when (this) { + is BookListViewModel.Action.OpenUrl -> { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(this.url)) + context.startActivity(intent) + } + is BookListViewModel.Action.RouteToBookDetails -> onOpenBook(this.id) + } +} + +@Composable +private fun EmptyState(message: StringDesc) { + Box(Modifier.fillMaxSize()) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = Icons.Default.Clear, + contentDescription = null, + modifier = Modifier.size(128.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = message.localized()) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun EmptyStatePreview() { + EmptyState(message = "no items".desc()) +} + +@Composable +private fun ErrorState(message: StringDesc) { + Box(Modifier.fillMaxSize()) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + imageVector = Icons.Filled.Warning, + contentDescription = null, + modifier = Modifier.size(128.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = message.localized(), color = Color.Red) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun ErrorStatePreview() { + ErrorState(message = "No internet :(".desc()) +} + +@Composable +private fun LoadingState() { + Box(Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun LoadingStatePreview() { + LoadingState() +} + +@Composable +private fun SuccessState(items: List) { + LazyColumn(Modifier.fillMaxSize()) { + items(items, key = { it.id }) { item -> + when (item) { + is BookListViewModel.ListUnit.AdvertUnit -> AdvertUnit(item) + is BookListViewModel.ListUnit.BookUnit -> BookUnit(item) + } + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun BookUnit(unit: BookListViewModel.ListUnit.BookUnit) { + ListItem( + text = { Text(unit.title) }, + trailing = { + Box(modifier = Modifier.fillMaxHeight()) { + Icon( + Icons.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.align(Alignment.Center) + ) + } + }, + modifier = Modifier.clickable { unit.onPressed() } + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun AdvertUnit(unit: BookListViewModel.ListUnit.AdvertUnit) { + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + Card( + modifier = Modifier.fillMaxWidth(), + onClick = { unit.onPressed() } + ) { + Text(modifier = Modifier.padding(8.dp), text = unit.text, textAlign = TextAlign.Center) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun SuccessStatePreview() { + SuccessState( + items = listOf( + BookListViewModel.ListUnit.BookUnit( + id = "1", + title = "1984", + onPressed = {} + ), + BookListViewModel.ListUnit.BookUnit( + id = "2", + title = "iOS Development", + onPressed = {} + ), + BookListViewModel.ListUnit.AdvertUnit( + id = "3", + text = "This advert!", + onPressed = {} + ), + BookListViewModel.ListUnit.BookUnit( + id = "4", + title = "Android Development", + onPressed = {} + ), + BookListViewModel.ListUnit.AdvertUnit( + id = "5", + text = "This advert 2!", + onPressed = {} + ) + ) + ) +} diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/BookReviewUI.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/BookReviewUI.kt new file mode 100644 index 00000000..dd5582ef --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/BookReviewUI.kt @@ -0,0 +1,199 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.android + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import dev.icerock.moko.mvvm.createViewModelFactory +import dev.icerock.moko.mvvm.flow.compose.observeAsActions +import dev.icerock.moko.mvvm.sample.declarativeui.BookReviewViewModel +import dev.icerock.moko.resources.compose.localized +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc + +@Composable +fun BookReviewScreen( + bookId: Int, + viewModel: BookReviewViewModel = viewModel( + factory = createViewModelFactory { BookReviewViewModel(bookId) } + ), + onCloseScreen: () -> Unit = {} +) { + val state: BookReviewViewModel.State by viewModel.state.collectAsState() + + viewModel.actions.observeAsActions { it.handleAction(onCloseScreen) } + + when (@Suppress("NAME_SHADOWING") val state = state) { + is BookReviewViewModel.State.Error -> ErrorState( + inputForm = state.form, + message = state.message, + onClose = { viewModel.onErrorClosed() } + ) + is BookReviewViewModel.State.Idle -> IdleState( + inputForm = state.form, + onRateChanged = { viewModel.onRateChange(it) }, + onMessageChanged = { viewModel.onMessageChange(it) }, + onSubmit = { viewModel.onSendPressed() } + ) + is BookReviewViewModel.State.Loading -> LoadingState( + inputForm = state.form + ) + } +} + +private fun BookReviewViewModel.Action.handleAction( + onCloseScreen: () -> Unit +) { + when (this) { + BookReviewViewModel.Action.CloseScreen -> onCloseScreen() + } +} + +@Composable +private fun ReviewInputForm( + inputForm: BookReviewViewModel.InputForm, + onRateChanged: ((Int) -> Unit)? = null, + onMessageChanged: ((String) -> Unit)? = null, + onSubmit: (() -> Unit)? = null +) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState) + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + for (i in 1..5) { + if (i != 1) { + Spacer(modifier = Modifier.width(8.dp)) + } + Button( + onClick = { onRateChanged?.invoke(i) }, + enabled = onRateChanged != null, + colors = ButtonDefaults.outlinedButtonColors( + backgroundColor = if (inputForm.rate == i) MaterialTheme.colors.secondary + else MaterialTheme.colors.surface + ) + ) { + Text(text = i.toString()) + } + } + } + Spacer(modifier = Modifier.height(8.dp)) + TextField( + modifier = Modifier.fillMaxWidth(), + value = inputForm.message, + enabled = onMessageChanged != null, + onValueChange = { onMessageChanged?.invoke(it) } + ) + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = { onSubmit?.invoke() }, + enabled = onSubmit != null + ) { + Text(text = "Send") + } + } +} + +@Composable +private fun IdleState( + inputForm: BookReviewViewModel.InputForm, + onRateChanged: (Int) -> Unit, + onMessageChanged: (String) -> Unit, + onSubmit: () -> Unit +) { + ReviewInputForm( + inputForm = inputForm, + onRateChanged = onRateChanged, + onMessageChanged = onMessageChanged, + onSubmit = onSubmit + ) +} + +@Preview(showSystemUi = true) +@Composable +private fun IdleStatePreview() { + IdleState( + inputForm = BookReviewViewModel.InputForm(rate = 3, message = "hello"), + onRateChanged = {}, + onMessageChanged = {}, + onSubmit = {} + ) +} + +@Composable +private fun ErrorState( + inputForm: BookReviewViewModel.InputForm, + message: StringDesc, + onClose: () -> Unit +) { + Box(modifier = Modifier.fillMaxSize()) { + ReviewInputForm(inputForm) + AlertDialog( + title = { Text("Error") }, + text = { Text(message.localized()) }, + onDismissRequest = onClose, + confirmButton = { + Button(onClick = onClose) { Text(text = "Close") } + } + ) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun ErrorStatePreview() { + ErrorState( + inputForm = BookReviewViewModel.InputForm(rate = 3, message = "hello"), + message = "fail to load!".desc(), + onClose = {} + ) +} + +@Composable +private fun LoadingState(inputForm: BookReviewViewModel.InputForm) { + Box(modifier = Modifier.fillMaxSize()) { + ReviewInputForm(inputForm) + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun LoadingStatePreview() { + LoadingState( + inputForm = BookReviewViewModel.InputForm(rate = 3, message = "hello") + ) +} diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/ComposeApp.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/ComposeApp.kt new file mode 100644 index 00000000..107a1384 --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/ComposeApp.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.android + +import androidx.compose.runtime.Composable +import androidx.navigation.NavOptions +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController + +@Composable +fun ComposeApp() { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = "login") { + composable("login") { + LoginScreen( + onLoginSuccess = { + navController.navigate( + "books", + navOptions = NavOptions.Builder().setLaunchSingleTop(true).build() + ) + } + ) + } + composable("books") { + BookListScreen( + onOpenBook = { + navController.navigate("books/$it/review") + } + ) + } + composable("books/{bookId}/review") { entry -> + val bookId: Int = entry.arguments?.getInt("bookId").let { requireNotNull(it) } + BookReviewScreen( + bookId = bookId, + onCloseScreen = { navController.popBackStack() } + ) + } + } +} diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/LoginUI.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/LoginUI.kt new file mode 100644 index 00000000..aeb07d9d --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/LoginUI.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.android + +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import dev.icerock.moko.mvvm.createViewModelFactory +import dev.icerock.moko.mvvm.flow.compose.observeAsActions +import dev.icerock.moko.mvvm.sample.declarativeui.LoginViewModel + +@Composable +fun LoginScreen( + viewModel: LoginViewModel = viewModel( + factory = createViewModelFactory { LoginViewModel() } + ), + onLoginSuccess: () -> Unit = {} +) { + val currentOnLoginSuccess by rememberUpdatedState(onLoginSuccess) + val context: Context = LocalContext.current + + val login: String by viewModel.login.collectAsState() + val password: String by viewModel.password.collectAsState() + val isLoading: Boolean by viewModel.isLoading.collectAsState() + val isLoginButtonEnabled: Boolean by viewModel.isLoginButtonEnabled.collectAsState() + + viewModel.actions.observeAsActions { it.handleAction(context, currentOnLoginSuccess) } + + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + TextField( + modifier = Modifier.fillMaxWidth(), + value = login, + label = { Text(text = "Login") }, + onValueChange = { viewModel.login.value = it } + ) + Spacer(modifier = Modifier.height(8.dp)) + TextField( + modifier = Modifier.fillMaxWidth(), + value = password, + label = { Text(text = "Password") }, + visualTransformation = PasswordVisualTransformation(), + onValueChange = { viewModel.password.value = it } + ) + Spacer(modifier = Modifier.height(8.dp)) + Button( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + enabled = isLoginButtonEnabled, + onClick = { viewModel.onLoginPressed() } + ) { + if (isLoading) CircularProgressIndicator(modifier = Modifier.size(24.dp)) + else Text(text = "Login") + } + } +} + +private fun LoginViewModel.Action.handleAction( + context: Context, + onLoginSuccess: () -> Unit +) { + when (this) { + LoginViewModel.Action.RouteToSuccess -> onLoginSuccess() + is LoginViewModel.Action.ShowError -> { + Toast.makeText(context, this.error.toString(context), Toast.LENGTH_SHORT).show() + } + } +} + +@Preview(showSystemUi = true, group = "empty") +@Composable +fun LoginScreen_Preview() { + LoginScreen() +} + +@Preview(showSystemUi = true, group = "filled") +@Composable +fun LoginScreenFilledLogin_Preview() { + LoginScreen( + viewModel = LoginViewModel().apply { + login.value = "test" + } + ) +} + +@Preview(showSystemUi = true, group = "filled") +@Composable +fun LoginScreenFilledAll_Preview() { + LoginScreen( + viewModel = LoginViewModel().apply { + login.value = "test" + password.value = "test pass" + } + ) +} + +@Preview(showSystemUi = true, group = "load") +@Composable +fun LoginScreenLoading_Preview() { + LoginScreen( + viewModel = LoginViewModel().apply { + login.value = "test" + password.value = "test pass" + onLoginPressed() + } + ) +} diff --git a/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/MainActivity.kt b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/MainActivity.kt new file mode 100644 index 00000000..9b32a6d3 --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/java/dev/icerock/moko/mvvm/sample/declarativeui/android/MainActivity.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.android + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material.MaterialTheme + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MaterialTheme { + ComposeApp() + } + } + } +} diff --git a/sample-declarative-ui/androidApp/src/main/res/values/colors.xml b/sample-declarative-ui/androidApp/src/main/res/values/colors.xml new file mode 100644 index 00000000..4faecfa8 --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #6200EE + #3700B3 + #03DAC5 + \ No newline at end of file diff --git a/sample-declarative-ui/androidApp/src/main/res/values/styles.xml b/sample-declarative-ui/androidApp/src/main/res/values/styles.xml new file mode 100644 index 00000000..1971a0a0 --- /dev/null +++ b/sample-declarative-ui/androidApp/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/sample-declarative-ui/build.gradle.kts b/sample-declarative-ui/build.gradle.kts new file mode 100644 index 00000000..f302fbe3 --- /dev/null +++ b/sample-declarative-ui/build.gradle.kts @@ -0,0 +1,24 @@ +buildscript { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") + classpath("com.android.tools.build:gradle:7.1.2") + classpath("dev.icerock.moko:kswift-gradle-plugin:0.4.0") + } +} + +allprojects { + repositories { + google() + mavenCentral() + mavenLocal() + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} diff --git a/sample-declarative-ui/gradle.properties b/sample-declarative-ui/gradle.properties new file mode 100644 index 00000000..3211eb81 --- /dev/null +++ b/sample-declarative-ui/gradle.properties @@ -0,0 +1,13 @@ +#Gradle +org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" + +#Kotlin +kotlin.code.style=official + +#Android +android.useAndroidX=true + +#MPP +kotlin.mpp.enableGranularSourceSetsMetadata=true +kotlin.native.enableDependencyPropagation=false +kotlin.mpp.enableCInteropCommonization=true \ No newline at end of file diff --git a/sample-declarative-ui/gradle/wrapper/gradle-wrapper.jar b/sample-declarative-ui/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/sample-declarative-ui/gradle/wrapper/gradle-wrapper.jar differ diff --git a/sample-declarative-ui/gradle/wrapper/gradle-wrapper.properties b/sample-declarative-ui/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..5fb8344c --- /dev/null +++ b/sample-declarative-ui/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Feb 25 19:31:30 NOVT 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/sample-declarative-ui/gradlew b/sample-declarative-ui/gradlew new file mode 100755 index 00000000..4f906e0c --- /dev/null +++ b/sample-declarative-ui/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/sample-declarative-ui/gradlew.bat b/sample-declarative-ui/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/sample-declarative-ui/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/sample-declarative-ui/iosApp/Podfile b/sample-declarative-ui/iosApp/Podfile new file mode 100644 index 00000000..b60f7545 --- /dev/null +++ b/sample-declarative-ui/iosApp/Podfile @@ -0,0 +1,9 @@ +platform :ios, '13.0' + +target 'iosApp' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for iosApp + # pod 'mokoMvvmFlowSwiftUI', :podspec => '../../mokoMvvmFlowSwiftUI.podspec' +end diff --git a/sample-declarative-ui/iosApp/Podfile.lock b/sample-declarative-ui/iosApp/Podfile.lock new file mode 100644 index 00000000..43e96948 --- /dev/null +++ b/sample-declarative-ui/iosApp/Podfile.lock @@ -0,0 +1,3 @@ +PODFILE CHECKSUM: 2da617fd937ebbcc6ce1896246df64876a6a334c + +COCOAPODS: 1.11.2 diff --git a/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..88bad6dc --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,467 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + 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 */; }; + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 226D765D27E44894004964F6 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226D765C27E44894004964F6 /* LoginView.swift */; }; + 226D766327E45FCC004964F6 /* LoginViewBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226D766227E45FCC004964F6 /* LoginViewBinding.swift */; }; + 226D766527E48C15004964F6 /* BookListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226D766427E48C15004964F6 /* BookListView.swift */; }; + 226D766727E48C3C004964F6 /* BookListViewBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 226D766627E48C3C004964F6 /* BookListViewBinding.swift */; }; + 2298ADF9281CE1F700E48E23 /* mokoMvvmFlowSwiftUI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE74E281BEDE000259368 /* mokoMvvmFlowSwiftUI.xcframework */; }; + 22BDE74B281BEDC700259368 /* dev_icerock_moko_resources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE748281BEDC700259368 /* dev_icerock_moko_resources.swift */; }; + 22BDE74C281BEDC700259368 /* declarative-ui-sample_shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE749281BEDC700259368 /* declarative-ui-sample_shared.swift */; }; + 22BDE74D281BEDC700259368 /* dev_icerock_moko_mvvm-flow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22BDE74A281BEDC700259368 /* dev_icerock_moko_mvvm-flow.swift */; }; + 22BDE752281BEE8000259368 /* MultiPlatformLibrary.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */; }; + 22BDE753281BEE8000259368 /* MultiPlatformLibrary.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 22EFAD46280A8BCE007A8DCB /* BookReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22EFAD45280A8BCE007A8DCB /* BookReviewView.swift */; }; + 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; + FCF4E6E3EA1204EC05EEF872 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 44E36C38F63DF79F7CEEAE3E /* Pods_iosApp.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 7555FFB4242A642300829871 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 22BDE753281BEE8000259368 /* MultiPlatformLibrary.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 226D765C27E44894004964F6 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; + 226D766227E45FCC004964F6 /* LoginViewBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewBinding.swift; sourceTree = ""; }; + 226D766427E48C15004964F6 /* BookListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookListView.swift; sourceTree = ""; }; + 226D766627E48C3C004964F6 /* BookListViewBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookListViewBinding.swift; sourceTree = ""; }; + 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MultiPlatformLibrary.xcframework; path = ../shared/build/xcode/MultiPlatformLibrary.xcframework; sourceTree = ""; }; + 22BDE748281BEDC700259368 /* dev_icerock_moko_resources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = dev_icerock_moko_resources.swift; path = ../MultiPlatformLibrarySwift/dev_icerock_moko_resources.swift; sourceTree = ""; }; + 22BDE749281BEDC700259368 /* declarative-ui-sample_shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "declarative-ui-sample_shared.swift"; path = "../MultiPlatformLibrarySwift/declarative-ui-sample_shared.swift"; sourceTree = ""; }; + 22BDE74A281BEDC700259368 /* dev_icerock_moko_mvvm-flow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "dev_icerock_moko_mvvm-flow.swift"; path = "../MultiPlatformLibrarySwift/dev_icerock_moko_mvvm-flow.swift"; sourceTree = ""; }; + 22BDE74E281BEDE000259368 /* mokoMvvmFlowSwiftUI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = mokoMvvmFlowSwiftUI.xcframework; path = "../../mvvm-flow/apple/xcode/xcframework/mokoMvvmFlowSwiftUI.xcframework"; sourceTree = ""; }; + 22EFAD45280A8BCE007A8DCB /* BookReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookReviewView.swift; sourceTree = ""; }; + 44E36C38F63DF79F7CEEAE3E /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4745F88F44B963D7A59D2180 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; + 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C67D2807155711FDB9BBDD09 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7555FF78242A565900829871 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2298ADF9281CE1F700E48E23 /* mokoMvvmFlowSwiftUI.xcframework in Frameworks */, + 22BDE752281BEE8000259368 /* MultiPlatformLibrary.xcframework in Frameworks */, + FCF4E6E3EA1204EC05EEF872 /* Pods_iosApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058557D7273AAEEB004C7B11 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 140DDE5BD0BCDADF6482A380 /* Pods */ = { + isa = PBXGroup; + children = ( + C67D2807155711FDB9BBDD09 /* Pods-iosApp.debug.xcconfig */, + 4745F88F44B963D7A59D2180 /* Pods-iosApp.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 22EE33F927C9235100538774 /* kswift */ = { + isa = PBXGroup; + children = ( + 22BDE749281BEDC700259368 /* declarative-ui-sample_shared.swift */, + 22BDE74A281BEDC700259368 /* dev_icerock_moko_mvvm-flow.swift */, + 22BDE748281BEDC700259368 /* dev_icerock_moko_resources.swift */, + ); + name = kswift; + path = ../shared/build/xcode/sharedSwift; + sourceTree = ""; + }; + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + 22BDE74E281BEDE000259368 /* mokoMvvmFlowSwiftUI.xcframework */, + 22BDE746281BEDBA00259368 /* MultiPlatformLibrary.xcframework */, + 22EE33F927C9235100538774 /* kswift */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + 7555FFB0242A642200829871 /* Frameworks */, + 140DDE5BD0BCDADF6482A380 /* Pods */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* iosApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF82242A565900829871 /* ContentView.swift */, + 226D765C27E44894004964F6 /* LoginView.swift */, + 226D766227E45FCC004964F6 /* LoginViewBinding.swift */, + 226D766427E48C15004964F6 /* BookListView.swift */, + 226D766627E48C3C004964F6 /* BookListViewBinding.swift */, + 22EFAD45280A8BCE007A8DCB /* BookReviewView.swift */, + 2152FB032600AC8F00CF470E /* iOSApp.swift */, + 7555FF8C242A565B00829871 /* Info.plist */, + 058557D7273AAEEB004C7B11 /* Preview Content */, + ); + path = iosApp; + sourceTree = ""; + }; + 7555FFB0242A642200829871 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 44E36C38F63DF79F7CEEAE3E /* Pods_iosApp.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + D1546C3CE06A9BC9D549831A /* [CP] Check Pods Manifest.lock */, + 7555FFB5242A651A00829871 /* Build Kotlin */, + 7555FF77242A565900829871 /* Sources */, + 7555FF78242A565900829871 /* Frameworks */, + 7555FF79242A565900829871 /* Resources */, + 7555FFB4242A642300829871 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* iosApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1130; + ORGANIZATIONNAME = orgName; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 7555FFB5242A651A00829871 /* Build Kotlin */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Kotlin"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$SRCROOT/..\"\n./gradlew \":shared:$BUILD_KOTLIN_GRADLE_TASK\"\n"; + }; + D1546C3CE06A9BC9D549831A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 22EFAD46280A8BCE007A8DCB /* BookReviewView.swift in Sources */, + 22BDE74C281BEDC700259368 /* declarative-ui-sample_shared.swift in Sources */, + 226D765D27E44894004964F6 /* LoginView.swift in Sources */, + 22BDE74D281BEDC700259368 /* dev_icerock_moko_mvvm-flow.swift in Sources */, + 226D766327E45FCC004964F6 /* LoginViewBinding.swift in Sources */, + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + 226D766527E48C15004964F6 /* BookListView.swift in Sources */, + 22BDE74B281BEDC700259368 /* dev_icerock_moko_resources.swift in Sources */, + 7555FF83242A565900829871 /* ContentView.swift in Sources */, + 226D766727E48C3C004964F6 /* BookListViewBinding.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C67D2807155711FDB9BBDD09 /* Pods-iosApp.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_KOTLIN_GRADLE_TASK = syncMultiPlatformLibraryDebugXCFramework; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iosApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4745F88F44B963D7A59D2180 /* Pods-iosApp.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_KOTLIN_GRADLE_TASK = syncMultiPlatformLibraryReleaseXCFramework; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iosApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = orgIdentifier.iosApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} diff --git a/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata b/sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..c009e7d7 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..ee7e3ca0 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..fb88a396 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000..4aa7c535 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/sample-declarative-ui/iosApp/iosApp/BookListView.swift b/sample-declarative-ui/iosApp/iosApp/BookListView.swift new file mode 100644 index 00000000..cc667035 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/BookListView.swift @@ -0,0 +1,93 @@ +// +// BookListView.swift +// iosApp +// +// Created by Aleksey Mikhailov on 18.03.2022. +// Copyright © 2022 orgName. All rights reserved. +// + +import Foundation +import SwiftUI +import MultiPlatformLibrary + +struct BookListViewBody: View { + let state: BookListViewModelStateKs + let onRetryPressed: () -> Void + + var body: some View { + switch(state) { + case .loading: + ProgressView() + case .empty(let data): + Text(data.message.localized()) + case .error(let data): + VStack { + Text(data.message.localized()) + Button("Retry") { + onRetryPressed() + } + } + case .success(let data): + List( + data.items, + id: \.id + ) { unit in + let unit = BookListViewModelListUnitKs(unit) + switch(unit) { + case .bookUnit(let bookUnit): + Button( + action: { bookUnit.onPressed() }, + label: { Text(bookUnit.title) } + ).buttonStyle(DefaultButtonStyle()) + case .advertUnit(let adUnit): + Button( + action: { adUnit.onPressed() }, + label: { Text(adUnit.text) } + ).buttonStyle(DefaultButtonStyle()) + } + } + } + } +} + +struct BookListViewBody_Previews: PreviewProvider { + struct Preview: View { + var body: some View { + BookListViewBody( + state: .loading, + onRetryPressed: {} + ) + BookListViewBody( + state: .empty(BookListViewModelStateEmpty(message: "empty".desc())), + onRetryPressed: {} + ) + BookListViewBody( + state: .error(BookListViewModelStateError(message: "error".desc())), + onRetryPressed: {} + ) + BookListViewBody( + state: .success( + BookListViewModelStateSuccess( + items: [ + BookListViewModelListUnitBookUnit(id: "1", title: "test", onPressed: {}), + BookListViewModelListUnitAdvertUnit(id: "2", text: "advert", onPressed: {}) + ] + ) + ), + onRetryPressed: {} + ) + } + } + + static var previews: some View { + Group { + Preview() + }.previewDisplayName("Light theme") + .preferredColorScheme(.light) + + Group { + Preview() + }.previewDisplayName("Dark theme") + .preferredColorScheme(.dark) + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/BookListViewBinding.swift b/sample-declarative-ui/iosApp/iosApp/BookListViewBinding.swift new file mode 100644 index 00000000..7d485205 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/BookListViewBinding.swift @@ -0,0 +1,73 @@ +// +// BookListViewBinding.swift +// iosApp +// +// Created by Aleksey Mikhailov on 18.03.2022. +// Copyright © 2022 orgName. All rights reserved. +// + +import Foundation +import SwiftUI +import MultiPlatformLibrary +import mokoMvvmFlowSwiftUI +import Combine + +func createViewModel() -> BookListViewModel { + return BookListViewModel().start() +} + +struct BookListView: View { + @ObservedObject var viewModel: BookListViewModel = createViewModel() + @State private var detailOpened: Bool = false + @State private var detailBookId: Int32? = nil + + var body: some View { + ZStack { + NavigationLink(isActive: $detailOpened) { + if let id = detailBookId { + BookReviewView(id: id) + } else { + EmptyView() + } + } label: { + EmptyView() + .hidden() + } + + BookListViewBody( + state: viewModel.stateKs, + onRetryPressed: { + viewModel.onRetryPressed() + } + ).onReceive(viewModel.actionsKs) { action in + switch(action) { + case .routeToBookDetails(let data): + detailBookId = data.id + detailOpened = true + case .openUrl(let data): + UIApplication.shared.open(URL(string: data.url)!) + } + }.navigationTitle("Books") + } + } +} + +extension BookListViewModel { + var stateKs: BookListViewModelStateKs { + get { + return self.state( + \.state, + equals: { $0 === $1 }, + mapper: { BookListViewModelStateKs($0) } + ) + } + } + + var actionsKs: AnyPublisher { + get { + return createPublisher(self.actions) + .map { BookListViewModelActionKs($0) } + .eraseToAnyPublisher() + } + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/BookReviewView.swift b/sample-declarative-ui/iosApp/iosApp/BookReviewView.swift new file mode 100644 index 00000000..6c8a51c8 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/BookReviewView.swift @@ -0,0 +1,23 @@ +// +// BookReviewView.swift +// iosApp +// +// Created by Aleksey Mikhailov on 16.04.2022. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI + +struct BookReviewView: View { + let id: Int32 + + var body: some View { + Text("Hello, World \(id)!") + } +} + +struct BookReviewView_Previews: PreviewProvider { + static var previews: some View { + BookReviewView(id: 10) + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/ContentView.swift b/sample-declarative-ui/iosApp/iosApp/ContentView.swift new file mode 100644 index 00000000..3e642e26 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/ContentView.swift @@ -0,0 +1,46 @@ +import SwiftUI +import MultiPlatformLibrary +import Combine + +struct ContentView: View { + @State var isAuthorized: Bool = true + + var body: some View { + if isAuthorized { + MainView { + withAnimation(.linear) { + isAuthorized = false + } + } + } else { + LoginView { + withAnimation(.easeIn) { + isAuthorized = true + } + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} + +struct MainView: View { + let onUnauthorized: () -> Void + + var body: some View { + NavigationView { + BookListView() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Logout") { + self.onUnauthorized() + } + } + } + } + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/Info.plist b/sample-declarative-ui/iosApp/iosApp/Info.plist new file mode 100644 index 00000000..8044709c --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UILaunchScreen + + + \ No newline at end of file diff --git a/sample-declarative-ui/iosApp/iosApp/LoginView.swift b/sample-declarative-ui/iosApp/iosApp/LoginView.swift new file mode 100644 index 00000000..def15829 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/LoginView.swift @@ -0,0 +1,78 @@ +// +// LoginView.swift +// iosApp +// +// Created by Aleksey Mikhailov on 18.03.2022. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI + +struct LoginViewBody: View { + @Binding var login: String + @Binding var password: String + let isButtonEnabled: Bool + let isLoading: Bool + let onLoginPressed: () -> Void + + var body: some View { + VStack(spacing: 8.0) { + TextField("Login", text: $login) + .textFieldStyle(.roundedBorder) + .disabled(isLoading) + + TextField("Password", text: $password) + .textFieldStyle(.roundedBorder) + .disabled(isLoading) + + if isLoading { + ProgressView() + } else { + Button("Login") { + onLoginPressed() + }.disabled(!isButtonEnabled) + } + }.padding() + } +} + + +struct LoginView_Previews: PreviewProvider { + struct Preview: View { + var body: some View { + LoginViewBody( + login: State(initialValue: "").projectedValue, + password: State(initialValue: "").projectedValue, + isButtonEnabled: false, + isLoading: false, + onLoginPressed: {} + ) + LoginViewBody( + login: State(initialValue: "test").projectedValue, + password: State(initialValue: "pass").projectedValue, + isButtonEnabled: true, + isLoading: false, + onLoginPressed: {} + ) + LoginViewBody( + login: State(initialValue: "test").projectedValue, + password: State(initialValue: "pass").projectedValue, + isButtonEnabled: false, + isLoading: true, + onLoginPressed: {} + ) + } + } + + static var previews: some View { + Group { + Preview() + }.previewDisplayName("Light theme") + .preferredColorScheme(.light) + + Group { + Preview() + }.previewDisplayName("Dark theme") + .preferredColorScheme(.dark) + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/LoginViewBinding.swift b/sample-declarative-ui/iosApp/iosApp/LoginViewBinding.swift new file mode 100644 index 00000000..07ff8ba2 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/LoginViewBinding.swift @@ -0,0 +1,54 @@ +// +// LoginViewBinding.swift +// iosApp +// +// Created by Aleksey Mikhailov on 18.03.2022. +// Copyright © 2022 orgName. All rights reserved. +// + +import SwiftUI +import MultiPlatformLibrary +import mokoMvvmFlowSwiftUI +import Combine + +struct LoginView: View { + @ObservedObject var viewModel: LoginViewModel = LoginViewModel() + let onLoginSuccess: () -> Void + @State var alertShowed: Bool = false + @State var alertMessage: String = "" + + var body: some View { + LoginViewBody( + login: viewModel.binding(\.login), + password: viewModel.binding(\.password), + isButtonEnabled: viewModel.state(\.isLoginButtonEnabled), + isLoading: viewModel.state(\.isLoading), + onLoginPressed: { viewModel.onLoginPressed() } + ).onReceive(viewModel.actionsKs) { action in + switch(action) { + case .routeToSuccess: + onLoginSuccess() + case .showError(let data): + print(data) + } + }.alert( + isPresented: $alertShowed + ) { + Alert( + title: Text("Error"), + message: Text(alertMessage), + dismissButton: .default(Text("Close")) + ) + } + } +} + +extension LoginViewModel { + var actionsKs: AnyPublisher { + get { + return createPublisher(self.actions) + .map { LoginViewModelActionKs($0) } + .eraseToAnyPublisher() + } + } +} diff --git a/sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..4aa7c535 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/sample-declarative-ui/iosApp/iosApp/iOSApp.swift b/sample-declarative-ui/iosApp/iosApp/iOSApp.swift new file mode 100644 index 00000000..0648e860 --- /dev/null +++ b/sample-declarative-ui/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/sample-declarative-ui/settings.gradle.kts b/sample-declarative-ui/settings.gradle.kts new file mode 100644 index 00000000..2eed2f7d --- /dev/null +++ b/sample-declarative-ui/settings.gradle.kts @@ -0,0 +1,4 @@ +rootProject.name = "declarative-ui-sample" + +include(":androidApp") +include(":shared") diff --git a/sample-declarative-ui/shared/build.gradle.kts b/sample-declarative-ui/shared/build.gradle.kts new file mode 100644 index 00000000..41d4e524 --- /dev/null +++ b/sample-declarative-ui/shared/build.gradle.kts @@ -0,0 +1,128 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework + +plugins { + kotlin("multiplatform") + id("com.android.library") + id("dev.icerock.moko.kswift") +} + +val mokoResourcesVersion = "0.19.0" +val mokoMvvmVersion = "0.13.0" +val dependenciesList = listOf( + "dev.icerock.moko:mvvm-core:$mokoMvvmVersion", + "dev.icerock.moko:mvvm-flow:$mokoMvvmVersion", + "dev.icerock.moko:resources:$mokoResourcesVersion" +) + +kotlin { + android() + + val xcf = XCFramework("MultiPlatformLibrary") + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { target -> + target.binaries.framework { + baseName = "MultiPlatformLibrary" + + xcf.add(this) + + dependenciesList.forEach { export(it) } + } + } + + sourceSets { + val commonMain by getting { + dependencies { + dependenciesList.forEach { api(it) } + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0-native-mt") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val androidMain by getting { + dependencies { + api("dev.icerock.moko:mvvm-flow-compose:$mokoMvvmVersion") + api("dev.icerock.moko:resources-compose:$mokoResourcesVersion") + } + } + val androidTest by getting + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + } + val iosX64Test by getting + val iosArm64Test by getting + val iosSimulatorArm64Test by getting + val iosTest by creating { + dependsOn(commonTest) + iosX64Test.dependsOn(this) + iosArm64Test.dependsOn(this) + iosSimulatorArm64Test.dependsOn(this) + } + } +} + +android { + compileSdk = 32 + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdk = 21 + targetSdk = 32 + } +} + +afterEvaluate { + val xcodeDir = File(project.buildDir, "xcode") + + tasks.filterIsInstance() + .forEach { xcFrameworkTask -> + val syncName: String = xcFrameworkTask.name.replace("assemble", "sync") + val xcframeworkDir = + File(xcFrameworkTask.outputDir, xcFrameworkTask.buildType.getName()) + + tasks.create(syncName, Sync::class) { + this.group = "xcode" + + this.from(xcframeworkDir) + this.into(xcodeDir) + + this.dependsOn(xcFrameworkTask) + } + } + + tasks.filterIsInstance() + .forEach { xcFrameworkTask -> + val frameworkDir: File = xcFrameworkTask.frameworks.first().outputFile + val swiftGenDir = File(frameworkDir.parent, frameworkDir.nameWithoutExtension + "Swift") + val xcframeworkDir = + File(xcFrameworkTask.outputDir, xcFrameworkTask.buildType.getName()) + val targetDir = File(xcframeworkDir, swiftGenDir.name) + + @Suppress("ObjectLiteralToLambda") + xcFrameworkTask.doLast(object : Action { + override fun execute(t: Task) { + targetDir.mkdirs() + swiftGenDir.copyRecursively(targetDir, overwrite = true) + } + }) + } +} + +kswift { + install(dev.icerock.moko.kswift.plugin.feature.SealedToSwiftEnumFeature) + install(dev.icerock.moko.kswift.plugin.feature.PlatformExtensionFunctionsFeature) +} diff --git a/sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml b/sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..6680e71f --- /dev/null +++ b/sample-declarative-ui/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/BookListViewModel.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/BookListViewModel.kt new file mode 100644 index 00000000..7eb9f17b --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/BookListViewModel.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui + +import dev.icerock.moko.mvvm.flow.CFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.cFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import dev.icerock.moko.mvvm.sample.declarativeui.model.Advertisement +import dev.icerock.moko.mvvm.sample.declarativeui.model.Book +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.random.Random +import kotlin.time.Duration.Companion.seconds + +/** + * Sample ViewModel with state in single StateFlow (mapped to class) and actions by Flow + * (also mapped to class) + * + * Notes: + * - Flow and StateFlow lost generics on Swift side (because it's interfaces), so we should convert + * it to classes - it was done by cStateFlow and cFlow extensions. CFlow and CStateFlow it's + * generic classes that can be used from Swift without problems + * - Flow and StateFlow can be observed with Jetpack Compose out of box. For SwiftUI required + * multiple utils functions - binding/state on Swift + * - To use exhaust swift enum we should use moko-kswift plugin + */ +class BookListViewModel : ViewModel() { + + private val _state = MutableStateFlow(State.Loading) + val state: CStateFlow get() = _state.cStateFlow() + + private val _actions = Channel(Channel.BUFFERED) + val actions: CFlow get() = _actions.receiveAsFlow().cFlow() + + fun start() = apply { loadBooks() } + + fun onRetryPressed() { + loadBooks() + } + + private fun loadBooks() { + _state.value = State.Loading + viewModelScope.launch { + delay(3.seconds) + + val random: Int = abs(Random.nextInt() % 3) + _state.value = when (random) { + 0 -> State.Empty("items not found!".desc()) + 1 -> State.Error("error!".desc()) + 2 -> State.Success( + items = (Book.items.map { book -> + ListUnit.BookUnit( + id = "book-" + book.id, + title = book.title + ) { onBookPressed(book) } + } + Advertisement.items.map { advert -> + ListUnit.AdvertUnit( + id = "advert-" + advert.id, + text = advert.text + ) { onAdvertisementPressed(advert) } + }).shuffled() + ) + else -> throw IllegalStateException("invalid random $random") + } + } + } + + private fun onBookPressed(book: Book) { + _actions.trySend(Action.RouteToBookDetails(book.id)) + } + + private fun onAdvertisementPressed(advert: Advertisement) { + _actions.trySend(Action.OpenUrl(advert.url)) + } + + sealed interface State { + object Loading : State + data class Empty(val message: StringDesc) : State + data class Success(val items: List) : State + data class Error(val message: StringDesc) : State + } + + sealed interface ListUnit { + val id: String + + data class BookUnit( + override val id: String, + val title: String, + val onPressed: () -> Unit + ) : ListUnit + + data class AdvertUnit( + override val id: String, + val text: String, + val onPressed: () -> Unit + ) : ListUnit + } + + sealed interface Action { + data class RouteToBookDetails(val id: Int) : Action + data class OpenUrl(val url: String) : Action + } +} diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/BookReviewViewModel.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/BookReviewViewModel.kt new file mode 100644 index 00000000..c7bc1169 --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/BookReviewViewModel.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui + +import dev.icerock.moko.mvvm.flow.CFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.cFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +/** + * Sample ViewModel for screen with input forms implemented by state + * in single StateFlow (mapped to class) and actions by Flow (also mapped to class). + * User can change state by onRateChange and onMessageChange - ViewModel + * just like reducer in Redux will change state and send new version to UI. + * + * Notes: + * - Flow and StateFlow lost generics on Swift side (because it's interfaces), so we should convert + * it to classes - it was done by cStateFlow and cFlow extensions. CFlow and CStateFlow it's + * generic classes that can be used from Swift without problems + * - Flow and StateFlow can be observed with Jetpack Compose out of box. For SwiftUI required + * multiple utils functions - binding/state on Swift + * - To use exhaust swift enum we should use moko-kswift plugin + */ +class BookReviewViewModel( + private val bookId: Int +) : ViewModel() { + + private val _state = MutableStateFlow( + State.Idle(form = InputForm(rate = 0, message = "")) + ) + val state: CStateFlow get() = _state.cStateFlow() + + private val _actions = Channel(Channel.BUFFERED) + val actions: CFlow get() = _actions.receiveAsFlow().cFlow() + + fun onRateChange(rate: Int) { + val state: State.Idle = _state.value as? State.Idle ?: return + _state.value = state.copy(form = state.form.copy(rate = rate)) + } + + fun onMessageChange(message: String) { + val state: State.Idle = _state.value as? State.Idle ?: return + _state.value = state.copy(form = state.form.copy(message = message)) + } + + fun onSendPressed() { + val state: State.Idle = _state.value as? State.Idle ?: return + val form: InputForm = state.form + + _state.value = State.Loading(form) + viewModelScope.launch { + println("here i send some request with $bookId and $form") + delay(3.seconds) + + if (form.rate == 0) { + _state.value = State.Error(form, "invalid rate!".desc()) + } else { + _actions.send(Action.CloseScreen) + } + } + } + + fun onErrorClosed() { + val state: State.Error = _state.value as? State.Error ?: return + _state.value = State.Idle(state.form) + } + + sealed interface State { + data class Idle(override val form: InputForm) : State + data class Loading(override val form: InputForm) : State + data class Error(override val form: InputForm, val message: StringDesc) : State + + val form: InputForm + } + + sealed interface Action { + object CloseScreen : Action + } + + data class InputForm( + val rate: Int, + val message: String + ) +} diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/LoginViewModel.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/LoginViewModel.kt new file mode 100644 index 00000000..6462a115 --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/LoginViewModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui + +import dev.icerock.moko.mvvm.flow.CFlow +import dev.icerock.moko.mvvm.flow.CMutableStateFlow +import dev.icerock.moko.mvvm.flow.CStateFlow +import dev.icerock.moko.mvvm.flow.cFlow +import dev.icerock.moko.mvvm.flow.cMutableStateFlow +import dev.icerock.moko.mvvm.flow.cStateFlow +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.desc.desc +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +class LoginViewModel : ViewModel() { + + val login: CMutableStateFlow = MutableStateFlow("").cMutableStateFlow() + val password: CMutableStateFlow = MutableStateFlow("").cMutableStateFlow() + + private val _isLoading: MutableStateFlow = MutableStateFlow(false) + val isLoading: CStateFlow = _isLoading.cStateFlow() + + val isLoginButtonEnabled: CStateFlow = + combine(isLoading, login, password) { isLoading, login, password -> + isLoading.not() && login.isNotBlank() && password.isNotBlank() + }.stateIn(viewModelScope, SharingStarted.Lazily, false).cStateFlow() + + private val _actions = Channel(Channel.BUFFERED) + val actions: CFlow get() = _actions.receiveAsFlow().cFlow() + + fun onLoginPressed() { + _isLoading.value = true + viewModelScope.launch { + delay(1.seconds) + + if (login.value != "error") { + _actions.send(Action.RouteToSuccess) + } else { + _actions.send(Action.ShowError("some error!".desc())) + } + + _isLoading.value = false + } + } + + sealed interface Action { + object RouteToSuccess : Action + data class ShowError(val error: StringDesc) : Action + } +} diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/model/Advertisement.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/model/Advertisement.kt new file mode 100644 index 00000000..5968c748 --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/model/Advertisement.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.model + +data class Advertisement( + val id: Int, + val text: String, + val url: String +) { + companion object { + val items: List = List(5) { idx -> + Advertisement(id = idx, text = "Here ad $idx", url = "https://icerock.dev") + } + } +} diff --git a/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/model/Book.kt b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/model/Book.kt new file mode 100644 index 00000000..20a7c812 --- /dev/null +++ b/sample-declarative-ui/shared/src/commonMain/kotlin/dev/icerock/moko/mvvm/sample/declarativeui/model/Book.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.sample.declarativeui.model + +data class Book( + val id: Int, + val title: String, + val author: String +) { + companion object { + val items: List = List(20) { idx -> + Book(id = idx, title = "Book #$idx", author = "Author of book #$idx") + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b2d0cfa8..f201041e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,11 +16,15 @@ includeBuild("mvvm-build-logic") include(":mvvm-internal") include(":mvvm-core") +include(":mvvm-flow") +include(":mvvm-flow:apple") +include(":mvvm-flow-compose") include(":mvvm-livedata") include(":mvvm-livedata-material") include(":mvvm-livedata-resources") include(":mvvm-livedata-glide") include(":mvvm-livedata-swiperefresh") +include(":mvvm-livedata-compose") include(":mvvm-databinding") include(":mvvm-viewbinding") include(":mvvm-state")