Chapters

Hide chapters

Real-World iOS by Tutorials

First Edition · iOS 15 · Swift 5.5 · Xcode 13

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

9. Adding Animations & Custom Controls
Written by Josh Steele

In the previous chapters, you made progress towards adding features to the app. That’s good! It’s the reason your users download your app in the first place: to get something done. For this app, that means finding a potential future pet.

With the primary functionality in place, it’s time to ask how you can:

  • Customize your app to improve user engagement?
  • Make your app available to as many potential users as possible?
  • Lower or eliminate any real or perceived barriers to using your app?

Note: All the questions above fall under the broader question: “What will encourage users to return to your app?” That’s the overarching topic for this section of the book.

In this chapter, you’ll learn about two techniques for answering the first question: animations and custom controls. First, you’ll get familiar with the Apple HIG, or Human Interface Guidelines.

Apple Human Interface Guidelines

Apple’s HIG, or Human Interface Guidelines for iOS are a set of guidelines, not requirements, that you can reference when designing your app’s user interface. Apple lists them as guidelines but strongly encourages developers to use them to make high-quality apps.

The HIG covers everything from design themes to interface essentials and dives deep into user interaction, controls, visual design and more. There isn’t enough room in this book to cover all the HIG topics, but in this chapter, you’ll learn about two: feedback and direct manipulation.

Feedback

The last thing you want users to feel when using your app is uncertain. Your app should respond to user interactions. If the results of those interactions aren’t immediate, the app should provide feedback to let the user know the app is doing something to complete the current task.

Of course, feedback shouldn’t get in the way of the app’s main purpose and should be quickly identifiable without much work on the user’s part.

Feedback use case: Animations

Animations can help bring your app to life by giving a sense of connection between your user and your app. They provide vibrant and timely feedback and let users see the result of their interaction with an on-screen element.

Animations can also provide a sense of fun in your app’s interactions. Tapping a heart to favorite an item is a great interaction, but making that heart grow and change color makes the interaction pop in the user’s eyes.

Animations, however, shouldn’t be overused. Keep in mind the following guidelines when deciding whether to add an animation to your app.

When not to use animations

Don’t use animations, or any feedback, unless it conveys essential, timely or actionable information. Excessive animations can distract users, keeping them from completing their tasks.

Here’s an example. Your app’s logo is in a prominent location of the user interface, the upper left-hand corner. That’s fine because branding is important.

But, your designer comes by and says they’re adding some reflective elements to the logo. They suggest you add an animation so the logo pops and gets the user’s attention.

This is not a good case to use an animation. Here’s why:

  • Drawing attention: The animation would indeed catch the user’s eye, but it would draw them away from their main task when using your app.
  • No actionable information: Users can’t do anything with the logo: It isn’t actionable. Drawing the user’s attention to the logo may indicate a false premise that interacting with it will do something.
  • Not important: Since the animation has no actionable information and draws the user’s attention away from their task, you can conclude that it is not important when using your app. Therefore, you shouldn’t add it.

However, animations can be useful. So, when should you use them?

When to use animations

When used properly, animations can add flair to your app. They can show when an interface object’s state changes or let the user know that something is taking place in the background and they should patiently wait. Here are a few examples:

  • Animating view attributes: Tapping a favorite button animates the icon’s color, size or shape.
  • Animating a real-life action: Tapping send on an email dialog brings up an animation of a mail envelope closing and flying away.
  • Denoting background behavior: You can unobtrusively display a simple spinner animation to show that a background process is taking place.

Animations make your app more enjoyable, but they shouldn’t be a requirement.

Animations are always optional

There are features like reduced motion that can affect (and even disable) effects or animations in your app. Knowing this you should always think of animations as optional. Therefore, don’t make animations an integral part of your app.

Here’s how you can minimize the animations above but still keep feedback in the app:

  • Favorite button: Simply change the color and size immediately, instead of animating it over a period of time.
  • Mail envelope: Tapping send instead updates a text status area after the OS sends the mail.
  • Background processes: Replace the spinner with a text label that describes any feedback to the user.

