swiftcnearly preview

Native modifier plugins

Register real SwiftUI modifiers as classes and compose them with cn.

Register exact tags and bracket utilities through TWGlobalRules.modifiers. Each registration produces native SwiftUI views or modifiers. Use cn to compose the classes with ordinary Swift state.

A complete example

This view registers a material background and a native 3D rotation modifier. Its state changes the rotation argument, while the animation classes select native motion.

NativeRegistryExample.swift
import SwiftUI
import SwiftCN

struct NativeRegistryExample: View {
    @State private var expanded = false

    private var rules: TWGlobalRules {
        TWGlobalRules(modifiers: [
            "frosted": .view { view, active in
                view.background {
                    RoundedRectangle(cornerRadius: 20)
                        .fill(.ultraThinMaterial)
                        .opacity(active ? 1 : 0)
                }
            },
            "tilt": .value(default: 0.0, order: 10) { view, degrees in
                view.rotation3DEffect(
                    .degrees(degrees), axis: (x: 0, y: 1, z: 0)
                )
            }
        ])
    }

    var body: some View {
        VStack(spacing: 24) {
            Text("Room for ideas")
                .tw(cn(
                    "text-xl font-semibold p-6 frosted",
                    "tilt-[\(expanded ? 8.0 : 0.0)]",
                    "animate-[\(Animation.default)]"
                ), value: expanded)

            Button("Change perspective") { expanded.toggle() }
                .buttonStyle(.tw("button-primary"))
        }
        .padding(32)
        .twRules(rules)
    }
}

Use NativeRegistryExample() in your app or preview. The documentation compiler check compiles this complete file for macOS and iOS, with both installation methods.

Registration forms

RegistrationClassFactory
.view { view, active in ... }frostedTransform a view; use active to select neutral values when absent.
.modifier { active, theme in ... }liftedReturn one concrete native ViewModifier type for both states.
.value(default: 0.0) { view, degrees in ... }tilt-[\(angle)]Read a native value; standard scalar text also works.
.argument(default: 0.0, parse: ...) { view, degrees in ... }tilt-[8deg]Decode a typed value, then use native modifiers directly.
.argument(default: "0") { argument, theme in ... }tilt-[8deg]Return a modifier, or nil for an invalid argument.

The argument factory receives TWArgument and the current theme. Its helpers include number, points, degrees, seconds, and components. The default argument supplies the inactive modifier; zero degrees is neutral for rotation. Use one as the default for a scale modifier.

Typed parsers and modifier argument factories are pure, Sendable closures. They can run during strict parsing and rendering; do not perform effects in them. Native view transforms and exact modifier factories run on the main actor. They can capture native bindings for controls and gestures. Disable those behaviors when the tag is inactive.

Named recipes can contain registered classes. The existing hover:, focus:, active:, disabled:, and group variants also apply. Native button styles supply press state; other views require explicit TWState for press or focus.

Compose with cn

cn returns TWClasses and preserves native interpolation values. It accepts optional class arguments or an array of optional classes:

let classes = cn("card frosted", selected ? "border-primary" : nil)
let shared = cn(["frosted", "p-4", TWClasses(extraClasses)])

Its builder form accepts strings, arrays, conditions, and loops:

let classes = cn {
    "card frosted"
    sharedClasses
    if selected { "border-primary" }
    "tilt-[\(angle)]"
}

cn preserves order and does not remove conflicting classes. The resolver applies the same property and state precedence as other utilities. Bracket underscores represent spaces; escape literal underscores with \_. Use bracket interpolation to pass native values without converting them to text. Use TWClasses for stored class values, or use the builder for runtime String variables. See typed interpolation for composition and native animation examples. Bindings and actions stay in Swift factories.

Conflicts and application order

Each registered name has its own conflict key by default. Repeated arguments for that name select one modifier value. Set conflictKey: when different registrations represent the same effect:

"tilt": .argument(default: 0.0, conflictKey: "orientation",
    parse: { argument, _ in argument.degrees }) { view, degrees in
    view.rotation3DEffect(.degrees(degrees), axis: (x: 0, y: 1, z: 0))
}

Within the same state and specificity, the later class wins. State variants retain their usual priority over base classes. These rules select a winner; they do not rearrange the native modifier chain. Custom conflict keys apply within the native registry, separately from built-in styling properties.

Native modifiers run in their selected phase, from lower to higher order:. Equal orders use registration names in alphabetical order. The default phase is .effects, which retains the existing application order. The order stays fixed when class order, arguments, or active state change. Exact named recipes take precedence over registrations. Registered tags take precedence over built-in tags outside named recipes. Registered bracket prefixes take precedence over style utility factories and built-in decoration arguments. Shared-element classes retain their special grammar.

Choose the modifier phase

