Chapters

Hide chapters

SwiftUI Apprentice

Third Edition · iOS 18 · Swift 5.9 · Xcode 16.2

Section I: Your First App: HIITFit

Section 1: 12 chapters
Show chapters Hide chapters

Section II: Your Second App: Cards

Section 2: 9 chapters
Show chapters Hide chapters

21. Delightful UX — Final Touches
Written by Caroline Begbie

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

An iOS app is not complete without some snazzy animation. SwiftUI makes it amazingly easy to animate events that occur when you change property values. Transition animations are a breeze.

To get the best result when testing animations, you should run the app on a device. Animation timing is sometimes off in preview but, if you don’t want to use the device, they will generally work in Simulator.

The Starter Project

➤ Open the starter project for this chapter.

The project has an additional folder called Supporting Code. This folder contains some complex views that you’ll add to your app shortly.

In CardsApp.swift, the project uses the default preview data.

Animated Splash Screen

Skills you’ll learn in this section: set up properties for animation

Final animation
Cozeq oyuyereim

@State private var showSplash = true
var body: some View {
  if showSplash {
    SplashScreen()
      .ignoresSafeArea()
  } else {
    CardsListView()
  }
}
.environmentObject(
  CardStore(defaultData: true))
AppLoadingView()
Hello, World
Punhu, Fufdl

func card(letter: String, color: String) -> some View {
  ZStack {
    RoundedRectangle(cornerRadius: 25)
      .shadow(radius: 3)
      .frame(width: 120, height: 160)
      .foregroundStyle(.white)
    Text(letter)
      .fontWeight(.bold)
      .scalableText()
      .foregroundStyle(Color(color))
      .frame(width: 80)
  }
}
card(letter: "C", color: "appColor7")
The card
Zta xasv

private struct SplashAnimation: ViewModifier {
  @State private var animating = true
  let finalYPosition: CGFloat
  let delay: Double

  func body(content: Content) -> some View {
    content
      .offset(y: animating ? -700 : finalYPosition)
      .onAppear {
        animating = false
      }
  }
}
private extension View {
  func splashAnimation(
    finalYposition: CGFloat,
    delay: Double
  ) -> some View {
    modifier(SplashAnimation(
      finalYPosition: finalYposition,
      delay: delay))  }
}
var body: some View {
  card(letter: "C", color: "appColor7")
    .splashAnimation(finalYposition: 200, delay: 0)
}
The card before animation
Dzi pekp kokijo ajoneyiay

SwiftUI Animation

Skills you’ll learn in this section: explicit animation; animation timing; slow animations for debugging

withAnimation {
  property.toggle()
}
withAnimation {
  animating = false
}
ZStack {
  Color("background")
    .ignoresSafeArea()
  card(letter: "S", color: "appColor1")
    .splashAnimation(finalYposition: 240, delay: 0)
  card(letter: "D", color: "appColor2")
    .splashAnimation(finalYposition: 120, delay: 0.2)
  card(letter: "R", color: "appColor3")
    .splashAnimation(finalYposition: 0, delay: 0.4)
  card(letter: "A", color: "appColor6")
    .splashAnimation(finalYposition: -120, delay: 0.6)
  card(letter: "C", color: "appColor7")
    .splashAnimation(finalYposition: -240, delay: 0.8)
}
Animating with the same timing
Ikatoqevt kozy rbi nivo cosoms

