一键导入
write-uikit-ui-code-declaratively-with-construkt
Guidelines for generating declarative UIKit code using the Construkt framework (SwiftUI syntax for UIKit).
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Guidelines for generating declarative UIKit code using the Construkt framework (SwiftUI syntax for UIKit).
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | Write UIKit UI code declaratively with Construkt |
| description | Guidelines for generating declarative UIKit code using the Construkt framework (SwiftUI syntax for UIKit). |
You are an expert iOS developer specialized in Construkt, a declarative UIKit framework that uses SwiftUI-like syntax but generates native UIView hierarchies under the hood via Swift Result Builders.
Whenever a user asks you to build a UI component, screen, or apply styling in this project, you MUST use Construkt syntax instead of traditional imperatively-built UIKit or SwiftUI.
UIView instances. There is no UIHostingController.VStackView, HStackView, ZStackView, LabelView, ButtonView, ImageView, etc.NSLayoutConstraint code. Layout is handled entirely by Spacers, padding, height/width modifiers, and Stack alignments.@Variable (a wrapper around Property<T>) and Signal<T>. Bind to UI using the $ prefix (e.g., .text(bind: $title)).UICollectionViewDataSource or delegate boilerplate. Use CollectionView with AnySection builders.When generating UI, use the Construkt primitives:
| SwiftUI / UIKit | Construkt Equivalent |
|---|---|
View / UIView | ViewBuilder (protocol returning View) |
Text / UILabel | LabelView("Text") |
Image / UIImageView | ImageView(UIImage) |
Button / UIButton | ButtonView("Title").onTap { ... } |
VStack / UIStackView | VStackView { ... } |
HStack / UIStackView | HStackView { ... } |
ZStack / UIView | ZStackView { ... } |
Spacer / UIView | SpacerView() |
Circle / UIView | CircleView() |
Toggle / UISwitch | Toggle(isOn: $state) |
Slider / UISlider | Slider(value: $value) |
ProgressView / UIProgressView | ProgressView(value: 0.5) |
Stepper / UIStepper | Stepper(value: $num, in: 0...10) |
TextEditor / UITextView | TextEditor(text: $text) |
LinearGradient / CAGradientLayer | LinearGradient(colors: [.red, .blue]) |
BlurView / UIVisualEffectView | BlurView(style: .regular) |
List / UITableView | TableView(DynamicItemViewBuilder) { ... } |
LazyVGrid/UICollectionView | CollectionView { AnySection { ... } } |
TraditionalCollectionView | Collection view with custom UICollectionViewLayout subclass support |
| Screen layout container | Screen { content }.navigationBar { bar } |
Always apply styling using chained modifiers just like SwiftUI.
VStackView {
LabelView("Title")
.font(.title1)
.color(.label)
ImageView(image)
.contentMode(.scaleAspectFill)
.height(200) // Fixed height
.clipsToBounds(true)
}
.spacing(16) // Spacing between stack items
.padding(h: 20, v: 24) // Horizontal and vertical padding
.backgroundColor(.systemBackground)
.cornerRadius(12)
In ZStackView, you can position items using .position() + .margins() (from Builder+Attributes):
ZStackView {
ImageView(backdrop)
.contentMode(.scaleAspectFill)
LabelView("Overlay Text")
.color(.white)
.position(.bottomLeft) // Positions to the bottom-left of the ZStack
.margins(16)
}
Use Screen to establish a standard page architecture with content and navigation bar "slots". This replaces manual ZStackView + .position(.top) boilerplate:
Screen {
CollectionView {
heroSection
popularSection
}
.onScroll { scrollView in
scrollBinding.offset = scrollView.contentOffset.y
}
}
.navigationBar {
HomeNavigationBar(
scrollOffset: scrollBinding.$offset.eraseToAnyViewBinding()
)
}
.backgroundColor(UIColor("#0A0A0A"))
The Screen handles Z-stacking automatically. Each screen provides its own distinct navigation bar.
Instead of manually updating UI elements when data changes, use Construkt's binding modifiers. The ViewModel exposes @Variable, and the View binds to $variable.
ViewModel:
class ProfileViewModel {
@Variable var name: String = "John Doe"
@Variable var isSaving: Bool = false
let onSaveTapped = Signal<Void>()
}
View:
VStackView {
LabelView($viewModel.name) // Automatically updates when `name` changes
.font(.body)
ButtonView("Save")
.hidden(bind: $viewModel.isSaving) // Hides when saving is true
.onTap { [weak viewModel] _ in
viewModel?.onSaveTapped.send()
}
ActivityIndicator()
.hidden(bind: $viewModel.isSaving.map { !$0 }) // Inverse mapping
}
Note: Do NOT write
.map { !$0 }unless you are mapping a Boolean. Always importConstruktto ensure modifiers are available.
For lists, always use Construkt's declarative CollectionView (compositional layout) or TraditionalCollectionView (custom layout subclasses). Never manually create Data Sources.
When binding to an array or an Rx @Variable array, provide the items: parameter to a AnySection constructor, and yield Cell(...) instances.
CollectionView {
AnySection(
id: "movies_section",
items: viewModel.movies, // or $viewModel.movies
header: Header { LabelView("Trending Now").font(.title1).padding(h: 16) }
) { movie in
AnyCell(movie, id: movie.id) { movieData in
MoviePosterCell(movie: movieData)
}
}
.onSelect { movie in
// Direct strongly-typed model access
print("Selected \(movie)")
}
.layout { environment in
// Return NSCollectionLayoutSection
}
}
You can build statically-defined declarative collections (e.g., Settings menus) by listing explicit AnyCell components within a AnySection:
CollectionView {
AnySection(id: "settings_section", header: Header { LabelView("General") }) {
AnyCell("Notifications", id: "notifications") { title in
SettingsRowView(title: title)
}
AnyCell("Privacy", id: "privacy") { title in
SettingsRowView(title: title)
}
}
.onSelect { title in
print("Tapped on \(title)")
}
}
Construkt supports natively swapping an entire AnySection with shimmer placeholders during load times. Use the .shimmer(count:when:...) modifier directly on the Section:
AnySection(id: "popular", items: viewModel.popularMovies) { movie in
AnyCell(movie, id: movie.id) { movie in
MoviePosterCell(movie: movie)
}
}
.shimmer(count: 5, when: $viewModel.isLoading) {
MoviePosterCell(movie: .placeholder) // Create geometry for shimmer
}
When you need a layout that UICollectionViewCompositionalLayout cannot express — such as a flow/tag-cloud layout with variable-width items wrapping to the next line, circular layouts, or physics-based layouts — use TraditionalCollectionView. It accepts any UICollectionViewLayout subclass instead of being hardcoded to compositional layout.
let flowLayout = UICollectionViewFlowLayout()
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
flowLayout.minimumInteritemSpacing = 8
flowLayout.minimumLineSpacing = 10
flowLayout.sectionInset = UIEdgeInsets(top: 8, left: 20, bottom: 24, right: 20)
flowLayout.headerReferenceSize = CGSize(width: 0, height: 44)
TraditionalCollectionView(layout: flowLayout) {
AnySection(id: .tags, items: tags) { tag in
AnyCell(tag, id: tag.id) { model in
TagChipCell(tag: model)
}
}
.header {
Header { SectionHeaderLabel(title: "TRENDING") }
}
.onSelect { (tag: Tag) in
print("Selected: \(tag.name)")
}
}
.contentInset(top: 16, bottom: 32)
When to use which:
CollectionView when you need per-section compositional layouts (.layout{}), decoration items, or background decorations.TraditionalCollectionView when you need a custom UICollectionViewLayout subclass (e.g., UICollectionViewFlowLayout with automaticSize for wrapping tag clouds).Shared DSL: TraditionalCollectionView uses the same AnySection/AnyCell result builders, diffable data source, selection handling (.onSelect, .onRoute), shimmer, headers/footers, and scroll observation as CollectionView.
Supported modifiers: .onSelect, .onRoute, .header(), .footer(), .headerHidden(when:), .footerHidden(when:), .shimmer(), .contentInset(), .onRefresh(), .onScroll(), .onWillBeginDragging(), .onDidEndDragging(), .onDidEndDecelerating(), .emptyState(when:).
Not applicable (compositional-layout-specific, silently ignored): .layout{}, .decorationItems(), .backgroundDecoration().
Note: One layout governs all sections in TraditionalCollectionView. For per-section layout control, use CollectionView with .layout{} on each AnySection.
When a UI becomes complex, you MUST extract it into separate, context-specific structs adopting ViewBuilder. Never generate a massive, single body with dozens of nested stacks.
For instance, if building a user profile screen, separate it into ProfileHeaderView, StatsRowView, and RecentActivityView. Then assemble them inside the root view.
import UIKit
import Construkt
struct UserProfileCard: ViewBuilder {
let user: User
var body: View { // Must return Construkt's `View` type
HStackView {
ImageView(user.avatar)
.size(width: 50, height: 50)
.cornerRadius(25)
VStackView {
LabelView(user.name).font(.headline)
LabelView(user.role).font(.subheadline).color(.secondaryLabel)
}
.spacing(4)
SpacerView()
}
.padding(16)
.backgroundColor(.secondarySystemBackground)
.cornerRadius(12)
}
}
To instantiate this into a raw UIKit UIView, call .build():
let customView: UIView = UserProfileCard(user: currentUser).build()
If you are building a layout containing multiple identical UI blocks (e.g., a row of feature highlights, three pricing cards, or identical buttons), you MUST extract the structural boilerplate into a dynamically parameterized ViewBuilder component. Pass unique text, images, or configurations as immutable properties (let).
Example: Extracting identical statistics cards
// 1. Define the reusable parameterized struct
struct StatCard: ViewBuilder {
let title: String
let value: String
var body: View {
VStackView {
LabelView(value).font(.title1)
LabelView(title).font(.caption1).color(.secondaryLabel)
}
.padding(16)
.backgroundColor(.secondarySystemBackground)
.cornerRadius(12)
}
}
// 2. Instantiate multiple times in the parent scope rather than duplicating raw views
struct DashboardStatsView: ViewBuilder {
var body: View {
HStackView {
StatCard(title: "Followers", value: "1.2k")
StatCard(title: "Following", value: "400")
StatCard(title: "Posts", value: "32")
}
.spacing(12)
.distribution(.fillEqually)
}
}
You must exclusively use these native Construkt modifiers. Do not invent SwiftUI names (e.g., use .backgroundColor not .background, .alpha not .opacity, .frame(height:width:) not .frame(maxWidth:)).
Builder+Constraints).frame(height:width:) — set explicit dimensions (both optional).size(width:height:) — shorthand for .width().height().height(CGFloat) / .height(CGFloat, priority:) — constrain height.height(min:) / .height(max:) — min/max height constraints.width(CGFloat) / .width(CGFloat, priority:) — constrain width.width(min:) / .width(max:) — min/max width constraints.contentCompressionResistancePriority(UILayoutPriority, for: .horizontal/.vertical).contentHuggingPriority(UILayoutPriority, for: .horizontal/.vertical).zIndex(CGFloat) — set layer z-positionBuilder+Padding, only on Paddable views: Stacks, Labels, Buttons).padding(CGFloat) — uniform all edges.padding(h:v:) — horizontal + vertical.padding(top:left:bottom:right:) — per-edge.padding(insets: UIEdgeInsets) — raw insetsBuilder+Attributes).margins(CGFloat) / .margins(h:v:) / .margins(top: 12, bottom: 12) — embed margins (parameters can be partial: top:left:bottom:right:)
⚠️ CRITICAL: The modifier is
.margins(with an 's'). NEVER use.marginwithout the 's' — it does not exist.
.position(.center) / .position(.top) / .position(.fill) — embed alignment.safeArea(Bool) — respect safe area when embedded.customConstraints { view in } — raw AutoLayout accessBuilder+View).backgroundColor(UIColor).alpha(CGFloat).cornerRadius(CGFloat) / .roundedCorners(radius:corners:).border(color:lineWidth:).shadow(color:radius:opacity:offset:).tintColor(UIColor).clipsToBounds(Bool).contentMode(UIView.ContentMode).hidden(Bool) / .hidden(bind: $variable).isOpaque(Bool).isUserInteractionEnabled(Bool) / .userInteractionEnabled(bind: $variable)Builder+Bindings).bind(keyPath, to: binding) — bind any writable keypath to a reactive stream.onReceive(binding) { context in } — react to any value change.hidden(when: $isHidden) — reactively show/hide.userInteractionEnabled(when: $binding) — reactively enable/disable interaction.eraseToAnyViewBinding() — type-erase any ViewBinding into AnyViewBinding<T>.font(UIFont) / .font(.headline) — set font.color(UIColor) / .color(bind: $variable) — set text color.text(bind: $variable) — reactively update text.alignment(NSTextAlignment) — text alignment.numberOfLines(Int) — line count.lineBreakMode(NSLineBreakMode) — truncation mode.spacing(CGFloat) / .customSpacing(CGFloat, after: View) — inter-item spacing.alignment(UIStackView.Alignment) — cross-axis alignment.distribution(UIStackView.Distribution) — main-axis distribution.layoutMarginsRelativeArrangement(Bool) — use layout margins.onTap { context in } — handle tap.font(UIFont) / .font(.headline) — title font.color(UIColor, for: .normal) — title color.backgroundColor(UIColor, for: .highlighted) — per-state background.alignment(UIControl.ContentHorizontalAlignment) — content alignment.enabled(Bool) / .enabled(bind: $variable) — enable/disable.tintColor(UIColor) — template image tint.image(bind: $variable) — reactively update image.isOn(bind: $variable) / .isOn(bidirectionalBind: $variable) — bind toggle state.onTintColor(UIColor) — on-state color.onChange { context in } — react to toggle changesBuilder+Gestures).onTapGesture { context in } / .onTapGesture(numberOfTaps: 2) { context in }.onSwipeRight { context in } / .onSwipeLeft { context in }.hideKeyboardOnBackgroundTap()Builder+Attributes, on ViewBuilderEventHandling views).onAppear { context in } — fires each time view enters window.onAppearOnce { context in } — fires only the first time.onDisappear { context in } — fires when leaving windowSpacerView() — flexible space (pushes siblings apart)SpacerView(h: 16) — minimum-height spacer / SpacerView(w: 8) — minimum-width spacerFixedSpacerView(8) — rigid height spacer / FixedSpacerView(width: 8) — rigid width spacerDividerView() — 1px separator line with .color(UIColor) modifierContainerView { ... } — single-child host view (also aliased as ZStackView)DynamicContainerView($binding) { value in ... } — reactively swaps child viewForEach(array) { element in ... } — iterate over an array to produce viewsForEach(count) { index in ... } — iterate N times to produce viewsCGFloat.scrollProgress(over: CGFloat) -> CGFloat — normalize scroll offset into 0…1 rangeConstrukt supports full application features declaratively.
Construkt provides multiple ways to navigate between screens.
Declarative Routing — attach navigation intent directly to sections:
AnySection(id: "popular", items: movies) { movie in
AnyCell(movie, id: movie.id) { movie in PosterCell(movie: movie) }
}
.onRoute { (movie: Movie) in
AppRoute.movieDetail(movieId: String(movie.id))
}
Sender-Based Routing — route from the tapped view's responder chain:
.onTapGesture { context in
context.view.route(HomeRoute.search, sender: nil)
}
Inline Route Handling — attach handlers when constructing screens:
HomeView()
.onReceiveRoute(HomeRoute.self) { route in
switch route {
case .movieDetail(let id): open(.movieDetail(movieId: id))
case .search: open(.search)
}
return true
}
.toPresentable()
Coordinator Pattern — for complex navigation hierarchies with parent-child relationships:
final class HomeCoordinator: BaseCoordinator, RouteHandlingCoordinator {
typealias Event = HomeRoute
let router: ConstruktRouter
init(router: ConstruktRouter) {
self.router = router
}
override func start() {
let homeVC = HomeView(viewModel: viewModel).toPresentable()
router.setRoot(homeVC, hideBar: true, animated: false, receiver: self)
}
func canReceive(_ event: HomeRoute, sender: Any?) -> Bool {
switch event {
case .movieDetail(let id):
router.push(MovieDetailView(movie: movie).toPresentable(), animated: true, receiver: self)
return true
case .search:
router.push(SearchViewController(), animated: true, receiver: self)
return true
}
}
}
Key types:
BaseCoordinator — base class with children, store(), free(), and start()RouteHandlingCoordinator — protocol requiring router + canReceive() to handle route eventsConstruktRouter / DefaultRouter — push, pop, present, dismiss, setRootreceiver: self — binds the coordinator to the VC so events route back correctlyFor scroll-driven UI (e.g., stretchy headers, fading nav bars), separate reactive data from imperative UIKit handles:
// Pure reactive data — observable, testable
private class ScrollBinding {
@Variable var offset: CGFloat = 0
@Variable var scrollToTopTrigger: UInt = 0
}
// Imperative UIKit handles — needed for constraint manipulation
private class ViewHandles {
weak var heroHeightConstraint: NSLayoutConstraint?
weak var scrollView: UIScrollView?
}
Bind scroll events to ScrollBinding, then use .onReceive to drive UI:
.onScroll { scrollView in
scrollBinding.offset = scrollView.contentOffset.y
}
// In the nav bar:
.onReceive(scrollBinding.$offset) { context in
context.view.alpha = context.value.scrollProgress(over: 100)
}
Do not build custom layouts on raw UIScrollView constraints. Use ScrollView and VerticalScrollView.
VerticalScrollView(safeArea: true) { // Automatically fills width
VStackView {
LabelView("Top")
SpacerView()
LabelView("Bottom")
}
}
.automaticallyAdjustForKeyboard()
TextField supports bidirectional binding to a @Variable string.
TextField(bidirectionalBind: $viewModel.username)
.placeholder("Enter Username")
.autocapitalizationType(.none)
.keyboardType(.emailAddress)
.onChange { context in
let text = context.value // The string inside the textfield
}
Any Construkt view can respond to gestures via chained modifiers:
ImageView(headerImage)
.onTapGesture(numberOfTaps: 2) { context in
print("Double Tapped!")
}
.onSwipeRight { context in
context.navigationController?.popViewController(animated: true)
}
// Hide keyboard globally on a root ZStackView
ZStackView { ... }.hideKeyboardOnBackgroundTap()
Text, Image, or VStack. Always use the Construkt equivalents (LabelView, ImageView, VStackView).UIKit and Construkt.setupConstraints() or use translatesAutoresizingMaskIntoConstraints = false.UICollectionViewDataSource logic. Use CollectionView or TraditionalCollectionView ResultBuilders..layout{}, .decorationItems(), or .backgroundDecoration() with TraditionalCollectionView. These are compositional-layout-specific and will be silently ignored. Configure layout via the UICollectionViewLayout instance passed at init.TraditionalCollectionView when CollectionView suffices. If your layout can be expressed with UICollectionViewCompositionalLayout (lists, grids, carousels, orthogonal scrolling), prefer CollectionView with per-section .layout{} modifiers. Reserve TraditionalCollectionView for layouts that genuinely require a custom UICollectionViewLayout subclass.Because ConstruktKit relies heavily on Swift Result Builders (@ViewBuilder), certain compilation errors can be opaque and misleading. When the Swift compiler fails to type-check a large nested view hierarchy, it usually points to the parent container instead of the exact line causing the issue.
If you see either of the following errors pointing to a VStackView, HStackView, ZStackView, or CollectionView:
"extra trailing closure passed in call""initializer 'init(_:)' requires the types '(() -> ()).Value' and '[any View]' be equivalent"DO NOT assume the structure itself is wrong. This almost always means there is a type mismatch or an invalid modifier deeply nested inside that container block. The Swift compiler ran out of time or inference capabilities and gave up at the top level.
To find the actual line causing the bug, you MUST isolate the components.
Extract inner views from the failing stack into local let variables or separate computed properties (var myView: View { ... }). By doing this, you force the Swift compiler to evaluate each piece independently. The compiler will immediately highlight the exact variable definition that contains the typo or invalid modifier.
Example of an obscure error:
var body: View {
VStackView { // ERROR: "extra trailing closure passed in call"
LabelView("Title")
ImageView(myImage)
.clipShape(.circle) // This is the real bug (SwiftUI modifier, not Construkt)
}
}
How to isolate it:
var body: View {
let titleLab = LabelView("Title")
// The compiler will now correctly flag THIS exact line:
let image = ImageView(myImage).clipShape(.circle)
return VStackView {
titleLab
image
}
}
When generating ConstruktKit code, AI often hallucinates SwiftUI equivalents. If a file fails to build, check for these common mistakes first:
Text instead of LabelView or Spacer instead of SpacerView).clipShape() instead of .cornerRadius().clipsToBounds(true)).border arguments)SpacerView(width:) instead of FixedSpacerView(width:))ZStackView + .position(.top) for nav bars instead of Screen { ... }.navigationBar { ... }Pattern: Parent-child reducer delegation
case .child(let childIntent):
let childResult = ChildFeature.reduce(state: &state.child, intent: childIntent)
return childResult.map(
effect: { Effect.child($0) },
output: { Output.child($0) }
)
Pattern: Effect executor delegation
case .child(let childEffect):
let childFeedback = try await childExecutor(childEffect, deps.childDeps)
return childFeedback.map(intent: { .child($0) }, output: { .child($0) })
Pattern: Policy delegation
case .child(let childEffect):
return ChildFeature.policy(for: childEffect)
Anti-pattern: Forgetting to map child types
// WRONG: Returns ReduceResult<ChildEffect, ChildOutput> — type mismatch
case .child(let childIntent):
return ChildFeature.reduce(state: &state.child, intent: childIntent)
// RIGHT: Map to parent types
case .child(let childIntent):
return ChildFeature.reduce(state: &state.child, intent: childIntent)
.map(effect: { .child($0) }, output: { .child($0) })
Anti-pattern: Creating separate FeatureStore for child
// WRONG: Child runs in its own runtime — no composition
let childStore = FeatureStore<ChildFeature>(...)
let parentStore = FeatureStore<ParentFeature>(...)
// RIGHT: Single parent store, child state embedded
let store = FeatureStore<ParentFeature>(...)
store.dispatch(.child(.loadProfile))
store.state.map(\.child.username) // observe child state through parent