This guide explains how to add new SwiftUI view modifiers to the Skip framework, bridging Swift/SwiftUI to Kotlin/Compose.
Adding a new modifier requires changes across three repositories:
| Repository | Purpose | Branch Pattern |
|---|---|---|
skip-ui |
Kotlin/Compose implementation | feature/<modifier>-modifier |
skip-fuse-ui |
Swift bridging layer | feature/<modifier>-modifier |
skipapp-showcase-fuse |
Test app/playground | feature/<modifier>-playground |
Sources/SkipUI/SkipUI/View/AdditionalViewModifiers.swift- Simple view modifiersSources/SkipUI/SkipUI/Text/Text.swift- Text-related modifiersSources/SkipUI/SkipUI/Compose/ComposeLayouts.swift- Layout composables (if needed)
Sources/SkipSwiftUI/View/AdditionalViewModifiers.swift- View modifier bridgesSources/SkipSwiftUI/Text/Text.swift- Text modifier bridges (check for existing stubs)
Sources/ShowcaseFuse/<Name>Playground.swift- New playground fileSources/ShowcaseFuse/PlaygroundListView.swift- Register the playground
skip-ui/AdditionalViewModifiers.swift:
// SKIP @bridge
public func blur(radius: CGFloat, opaque: Bool = false) -> any View {
#if SKIP
return ModifiedContent(content: self, modifier: RenderModifier { ... })
#else
return self
#endif
}skip-fuse-ui/AdditionalViewModifiers.swift:
extension View {
/* @inlinable */ nonisolated public func blur(radius: CGFloat, opaque: Bool = false) -> some View {
return ModifierView(target: self) {
$0.Java_viewOrEmpty.blur(radius: radius, opaque: opaque)
}
}
}For modifiers that affect text rendering, use the TextEnvironment pattern.
IMPORTANT: Text modifiers require changes in THREE places in skip-fuse-ui:
Text.swift- TheText.modifier()method (usesmodifierChain)AdditionalViewModifiers.swift- TheView.modifier()bridge (usesModifierView)
- Add field to
TextEnvironmentstruct:
struct TextEnvironment: Equatable {
var fontWeight: Font.Weight?
var tracking: CGFloat? // Add new field
// ...
}- Add the
Text.tracking()method that forwards to modifiedView:
public func tracking(_ tracking: CGFloat) -> Text {
return Text(textView: textView, modifiedView: modifiedView.tracking(tracking))
}- Add the View extension modifier with bridge:
// SKIP @bridge
public func tracking(_ tracking: CGFloat) -> any View {
#if SKIP
return textEnvironment(for: self) { $0.tracking = tracking }
#else
return self
#endif
}- Apply in the Render method:
if let tracking = textEnvironment.tracking {
options = options.copy(letterSpacing: tracking.sp)
}Critical: The Text.tracking() method must use modifierChain pattern (like bold, italic, underline):
nonisolated public func tracking(_ tracking: CGFloat) -> Text {
var text = self
text.modifierChain.append {
$0.tracking(tracking)
}
return text
}Common mistake: Just returning self compiles but doesn't apply the modifier!
// WRONG - compiles but does nothing on Android!
nonisolated public func tracking(_ tracking: CGFloat) -> Text {
return self
}// MARK: - Tracking
extension View {
/* @inlinable */ nonisolated public func tracking(_ tracking: CGFloat) -> some View {
return ModifierView(target: self) {
$0.Java_viewOrEmpty.tracking(tracking)
}
}
}skip-ui/AdditionalViewModifiers.swift:
// SKIP @bridge
public func mask(horizontalAlignmentKey: String, verticalAlignmentKey: String, bridgedMask: any View) -> any View {
#if SKIP
let alignment = Alignment(horizontal: HorizontalAlignment(key: horizontalAlignmentKey),
vertical: VerticalAlignment(key: verticalAlignmentKey))
return ModifiedContent(content: self, modifier: MaskModifier(alignment: alignment, mask: bridgedMask))
#else
return self
#endif
}skip-fuse-ui/AdditionalViewModifiers.swift:
extension View {
/* @inlinable */ nonisolated public func mask<Mask>(alignment: Alignment = .center, @ViewBuilder _ mask: () -> Mask) -> some View where Mask : View {
let maskView = mask()
return ModifierView(target: self) {
$0.Java_viewOrEmpty.mask(
horizontalAlignmentKey: alignment.horizontal.key,
verticalAlignmentKey: alignment.vertical.key,
bridgedMask: maskView.Java_viewOrEmpty
)
}
}
}- Place directly above the method that should be exposed to the bridge
- Method must return
any View(notsome View) - Parameter types must be bridgeable (primitives, String, or bridged types)
| Type | Convention | Example |
|---|---|---|
| Simple values | Direct pass-through | radius: CGFloat |
| Alignment | Decompose to keys | horizontalAlignmentKey: String |
| View parameters | Add "bridged" prefix | bridgedMask: any View |
| UnitPoint | Extract components | anchorX: CGFloat, anchorY: CGFloat |
| Enums | Use raw value or key | bridgedWeight: Int? |
Check both repositories for @available(*, unavailable) stubs:
- skip-ui: Remove the
@availableattribute, implement the modifier - skip-fuse-ui: Remove the stub entirely if implementing in AdditionalViewModifiers.swift
Sources/ShowcaseFuse/<Name>Playground.swift:
// Copyright 2023–2026 Skip
import SwiftUI
struct TrackingPlayground: View {
var body: some View {
ScrollView {
VStack(spacing: 16) {
HStack {
Text("Label")
Spacer()
Text("Example")
.tracking(5)
}
// More examples...
}
.padding()
}
.toolbar {
PlaygroundSourceLink(file: "TrackingPlayground.swift")
}
}
}Four changes required:
- Add enum case (alphabetical order):
case tracking- Add title:
case .tracking:
return LocalizedStringResource("Tracking", comment: "Title of Tracking playground")- Add body:
case .tracking:
TrackingPlayground()- Add to newPlaygrounds set (for "New only" filter):
private let newPlaygrounds: Set<PlaygroundType> = [
.mask,
.tracking
]- skip-ui: Implement modifier with
// SKIP @bridge - skip-ui: Add any required imports (e.g.,
import androidx.compose.ui.unit.sp) - skip-ui: Remove
@available(*, unavailable)if present - skip-fuse-ui: Add View bridge in AdditionalViewModifiers.swift
- skipapp-showcase-fuse: Create playground file
- skipapp-showcase-fuse: Update PlaygroundListView.swift (4 places)
- Test on both iOS and Android
- skip-ui/Text.swift: Add field to
TextEnvironmentstruct - skip-ui/Text.swift: Add
Text.modifier()method forwarding tomodifiedView - skip-ui/Text.swift: Add
View.modifier()with// SKIP @bridgeusingtextEnvironment(for:) - skip-ui/Text.swift: Apply value in
Rendermethod viaMaterial3TextOptions - skip-fuse-ui/Text.swift: Add
Text.modifier()usingmodifierChain.appendpattern - skip-fuse-ui/AdditionalViewModifiers.swift: Add View bridge with
ModifierView
| Modifier | Pattern | Key Files |
|---|---|---|
mask |
View parameter | AdditionalViewModifiers.swift, ComposeLayouts.swift |
tracking |
Text environment | Text.swift |
blur |
Simple modifier | AdditionalViewModifiers.swift |
fontWeight |
Text environment | Text.swift |
shadow |
Simple with multiple params | AdditionalViewModifiers.swift |