From fb492539f61db3b3907687142afad37f5648845c Mon Sep 17 00:00:00 2001 From: Abe White Date: Wed, 4 Oct 2023 13:10:10 -0500 Subject: [PATCH] Update ComposeView content and Compose function to return a value. See comments in ComposeView for explanation --- README.md | 7 ++- .../SkipUI/Compose/ComposeContainer.swift | 1 - .../SkipUI/Compose/ComposeContext.swift | 37 ++++++------ .../SkipUI/SkipUI/Compose/ComposeView.swift | 19 ++++-- Sources/SkipUI/SkipUI/Containers/Group.swift | 5 +- Sources/SkipUI/SkipUI/Containers/List.swift | 2 +- .../SkipUI/SkipUI/Containers/Navigation.swift | 2 +- .../SkipUI/SkipUI/Containers/TabView.swift | 4 +- Sources/SkipUI/SkipUI/Containers/VStack.swift | 58 +++++++++---------- Sources/SkipUI/SkipUI/Text/Label.swift | 8 +-- Sources/SkipUI/SkipUI/View/View.swift | 7 ++- Tests/SkipUITests/CanvasTests.swift | 2 + 12 files changed, 83 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 28fc7e4a..1f7c22dd 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ class V: View { } else { Text("Goodbye!").Compose(context: composectx) } + ComposeResult.ok } } @@ -188,6 +189,7 @@ VStack { Text("Hello from SwiftUI") ComposeView { _ in androidx.compose.material3.Text("Hello from Compose") + return .ok } } ``` @@ -196,10 +198,11 @@ Skip also enhances all SwiftUI views with a `Compose()` method, allowing you to ```swift ComposeView { context in - androidx.compose.foundation.layout.Column { + androidx.compose.foundation.layout.Column(modifier: context.modifier) { Text("Hello from SwiftUI").Compose(context: context.content()) androidx.compose.material3.Text("Hello from Compose") } + return .ok } ``` @@ -210,7 +213,7 @@ ComposeView { context in VStack { Text("Hello from SwiftUI").Compose(context: context.content()) androidx.compose.material3.Text("Hello from Compose") - }.Compose(context: context.content()) + }.Compose(context: context) // Returns .ok } ``` diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift b/Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift index f175c3ba..9b54bf0b 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift @@ -73,5 +73,4 @@ import androidx.compose.ui.Modifier content(modifier) } } - #endif diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeContext.swift b/Sources/SkipUI/SkipUI/Compose/ComposeContext.swift index b4d6062d..c88d486f 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeContext.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeContext.swift @@ -30,12 +30,20 @@ public struct ComposeContext { } } +/// The result of composing content. +/// +/// Reserved for future use. Having a return value also expands recomposition scope. See `ComposeView` for details. +public struct ComposeResult { + public static let ok = ComposeResult() +} + /// Mechanism for a parent view to change how a child view is composed. public protocol Composer { /// Called before a `ComposeView` composes its content. - /// - /// Reset state gathered during sibling view composition. - public func reset() + public func willCompose() + + /// Called after a `ComposeView` composes its content. + public func didCompose(result: ComposeResult) /// Compose the given view. /// @@ -44,7 +52,10 @@ public protocol Composer { } extension Composer { - public func reset() { + public func willCompose() { + } + + public func didCompose(result: ComposeResult) { } } @@ -52,24 +63,14 @@ extension Composer { /// /// - Warning: Child composables may recompose at any time. Be careful with relying on block capture. struct ClosureComposer: Composer { - private let resetClosure: () -> Void - private let composeClosure: @Composable (inout View, (Bool) -> ComposeContext) -> Void - - init(reset: () -> Void, compose: @Composable (inout View, (Bool) -> ComposeContext) -> Void) { - self.resetClosure = reset - self.composeClosure = compose - } - - convenience init(compose: @Composable (inout View, (Bool) -> ComposeContext) -> Void) { - self.init(reset: {}, compose: compose) - } + private let compose: @Composable (inout View, (Bool) -> ComposeContext) -> Void - override func reset() { - resetClosure() + init(compose: @Composable (inout View, (Bool) -> ComposeContext) -> Void) { + self.compose = compose } @Composable override func Compose(view: inout View, context: (Bool) -> ComposeContext) { - composeClosure(&view, context) + compose(&view, context) } } diff --git a/Sources/SkipUI/SkipUI/Compose/ComposeView.swift b/Sources/SkipUI/SkipUI/Compose/ComposeView.swift index 6d4de3ca..5c3a5413 100644 --- a/Sources/SkipUI/SkipUI/Compose/ComposeView.swift +++ b/Sources/SkipUI/SkipUI/Compose/ComposeView.swift @@ -9,19 +9,28 @@ import androidx.compose.runtime.Composable /// /// Used to wrap the content of SwiftUI `@ViewBuilders`, and may be used manually to embed raw Compose code. public struct ComposeView: View { - private let content: @Composable (ComposeContext) -> Void + private let content: @Composable (ComposeContext) -> ComposeResult - public init(content: @Composable (ComposeContext) -> Void) { + /// Constructor + /// + /// The supplied `content` is the content to compose. When transpiling SwiftUI code, this is the logic embedded in the user's `body` and within each container view in + /// that `body`, as well as within other `@ViewBuilders`. + /// + /// - Note: Returning a result from `content` is important. This prevents Compose from recomposing `content` on its own. Instead, a change that would recompose + /// `content` elevates to our void `ComposeContent` function. This allows us to prepare for recompositions, e.g. making the proper callbacks to the context's `composer`. + public init(content: @Composable (ComposeContext) -> ComposeResult) { self.content = content } - @Composable public override func Compose(context: ComposeContext) { - context.composer?.reset() + @Composable public override func Compose(context: ComposeContext) -> ComposeResult { ComposeContent(context) + return .ok } @Composable public override func ComposeContent(context: ComposeContext) { - content(context) + context.composer?.willCompose() + let result = content(context) + context.composer?.didCompose(result: result) } } #endif diff --git a/Sources/SkipUI/SkipUI/Containers/Group.swift b/Sources/SkipUI/SkipUI/Containers/Group.swift index ad6686d6..1e7a7f44 100644 --- a/Sources/SkipUI/SkipUI/Containers/Group.swift +++ b/Sources/SkipUI/SkipUI/Containers/Group.swift @@ -15,11 +15,12 @@ public struct Group : View where Content : View { #if SKIP @Composable public override func ComposeContent(context: ComposeContext) { - content.Compose(context: context) + let _ = content.Compose(context: context) } - @Composable public override func Compose(context: ComposeContext) { + @Composable public override func Compose(context: ComposeContext) -> ComposeResult { ComposeContent(context: context) + return .ok } #else public var body: some View { diff --git a/Sources/SkipUI/SkipUI/Containers/List.swift b/Sources/SkipUI/SkipUI/Containers/List.swift index acb5ad72..b39d5bf7 100644 --- a/Sources/SkipUI/SkipUI/Containers/List.swift +++ b/Sources/SkipUI/SkipUI/Containers/List.swift @@ -114,7 +114,7 @@ public struct List : View where SelectionValue: Hashabl private static let verticalInset = 32.0 private static let minimumItemHeight = 44.0 private static let horizontalItemInset = 16.0 - private static let verticalItemInset = 4.0 + private static let verticalItemInset = 6.0 @Composable private func ComposeItem(view: inout View, context: ComposeContext, style: ListStyle) { let contentModifier = Modifier.padding(horizontal: Self.horizontalItemInset.dp, vertical: Self.verticalItemInset.dp).fillWidth().requiredHeightIn(min: Self.minimumItemHeight.dp) diff --git a/Sources/SkipUI/SkipUI/Containers/Navigation.swift b/Sources/SkipUI/SkipUI/Containers/Navigation.swift index ee871cce..0425d6e5 100644 --- a/Sources/SkipUI/SkipUI/Containers/Navigation.swift +++ b/Sources/SkipUI/SkipUI/Containers/Navigation.swift @@ -408,7 +408,7 @@ public struct NavigationLink : View, ListItemAdapting { #if SKIP @Composable public override func ComposeContent(context: ComposeContext) { - label.Compose(context: context.content(modifier: NavigationModifier(context.modifier))) + let _ = label.Compose(context: context.content(modifier: NavigationModifier(context.modifier))) } @Composable func shouldComposeListItem() -> Bool { diff --git a/Sources/SkipUI/SkipUI/Containers/TabView.swift b/Sources/SkipUI/SkipUI/Containers/TabView.swift index f3f9833d..3cf9d4c5 100644 --- a/Sources/SkipUI/SkipUI/Containers/TabView.swift +++ b/Sources/SkipUI/SkipUI/Containers/TabView.swift @@ -129,7 +129,7 @@ struct TabItem: View { } @Composable public override func ComposeContent(context: ComposeContext) { - view.Compose(context: context) + let _ = view.Compose(context: context) } @Composable func ComposeTitle(context: ComposeContext) { @@ -163,7 +163,7 @@ class TabIndexComposer: Composer { self.index = index } - override func reset() { + override func willCompose() { currentIndex = 0 } diff --git a/Sources/SkipUI/SkipUI/Containers/VStack.swift b/Sources/SkipUI/SkipUI/Containers/VStack.swift index 99f24fba..fa61ae53 100644 --- a/Sources/SkipUI/SkipUI/Containers/VStack.swift +++ b/Sources/SkipUI/SkipUI/Containers/VStack.swift @@ -5,9 +5,7 @@ #if SKIP import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -55,16 +53,8 @@ public struct VStack : View where Content : View { contentContext = context.content() columnArrangement = Arrangement.spacedBy(spacing.dp) } else { - contentContext = context.content() - columnArrangement = Arrangement.spacedBy(Self.defaultSpacing.dp) - -// var lastViewWasText: Bool? = nil -// contentContext = context.content(composer: { view, context in -// lastViewWasText = ComposeDefaultSpacedItem(view: &view, context: context, lastViewWasText: lastViewWasText) -// }) -// //~~~ -//// columnArrangement = Arrangement.Center -// columnArrangement = Arrangement.spacedBy(Self.defaultSpacing.dp) + contentContext = context.content(composer: VStackComposer()) + columnArrangement = Arrangement.spacedBy(0.dp) } ComposeContainer(modifier: context.modifier) { modifier in Column(modifier: modifier, verticalArrangement: columnArrangement, horizontalAlignment: columnAlignment) { @@ -77,30 +67,38 @@ public struct VStack : View where Content : View { } } } + #else + public var body: some View { + stubView() + } + #endif +} +#if SKIP +class VStackComposer: Composer { private static let defaultSpacing = 8.0 // SwiftUI spaces adaptively based on font, etc, but this is at least closer to SwiftUI than our defaultSpacing private static let textSpacing = 1.0 -// @Composable private func ComposeDefaultSpacedItem(view: inout View, context: ComposeContext, lastViewWasText: Bool?) -> Bool { -// // If the Text has spacing modifiers, no longer special case its spacing -// let isText = view.strippingModifiers(until: { $0 == .spacing }) { $0 is Text } -// //~~~ -// if let lastViewWasText { -// let spacing = lastViewWasText && isText ? Self.textSpacing : Self.defaultSpacing -// let modifier = Modifier.padding(top: spacing.dp).then(context.modifier) -// view.ComposeContent(context: context.content(modifier: modifier)) -// } else { -// view.ComposeContent(context: context) -// } -// return isText -// } - #else - public var body: some View { - stubView() + private var lastViewWasText: Bool? = nil + + override func willCompose() { + lastViewWasText = nil + } + + @Composable override func Compose(view: inout View, context: (Bool) -> ComposeContext) { + // If the Text has spacing modifiers, no longer special case its spacing + let isText = view.strippingModifiers(until: { $0 == .spacing }) { $0 is Text } + var contentContext = context(false) + if let lastViewWasText { + let spacing = lastViewWasText && isText ? Self.textSpacing : Self.defaultSpacing + androidx.compose.foundation.layout.Spacer(modifier: Modifier.height(spacing.dp)) + } + view.ComposeContent(context: contentContext) + lastViewWasText = isText } - #endif } +#endif #if !SKIP diff --git a/Sources/SkipUI/SkipUI/Text/Label.swift b/Sources/SkipUI/SkipUI/Text/Label.swift index 681db51e..05c2f736 100644 --- a/Sources/SkipUI/SkipUI/Text/Label.swift +++ b/Sources/SkipUI/SkipUI/Text/Label.swift @@ -73,13 +73,13 @@ public struct Label : View, ListItemAdapting { } /// Compose only the title of this label. - @Composable func ComposeTitle(context: ComposeContext) { - title.Compose(context: context) + @Composable func ComposeTitle(context: ComposeContext) -> ComposeResult { + return title.Compose(context: context) } /// Compose only the image of this label. - @Composable func ComposeImage(context: ComposeContext) { - image.Compose(context: context) + @Composable func ComposeImage(context: ComposeContext) -> ComposeResult { + return image.Compose(context: context) } @Composable func shouldComposeListItem() -> Bool { diff --git a/Sources/SkipUI/SkipUI/View/View.swift b/Sources/SkipUI/SkipUI/View/View.swift index 5c92cf03..f923f8da 100644 --- a/Sources/SkipUI/SkipUI/View/View.swift +++ b/Sources/SkipUI/SkipUI/View/View.swift @@ -39,12 +39,12 @@ public protocol View { #if SKIP extension View { /// Compose this view without an existing context - typically called when integrating a SwiftUI view tree into pure Compose. - @Composable public func Compose() { - Compose(context: ComposeContext()) + @Composable public func Compose() -> ComposeResult { + return Compose(context: ComposeContext()) } /// Calls to `Compose` are added by the transpiler. - @Composable public func Compose(context: ComposeContext) { + @Composable public func Compose(context: ComposeContext) -> ComposeResult { if let composer = context.composer { composer.Compose(view: &self, context: { retain in guard !retain else { @@ -57,6 +57,7 @@ extension View { } else { ComposeContent(context: context) } + return .ok } /// Compose this view's content. diff --git a/Tests/SkipUITests/CanvasTests.swift b/Tests/SkipUITests/CanvasTests.swift index 605f0e67..8ef12a3c 100644 --- a/Tests/SkipUITests/CanvasTests.swift +++ b/Tests/SkipUITests/CanvasTests.swift @@ -75,6 +75,7 @@ final class CanvasTests: XCSnapshotTestCase { androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.background(androidx.compose.ui.graphics.Color.White).size(12.dp), contentAlignment: androidx.compose.ui.Alignment.Center) { androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.background(androidx.compose.ui.graphics.Color.Black).size(6.dp, 6.dp)) } + return .ok })).pixmap, plaf(""" F F F F F F F F F F F F @@ -101,6 +102,7 @@ final class CanvasTests: XCSnapshotTestCase { androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.size(12.dp).background(androidx.compose.ui.graphics.Color.White), contentAlignment: androidx.compose.ui.Alignment.Center) { androidx.compose.foundation.layout.Box(modifier: androidx.compose.ui.Modifier.size(6.dp, 6.dp).background(androidx.compose.ui.graphics.Color.Black)) } + return .ok })).pixmap, plaf(""" F F F F F F F F F F F F