Swiftmas – Day 08 – Pets at Christmas

Posted by

·

Welcome to Day 08 of #The12DaysOfSwiftmas

Today’s prompt…

Swiftmas 2023 GitHub repo: https://github.com/thecodingsprite/Swiftmas-2023/tree/main

Who doesn’t love booping a cat nose! (Seriously you are deranged if you don’t ;))

So my idea for this app was pretty simple & easy to come up with as a cat owner. A cat whose nose you can boop & something magical happens!

So you guessed it from the above image. The cat is a design file. One I made myself in illustrator. However if you clone the GitHub repo for this project then you’ll see there are a few additional images for when the nose is booped! (Spoilers!)

So whilst we (hopefully) have a good idea of the logic this kind of app is going to be using. I want to start as I always do, laying out the design > adding logic > refactoring code.

import SwiftUI

struct ContentView: View {
    
    
    var body: some View {
        ZStack {
            // Background
            Color.mint
                .ignoresSafeArea()
            // Instruction
            VStack {
                Text("Boop the nose")
                    .font(.headline)
                Spacer()
            }
            .padding()
            
            
            Image("cat-body")
                .resizable()
                .aspectRatio(contentMode: .fit)
            
            Image("nose-1")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .onTapGesture {
                    // TODO: change nose
                }
        }
    }
}

So you guessed it, the first thing we are going to need for the logic part is a @State property to keep track of when the user boops the nose. Again a little tip, for the designing part of the items when the nose will be booped by the user, you can just hard code the true instead of the false here & hard code it back to false after the design is complete.

@State var isNoseBooped = false

So next we are going to change the nose 1 where the //TODO: is.

 if isNoseBooped == false {
                Image("nose-1")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .onTapGesture {
                        isNoseBooped.toggle()
                    }
            }

Now we add the view for when the nose is booped by the user. For the first part I am only concentrating on the nose changing. It was later on that I added the other elements to the design to make it pop. We will be adding those other elements, don’t think I’ve forgotten.

 // Festive Nose
            if isNoseBooped {
                ZStack {
                    
                    VStack {
                        Spacer()
                        
                        Circle()
                            .foregroundStyle(.red)
                            .blur(radius: 5.0)
                            .opacity(0.6)
                            .frame(width: 50)
                            .padding(.bottom, 160)
                            .padding(.trailing, 25)
                        
                        Spacer()
                        
                        Text("Meowy Christmas")
                            .font(.title)
                            .bold()
                            .foregroundStyle(.red)
                            .padding()
                    }
                    
                    Image("nose-2")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .onTapGesture {
                            isNoseBooped.toggle()
                        }
                }
            }

So by now this code should be working. When you boop the nose, we have a Rudolph nose & a Meowy Christmas message. But we can do better, as always there is room for improvement. My first thing is to add animation to the change. It just adds a smoothness that is so worth it.

So we add the withAnimation{} wrapper around the toggle() in the onTapGesture buttons like so (both buttons so it works both ways, this will add a default smooth transition between the changes):

.onTapGesture {
     withAnimation {
           isNoseBooped.toggle()
                   }
               }

Now let’s add a custom modifier to clean up the code a bit before we add more images:

// MARK: - Custom Modifiers

extension Image {
    func resize() -> some View {
        self
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}

Now we can add the code for the antlers & eyes to change too.

// Cat Images
            Image(isNoseBooped ? "antlers" : "")
                .resize()
            
            Image("cat-body")
                .resize()
            
            Image(isNoseBooped ? "eyes-closed" : "eyes-open")
                .resize()
            
