Clean Object Configuration in Swift with `with` and `withCopy`
Two Swift utility functions inspired by Kotlin's scope functions for cleaner object configuration.
Kotlin’s apply and also scope functions let you configure objects inline without temporary variables. Swift doesn’t have these built-in, but we can create something just as elegant.
The Problem
Consider configuring a UIButton:
1
2
3
4
5
6
let button = UIButton(type: .system)
button.setTitle("Submit", for: .normal)
button.backgroundColor = .systemBlue
button.layer.cornerRadius = 8
button.titleLabel?.font = .preferredFont(forTextStyle: .headline)
return button
This works, but configuration is scattered. We need a mutable var or rely on the object being a class. It gets worse when returning from a closure or configuring multiple objects.
The Solution
Two utility functions that change everything:
1
2
3
4
5
6
7
8
9
10
11
12
@discardableResult
public func with<T>(_ object: T, _ configure: (T) throws -> Void) rethrows -> T {
try configure(object)
return object
}
@discardableResult
public func withCopy<T>(_ value: T, _ configure: (inout T) throws -> Void) rethrows -> T {
var copy = value
try configure(©)
return copy
}
with for Reference Types
Use with when working with classes. The object is passed directly to the closure:
1
2
3
4
5
let label = with(UILabel()) {
$0.text = "Hello"
$0.textColor = .systemBlue
$0.font = .preferredFont(forTextStyle: .headline)
}
Or inline in a lazy property:
1
2
3
4
5
6
lazy var tableView = with(UITableView()) {
$0.delegate = self
$0.dataSource = self
$0.rowHeight = UITableView.automaticDimension
$0.register(MyCell.self, forCellReuseIdentifier: "cell")
}
withCopy for Value Types
Use withCopy when you need to configure structs. It creates a mutable copy:
1
2
3
4
let rect = withCopy(CGRect.zero) {
$0.size.width = 100
$0.size.height = 50
}
Or modifying existing values:
1
2
3
4
let updatedInsets = withCopy(originalInsets) {
$0.top += 16
$0.bottom += 16
}
Why Two Functions?
Classes technically conform to both signatures since they can be passed as inout too. Swift’s compiler gets confused with a single function:
1
2
// "Ambiguous use of 'with'"
let params = with(SomeClass()) { $0.property = value }
Separating them with distinct names makes the intent clear and the compiler happy.
Implementation Breakdown
1
2
3
4
5
@discardableResult
public func with<T>(_ object: T, _ configure: (T) throws -> Void) rethrows -> T {
try configure(object)
return object
}
@discardableResult— Silences warnings when you don’t need the return value<T>— Generic over any typethrows/rethrows— The closure can throw, and the error propagates up- Returns the same object for chaining or assignment
1
2
3
4
5
6
@discardableResult
public func withCopy<T>(_ value: T, _ configure: (inout T) throws -> Void) rethrows -> T {
var copy = value
try configure(©)
return copy
}
inout T— Allows mutation of the copied valuevar copy— Creates a mutable copy of the value type- Returns the modified copy
Real-World Example
Here’s how it cleans up a view controller setup:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class ProfileViewController: UIViewController {
private lazy var stackView = with(UIStackView()) {
$0.axis = .vertical
$0.spacing = 16
$0.alignment = .fill
}
private lazy var avatarView = with(UIImageView()) {
$0.contentMode = .scaleAspectFill
$0.layer.cornerRadius = 40
$0.clipsToBounds = true
}
private lazy var nameLabel = with(UILabel()) {
$0.font = .preferredFont(forTextStyle: .title1)
$0.textAlignment = .center
}
}
No temporary variables. No scattered configuration. Just clean, readable intent.
☕ Support My Work
If you found this post helpful and want to support more content like this, you can buy me a coffee!
Your support helps me continue creating useful articles and tips for fellow developers. Thank you! 🙏