Skip to content

Commit

Permalink
Add back handling for iOS as well as make iOS sample work for non-Pre…
Browse files Browse the repository at this point in the history
…view and View related items
  • Loading branch information
blakelee committed Jun 28, 2024
1 parent d979a91 commit f1826a1
Show file tree
Hide file tree
Showing 16 changed files with 357 additions and 11 deletions.
6 changes: 6 additions & 0 deletions samples/compose-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
This module is named "compose-samples" because the binary validation tool seems to refuse to look
at the `:workflow-ui:compose` module if this one is also named `compose`.


# iOS
1. To run the iOS target you need to be on a Mac and have Xcode installed.
2. Then install the Kotlin Multiplatform plugin for your intellij IDE
3. Finally go to run configurations and add a new iOS Application configuration and select the iOS
project file in ./iosApp

[To enable iOS debugging](https://appkickstarter.com/blog/debug-an-ios-kotlin-multiplatform-app-from-android-studio/) open Settings -> Advanced Settings -> Enable experimental Multiplatform IDE features

[BackHandler implementation for iOS here](https://exyte.com/blog/jetpack-compose-multiplatform)
17 changes: 13 additions & 4 deletions samples/compose-samples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ kotlin {
jsMain.get().dependsOn(this)
jvmMain.get().dependsOn(this)
}

// Currently just used to make a noop BackHandler
val nonMobileMain by creating {
appleMain.get().dependsOn(this)
jsMain.get().dependsOn(this)
jvmMain.get().dependsOn(this)
dependsOn(commonMain.get())
}
}
}

