Chapters

Hide chapters

watchOS With SwiftUI by Tutorials

Second Edition · watchOS 9 · Swift 5.8 · Xcode 14.3

Section I: watchOS With SwiftUI

Section 1: 13 chapters
Show chapters Hide chapters

7. Lifecycle
Written by Scott Grosch

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The lifecycle of a watchOS app is a bit more complex than that of iOS apps. There are five possible states that a watchOS app may find itself in:

  • Not running
  • Inactive
  • Active
  • Background
  • Suspended

Common State Transitions

The five possible states that a watchOS app finds itself in are represented by the grey boxes in the following image:

Foreground Not running Inactive Background B Active Background Suspended A C

As a developer, you’ll only interact with three of the states. The not running and suspended states are only active, pun intended :], when your app is not running.

Launching to the Active State

If the user has yet to run the app or the system purged the app from memory, the app begins in the not running state. Once the app launches, it follows path A and transitions to the inactive state.

Transitioning to an Inactive State

As soon as the user lowers their arm, they’re no longer actively using the app. At that point, watchOS will change to the inactive state. As previously mentioned, the app is still running and executing your code. Having your app still running, even though it’s in an inactive state, is a point of confusion for many new watchOS developers.

Transitioning to the Background

Two minutes after transitioning to the inactive state, or when the user switches to another app, your app will transition to the background state. By following the lower part of path A, you can also launch the app directly to the background mode via the system. Background sessions and background tasks will both launch an app directly to the background state.

Returning to Clock

Before watchOS 7, you could ask for eight minutes before your app transitioned to the background. Your app can no longer make that change because developers kept forgetting to set it back to two minutes.

Additional Background Execution Time

If the amount of work you need to perform when transitioning to the background takes more time than watchOS provides your app, you need to rethink what you’re doing. For example, this is not the time to make a network call. If you’ve performed all the optimizations you can and still need a bit more processing time, you can call the performExpiringActivity(withReason:using:) method of the ProcessInfo class.

processInfo.performExpiringActivity(
  withReason: "I'm really slow"
) { suspending in
  // 1
  guard !suspending else {
    cancel = true
    return
  }

  // 2
  guard !cancel else { return }
  try? managedObjectContext.save()

  // 3
  guard !cancel else { return }
  userDefaults.set(someData(), forKey: "criticalData")
}

Transitioning Back to the Active State

If the user interacts with the app while it’s in the background state, watchOS will transition it back to active via this process:

Transitioning to the Suspended State

When your app finally transitions to the suspended state, all code execution stops. Your app is still in memory but is not processing events. The system will transition your app to the suspended state when your app is in the background and doesn’t have any pending tasks to complete.

Always On State

Until watchOS 6, the Apple Watch would appear to go to sleep when the user had not recently interacted with it. Always On state changed that so that the watch continued to display the time. However, watchOS would blur the currently running app and show the time over your app’s display.

State Change Sample

The sample materials for this chapter contain a Lifecycle project which you can run against a physical device to observe state changes. When you raise and lower your wrist, you’ll see the state changing between active and inactive. If you leave the app inactive for two minutes, you’ll notice it switching to background mode.

Extended Runtime Sessions

It’s possible to keep your app running, sometimes even while in the background, for four specific types of use cases.

Self Care

Apps focused on the user’s emotional well-being or health will run in the foreground, even when the watch screen isn’t on. watchOS will give your app a 10 minute session that will continue until the user switches to another app or you invalidate the session.

Mindfulness

Silent meditation has become a popular practice in recent years. Like self-care, mindfulness apps will stay in the foreground. Meditation is a time-consuming process, though, so watchOS will give your app a 1 hour session.

Physical Therapy

Stretching, strengthening and range‑of‑motion exercises are perfect for a physical therapy session. Unlike the last two session types, physical therapy sessions run in the background. A background session will run until the time limit expires or the app invalidates the session, even if the user launches another app.

Smart Alarm

Smart alarms are a great option when you need to schedule a time to check the user’s heart rate and motion. You’ll get a 30 minute session for your watch to run in the background.

Brush Your Teeth

