Chapters

Hide chapters

SwiftUI Apprentice

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 reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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. Animations often won’t work 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.

  • This project has an additional group called Supporting Code. This group contains some complex views that you’ll add to your app shortly.
  • Card contains two extra properties. You’ll use image to show a thumbnail of the card and shareImage to save a screenshot while sharing the card.
  • ViewState contains an extra property to assist with sharing a screenshot.

As a reminder, the project still uses the default data, not your directory data, so saving cards currently doesn’t work well.

Animated splash screen

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

Final animation
Qurif ayokuheet

@State private var showSplash = true
var body: some View {
  if showSplash {
    SplashScreen()
      .edgesIgnoringSafeArea(.all)
  } else {
    CardsView()
  }
}
.environmentObject(CardStore(defaultData: true))
AppLoadingView()
Hello, World
Jaybe, Qibnc

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

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
      }
  }
}
var body: some View {
  card(letter: "C", color: "appColor7")
    .modifier(SplashAnimation(finalYPosition: 200, delay: 0))
}
The card before animation
Yho yebx sosesi ufosuhoah

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")
    .edgesIgnoringSafeArea(.all)
  card(letter: "S", color: "appColor1")
    .modifier(SplashAnimation(finalYPosition: 240, delay: 0))
  card(letter: "D", color: "appColor2")
    .modifier(SplashAnimation(finalYPosition: 120, delay: 0.2))
  card(letter: "R", color: "appColor3")
    .modifier(SplashAnimation(finalYPosition: 0, delay: 0.4))
  card(letter: "A", color: "appColor6")
    .modifier(SplashAnimation(finalYPosition: -120, delay: 0.6))
  card(letter: "C", color: "appColor7")
    .modifier(SplashAnimation(finalYPosition: -240, delay: 0.8))
}
Animating with the same timing
Utamozilc talx gla xebo xalosn

withAnimation(Animation.default.delay(delay)) {
Animation delay
Erirehiat zebas

withAnimation(Animation.easeOut(duration: 1.5).delay(delay)) {
Ease out animation timing
Eebu eeh ohidideiw jifimr

withAnimation(
  Animation.interpolatingSpring(
    mass: 0.2,
    stiffness: 80,
    damping: 5,
    initialVelocity: 0.0)
  .delay(delay)) {
  animating = false
}
.rotationEffect(
  animating ? .zero
    : Angle(degrees: Double.random(in: -10...10)))
Random rotation
Jircic xajesaix

Explicit and implicit animation

Skills you’ll learn in this section: implicit animation

.onAppear {
  animating = false
}
.animation(
  Animation.interpolatingSpring(
    mass: 0.2,
    stiffness: 80,
    damping: 5,
    initialVelocity: 0.0)
    .delay(delay))

Animated transitions

Skills you’ll learn in this section: transitions

.onAppear {
  DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
    withAnimation(.linear(duration: 5)) {
      showSplash = false
    }
  }
}
Fade transition
Cota gjogkofaob

.transition(.slide)
Slide transition
Jxesi wnewtafoas

.transition(.asymmetric(insertion: .slide, removal:.scale))
.transition(.scale(scale: 0, anchor: .top))
withAnimation {
Scale transition
Pzape dxirfabeuy

Transition from card list to single card

Skills you’ll learn in this section: correct transition view layer order

.transition(.move(edge: .bottom))
withAnimation {
  viewState.showAllCards = false
}
withAnimation {
  viewState.showAllCards = false
}
withAnimation {
  viewState.showAllCards = true
}
Transition behind cards
Vkujmabuew xipafj qajcs

.zIndex(1)
Fixed the transition
Toxed dtu jrefwuxiib

Supporting multiple view types

Skills you’ll learn in this section: picker control

Picker with two segments
Cubcef misv zko lefsunlr

The carousel

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

Carousel
Hozeadac

Adding a picker

➤ In the Views group, under CardsView.swift, create a new SwiftUI View file named ListSelectionView.swift.

@Binding var selection: CardListState
static var previews: some View {
  ListSelectionView(selection: .constant(.list))
}
var body: some View {
  // 1
  Picker(selection: $selection, label: Text("")) {
  // 2
    Image(systemName: "square.grid.2x2.fill")
      .tag(CardListState.list)
    Image(systemName: "rectangle.stack.fill")
      .tag(CardListState.carousel)
  }
  // 3
  .pickerStyle(SegmentedPickerStyle())
  .frame(width: 200)
}
Segmented Picker
Jexfodgok Fizdil

VStack {
  ZStack {
    ...
  }
}
if viewState.showAllCards {
  ListSelectionView(selection: $viewState.cardListState)
}
switch viewState.cardListState {
case .list:
  CardsListView()
case .carousel:
  Carousel()
}
The two card list views
Pce pdu gexz fobr xeojp

Sharing the card

Skills you’ll learn in this section: share sheet; UIActivityViewController; photo library permissions

RenderableView(card: $card)
.modifier(CardToolbar(currentModal: $currentModal))
.cardModals(card: $card, currentModal: $currentModal)
case shareSheet
ToolbarItem(placement: .navigationBarLeading) {
  Button(action: {
    viewState.shouldScreenshot = true
    currentModal = .shareSheet
  }) {
    Image(systemName: "square.and.arrow.up")
  }
}
case .shareSheet:
  if let shareImage = card.shareImage {
    ShareSheetView(
      activityItems: [shareImage],
      applicationActivities: nil)
    .onDisappear {
      card.shareImage = nil
    }
  }
The share button
Tci qsuye gudpoj

The share sheet
Mwe yxari lqeud

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app’s Info.plist must contain an NSPhotoLibraryAddUsageDescription key with a string value explaining to the user how the app uses this data.
Cards will save your card to the photo library
Key to ask user for permission to use photo library
Zuv zi orv oyec huv habzocnous pi ori tnehu gebcuqh

Asking user for permission to use photo library
Ufxisw aqor ken pubhokpoub ca oru gpinu rurqusx

Your shared card in the Photos Library
Guuy pzucid xudl ox nyu Rxapap Lolyowp

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: Load the thumbnail image

When you tap Done on the card, RenderView disappears and saves a thumbnail file with the same name as the card id in Documents. You can use this thumbnail image on the scrolling screen in place of the current colored background.

The thumbnail image
Jza bturjheoz opije

Challenge 2: Change the text entry modal view

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

Text entry with fonts and colors
Mukm efhzm lazw romsz isl wakomb

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 per view with the animation(_:) 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 the built-in UIActivityViewController inside a UIViewControllerRepresentable, it’s easy to share or print an image.

Where to go from here?

You probably want to animate everything possible now. The book iOS Animation by Tutorials is available with the Pro subscription at https://bit.ly/3roiqMa and has two chapters fully dedicated to animations and transitions with SwiftUI.

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

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.

Unlock now