"Guidelines for generating declarative UIKit code using the Construkt framework (SwiftUI syntax for UIKit)."
Install
npx skillscat add mainactordev/construkt Install via the SkillsCat registry.
Write Construkt UI Code
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.
Core Principles
- 100% UIKit: Construkt components generate native
UIViewinstances. There is noUIHostingController. - SwiftUI Syntax: The syntax mirrors SwiftUI perfectly. Use
VStackView,HStackView,ZStackView,LabelView,ButtonView,ImageView, etc. - No Auto Layout: Never write
NSLayoutConstraintcode. Layout is handled entirely by Spacers, padding, height/width modifiers, and Stack alignments. - State Management: Construkt uses reactive bindings via
@Variable(a wrapper aroundProperty<T>) andSignal<T>. Bind to UI using the$prefix (e.g.,.text(bind: $title)). - Collection Views: Never write
UICollectionViewDataSourceor delegate boilerplate. UseCollectionViewwithSectionbuilders.
๐ Basic Components
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 { Section { ... } } |
๐ Layout & Modifiers
Always apply styling using chained modifiers just like SwiftUI.
Sizing and Spacing
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)Alignment (ZStackView)
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)
}โก๏ธ Reactive State (Binding)
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.
๐ Lists and Collections
For lists, always use Construkt's declarative CollectionView. Never manually create Data Sources.
1. Dynamic Collections
When binding to an array or an Rx @Variable array, provide the items: parameter to a Section constructor, and yield Cell(...) instances.
CollectionView {
Section(
id: "movies_section",
items: viewModel.movies, // or $viewModel.movies
header: Header { LabelView("Trending Now").font(.title1).padding(h: 16) }
) { movie in
Cell(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
}
}2. Static Collections
You can build statically-defined declarative collections (e.g., Settings menus) by listing explicit Cell components within a Section:
CollectionView {
Section(id: "settings_section", header: Header { LabelView("General") }) {
Cell("Notifications", id: "notifications") { title in
SettingsRowView(title: title)
}
Cell("Privacy", id: "privacy") { title in
SettingsRowView(title: title)
}
}
.onSelect { title in
print("Tapped on \(title)")
}
}3. Skeleton Loading States
Construkt supports natively swapping an entire Section with skeleton placeholders during load times. Use the .skeleton(count:when:...) modifier directly on the Section:
Section(id: "popular", items: viewModel.popularMovies) { movie in
Cell(movie, id: movie.id) { movie in
MoviePosterCell(movie: movie)
}
}
.skeleton(count: 5, when: $viewModel.isLoading) {
MoviePosterCell(movie: .placeholder) // Create geometry for skeleton
}๐ View Composition (Creating custom views)
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()Component Parameterization & Reusability
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)
}
}7. Comprehensive View Modifiers Reference
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:)).
Layout & Sizing (from 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-position
Padding (from Builder+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 insets
Container Embedding (from Builder+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 access
Appearance & Styling (from Builder+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)
Reactive Bindings (from 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
Typography (LabelView modifiers)
.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
StackView (HStackView / VStackView)
.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
ButtonView modifiers
.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
ImageView modifiers
.tintColor(UIColor)โ template image tint.image(bind: $variable)โ reactively update image
SwitchView modifiers
.isOn(bind: $variable)/.isOn(bidirectionalBind: $variable)โ bind toggle state.onTintColor(UIColor)โ on-state color.onChange { context in }โ react to toggle changes
Gestures (from Builder+Gestures)
.onTapGesture { context in }/.onTapGesture(numberOfTaps: 2) { context in }.onSwipeRight { context in }/.onSwipeLeft { context in }.hideKeyboardOnBackgroundTap()
Lifecycle (from 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 window
Utility Components
SpacerView()โ 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 asZStackView)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 views
8. Advanced Capabilities
Construkt supports full application features declaratively.
Navigation and Routing
When handling taps or selections, Construkt provides a context proxy to the current UIView and its closest UIViewController/UINavigationController.
ButtonView("Open Details")
.onTap { context in
// Push a generic view natively
context.push(DetailView(item: item))
// Present a specific View Controller
context.present(CustomViewController())
}Scroll Views
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()Forms & TextFields
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
}Gestures
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()โ ๏ธ Anti-Patterns (What NOT to do)
- Never use SwiftUI
Text,Image, orVStack. Always use the Construkt equivalents (LabelView,ImageView,VStackView). - Never import SwiftUI. Only import
UIKitandConstrukt. - Never write
setupConstraints()or usetranslatesAutoresizingMaskIntoConstraints = false. - Never create generic constraint arrays.
- Never write
UICollectionViewDataSourcelogic. UseCollectionViewResultBuilders.
๐ Troubleshooting & Debugging Compiler Errors
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.
1. Handling "Opaque" Error Messages
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.
2. Isolation Strategy (How to find the real error)
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
}
}3. Common AI Hallucinations
When generating ConstruktKit code, AI often hallucinates SwiftUI equivalents. If a file fails to build, check for these common mistakes first:
- Non-existent components (e.g. using
Textinstead ofLabelVieworSpacerinstead ofSpacerView) - Non-existent modifiers (e.g. using
.clipShape()instead of.cornerRadius().clipsToBounds(true)) - Wrong modifier signatures (e.g. wrong padding parameters or
.borderarguments) - Wrong component initializer signatures (e.g.
SpacerView(width:)instead ofFixedSpacerView(width:))