            if isNoseBooped == false {
                Image("nose-1")
                    .resize()
                    .onTapGesture {
                        withAnimation {
                            isNoseBooped.toggle()
                        }
                    }

If you want to go another step, why not change the message up the top when the nose is booped too:

Text(isNoseBooped ? "Meow ho ho" : "Boop the nose")

Fancy a challenge? Why not add a meow sound too!

So if you knew my cat you would know that every time I boop her nose, or touch her in general (I swear she is broken ha ha) then I will get meowned at. So it was only right this cat meowed too.

So to start with you will need a sound file. You can find the original in the GitHub repo for this project or use your own. Just make sure to drag it into your Xcode project (in the file navigator) & check “copy items if needed” & target should be this application). Also note the file name & extension type as you will need this. For this project the file is named “Meow” & is an .mp3 file type. To begin we will add a SystemSoundID extension to our project underneath the Image extension we made earlier.

extension SystemSoundID {
    static func playFileNamed(fileName: String, withExtenstion fileExtension: String) {
        var sound: SystemSoundID = 0
        if let soundURL = Bundle.main.url(forResource: fileName, withExtension: fileExtension) {
            AudioServicesCreateSystemSoundID(soundURL as CFURL, &sound)
            AudioServicesPlaySystemSound(sound)
        }
    }
}

Now we want the sound to play when the nose is booped when the image is plain & not festive. This is where we will add our function call passing in the filename & the extension type like so:

if isNoseBooped == false {
         Image("nose-1")
            .resize()
            .onTapGesture {
                 withAnimation {
                     // Meow sound
                     SystemSoundID.playFileNamed(fileName: "Meow", withExtenstion: "mp3")
                            
                            isNoseBooped.toggle()
                        }
                    }
                }

Now when you boop the nose, a little meow plays too! If only I could figure out how to get my screenshot capture software to record the system audio then I could show you ha!

The complete functional code is at the bottom of this post.


Our Completed Application

See you tomorrow for Day 09


Complete Code

import SwiftUI
import AudioToolbox

struct ContentView: View {
    
    @State var isNoseBooped = false
    
    var body: some View {
        ZStack {
            // Background
            Color.mint
                .ignoresSafeArea()
            
            // Instruction
            VStack {
                Text(isNoseBooped ? "Meow ho ho" : "Boop the nose")
                    .font(.headline)
                Spacer()
            }
            .padding()
            
            // Cat Images
            Image(isNoseBooped ? "antlers" : "")
                .resize()
            
            Image("cat-body")
                .resize()
            
            Image(isNoseBooped ? "eyes-closed" : "eyes-open")
                .resize()
            
            if isNoseBooped == false {
                Image("nose-1")
                    .resize()
                    .onTapGesture {
                        withAnimation {
                            // Meow sound
                            SystemSoundID.playFileNamed(fileName: "Meow", withExtenstion: "mp3")
                            
                            isNoseBooped.toggle()
                        }
                    }
                }
            
            // Festive Nose
            if isNoseBooped {
                ZStack {
                    
                    VStack {
                        Spacer()
                        
                        // Nose glow
                        Circle()
                            .foregroundStyle(.red)
                            .blur(radius: 5.0)
                            .opacity(0.6)
                            .frame(width: 50)
                            .padding(.bottom, 160)
                            .padding(.trailing, 25)
                        
                        Spacer()
                        
                        Text("Meowy Christmas")
                            .font(.title)
                            .bold()
                            .foregroundStyle(.red)
                            .padding()
                    }
                    
                    Image("nose-2")
                        .resize()
                        .onTapGesture {
                            withAnimation {
                                isNoseBooped.toggle()
                            }
                        }
                }
            }
        }
    }
}

// MARK: - Custom Modifiers

extension Image {
    func resize() -> some View {
        self
            .resizable()
            .aspectRatio(contentMode: .fit)
    }
}


extension SystemSoundID {
    static func playFileNamed(fileName: String, withExtenstion fileExtension: String) {
        var sound: SystemSoundID = 0
        if let soundURL = Bundle.main.url(forResource: fileName, withExtension: fileExtension) {
            AudioServicesCreateSystemSoundID(soundURL as CFURL, &sound)
            AudioServicesPlaySystemSound(sound)
        }
    }
}

Discover more from

Subscribe to get the latest posts sent to your email.

thecodingsprite avatar

About the author

Hi! My name is Billie, my friends call me Billie Boo. I am a self taught iOS developer with a background in computer science, animation, graphic design & web design. I love sharing my knowledge & projects with the world & that is my mission for this blog. It’s never too late or too hard to follow your dreams.