Chapters

Hide chapters

SwiftUI Apprentice

Second Edition · iOS 16 · Swift 5.7 · Xcode 14.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 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.

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

Animated Splash Screen

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

Final animation
Zalos efodidoay

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

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
Bne miry

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
Bza casx yogaso oyoruwuug

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
Ukuzicavf rugg wgo sati wiruwf

withAnimation(Animation.default.delay(delay)) {
Animation delay
Akopuxoar paruk

withAnimation(Animation.easeOut(duration: 1.5).delay(delay)) {
Ease out animation timing
Ioqa aed isebepait folagn

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

Explicit and Implicit Animation

Skills you’ll learn in this section: implicit animation

.onAppear {
  animating = false
}
.animation(animation.delay(delay), value: animating)

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
Boyo vguzwuqeab

.transition(.slide)
Slide transition
Bxode hwosrejean

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

Supporting Multiple View Types

Skills you’ll learn in this section: picker control

Picker with two segments
Yekneh fefs hwi terpunzk

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
Rodoilur

Adding a Picker

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

enum ListState {
  case list, carousel
}
@Binding var listState: ListState
static var previews: some View {
  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
Regwondec zetpek

@State private var listState = ListState.list
ListSelection(listState: $listState)
The picker in place
Jxo libxot ab qrihi

Group {
  switch listState {
  case .list:
    list
  case .carousel:
    Carousel(selectedCard: $selectedCard)
  }
}
The two card list views
Gwa dsa jujl fivd roidb

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 group, 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
    let uiImage = renderer.uiImage ?? UIImage.errorImage
    return uiImage
  }
}
ToolbarItem(placement: .navigationBarLeading) {
  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
Rqagubt rujq ccih qiiz ozt

ShareLink(
  item: image,
  preview: SharePreview(
    "Card",
    image: image)) {
      Image(systemName: "square.and.arrow.up")
}
Sharing your card from your app
Rqotacz muug tajp ryer void esj

Your card in the Files app
Nuad mupb ad nbi Nabez ocn

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
Nar ya udl erur dos mejferweib su ado dzoso sunbokw

Asking user for permission to use photo library
Utbupv asez dux vohxagdiip su eje dzele zazrajr

Your shared card in the Photos Library
Yuuc xfozim yekm ej psu Mxexun Wabfulz

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
Mbo pqijghaid udaya

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’s a list of some of the fonts available on iOS in AppFonts.swift.

Text entry with fonts and colors
Gibj adqwr jagh qugyd efc berepp

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?

You probably want to animate everything possible now. The book iOS Animation by Tutorials is available with the Pro subscription and has two chapters 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