With those guidelines in mind, it’s time to add a few animations to PetSave.

Adding an animation to PetSave

Animations can convey various feedback to the user. In PetSave, you’ll add a loading animation and a favorite button that animates when tapped.

Building animated GIFs

When apps fetch data from the network, there’s always a possibility of delays. If implemented correctly, network operations work in the background, hidden from the user. Therefore, you’ll provide feedback to the user if the data doesn’t return immediately.

Here, immediately is typically two to three seconds. Beyond that, the user may start to get frustrated that the app isn’t responding or showing the data they requested.

Something as simple as an animated view can let the user know work is going on and let them know they don’t need to get so animated while waiting for the task to finish.

Open Core/views and create a new SwiftUI view called LoadingAnimation.swift. Replace the code in the file with:

// 1
struct LoadingAnimation: UIViewRepresentable {
  let animatedFrames: UIImage
  let image: UIImageView
  let squareDimension: CGFloat = 125

  // 2
  init() {
    var images: [UIImage] = []
    // 3
    for i in 1...127 {
      guard let image =
        UIImage(named: "dog_\(String(format: "%03d", i))")
        else { continue }
      images.append(image)
    }
    // 4
    animatedFrames = UIImage.animatedImage(with: images,
      duration: 4) ?? UIImage()
    // 5
    image = UIImageView(frame: CGRect(x: 0, y: 0, width: squareDimension, height: squareDimension))
  }

  // 6
  func makeUIView(context: Context) -> UIView {
    let view = UIView(frame: CGRect(x: 0, y: 0,
      width: squareDimension, height: squareDimension))
    image.clipsToBounds = true
    image.autoresizesSubviews = true
    image.contentMode = .scaleAspectFit
    image.image = animatedFrames
    image.center = CGPoint(x: view.frame.width / 2,
      y: view.frame.height / 2)
    view.backgroundColor = .red
    view.addSubview(image)

    return view
  }

  func updateUIView(_ uiView: UIViewType, context: Context) {
    // no code here; just for protocol
  }
}

// 7
struct LoadingAnimationView: View {
  var body: some View {
    VStack {
      LoadingAnimation()
    }
  }
}


// 8
struct LoadingAnimationView_Previews: PreviewProvider {
  static var previews: some View {
    LoadingAnimationView()
  }
}

A lot going on here, but it breaks down into the following major components:

  1. LoadingAnimationView is a UIViewRepresentable. There isn’t a good SwiftUI set of widgets to use here, so you’ll use a UIKit component and wrap it in a UIViewRepresentable so SwiftUI can use it.
  2. The init method gets this image ready to use.
  3. This loop fills the images array with the images required to make the animation. The string format %03d constructs an integer with three digits and leading zeroes. i is the value passed in to construct the integer.
  4. To set up animatedFrames you used animatedImage(with:duration:). This is a static method of UIKit’s UIImage that takes in an array of UIImages and a duration and returns an animated image.
  5. Set up image. This is an UIImageView that will act as the container for your animated image.
  6. makeUIView places the animated image in a UIImageView. The UIImageView goes in a UIView for the UIViewRepresentable to present on screen.
  7. This is the actual SwiftUI View that uses a LoadingAnimation() inside a VStack.
  8. The preview provider for your SwiftUI View. So you can see it on Xcode Previews.

As mentioned earlier, an animation like this is useful when loading data from the network.

Open AnimalsNearYouView.swift. Look for the ProgressView implementation passed in a closure to the AnimalListView, inside then NavigationView, and replace it with:

// 1
HStack(alignment: .center) {
  // 2
  LoadingAnimation()
    .frame(maxWidth: 125, minHeight: 125)
  Text("Loading more animals...")
}
// 3
.task {
  await viewModel.fetchMoreAnimals()
}

Here’s what you did:

  1. You used an HStack and set alignment to be center.
  2. Inside the HStack is the new LoadingAnimation and a frame modifier that sets the max width and height. Also, you added a Text to display the message “Loading more animals…”
  3. Put the asynchronous call to fetchMoreAnimals inside a task(priority:_:) modifier. You need to do this because the method is async. This code is called when the view appears.

