Skip to content

Commit

Permalink
Use a single ComposeModifierView for all modifiers
Browse files Browse the repository at this point in the history
Add ability to peek at unmodified inner view.
Support SwiftUI-like variable default spacing in VStack
  • Loading branch information
aabewhite committed Sep 20, 2023
1 parent 96200cf commit 447b610
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 37 deletions.
17 changes: 0 additions & 17 deletions Sources/SkipUI/SkipUI/Compose/ComposeContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,4 @@ public struct ComposeContext {
return context
}
}

/// Used internally by modifiers to apply changes to the context supplied to modified views.
struct ComposeContextView: View {
let view: View
let contextTransform: @Composable (inout ComposeContext) -> Void

init(_ view: any View, contextTransform: @Composable (inout ComposeContext) -> Void) {
self.view = view
self.contextTransform = contextTransform
}

@Composable override func ComposeContent(context: ComposeContext) {
var context = context
contextTransform(&context)
view.ComposeContent(context)
}
}
#endif
60 changes: 60 additions & 0 deletions Sources/SkipUI/SkipUI/Compose/ComposeModifierView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU Lesser General Public License 3.0
// as published by the Free Software Foundation https://fsf.org

#if SKIP
import androidx.compose.runtime.Composable

/// Recognized modifier roles.
public enum ComposeModifierRole { // View.strippingModifiers becomes public in Kotlin and exposes this type, so it must be public
case accessibility
case spacing
case unspecified
}

