We have a weird case where our view is not appearing as expected and we've traced it to AutoLayout and some sequence of events that we can't see why it would change things.
To simplify the example, here's some test code you can run directly in an Xcode playground. (We're using Xcode 11.3.1 if it matters.)
First, we define an InlineConfigure
operator, like so...
// 'InlineConfigure' operator
infix operator ~> : AssignmentPrecedence
@discardableResult
public func ~> <T>(item:T, _ configure:(T)->Void) -> T {
configure(item)
return item
}
Next, to illustrate it's not a case of something being deallocated when not expected, we create a UIView
subclass specifically to log that...
class SomeView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
print("SomeView created")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("SomeView deinitialized")
}
}
With both of the above in place, take a look at this code. It works as expected.
class TestViewController : UIViewController {
let margin:CGFloat = 24
override func loadView() {
view = UIView()
view.backgroundColor = .yellow
SomeView()~>{
$0.backgroundColor = .blue
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
NSLayoutConstraint.activate([
$0.leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin),
$0.topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
$0.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin),
$0.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin)
])
}
}
}
PlaygroundPage.current.liveView = TestViewController()
If we however change the line that creates SomeView
to take in a frame that has a non-zero area (e.g. both a width and a height) then that same code no longer works and you don't see the blue view at all.
override func loadView() {
view = UIView()
view.backgroundColor = .yellow
SomeView(frame:CGRect(x: 0, y: 0, width: 20, height: 20))~>{
$0.backgroundColor = .blue
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
NSLayoutConstraint.activate([
$0.leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin),
$0.topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
$0.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin),
$0.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin)
])
}
}
Setting the frame to have a zero area and it works again...
SomeView(frame:CGRect(x: 0, y: 0, width: 0, height: 20))~>{
[...]
}
But back to the non-zero frame, if you move the constraint-setting code to outside of the configuration closure, then it always works regardless of the frame having an area or not!
override func loadView() {
view = UIView()
view.backgroundColor = .yellow
let someView = SomeView(frame:CGRect(x: 0, y: 0, width: 20, height: 20))~>{
$0.backgroundColor = .blue
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
NSLayoutConstraint.activate([
someView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin),
someView.topAnchor.constraint(equalTo: view.topAnchor, constant: margin),
someView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin),
someView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -margin)
])
}
What's confusing us is in the earlier code, $0
inside the closure is a strong reference to the newly-created view (and the parent also holds a reference to it when it was added as a subview so it's not unloading), and that exact same reference is assigned to someView
on the outside where it then executes the exact same code that used to be in the closure. There is nothing in between the closure and the code on the outside except that assignment, yet it's giving different results! But why?
Can anyone explain why we're seeing this behavior? What are we missing?
I'd say it's because playgrounds are the work of the devil. I ran your code in a real iOS app project and it worked as expected even with
SomeView(frame:CGRect(x: 0, y: 0, width: 20, height: 20))
Basically I wouldn't expect a playground to go through all the lifetime stages of a view controller correctly.