Browse Source

Compose inside SwiftUI example (#2755)

pull/2786/head
dima.avdeev 1 year ago committed by GitHub
parent
commit
a1cb760b7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      experimental/examples/chat-mpp/README.md
  2. 2
      experimental/examples/chat-mpp/gradle.properties
  3. 28
      experimental/examples/chat-mpp/iosApp/iosApp.xcodeproj/project.pbxproj
  4. 50
      experimental/examples/chat-mpp/iosApp/iosApp/ComposeInsideSwiftUIScreen.swift
  5. 12
      experimental/examples/chat-mpp/iosApp/iosApp/ComposeViewControllerToSwiftUI.swift
  6. 27
      experimental/examples/chat-mpp/iosApp/iosApp/GradientTemplate.swift
  7. 25
      experimental/examples/chat-mpp/iosApp/iosApp/KotlinToSwiftHelper.swift
  8. 11
      experimental/examples/chat-mpp/iosApp/iosApp/YetAnotherSwiftUIScreen.swift
  9. 31
      experimental/examples/chat-mpp/iosApp/iosApp/iosApp.swift
  10. 2
      experimental/examples/chat-mpp/shared/src/androidMain/kotlin/main.android.kt
  11. 60
      experimental/examples/chat-mpp/shared/src/commonMain/kotlin/ChatApp.kt
  12. 8
      experimental/examples/chat-mpp/shared/src/commonMain/kotlin/Colors.kt
  13. 54
      experimental/examples/chat-mpp/shared/src/commonMain/kotlin/Messages.kt
  14. 4
      experimental/examples/chat-mpp/shared/src/desktopMain/kotlin/main.desktop.kt
  15. 11
      experimental/examples/chat-mpp/shared/src/iosMain/kotlin/main.ios.kt
  16. 2
      experimental/examples/chat-mpp/shared/src/jsMain/kotlin/main.js.kt
  17. 2
      experimental/examples/chat-mpp/shared/src/macosMain/kotlin/main.macos.kt

9
experimental/examples/chat-mpp/README.md

@ -25,3 +25,12 @@ Then choose **iosApp** configuration in IDE and run it.
## Run JS in browser with WebAssembly Skia via Gradle
`./gradlew jsApp:jsBrowserDevelopmentRun`
### Run MacOS via Gradle:
- on Intel CPU: `./gradlew :shared:runDebugExecutableMacosX64`
- on Apple Silicon: `./gradlew :shared:runDebugExecutableMacosArm64`
## SwiftUI interop
This example shows how you can set up an interop between SwiftUI and Compose.
Pay attention to the file [ComposeViewControllerToSwiftUI.swift](iosApp%2FiosApp%2FComposeViewControllerToSwiftUI.swift).
This file help to add Compose inside SwiftUI hierarchy.

2
experimental/examples/chat-mpp/gradle.properties

@ -14,4 +14,4 @@ kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.binary.memoryModel=experimental
kotlin.version=1.8.0
agp.version=7.1.3
compose.version=1.3.0
compose.version=1.4.0-alpha01-dev940

28
experimental/examples/chat-mpp/iosApp/iosApp.xcodeproj/project.pbxproj

@ -7,11 +7,21 @@
objects = {
/* Begin PBXBuildFile section */
184880BA0E9910C2B5012412 /* KotlinToSwiftHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18488D89B8500CD7696A04EB /* KotlinToSwiftHelper.swift */; };
1848810122D49D4AD3668D91 /* ComposeViewControllerToSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18488656503C85EEDA66341D /* ComposeViewControllerToSwiftUI.swift */; };
184881FBE98BA5BF02A0A186 /* GradientTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1848892FA748717E4087E7D7 /* GradientTemplate.swift */; };
1848823F43E447F9A8B4AC7C /* YetAnotherSwiftUIScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18488451271FAAD8FA34A2FB /* YetAnotherSwiftUIScreen.swift */; };
184882D8AF2A7A3642004010 /* ComposeInsideSwiftUIScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18488CAB0978B80826E5BBB4 /* ComposeInsideSwiftUIScreen.swift */; };
2152FB042600AC8F00CF470E /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iosApp.swift */; };
C1FC908188C4E8695729CB06 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DE96E47030356CE6AD9794A /* Pods_iosApp.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
18488451271FAAD8FA34A2FB /* YetAnotherSwiftUIScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YetAnotherSwiftUIScreen.swift; sourceTree = "<group>"; };
18488656503C85EEDA66341D /* ComposeViewControllerToSwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewControllerToSwiftUI.swift; sourceTree = "<group>"; };
1848892FA748717E4087E7D7 /* GradientTemplate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GradientTemplate.swift; sourceTree = "<group>"; };
18488CAB0978B80826E5BBB4 /* ComposeInsideSwiftUIScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeInsideSwiftUIScreen.swift; sourceTree = "<group>"; };
18488D89B8500CD7696A04EB /* KotlinToSwiftHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KotlinToSwiftHelper.swift; sourceTree = "<group>"; };
1EB65E27D2C0F884D0A1A133 /* 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 = "<group>"; };
2152FB032600AC8F00CF470E /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = "<group>"; };
3D7A606AB0AD7636269BD9D0 /* 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 = "<group>"; };
@ -57,6 +67,11 @@
children = (
7555FF8C242A565B00829871 /* Info.plist */,
2152FB032600AC8F00CF470E /* iosApp.swift */,
18488656503C85EEDA66341D /* ComposeViewControllerToSwiftUI.swift */,
18488D89B8500CD7696A04EB /* KotlinToSwiftHelper.swift */,
18488CAB0978B80826E5BBB4 /* ComposeInsideSwiftUIScreen.swift */,
18488451271FAAD8FA34A2FB /* YetAnotherSwiftUIScreen.swift */,
1848892FA748717E4087E7D7 /* GradientTemplate.swift */,
);
path = iosApp;
sourceTree = "<group>";
@ -181,6 +196,11 @@
buildActionMask = 2147483647;
files = (
2152FB042600AC8F00CF470E /* iosApp.swift in Sources */,
1848810122D49D4AD3668D91 /* ComposeViewControllerToSwiftUI.swift in Sources */,
184880BA0E9910C2B5012412 /* KotlinToSwiftHelper.swift in Sources */,
184882D8AF2A7A3642004010 /* ComposeInsideSwiftUIScreen.swift in Sources */,
1848823F43E447F9A8B4AC7C /* YetAnotherSwiftUIScreen.swift in Sources */,
184881FBE98BA5BF02A0A186 /* GradientTemplate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -316,13 +336,13 @@
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iosApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Chat${TEAM_ID}";
PRODUCT_NAME = "Chat";
PRODUCT_NAME = Chat;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@ -340,13 +360,13 @@
DEVELOPMENT_TEAM = "${TEAM_ID}";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = iosApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.jetbrains.Chat${TEAM_ID}";
PRODUCT_NAME = "Chat";
PRODUCT_NAME = Chat;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

50
experimental/examples/chat-mpp/iosApp/iosApp/ComposeInsideSwiftUIScreen.swift

@ -0,0 +1,50 @@
import SwiftUI
struct ComposeInsideSwiftUIScreen: View {
var body: some View {
ZStack {
ComposeLayer()
TextInputLayer()
}.onTapGesture {
// Hide keyboard on tap outside of TextField
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
}
struct ComposeLayer: View {
var body: some View {
GradientTemplate(title: "Compose inside SwiftUI") {
ComposeViewControllerToSwiftUI()
.ignoresSafeArea(.keyboard) // Compose have own keyboard handler
}
}
}
struct TextInputLayer: View {
@State private var textState: String = "text message"
@FocusState private var textFieldFocused: Bool
var body: some View {
VStack {
Spacer()
HStack {
TextField("Type message...", text: $textState, axis: .vertical)
.focused($textFieldFocused)
.lineLimit(3)
if (!textState.isEmpty) {
Button(action: {
sendMessage(textState)
textFieldFocused = false
textState = ""
}) {
HStack {
Image(systemName: "play.fill")
Text("Send")
}.tint(.white)
}
}
}.padding(10).background(RoundedRectangle(cornerRadius: 10).fill(gradient).opacity(0.8)).padding(6)
}
}
}

12
experimental/examples/chat-mpp/iosApp/iosApp/ComposeViewControllerToSwiftUI.swift

@ -0,0 +1,12 @@
import UIKit
import SwiftUI
import shared
struct ComposeViewControllerToSwiftUI: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return Main_iosKt.ChapViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}

27
experimental/examples/chat-mpp/iosApp/iosApp/GradientTemplate.swift

@ -0,0 +1,27 @@
import SwiftUI
struct GradientTemplate<Content: View>: View {
var title: String
var content: () -> Content
var body: some View {
NavigationView {
ZStack {
surfaceColor()
VStack {
gradient.ignoresSafeArea(edges: .top).frame(height: 0)
Spacer()
}
content()
VStack {
Spacer()
Rectangle().frame(height: 0).background(gradient)
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.statusBar(hidden: false)
}
.toolbar(.visible, for: .tabBar)
}
}

25
experimental/examples/chat-mpp/iosApp/iosApp/KotlinToSwiftHelper.swift

@ -0,0 +1,25 @@
import UIKit
import SwiftUI
import shared
public func sendMessage(_ text: String) {
Main_iosKt.sendMessage(text: text)
}
public func gradient3Colors() -> [Color] {
return Main_iosKt.gradient3Colors().map { hex in
Color(getCGColor(hex.intValue)).opacity(1.0)
}
}
public func surfaceColor() -> Color {
Color(getCGColor(Int(Main_iosKt.surfaceColor())))
}
private func getCGColor(_ argb: Int) -> CGColor {
func clr(_ component: Int) -> CGFloat {
CGFloat(component & 0xff) / 255.0
}
return CGColor(red: clr(argb >> 16), green: clr(argb >> 8), blue: clr(argb), alpha: clr(argb >> 24))
}

11
experimental/examples/chat-mpp/iosApp/iosApp/YetAnotherSwiftUIScreen.swift

@ -0,0 +1,11 @@
import SwiftUI
struct YetAnotherSwiftUIScreen: View {
var body: some View {
GradientTemplate(title: "SwiftUI") {
VStack {
Text("Yet another SwiftUI screen")
}
}
}
}

31
experimental/examples/chat-mpp/iosApp/iosApp/iosApp.swift

@ -1,15 +1,22 @@
import UIKit
import shared
import SwiftUI
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let gradient = LinearGradient(
colors: gradient3Colors(),
startPoint: .topLeading, endPoint: .bottomTrailing
)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
let mainViewController = Main_iosKt.MainViewController()
window?.rootViewController = mainViewController
window?.makeKeyAndVisible()
return true
}
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
TabView {
ComposeInsideSwiftUIScreen()
.tabItem { Label("Compose", systemImage: "square.and.pencil") }
YetAnotherSwiftUIScreen()
.tabItem { Label("SwiftUI", systemImage: "list.dash") }
}.accentColor(.white).preferredColorScheme(.dark)
}
}
}

2
experimental/examples/chat-mpp/shared/src/androidMain/kotlin/main.android.kt

@ -1,4 +1,4 @@
import androidx.compose.runtime.Composable
@Composable
fun MainView() = ChatApp()
fun MainView() = ChatAppWithScaffold()

60
experimental/examples/chat-mpp/shared/src/commonMain/kotlin/ChatApp.kt

@ -3,6 +3,9 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
val myUser = User("Me")
@ -12,34 +15,43 @@ val friendMessages = listOf(
"Nice to see you!",
"Multiline\ntext\nmessage"
)
val store = CoroutineScope(SupervisorJob()).createStore()
@Composable
internal fun ChatApp() {
val coroutineScope = rememberCoroutineScope()
val store = remember { coroutineScope.createStore() }
val state by store.stateFlow.collectAsState()
internal fun ChatAppWithScaffold(displayTextField: Boolean = true) {
Theme {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Chat sample") },
backgroundColor = MaterialTheme.colors.background,
)
}) {
ChatApp(displayTextField = displayTextField)
}
}
}
MaterialTheme {
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Chat sample") }
)
}
) {
@Composable
internal fun ChatApp(displayTextField: Boolean = true) {
val state by store.stateFlow.collectAsState()
Theme {
Surface {
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize()
) {
Box(Modifier.weight(1f)) {
Messages(state.messages)
}
SendMessage { text ->
store.send(
Action.SendMessage(
Message(myUser, timeMs = timestampMs(), text)
if (displayTextField) {
SendMessage { text ->
store.send(
Action.SendMessage(
Message(myUser, timeMs = timestampMs(), text)
)
)
)
}
}
}
}
@ -60,3 +72,15 @@ internal fun ChatApp() {
}
}
}
@Composable
internal fun Theme(content: @Composable () -> Unit) {
MaterialTheme(
colors = darkColors(
surface = Color(ChatColors.SURFACE),
background = Color(ChatColors.BACKGROUND),
),
) {
content()
}
}

8
experimental/examples/chat-mpp/shared/src/commonMain/kotlin/Colors.kt

@ -0,0 +1,8 @@
object ChatColors {
val GRADIENT_3 = listOf(0xFF7F52FF, 0xFFC811E2, 0xFFE54857)
val GRADIENT_2 = listOf(0xFFC811E2, 0xFF7F52FF)
val PRIMARY = 0xFFAA77EE
val SURFACE = 0xFFCC99FF
val BACKGROUND = 0xFF663388
}

54
experimental/examples/chat-mpp/shared/src/commonMain/kotlin/Messages.kt

@ -3,7 +3,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
@Composable
@ -43,27 +44,31 @@ internal inline fun Messages(messages: List<Message>) {
ChatMessage(isMyMessage = message.user == myUser, message)
}
}
// items(messages, key = { it.id }) { message -> //TODO not working in JS
// ChatMessage(isMyMessage = message.user == myUser, message)
// }
item {
Box(Modifier.height(50.dp))
}
}
}
@Composable
private inline fun ChatMessage(isMyMessage: Boolean, message: Message) {
Box(modifier = Modifier.fillMaxWidth()) {
val focusManager = LocalFocusManager.current
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = if (isMyMessage) Alignment.CenterEnd else Alignment.CenterStart
) {
Surface(
modifier = Modifier.padding(4.dp)
.align(if (isMyMessage) Alignment.CenterStart else Alignment.CenterEnd),
modifier = Modifier.padding(4.dp),
shape = RoundedCornerShape(size = 20.dp),
elevation = 8.dp
) {
Box(
Modifier.background(brush = Brush.horizontalGradient(listOf(Color(0xff8888ff), Color(0xffddddff))))
.padding(10.dp),
Modifier.background(brush = Brush.horizontalGradient(
ChatColors.GRADIENT_2.map { Color(it) })
).padding(10.dp),
) {
Row(verticalAlignment = Alignment.Top) {
if (isMyMessage) {
if (!isMyMessage) {
UserPic(message.user)
Spacer(Modifier.size(8.dp))
}
@ -83,25 +88,26 @@ private inline fun ChatMessage(isMyMessage: Boolean, message: Message) {
text = message.text
)
}
if (!isMyMessage) {
if (isMyMessage) {
Spacer(Modifier.size(8.dp))
UserPic(message.user)
}
}
}
}
if (!isMyMessage) {
var liked by remember { mutableStateOf(false) }
Icon(
modifier = Modifier.align(Alignment.BottomEnd)
.clickable {
liked = !liked
}
.padding(4.dp),
imageVector = if (liked) Icons.Filled.Favorite else Icons.Outlined.Favorite,
contentDescription = "Like",
tint = if (liked) Color.Red else Color.Gray
)
if (!isMyMessage) {
var liked by remember { mutableStateOf(false) }
Icon(
modifier = Modifier.align(Alignment.BottomEnd)
.clickable {
liked = !liked
focusManager.clearFocus(true)
}
.padding(3.dp),
imageVector = if (liked) Icons.Filled.Favorite else Icons.Outlined.Favorite,
contentDescription = "Like",
tint = if (liked) Color.Red else Color.Gray
)
}
}
}
}

4
experimental/examples/chat-mpp/shared/src/desktopMain/kotlin/main.desktop.kt

@ -2,10 +2,10 @@ import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.runtime.Composable
@Composable
fun MainView() = ChatApp()
fun MainView() = ChatAppWithScaffold()
@Preview
@Composable
fun ChatPreview() {
ChatApp()
MainView()
}

11
experimental/examples/chat-mpp/shared/src/iosMain/kotlin/main.ios.kt

@ -1,8 +1,15 @@
import androidx.compose.ui.window.Application
import platform.UIKit.UIViewController
fun MainViewController(): UIViewController =
fun ChapViewController(): UIViewController =
Application("Chat") {
ChatApp()
ChatApp(displayTextField = false)
}
fun sendMessage(text: String) {
store.send(Action.SendMessage(Message(myUser, timestampMs(), text)))
}
fun gradient3Colors() = ChatColors.GRADIENT_3
fun surfaceColor() = ChatColors.SURFACE

2
experimental/examples/chat-mpp/shared/src/jsMain/kotlin/main.js.kt

@ -1,6 +1,6 @@
import androidx.compose.runtime.Composable
@Composable
fun MainView() = ChatApp()
fun MainView() = ChatAppWithScaffold()

2
experimental/examples/chat-mpp/shared/src/macosMain/kotlin/main.macos.kt

@ -5,7 +5,7 @@ import platform.AppKit.NSApplication
fun main() {
NSApplication.sharedApplication()
Window("Chat App") {
ChatApp()
ChatAppWithScaffold()
}
NSApp?.run()
}

Loading…
Cancel
Save