قالب وردپرس درنا توس
Home / IOS Development / UIPresentationController Tutorial: Getting Started | raywenderlich.com

UIPresentationController Tutorial: Getting Started | raywenderlich.com



Update Note : Ron Kliffer updated this tutorial for iOS 13, Xcode 11 and Swift 5. He also wrote the original.

View controller presentation has been an integral part of every iOS developer toolkit since the first days of iOS.

You've probably used present (_: animated: completion :) before, but if you like many developers, you've kept up with standard transition styles provided with iOS.

In this UIPresentationController tutorial, you will learn how to present display controllers with custom transitions and custom presentation styles.

You will no longer be limited to full screen or popover presentations and standard transition animations. You start with some sad, lifeless viewer presentations and give them life.

When finished, learn how to do:

  1. Creating a UIPresentationController subclass.
  2. Use a UIP presentationController for smooth custom presentations. The presented display controller does not even need to know that it is there.
  3. Reuse a UIP presentationController for various controllers, by tweaking presentation parameters.
  4. Create adaptive presentations that support the screen size your app can reach.

Getting Started

With the summer games 2020 a year away, a client hires you to create an app that creates the medal number for the competing nations.

While the feature requirements are quite simple, your sponsor asked for a pretty cool look slide-in to present the list of games.

To begin with, you feel a bit panicked. But then you realize that has tools for transition-building at your fingertips. You even add the paper bag!

Use Download the materials button at the top or bottom of this tutorial to download the startup project.

Take the time to familiarize yourself with the project and the following elements:

  • MainViewController.swift : The main controller of this project from which all presentations start. This will be the only existing file in the project that you want to change.
  • GamesTableViewController.swift : Displays a list of games that the user can select.
  • MedalCountViewController.swift : Displays the medal number of the selected sporting event.
  • GamesDataStore.swift : Represents the model layer of the project. It is responsible for creating and storing model objects.

Before you start changing things around, build and run the app to see what it does. You see the following screen:

 App landing page before changes

First press Summer to bring up the summer menu, GamesTableViewController .

 Summer menu

Note that the menu presentation is standard, which is from the bottom. The client wants to see an elegant slide instead.

Then press London 2012 to reject the menu and return to the main screen. This time you will see an updated logo.

 London 2012

Finally, press Medal Count to bring up MedalCountViewController for the 2012 games.

 Number of medals

As you can see, this controller also uses the old bottom-up, standard presentation. Tap the screen to reject it.

Now you've seen the app you're upgrading, it's time to focus on some core concepts and theory for UIPresentationController .

Core concepts for iOS transition

When you call present (_: animated: completion :) iOS does three things.

First, it provides a UIP presentationController . Secondly, it associates the presented display controller with itself. Finally, it presents it using one of the built-in modal presentation styles.

You have the power to override this mechanism and give your own UIPresentationController subclass to a custom presentation.

To build sweet presentations in your apps, it is mandatory to understand these important components:

  1. The presented display controller has a transition delegate responsible for loading UIPresentationController and animation controls for presentation and parting. This delegate is an object that matches UIViewControllerTransitionDelegate .
  2. UIPresentationController subclass is an object that has many presentation matching methods. You will see some of these later in the tutorial.
  3. An animation controller is responsible for the presentation and dismissal animations. It matches UIViewControllerAnimatedTransitioning . Note that some uses warrant two controllers: one for presentation and one for parting.
  4. A presentation controller delegate tells the presentation controller what to do when the feature collection changes. For the sake of adaptability, the delegate must be an object that matches UIAdaptivePresentationControllerDelegate .

That's all you need to know before you dive in! [1965943] Creating the Transitioning Delegate transitingDelegate inherits from NSObject and is in accordance with UIViewControllerTransitionDelegate .

UIViewControllerTransitioningDelegate protocol, as the name suggests, declares five optional methods for managing transitions.

You will be working on three of these methods pretty much in this tutorial.

Setting up the framework

Go to File ▸ New ▸ file… select iOS ▸ Source ▸ Cocoa Touch Class and click Next . Set the name to SlideInPresentationManager makes it a subclass of NSObject and sets the language to Swift .

Click Next and set the group Presentation and then click Create .

