Chapters

Hide chapters

UIKit Apprentice

Second Edition · iOS 15 · Swift 5.5 · Xcode 13

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

38. Polish the Pop-up
Written by Fahim Farook

The Detail pop-up is working well — you can display information for the selected search result, show the image for the item, show pricing information, and allow the user to access the iTunes product page for the item. You are done with the Detail pop-up and can move on to the next item, right?

Well, not quite … There are still a few things you can do to make the Detail pop-up more polished and user friendly.

This chapter will cover the following:

  • Dynamic type: Add support for dynamic type so that your text can display at a size specified by the user.
  • Gradients in the background: Add a gradient background to make the Detail pop-up background look more polished.
  • Animation!: Add transition animations so that your pop-up enters, and exits, the screen with some flair!

Dynamic Type

The iOS Settings app has an accessibility option — under Accessibility ▸ Display & Text Size ▸ Larger Text — that allows users to choose larger or smaller text. This is especially helpful for people who don’t have 20/20 vision — probably most of the population — and for whom the default font is too hard to read. Nobody likes squinting at their device!

You can find this setting both in your device and in the Simulator:

The Larger Text accessibility settings
The Larger Text accessibility settings

Apps have to opt-in to use this Dynamic Type feature. Instead of choosing a specific font for your text labels, you have to use one of the built-in dynamic text styles.

Configure for Dynamic Type

To provide a better user experience for all users, whether their eyesight is good or bad, you’ll change the Detail pop-up to use Dynamic Type for its labels.

➤ Open the storyboard and go to the Detail scene. Change the Font setting for the Name label to the Headline text style:

Changing the font to the dynamic Headline style
Changing the font to the dynamic Headline style

You can’t pick a font size when selecting text styles — the font size depends on the user and the Larger Text setting they use on their device.

➤ Under Dynamic Type, check the Automatically Adjusts Font checkbox. This ensures that the label’s text size will automatically change when the system text size changes without you having to detect the text size change in your code and reload the view to display the label at the new text size.

➤ Set the Lines attribute to 0. This allows the Name label to fit more than one line of text.

Of course, if you don’t know beforehand how large the label’s font will be, you also won’t know how large the label itself will end up being, especially if it sometimes may have more than one line of text. Generally, you would have used Auto Layout to set up constraints between the display elements in the pop-up view to handle the changes in layout as the font size changes because Auto Layout and Dynamic Type go hand-in-hand.

However, since you used stack views to set up your layout, most of the spacing and layout issues are automatically handled for you by Stack View and you don’t need to go through setting up, testing, and changing your Auto Layout constraints.

➤ Select the Artist Name label and set its Font to the Subhead text style.

➤ Set the Font of the other four labels to the Caption 1 text style. You can do this in a single go if you multiple-select these labels by holding down the key.

➤ Check Automatically Adjusts Font for all five of the above labels. Again, you can hold down to select multiple items — in this particular instance, doing so from the Document Outline is probably the easiest.

Test Dynamic Type

➤ Close the app and open the Settings app. Go to Accessibility ▸ Display & Text Size ▸ Larger Text. Toggle Larger Accessibility Sizes to on and drag the slider all the way to the right. That gives you the maximum font size — it’s huge!

Now go back to StoreSearch and open a new pop-up. The text will be a lot bigger!

For fun, change the font of the Name label to Body. Bazinga, that’s some big text!

When you’re done playing, put the Name label font back to Headline, and turn off the Larger Text setting — the slider goes in the middle.

Dynamic Type is an important feature to add to your apps. This was only a short introduction, but I hope the principle is clear: instead of a font with a fixed size, you use one of the available Text Styles: Body, Headline, Caption, and so on.

➤ Run the app, change the font size via Settings and see how the pop-up displays at different text sizes:

The pop-up at different text sizes
The pop-up at different text sizes

Well, the word-wrapping seems to work, but at higher text size things get a little squished together and then at the highest text sizes, things go really wrong.

Exercise: How do you think you can fix this layout?

Answer: You set up the pop-up view originally to be 280 x 280 points. It’s this size constraint which is forcing the content to get squished together. If you remove the size constraints — either for height or both width and height — the content should display much better.

