Skip to content

Commit

Permalink
Update ComposeView content and Compose function to return a value.
Browse files Browse the repository at this point in the history
See comments in ComposeView for explanation
  • Loading branch information
aabewhite committed Oct 4, 2023
1 parent 3d372df commit fb49253
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 69 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class V: View {
} else {
Text("Goodbye!").Compose(context: composectx)
}
ComposeResult.ok
}
}

Expand Down Expand Up @@ -188,6 +189,7 @@ VStack {
Text("Hello from SwiftUI")
ComposeView { _ in
androidx.compose.material3.Text("Hello from Compose")
return .ok
}
}
```
Expand All @@ -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
}
```

Expand All @@ -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
}
```

Expand Down
1 change: 0 additions & 1 deletion Sources/SkipUI/SkipUI/Compose/ComposeContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,4 @@ import androidx.compose.ui.Modifier
content(modifier)
}
}

#endif
37 changes: 19 additions & 18 deletions Sources/SkipUI/SkipUI/Compose/ComposeContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -44,32 +52,25 @@ public protocol Composer {
}

extension Composer {
public func reset() {
public func willCompose() {
}

public func didCompose(result: ComposeResult) {
}
}

/// Builtin composer that executes a closure to compose.
///
/// - 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)
}
}

Expand Down
19 changes: 14 additions & 5 deletions Sources/SkipUI/SkipUI/Compose/ComposeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 3 additions & 2 deletions Sources/SkipUI/SkipUI/Containers/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ public struct Group<Content> : 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 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public struct List<SelectionValue, Content> : 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)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SkipUI/SkipUI/Containers/Navigation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions Sources/SkipUI/SkipUI/Containers/TabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -163,7 +163,7 @@ class TabIndexComposer: Composer {
self.index = index
}

override func reset() {
override func willCompose() {
currentIndex = 0
}

Expand Down
58 changes: 28 additions & 30 deletions Sources/SkipUI/SkipUI/Containers/VStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,16 +53,8 @@ public struct VStack<Content> : 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) {
Expand All @@ -77,30 +67,38 @@ public struct VStack<Content> : 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

Expand Down
8 changes: 4 additions & 4 deletions Sources/SkipUI/SkipUI/Text/Label.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions Sources/SkipUI/SkipUI/View/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -57,6 +57,7 @@ extension View {
} else {
ComposeContent(context: context)
}
return .ok
}

/// Compose this view's content.
Expand Down
2 changes: 2 additions & 0 deletions Tests/SkipUITests/CanvasTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit fb49253

Please sign in to comment.