Note : You declare SlideInPresentationManager to be a NSObject because transitional delegate of UIViewController must match to NSObjectProtocol .

Open SlideInPresentationManager.swift and add the following extension:

  // MARK: - UIViewControllerTransitionDelegate
extension SlideInPresentationManager: UIViewControllerTransitioningDelegate {
}

Here you make SlideInPresentationManager to match UIViewControllerTransitionDelegate .

In MainViewController you have buttons for both seasons: Summer left and Winter right. There is also a medal count on the bottom.

To get the presentations to fit each button's context, add a directional property to SlideInPresentationManager . Later, you will transfer this feature to presentation and animation controllers.

Add the following to the top of SlideInPresentationManager.swift :

  enum PresentationDirection {
again
matter top
thing right
thing bottom
}

Here you declare a single enum to represent the direction of the presentation.

Then add the following property to SlideInPresentationManager :

  was direction: PresentationDirection = .left

Here you add a direction and give it a default value of on the left .

Assigning an instance of SlideInPresentationManager as transitingDelegate for each controller you present, open MainViewController.swift and add the following above dataStore ] definition:

  lazy was slideInTransitioningDelegate = SlideInPresentationManager ()

You may ask yourself why you add this as a property on MainViewController . There are two reasons:

  1. transitDelegat is a weak property, so you need to keep a strong reference to the delegate somewhere.
  2. You will not keep this reference to the presented by the controller himself, which you may want to use on various presentation styles. Determining which presentation to use is now presenting the task of the control.

Next, find prepare (for: sender :) and replace it with the following:

  override func preparation (for segue: UIStoryboardSegue, sender: Someone?) {
if la controller = segue.destination as? GamesTableViewController {
if segue.identifier == "SummerSegue" {
controller.gamesArray = dataStore.allGames.summer

// 1
slideInTransitioningDelegate.direction =. left
} else if segue.identifier == "WinterSegue" {
controller.gamesArray = dataStore.allGames.winter

// 2
slideInTransitioningDelegate.direction = .right
}
controller.delegate = self

// 3
controller.transitioningDelegate = slideInTransitionDelegate

// 4
controller.modalPresentationStyle = .passed
} else if let controller = segue.destination as? MedalCountViewController {
controller.medalWinners = presented games? .medalWinners

// 5
slideInTransitioningDelegate.direction = .bottom
controller.transitioningDelegate = slideInTransitionDelegate
controller.modalPresentationStyle = .passed
}
}

Here is a section-by-section explanation of what you have set up:

  1. Presentation direction of the summer game menu is . Left .
  2. Presentation direction for the winter play menu is .right .
  3. The game controller transitionDelegate is now slideInTransitionDelegate declared earlier.
  4. modalPresentationStyle is .custom . This causes the presented controller to expect a custom presentation rather than an iOS presentation.
  5. Presentation Direction of MedalCountViewController is now .bottom . Transitional Delegate and modalPresentationStyle are set as you did in Steps 3 and 4.

You have reached the end of the section and have set you up with the foundation needed for the next big thing like comes towards you: the subclass UIPresentationController .

Creating UIPresentationController

Sit back and watch this for a moment: The presented controller will record two thirds of the screen and the remaining third will appear as dimmed. To deposit the controller, simply press the dimmed part. Can you see it?

Well done your head and come back to the project. In this section, you will take care of three critical pieces: subclass, dimming and customizing the transition.

Creating and initializing a UIPresentationController subclass

Go to File ▸ New ▸ file ... select iOS ▸ Source ▸ Cocoa Touch Class and click Next . Set the name to SlideInPresentationController makes it a subclass of UIPresentationController and set the language to Swift .

Click Next and set the group Presentation .

Click Create to create your new file and add the following to the class definition in SlideInPresentationController.swift :

  // 1
// NOTE: - Properties
private alert: Presentation direction

// 2
init (presentedViewController: UIViewController,
presenting presentingViewController: UIViewController?,
direction: Presentation Guide) {
self.direction = direction

// 3
super.init (presentViewController: presentViewController,
presenting: presentingViewController)
}

This is what this does:

  1. Declares direction to represent the direction of the presentation.
  2. Declares an initialization that accepts presented and presenting display controllers, as well as the presentation direction.
  3. Call the designated initializer for UIPresentationController .