Register phase: to place a general view modifier around built-in styling:

PhasePosition
.contentAfter inherited text attributes, before padding and frames
.layoutAfter padding and frames, before backgrounds and borders
.decorationAfter backgrounds and borders, before opacity and transforms
.effectsAfter opacity, blur, scale, rotation, and offset; the default

Phase order takes priority over order:. Changing class order selects values; it does not rearrange the modifier chain. Use separate .tw(...) calls when you need another complete styling boundary.

let rules = TWGlobalRules(modifiers: [
    "inset": .value(default: CGFloat.zero, phase: .content) { view, amount in
        view.padding(amount)
    }
])
// The blue background includes both padding values.
Text("Inside").tw("inset-[8] p-4 bg-[#00f]").twRules(rules)

Keep native source methods

Use target-specific factories for methods available on Text, Image, or Shape. Text factories return Text; image factories return Image. Shape factories use SwiftUI's AnyShape and return AnyShape. These transformations run before general view modifiers and retain native source APIs.

NativeCompositionExample.swift
import SwiftUI
import SwiftCN

struct NativeCompositionExample: View {
    @State private var expanded = false

    private var rules: TWGlobalRules {
        TWGlobalRules(modifiers: [
            "kern": .textValue(default: CGFloat.zero) { text, points in
                text.kerning(points)
            },
            "resize": .image { image, active in
                active ? image.resizable() : image
            },
            "trim": .shapeValue(default: CGFloat(1)) { shape, end in
                AnyShape(shape.trim(from: 0, to: end))
            },
            "inset": .value(default: CGFloat.zero, phase: .content) { view, amount in
                view.padding(amount)
            }
        ])
    }

    var body: some View {
        VStack(spacing: 20) {
            Text("Native source methods")
                .tw(cn("kern-[1.5]", "inset-[8] bg-accent rounded-md"))
            Image(systemName: "bird.fill")
                .tw("resize w-[60] h-[40] text-primary")
            Circle().tw("trim-[0.7] w-[40] h-[40] text-primary")
            Button("Resize") { expanded.toggle() }
                .buttonStyle(.tw("button-primary"))
        }
        .tw("p-6 rounded-xl border w-[\(CGFloat(expanded ? 320 : 260))] animate-spring",
            value: expanded, animationScope: .layout)
        .twRules(rules)
    }
}

Use .text, .image, or .shape for an exact tag. Use .textValue(default:), .imageValue(default:), or .shapeValue(default:) for a typed bracket argument. Target factories support state variants, recipes, typed interpolation, and conflict keys. Their order is fixed before general .content registrations.

Apply target-specific tags directly to the native source, before .padding, .frame, or another view wrapper. For example, Image(...).padding().tw("resize") reports an unsupported target. Validate this in tooling with TWStyle.parse(classes, rules: rules, target: .image). An invalid target leaves the complete surface undecorated. Unselected registrations for other targets do not run on the source.

AnyShape exposes general shape methods; it does not retain methods unique to a concrete shape or InsettableShape. Apply those methods in native Swift before .tw. Unselected Text and Image registrations leave the source unchanged. This prevents an inactive text attribute from masking the selected conflict winner. Their factories run only for selected tags, with active set to true. The typed default parameters remain available for source compatibility. Shape registrations retain an inactive transformation to preserve their erased animation structure. Their defaults must preserve the intended appearance when their tags are absent.

Keep state and animation native

Every explicit .tw surface retains a slot for each inherited general view registration, even when its tag is absent. Text and Image registrations run only when selected on their matching native sources. Shape registrations retain inactive slots on native shapes. An inactive slot applies its neutral modifier instead of removing a wrapper. This keeps child state and modifier structure stable during class changes. Scope large registries to the subtree that needs them.

Keep the same concrete modifier type for each registration. In .view factories, change modifier values rather than branching between differently wrapped content. General view and shape inactive factories must leave layout, appearance, and interaction unchanged. Its default argument must remain valid in every inherited theme.

Changing the registry's names, order, or returned view structure changes the chain and can reset child state. Treat those changes as structural configuration changes. The runtime registry uses type erasure; it cannot preserve identity through arbitrary structural changes inside a factory.

Animation classes apply to the native modifiers in these slots. Use .tw(classes, value: state) when child layout or content must also animate. Without a preset, swiftcn preserves the caller's transaction, including native withAnimation(.default). Custom modifiers still need animatable properties or an Animatable implementation for interpolation. Explicit swiftcn presets respect Reduce Motion and disabled animation transactions.

Strict parsing rejects unregistered tags and invalid arguments. It also checks string inactive defaults for selected modifier factories. Typed inactive values must be safe for their native API. One invalid class leaves the complete surface undecorated, with native slots inactive. See troubleshooting for diagnostics and animations for transaction scope.

On this page