As a number of people pointed out, using keypath initialization for uniform type members has its bright spots and its drawbacks. The particular use-case I wrote about (UIEdgeInsets
) has four same-type members, and two common approaches: set all insets to the same value and set the vertical and horizontal inset values in tandem.
Because of this specific extension (adding vertical
, etc members), you open the door to potential bugs, namely:
let insets: UIEdgeInsets = [\.horizontal: 8, \.left: 4]
In this example, the developer intent is unclear. Should the left
inset override the horizontal
choice? Or should this be disallowed or warned? This occurs almost entirely to (my) bad design in letting \.horizontal
and its fellow compound members act as initializer choices. Get rid of the compound settable members, get rid of the problem.
A similar bug occurs like this:
let insets: UIEdgeInsets = [\.left: 4, \.right: 4, \.left: 8]
Since there’s no checking in this approach, you can easily duplicate entries without the compiler warning or emitting an error. The Swift compiler does not complain about duplicate keys in dictionary literals (SR-7066). Aside from that, it’s a cute way to do an unconventional memberwise initialization but one that probably should never make its way into production code.
So what would be a better choice for this example? A custom initializer wouldn’t hurt. For example (and I’m not sure the tests against zero are helping or hurting performance here):
extension UIEdgeInsets { public init(all: CGFloat = 0) { self.init() if all != 0 { (top, bottom, left, right) = (all, all, all, all) } } public init(vertical: CGFloat = 0, horizontal: CGFloat = 0) { self.init() if vertical != 0 { (top, bottom) = (vertical, vertical) } if horizontal != 0 { (left, right) = (horizontal, horizontal } } }
This gives you two convenience initializers specific to common use cases. You could alternatively write some kind of static method that builds an instance (something like UIEdgeInsets.inset(by:)
) but I don’t think that gets you anything big beyond normal initializers.
Getting back to using dictionary literals, someone asked if you can use the same approach to create ad-hoc, unordered, type-erased member initialization. I’m leaning towards “no” but you might be able to play with some kind of Initializable
protocol (ensuring init()
) and then looking up the value for each keypath and grab a type from that. It would be a pain. If you do get something like that working, drop me a note. At the least, it would be “unSwiftlike” and antithetical to type safety.
4 Comments
Here is a little trick I like to use when setting up complex objects (inspired by C# syntax):
infix operator =>
func => (lhs: T, rhs: (T) -> Void) -> T {
rhs(lhs)
return lhs
}
and then use it as follows:
let viewController = UILabel() => {
$0.backgroundColor = .black
$0.textColor = .white
}
When it comes to views, here is another trick for building hierarchies:
extension UIView {
func addSubview(_ type: T.Type, configure: (T) -> Void) -> T {
let subview = type.init(frame: .zero)
subview.translatesAutoresizingMaskIntoConstraints = false
configure(subview)
addSubview(subview)
return subview
}
}
I often do something along these lines:
let x: Type = {
$0.whatever = set up tasks
$0.whatever = set up tasks
$0.whatever = set up tasks
...
}(Type())
This allows you to create a `let` instance for both value and reference types. Brent and I also put in a proposal for `with` here: https://github.com/apple/swift-evolution/pull/346
The default value of an argument cannot be another argument. However, that does not mean it has to be a constant. It can be the return value of a function call.
Hey Erica!
as always good thoughts and insights!
Just forgot about enclosing parentheses
if horizontal != 0 { (left, right) = (horizontal, horizontal) }