Setting up the dimming display

As mentioned earlier, the presentation controller will have a dimmed background. Add the following in SlideInPresentationController directly above direction :

  privately was dimmingView: UIView!

Then add the following extension:

  // MARK: - Private
private extension SlideInPresentationController {
func setupDimmingView () {
dimmingView = UIView ()
dimmingView.translatesAutoresizingMaskIntoConstraints = false
dimmingView.backgroundColor = UIColor (white: 0.0, alpha: 0.5)
dimmingView.alpha = 0.0
}
}

Here you create the dimming view, prepare it for Auto Layout and set the background color. Note that you are not adding it to a monitoring yet. You will do so when the presentation transition starts, as you will see later in this section.

The presented controller must rarely make itself felt by pressing the dimmed view. Add the following under setupDimmingView () to get it:

  @objc func handleTap (recognizes: UITapGestureRecognizer) {
presentingViewController.dismiss (animated: true)
}

Here you create a UITapGestureRecognizer handler that rejects the controller.

Of course, you have to write it UITapGestureRecognizer then add the following to the bottom of setupDimmingView () :

  easy recognition = UITapGestureRecognizer (
goal: self,
action: #selector (handleTap (recognizes :))
dimmingView.addGestureRecognizer (connoisseur)

This adds a push motion to the fog display and connects it to the action you just added.

Finally, add a call to setupDimmingView () at the end of init (presentedViewController: present: direction :) :

  setupDimmingView ()

Override presentation control methods

Before you can begin customizing the transition, you must override four methods and a property from UIPresentationController . The standard methods do nothing, so there is no need to call super .

First, for a smooth transition, overrides TransitionWillBegin () to get the fog vision to fade along with the presentation. Add the following code in the main class definition in SlideInPresentationController.swift :

  override func presentationTransitionWillBegin () {
guard let dimmingView = dimmingView other {
return
}
// 1
Container view? .insertSubview (dimmingView, at: 0)

// 2
NSLayoutConstraint.activate (
NSLayoutConstraint.constraints (withVisualFormat: "V: | [dimmingView] |",
options: [] calculations: zero, views: ["dimmingView": dimmingView]))
NSLayoutConstraint.activate (
NSLayoutConstraint.constraints (withVisualFormat: "H: | [dimmingView] |",
options: [] calculations: zero, views: ["dimmingView": dimmingView]))

// 3
guard let coordinator = presentedViewController.transitionCoordinator other {
dimmingView.alpha = 1.0
return
}

coordinator.animate (along with transmission: {_ in
self.dimmingView.alpha = 1.0
})
}

This is what this code does:

  1. UIPresentationController has a property called containerView . It has the presentation hierarchy for the presentation and presented controllers. This section is where you insert dimmingView on the back of the display hierarchy.
  2. Next, limit the dimming display to the edges of the container display so that it fills the entire screen.
    Transition Coordinator by UIPresentationController has a very cool method of animating things during the transition. In this section, set the dimming view alpha to 1.0 along with the presentation transition.

Now you want to hide the fog when you dismiss the presented controller. Override ReferralTransitionWillBegin () by adding this code to the previous override method:

  override func dismissalTransitionWillBegin () {
guard let coordinator = presentedViewController.transitionCoordinator other {
dimmingView.alpha = 0.0
return
}

coordinator.animate (along with transmission: {_ in
self.dimmingView.alpha = 0.0
})
}

Like the presentation TransitionWillBegin () you set the dimming view alpha to 0.0 next to the termination. This gives the effect of fading the dimming display.

This next override will respond to layout changes in the presentation controller containerView . Add this code to the previous override method:

  overrides func containerViewWillLayoutSubviews () {
presented view? .frame = frameOfPresentedViewInContainerView
}

Here you reset the displayed presentation frame to fit all changes in the containerView frame.

Then, give the size of the contents of that presentation viewer to the presentation controller. Add this code to the previous override method:

  override func size (forChildContentContainer container: UIContentContainer,
withParentContainerSize parentSize: CGSize) -> CGSize {
change direction {
case. left right.
return CGSize (width: parentSize.width * (2.0 / 3.0), height: parentSize.height)
case. bottom, .top:
return CGSize (width: parentSize.width, height: parentSize.height * (2.0 / 3.0))
}
}

This method receives the content container and parent display size, and then calculates the size of the presented content. In this code, limit the display to 2/3 of the screen by returning the 2/3 width for horizontal and 2/3 height for vertical presentations.

In addition to calculating the size of the displayed view, you must return the entire frame. To do this, you will override frameOfPresentedViewInContainerView . Under the properties at the top of the class definition, add the following:

  override was frameOfPresentedViewInContainerView: CGRect {
// 1
was frame: CGRect = .zero
frame.size = size (forChildContentContainer: presentedViewController,
withParentContainerSize: containerView! .bounds.size)

// 2
change direction {
case. straight:
frame.origin.x = containerView! .frame.width * (1.0 / 3.0)
case. fine:
frame.origin.y = containerView! .frame.high * (1.0 / 3.0)
default:
frame.origin = .zero
}
return frame
}

Look at this code section for section:

  1. You declare a frame and give it the size calculated in size (forChildContentContainer: withParentContainerSize :) .
  2. For directions .right and .bottom adjust the origin by moving the x-origin ( .right ) and the y-original ( .right ) and the y-original (). bottom ) 1/3 of the width or height.