Changing the pop-up view

When you use Auto Layout, the height of a UI element can be determined by the Auto Layout constraints of its child elements.

For example, take the following image which shows all the constraints for the pop-up view we used to have before using stack views:

The height of the pop-up view is determined by the constraints
The height of the pop-up view is determined by the constraints

As you can see, if all the items within the pop-up view had correct Auto Layout constraints set up, then you know the size and position of each of those items. And if you know that, you can add up the sizes and positions of the relevant child items to get the height of the parent, in this case, the pop-up view.

“But we didn’t use Auto Layout for the pop-up view!” I hear you say. But we did. Or rather, under the hood, the stack views we added use Auto Layout to position their contents. And if you recall, you did set up Auto Layout constraints of 16 points all around between the topmost stack view and the pop-up view.

So the above principle still applies – iOS can still calculate the height of the pop-up view based on its child views and so we do not really need a height constraint on the pop-up view.

➤ Select Pop-up View in the storyboard, go to the Size inspector and delete the 280 point height constraint. Now the pop-up view can expand (or contract) as its contents dictates.

But hang on, what would happen if the content was so long/large that the pop-up view’s height was larger than the height of the device screen? Then, the view would extend beyond the screen bounds – like this:

The pop-up view is too tall for the screen
The pop-up view is too tall for the screen

That’s not a very nice look at all! What if you added top and bottom constraints to limit how much the pop-up view can grow? Also, while you’re at it, why don’t you also let the pop-up grow horizontally by removing the 280 point width constraint and instead, set up left and right right constraints?

➤ Select Pop-up View in the storyboard and remove the 280 point width constraint in the Size inspector.

➤ Pin the pop-up view to its parent view so that left=32, top>=16, right=32 and bottom>=16.

Exercise: Why did you set up the constraints as “greater than or equal to” rather than simply “equal to” for top and bottom?

Answer: Because if you’d set up the constraints as equal to then the pop-up view would be exactly 16 points away from the top and bottom edge of the screen all the time. It wouldn’t grow or shrink to fit the content but instead, will always be this huge view covering the rest of the screen.

➤ Run the app and try out your changes at the largest font size.

The pop-up now shows all the content
The pop-up now shows all the content

That at least shows all the content on screen and so that should count as an improvement, right?

Tweaks

You’ll notice that the price button looks rather puny amongst all the other items. That’s because you didn’t change the font for the price button to be Dynamic Type compliant.

It’s easy – you follow the same steps as before and change the font to one of the pre-defined styles instead of an actual font. But do note that the price button would also be competing for the available screen space at that point.

Additionally, the price button uses a stretchable image as its background and that button currently only scales horizontally. If you set the price button title to dynamically size, it would change the height of the price button and result in weird drawing issues. You can fix this by changing the stretchable image configuration to stretch the image both horizontally and vertically. I leave this to you as an exercise :]

Did you also note that the Close button has gotten huge. Why do you think that is?

Remember that the Close button uses a Symbol Image, which are scalable and configurable symbols and not images. They scale according to font size settings. One quick fix you can make is to specify how you want the symbol to scale.

➤ Select the Close button in the story board and in the Attributes inspector change Symbol Scale to Small.

The above change will drop the Close button from 25 x 25 to around 18 x 18 at normal size but at really gigantic Dynamic Type settings, it would not be as huge as you have it above.

You now have an automatically resizing Detail pop-up that uses Dynamic Type for its labels!

Feel free to play around with the various settings and see how one change impacts the overall layout of the pop-up view. You’ll learn more by experimenting than you would by me telling you to do stuff. So experiment and see how you can make things better for smaller screen devices vs. larger screen devices.

➤ This is a good time to commit the changes.

Exercise: Set up the cells from the table view for Dynamic Type. There’s a catch: when the user returns from changing the text size settings, the app should refresh the screen without needing an app restart. You can do this by reloading the table view when the app receives a UIContentSizeCategoryDidChange notification — see the previous app for a refresher on how to handle notifications.

Also check out the property adjustsFontForContentSizeCategory on UILabel. If you set this to true, then the app will automatically update the label whenever the font size changes. Good luck! Check the forums at forums.raywenderlich.com for solutions from other readers.