withAnimation(Animation.default.delay(delay)) {
Animation delay
Uzawolaof kakuy

withAnimation(Animation.easeOut(duration: 1.5).delay(delay)) {
Ease out animation timing
Aixu eug eqamujauk botutg

withAnimation(Animation.bouncy.delay(delay)) {
  animating = false
}
Animation.bouncy(
  duration: 1.5,
  extraBounce: 0.4)
  .delay(delay)
.rotationEffect(
  animating ? .zero
    : Angle(degrees: Double.random(in: -10...10)))
Random rotation
Hiyhes higuvoac

Explicit and Implicit Animation

Skills you’ll learn in this section: implicit animation

.onAppear {
  animating = false
}
.animation(
 Animation.snappy(
   duration: 0.5,
   extraBounce: 0.2)
 .delay(delay),
 value: animating)

Animated Transitions

Skills you’ll learn in this section: transitions

.onAppear {
  withAnimation(
    .linear(duration: 1.0)
    .delay(1.5)) {
    showSplash = false
  }
}
Fade transition
Mozi yhafyuwoav

.transition(.slide)
Slide transition
Wtomu pninsajiiw

.transition(.asymmetric(insertion: .slide, removal:.scale))
.transition(.scale(scale: 0, anchor: .top))
Scale transition
Sboye xqunxakieb

The Zoom Transition

Skills you’ll learn in this section: zoom transition

The full screen cover transition
Wli wezp htcaep gaqoj yyowwunaac

@Namespace private var namespace
.navigationTransition(
  .zoom(
    sourceID: card.id,
    in: namespace))
.matchedTransitionSource(
  id: card.id,
  in: namespace)
The zoom transition
Zgo yioq bnugbolaal

.interactiveDismissDisabled(true)

Supporting Multiple View Types

Skills you’ll learn in this section: picker control

Picker with two segments
Yuwfew suhn nli qipxilkz

The Carousel

Carousel.swift, included in the starter project in the Supporting Code folder, is an alternative view for listing the cards. It’s an example of a TabView, similar to the one you created in Section 1.

Carousel
Cixiazum

Adding a Picker

➤ In the Views folder, create a new SwiftUI View file named ListSelection.swift.

enum ListState {
  case list, carousel
}
@Binding var listState: ListState
ListSelection(listState: .constant(.list))
var body: some View {
  // 1
  Picker(selection: $listState, label: Text("")) {
  // 2
    Image(systemName: "square.grid.2x2.fill")
      .tag(ListState.list)
    Image(systemName: "rectangle.stack.fill")
      .tag(ListState.carousel)
  }
  // 3
  .pickerStyle(.segmented)
  .frame(width: 200)
}
Segmented picker
Deprinvad jegniz

@State private var listState = ListState.list
ListSelection(listState: $listState)
The picker in place
Zyo hoggok id ycuqa

Group {
  switch listState {
  case .list:
    list
  case .carousel:
    Carousel(selectedCard: $selectedCard)
  }
}
The two card list views
Lmo jya yonl quqt roems

Sharing the Card

Skills you’ll learn in this section: rendering views; share sheet; @MainActor; photo library permissions

Rendering a View to an Image

➤ In the Extensions folder, open UIImageExtensions.swift. Add a new extension at the end of the file:

extension UIImage {
  // 1
  @MainActor static func screenshot(
    card: Card,
    size: CGSize
  ) -> UIImage {
    // 2
    let cardView = ShareCardView(card: card)
    let content = cardView.content(size: size)
    // 3
    let renderer = ImageRenderer(content: content)
    // 4
    return renderer.uiImage ?? UIImage.error
  }
}
ToolbarItem(placement: .topBarLeading) {
  let uiImage = UIImage.screenshot(
    card: card,
    size: Settings.cardSize)
  let image = Image(uiImage: uiImage)
  // Add ShareLink here
}

Sharing Images

SwiftUI provides a standard share sheet for sharing any item that conforms to Transferable. For example, this code will allow you to save text to the Files app through the share sheet:

ShareLink("Share Text", item: "Hello world")
Sharing text from your app
Psamaxx lirj sqit buuz esw

ShareLink(
  item: image,
  preview: SharePreview(
    "Card",
    image: image)) {
      Image(systemName: "square.and.arrow.up")
}
Sharing your card from your app
Ymeduyr xail sinc kyor coaj orj

Your card in the Files app
Qous mipx ar zpo Zevan atb

Configuring Your App to Save Photos

Because of privacy permissions, any app that wishes to save images to the Photo Library first has to configure the app. You’ll have to get permission from the user and let them know how you will use the library data.

Cards will save your card to the Photo Library
Key to ask user for permission to use photo library
Sug ge ukl atuv ler curvucteol hu iyi cfoso kojkajb

Asking user for permission to use photo library
Eplerl uvow dig qugfexquoj xi elo xvibo wodvudr

Your shared card in the Photos Library
Gook snimag tagm ib vsi Ypesul Soqzejt

Challenges

With your app almost completed, in CardsApp, change CardStore to use real data instead of the default preview data. Erase all contents and settings in Simulator to make sure that there are no cards in the app.

Challenge 1: Save & Load the Card Thumbnail

Currently, the list of cards doesn’t show a preview of the card. When you tap Done on the card, you should save a preview of the card to a file and show this as the card thumbnail in place of the card’s background color.

The thumbnail image
Ghu yzedrsuih otevi

Challenge 2: Change the Text Entry Modal View

In the Supporting Code folder, you’ll find an enhanced Text Entry view, called TextView.swift, that lets users pick fonts and colors when they enter text. There’s a list of some of the fonts available on iOS in AppFonts.swift.

Text entry with fonts and colors
Halh evgfd duny qikpd igq rocexm

Key Points

  • Animation is easy to implement with the withAnimation(_:_:) closure and makes a good app great.
  • You can animate explicitly with withAnimation(_:_:) or implicitly by observing a property with the animation(_:value:) modifier.
  • Transitions are also easy with the transition(_:) modifier. Remember to use withAnimation(_:_:) on the property that controls the transition so that the transition animates.
  • Picker views allow the user to pick one of a set of values. You can have a wheel style picker or a segmented style picker.
  • Using SwiftUI’s ShareLink, you can share any item that conforms to Transferable. The share sheet will automatically show apps that make sense for the item.

Where to Go From Here?

A great example of an app with complex layout and animation is Apple’s Fruta sample app. This is a full–featured app where “Users can order smoothies, save favorite drinks, collect rewards, and browse recipes.” Fruta also has various features, such as widgets. Download the app and see if you can work out how it all fits together.

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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now