With all the overruns done, you are ready to make the finishing touches!

Implementation of presentation styles

Do you remember in the previous section how to create the transitionDelegate ? Well, it's there, but doesn't do much at the moment. There is a great need for some extra implementation.

Open SlideInPresentationManager.swift find the UIViewControllerTransitioningDelegate extension and add the following:

  func presentationController (
forPresented presented: UIViewController,
present: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
let presentationController = SlideInPresentationController (
presentViewController: presented,
present: present,
direction: direction
)
Return presentation controls
}

Here you provide a SlideInPresentationController with the direction from SlideInPresentationManager . You return it to be used for the presentation.

Now you get things to happen! Build and run the app. Press Summer Winter and Medal Count to watch your fancy new presentation styles in action.

 Medal count

Quite a difference, don't you think? The new presentation styles look sharp, but all the presentation images still glide in from the bottom.

and winter menus will slip from since and . You have to bend the animation muscles to make this happen.

Creating the Animation Controller

To add a custom animation transition, create a subclass of NSObject that matches UIViewControllerAnimatedTransitioning .

For complex animations you will usually create two controllers - one for presentation and one for parting. As for this app, launching mirrors presents so you only need an animation controller.

Go to File ▸ New ▸ file… select iOS ▸ Source ▸ Cocoa Touch Class and click Next . Set the name to SlideInPresentationAnimator makes it a subclass of NSObject and set the language to Swift .

Click Next and set the group Presentation and then click Create to create your new file. Open SlideInPresentationAnimator.swift and replace the content with the following:

  import UIKit

end class SlideInPresentationAnimator: NSObject {
// 1
// NOTE: - Properties
la direction: Presentation direction

// 2
la isPresentation: Bool

// 3
// NOTE: - Initializers
init (direction: Presentation Guide, ice presentation: Bool) {
self.direction = direction
self.isPresentation = isPresentation
super.init ()
}
}

Here you declare:

  1. direction which tells the animation controller the direction from which to animate the viewer's viewpoint.
  2. isPresentation to tell the animation controller whether to present or reject the display controller.
  3. An initializer that accepts the two declared values ​​above.

Then add the match to UIViewControllerAnimatedTransitioning by adding the following extension:

  // MARK: - UIViewControllerAnimatedTransitioning
extension SlideInPresentationAnimator: UIViewControllerAnimatedTransitioning {
func transitDuration (
using transitContext: UIViewControllerContextTransitioning?
) -> TimeInterval {
return 0.3
}

func animateTransition (
uses transitContext: UIViewControllerContextTransitioning
) {}
}

The protocol has two required methods - one for defining how long the transition takes (0.3 seconds in this case) and one for performing the animations. The animation method is a stubble to keep the compiler satisfied.