/// Used internally by modifiers to apply changes to the context supplied to modified views.
struct ComposeModifierView: View {
let view: View
let role: ComposeModifierRole
private let contextTransform: (@Composable (inout ComposeContext) -> Void)?
private let composeContent: (@Composable (any View, ComposeContext) -> Void)?

/// A modfiier that transforms the compose context.
init(contextView: any View, role: ComposeModifierRole = .unspecified, contextTransform: @Composable (inout ComposeContext) -> Void) {
// Don't copy view
// SKIP REPLACE: this.view = contextView
self.view = contextView
self.role = role
self.contextTransform = contextTransform
self.composeContent = nil
}

/// A modifier that takes over the composition.
init(contentView: any View, role: ComposeModifierRole = .unspecified, composeContent: @Composable (any View, ComposeContext) -> Void) {
// Don't copy view
// SKIP REPLACE: this.view = contentView
self.view = contentView
self.role = role
self.contextTransform = nil
self.composeContent = composeContent
}

@Composable override func ComposeContent(context: ComposeContext) {
if let composeContent {
composeContent(view, context)
} else if let contextTransform {
var context = context
contextTransform(&context)
view.ComposeContent(context)
}
}

func strippingModifiers<R>(whileRole: (ComposeModifierRole) -> Bool = { _ in true}, perform: (any View?) -> R) -> R {
if whileRole(role) {
return view.strippingModifiers(whileRole: whileRole, perform: perform)
} else {
return perform(self)
}
}
}
#endif
1 change: 1 addition & 0 deletions Sources/SkipUI/SkipUI/Containers/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public struct List<SelectionValue, Content> : View where SelectionValue: Hashabl
@Composable private func ComposeItem(view: inout View, context: ComposeContext, style: ListStyle) {
let contentModifier = Modifier.padding(horizontal: Self.horizontalItemInset.dp, vertical: Self.verticalItemInset.dp).fillMaxWidth().requiredHeightIn(min: Self.minimumItemHeight.dp)
Column(modifier: Modifier.background(BackgroundColor(style: .plain)).then(context.modifier)) {
// We can't strip modifiers here because there is no way to re-apply them, so any modifier prevents adaptive list rendering
if let listItemAdapting = view as? ListItemAdapting, listItemAdapting.shouldComposeListItem() {
listItemAdapting.ComposeListItem(context: context, contentModifier: contentModifier)
} else {
Expand Down
11 changes: 7 additions & 4 deletions Sources/SkipUI/SkipUI/Containers/VStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,21 @@ public struct VStack<Content> : View where Content : View {
}
}

private static let defaultSpacing = 40.0
private static let textSpacing = 20.0
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(whileRole: { $0 != .spacing }) { $0 is Text }
if let lastViewWasText {
let spacing = lastViewWasText ? Self.textSpacing : Self.defaultSpacing
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 lastViewWasText != true
return isText
}
#else
public var body: some View {
Expand Down
8 changes: 4 additions & 4 deletions Sources/SkipUI/SkipUI/Environment/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,11 @@ extension View {
// Must be public to allow access from our inline `environment` function.
public func environmentObject(type: Any.Type, object: Any?) -> some View {
#if SKIP
return ComposeView { context in
return ComposeModifierView(contentView: self) { view, context in
let compositionLocal = EnvironmentValues.shared.objectCompositionLocal(type: type)
let value = object ?? Unit
// SKIP INSERT: val provided = compositionLocal provides value
androidx.compose.runtime.CompositionLocalProvider(provided) { self.Compose(context) }
androidx.compose.runtime.CompositionLocalProvider(provided) { view.Compose(context) }
}
#else
return self
Expand All @@ -129,11 +129,11 @@ extension View {
// We rely on the transpiler to turn the `WriteableKeyPath` provided in code into a `setValue` closure
public func environment<V>(_ setValue: (V) -> Void, _ value: V) -> some View {
#if SKIP
return ComposeView { context in
return ComposeModifierView(contentView: self) { view, context in
EnvironmentValues.shared.setValues {
_ in setValue(value)
} in: {
self.Compose(context)
view.Compose(context)
}
}
#else
Expand Down
6 changes: 3 additions & 3 deletions Sources/SkipUI/SkipUI/System/Accessibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import struct CoreGraphics.CGPoint
extension View {
public func accessibilityIdentifier(_ identifier: String) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self, role: .accessibility) {
$0.modifier = $0.modifier.testTag(identifier)
}
#else
Expand All @@ -21,7 +21,7 @@ extension View {
extension View {
public func accessibilityLabel(_ label: Text) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self, role: .accessibility) {
$0.modifier = $0.modifier.semantics { contentDescription = label.text }
}
#else
Expand All @@ -31,7 +31,7 @@ extension View {

public func accessibilityLabel(_ label: String) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self, role: .accessibility) {
$0.modifier = $0.modifier.semantics { contentDescription = label }
}
#else
Expand Down
23 changes: 14 additions & 9 deletions Sources/SkipUI/SkipUI/View/View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,18 @@ extension View {
@Composable public func ComposeContent(context: ComposeContext) -> Void {
body.ComposeContent(context)
}

/// Strip modifier views unless they have one of the given roles.
func strippingModifiers<R>(whileRole: (ComposeModifierRole) -> Bool = { _ in true}, perform: (any View?) -> R) -> R {
return perform(self)
}
}
#endif

extension View {
public func background(_ color: Color) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self) {
$0.modifier = $0.modifier.background(color.colorImpl())
}
#else
Expand All @@ -72,7 +77,7 @@ extension View {

public func border(_ color: Color, width: CGFloat = 1.0) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self) {
$0.modifier = $0.modifier.border(width: width.dp, color: color.colorImpl())
}
#else
Expand All @@ -94,7 +99,7 @@ extension View {

public func frame(width: CGFloat? = nil, height: CGFloat? = nil) -> some View {
#if SKIP
return ComposeView { context in
return ComposeModifierView(contentView: self) { view, context in
var context = context
if let width {
context.modifier = context.modifier.width(width.dp)
Expand All @@ -110,7 +115,7 @@ extension View {
$0.set_fillHeight(nil)
}
} in: {
self.Compose(context: context)
view.ComposeContent(context: context)
}
}
#else
Expand Down Expand Up @@ -138,7 +143,7 @@ extension View {

public func opacity(_ opacity: Double) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self) {
$0.modifier = $0.modifier.alpha(Float(opacity))
}
#else
Expand All @@ -148,7 +153,7 @@ extension View {

public func padding(_ insets: EdgeInsets) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self, role: .spacing) {
$0.modifier = $0.modifier.padding(start: insets.leading.dp, top: insets.top.dp, end: insets.trailing.dp, bottom: insets.bottom.dp)
}
#else
Expand All @@ -163,7 +168,7 @@ extension View {
let end = edges.contains(.trailing) ? amount : 0.dp
let top = edges.contains(.top) ? amount : 0.dp
let bottom = edges.contains(.bottom) ? amount : 0.dp
return ComposeContextView(self) {
return ComposeModifierView(contextView: self, role: .spacing) {
$0.modifier = $0.modifier.padding(start: start, top: top, end: end, bottom: bottom)
}
#else
Expand All @@ -177,7 +182,7 @@ extension View {

public func rotationEffect(_ angle: Angle) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self) {
$0.modifier = $0.modifier.rotate(Float(angle.degrees))
}
#else
Expand Down Expand Up @@ -214,7 +219,7 @@ extension View {

public func scaleEffect(x: CGFloat = 1.0, y: CGFloat = 1.0) -> some View {
#if SKIP
return ComposeContextView(self) {
return ComposeModifierView(contextView: self) {
$0.modifier = $0.modifier.scale(scaleX: Float(x), scaleY: Float(y))
}
#else
Expand Down
18 changes: 18 additions & 0 deletions Tests/SkipUITests/ModifierTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2023 Skip
//
// This is free software: you can redistribute and/or modify it
// under the terms of the GNU Lesser General Public License 3.0
// as published by the Free Software Foundation https://fsf.org

import SwiftUI
import XCTest

final class ModifierTests: XCTestCase {
func testModifierViewDoesNotCopy() {
#if SKIP
let text = Text("test")
let modified = text.font(.title).padding(.all, 10.0)
modified.strippingModifiers { XCTAssertEqual($0, text) }
#endif
}
}

0 comments on commit 447b610

Please sign in to comment.