Build and run the project in the simulator. Scroll down to the available set of data in the Animals Near You View tab:

The Animals Near You view.
The Animals Near You view.

Don’t blink or you might miss the animation! The network operation for finding animals near you is pretty quick, so the animation barely has a chance to appear on screen.

You’ll need a slow connection to test this. To help you with this Apple provides a tool called Network Link Conditioner.

If you haven’t already downloaded it, you’ll need to grab the Additional Tools for Xcode from the Apple Developer website.

To install it open Additional_Tools_for_Xcode_13.3_beta_3.dmg. Inside the Hardware folder double-click Network Link Condition.prefPane.

Dialog confirming the Network Link Conditioner Installation.
Dialog confirming the Network Link Conditioner Installation.

A message asking for confirmation will appear. Click Install.

Note: If you’re on macOS Big Sur, you may need to get the Additional Tools for Xcode 12.5 if you have trouble installing the Network Link Conditioner preference pane for Xcode 13.

Once installed, open System Preferences and open the Network Link Conditioner. Turn it on, and set 100 % Loss for the Profile, this will simulate a low or absent network:

The Network Link Conditioner preference pane.
The Network Link Conditioner preference pane.

Now build and rerun the app. This time, when the app searches for pets near you, you’ll see an animated progress indicator that’s appropriately themed for your app!

The custom animation now appears next to the informative text (best viewed in your simulator or device).
The custom animation now appears next to the informative text (best viewed in your simulator or device).

Disable the network link conditioner, and the app will download new animals to display as usual.

Note: The Network Link Conditioner impacts your entire system, so don’t forget to turn it off when you’re done testing. Otherwise, your next trip to the web will be a slow one!

Animation modifiers in SwiftUI

You can also add animations to your app by using SwiftUI’s built-in animation capabilities. SwiftUI uses both implicit and explicit animations:

  • Implicit Animations: Implicit animations work via modifiers that take in one or more values. When those values change, the SwiftUI rendering system smoothly animates those changes for you with an .animation modifier.
  • Explicit Animations: Explicit animations aren’t tied to a particular view via a modifier but instead operate directly on the change you want to make. This change appears within a withAnimation block.

For PetSave, you’ll use an implicit animation to animate the heart’s color when the user taps the favorite button for a pet.

Open AnimalHeaderView.swift and add this lines right below HeaderTitle view:


// 1
Image(systemName: favorited ? "heart.fill" : "heart")
  .font(.system(size: 50))
  .foregroundColor( favorited ? Color(.systemRed) : Color(.black))
  .frame(minWidth: 50, maxWidth: 50, minHeight: 50, maxHeight: 50)
  // 2
  .animation(favorited ? .interpolatingSpring(
    mass: 5,
    stiffness: 3.0,
    damping: 1.0,
    initialVelocity: 1) :
    .default,
    value: $favorited.wrappedValue)
  .onTapGesture {
    $favorited.wrappedValue.toggle()
}

Here’s what’s going on:

  1. The Image has some modifiers that depend on the state of the favorite property, such as the foregroundColor.
  2. The animation modifier also responds to changes in favorited and uses a spring animation when the user taps the heart. The modifier uses a simple default animation when unfavorited. Because you use the animation modifier, this is an implicit animation.

Open AnimalDetailsView.swift and, if necessary, click Resume in the preview canvas. Click Play to activate the live preview. When it becomes active, tap the favorite button. You’ll see the heart icon change its fill color smoothly when you tap on it.

Animal Details View with animation (best viewed in your simulator or device).
Animal Details View with animation (best viewed in your simulator or device).

Note: Try removing the .animation(_:value:) modifier and tapping on the heart icon, so you can appreciate how it looks without animation.

Feedback is a great way to send information back to the user. But something must trigger that feedback, usually in the form of interaction with your app. One of the most common interactions is direct manipulations of UI elements.

Direct manipulation