Replace animateTransition (user :) stump with the following:

  func animateTransition (
using transitContext: UIViewControllerContextTransitioning) {
// 1
let key: UITransitionContextViewControllerKey = isPresentation? .to from

guard let the controller = transitContext.viewController (forKey: key)
other {return}

// 2
if isPresentation {
transitionContext.containerView.addSubview (controller.view)
}

// 3
leave presentFrame = transitContext.finalFrame (for: controller)
var scriptedFrame = presentFrame
change direction {
case. left:
cut off Frame.origin.x = -presentedFrame.width
case. straight:
copyFrame.origin.x = transitContext.containerView.frame.size.width
case. top:
cut off Frame.origin.y = -presentedFrame.height
case. fine:
copyFrame.origin.y = transitContext.containerView.frame.size.height
}

// 4
la initialFrame = isPresentation? cut off frame: presentedFrame
la finalFrame = isPresentation? presentedFrame: rejectedFrame

// 5
la animationDuration = transitDuration (user: transitContext)
controller.view.frame = initialFrame
UIView.animate (
withDuration: animationDuration,
animations: {
controller.view.frame = finalFrame
}, completion: {finished in
if! self.is Presentation {
controller.view.removeFromSuperview ()
}
transitionContext.completeTransition (finished)
})
}

I said this makes the heavy promise! This is what each section does:

  1. If this is a presentation the method asks the transitionContext of the display controller associated with to . This is the view controller you move to . If termination requests the transitionContext about the display controller associated with .from . This is the view controller you move from .
  2. If the action is a presentation, add your code to the display controller's view in the display hierarchy. This code uses the transition context to get the container view.
  3. Calculate the frames you animate from and to . The first line asks the transitionContext about the outlook frame when presented. The rest of the part deals with the more difficult task of calculating the outlook frame when it is rejected. This section indicates the origin of the frame so that it is just outside the visible area based on the presentation direction.
  4. Determine the first and last frames of the transition. When presents the display controller, it moves from the partitioned frame to the presented frame - conversely when it is fired.
  5. Finally, this method animates the view from the first to the last frame. If this is a termination clear the viewer's view from the view hierarchy. Note that you call completeTransition (_ :) on transitionContext to inform if the transition is complete.

Connecting to the animation controller

You're at the last step of building the transition: Connect to the animation controller!

Open SlideInPresentationManager.swift and add the following two methods to the end of UIViewControllerTransitioningDelegate extension:

  func animationController (
forPresented presented: UIViewController,
presents: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SlideInPresentationAnimator (direction: direction, isPresentation: true)
}

func animationController (
for Dississed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
return SlideInPresentationAnimator (direction: direction, isPresentation: false)
}

The first method returns the animation controller for presentation of the display controller. The second returns the animation controller to reject the display controller. Both are instances of SlideInPresentationAnimator but they have different isPresentation values.

Build and run the app. Check out the transitions! You should see a smooth animation from the left when you press Summer . For Winter it comes from the right, and Medal Count comes from the bottom.

 medal_gif_03

Your result is exactly what you set out to do!

It works well ... as long as the device is in portrait . Try rotating to landscape .

Adaptivity

The good news is that you've done the hardest part! The transitions work perfectly. In this section you will have the effect of working beautifully on all devices and both directions.

Build and run the app again, but run it this time on an iPhone SE. You can use the simulator if you do not have the actual device. Try opening the summer menu in landscape . Do you see something wrong here?
  medal_count_07
Well, no. This actually looks good! Take a round of victory around your desk.

But what happens when you try to get the medal count? Select a year from the menu and press Medal Count . You should see the following screen:
  medal_count_08
SlideInPresentationController limits the display to 2/3 of the screen, leaving little space to display the medal count display. If you send the app like that, you're sure to hear complaints.

Fortunately for you, adaptivity is one thing. The IPhone has .regular height-size class in portrait and . Compact height size class in landscape.

UIPresentationController has a delegate that conforms to UIAdaptivePresentationControllerDelegate and it defines several methods to support adaptivity. You’ll use two of them in a moment.

First, you’ll make SlideInPresentationManager the delegate of SlideInPresentationController. This is the best option because the controller you choose to present determines whether the app should support compact height or not.

For example, GamesTableViewController looks correct in compact height, so there’s no need to limit its presentation. However, you do want to adjust the presentation for MedalCountViewController.

