BLACK FRIDAY: Save 50% on all my Swift books and bundles! >>

Ending the app with allowsHitTesting()

Paul Hudson    @twostraws   

SwiftUI lets us disable interactivity for a view by setting allowsHitTesting() to false, so in our project we can use it to disable swiping on any card when the time runs out by checking the value of timeRemaining.

Start by adding this modifier to the innermost ZStack – the one that shows our card stack:

.allowsHitTesting(timeRemaining > 0)

That enables hit testing when timeRemaining is 1 or greater, but sets it to false otherwise because the user is out of time.

The other outcome is that the user flies through all the cards correctly, and ends with none left. When the final card goes away, right now our timer slides down to the center of the screen, and carries on ticking. What we want to happen is for the timer to stop so users can see how fast they were, and also to show a button allowing them to reset their cards and try again.

This takes a little thinking, because just setting isActive to false isn’t enough – if the app moves to the background and returns to the foreground, isActive will be re-enabled even though there are no cards left.

Let’s tackle it piece by piece. First, we need a method to run to reset the app so the user can try again, so add this to ContentView:

func resetCards() {
    cards = Array<Card>(repeating: .example, count: 10)
    timeRemaining = 100
    isActive = true
}

Second, we need a button to trigger that, shown only when all cards have been removed. Put this after the innermost ZStack, just below the allowsHitTesting() modifier:

if cards.isEmpty {
    Button("Start Again", action: resetCards)
        .padding()
        .background(.white)
        .foregroundStyle(.black)
        .clipShape(.capsule)
}

Now we have code to restart the timer when resetting the cards, but now we need to stop the timer when the final card is removed – and make sure it stays stopped when coming back to the foreground.

We can solve the first problem by adding this to the end of the removeCard(at:) method:

if cards.isEmpty {
    isActive = false
}

As for the second problem – making sure isActive stays false when returning from the background – we should just update our scene phase code so it explicitly checks for cards:

.onChange(of: scenePhase) {
    if scenePhase == .active {
        if cards.isEmpty == false {
            isActive = true
        }
    } else {
        isActive = false
    }
}

Done!

Save 50% in my WWDC sale.

SAVE 50% All our books and bundles are half price for Black Friday, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.