Gradients in the background

As you can see in the previous screenshots, the table view in the background is dimmed by the view of the DetailViewController, which is semi-transparent. That allows the pop-up to stand out more.

It works well, but a plain overlay is a bit dull. Let’s turn it into a circular gradient instead.

You could use Photoshop to draw such a gradient and place an image view behind the pop-up, but why use an image when you can also draw using Core Graphics? Additionally, an image would increase the size of your app and might also create some issues when you need to support larger screen sizes. Not to mention the fact that you’d have to do two image variants to support both Light and Dark appearances.

To pull this off, you will create your own UIView subclass.

The GradientView class

➤ Add a new Swift File to the project. Name it GradientView.

This will be a very simple view. It simply draws a circular gradient that goes from opaque in the corners to mostly transparent in the center.

➤ Replace the contents of GradientView.swift with:

import UIKit

class GradientView: UIView {
  override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = UIColor.clear
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    backgroundColor = UIColor.clear
  }

  override func draw(_ rect: CGRect) {
    // 1
    let traits = UITraitCollection.current
    let color: CGFloat = traits.userInterfaceStyle == .light ? 0.314 : 1
    // 2
    let components: [CGFloat] = [
      color, color, color, 0.2,
      color, color, color, 0.4,
      color, color, color, 0.6,
      color, color, color, 1
    ]
    let locations: [CGFloat] = [ 0, 0.5, 0.75, 1 ]
    // 3
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let gradient = CGGradient(
      colorSpace: colorSpace,
      colorComponents: components,
      locations: locations,
      count: 4)
    // 4
    let x = bounds.midX
    let y = bounds.midY
    let centerPoint = CGPoint(x: x, y: y)
    let radius = max(x, y)
    // 5
    let context = UIGraphicsGetCurrentContext()
    context?.drawRadialGradient(
      gradient!,
      startCenter: centerPoint,
      startRadius: 0,
      endCenter: centerPoint,
      endRadius: radius,
      options: .drawsAfterEndLocation)
  }
}

In the init(frame:) and init?(coder:) methods you simply set the background color to fully transparent — the “clear” color. Then in draw() you draw the gradient on top of that transparent background, so that it blends with whatever is below.

The drawing code uses the Core Graphics framework. It may look a little scary but this is what it does:

  1. First, you determine the color to use based on the current appearance. The UITraitCollection object allows you to access the current iOS traits such as screen size, appearance etc. If the appearance is Light, you go with a color value which will result in a dark grey gradient and if the appearance is Dark, you with a color value which will give you a white gradient.

  2. Next, you create two arrays that define how the gradient will be drawn. A gradient is drawn by using a set of colors and stops/locations. You start at the first location with the first color and draw till you get to the second location and then switch to the second color and draw till you get to the third location etc.

    The first array, components, gives you the colors to be used in the gradient and the second array, locations, gives you the points along the gradient path where you want the color changes to occur.

    The drawing will start at the center of the screen because you’re drawing a circular gradient. Note that all values in these two arrays go from 0 to 1 where the colors — which can go from 0 to 255 — are represented as the actual color value divided by 255.

  3. With those color stops you can create the gradient. This gives you a new CGGradient object.

  4. Now that you have the gradient object, you have to figure out how big you need to draw it. The midX and midY properties return the center point of a rectangle. That rectangle is given by bounds, a CGRect object that describes the dimensions of the view.

    If I can avoid it, I prefer not to hard-code any dimensions such as “375 by 667 points”. By using bounds, you can use this view anywhere you want to, no matter how big a space it should fill. You can use it without problems on any screen size from the smallest iPhone to the biggest iPad.

    The centerPoint constant contains the coordinates for the center point of the view and radius contains the larger of the x and y values; max() is a handy function that you can use to determine which of two values is the biggest.

  5. With all those preliminaries done, you can finally draw the thing. Core Graphics drawing always takes places in what’s known as a graphics context. We’re not going to worry about exactly what that is, just know that you need to obtain a reference to the current context and then you can do your drawing.

    And finally, the drawRadialGradient() function draws the gradient according to your specifications.