If you have children, you know what a chore it can be to get them to brush their teeth. Not only do you have to convince them to start brushing, but then they have to brush for the full two minutes recommended by most dentists. Seems like a great job for an Apple Watch app!

Assigning a Session Type

Brushing teeth falls into the self-care session type, but Xcode doesn’t know that unless you tell it. Following the steps in the image below, add a new capability to your Watch App target. First, select the Toothbrush app from the Project Navigator menu. Next, select the Toothbrush Watch App Target. Then, choose Signing & Capabilities from the main view and press the + Capability option. Finally, select Background Modes from the list of capabilities when prompted.

The Content Model

Create a new file named ContentModel. Add:

import SwiftUI

// 1
final class ContentModel: NSObject, ObservableObject {
  // 2
  @Published var roundsLeft = 0
  @Published var endOfRound: Date?
  @Published var endOfBrushing: Date?

  // 3
  private var timer: Timer!
  private var session: WKExtendedRuntimeSession!
}
func startBrushing() {
  session = WKExtendedRuntimeSession()
  session.delegate = self
  session.start()
}
extension ContentModel: WKExtendedRuntimeSessionDelegate {
  // 1
  func extendedRuntimeSessionDidStart(
    _ extendedRuntimeSession: WKExtendedRuntimeSession
  ) {
  }

  // 2
  func extendedRuntimeSessionWillExpire(
    _ extendedRuntimeSession: WKExtendedRuntimeSession
  ) {
  }

  // 3
  func extendedRuntimeSession(
    _ extendedRuntimeSession: WKExtendedRuntimeSession,
    didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
    error: Error?
  ) {
  }
}
let secondsPerRound = 30.0
let now = Date.now

// 1
endOfRound = now.addingTimeInterval(secondsPerRound)
endOfBrushing = now.addingTimeInterval(secondsPerRound * 4)

roundsLeft = 4

// 2
let device = WKInterfaceDevice.current()
device.play(.start)
// 1
timer = Timer(
  fire: endOfRound!,
  interval: secondsPerRound,
  repeats: true
) { _ in
  self.roundsLeft -= 1

  // 2
  guard self.roundsLeft == 0 else {
    self.endOfRound = Date.now.addingTimeInterval(secondsPerRound)
    device.play(.success)
    return
  }

  // 3
  device.play(.success)
  device.play(.success)
}

// 4
RunLoop.main.add(timer, forMode: .common)
extendedRuntimeSession.invalidate()
timer.invalidate()
timer = nil

endOfRound = nil
endOfBrushing = nil
roundsLeft = 0

The Content View

Edit ContentView, and you’ll see that it’s already configured to print the ScenePhase for you during phase updates. The first task required is, of course, to use the model you just created. So, add the following line to the view:

@ObservedObject private var model = ContentModel()
// 1
VStack {
  // 2
  Button {
    model.startBrushing()
  } label: {
    Text("Start brushing")
  }
  .disabled(model.roundsLeft != 0)
  .padding()

  // 3
  if let endOfBrushing = model.endOfBrushing,
     let endOfRound = model.endOfRound {
    Text("Rounds Left: \(model.roundsLeft - 1)")
    Text("Total time left: \(endOfBrushing, style: .timer)")
    Text("This round time left: \(endOfRound, style: .timer)")
  }
}

Ready, Set, Go

While functional, you can do better! Have you ever used the Workout app on your watch? When you start a workout, you get a few seconds to get ready before it begins. That seems useful for your app as well. The starter sample contains GetReadyView that simulates that display.

@Published var showGettingReady = false
showGettingReady = false
model.showGettingReady = true
// 1
.overlay(
  // 2
  VStack {
    if model.showGettingReady {
      // 3
      GetReadyView {
        model.startBrushing()
      }
      .frame(width: 125, height: 125)
      .padding()
    } else {
      // 4
      EmptyView()
    }
  }
)

Key Points

  • An inactive phase on watchOS doesn’t mean the app isn’t running.
  • Prefer SwiftUI’s .scenePhase environment variable over extension delegate methods.
  • For specific types of apps, extended runtimes let your app keep running even when in the background.

Where to Go From Here?

Check out Apple’s documentation for WatchKit Life Cycles and Extended Runtime Sessions.

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