Open SlideInPresentationManager.swift and add the following below direction:

var disableCompactHeight = false

Here you add disableCompactHeight to indicate if the presentation supports compact height.

Next, add an extension that conforms to UIAdaptivePresentationControllerDelegate and implements adaptivePresentationStyle(for:traitCollection:) as follows:

// MARK: - UIAdaptivePresentationControllerDelegate
extension SlideInPresentationManager: UIAdaptivePresentationControllerDelegate {
  func adaptivePresentationStyle(
    for controller: UIPresentationController,
    traitCollection: UITraitCollection
  ) -> UIModalPresentationStyle {
    if traitCollection.verticalSizeClass == .compact && disableCompactHeight {
      return .overFullScreen
} other {
      return .none
}
}
}

This method accepts a UIPresentationController and a UITraitCollection and returns the desired UIModalPresentationStyle.

Next, it checks if verticalSizeClass equals .compact and if compact height is disabled for this presentation.

  • If yes, it returns a presentation style of .overFullScreen. This way, the presented view will cover the entire screen — not just 2/3 as defined in SlideInPresentationController.
  • If no, it returns .noneto stay with the implementation of UIPresentationController.

Find presentationController(forPresented:presenting:source:).
Set SlideInPresentationManager as the presentation controller’s delegate by adding the following line above the return statement:

presentationController.delegate = self

Finally, you’ll tell SlideInPresentationManager when to disable compact height.

Open MainViewController.swift and locate prepare(for:sender:). Find where the segue’s destination view controller is GamesTableViewControllerand then add the following line to the if block:

slideInTransitioningDelegate.disableCompactHeight = false

Find where the segue’s destination view controller is MedalCountViewController and add the following to the if block:

slideInTransitioningDelegate.disableCompactHeight = true

Build and run the app, bring up a medal count and rotate the device to landscape. The view should now take the entire screen, as shown below:
Landscape

This works great!

Overriding the presented controller

There’s still a niggly bit for the use case where you have a view that can only show in regular height. Maybe there’s something on there that’s just too tall to fit in a compact height. UIAdaptivePresentationControllerDelegate can help you.

Set it up by opening SlideInPresentationManager.swift and adding the following method to UIAdaptivePresentationControllerDelegate extension:

func presentationController(
  _ controller: UIPresentationController,
  viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle
) -> UIViewController? {
  guard case(.overFullScreen) = style else { return nil }
  return UIStoryboard(name: "Main", bundle: nil)
    .instantiateViewController(withIdentifier: "RotateViewController")
}

This method accepts a UIPresentationController and a UIModalPresentationStyle. It returns a view controller that overrides the original controller to present, or nil if the original view controller should be presented.

If the presentation style is .overFullScreen it creates and returns a different view controller controller. This new controller is just a simple UIViewController with an image that tells the user to rotate the screen to portrait.

Build and run the app, bring up the medal count and rotate the device to landscape. You should see the message:

Rotate the device back to portrait to view the medal count again.

Where To Go From Here?

Congratulations! You made it through a world-class athletic journey (for your brain) and built a custom UIPresentationController.

You can download the completed version of the project using Download Materials at the top or bottom of this tutorial.

You’ve covered quite a bit! You learned how to customize and reuse a UIPresentationController to create neat slide-in effects from any direction.

You also learned how to adapt presentations for various devices and both orientations, as well as how to handle cases where the device can’t handle landscape orientation.

I’m sure you can come up with many more creative ideas for presentations in your app. I encourage you to try them out based on what you’ve learned in this tutorial.

To delve deeper into custom transitions, check out this Custom View Controller Presentation Transitions tutorial by Marin Todorov & Fabrizio Brancati. You’ll find some interesting videos on the topic in our Beginning iOS Animation video series.

For more on Adaptive Layoutscheck out this adaptive layout in iOS tutorial by Adam Rush, or these adaptive layout video tutorials in the second section of our Mastering Auto Layout video series.

For more information on UIPresentationControllercheck out Apple’s documentation.

This iOS tutorial was a lot of fun to put together, and I’m keen to hear how it went for you. Bring your questions, blockers and discoveries to the forums below and let’s talk presentations! :]


Source link