Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5a3a84d
Fix SnapFrame's borderWidth has no effect
Adobels Sep 4, 2025
c12a265
Update code style
Adobels Sep 5, 2025
5312156
Configure Swiftlint to scan only Sources and Tests folders
Adobels Sep 5, 2025
de52d81
Set CodeCallTracker to upNextMinor 1.0.0
Adobels Sep 5, 2025
e3207ac
Fix linter warnings
Adobels Sep 5, 2025
d45a008
Testing new names of previews
Adobels Sep 5, 2025
a02a5aa
Improve support of Swift 6 concurrency
Adobels Sep 6, 2025
aac098f
Fix Lint warnings
Adobels Sep 8, 2025
feb3fdc
Delete ibApply for UIView which was a mistake
Adobels Sep 12, 2025
e650980
Delete support for callAsFunction
Adobels Sep 12, 2025
1e89c46
Move IBLayoutConstraintBuilder to dedicated file
Adobels Sep 12, 2025
34d809e
Rename result builders
Adobels Sep 12, 2025
b8c50fe
This is a test for swift-tools-version 6 with implicit swiftmode v6
Adobels Sep 14, 2025
438c9c5
Upgrade platform ios to v15 becase AppStore supports only apps built …
Adobels Sep 14, 2025
d0e2670
Remove support for CodeCallTracker, it will be back in form of dedica…
Adobels Sep 14, 2025
a91513a
Delete exported frameworks, this should be handled by application pro…
Adobels Sep 14, 2025
559d02f
Update documentation
Adobels Sep 14, 2025
0cd8077
Add discardableResult to ibApply and add ibApply extension on UIViewD…
Adobels Sep 14, 2025
cb9f745
Update documentation
Adobels Sep 14, 2025
734d858
Update Documentation
Adobels Sep 14, 2025
12e8912
Add IBFreeForm Preview Demo Gif
Adobels Sep 15, 2025
42c9c71
Improve documentation
Adobels Sep 15, 2025
c37a7eb
Improve documentation
Adobels Sep 15, 2025
f4e3430
Delete IBFreeForm init which leads to collisions with variants with s…
Adobels Sep 15, 2025
d66acc8
Clean up documentation
Adobels Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ disabled_rules:
- identifier_name
- trailing_comma
opt_in_rules:
included:
- Sources
- Tests
Binary file added IBFreeFormPreview.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 0 additions & 14 deletions Package.resolved

This file was deleted.

9 changes: 3 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
// swift-tools-version: 5.7
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "UIViewKit",
platforms: [
.iOS(.v13)
.iOS(.v15)
],
products: [
.library(name: "UIViewKit", targets: ["UIViewKit"])
],
dependencies: [
.package(url: "https://github.com/Adobels/CodeCallTracker.git", revision: "1d27da6706466a5b83bdb0f4097fc86f678b146f")
],
targets: [
.target(name: "UIViewKit", dependencies: ["CodeCallTracker"]),
.target(name: "UIViewKit"),
.testTarget(name: "UIViewKitTests", dependencies: ["UIViewKit"])
]
)
175 changes: 107 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,33 @@

# UIViewKit

UIViewKit is a Swift tool that makes designing and setting up UIKit views as simple as using InterfaceBuilder, but with Swift's strong type checks. It offers a look similar to SwiftUI and has lots of easy methods for attributes, outlets, and constraints. Thanks to the @resultBuilder attribute, the code is quick to write, looks cleaner, and is more pleasing to the eye.
UIViewKit lets you build UIKit views directly in code with a syntax that feels like SwiftUI.
It provides a DSL powered by @resultBuilder for both UIView hierarchies and NSLayoutConstraints, so you can create entire UIViewController scenes in strongly typed Swift - without relying on storyboards or XIBs.

## Key Features

- SwiftUI-Style Syntax for UIKit: Embrace SwiftUI's declarative approach, but tailored for UIKit.
- No More Storyboards/Xibs: Design UI directly in code, bypassing storyboards and xib files.
- Constraint Configuration Generator: Produce complex AutoLayout setups with a single method.
- Previews for Views & Controllers: Preview your UIKit views and controllers in code, just like SwiftUI views.
- **DSL with @resultBuilder for UIKit** - Build UIView hierarchies in Swift using a declarative, SwiftUI-like syntax. With builders like ibSubviews, ibAttributes, and ibApply, your code becomes compact, expressive, and easy to read.

- **Constraint Generator** - Define AutoLayout with ibConstraints for fast, expressive, and compact constraint definitions.

- **FreeForm Preview** - Instantly preview UIKit views and controllers with live constraint evaluation. Test layouts across multiple device sizes without leaving Xcode.


## How to Use

### Defining ViewController's View, with "Hello, world!" label
### "Hello, World!"

