Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,29 @@ TextField("Enter username:", text: $username)
#endif
```

You can also apply [scoped modifiers](https://developer.android.com/develop/ui/compose/modifiers#scope-safety). e.g. in a `LazyHStack` you can use modifiers scoped to [`LazyItemScope`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyItemScope), like `animateItem()`.

```swift
#if SKIP
import androidx.compose.foundation.lazy.LazyItemScope
#endif

...

LazyHStack {
ForEach(0..<count, id: \.self) { i in
Color.red
.frame(width: 20, height: 20)
.id(i + 1)
#if SKIP
.composeModifier(scope: LazyItemScope.self) {
$0.animateItem()
}
#endif
}
}
```

## Material

Under the hood, SkipUI uses Android's Material 3 colors and components. While we expect you to use SwiftUI's built-in color schemes (`.preferredColorScheme`) and modifiers (`.background`, `.foregroundStyle`, `.tint`, and so on) for most UI styling, there are some Android customizations that have no SwiftUI equivalent. Skip therefore adds additional, Android-only API for manipulating Material colors and components.
Expand Down
6 changes: 5 additions & 1 deletion Sources/SkipUI/SkipUI/Compose/ComposeContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ import androidx.compose.ui.Modifier

/// Use in conjunction with `rememberSaveable` to store view state.
public var stateSaver: Saver<Any?, Any> = ComposeStateSaver()

/// The scope of the current composition (so users can call scoped modifiers)
public var scope: AnyObject?

/// The context to pass to child content of a container view.
///
/// By default, modifiers and the `composer` are reset for child content.
public func content(modifier: Modifier = Modifier, composer: Composer? = nil, stateSaver: Saver<Any?, Any>? = nil) -> ComposeContext {
public func content(modifier: Modifier = Modifier, composer: Composer? = nil, stateSaver: Saver<Any?, Any>? = nil, scope: AnyObject? = nil) -> ComposeContext {
var context = self
context.modifier = modifier
context.composer = composer
context.stateSaver = stateSaver ?? self.stateSaver
context.scope = scope
return context
}
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/SkipUI/SkipUI/Compose/ComposeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ extension View {
return ComposeResult.ok
}
}

/// Add the given scoped modifier to the underlying Compose view.
// SKIP DECLARE: fun <S: Any> composeModifier(scope: KClass<S>, modifier: S.(Modifier) -> Modifier): View
public func composeModifier<S>(scope: S.Type, _ modifier: (Modifier) -> Modifier) throws -> View {
return ComposeModifierView(targetView: self) { context in
let scope = try context.scope as S
scope.run {
context.modifier = modifier(context.modifier)
}
return ComposeResult.ok
}
}
#endif

/// Apply the given `ContentModifier`.
Expand Down
13 changes: 6 additions & 7 deletions Sources/SkipUI/SkipUI/Containers/LazyHGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public struct LazyHGrid: View {
let viewsCollector = context.content(composer: collectingComposer)
content.Compose(context: viewsCollector)

let itemContext = context.content()
let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) }
ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true, fillHeight: false) { modifier in
// Integrate with our scroll-to-top and ScrollViewReader
Expand Down Expand Up @@ -97,7 +96,7 @@ public struct LazyHGrid: View {
item: { view, _ in
item {
Box(contentAlignment: boxAlignment) {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
},
Expand All @@ -106,37 +105,37 @@ public struct LazyHGrid: View {
let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!($0)) }
items(count: count, key: key) { index in
Box(contentAlignment: boxAlignment) {
factory(index + range.start).Compose(context: itemContext)
factory(index + range.start).Compose(context: context.content(scope: self))
}
}
},
objectItems: { objects, identifier, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objects[$0])) }
items(count: objects.count, key: key) { index in
Box(contentAlignment: boxAlignment) {
factory(objects[index]).Compose(context: itemContext)
factory(objects[index]).Compose(context: context.content(scope: self))
}
}
},
objectBindingItems: { objectsBinding, identifier, _, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objectsBinding.wrappedValue[$0])) }
items(count: objectsBinding.wrappedValue.count, key: key) { index in
Box(contentAlignment: boxAlignment) {
factory(objectsBinding, index).Compose(context: itemContext)
factory(objectsBinding, index).Compose(context: context.content(scope: self))
}
}
},
sectionHeader: { view in
item(span: { GridItemSpan(maxLineSpan) }) {
Box(contentAlignment: androidx.compose.ui.Alignment.Center) {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
},
sectionFooter: { view in
item(span: { GridItemSpan(maxLineSpan) }) {
Box(contentAlignment: androidx.compose.ui.Alignment.Center) {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
}
Expand Down
13 changes: 6 additions & 7 deletions Sources/SkipUI/SkipUI/Containers/LazyHStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ public struct LazyHStack : View {
let viewsCollector = context.content(composer: collectingComposer)
content.Compose(context: viewsCollector)

let itemContext = context.content()
let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) }
ComposeContainer(axis: .horizontal, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true, fillHeight: false) { modifier in
// Integrate with ScrollViewReader
Expand Down Expand Up @@ -88,36 +87,36 @@ public struct LazyHStack : View {
startItemIndex: 0,
item: { view, _ in
item {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
},
indexedItems: { range, identifier, _, _, _, _, factory in
let count = range.endExclusive - range.start
let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!($0)) }
items(count: count, key: key) { index in
factory(index + range.start).Compose(context: itemContext)
factory(index + range.start).Compose(context: context.content(scope: self))
}
},
objectItems: { objects, identifier, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objects[$0])) }
items(count: objects.count, key: key) { index in
factory(objects[index]).Compose(context: itemContext)
factory(objects[index]).Compose(context: context.content(scope: self))
}
},
objectBindingItems: { objectsBinding, identifier, _, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objectsBinding.wrappedValue[$0])) }
items(count: objectsBinding.wrappedValue.count, key: key) { index in
factory(objectsBinding, index).Compose(context: itemContext)
factory(objectsBinding, index).Compose(context: context.content(scope: self))
}
},
sectionHeader: { view in
item {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
},
sectionFooter: { view in
item {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
)
Expand Down
15 changes: 7 additions & 8 deletions Sources/SkipUI/SkipUI/Containers/LazyVGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ public struct LazyVGrid: View {
let searchableState = EnvironmentValues.shared._searchableState
let isSearchable = searchableState?.isOnNavigationStack() == false

let itemContext = context.content()
let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) }
ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true, fillHeight: false) { modifier in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in
Expand Down Expand Up @@ -114,7 +113,7 @@ public struct LazyVGrid: View {
item: { view, _ in
item {
Box(contentAlignment: boxAlignment) {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
},
Expand All @@ -123,45 +122,45 @@ public struct LazyVGrid: View {
let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!($0)) }
items(count: count, key: key) { index in
Box(contentAlignment: boxAlignment) {
factory(index + range.start).Compose(context: itemContext)
factory(index + range.start).Compose(context: context.content(scope: self))
}
}
},
objectItems: { objects, identifier, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objects[$0])) }
items(count: objects.count, key: key) { index in
Box(contentAlignment: boxAlignment) {
factory(objects[index]).Compose(context: itemContext)
factory(objects[index]).Compose(context: context.content(scope: self))
}
}
},
objectBindingItems: { objectsBinding, identifier, _, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objectsBinding.wrappedValue[$0])) }
items(count: objectsBinding.wrappedValue.count, key: key) { index in
Box(contentAlignment: boxAlignment) {
factory(objectsBinding, index).Compose(context: itemContext)
factory(objectsBinding, index).Compose(context: context.content(scope: self))
}
}
},
sectionHeader: { view in
item(span: { GridItemSpan(maxLineSpan) }) {
Box(contentAlignment: androidx.compose.ui.Alignment.Center) {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
},
sectionFooter: { view in
item(span: { GridItemSpan(maxLineSpan) }) {
Box(contentAlignment: androidx.compose.ui.Alignment.Center) {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
}
)
if isSearchable {
item(span: { GridItemSpan(maxLineSpan) }) {
let modifier = Modifier.padding(start: 16.dp, end: 16.dp, top: 16.dp, bottom: 8.dp).fillMaxWidth()
SearchField(state: searchableState!, context: context.content(modifier: modifier))
SearchField(state: searchableState!, context: context.content(modifier: modifier, scope: self))
}
}
for (view, level) in collectingComposer.views {
Expand Down
13 changes: 6 additions & 7 deletions Sources/SkipUI/SkipUI/Containers/LazyVStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public struct LazyVStack : View {
let searchableState = EnvironmentValues.shared._searchableState
let isSearchable = searchableState?.isOnNavigationStack() == false

let itemContext = context.content()
let factoryContext = remember { mutableStateOf(LazyItemFactoryContext()) }
ComposeContainer(axis: .vertical, scrollAxes: scrollAxes, modifier: context.modifier, fillWidth: true, fillHeight: false) { modifier in
IgnoresSafeAreaLayout(expandInto: [], checkEdges: [.bottom], modifier: modifier) { _, safeAreaEdges in
Expand Down Expand Up @@ -106,36 +105,36 @@ public struct LazyVStack : View {
startItemIndex: isSearchable ? 1 : 0,
item: { view, _ in
item {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
},
indexedItems: { range, identifier, _, _, _, _, factory in
let count = range.endExclusive - range.start
let key: ((Int) -> String)? = identifier == nil ? nil : { composeBundleString(for: identifier!($0)) }
items(count: count, key: key) { index in
factory(index + range.start).Compose(context: itemContext)
factory(index + range.start).Compose(context: context.content(scope: self))
}
},
objectItems: { objects, identifier, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objects[$0])) }
items(count: objects.count, key: key) { index in
factory(objects[index]).Compose(context: itemContext)
factory(objects[index]).Compose(context: context.content(scope: self))
}
},
objectBindingItems: { objectsBinding, identifier, _, _, _, _, _, factory in
let key: (Int) -> String = { composeBundleString(for: identifier(objectsBinding.wrappedValue[$0])) }
items(count: objectsBinding.wrappedValue.count, key: key) { index in
factory(objectsBinding, index).Compose(context: itemContext)
factory(objectsBinding, index).Compose(context: context.content(scope: self))
}
},
sectionHeader: { view in
item {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
},
sectionFooter: { view in
item {
view.Compose(context: itemContext)
view.Compose(context: context.content(scope: self))
}
}
)
Expand Down