MainActorDev

Write UIKit UI code declaratively with Construkt

"Guidelines for generating declarative UIKit code using the Construkt framework (SwiftUI syntax for UIKit)."

MainActorDev 18 Updated 3mo ago
GitHub

Install

npx skillscat add mainactordev/construkt

Install via the SkillsCat registry.

SKILL.md

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

  1. 100% UIKit: Construkt components generate native UIView instances. There is no UIHostingController.
  2. SwiftUI Syntax: The syntax mirrors SwiftUI perfectly. Use VStackView, HStackView, ZStackView, LabelView, ButtonView, ImageView, etc.
  3. No Auto Layout: Never write NSLayoutConstraint code. Layout is handled entirely by Spacers, padding, height/width modifiers, and Stack alignments.
  4. State Management: Construkt uses reactive bindings via @Variable (a wrapper around Property<T>) and Signal<T>. Bind to UI using the $ prefix (e.g., .text(bind: $title)).
  5. Collection Views: Never write UICollectionViewDataSource or delegate boilerplate. Use CollectionView with Section builders.

๐Ÿ›  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 import Construkt to 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 .margin without 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 spacer
  • FixedSpacerView(8) โ€” rigid height spacer / FixedSpacerView(width: 8) โ€” rigid width spacer
  • DividerView() โ€” 1px separator line with .color(UIColor) modifier
  • ContainerView { ... } โ€” single-child host view (also aliased as ZStackView)
  • DynamicContainerView($binding) { value in ... } โ€” reactively swaps child view
  • ForEach(array) { element in ... } โ€” iterate over an array to produce views
  • ForEach(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)

  1. Never use SwiftUI Text, Image, or VStack. Always use the Construkt equivalents (LabelView, ImageView, VStackView).
  2. Never import SwiftUI. Only import UIKit and Construkt.
  3. Never write setupConstraints() or use translatesAutoresizingMaskIntoConstraints = false.
  4. Never create generic constraint arrays.
  5. Never write UICollectionViewDataSource logic. Use CollectionView ResultBuilders.

๐Ÿ› 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:

  1. Non-existent components (e.g. using Text instead of LabelView or Spacer instead of SpacerView)
  2. Non-existent modifiers (e.g. using .clipShape() instead of .cornerRadius().clipsToBounds(true))
  3. Wrong modifier signatures (e.g. wrong padding parameters or .border arguments)
  4. Wrong component initializer signatures (e.g. SpacerView(width:) instead of FixedSpacerView(width:))