Write a function that checks whether one CGRect fully contains another, then explain how you decide whether a touch lies inside a view that sits inside a scrolled parent.
Predict first
What do the containment checks and the conversions print?
import UIKit
func contains(_ outer: CGRect, _ inner: CGRect) -> Bool {
let o = outer.standardized, i = inner.standardized // normalise negative width/height
guard !o.isNull, !i.isNull else { return false }
return i.minX >= o.minX && i.maxX <= o.maxX && i.minY >= o.minY && i.maxY <= o.maxY
}
let outer = CGRect(x: 0, y: 0, width: 100, height: 100)
print(contains(outer, CGRect(x: 10, y: 10, width: 20, height: 20)))
print(contains(outer, outer)) // touching edges still count as inside
print(contains(outer, CGRect(x: 90, y: 90, width: 20, height: 20)))
print(contains(outer, CGRect(x: 30, y: 30, width: -20, height: -20))) // negative size means x 10...30
print(contains(outer, .null))
print(outer.contains(CGRect(x: 10, y: 10, width: 20, height: 20))) // the built-in does the same job
// Coordinate spaces: a point is only meaningful together with the view it is measured in.
let root = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
let scroll = UIView(frame: CGRect(x: 20, y: 40, width: 200, height: 300))
let button = UIView(frame: CGRect(x: 10, y: 100, width: 80, height: 44))
root.addSubview(scroll); scroll.addSubview(button)
let tapInRoot = CGPoint(x: 45, y: 120)
print("button frame in root:", scroll.convert(button.frame, to: root))
print("tap in button space:", root.convert(tapInRoot, to: button))
print("hit before scroll:", button.bounds.contains(root.convert(tapInRoot, to: button)))
scroll.bounds.origin = CGPoint(x: 0, y: 60) // content scrolled down by 60
print("button frame in root:", scroll.convert(button.frame, to: root))
print("hit after scroll:", button.bounds.contains(root.convert(tapInRoot, to: button)))
- true / true / false / true / false / true, then button frame in root (30, 140, 80, 44), tap in button space (15, -20), hit false, then frame y 80 and hit true after scrolling
- true / false / false / true / false / true, then the same frame both times
- true / true / false / false / true / true, hit true before scrolling
Mental model. Two people give directions from different starting points. “Three blocks north” means nothing until both are translated to the same map.
Containment is four comparisons: the inner rectangle’s minX and minY must be at least the outer’s, and its maxX and maxY at most the outer’s. Two details make it robust. Call standardized first, because a CGRect may have a negative width or height (x: 30, width: -20 covers 10 to 30), and the comparisons assume positive sizes. And reject .null (and say what you do for empty and infinite rectangles). Touching edges count as inside, so a rectangle contains itself; the built-in outer.contains(inner) gives the same answers for these cases, as the check shows, so say you would normally use it and explain what it does. Reported at Meta (anonymous, unverified).
The second half is about coordinate spaces. A touch point comes in the window or root view’s space; a button’s frame is in its superview’s space (Q239). To test it, convert the point into the button’s own space with root.convert(point, to: button) and check button.bounds.contains(local), or convert the button’s frame into the root’s space with scroll.convert(button.frame, to: root). A scroll view’s bounds origin is its content offset: with the parent scrolled by 60, the same button’s frame in the root moves up by 60 (from y 140 to y 80), and a tap that missed it at y 120 now hits, as the program shows. Never compare raw coordinates from different spaces. Run in the iOS Simulator.
Edges may touch, negative sizes are normalised, null is rejected, and scrolling the parent by 60 moves the button’s frame in root space up by 60.
Common traps
- Comparing a touch in window space with a frame in the superview’s space. Convert one into the other’s space first.
- Forgetting
standardized. A rectangle with a negative size makes the min/max comparisons wrong.
They might follow up with
- What if the view is rotated or scaled?
convert(_:to:)accounts for the transform; do not hand-compute it fromframe(Q239). - How do hit tests decide which view gets a touch? UIKit walks down the tree converting the point at each level (Q235).
Related questions
- 7.2.2 What is the difference between frame and bounds, and what happens to them under a transform?
- 7.1.1 How does UIKit decide which view receives a touch, and why can a subview outside its parent’s bounds not be…
- 23.2.1 How do you debug layout and views with the view debugger, and how do you inspect what your app really sends…