I am trying to recreate the sheet transition animation like the one in Apple Maps. For example, when you tap on a place, another sheet pops on top of the current sheet and it sorts of ‘merges’ with the sheet right below it.
Here’s a video of what I am exactly referring to: https://streamable.com/guz5cf
Like how do I go about doing it? Searched all over the place with zero information
Is it a view controller or a UIView and are the any masking/blend modes involved? I am trying to understand what sorts of techniques can be used to achieve it. Custom presentation controllers?
Would appreciate if anyone can give a simple example to start from.
This is the code I tried but it is no where close to what is in Apple Maps
import UIKit
class ViewControllerStack {
static let shared = ViewControllerStack()
var views: [UIViewController] = []
}
class ViewController: UIViewController {
override func loadView() {
view = MKMapView()
}
override func viewDidAppear(_ animated: Bool) {
let welcomeVC = WelcomeViewController()
welcomeVC.isModalInPresentation = true
if let sheet = welcomeVC.sheetPresentationController {
sheet.detents = [.custom { context in
return 200
}, .medium(), .large()]
sheet.prefersGrabberVisible = true
sheet.preferredCornerRadius = 15
sheet.largestUndimmedDetentIdentifier = .large
}
present(welcomeVC, animated: true)
ViewControllerStack.shared.views.append(welcomeVC)
}
}
class WelcomeViewController: UIViewController {
lazy var blurEffectView: UIVisualEffectView = {
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = view.bounds
return blurEffectView
}()
override func viewDidLoad() {
view.addSubview(blurEffectView)
view.sendSubviewToBack(blurEffectView)
let button = UIButton()
button.setTitle("Go to View Controller 2", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.widthAnchor.constraint(equalToConstant: 200),
button.heightAnchor.constraint(equalToConstant: 44)
])
}
@objc func buttonTapped() {
let vc2 = ViewController2()
vc2.modalPresentationStyle = .currentContext
vc2.modalTransitionStyle = .coverVertical
present(vc2, animated: true)
UIView.animate(withDuration: 0.75) {
self.view.alpha = 0
self.view.layer.masksToBounds = true
}
ViewControllerStack.shared.views.append(vc2)
}
}
class ViewController2: UIViewController {
override func viewDidLoad() {
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = view.bounds
view.addSubview(blurEffectView)
view.sendSubviewToBack(blurEffectView)
let button = UIButton()
button.setTitle("Go back to Welcome VC", for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.widthAnchor.constraint(equalToConstant: 200),
button.heightAnchor.constraint(equalToConstant: 44)
])
}
@objc func buttonTapped() {
ViewControllerStack.shared.views.removeLast()
guard let lastVCInStack = ViewControllerStack.shared.views.last else { return }
lastVCInStack.view.alpha = 1
lastVCInStack.dismiss(animated: true)
}
}
2
Answers
The following is code that reproduces the sheet transition animation like in Apple Maps.
To run this code, create a new iOS app project in Xcode. Replace the contents of ViewController.swift with the code below and then add two new Swift files (SheetVC.swift and ModalVC.swift) as below.
When run, ViewController shows a plain yellow background with a "Present" button. This simulates having a map with tappable points of interest. An initial sheet is shown with SheetVC. This simulates the initial Maps app sheet when no point of interest is selected. Tapping the "Present" button will show a new sheet (ModalVC) over the original with a random color. Tapping the "Present" button again will replace the first color with a new color. This simulates selecting a new point of interest on the map. Dismissing ModalVC (with the close button) reveals the original sheet (SheetVC).
Start by adding these custom detents (add these to ViewController.swift or some other file you desire):
These custom detents better simulate the three detents of the Maps app.
ViewController.swift (this would likely be the VC with the map view):
SheetVC.swift (the VC controlling the initial sheet’s contents):
ModalVC.swift (the VC controlling the details shown in the 2nd sheet):
There is a bit to unpack here…
You are trying to emulate the "double sheet" behavior of the Apple Maps app. It’s pretty straightforward to present a sheet controller, and then present another one on top of it – which is what it appears Maps does.
However, based on your comments (and your multiple other questions), you want the sheet(s) to have translucency.
Couple issues with that.
When views with less-than 100% opacity overlap, the alphas get added together. In this image, I have two views with white background, each set to
.alpha = 0.75
:As we see, the overlap area is much more opaque. This is not noticeable in the Maps app, because the sheet views have an alpha of probably
0.99
or0.995
— so the single sheet is just barely translucent, and when the second sheet is presented on top there is virtually no translucency. One has to look really, really closely to even see it.The second issue is that a presented sheet controller has a shadow behind it.
Here is how it looks when presenting a controller with a completely clear background:
and, when presenting a second sheet on top of it, we get a darker shadow (two shadow layers):
If we drag the second sheet up to a taller detent, it’s very obvious what’s going on:
Again, in the Maps app, not noticeable because the sheets have almost-opaque backgrounds… however, if we do a screen-cap and zoom way in, we can see the extra shadow: https://phpout.com/wp-content/uploads/2024/01/IzzZS.png
So, how to get this appearance / behavior:
One approach is to present a single controller which will hold two "container" views. We load the "content" controllers as children, and add their views to the "container" views, positioning the "detail" view below the "welcome" view.
To show the "detail" view, we can slide it up, collapsing the height of the "welcome" view. Reverse the process to "dismiss" it.
Here is some quick example code – no
@IBOutlet
or@IBAction
connections… just create a new project and use a default view controller.View Controller class
Container Controller class – this will be sheet-presented
Welcome Controller class
Detail Controller class
Data class – so we can show "related" images
Please note: this is Example Code Only!!!
It should not be considered "production ready" — it’s intended to be a Learning exercise.