Generally speaking, it isn’t optimal to create new objects inside your draw() method, such as gradients, especially if draw() is called often. In such cases it is better to create the objects the first time you need them and to reuse the same instance over and over — lazy loading for the win!

However, you don’t really have to do that here because this draw() method will be called just once — when the DetailViewController gets loaded — so you can get away with being less than optimal.

Note: By the way, you’ll only be using init(frame:) to create the GradientView instance. The other init method, init?(coder:), is never used in this app. However, UIView demands that all subclasses implement init?(coder:) — that’s why it is marked as required — and if you remove this method, Xcode will complain with an error.

Use GradientView

Putting this new GradientView class to work is pretty easy. You’ll add it to your DetailViewController. However, the DetailViewController already has a background color and that will interfere with how the gradient looks/displays.

We could simply change the current background color to transparent in the storyboard but instead, we’ll do that in code as well.

➤ Open DetailViewController.swift and add the following code to the end of viewDidLoad:

// Gradient view
view.backgroundColor = UIColor.clear
let dimmingView = GradientView(frame: CGRect.zero)
dimmingView.frame = view.bounds
view.insertSubview(dimmingView, at: 0)

The first line sets the DetailViewController background to be clear. Then you create a new instance of GradientView, set its size to match that of DetailViewController, and finally add the new gradient view to the DetailViewController view hierarchy.

➤ Run the app and see what happens.

The background behind the pop-up now has a gradient
The background behind the pop-up now has a gradient

Nice! That looks a lot smarter.

Animation!

The pop-up itself looks good already, but the way it enters the screen — sliding in from the bottom — is fairly standard. Let’s jazz things up a bit!

You’ve used Core Animation and UIView animations before. This time you’ll use a keyframe animation to make the pop-up bounce into view.

To animate the transition between two screens, you use an animation controller object. The purpose of this object is to animate a screen while it’s being presented or dismissed, nothing more.

Now let’s add some liveliness to this pop-up!

The animation controller class

➤ Add a new Swift File to the project, named BounceAnimationController.

➤ Replace the contents of the new file with:

import UIKit

class BounceAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
  func transitionDuration(
    using transitionContext: UIViewControllerContextTransitioning?
  ) -> TimeInterval {
    return 0.4
  }

  func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning
  ) {
    if let toViewController = transitionContext.viewController(
      forKey: UITransitionContextViewControllerKey.to),
      let toView = transitionContext.view(
        forKey: UITransitionContextViewKey.to) {
      let containerView = transitionContext.containerView
      toView.frame = transitionContext.finalFrame(
        for: toViewController)
      containerView.addSubview(toView)
      toView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)

      UIView.animateKeyframes(
        withDuration: transitionDuration(
          using: transitionContext),
        delay: 0,
        options: .calculationModeCubic,
        animations: {
          UIView.addKeyframe(
            withRelativeStartTime: 0.0,
            relativeDuration: 0.334) {
              toView.transform = CGAffineTransform(
                scaleX: 1.2, y: 1.2)
          }
          UIView.addKeyframe(
            withRelativeStartTime: 0.334,
            relativeDuration: 0.333) {
              toView.transform = CGAffineTransform(
                scaleX: 0.9, y: 0.9)
          }
          UIView.addKeyframe(
            withRelativeStartTime: 0.666,
            relativeDuration: 0.333) {
              toView.transform = CGAffineTransform(
                scaleX: 1.0, y: 1.0)
          }
        }, completion: { finished in
          transitionContext.completeTransition(finished)
        })
    }
  }
}

To become an animation controller, the object needs to extend NSObject and also implement the UIViewControllerAnimatedTransitioning protocol — quite a mouthful! The important methods from this protocol are:

  • transitionDuration(using:) – This determines how long the animation is. You’re making the pop-in animation last for only 0.4 seconds, but that’s long enough. Animations are fun, but they shouldn’t keep the user waiting.

  • animateTransition(using:) – This performs the actual animation.

To find out what to animate, you look at the transitionContext parameter. This gives you a reference to a new view controller and lets you know how big it should be.

