I have a grid view, it's like a chess board. The hierarchy is this :
UIScrollView
-- UIView
---- [UIViews]
Here's a screenshot.
Knowing that a tile has width and height of tileSide
, how can I find a way to programmatically zoom in focusing on the area with the blue border? I need basically to find the right zoomScale
.
What I'm doing is this :
let centralTilesTotalWidth = tileSide * 5
zoomScale = CGFloat(centralTilesTotalWidth) / CGFloat(actualGridWidth) + 1.0
where actualGridWidth
is defined as tileSide
multiplied by the number of columns. What I'm obtaining is to see almost seven tiles, not the five I want to see.
Keep also present that the contentView
(the brown one) has a full screen frame, like the scroll view in which it's contained.
You can do this with zoom(to rect: CGRect, animated: Bool)
(Apple docs).
zoom(to:...)
Here is a complete example - all via code, no @IBOutlet
or @IBAction
connections - so just create a new view controller and assign its custom class to GridZoomViewController
:
class GridZoomViewController: UIViewController, UIScrollViewDelegate {
let scrollView: UIScrollView = {
let v = UIScrollView()
return v
}()
let contentView: UIView = {
let v = UIView()
return v
}()
let gridStack: UIStackView = {
let v = UIStackView()
v.axis = .vertical
v.distribution = .fillEqually
return v
}()
var selectedTiles: [TileView] = [TileView]()
override func viewDidLoad() {
super.viewDidLoad()
[gridStack, contentView, scrollView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
var bColor: Bool = false
// create a 9x7 grid of tile views, alternating cyan and yellow
for _ in 1...7 {
// horizontal stack view
let rowStack = UIStackView()
rowStack.translatesAutoresizingMaskIntoConstraints = false
rowStack.axis = .horizontal
rowStack.distribution = .fillEqually
for _ in 1...9 {
// create a tile view
let v = TileView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = bColor ? .cyan : .yellow
v.origColor = v.backgroundColor!
bColor.toggle()
// add a tap gesture recognizer to each tile view
let g = UITapGestureRecognizer(target: self, action: #selector(self.tileTapped(_:)))
v.addGestureRecognizer(g)
// add it to the row stack view
rowStack.addArrangedSubview(v)
}
// add row stack view to grid stack view
gridStack.addArrangedSubview(rowStack)
}
// add subviews
contentView.addSubview(gridStack)
scrollView.addSubview(contentView)
view.addSubview(scrollView)
let padding: CGFloat = 20.0
// respect safe area
let g = view.safeAreaLayoutGuide
// for scroll view content constraints
let cg = scrollView.contentLayoutGuide
// let grid width shrink if 7:9 ratio is too tall for view
let wAnchor = gridStack.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 1.0)
wAnchor.priority = .defaultHigh
NSLayoutConstraint.activate([
// constrain scroll view to view (safe area), all 4 sides with "padding"
scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: padding),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: padding),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -padding),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -padding),
// constrain content view to scroll view contentLayoutGuide, all 4 sides
contentView.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
// content view width and height equal to scroll view width and height
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: 0.0),
contentView.heightAnchor.constraint(equalTo: scrollView.frameLayoutGuide.heightAnchor, constant: 0.0),
// activate gridStack width anchor
wAnchor,
// gridStack height = gridStack width at 7:9 ration (7 rows, 9 columns)
gridStack.heightAnchor.constraint(equalTo: gridStack.widthAnchor, multiplier: 7.0 / 9.0),
// make sure gridStack height is less than or equal to content view height
gridStack.heightAnchor.constraint(lessThanOrEqualTo: contentView.heightAnchor),
// center gridStack in contentView
gridStack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0.0),
gridStack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0.0),
])
// so we can see the frames
view.backgroundColor = .blue
scrollView.backgroundColor = .orange
contentView.backgroundColor = .brown
// delegate and min/max zoom scales
scrollView.delegate = self
scrollView.minimumZoomScale = 0.25
scrollView.maximumZoomScale = 5.0
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return contentView
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: nil, completion: {
_ in
if self.selectedTiles.count == 2 {
// re-zoom the content on size change (such as device rotation)
self.zoomToSelected()
}
})
}
@objc
func tileTapped(_ gesture: UITapGestureRecognizer) -> Void {
// make sure it was a Tile View that sent the tap gesture
guard let tile = gesture.view as? TileView else { return }
if selectedTiles.count == 2 {
// if we already have 2 selected tiles, reset everything
reset()
} else {
// add this tile to selectedTiles
selectedTiles.append(tile)
// if it's the first one, green background, if it's the second one, red background
tile.backgroundColor = selectedTiles.count == 1 ? UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0) : .red
// if it's the second one, zoom
if selectedTiles.count == 2 {
zoomToSelected()
}
}
}
func zoomToSelected() -> Void {
// get the stack views holding tile[0] and tile[1]
guard let sv1 = selectedTiles[0].superview,
let sv2 = selectedTiles[1].superview else {
fatalError("problem getting superviews! (this shouldn't happen)")
}
// convert tile view frames to content view coordinates
let r1 = sv1.convert(selectedTiles[0].frame, to: contentView)
let r2 = sv2.convert(selectedTiles[1].frame, to: contentView)
// union the two frames to get one larger rect
let targetRect = r1.union(r2)
// zoom to that rect
scrollView.zoom(to: targetRect, animated: true)
}
func reset() -> Void {
// reset the tile views to their original colors
selectedTiles.forEach {
$0.backgroundColor = $0.origColor
}
// clear the selected tiles array
selectedTiles.removeAll()
// zoom back to full grid
scrollView.zoom(to: scrollView.bounds, animated: true)
}
}
class TileView: UIView {
var origColor: UIColor = .white
}
It will look like this to start:
The first "tile" you tap will turn green:
When you tap a second tile, it will turn red and we'll zoom in to that rectangle:
Tapping a third time will reset to starting grid.