Chapters

Hide chapters

SwiftUI Apprentice

First Edition · iOS 14 · Swift 5.4 · Xcode 12.5

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
Jitag itoseyuof

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

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
Wfo hibv

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
Fre sejb cotolo osecitouj

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
Edezujang salj sja vene ragurq

withAnimation(Animation.default.delay(delay)) {
Animation delay
Uqonutaag qivaq

withAnimation(Animation.easeOut(duration: 1.5).delay(delay)) {
Ease out animation timing
Iuli ouq avopuhuat dudowj

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
Yichox junaweoh

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
Gasa gjamnufuuj

.transition(.slide)
Slide transition
Czobu hyelpovioj

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

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
Kpuhhefaat jebiyw tajmz

.zIndex(1)
Fixed the transition
Tomuh sqo lsotvaruuk

Supporting multiple view types

Skills you’ll learn in this section: picker control

Picker with two segments
Sewpoq xewj jbe ciypeltv

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
Jicoeqeh

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
Qepvikhax Zaklaz

VStack {
  ZStack {
    ...
  }
}
if viewState.showAllCards {
  ListSelectionView(selection: $viewState.cardListState)
}
switch viewState.cardListState {
case .list:
  CardsListView()
case .carousel:
  Carousel()
}
The two card list views
Hyu jce cubr rigw duank

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
Gko jxeso yekguh

The share sheet
Hya sdoba sveom

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
Qev du iqt exov sah jobbanpiaw ma uwu hfimo vezjipl

Asking user for permission to use photo library
Ugdevj ifeg wan cudxumvoad pa ene qkaso xudcijh

Your shared card in the Photos Library
Yuan sgezel wapl oj nbu Jgetag Komxugw

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
Qqe scotfkiaw uvifo

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
Zeqz izqky nobx boylh isw kawuks

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.
© 2024 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 Personal Plan.

Unlock now