Expand All @@ -103,10 +111,6 @@ kotlin {
* the language version here fixes the issue.
* TODO: Figure out how to disable the release flag for the sample app
*/
// java {
// toolchain.languageVersion.set(null as? JavaLanguageVersion)
// }

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "9"
}
Expand All @@ -119,12 +123,17 @@ android {

buildFeatures {
viewBinding = true
// This is needed for the layout inspector to work in Kotlin 2.0.0 even though we need it for
// the current Kotlin version we should leave this here when we upgrade to 2.0.0
compose = true
}

defaultConfig {
applicationId = "com.squareup.sample.compose"
}

// In Kotlin 2.0.0 this isn't strictly necessary but it is necessary for the layout inspector to
// work in Kotlin 2.0.0
composeOptions.kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
namespace = "com.squareup.sample.compose"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; };
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; };
7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; };
86B843592C2F435300048ED6 /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86B843582C2F435300048ED6 /* ContainerViewController.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -20,6 +21,7 @@
7555FF7B242A565900829871 /* Workflow Compose Samples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Workflow Compose Samples.app"; sourceTree = BUILT_PRODUCTS_DIR; };
7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
86B843582C2F435300048ED6 /* ContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = "<group>"; };
AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -72,6 +74,7 @@
children = (
058557BA273AAA24004C7B11 /* Assets.xcassets */,
7555FF82242A565900829871 /* ContentView.swift */,
86B843582C2F435300048ED6 /* ContainerViewController.swift */,
7555FF8C242A565B00829871 /* Info.plist */,
2152FB032600AC8F00CF470E /* iOSApp.swift */,
058557D7273AAEEB004C7B11 /* Preview Content */,
Expand Down Expand Up @@ -185,6 +188,7 @@
buildActionMask = 2147483647;
files = (
2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */,
86B843592C2F435300048ED6 /* ContainerViewController.swift in Sources */,
7555FF83242A565900829871 /* ContentView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

class ContainerViewController: UIViewController {
private let onTouchDown: (CGPoint) -> Void

init(child: UIViewController, onTouchDown: @escaping (CGPoint) -> Void) {
self.onTouchDown = onTouchDown
super.init(nibName: nil, bundle: nil)
addChild(child)
child.view.frame = view.frame
view.addSubview(child.view)
child.didMove(toParent: self)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let startPoint = touches.first?.location(in: nil) {
onTouchDown(startPoint)
}
}
}
52 changes: 48 additions & 4 deletions samples/compose-samples/iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,60 @@ import SwiftUI
import ComposeApp

struct ComposeView: UIViewControllerRepresentable {
var onSwipe: () -> Void

func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController()
let mainViewController = MainViewControllerKt.MainViewController()

let containerController = ContainerViewController(child: mainViewController) {
context.coordinator.startPoint = $0
}

let swipeGestureRecognizer = UISwipeGestureRecognizer(
target:
context.coordinator, action: #selector(Coordinator.handleSwipe)
)
swipeGestureRecognizer.direction = .right
swipeGestureRecognizer.numberOfTouchesRequired = 1
containerController.view.addGestureRecognizer(swipeGestureRecognizer)
return containerController
}

func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator(onSwipe: onSwipe)
}

class Coordinator: NSObject, UIGestureRecognizerDelegate {
var onSwipe: () -> Void
var startPoint: CGPoint?

init(onSwipe: @escaping () -> Void) {
self.onSwipe = onSwipe
}

@objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
if gesture.state == .ended, let startPoint = startPoint, startPoint.x < 50 {
onSwipe()
}
}

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
true
}
}
}

struct ContentView: View {
var body: some View {
ComposeView()
.ignoresSafeArea(.keyboard) // Compose has own keyboard handler
ComposeView {
onBackGesture()
}
.ignoresSafeArea(.keyboard)
}
}

public func onBackGesture() {
MainViewControllerKt.onBackGesture()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.squareup.sample.compose

import androidx.compose.runtime.Composable
import com.squareup.workflow1.RuntimeConfig
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
Expand All @@ -14,3 +15,7 @@ actual fun defaultRuntimeConfig(): RuntimeConfig =
@OptIn(WorkflowUiExperimentalApi::class)
actual fun defaultViewEnvironment(): ViewEnvironment =
ViewEnvironment.EMPTY.withComposeInteropSupport()

@Composable
actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) =
androidx.activity.compose.BackHandler(isEnabled, onBack)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.squareup.sample.compose

import androidx.compose.runtime.Composable
import com.squareup.workflow1.RuntimeConfig
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
Expand All @@ -8,3 +9,6 @@ expect fun defaultRuntimeConfig(): RuntimeConfig

@OptIn(WorkflowUiExperimentalApi::class)
expect fun defaultViewEnvironment(): ViewEnvironment

@Composable
expect fun BackHandler(isEnabled: Boolean = true, onBack: () -> Unit)
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,3 @@ private val viewEnvironment = defaultViewEnvironment()
)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.squareup.sample.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import store

@Composable
actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) {
LaunchedEffect(isEnabled) {
if (isEnabled) {
store.events.collect {
onBack()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.window.ComposeUIViewController
import com.squareup.sample.compose.defaultViewEnvironment
import com.squareup.sample.compose.launcher.SampleWorkflow
import com.squareup.sample.compose.states.Action
import com.squareup.sample.compose.states.Store
import com.squareup.sample.compose.states.createStore
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.renderAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.withContext

fun MainViewController() = ComposeUIViewController { App() }

val store: Store = CoroutineScope(SupervisorJob()).createStore()

@OptIn(WorkflowUiExperimentalApi::class)
@Composable
fun App() {
BasicText("Hello World")
MaterialTheme {
val rendering by SampleWorkflow.renderAsState(Unit) {}

WorkflowRendering(
rendering,
defaultViewEnvironment()
)
}
}

fun onBackGesture() {
store.send(Action.OnBackPressed)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.squareup.sample.compose.inlinerendering

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.squareup.sample.compose.defaultViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.compose.renderAsState

@OptIn(WorkflowUiExperimentalApi::class)
@Composable
fun InlineRenderingApp() {
val rendering by InlineRenderingWorkflow.renderAsState(Unit) {}
WorkflowRendering(rendering, defaultViewEnvironment())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@file:OptIn(WorkflowUiExperimentalApi::class)

package com.squareup.sample.compose.launcher

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.ComposeScreen

data class SampleScreen(val onSampleClicked: (Sample) -> Unit) : ComposeScreen {
@Composable
override fun Content(viewEnvironment: ViewEnvironment) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize().padding(16.dp)
) {
items(samples) { sample ->
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { onSampleClicked(sample) }
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(sample.name, style = MaterialTheme.typography.h6)
Text(sample.description, style = MaterialTheme.typography.body1)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@file:OptIn(WorkflowUiExperimentalApi::class)

package com.squareup.sample.compose.launcher

import androidx.compose.runtime.Composable
import com.squareup.sample.compose.BackHandler
import com.squareup.sample.compose.hellocomposebinding.HelloWorkflow
import com.squareup.sample.compose.hellocomposebinding.viewEnvironment
import com.squareup.sample.compose.hellocomposeworkflow.HelloComposeWorkflow
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.Workflow
import com.squareup.workflow1.WorkflowAction.Companion.noAction
import com.squareup.workflow1.mapRendering
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.ViewEnvironment
import com.squareup.workflow1.ui.WorkflowUiExperimentalApi
import com.squareup.workflow1.ui.compose.ComposeScreen
import com.squareup.workflow1.ui.compose.WorkflowRendering
import com.squareup.workflow1.ui.withEnvironment

object SampleWorkflow : StatefulWorkflow<Unit, Sample?, Nothing, Screen>() {
override fun render(
renderProps: Unit,
renderState: Sample?,
context: RenderContext
): Screen {
val screen = when (val workflow = renderState?.workflow) {
null -> SampleScreen(context.eventHandler { sample -> state = sample })
is HelloComposeWorkflow -> context.renderChild(
child = workflow,
props = "Hello",
) { noAction() }

is HelloWorkflow -> context.renderChild(
child = workflow.mapRendering { it.withEnvironment(viewEnvironment) }
) { noAction() }

else -> context.renderChild(child = workflow as Workflow<Unit, Nothing, Screen>)
}

return BackHandlerScreen(screen, onBack = context.eventHandler { state = null })
}

override fun initialState(
props: Unit,
snapshot: Snapshot?
) = null

override fun snapshotState(state: Sample?): Snapshot? = null
}

data class BackHandlerScreen(val screen: Screen, val onBack: () -> Unit) : ComposeScreen {
@Composable
override fun Content(viewEnvironment: ViewEnvironment) {
BackHandler(onBack = onBack)
WorkflowRendering(screen, viewEnvironment)
}
}
Loading

0 comments on commit f1826a1

Please sign in to comment.