Direct manipulation is the most, well, direct way a user can interact with your app. Users can reach out with their fingers and interact with your app.

Users can control the various views and controls in your app with tapping, swiping and gestures. You can take that a step further by introducing something into your app that has all three.

Direct manipulation use case: Custom Controls

Developers can go beyond the built-in controls in iOS and make their own custom controls. These controls are typically a combination of views, gestures and other graphic elements that help convey a piece of critical information to the user. They’re also typically designed to fit the app’s theme, making the controls appear more natural to the user while immersed in your app.

Like animations, be judicious in your use of custom controls.

When not to create custom controls

Apple’s set of controls is fairly expansive, and more importantly, familiar to users. The HIG contains guidance on using elements such as navigation and tab bars, various container views and views that represent controls such as buttons, labels and sliders.

Therefore, the guidance on when to not create custom controls is when that control already exists!

Apple optimizes the built-in controls and tests them to make sure they work well. For example, UIKit’s UITableView contains two delegate objects to help populate and style the table. Developers don’t know how that table gets rendered on-screen: Apple handles that behind the scenes. It’s highly optimized to render well and includes many built-in controls, such as swipe to delete. If you tried to replicate all the capabilities and optimizations of UITableView, you’d waste a lot of time!

But custom controls can come in handy!

When to create custom controls

iOS contains plenty of optimized controls for you to use during development. However, there are some reasons you might develop your own control:

  • Custom Interaction: Your app may have specific interaction needs. For example, an internet radio app might try to emulate a typical car radio for its controls. This would require spinning a dial, which is not a built-in control in iOS.
  • Custom Behavior: When a user interacts with a control, you may want to include custom behaviors that better fit your app’s look and feel.
  • Custom Appearance: iOS exposes a fair amount of APIs that let developers customize the appearance of the built-in controls. But, if those APIs don’t provide exactly what you need, or the control has a presentation bug, you may need to customize the control. You can also customize controls to apply themes that match the app’s overall aesthetic.

When you make a custom control, it’ll probably be for at least one of the reasons above. You’ll most likely use a combination of existing iOS functionality, such as animation, gestures and custom views with UIKit or SwiftUI.

With those guidelines in mind, it’s time for you to add a custom control to PetSave.

Adding a custom control to PetSave

You’ll add a ranking control that lets users provide feedback on pet details and specify how likely they are to adopt that pet. To accomplish this, you’ll use the following techniques:

  • SwiftUI Views: The overall view is a series of Image views in an HStack.
  • Gestures: When a user taps on a single Image, it’ll adjust the opacity of the other Images where appropriate and update the current ranking.

Open Core/views and create a new SwiftUI view called PetRankingView.swift. Replace the current implementation for PetRankingView with:

import SwiftUI

struct PetRankingView: View {
  // 1
  @ObservedObject var viewModel: PetRankingViewModel
  var animal: AnimalEntity

  // 2
  init(animal: AnimalEntity) {
    self.animal = animal
    viewModel = PetRankingViewModel(animal: animal)
  }

  // 3
  var body: some View {
    HStack {
      Text("Rank me!")
        .multilineTextAlignment(.center)
      ForEach(0...4, id: \.self) { index in
        PetRankImage(index: index, recentIndex: $viewModel.ranking)
      }
    }
  }
}

Here’s what this code does:

  1. PetRankingViewModel, which is an @ObservedObject, responds to changes in the published elements of the model.
  2. The init method sets the animal and initializes the PetRankingViewModel.
  3. The body consists of a Text label in front of five PetRankImages arranged in an HStack.

Add the following code below PetRankingView:

struct PetRankImage: View {
  let index: Int
  // 1
  @State var opacity: Double = 0.4
  @State var tapped = false
  @Binding var recentIndex: Int

  var body: some View {
    // 2
    Image("creature_dog-and-bone")
      .resizable()
      .aspectRatio(contentMode: .fit)
      .opacity(opacity)
      .frame(width: 50, height: 50)
      .onTapGesture {
        opacity = tapped ? 0.4 : 1.0
        tapped.toggle()
        recentIndex = index
      }
      .onChange(of: recentIndex) { value in
        checkOpacity(value: value)
      }
      .onAppear {
        checkOpacity(value: recentIndex)
      }
  }

