https://github.com/skiptools/skip-ui
SwiftUI for Android
https://github.com/skiptools/skip-ui
Last synced: 4 months ago
JSON representation
SwiftUI for Android
- Host: GitHub
- URL: https://github.com/skiptools/skip-ui
- Owner: skiptools
- License: other
- Created: 2023-07-24T00:45:39.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2026-02-06T19:11:12.000Z (4 months ago)
- Last Synced: 2026-02-07T05:23:03.155Z (4 months ago)
- Language: Swift
- Homepage: https://skip.dev/docs/modules/skip-ui/
- Size: 4.26 MB
- Stars: 285
- Watchers: 9
- Forks: 40
- Open Issues: 80
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.LGPL
Awesome Lists containing this project
README
# SkipUI
SwiftUI support for [Skip](https://skip.dev) apps.
## Setup
To include this framework in your project, add the following
dependency to your `Package.swift` file:
```swift
let package = Package(
name: "my-package",
products: [
.library(name: "MyProduct", targets: ["MyTarget"]),
],
dependencies: [
.package(url: "https://source.skip.dev/skip-ui.git", from: "1.0.0"),
],
targets: [
.target(name: "MyTarget", dependencies: [
.product(name: "SkipUI", package: "skip-ui")
])
]
)
```
## About SkipUI
SkipUI vends the `skip.ui` Kotlin package. It is a reimplementation of SwiftUI for Kotlin on Android using Jetpack Compose. Its goal is to mirror as much of SwiftUI as possible, allowing Skip developers to use SwiftUI with confidence.

{: .diagram-vector }
SkipUI is used directly by [Skip Lite](https://skip.dev/docs/status/#skip_fuse) transpiled Swift, and it is used indirectly by [Skip Fuse](https://skip.dev/docs/status/#skip_fuse) compiled Swift through the SkipFuseUI native framework.
## Dependencies
SkipUI depends on the [skip](https://source.skip.dev/skip) transpiler plugin. The transpiler must transpile SkipUI's own source code, and SkipUI relies on the transpiler's transformation of SwiftUI code. See [Implementation Strategy](#implementation-strategy) for details. SkipUI also depends on the [SkipFoundation](https://github.com/skiptools/skip-foundation) and [SkipModel](https://github.com/skiptools/skip-model) packages.
SkipUI is part of the core *Core Skip Frameworks* and is not intended to be imported directly.
The module is transparently adopted by importing SwiftUI in compiled Swift, and through the translation of `import SwiftUI` into `import skip.ui.*` for transpiled code.
### Android Libraries
- SkipUI adds an Android dependency on [Coil](https://coil-kt.github.io/coil/) to implement `AsyncImage`.
- SkipUI includes source code from the [ComposeReorderable](https://github.com/aclassen/ComposeReorderable) project to implement drag-to-reorder in `Lists`.
## Status
SkipUI has robust support for the building blocks of SwiftUI, including its state flow and declarative syntax. SkipUI also implements a large percentage of SwiftUI's components and modifiers. It is possible to write an Android app entirely in SwiftUI utilizing SkipUI's current component set.
Some of SwiftUI's vast surface area, however, is not yet implemented. See [Supported SwiftUI](#supported-swiftui) for a full list of supported API.
When you want to use a SwiftUI construct that has not been implemented, you have options. You can try to find a workaround using only supported components, [embed Compose code directly](#composeview), or [add support to SkipUI](#implementation-strategy). If you choose to enhance SkipUI itself, please consider [contributing](#contributing) your code back for inclusion in the official release.
## ComposeView
`ComposeView` is an Android-only SwiftUI view that you can use to embed Compose directly into your SwiftUI view tree. Its use differs for Skip Fuse compiled code and Skip Lite transpiled code.
### Skip Fuse
In the following SkipFuseUI example, we use a SwiftUI `Text` to write "Hello from SwiftUI", followed by calling the `androidx.compose.material3.Text()` Compose function to write "Hello from Compose" below it. Notice that integrating Compose functions in Skip Fuse has two parts:
1. Define a `ContentComposer` in a transpiled `#if SKIP` block.
1. Use a `ComposeView` to render the `ContentComposer` in the SwiftUI view tree.
```swift
import SwiftUI
...
VStack {
Text("Hello from SwiftUI")
#if os(Android)
ComposeView { MessageComposer(message: "Hello from Compose") }
#endif
}
#if SKIP
struct MessageComposer : ContentComposer {
let message: String
@Composable func Compose(context: ComposeContext) {
androidx.compose.material3.Text(message)
}
}
#endif
```
The `ContentComposer` protocol consists of a single function:
```swift
public protocol ContentComposer {
@Composable func Compose(context: ComposeContext)
}
```
#### Passing State
Remember that any data you pass to your `ContentComposer` must be bridged from your compiled Swift to your `#if SKIP` block's transpiled Kotlin. Simple data types like the `String` used in the example above bridge automatically.
Skip also allows you to pass many built-in SwiftUI types. These types will bridge to their `SkipUI` implementations, which have been enhanced to allow you to use them in Compose. Examples include:
- `Color`: Pass any `Color` value to your `ContentComposer`, then use the `SkipUI.Color.asComposeColor()` function within your Compose code to get an `androidx.compose.ui.graphics.Color` value.
- `Font`: Similarly, pass any `Font` value and use the `SkipUI.Font.asComposeTextStyle()` function to get an `androidx.compose.ui.text.TextStyle`.
- `Image`: You can pass a native `Image` to your `ContentComposer` and receive the equivalent `SkipUI.Image`. Images, however, do not have a one-to-one equivalent Compose value type. You can still call `Image.Compose(context:)` to render it in your Compose code.
- `Text`: Passing a `Text` is a useful way to encapsulate a localizable string value. Call `SkipUI.Text.localizedTextString()` within your Compose code to get the localized value.
Here is sample code using some of these techniques:
```swift
...
#if os(Android)
ComposeView {
MessageComposer(message: Text("Welcome"), textColor: .red)
}
#endif
...
#if SKIP
struct MessageComposer : ContentComposer {
let message: Text
let textColor: Color
@Composable override func Compose(context: ComposeContext) {
androidx.compose.material3.Text(message.localizedTextString(), color: textColor.asComposeColor())
}
}
#endif
```
See the [bridging](https://skip.dev/docs/modes/#bridging) documentation for information on bridging your own data types.
### Skip Lite
In the following transpiled example, we use a SwiftUI `Text` to write "Hello from SwiftUI", followed by calling the `androidx.compose.material3.Text()` Compose function to write "Hello from Compose" below it:
```swift
import SwiftUI
...
VStack {
Text("Hello from SwiftUI")
#if SKIP
ComposeView { _ in
androidx.compose.material3.Text("Hello from Compose")
}
#endif
}
```
Unlike Skip Fuse, Skip Lite transpiled Swift can invoke Compose functions directly within the `ComposeView` body.
### Compose(context:)
SkipUI enhances all SwiftUI views with a `Compose(context:)` method, allowing you to use SwiftUI views from within Compose. The following example again uses a SwiftUI `Text` to write "Hello from SwiftUI", but this time from within a `ComposeView`.
Skip Fuse:
```swift
#if os(Android)
ComposeView { ColumnComposer() }
#endif
...
#if SKIP
struct ColumnComposer : ContentComposer {
@Composable func Compose(context: ComposeContext) {
androidx.compose.foundation.layout.Column(modifier: context.modifier) {
Text("Hello from SwiftUI").Compose(context: context.content())
androidx.compose.material3.Text("Hello from Compose")
}
}
}
#endif
```
Skip Lite:
```swift
#if SKIP
ComposeView { context in
androidx.compose.foundation.layout.Column(modifier: context.modifier) {
Text("Hello from SwiftUI").Compose(context: context.content())
androidx.compose.material3.Text("Hello from Compose")
}
}
#endif
```
With `ComposeView` and the `Compose(context:)` function, you can move fluidly between SwiftUI and Compose code. These techniques work not only with standard SwiftUI and Compose components, but with your own custom SwiftUI views and Compose functions as well.
### Additional Considerations
There are additional considerations when integrating SwiftUI into a Compose application that is **not** managed by Skip. SwiftUI relies on its own mechanisms to save and restore `Activity` UI state, such as `@AppStorage` and navigation path bindings. It is not compatible with Android's `Activity` UI state restoration. Use a pattern like the following to exclude SwiftUI from `Activity` state restoration when integrating SwiftUI views:
```kotlin
val stateHolder = rememberSaveableStateHolder()
stateHolder.SaveableStateProvider("myKey") {
MySwiftUIRootView().Compose()
SideEffect { stateHolder.removeState("myKey") }
}
```
This pattern allows SkipUI to take advantage of Compose's UI state mechanisms internally while excluding it from `Activity` state restoration.
## composeModifier
In addition to `ComposeView` above, SkipUI offers the `composeModifier` SwiftUI modifier. This modifier allows you to apply any Compose modifiers to the underlying Compose view.
### Skip Fuse
Using `composeModifier` from Skip Fuse is much like using `ComposeView`:
1. Define a `ContentModifier` in a transpiled `#if SKIP` block.
1. Use `.composeModifier` to apply the `ContentModifier ` to the target SwiftUI view.
Within your `ContentModifier`, apply any SkipUI modifiers to the given target view. This includes the [Material](#material) modifiers we describe below, or the same-named transpiled `composeModifier`, which takes a block that accepts a single `androidx.compose.ui.Modifier` parameter and returns a `Modifier` as well. The following example applies Compose's `imePadding` modifier our SwiftUI on Android:
```swift
import SwiftUI
...
VStack {
TextField("Enter username:", text: $username)
#if os(Android)
.composeModifier { IMEPaddingModifier() }
#endif
}
#if SKIP
import androidx.compose.foundation.layout.imePadding
struct IMEPaddingModifier : ContentModifier {
func modify(view: any View) -> any View {
view.composeModifier { $0.imePadding() }
}
}
#endif
```
The `ContentModfiier` protocol consists of a single function:
```swift
public protocol ContentModifier {
func modify(view: any View) -> any View
}
```
### Skip Lite
If you are writing your SwiftUI using Skip Lite, you don't need to define a `ContentModfifier`. You can apply [Material](#material) modifiers or `.composeModifier` directly:
```swift
#if SKIP
import androidx.compose.foundation.layout.imePadding
#endif
...
TextField("Enter username:", text: $username)
#if SKIP
.composeModifier { $0.imePadding() }
#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.. ColorScheme)?, content: @Composable () -> Unit)
```
The `scheme` argument takes a closure with two arguments: Skip's default `androidx.compose.material3.ColorScheme`, and whether dark mode is being requested. Your closure returns the `androidx.compose.material3.ColorScheme` to use for the supplied content.
For example, to customize the surface colors for your entire app, you could edit `Main.kt` as follows:
```kotlin
@Composable
internal fun PresentationRootView(context: ComposeContext) {
Material3ColorScheme({ colors, isDark ->
colors.copy(surface = if (isDark) Color.purple.asComposeColor() else Color.yellow.asComposeColor())
}, content = {
// ... Original content of this function ...
})
}
```
Skip also provides the SwiftUI `.material3ColorScheme(_:)` modifier to customize a SwiftUI view hierarchy. The modifier takes the same closure as the `Material3ColorScheme` Kotlin function. Apply this modifier using the `.composeModifier` techniques discussed in the previous section. For example:
Skip Fuse:
```swift
MyView()
#if os(Android)
.composeModifier { ColorSchemeModifier() }
#endif
...
#if SKIP
struct ColorSchemeModifier : ContentModifier {
func modify(view: any View) -> any View {
view.material3ColorScheme { colors, isDark in
colors.copy(surface: isDark ? Color.purple.asComposeColor() : Color.yellow.asComposeColor())
}
}
}
#endif
```
Skip Lite:
```swift
MyView()
#if SKIP
.material3ColorScheme { colors, isDark in
colors.copy(surface: isDark ? Color.purple.asComposeColor() : Color.yellow.asComposeColor())
}
#endif
```
Skip's built-in components use the following Material 3 colors, if you'd like to customize them:
- `surface`
- `primary`
- `onBackground`
- `outline`
- `outlineVariant`
### Material Components
In addition to the `.material3ColorScheme` modifier detailed above, Skip includes many other `.material3` modifiers for its underlying Material 3 components. This family of modifiers share a common API pattern:
- The modifiers take a closure argument. This closure receives a `Material3Options` struct configured with Skip's defaults, and it returns a struct with any desired modifications.
- Every `Material3Options` struct implements a conventional Kotlin `copy` method. This allows you to copy and modify the struct in a single call.
- The modifiers place your closure into the SwiftUI `Environment`. This means that you can apply the modifier on a root view, and it will affect all subviews. While you may be used to placing navigation and tab bar modifiers on the views *within* the `NavigationStack` or `TabView`, the `.material3` family of modifiers always go *on or outside* the views you want to affect.
- Because they are designed to reach beneath Skip's SwiftUI covers, the modifiers use Compose terminology and types. In fact the properties of the supplied `Material3Options` structs typically exactly match the corresponding `androidx.compose.material3` component function parameters.
> [!NOTE]
> You can find details on Material 3 component API in [this Android API documentation](https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary).
Here is an example of changing the selected indicator color on your Android tab bar, which is implemented by the Material 3 `NavigationBar` component:
Skip Fuse:
```swift
TabView {
...
}
#if os(Android)
.composeModifier { NavigationBarModifier() }
#endif
...
#if SKIP
struct NavigationBarModifier : ContentModifier {
func modify(view: any View) -> any View {
view.material3NavigationBar { options in
let updatedColors = options.itemColors.copy(selectedIndicatorColor: Color.green.asComposeColor())
return options.copy(itemColors: updatedColors)
}
}
}
#endif
```
Skip Lite:
```swift
TabView {
...
}
#if SKIP
.material3NavigationBar { options in
let updatedColors = options.itemColors.copy(selectedIndicatorColor: Color.green.asComposeColor())
return options.copy(itemColors: updatedColors)
}
#endif
```
SkipUI currently includes the following Material modifiers:
```swift
extension View {
public func material3BottomAppBar(_ options: @Composable (Material3BottomAppBarOptions) -> Material3BottomAppBarOptions) -> some View
}
public struct Material3BottomAppBarOptions {
public var modifier: androidx.compose.ui.Modifier
public var containerColor: androidx.compose.ui.graphics.Color
public var contentColor: androidx.compose.ui.graphics.Color
public var tonalElevation: androidx.compose.ui.unit.Dp
public var contentPadding: androidx.compose.foundation.layout.PaddingValues
}
extension View {
public func material3Button(_ options: @Composable (Material3ButtonOptions) -> Material3ButtonOptions) -> some View
}
public struct Material3ButtonOptions {
public var onClick: () -> Void
public var modifier: androidx.compose.ui.Modifier
public var enabled: Bool
public var shape: androidx.compose.ui.graphics.Shape
public var colors: androidx.compose.material3.ButtonColors
public var elevation: androidx.compose.material3.ButtonElevation?
public var border: androidx.compose.foundation.BorderStroke?
public var contentPadding: androidx.compose.foundation.layout.PaddingValues
public var interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource?
}
extension View {
public func material3NavigationBar(_ options: @Composable (Material3NavigationBarOptions) -> Material3NavigationBarOptions) -> some View
}
public struct Material3NavigationBarOptions {
public var modifier: androidx.compose.ui.Modifier
public var containerColor: androidx.compose.ui.graphics.Color
public var contentColor: androidx.compose.ui.graphics.Color
public var tonalElevation: androidx.compose.ui.unit.Dp
public var onItemClick: (Int) -> Void
public var itemIcon: @Composable (Int) -> Void
public var itemModifier: @Composable (Int) -> androidx.compose.ui.Modifier
public var itemEnabled: (Int) -> Boolean
public var itemLabel: (@Composable (Int) -> Void)?
public var alwaysShowItemLabels: Bool
public var itemColors: androidx.compose.material3.NavigationBarItemColors
public var itemInteractionSource: androidx.compose.foundation.interaction.MutableInteractionSource?
}
extension View {
public func material3SegmentedButton(_ options: @Composable (Material3SegmentedButtonOptions) -> Material3SegmentedButtonOptions) -> some View
}
public struct Material3SegmentedButtonOptions {
public let index: Int
public let count: Int
public var selected: Boolean
public var onClick: () -> Void
public var modifier: androidx.compose.ui.Modifier
public var enabled: Bool
public var shape: androidx.compose.ui.graphics.Shape
public var colors: androidx.compose.material3.SegmentedButtonColors
public var border: androidx.compose.foundation.BorderStroke?
public var contentPadding: androidx.compose.foundation.layout.PaddingValues
public var interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource?
public var icon: @Composable () -> Void
}
extension View {
public func material3Text(_ options: @Composable (Material3TextOptions) -> Material3TextOptions) -> some View
}
public struct Material3TextOptions {
public var text: String?
public var annotatedText: AnnotatedString?
public var modifier: androidx.compose.ui.Modifier
public var color: androidx.compose.ui.graphics.Color
public var fontSize: androidx.compose.ui.unit.TextUnit
public var fontStyle: androidx.compose.ui.text.font.FontStyle?
public var fontWeight: androidx.compose.ui.text.font.FontWeight?
public var fontFamily: androidx.compose.ui.text.font.FontFamily?
public var letterSpacing: androidx.compose.ui.unit.TextUnit
public var textDecoration: androidx.compose.ui.text.style.TextDecoration?
public var textAlign: androidx.compose.ui.text.style.TextAlign?
public var lineHeight: androidx.compose.ui.unit.TextUnit
public var overflow: androidx.compose.ui.text.style.TextOverflow
public var softWrap: Bool
public var maxLines: Int
public var minLines: Int
public var onTextLayout: ((androidx.compose.ui.text.TextLayoutResult) -> Void)?
public var style: androidx.compose.ui.text.style.TextStyle
}
extension View {
public func material3TextField(_ options: @Composable (Material3TextFieldOptions) -> Material3TextFieldOptions) -> some View
}
public struct Material3TextFieldOptions {
public var value: androidx.compose.ui.text.input.TextFieldValue
public var onValueChange: (androidx.compose.ui.text.input.TextFieldValue) -> Void
public var modifier: androidx.compose.ui.Modifier
public var enabled: Bool
public var readOnly: Bool
public var textStyle: androidx.compose.ui.text.TextStyle
public var label: (@Composable () -> Void)?
public var placeholder: (@Composable () -> Void)?
public var leadingIcon: (@Composable () -> Void)?
public var trailingIcon: (@Composable () -> Void)?
public var prefix: (@Composable () -> Void)?
public var suffix: (@Composable () -> Void)?
public var supportingText: (@Composable () -> Void)?
public var isError: Bool
public var visualTransformation: androidx.compose.ui.text.input.VisualTransformation
public var keyboardOptions: androidx.compose.foundation.text.KeyboardOptions
public var keyboardActions: androidx.compose.foundation.text.KeyboardActions
public var singleLine: Bool
public var maxLines: Int
public var minLines: Int
public var interactionSource: androidx.compose.foundation.interaction.MutableInteractionSource?
public var shape: androidx.compose.ui.graphics.Shape
public var colors: androidx.compose.material3.TextFieldColors
}
extension View {
public func material3TopAppBar(_ options: @Composable (Material3TopAppBarOptions) -> Material3TopAppBarOptions) -> some View
}
public struct Material3TopAppBarOptions {
public var title: @Composable () -> Void
public var modifier: androidx.compose.ui.Modifier
public var navigationIcon: @Composable () -> Void
public var colors: androidx.compose.material3.TopAppBarColors
public var scrollBehavior: androidx.compose.material3.TopAppBarScrollBehavior?
public var preferCenterAlignedStyle: Bool
public var preferLargeStyle: Bool
}
```
Note that `.material3TopAppBar` involves API that Compose deems experimental, so you must add the following to any Skip Fuse `ContentModfifier` or Skip Lite `View` where you use it:
```swift
// SKIP INSERT: @OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class)
struct MyContentModifier : ContentModifier {
...
}
```
### Material Effects
Compose applies an automatic "ripple" effect to components on tap. You can customize the color and alpha of this effect with the `material3Ripple` modifier. To disable the effect altogether, return `nil` from your modifier closure.
```swift
extension View {
public func material3Ripple(_ options: @Composable (Material3RippleOptions?) -> Material3RippleOptions?) -> some View
}
public struct Material3RippleOptions {
public var color: androidx.compose.ui.graphics.Color = androidx.compose.ui.graphics.Color.Unspecified
public var rippleAlpha: androidx.compose.material.ripple.RippleAlpha? = nil
}
```
## Supported SwiftUI
The following table summarizes SkipUI's SwiftUI support on Android. Anything not listed here is likely not supported. Note that in your iOS-only code - i.e. code within `#if !os(Android)` blocks - you can use any SwiftUI you want.
Support levels:
- ✅ – Full
- 🟢 – High
- 🟡 – Medium
- 🟠 – Low
SupportAPI
🟢
@AppStorage (example)
- Optional values are not supported
✅
@Bindable
✅
@Binding (example)
✅
@Environment (example)
- See Environment Keys
✅
@EnvironmentObject (example)
🟢
@FocusState
-
FocusState.Bindingis not supported, though you can manually create aBinding(get: { myFocusState.wrappedValue }, set: { myFocusState.wrappedValue = $0 })
🟡
@GestureState
- Only supported in SkipFuseUI compiled Swift
✅
@ObservedObject (example)
✅
@State (example)
✅
@StateObject (example)
✅
AsyncImage (example)
✅
Button (example)
✅
Capsule (example)
✅
Circle (example)
🟢
Color (example)
init(red: Double, green: Double, blue: Double, opacity: Double = 1.0)init(white: Double, opacity: Double = 1.0)init(hue: Double, saturation: Double, brightness: Double, opacity: Double = 1.0)init(_ color: UIColor)init(uiColor: UIColor)init(_ name: String, bundle: Bundle? = nil)static let accentColor: Colorstatic let primary: Colorstatic let secondary: Colorstatic let clear: Colorstatic let white: Colorstatic let black: Colorstatic let gray: Colorstatic let red: Colorstatic let orange: Colorstatic let yellow: Colorstatic let green: Colorstatic let mint: Colorstatic let teal: Colorstatic let cyan: Colorstatic let blue: Colorstatic let indigo: Colorstatic let purple: Colorstatic let pink: Colorstatic let brown: Colorfunc opacity(_ opacity: Double) -> Colorvar gradient: AnyGradient- See Colors
🟡
DatePicker (example)
init(selection: Binding<Date>, displayedComponents: DatePickerComponents = [.hourAndMinute, .date], @ViewBuilder label: () -> any View)init(_ title: String, selection: Binding<Date>, displayedComponents: DatePickerComponents = [.hourAndMinute, .date])- Date range constraints (
in: ClosedRange<Date>) are supported via Skip Fuse bridging
🟡
DisclosureGroup (example)
init(isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> any View, @ViewBuilder label: () -> any View)init(_ titleKey: LocalizedStringKey, isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> any View)init(_ titleResource: LocalizedStringResource, isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> any View)init(_ label: String, isExpanded: Binding<Bool>, @ViewBuilder content: @escaping () -> any View)- Does not animate when used as a
ListorFormitem - Always animates when **not** used as a
ListorFormitem
✅
Divider (example)
🟢
DragGesture (example)
- See Gestures
🟡
EllipticalGradient (example)
- Fills as a circular gradient instead of elliptical unless the gradient is used as its own
View
✅
EmptyModifier
✅
EmptyView
🟡
Font
static let largeTitle: Fontstatic let title: Fontstatic let title2: Fontstatic let title3: Fontstatic let headline: Fontstatic let subheadline: Fontstatic let body: Fontstatic let callout: Fontstatic let footnote: Fontstatic let caption: Fontstatic let caption2: Fontstatic func system(_ style: Font.TextStyle, design: Font.Design? = nil, weight: Font.Weight? = nil) -> Fontstatic func system(size: CGFloat, weight: Font.Weight? = nil, design: Font.Design? = nil) -> Fontstatic func custom(_ name: String, size: CGFloat) -> Fontstatic func custom(_ name: String, size: CGFloat, relativeTo textStyle: Font.TextStyle) -> Fontstatic func custom(_ name: String, fixedSize: CGFloat) -> Fontfunc italic(_ isActive: Bool = true) -> Fontfunc weight(_ weight: Font.Weight) -> Fontfunc bold(_ isActive: Bool = true) -> Fontfunc monospaced(_ isActive: Bool = true) -> Fontfunc pointSize(_ size: CGFloat) -> Fontfunc scaledBy(_ factor: CGFloat) -> Font
🟢
ForEach
- See ForEach
✅
Form (example)
🟡
GeometryProxy
var size: CGSizefunc frame(in coordinateSpace: some CoordinateSpaceProtocol) -> CGRectvar safeAreaInsets: EdgeInsets- Only
.localand.globalcoordinate spaces are supported
🟡
GeometryReader
- See
GeometryProxy
✅
Group
✅
HStack (example)
🟢
Image (example)
init(_ name: String, bundle: Bundle? = Bundle.main)init(_ name: String, bundle: Bundle? = Bundle.main, label: Text)init(systemName: String)init(uiImage: UIImage)- See Images
🟢
Label (example)
init(@ViewBuilder title: () -> any View, @ViewBuilder icon: () -> any View)init(_ title: String, systemImage: String)init(_ title: String, image: String)- See Images
🟡
LazyHGrid
- See Grids
🟡
LazyHStack
- Does not support pinned headers and footers
- When placed in a
ScrollView, it must be the only child of that view
🟡
LazyVGrid
- See Grids
🟡
LazyVStack
- Does not support pinned headers and footers
- When placed in a
ScrollView, it must be the only child of that view
✅
LinearGradient (example)
✅
Link (example)
🟢
List (example)
- See Lists
🟢
LongPressGesture (example)
- See Gestures
🟢
MagnifyGesture (example)
- See Gestures
✅
Menu (example)
🟢
NavigationLink (example)
- See Navigation
🟢
NavigationPath
init()init(_ elements: any Sequence)var count: Intvar isEmpty: Boolmutating func append(_ value: Any)mutating func removeLast(_ k: Int = 1)- Does not support `codable` property
🟢
NavigationStack (example)
- See Navigation
✅
Oval (example)
✅
Picker (example)
🟢
ProgressView (example)
init()init(value: Double?, total: Double = 1.0)init(@ViewBuilder label: () -> any View)init(_ titleKey: LocalizedStringKey)init(_ titleResource: LocalizedStringResource)init(_ title: String)init(value: Double?, total: Double = 1.0, @ViewBuilder label: () -> any View)init(_ titleKey: LocalizedStringKey, value: Double?, total: Double = 1.0)init(_ titleResource: LocalizedStringResource, value: Double?, total: Double = 1.0)init(_ title: String, value: Double?, total: Double = 1.0)
✅
RadialGradient (example)
✅
Rectangle (example)
🟢
RotateGesture (example)
- See Gestures
✅
RoundedRectangle (example)
✅
ScrollView (example)
✅
ScrollView
- See Scrolling
🟡
ScrollViewProxy
- See Scrolling
🟡
ScrollViewReader
- See Scrolling
🟢
Section (example)
- See Lists
✅
SecureField (example)
🟠
ShareLink (example)
- Supports sharing
StringorURLdata only
🟡
Slider (example)
init(value: Binding<Double>, in bounds: ClosedRange<Double> = 0.0...1.0, step: Double? = nil)init(value: Binding<Double>, in bounds: ClosedRange<Double> = 0.0...1.0, step: Double? = nil, @ViewBuilder label: () -> any View)init(value: Binding<Double>, in bounds: ClosedRange<Double> = 0.0...1.0, step: Double? = nil, onEditingChanged: (Bool) -> Void)init(value: Binding<Double>, in bounds: ClosedRange<Double> = 0.0...1.0, step: Double? = nil, @ViewBuilder label: () -> any View, onEditingChanged: (Bool) -> Void)
🟢
Spacer (example)
- In Compose, when multiple elements want to expand they will share the available space equally
🟡
Stepper
init(value: Binding<Int>, step: Int = 1, @ViewBuilder label: () -> any View)-
init(value: Binding<Int>, in bounds: ClosedRange<Int>, step: Int = 1, @ViewBuilder label: () -> any View)(Fuse bridging) init(value: Binding<Double>, step: Double = 1.0, @ViewBuilder label: () -> any View)-
init(value: Binding<Double>, in bounds: ClosedRange<Double>, step: Double = 1.0, @ViewBuilder label: () -> any View)(Fuse bridging) init(_ title: String, value: Binding<Int>, step: Int = 1)-
init(_ title: String, value: Binding<Int>, in bounds: ClosedRange<Int>, step: Int = 1)(Fuse bridging) init(_ title: String, value: Binding<Double>, step: Double = 1.0)-
init(_ title: String, value: Binding<Double>, in bounds: ClosedRange<Double>, step: Double = 1.0)(Fuse bridging) init(_ title: String, onIncrement: (() -> Void)?, onDecrement: (() -> Void)?)init(@ViewBuilder label: () -> any View, onIncrement: (() -> Void)?, onDecrement: (() -> Void)?)
🟢
Tab (example)
- See Navigation
🟠
Table
init(_ data: any RandomAccessCollection<ObjectType>, @ViewBuilder content: () -> some View)init(_ data: any RandomAccessCollection<ObjectType>, selection: Binding<ObjectType?>, @ViewBuilder columns: () -> some View)init(_ data: any RandomAccessCollection<ObjectType>, selection: Binding<Set<ObjectType>>, @ViewBuilder columns: () -> some View)- All
TableColumnsmust be directly nested in the parentTablecontent block - Multiple selection is not supported
🟠
TableColumn
init(_ title: String, value: (ObjectType) -> String, comparator: Comparator? = nil)init(_ title: String, value: Value? = nil, comparator: Comparator? = nil, @ViewBuilder content: @escaping (ObjectType) -> some View)init(_ title: Text, value: (ObjectType) -> String, comparator: Comparator? = nil)init(_ title: Text, value: Value? = nil, comparator: Comparator? = nil, @ViewBuilder content: @escaping (ObjectType) -> some View)func width(_ value: CGFloat? = nil)func width(min: CGFloat? = nil, ideal: CGFloat? = nil, max: CGFloat? = nil)
🟠
TabSection
- See Navigation
🟢
TabView (example)
- See Navigation
🟢
TapGesture (example)
- See Gestures
🟢
Text (example)
-
Text(...) + Text(...)is not supported - For formatters, only
Text.DateStyle.dateandText.DateStyle.timeare supported
🟢
TextEditor
-
.font,.lineSpacing, etc modifiers have no effect
🟢
TextField (example)
init(_ title: String, text: Binding<String>, selection: Binding<TextSelection?>? = nil, prompt: Text? = nil)
🟡
Toggle (example)
init(isOn: Binding<Bool>, @ViewBuilder label: () -> any View)
🟢
ToolbarContent
- All of the items in a given custom
ToolbarContentview must have the sameToolbarItemPlacement
✅
ToolbarItem (example)
✅
ToolbarItemGroup (example)
✅
ToolbarSpacer
✅
ToolbarTitleMenu
🟠
UIKit
- See Supported UIKit
✅
UnevenRoundedRectangle (example)
✅
Custom
Views
✅
Custom
ViewModifiers
✅
ViewThatFits
✅
VStack (example)
- See Layout
🟡
withAnimation
- See Animation
✅
ZStack (example)
🟢
.accessibilityAddTraits
- Only traits that map to Compose accessibility roles are used
✅
.accessibilityHeading
✅
.accessibilityHidden
✅
.accessibilityIdentifier
✅
.accessibilityLabel
✅
.accessibilityValue
✅
.alert
✅
.allowsHitTesting
🟡
.animation
- See Animation
🟡
.aspectRatio
-
contentModeis only supported for images
✅
.autocorrectionDisabled
🟢
.background (example)
- See Safe Area
✅
.backgroundStyle
🟡
.badge
- Supported on
Listitems - Not yet supported on
TabView
🟢
.blendMode
-
.plusDarkerand.plusLighterboth map to ComposePlus
🟢
.blur
-
opaqueparameter is ignored
✅
.bold
✅
.border (example)
✅
.brightness
🟢
.buttonStyle
.automatic.plain.borderless.bordered.borderedProminent- Custom styles are not supported
🟡
.clipped
- Most content in Compose clips automatically
✅
.clipShape
✅
.colorScheme
- See also ColorScheme
✅
.colorMultiply
✅
.confirmationDialog (example)
✅
.contrast
✅
.cornerRadius
🟠
.datePickerStyle
.automatic.compact- Custom styles are not supported
✅
.deleteDisabled
✅
.disabled
🟢
.drawingGroup
-
opaqueandcolorModeparameters are ignored
✅
.environment
✅
.environmentObject
✅
.fill
✅
.focused
✅
.flipsForRightToLeftLayoutDirection
✅
.font (example)
✅
.foregroundColor
✅
.foregroundStyle
🟢
.frame (example)
- See Layout
🟢
.fullScreenCover
func fullScreenCover(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> any View) -> some View- See Modals
🟢
.gesture (example)
- See Gestures
✅
.gradient (example)
✅
.grayscale
✅
.hidden
✅
.hueRotation
🟢
.ignoresSafeArea
- See Safe Area
✅
.inset
✅
.interactiveDismissDisabled
- See Modals
✅
.italic
✅
.keyboardType (example)
✅
.labelsHidden
✅
.labelStyle
.automatic.titleOnly.iconOnly.titleAndIcon- Custom styles are not supported
🟢
.lineLimit
- func lineLimit(_ number: Int?) -> some View
- func lineLimit(_ number: Int, reservesSpace: Bool) -> some View
✅
.linespacing
✅
.listItemTint
✅
.listRowBackground
✅
.listRowSeparator
✅
.listStyle
✅
.luminanceToAlpha
✅
.mask
✅
.minimumScaleFactor
✅
.modifier (example)
✅
.monospaced
✅
.moveDisabled
✅
.multilineTextAlignment
✅
.navigationBarBackButtonHidden
✅
.navigationBarTitleDisplayMode
✅
.navigationDestination
🟢
.navigationTitle
func navigationTitle(_ title: String) -> some Viewfunc navigationTitle(_ title: Text) -> some View
✅
.offset (example)
✅
.onAppear
✅
.onChange
✅
.onDelete
✅
.onDisappear
🟢
.onLongPressGesture (example)
- See Gestures
✅
.onMove
✅
.onOpenURL
✅
.onPreferenceChange
✅
.onReceive
✅
.onSubmit
🟢
.onTapGesture
- See Gestures
✅
.opacity
✅
.overlay (example)
✅
.padding
🟡
.pickerStyle
.automatic.navigationLink.menu.segmented- Custom styles are not supported
✅
.position
✅
.preference
✅
.preferredColorScheme
- See also ColorScheme
🟢
.progressViewStyle
.automatic.linear.circular- Custom styles are not supported
🟡
.redacted
- Only
RedactionReasons.placeholderis supported
✅
.refreshable
🟠
.resizable
func resizable() -> Image
🟢
.rotation
func rotation(_ angle: Angle) -> any Shape
✅
.rotationEffect
func rotationEffect(_ angle: Angle) -> some Viewfunc rotationEffect(_ angle: Angle, anchor: UnitPoint) -> some View
🟢
.rotation3DEffect
func rotation3DEffect(_ angle: Angle, axis: (x: CGFloat, y: CGFloat, z: CGFloat), perspective: CGFloat = 1.0) -> some View
✅
.saturation
🟢
.scale
func scale(_ scale: CGFloat) -> any Shapefunc scale(x: CGFloat = 1.0, y: CGFloat = 1.0) -> any Shape
🟡
.scaledToFill
- Only supported for images
🟡
.scaledToFit
- Only supported for images
✅
.scaleEffect
func scaleEffect(_ scale: CGSize, anchor: UnitPoint = .center) -> some Viewfunc scaleEffect(_ s: CGFloat, anchor: UnitPoint = .center) -> some Viewfunc scaleEffect(x: CGFloat = 1.0, y: CGFloat = 1.0, anchor: UnitPoint = .center) -> some View
✅
.scrollContentBackground
🟢
.scrollDismissesKeyboard
- In Compose, the default behavior (
.automatic) is to never dismiss on scroll -
.interactivelybehaves like.immediately
🟠
.scrollTargetBehavior
- Only
.viewAlignedis supported - See Scrolling
🟠
.scrollTargetLayout
- See Scrolling
🟡
.searchable (example)
func searchable(text: Binding<String>, prompt: Text? = nil) -> some Viewfunc searchable(text: Binding<String>, prompt: String) -> some View
🟢
.shadow (example)
- Place this modifier before
.background,.overlaymodifiers
🟢
.sheet (example)
func sheet(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> any View) -> some View- See Modals
🟢
.strikethrough
-
patternandcolorare ignored
✅
.stroke
✅
.strokeBorder
✅
.submitLabel
✅
.symbolVariant
✅
.tabItem
- See Navigation
✅
.tabViewStyle
.automatic.page.tabBarOnly- Custom styles are not supported
✅
.tag
✅
.task
✅
.textCase
🟢
.textContentType
✅
.textEditorStyle
🟡
.textFieldStyle
.automatic.roundedBorder
✅
.textInputAutocapitalization
✅
.tint
🟢
.toolbar
func toolbar(@ViewBuilder content: () -> any View) -> some Viewfunc toolbar(_ visibility: Visibility, for bars: ToolbarPlacement...) -> some View
✅
.toolbarBackground
✅
.toolbarColorScheme
✅
.toolbarTitleDisplayMode
✅
.toolbarTitleMenu
✅
.tracking
🟢
.transition
- See Animation
🟢
.underline
-
patternandcolorare ignored
✅
.truncationMode
✅
.zIndex (example)
## Supported UIKit
SkipUI does not support UIKit views themselves, but it does support a subset of the UIKit framework, such as the pasteboard and haptic feedback classes, that act as interfaces to the underlying services on Android.
The following table summarizes SkipUI's UIKit support on Android. Anything not listed here is likely not supported. Note that in your iOS-only code - i.e. code within `#if !SKIP` blocks - you can use any UIKit you want.
Support levels:
- ✅ – Full
- 🟢 – High
- 🟡 – Medium
- 🟠 – Low
SupportAPI
🟠
UIApplication
static var shared: UIApplicationvar applicationState: UIApplication.Statevar isIdleTimerDisabled: Boolfunc open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey : Any] = [:]) async -> Bool
✅
#colorLiteral()
🟠
UIColor
init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat)
🟠
UIImage
init?(contentsOfFile: String)init?(data: Data)init?(data: Data, scale: CGFloat)
✅
UIImpactFeedbackGenerator
✅
UINotificationFeedbackGenerator
🟠
UIPasteboard
static var general: UIPasteboardstatic var changedNotification: Notification.Namevar numberOfItems: Intvar hasStrings: Boolvar string: String?var strings: [String]?var hasURLs: Boolvar url: URL?var urls: [URL]?
✅
UISelectionFeedbackGenerator
## Supported UserNotifications
Skip integrates its support for the UserNotifications framework into SkipUI.
**Important:** on Android devices to properly display notification icons in the status bar they must have specific properites. The icon must have a transparent background and a solid shape (usually white). It's recommended to use a specific icon for that and it must be specified in the AndroidManifest.xml with the following code inside the application tag:
```xml
```
SKIP assume that the icon is called `ic_notification` and it's available inside the drawable resources. Otherwise it will fall back to use the app icon (eg. @mipmap/ic_launcher). Note that in this case it will most likely be displayed as solid white dot instead of the usual app icon.
The following table summarizes SkipUI's UserNotifications support on Android. Anything not listed here is likely not supported. Note that in your iOS-only code - i.e. code within `#if !SKIP` blocks - you can use any UserNotifications API you want.
Support levels:
- ✅ – Full
- 🟢 – High
- 🟡 – Medium
- 🟠 – Low
SupportAPI
🟡
UNAuthorizationOptions
- Ignored on Android
🟡
UNMutableNotificationContent
- See `UNNotificationContent`
✅
UNNotification
🟡
UNNotificationContent
- Only `title`, `body`, `userInfo`, and `public.image`-type attachments are used
🟡
UNNotificationPresentationOptions
- Only `.banner` and `.alert` are used
✅
UNNotificationRequest
✅
UNNotificationResponse
🟡
UNNotificationSound
- Ignored on Android
🟡
UNNotificationTrigger
- Ignored on Android
🟠
UNUserNotificationCenter
static func current() -> UNUserNotificationCenterfunc requestAuthorization(options: UNAuthorizationOptions) async throws -> Boolvar delegate: (any UNUserNotificationCenterDelegate)?func add(_ request: UNNotificationRequest) async throws- The `add` function ignores all scheduling and repeat options and simply delivers the notification immediately.
✅
UNUserNotificationCenterDelegate
## Topics
### Animation
Skip supports SwiftUI's `.animation` and `.transition` modifiers as well as its `withAnimation` function on Android.
The following properties are currently animatable:
- `.background` color
- `.border` color
- `.fill` color
- `.font` size
- `.foregroundColor`
- `.foregroundStyle` color
- `.frame` width and height
- `.offset`
- `.opacity`
- `.rotationEffect`
- `.scaleEffect`
- `.stroke` color
All of SwiftUI's built-in transitions are supported on Android. To use transitions or to animate views being added or removed in general, however, you **must** assign a unique `.id` value to every view in the parent `HStack`, `VStack`, or `ZStack`:
```swift
VStack {
FirstView()
.id(100)
if condition {
SecondView()
.transition(.scale)
.id(200)
}
}
.animation(.default)
```
Skip converts the various SwiftUI animation types to their Compose equivalents. For many SwiftUI spring animations, though, Skip uses Compose's simple `EaseInOutBack` easing function rather than a true spring. Only constructing a spring with `SwiftUI.Spring(mass:stiffness:damping:)` creates a true Compose spring animation. Using an easing function rather than a true spring allows us to overcome Compose's limitations on springs:
- True spring animations cannot set a duration
- True spring animations cannot have a delay
- True spring animations cannot repeat
Custom `Animatables` and `Transitions` are not supported. Finally, if you nest `withAnimation` blocks, Android will apply the innermost animation to all block actions.
### Colors
#### Accent Color
In addition to programmatically using SwiftUI's `.tint` modifier, iOS allows you to set your application's accent color via the `AccentColor` resource in your app's `Assets` asset catalog. In a Skip app, you can find `Assets` in the `Darwin` folder.
Skip also supports these mechanisms, but your generated Android app can't access the `Darwin` folder contents. To define an accent color resource for your Android app, create a color set called `AccentColor` in the `Sources//Resources/Module` asset catalog. See the section on Named Colors below for additional details.
#### Named Colors
Named colors can be bundled in asset catalogs provided in the `Resources` folder of your SwiftPM modules. Your `Package.swift` project should have the module's `.target` include the `Resources` folder for resource processing (which is the default for projects created with `skip init`):
```swift
.target(name: "MyModule", dependencies: ..., resources: [.process("Resources")], plugins: skipstone)
```
Once an asset catalog is added to your `Resources` folder, any named colors can be loaded and displayed using the `Color(_:bundle:)` constructor. For example:
```swift
Color("WarningYellow", bundle: .module)
```
> [!TIP]
> Your named colors must use Xcode's "Floating point (0.0-1.0)" input method. You can convert named colors using other methods by selecting them in Xcode and using the UI picker to update the input method. The values will be preserved.
See the [Skip Showcase app](https://github.com/skiptools/skipapp-showcase) `ColorPlayground` for a concrete example of using a named color in an asset catalog, and see that project's Xcode project file ([screenshot](https://assets.skip.dev/screens/SkipUI_Asset_Image.png)) to see the configuration of the `.xcassets` file for the app module.
When an app project is first created with `skip init`, it will contain two separate asset catalogs: a project-level `Assets.xcassets` catalog that contains the app's icons, and an empty module-level `Module.xcassets` catalog. **Add your assets to `Module.xcassets`.** Only the module-level catalog will be transpiled, since the project-level catalog is not processed by the skip transpiler.
> [!NOTE]
> Note that you also **must** specify the `bundle` parameter for colors explicitly, since a Skip project uses per-module resources, rather than the default `Bundle.main` bundle that would be assumed of the parameter were omitted.
For Android, Skip only uses named colors that you've set for "Universal" devices. You can define the color using RGB values or use any of the "Universal System Color" constants.
### ColorScheme
SkipUI fully supports the `.preferredColorScheme` modifier. If you created your app with the `skip` tool prior to v0.8.26, however, you will have to update the included `Android/app/src/main/kotlin/.../Main.kt` file in order for the modifier to work correctly. Using the latest [`Main.kt`](https://github.com/skiptools/skipapp-hello/blob/main/Android/app/src/main/kotlin/hello/skip/Main.kt) as your template, please do the following:
1. Replace the all of the import statements with ones from latest `Main.kt`
1. Replace the contents of the `setContent { ... }` block with the content from the latest `Main.kt`
1. Replace the `MaterialThemeRootView()` function with the `PresentationRootView(context:)` function from the latest `Main.kt`
With these updates in place, you should be able to use `.preferredColorScheme` successfully.
### Custom Fonts
Custom fonts can be embedded and referenced using `Font.custom`. Fonts are loaded differently depending on the platform. On iOS the custom font name is the full Postscript name of the font, and on Android the name is the font's file name without the extension.
Android requires that font file names contain only alphanumeric characters and underscores, so you should manually name your embedded font to the lowercased and underscore-separated form of the Postscript name of the font. SkipUI's `Font.custom` call will accommodate this by translating your custom font name like "Protest Guerrilla" into an Android-compatible name like "protest_guerrilla.ttf".
```swift
Text("Custom Font")
.font(Font.custom("Protest Guerrilla", size: 30.0)) // protest_guerrilla.ttf
```
Custom fonts are embedded differently for each platform. On Android you should create a folder `Android/app/src/main/res/font/` and add the font file, which will cause Android to automatically embed any fonts in that folder as resources.
For iOS, you must add the font by adding to the Xcode project's app target and ensure the font file is included in the file list in the app target's "Build Phases" tab's "Copy Bundle Resources" phase. In addition, iOS needs to have the font explicitly listed in the Xcode project target's "Info" tab under "Custom Application Target Properties" by adding a new key for the "Fonts provided by application" (whose raw name is "UIAppFonts") and adding each font's file name to the string array.
See the [Skip Showcase app](https://github.com/skiptools/skipapp-showcase) `TextPlayground` for a concrete example of using a custom font, and see that project's Xcode project file ([screenshot](https://assets.skip.dev/screens/SkipUI_Custom_Font.png)) to see how the font is included on both the iOS and Android sides of the app.
### Environment Keys
SwiftUI has many built-in environment keys. These keys are defined in `EnvironmentValues` and typically accessed with the `@Environment` property wrapper. In additional to supporting your custom environment keys, SkipUI exposes the following built-in environment keys:
- `autocorrectionDisabled` (read-only)
- `backgroundStyle`
- `dismiss`
- `font`
- `horizontalSizeClass`
- `isEnabled`
- `isSearching` (read-only)
- `layoutDirection`
- `lineLimit`
- `locale`
- `openURL`
- `refresh`
- `scenePhase`
- `scrollDismissesKeyboardMode`
- `timeZone`
- `truncationMode`
- `verticalSizeClass`
### ForEach
The SwiftUI `ForEach` view allows you to generate views for a range or collection of content. SkipUI support any `Int` range or any `RandomAccessCollection`. If the collection elements do not implement the `Identifiable` protocol, specify the key path to a property that can be used to uniquely identify each element. These `id` values must follow our [Restrictions on Identifiers](#restrictions-on-identifiers).
```swift
ForEach([person1, person2, person3], id: \.fullName) { person in
HStack {
Text(person.fullName)
Spacer()
Text(person.age)
}
}
```
**Important**: When the body of your `ForEach` contains multiple top-level views (e.g. a full row of a `VGrid`), or any single view that expands to additional views (like a `Section` or a nested `ForEach`), SkipUI must "unroll" the loop in order to supply all its views individually to Compose. This means that the `ForEach` will be entirely iterated up front, though the views it produces won't yet be rendered.
### Gestures
SkipUI currently supports tap, long press, drag, magnify, and rotate gestures. You can use either the general `.gesture` modifier or the specialized modifiers like `.onTapGesture` to add gesture support to your views. The following limitations apply:
- `@GestureState` is only supported in Skip Fuse. Use the `Gesture.onEnded` modifier to reset your state.
- Tap counts > 2 are not supported.
- Gesture velocity and predicted end location are always reported as zero and the current location, respectively.
- Only the `onChanged` and `onEnded` gesture modifiers are supported.
- Customization of minimum touch duration, distance, etc. is generally not supported.
- When applying gestures to an offset view, place any gesture modifiers **before** the `.offset` modifier.
There is one exception to the last limitation: you **can** create a `DragGesture(minimumDistance: 0)` in order to detect touch down events immediately.
#### Shapes and Paths
SwiftUI automatically applies a mask to shapes and paths so that touches outside the shape do not trigger its gestures. SkipUI emulates this feature, but it is **not** supported on custom shapes and paths that have a `.stroke` applied. These shapes will register touches anywhere in their bounds. Consider using `.strokeBorder` instead of `.stroke` when a gesture mask is needed on a custom shape.
### Grids
SkipUI renders SwiftUI grid views using native Compose grids. This provides maximum performance and a native feel on Android. The different capabilities of SwiftUI and Compose grids, however, imposes restrictions on SwiftUI grid support in Android:
- Pinned headers and footers are not supported.
- When you place a `LazyHGrid` or `LazyVGrid` in a `ScrollView`, it must be the only child of that view. (This is because Compose grids implement their own internal scroll containers.) To simulate having other non-grid items in your `ScrollView`, put your non-grid content in the `header`s of empty `Section` containers inside the grid.
```swift
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
Section {} header: {
Text("Insert non-grid content here")
}
ForEach(0..<50) { index in
Text("Grid Cell \(index)")
}
Section {} header: {
Text("More non-grid content here")
}
}
}
```
- When you define your grid with an array of `GridItem` specs, your Android grid is **based on the first `GridItem`**. Compose does not support different specs for different rows or columns, so SkipUI applies the first spec to all of them.
- Maximum `GridItem` sizes are ignored.
- Also see the `ForEach` [topic](#foreach).
### Haptics
SkipUI supports UIKit's `UIFeedbackGenerator` API for generating haptic feedback on the device, typically as a result of user interaction. Some examples are as follows:
```swift
// impact haptic feedback
UIImpactFeedbackGenerator(style: .light).impactOccurred()
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
UIImpactFeedbackGenerator().impactOccurred(intensity: 0.5)
// notification haptic feedback
UINotificationFeedbackGenerator().notificationOccurred(.success)
UINotificationFeedbackGenerator().notificationOccurred(.warning)
UINotificationFeedbackGenerator().notificationOccurred(.error)
// selection haptic feedback
UISelectionFeedbackGenerator().selectionChanged()
```
> [!TIP]
> Android requires adding a permission in order to be able to utilize the device's haptic feedback service (`android.content.Context.VIBRATOR_MANAGER_SERVICE`) by adding to the `Android/app/src/main/AndroidMetadata.xml` file's manifest section: ``
### Images
#### Network Images
SkipUI supports loading images from network URLs using SwiftUI's `AsyncImage`. Our implementation uses the [Coil](https://coil-kt.github.io/coil/) library to download images on Android. This includes support for a loading indicator, such as:
```swift
AsyncImage(url: URL(string: "https://picsum.photos/id/237/200/300")) { image in
image.resizable()
} placeholder: {
ProgressView()
}
```
#### Image Assets
Images can be bundled in asset catalogs provided in the `Resources` folder of your SwiftPM modules. Your `Package.swift` project should have the module's `.target` include the `Resources` folder for resource processing (which is the default for projects created with `skip init`):
```swift
.target(name: "MyModule", dependencies: ..., resources: [.process("Resources")], plugins: skipstone)
```
Once an asset catalog is added to your `Resources` folder, any bundled images can be loaded and displayed using the `Image(name:bundle:)` constructor. For example:
```swift
Image("Cat", bundle: .module, label: Text("Cat JPEG image"))
```
> [!WARNING]
> When an app project is first created with `skip init`, it will contain two separate asset catalogs: a project-level `Assets.xcassets` catalog that contains the app's icons, and an empty module-level `Module.xcassets` catalog. **Add your assets to `Module.xcassets`.** Only the module-level catalog will be transpiled, since the project-level catalog is not processed by the skip transpiler.
See the [Skip Showcase app](https://github.com/skiptools/skipapp-showcase) `ImagePlayground` for a concrete example of using a bundled image in an asset catalog, and see that project's Xcode project file ([screenshot](https://assets.skip.dev/screens/SkipUI_Asset_Image.png)) to see the configuration of the `.xcassets` file for the app module.
> [!NOTE]
> Note that you **must** specify the `bundle` parameter for images explicitly, since a Skip project uses per-module resources, rather than the default `Bundle.main` bundle that would be assumed of the parameter were omitted.
In addition to raster image formats like .png and .jpg, vector images in the .svg and .pdf forma