Closed yoiang closed 5 years ago
Hi Ian. Interesting question.
If I understand you correctly you don't want to set an initial frame on your SUT view and that it has AutoLayout constraints that size itself. I usually let the parent set constraints on child views so I can't say I've stumped upon this use case and therefore I can't say I recommend a certain way of doing this.
SnapshotTest does layout AutoLayout constraints before recording and comparing snapshots so what you can do is wrap the view in a containing view that is larger than the SUT view.
Consider the SUT:
class SelfSizingView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setUp()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUp()
}
func setUp() {
backgroundColor = .red
NSLayoutConstraint.activate([
widthAnchor.constraint(equalToConstant: 100),
heightAnchor.constraint(equalToConstant: 100)
])
}
}
Then in your test case:
class SelfSizingViewTests: SnapshotTestCase {
func testSelfSizingView() {
let wrapper = UIView(frame: CGRect(origin: .zero, size: CGSize(width: .max, height: .max)))
let sut = SelfSizingView(frame: .zero)
sut.translatesAutoresizingMaskIntoConstraints = false
wrapper.addSubview(sut)
NSLayoutConstraint.activate([
sut.centerXAnchor.constraint(equalTo: wrapper.centerXAnchor),
sut.centerYAnchor.constraint(equalTo: wrapper.centerYAnchor)
])
RecordSnapshot(sut)
}
}
Just make sure you snapshot the SUT and not it's container so you only get the self sized view you want:
If this is a common use case for you it might be a good idea to build a helping type. I tried making one but in all honesty it feels sort of hacky.
func wrap(_ view: UIView, then: (UIView) -> Void) {
let wrapper = WrapperView(for: view)
wrapper.layoutIfNeeded()
then(view)
}
class WrapperView : UIView {
init(for view: UIView, size: CGSize = CGSize(width: .max, height: .max)) {
super.init(frame: CGRect(origin: .zero, size: size))
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
NSLayoutConstraint.activate([
view.centerXAnchor.constraint(equalTo: centerXAnchor),
view.centerYAnchor.constraint(equalTo: centerYAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In your test you could just:
class SelfSizingViewTests: SnapshotTestCase {
func testSelfSizingView() {
wrap(SelfSizingView(frame: .zero)) { sut in
RecordSnapshot(sut)
}
}
func testSelfSizingView_withAlterationBlue() {
wrap(SelfSizingView(frame: .zero)) { sut in
sut.backgroundColor = .blue
RecordSnapshot(sut)
}
}
}
The reason I think it's hacky is that I only do wrapper.layoutIfNeeded()
to get rid of the warning that wrapper
is unused. You just need a reference to it so it doesn't get released before the snapshot is recorded.
I bet we could come up with a better solution but this is what I would do off the top of my head.
Ah that works brilliantly, thanks so much! I tried similar but my wrapper was instead sized zero and I had hoped layout-ing would push both the wrapper and my wrapped view out with no luck. I never thought to give is as much space as it needed, feels like a duhhh moment ;)
Rather than the layoutIfNeeded
we could ensure post wrap
our View is rid of any "unexpected" (perhaps by the writer of the test) side effects, in our case becoming a subview.
extension UIView {
public func removeAllSubviews() {
while self.subviews.count > 0
{
self.subviews.last?.removeFromSuperview()
}
}
}
func wrap(_ view: UIView, then: (UIView) -> Void) {
let wrapper = WrapperView(for: view)
then(view)
wrapper.removeAllSubviews()
}
Then we could rename it from wrap
to autolayoutForTest
or something along those lines.
Glad I could help.
I'm a little surprised that this is the first time that this situation has come up, from my (obviously very specific 😉) point of view I could see the above code having a place in the SnapshotTest
code, making it available to others.
I don't think it's widely used is all. Our modus has been to build for our needs and not make assumptions regarding how other people do things.
That said, incorporating a wrapping system is absolutely something that could reasonably be a part of SnapshotTest. I started working on a more generic Wrappable
that was a convenience wrapper for cases where a Snapshotable
would need wrapping to cater to AutoLayout's behavior.
The problem domain was snapshotting cells such as UICollectionViewCell
and UITableViewCell
since UIReusableView
has odd behavior in juxtaposition to it's more predictable parent UIView
. I imagine the type could be extended to also cater for use cases like yours.
Hello! I'd love to adopt
SnapshotTest
for a set of views, however they are sized based on their content and constraints. Is there a recommended setup so that they'll properly resolve their constraints and size themselves?Cheers!