  // 3
  func checkOpacity(value: Int) {
    opacity = value >= index ? 1.0 : 0.4
    tapped.toggle()
  }
}

The PetRankImage view encapsulates the Image of a dog holding a bone. It also controls the image’s opacity, which helps show the enabled state. Here’s what the PetRankImage code does:

  1. opacity and tapped are @State properties. They track whether this particular image is enabled, contributing to the overall ranking. The recentIndex @Binding comes in from the parent control and determines whether this image is enabled.
  2. The Image has opacity, onTapGesture and onChange modifiers to help change the state based on user interaction.
  3. checkOpacity updates the image’s opacity based on the passed in recentIndex.

Below the PetRankImage struct, add:

final class PetRankingViewModel: ObservableObject {
  var animal: AnimalEntity
  // 1
  var ranking: Int {
    didSet {
      animal.ranking = Int32(ranking)
      objectWillChange.send()
    }
  }

  // 2
  init(animal: AnimalEntity) {
    self.animal = animal
    self.ranking = Int(animal.ranking)
  }
}

PetRankingViewModel monitors the current ranking the user chose, and sets that value to the animal’s ranking property. Items of note in the PetRankingViewModel:

  1. The ranking property has didSet that publishes the change to listeners of the model using objectWillChange.send().
  2. The init method initializes the animal and ranking properties based on the passed in animal property.

Update the PetRankingView_Previews struct to use a test animal entity to populate PetRankingView and add a padding and previewLayout modifier to customize the preview:

struct PetRankingView_Previews: PreviewProvider {
  static var previews: some View {
    if let animal = CoreDataHelper.getTestAnimalEntity() {
      PetRankingView(animal: animal)
        .padding()
        .previewLayout(.sizeThatFits)
    }
  }
}

In the preview pane, click Resume. You’ll see your control:

The PetSave custom ranking control.
The PetSave custom ranking control.

This is looking pretty good. 5/5 dogs, would create again!

Play the preview, and tap the low opacity dogs in the control to set the rating for this animal.

The PetSave custom ranking control has a 5 out of 5 ranking!
The PetSave custom ranking control has a 5 out of 5 ranking!

Finally, it’s time to add the custom control to the app. Open AnimalDetails/view. In AnimalDetailsView.swift, add PetRankingView between the first Divider and AnimalDetailRow add the following code:

PetRankingView(animal: animal)
  .padding()
  .blur(radius: zoomed ? 20 : 0)

Here you’re adding add PetRankingView passing the animal you want to rate. In addition, you added two modifiers to the new view: padding(_:_:) to add some separation and .blur(radius:opaque:) which, depending on the state of zoomed, could be 20 or zero.

Build and run the app. Now, interact with the custom control to rank the pets you view:

The completed Animal Details view!
The completed Animal Details view!

Key points

  • Apple’s Human Interface Guidelines are a great resource for ensuring your app has a great look and feel and fits alongside other apps in the store.
  • Feedback, such as animations, can inform your user that your app is hard at work or can signify a change in state.
  • Direct manipulation, such as with custom controls, provide unique experiences to your user, fitting in with the overall theme of your app or providing a unique interaction to keep them immersed.

Where to go from here?

Congratulations! You took your first dive into the Apple Human Interface Guidelines. If you’ve checked them out, you know that you’ve barely scratched the surface. You’ll read about a few more areas in the later chapters of this section.

You also learned important “Dos and Don’ts” for when to use animations and custom controls. Both can help a user feel connected with your app, but be careful not to overuse them.

If you’re interested in learning more about the world of iOS Animations, check out iOS Animations by Tutorials.

You’re not done taking advantage of the wealth of information in the HIG! In the next chapter, you’ll learn how to make your app accessible to a broader set of users by learning about Accessibility in iOS.

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.