Field
Native field composition, package imports, and editable Swift source.
Preview


Use it
Import SwiftCN to use CNField directly. This complete example also runs in the native component gallery.
import SwiftUI
import SwiftCN
struct FieldExample: View {
@State private var text = "Design system"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
CNField(isInvalid: text.isEmpty) {
CNFieldLabel("Workspace name")
CNInput("Name", text: $text)
CNFieldDescription("A name your team will recognize.")
if text.isEmpty { CNFieldError("Enter a workspace name.") }
}
}
}
}Native behavior
Imported and copied parts share the public validation environment. Native controls retain their labels, bindings, focus, and accessibility. Use native labels or accessibilityLabel to associate field meaning. CNField defaults to a vertical layout. Set axis: .horizontal to place the label beside a grouped control and its supporting text. Choose the axis from native size information. AnyLayout keeps the child views mounted when the axis changes; .tw("feedback-motion", value: axis) supplies optional motion.
Compose with native SwiftUI
See complete native composition examples for direct native controls, caller-owned sheets, navigation, and desktop split views. The examples use the same editable recipes.
Style and compose
Visual recipes use .tw classes. Append classes: to override a recipe locally, or change the recipe in TWGlobalRules.named for your subtree. Native layout and interaction modifiers remain SwiftUI APIs. For control colors, append cn-tint-[primary], another theme token, or an interpolated Color.
Motion recipes are editable too: control-motion uses a 150 ms smooth response, disclosure-motion uses a 250 ms spring, and feedback-motion uses a 200 ms smooth response. Set a recipe to animate-none in your global rules to turn it off. Presets respect Reduce Motion. Static components stay still; native scrolling, drag tracking, and platform presentation keep their own gestures.
Action groups and card, dialog, and questionnaire footers stack when their content does not fit. The native layout preserves the same child controls during this change.
View builders preserve native child content. Native Button roles, labels, keyboard shortcuts, submit actions, and presentation modifiers still work. A string recipe does not replace a native control API or a typed chart mark.
Own the source
Copy the complete file below into your app alongside import SwiftCN. The file contains related composable parts from the same family. Its dependencies use public package APIs, so copied and imported components can coexist.
To own the styling engine too, download the complete source bundle. It contains every component, the .tw engine, its recipes, and the MIT license. Keep those files in one module and remove import SwiftCN from the usage example.
import SwiftUI
import SwiftCN
/// Group native controls and their descriptions without replacing their accessibility elements.
public struct CNField<Content: View>: View {
private let isInvalid: Bool
private let classes: TWClasses
private let spacing: CGFloat
private let axis: Axis
private let content: Content
/// Group the control and supporting text in one child when the label sits beside them.
/// Changing the axis keeps the same child views and their native control state.
public init(isInvalid: Bool = false, classes: TWClasses = "", spacing: CGFloat = 6, axis: Axis = .vertical, @ViewBuilder content: () -> Content) {
self.isInvalid = isInvalid
self.classes = classes
self.spacing = spacing
self.axis = axis
self.content = content()
}
public var body: some View {
let layout = axis == .horizontal
? AnyLayout(HStackLayout(alignment: .top, spacing: spacing))
: AnyLayout(VStackLayout(alignment: .leading, spacing: spacing))
layout { content }
.frame(maxWidth: .infinity, alignment: .leading)
.tw(cn("field", classes))
.environment(\.twFieldInvalid, isInvalid)
}
}
/// Group independent fields. Each CNField owns its own validation appearance.
public struct CNFieldGroup<Content: View>: View {
private let classes: TWClasses
private let spacing: CGFloat
private let content: Content
public init(_ classes: TWClasses = "", spacing: CGFloat = 20, @ViewBuilder content: () -> Content) {
self.classes = classes
self.spacing = spacing
self.content = content()
}
public var body: some View {
VStack(alignment: .leading, spacing: spacing) { content }
.frame(maxWidth: .infinity, alignment: .leading)
.tw(cn("field-group", classes))
}
}
/// Decorate an arbitrary native control; its label, binding, and behavior remain native.
public struct CNFieldControl<Content: View>: View {
private let style: TWStyle
private let state: TWState
private let content: Content
public init(_ classes: TWClasses = "input", state: TWState = TWState(), @ViewBuilder content: () -> Content) {
style = .classes(classes)
self.state = state
self.content = content()
}
public init(_ first: TWStyle, _ styles: TWStyle..., state: TWState = TWState(), @ViewBuilder content: () -> Content) {
style = TWStyle([first] + styles)
self.state = state
self.content = content()
}
public var body: some View { content.modifier(TWFieldControlModifier(style: style, state: state)) }
}
/// A field part that accepts either localized text or custom native content.
public struct CNFieldLabel<Content: View>: View {
private let classes: TWClasses
private let content: Content
public init(_ classes: TWClasses = "", @ViewBuilder content: () -> Content) {
self.classes = classes
self.content = content()
}
public init(_ title: LocalizedStringKey, classes: TWClasses = "") where Content == Text {
self.classes = classes
content = Text(title)
}
public var body: some View { content.tw(cn("field-label", classes)) }
}
/// A field part that accepts either localized text or custom native content.
public struct CNFieldDescription<Content: View>: View {
private let classes: TWClasses
private let content: Content
public init(_ classes: TWClasses = "", @ViewBuilder content: () -> Content) {
self.classes = classes
self.content = content()
}
public init(_ title: LocalizedStringKey, classes: TWClasses = "") where Content == Text {
self.classes = classes
content = Text(title)
}
public var body: some View { content.tw(cn("field-description", classes)) }
}
/// A field part that accepts either localized text or custom native content.
public struct CNFieldError<Content: View>: View {
private let classes: TWClasses
private let content: Content
public init(_ classes: TWClasses = "", @ViewBuilder content: () -> Content) {
self.classes = classes
self.content = content()
}
public init(_ title: LocalizedStringKey, classes: TWClasses = "") where Content == Text {
self.classes = classes
content = Text(title)
}
public var body: some View { content.tw(cn("field-error", classes)) }
}