Most of the screen transitions that happen in our apps falls into this categories:
- modal presentations
- push/pop on a navigation stack
And while this is enough for most of the work we need to do, there are some edge cases where we might want to adopt a less “native” and more custom solution.
Fortunately for us, UIKit provides exactly the right tools for the job. As always, we don’t have to go too far from the official documentation (View Controller Programming Guide), more in detail, we’re looking for Creating Custom Presentations. This guides are in the documentation archive and with Objective-C examples, but they’re still the best content you’ll find out there.
Reading the guide will reveal a few interesting points. When a view controller is about to be presented, UIKit does the following:
- Calls the
presentationControllerForPresentedViewController:presentingViewController:sourceViewController:
method of the transitioning delegate to retrieve your custom presentation controller. - Asks the transitioning delegate for the animator and interactive animator objects, if any.
- Calls your presentation controller’s
presentationTransitionWillBegin
method. - Performs the transition animations. During the animation process, UIKit calls the
containerViewWillLayoutSubviews
andcontainerViewDidLayoutSubviews
methods of your presentation controller so that you can adjust the layout of your custom views as needed. - Calls the
presentationTransitionDidEnd
: method when the transition animations finish.
So we have a few points where we can operate and change both the animation and presentation styles.
Presenting a banner
Let’s say we were to upload a file to our servers and we wanted to notify the user once the operation is completed:
func upload(file: File, using uploader: FileUploader) {
uploader.send(file, then: {
let banner = Banner(message: "File successfully uploaded ✅")
self.present(banner, animated:true)
})
}
Now this would present our banner full-screen modally, but we might want to make it look like a banner on the bottom of the screen, so that it is less invasive. We’ll focus on a custom presentation, while using the default transition from the bottom.
First thing, let’s create our custom presentation controller:
class BannerPresentationController: UIPresentationController {
override var frameOfPresentedViewInContainerView: CGRect {
//here we should compute the frame for the presented banner
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
}
override func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
presentedView?.layer.cornerRadius = 12
}
}
The key here is the frameOfPresentedViewInContainerView
property, which we’ll use to calculate the appropriate size for the banner.
We should take into account the safe area and calculate the required height given a fixed width (the screen width minus the insets).
The tool for the job is the UIView.systemLayoutSizeFitting
systemLayoutSizeFitting(targetSize:horizontalFittingPriority:verticalFittingPriority:)
method, which calculates the required size for a view, based on its constraints or intrinsic content size.
Our implementation should look something like this:
override var frameOfPresentedViewInContainerView: CGRect {
let safeBounds = containerView.bounds.inset(by: containerView.safeAreaInsets)
let inset: CGFloat = 16
let targetWidth = safeBounds.width - 2*16
let targetSize = CGSize(
width: targetWidth,
height: UIView.layoutFittingCompressedSize.height
)
let targetHeight = presentedView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow).height
return CGRect(x: inset, y: yPosition, width: targetWidth, height: targetHeight)
}
Putting the pieces together
Now all we need to do is to assign out brand new BannerPresentationController
as the presentation controller for our Banner
:
extension Banner: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return BannerPresentationController(presentedViewController: presented, presenting: presenting)
}
}
//when presenting the banner
banner.transitioningDelegate = banner
And specify a custom presentation style:
banner.modalPresentationStyle = .custom
And the result will be this:
Conclusion
Despite being very far from the latest trends in UI development like reactive concepts, UIKit has shown that it is a powerful framework with multiple customization points.
Hope you liked this little experiment 😊