mstfy
(Mustafa Yusuf)
1
Hi! I am writing a UIKit dsl library. My goal is to provide fluent api for UIView types. Like
UILabel()
.set(\.text, "Hello World")
.set(\.textColor, .label)
To create a generic set function I tried this:
extension Self where Self: UIView {
@discardableResult
mutating func set<Property>(
_ keyPath: WritableKeyPath<Self, Property>,
_ value: Property
) -> Self {
self[keyPath: keyPath] = value
return self
}
}
It seems this is not supported in current swift. As a workaround I created wrapper type like this:
final class Create<View> where View: UIView {
public var view: View
public init(_ view: View) {
self.view = view
}
@discardableResult
public func set<Property>(
_ keyPath: WritableKeyPath<View, Property>,
_ value: Property
) -> Self {
view[keyPath: keyPath] = value
return self
}
}
With this I can write the same code above as this:
Create(UILabel())
.set(\.text, "Hello World")
.set(\.textColor, .label)
This seems working but it creates syntax noise. So my question is how can I make this more clear?
mayoff
(Rob Mayoff)
2
One common solution is generic configuration function which I call with:
public func with<Value>(
_ value: Value,
do body: (_ value: inout Value) throws -> ()
) rethrows -> Value {
var value = value
try body(&value)
return value
}
We'd apply with to your example as follows:
let label = with (UILabel()) {
$0.text = "Hello World"
$0.textColor = .label
}
And here's an example that uses with in-line, copied from one of my projects:
let request = UNNotificationRequest(
identifier: posting.id,
content: with (UNMutableNotificationContent()) {
$0.title = note.title
$0.subtitle = note.subtitle
$0.body = note.body
$0.sound = note.soundName.map { UNNotificationSound(named: .init($0)) } ?? .default
},
trigger: nil
)
Sometimes you will see a function like this defined as a member of a type instead of as a top-level function. For example, swift-protobuf defines it as a static member of the Message protocol, like this:
extension Message {
public static func with(
_ populator: (inout Self) throws -> ()
) rethrows -> Self {
var message = Self()
try populator(&message)
return message
}
}
It lets you create and populate an instance of any protobuf message. Here's an example from one of my projects:
let message = AppMessage.with {
$0.type = .loginStatus
$0.loginStatus = .with {
$0.user = "test"
$0.state = .loginSuccess
$0.system = .demo
$0.region = .us
}
}
mayoff
(Rob Mayoff)
3
On the other hand, if you don't like the with function idea, you can make your keypath function work using a protocol. You can find the trick used in (for example) Publishers+KeyValueObserving.swift, which implements the Combine publisher(for:options:) method on NSObject.
The trick is to define a protocol, conform UIView (or even NSObject) to the protocol, and then extend the protocol where Self: UIView (or Self: NSObject). Thus:
protocol _KeyPathSetting { }
extension UIView: _KeyPathSetting { }
extension _KeyPathSetting where Self: UIView {
@discardableResult
func set<Property>(
_ keyPath: ReferenceWritableKeyPath<Self, Property>,
_ value: Property
) -> Self {
self[keyPath: keyPath] = value
return self
}
}
2 Likes
mstfy
(Mustafa Yusuf)
4
I go with protocol extension option. Now the code is looking much cleaner. Here is an example:
private func PizzaCell() -> UIStackView {
HorizontalStackView(alignment: .center, spacing: 10) {
UIImageView()
.set(\.image, UIImage(named: "pizzaHutLogo"))
VerticalStackView(alignment: .leading, spacing: 5) {
UILabel()
.set(\.text, "Pizza Hut Pizza Hut Pizza Hut Pizza Hut")
.set(\.font, .boldSystemFont(ofSize: 14))
.set(\.numberOfLines, 2)
.set(\.textColor, UIColor(white: 51/255, alpha: 1))
UILabel()
.set(\.text, "İtalyan Mutfağı")
.set(\.font, .systemFont(ofSize: 10))
.set(\.textColor, UIColor(white: 102/255, alpha: 1))
RatingLabel(rating: "4.8")
}
}
}
Thank you very much @mayoff
1 Like