The actual animation starts at the line UIView.animateKeyframes(…). This works like all UIView-based animations: you set the initial state before the animation block, and UIKit will automatically animate any properties that get changed inside the closure. The difference from before is that a keyframe animation lets you animate the view in several distinct stages.

The property you’re animating is the transform. If you’ve ever taken any matrix math you’ll be pleased — or terrified! — to hear that this is an affine transformation matrix. It allows you to do all sorts of funky stuff with the view, such as rotating it or shearing it. But the most common use of the transform is for scaling.

The animation consists of several keyframes. It will smoothly proceed from one keyframe to the next over a certain amount of time. Because you’re animating the view’s scale, the different toView.transform values represent how much bigger or smaller the view will be over time.

The animation starts with the view scaled down to 70% (scale 0.7). The next keyframe inflates it to 120% of its normal size. After that, it will scale the view down a bit again but not as much as before — only 90% of its original size. The final keyframe ends up with a scale of 1.0, which restores the view to an undistorted shape.

By quickly changing the view size from small to big to small to normal, you create a bounce effect.

You also specify the duration between the successive keyframes. In this case, each transition from one keyframe to the next takes 1/3rd of the total animation time. These times are not in seconds but in fractions of the animation’s total duration, which is 0.4 seconds.

Do tinker with the animation code. I’m sure you can make it even more spectacular!

Use the new animation controller

To use this animation in your app, you have to tell the app to use the new animation controller when presenting the Detail pop-up.

➤ In DetailViewController.swift, add the following extension to the end of the file:

extension DetailViewController: UIViewControllerTransitioningDelegate {
  func animationController(
    forPresented presented: UIViewController,
    presenting: UIViewController,
    source: UIViewController
  ) -> UIViewControllerAnimatedTransitioning? {
    return BounceAnimationController()
  }
}

The methods from this delegate protocol tell UIKit what objects it should use to perform the transition to the Detail View Controller. It will now use your new BounceAnimationController class for the presentation animation instead of whatever it uses by default.

➤ Also add the following init method to DetailViewController:

required init?(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
  transitioningDelegate = self
}

Recall that init?(coder) is invoked to load the view controller from the storyboard. Here you set the transitioning delegate so that the method you just implemented is called when an animation controller is needed.

And that’s all you need to do.

➤ Run the app and get ready for some bouncing action!

The pop-up looks a lot spiffier with the bounce animation, but the animation upon dismissal of the pop-up is very plain.

Animate the pop-up exit

After tapping the Close button, the pop-up slides off the screen, like modal screens always do. Let’s make this a bit more exciting and make it slide up instead of down. For that you need another animation controller.

➤ Add a new Swift File to the project, named SlideOutAnimationController.

➤ Replace the new file’s contents with:

import UIKit

class SlideOutAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
  func transitionDuration(
    using transitionContext: UIViewControllerContextTransitioning?
  ) -> TimeInterval {
    return 0.3
  }

  func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning
  ) {
    if let fromView = transitionContext.view(
      forKey: UITransitionContextViewKey.from) {
      let containerView = transitionContext.containerView
      let time = transitionDuration(using: transitionContext)
      UIView.animate(
        withDuration: time,
        animations: {
        fromView.center.y -= containerView.bounds.size.height
        fromView.transform = CGAffineTransform(
          scaleX: 0.5, y: 0.5)
        }, completion: { finished in
        transitionContext.completeTransition(finished)
        }
      )
    }
  }
}

This is pretty much the same as the other animation controller, except that the animation itself is different. Inside the animation block you subtract the height of the screen from the view’s center position while simultaneously zooming it out to 50% of its original size, making the Detail screen fly up-up-and-away.

➤ In DetailViewController.swift, add the following method to the UIViewControllerTransitioningDelegate extension:

func animationController(
  forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
  return SlideOutAnimationController()
}

This simply overrides the animation controller that is used when a view controller is dismissed.

➤ Run the app and try it out. That looks pretty sweet if you ask me!

➤ If you’re happy with the way the animations look, then commit your changes.

Exercise: Create some exciting new animations. I’m sure you can improve on mine. Hint: use the transform matrix to add some rotation to the mix.

You can find the project files for this chapter under 38-Polish-the-popup in the Source Code folder.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.