```swift
class ViewController: UIViewController {
import UIViewKit

var label: UILabel!
class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

view {
UILabel().ibOutlet(&label).ibAttributes {
view.ibSubviews {
UILabel().ibAttributes {
$0.centerXAnchor.constraint(equalTo: view.centerXAnchor)
$0.centerYAnchor.constraint(equalTo: view.centerYAnchor)

$0.text = "Hello, world!"
}
}
Expand All @@ -39,76 +40,114 @@ class ViewController: UIViewController {
### Defining ViewController's View, Complex

```swift
class ViewControllerComplex: UIViewController {

var labelTitle: UILabel!
var labelsText: [UILabel] = []
var button: UIButton = .init()
var heightConstraint: NSLayoutConstraint!

override func viewDidLoad() {
super.viewDidLoad()

view {
UIStackView(axis: .vertical, spacing: 10, alignment: .center) { stackView in
UILabel().ibOutlet(&labelTitle).ibAttributes {
$0.text = "Title"
$0.font = .init(name: "Arial", size: 30)
final class ViewController: UIViewController {

private var profileItems: [(title: String, value: String)] = [
("Framework: ", "UIViewKit"),
("Platform: ", "iOS"),
("Programming Language: ", "Swift"),
("Device:" , "Simulator"),
("FreeForm Preview:" , "UIKit, SwiftUI"),
]

private var headerView: UIStackView!

override func loadView() {
super.loadView()
view.ibSubviews {
UIStackView(axis: .vertical, alignment: .fill).ibOutlet(&headerView).ibSubviews {
UIImageView().ibAttributes {
$0.widthAnchor.constraint(equalTo: $0.heightAnchor).ibPriority(.required)
$0.image = .init(systemName: "person.circle")
$0.contentMode = .scaleAspectFit
$0.tintColor = .white
$0.layer.cornerRadius = 20
$0.backgroundColor = .systemBlue
}
UIView() { superview in
UILabel().ibOutlet(in: &labelsText).ibAttributes {
$0.topAnchor.constraint(equalTo: superview.topAnchor)
$0.leftAnchor.constraint(equalTo: superview.leftAnchor)
$0.rightAnchor.constraint(equalTo: superview.rightAnchor)
$0.bottomAnchor.constraint(equalTo: superview.bottomAnchor)

$0.text = "Label 1"
$0.textColor = .red
}.ibAttributes {
$0.leadingAnchor.constraint(equalTo: view.leadingAnchor)
$0.topAnchor.constraint(equalTo: view.topAnchor)
$0.trailingAnchor.constraint(equalTo: view.trailingAnchor)
$0.backgroundColor = .systemGreen
$0.layoutMargins = .init(top: 20, left: 20, bottom: 20, right: 20)
$0.isLayoutMarginsRelativeArrangement = true
}
UIStackView(axis: .vertical).ibSubviews {
for item in profileItems {
RowView().ibAttributes {
$0.titleLabel.text = item.title
$0.valueLabel.text = item.value
}
}
UILabel().ibOutlet(in: &labelsText).ibAttributes {
$0.text = "Label 2"
UIView()
}.ibAttributes {
$0.topAnchor.constraint(equalTo: headerView.bottomAnchor)
$0.leadingAnchor.constraint(equalTo: view.leadingAnchor)
$0.trailingAnchor.constraint(equalTo: view.trailingAnchor)
$0.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
}
}.ibAttributes {
$0.backgroundColor = .systemBackground
}
}
}

final class RowView: UIView {

var titleLabel: UILabel!
var valueLabel: UILabel!

required init?(coder: NSCoder) {
fatalError()
}

override init(frame: CGRect) {
super.init(frame: frame)
self.ibSubviews {
UIStackView(axis: .vertical).ibSubviews {
UIStackView(axis: .horizontal, spacing: 10).ibSubviews {
UILabel().ibOutlet(&titleLabel)
UILabel().ibOutlet(&valueLabel).ibAttributes {
$0.textColor = .systemGray
$0.textAlignment = .right
}
}
UIView().ibAttributes {
$0.leftAnchor.constraint(equalTo: stackView.leftAnchor)
$0.heightAnchor.constraint(equalToConstant: 50).ibPriority(.defaultHigh - 1).ibOutlet(&heightConstraint)

$0.backgroundColor = .yellow
}
UILabel().ibAttributes {
$0.text = "Label 3 Without Outlet"
}
UIButton().ibOutlet(&button).ibAttributes {
$0.widthAnchor.constraint(equalToConstant: 300)

$0.setTitle("Button", for: .normal)
$0.backgroundColor = .blue

$0.addTarget(self, action: #selector(didTap), for: .touchUpInside)
$0.heightAnchor.constraint(equalToConstant: 1)
$0.backgroundColor = .separator
}
}.ibAttributes {
$0.centerXAnchor.constraint(equalTo: view.centerXAnchor)
$0.centerYAnchor.constraint(equalTo: view.centerYAnchor)
$0.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor)
$0.topAnchor.constraint(equalTo: topAnchor)
$0.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor)
$0.bottomAnchor.constraint(equalTo: bottomAnchor)
$0.heightAnchor.constraint(equalToConstant: 66)
}
}

labelsText.forEach { $0.text = "toto" }
}

@objc func didTap() {
print(#function)
}
}
```

#if DEBUG
## IBFreeForm Preview Demo

```swift
import SwiftUI

struct ViewControllerPreviews: PreviewProvider {
static var previews: some View {
IBRepresentableFreeFormViewController(ViewControllerComplex())
#Preview {
IBFreeForm {
ViewController()
}
}

#endif
}
```

![](IBFreeFormPreview.gif)

## 📖 Documentation
- ibSubviews - Define the hierarchy of views, similar to Interface Builder's Document Outline. When applied to a UIStackView, the DSL uses the `addArrangedSubview` method
- ibAttributes - Configure attributes and constraints of a view. This corresponds to Interface Builder’s Identity, Attributes, Size, and Connections Inspectors.

⚠️ When defining a constraint, either the first or second item must be the same view to which you are applying ibAttributes.

- ibApply - Similar to ibAttributes, but without a @resultBuilder for constraints — useful for custom configurations, it works with NSObject and UIView
- IBFreeForm - Wraps a UIView, UIViewController, or even a SwiftUI.View, allowing resizing in the simulator. You can also define a snapFrame to display frames of specific devices (e.g., iPhone SE).
- IBDebug - Provides showColors, showFrames to help visualize layout frames during debugging. Works only with UIKit.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import Foundation

extension NSObjectProtocol {


@discardableResult
public func ibApply(_ block: (Self) -> Void) -> Self {
block(self);
block(self)
return self
}

Expand Down
8 changes: 6 additions & 2 deletions Sources/UIViewKit/IBConstraints/IBConstraints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import UIKit
public final class IBConstraints {

private init() {}


@MainActor
public static func create(from: UIView, to: UIView, guide: LayoutGuide, anchors: ViewAnchor...) -> [NSLayoutConstraint] {
createConstraints(from: from, to: to, guide: guide, anchors: anchors)
}


@MainActor
static func createConstraints(from: UIView, to: UIView, guide: LayoutGuide, anchors: [ViewAnchor]) -> [NSLayoutConstraint] {
switch guide {
case .view:
Expand All @@ -26,6 +28,8 @@ public final class IBConstraints {
}
}

@MainActor
// swiftlint:disable:next cyclomatic_complexity
private static func createConstraints(from view: UIView, to target: Any, anchors: [ViewAnchor]) -> [NSLayoutConstraint] {
var constraints: [NSLayoutConstraint] = []
// swiftlint:disable force_cast
Expand Down
10 changes: 9 additions & 1 deletion Sources/UIViewKit/IBDebug/IBDebug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import UIKit

public final class IBDebug {

private init() {}

@MainActor
public static func showColors(of view: UIView, includeGivenView: Bool = true, includeUIKitPrivateViews: Bool = false) {
let colors = [UIColor.red, .blue, .brown, .cyan, .darkGray, .magenta, .green, .lightGray, .orange, .purple, .yellow]
if includeGivenView {
Expand All @@ -21,6 +22,7 @@ public final class IBDebug {
}
}

@MainActor
public static func showFrames(of view: UIView, borderColor: UIColor? = nil, includeGivenView: Bool = true, includeUIKitPrivateViews: Bool = false) {
if includeGivenView {
view.layer.borderWidth = 1
Expand All @@ -36,6 +38,7 @@ public final class IBDebug {
}
}

@MainActor
public static func allSubviews(of view: UIView, includeUIKitPrivateViews: Bool = false) -> [UIView] {
var all = [UIView]()

Expand All @@ -52,6 +55,7 @@ public final class IBDebug {
return all
}

@MainActor
public static func allSubviewsPrettyString(of view: UIView, includeGivenView: Bool, includeUIKitPrivateViews: Bool = false) -> String {
let allSubviews: [UIView]
if includeGivenView {
Expand Down Expand Up @@ -79,10 +83,12 @@ public final class IBDebug {
return output
}

@MainActor
public static func allSubviewsPrettyPrint(of view: UIView, includeGivenView: Bool, includeUIKitPrivateViews: Bool = false) {
print(allSubviewsPrettyString(of: view, includeGivenView: includeGivenView, includeUIKitPrivateViews: includeUIKitPrivateViews), separator: "\n")
}

@MainActor
public static func showViewsWhichHasAmbiguousLayout(for view: UIView) {

IBHelper.allSubviews(of: view).forEach { subview in
Expand Down Expand Up @@ -114,6 +120,8 @@ public final class IBDebug {
}

final class IBHelper {

@MainActor
static func allSubviews(of view: UIView) -> [UIView] {
view.subviews.flatMap {
[$0] + IBHelper.allSubviews(of: $0)
Expand Down
Loading