/SKILL.md
Guidelines for generating declarative UIKit code using the Construkt framework (SwiftUI syntax for UIKit).
npx skillsauth add mainactordev/construkt Write UIKit UI code declaratively with ConstruktInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 { ... } } |
| 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. 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 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 ResultBuilders.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 { ... }development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.