[SWIFT] iOS app development: Timer app (7. Implementation of alarm sound selection)

スクリーンショット 2020-10-28 11.04.43.png

Contents

The points for creating a timer app are posted in multiple articles. In this article, I will post about the implementation of the alarm sound selection screen.

environment

Git repository

You can see the sample code from the URL of the Git repository below. https://github.com/msnsk/Qiita_Timer.git

procedure

  1. Examine the elements (property) required for the alarm sound structure
  2. Create an alarm sound Struct in Data
  3. Make a list of alarm sounds
  4. Allow the alarm sound to be auditioned
  5. Reflect the selected alarm sound in the properties of the TimeManager class
  6. Implement screen transition between setting screen and alarm sound list

1. Examine the elements (property) required for the alarm sound structure

At this time, the alarm sound is specified by default and cannot be changed. We will prepare some alarm sounds and make them changeable from the setting screen.

Considering what elements are necessary as an alarm sound object, the following two are sufficient.

--Sound ID: Required to make a sound with a method in the AudioToolbox library --Sound name: Required to display on the setting screen or list screen

2. Create an alarm sound Struct in Data

Create a structure named Sound in your Data.swift file and include the two elements considered in step 1 as its properties.

At this time, the data type of the Sound ID property must be SystemSoundID. This data type is included in the AudioToolbox library, so be sure to import it.

It inherits the Identifiable protocol because the sound ID requires the instance created from this structure to be unique.

Data.swift


import SwiftUI
import AudioToolbox //Additional import

//(Other enum omitted)

struct Sound: Identifiable {
    let id: SystemSoundID
    let soundName: String
}

3. Make a list of alarm sounds

Create a new Swift file named SoundListView from your SwiftUI template. Import the AudioToolbox library to use the system sounds.

Like any other View, we always want to see the properties of the TimeManager class, so we create an instance of it. Prefix the @EnvironmentObject property wrapper to var.

SoundListView


import SwiftUI
import AudioToolbox

struct SoundListView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        Text("Hello, World!")
    }
}

Next, prepare the alarm sound data.

Create an instance from the Sound structure created in step 1, specifying the id and soundName properties, and store it in an Array named Sounds. Refer to the following resources for the specific sound ID. Make soundName easy to understand by referring to the file name in the resource. https://github.com/TUNER88/iOSSystemSoundsLibrary

SoundListView


struct SoundListView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    let sounds: [Sound] = [
        Sound(id: 1151, soundName: "Beat"),
        Sound(id: 1304, soundName: "Alert"),
        Sound(id: 1309, soundName: "Glass"),
        Sound(id: 1310, soundName: "Horn"),
        Sound(id: 1313, soundName: "Bell"),
        Sound(id: 1314, soundName: "Electronic"),
        Sound(id: 1320, soundName: "Anticipate"),
        Sound(id: 1327, soundName: "Minuet"),
        Sound(id: 1328, soundName: "News Flash"),
        Sound(id: 1330, soundName: "Sherwood Forest"),
        Sound(id: 1333, soundName: "Telegraph"),
        Sound(id: 1334, soundName: "Tiptoes"),
        Sound(id: 1335, soundName: "Typewriterst"),
        Sound(id: 1336, soundName: "Update")
    ]

    var body: some View {
        Text("Hello, World!")
    }
}

Prepare List {} in the outermost frame inside body {}. This List is similar to the Form used in SettingView, and is used to display many items in tabular format separated by ruled lines.

ForEach is useful when you want to order the items in an Array in a List {}. By setting ForEach (sounds), the items in sounds will be looped in order. The processing content is described in {}.

First, write the sound name as text in {}.

SoundListView


struct SoundListView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    let sounds: [Sound] = [
        //(Each sound object omitted)
    ]

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //Display in the list by the value of soundName
                Text(("\(sound.soundName)"))
            }
        }
    }
}

4. Allow the alarm sound to be auditioned

As an actual operation of the app, I want to check what kind of sound it sounds when selecting an alarm.

We will display the audition icon at the left end of each line of the list, and tap it to play the sound.

Add HStack {} inside the ForEach {} so that the components are side by side on each row.

At the very beginning of HStack {}, add an Image component as an icon. I chose "speaker.2.fill" from SF Symbols. Then add the .onTapGesture {} modifier and write the AudioToolbox library method AudioServicesPlayAlertSoundWithCompletion () in the closure {}. By setting the argument to sound.id, the id of the tapped line is picked up as soundID and played. The second argument can be nil.

SoundListView


struct SoundListView: View {
    //(Property omitted)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //Description of list rows
                HStack {
                    //Audition icon
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //Display in the list by the value of soundName
                    Text(("\(sound.soundName)"))
                }
            }
        }
        .navigationBarTitle("Alarm Sound Setting", displayMode: .automatic)
    }
}

5. Reflect the selected alarm sound in the properties of the TimeManager class

Tap a row to have that sound selected.

Specifically, HStack {} corresponds to a row in the list, so add .onTapGesture {} as its modifier. In the .onTapGesture {} closure {}, make sure that the soundID and soundname of the selected line are assigned to the soundID and soundName properties of the TimeManager class, respectively.

However, with this alone, you can only select it by tapping the display of the sound name on the sound list. There is no response even if you tap the blank part of the line. It's fine, but I'll fix it here as well.

Add .contentShape (Rectangle ()) before .onTapGesture {} as a modifier for HStack. This makes the HStack look like a square object, which means that the entire row is a single object, and you can tap a blank area to select an alarm sound.

SoundListView


struct SoundListView: View {
    //(Property omitted)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //Description of list rows
                HStack {
                    //Audition icon
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //Display in the list by the value of soundName
                    Text(("\(sound.soundName)"))
                }
                //Think of HStack as a square object
                .contentShape(Rectangle())
                //Tap a line to select a sound (reflect ID and name in TimeManager)
                .onTapGesture {
                    self.timeManager.soundID = sound.id
                    self.timeManager.soundName = sound.soundName
                }
            }
        }
    }
}

In addition, use the if syntax so that a checkmark appears to the right of the currently selected sound name. I want the checkmark to be on the far right of the line, so put Spacer () between the sound name and the checkmark.

SoundListView


struct SoundListView: View {
    //(Property omitted)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //Description of list rows
                HStack {
                    //Audition icon
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //Display in the list by the value of soundName
                    Text(("\(sound.soundName)"))
                    
                    Spacer()
                    
                    //Show a checkmark for the currently selected sound
                    if self.timeManager.soundID == sound.id {
                        Image(systemName: "checkmark")
                    }
                }
                //Think of HStack as a square object
                .contentShape(Rectangle())
                //Tap a line to select a sound (reflect ID and name in TimeManager)
                .onTapGesture {
                    self.timeManager.soundID = sound.id
                    self.timeManager.soundName = sound.soundName
                }
            }
        }
    }
}

Check the SoundListView in Canvas. Below is the preview code.

struct SoundListView_Previews: PreviewProvider {
    static var previews: some View {
        SoundListView()
            .environmentObject(TimeManager())
    }
}
スクリーンショット 2020-10-28 11.04.57.png

6. Implement screen transition between setting screen and alarm sound list

Add an alarm sound selection item to the main setting screen SettingView so that it transitions to the sound list screen SoundListView.

The outermost part of the SettingView body {} is surrounded by NavigationView {}. This allows you to move the screen to the next level by adding NavigationLink {} inside the Form {} in it.

Set the setting item name to "Sound Selection" with Text (), put Spacer () in between so that the name of the currently selected alarm sound is displayed at the right end of the line, and use Text () to TimeManager. Specifies the class property soundName.

Add the following two form modifiers to specify the display of the NavigationBar (at the top of the screen) and the display style of the entire NavigationView.

.navigationBarTitle() .navigationViewStyle()

SettingView.swift


struct SettingView: View {
    //(Property omitted)
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Alarm:")) {
                    Toggle(isOn: $timeManager.isAlarmOn) {
                        Text("Alarm Sound")
                    }
                    Toggle(isOn: $timeManager.isVibrationOn) {
                        Text("Vibration")
                    }
                    //Screen transition to sound selection screen
                    NavigationLink(destination: SoundListView()) {
                        HStack {
                            //Setting item name
                            Text("Sound Selection")
                            Spacer()
                            //Currently selected alarm sound
                            Text("\(timeManager.soundName)")
                        }
                    }
                }
                Section(header: Text("Animation:")) {
                    //(The contents of the Animation section are omitted)
                }
                Section(header: Text("Save:")) {
                    //(The contents of the Save section are omitted)
                }
            }
            .navigationBarTitle("Setting", displayMode: .automatic)
            .navigationViewStyle(DefaultNavigationViewStyle())
        }
    }
}

Then, add the .navigationBarTitle modifier to List {} of SoundListView and give the screen title "Alarm Sound Setting".

SoundListView


struct SoundListView: View {
    //(Property omitted)

    var body: some View {
        List {
            ForEach(sounds) {sound in
                //Description of list rows
                HStack {
                    //Audition icon
                    Image(systemName: "speaker.2.fill")
                        .onTapGesture {
                            AudioServicesPlayAlertSoundWithCompletion(sound.id, nil)
                    }           
                    //Display in the list by the value of soundName
                    Text(("\(sound.soundName)"))
                    
                    Spacer()
                    
                    //Show a checkmark for the currently selected sound
                    if self.timeManager.soundID == sound.id {
                        Image(systemName: "checkmark")
                    }
                }
                //Tap a line to select a sound (reflect ID and name in TimeManager)
                .onTapGesture {
                    self.timeManager.soundID = sound.id
                    self.timeManager.soundName = sound.soundName
                }
            }
        }
        .navigationBarTitle("Alarm Sound Setting", displayMode: .automatic)
    }
}

This completes the implementation of the setting screen including the alarm sound selection. Check the SettingView on Canvas. It should look like the image below. スクリーンショット 2020-10-28 11.08.56.png

Next time, I will post about the implementation of a visually pleasing progress bar.

Recommended Posts

iOS app development: Timer app (7. Implementation of alarm sound selection)
iOS app development: Timer app (5. Implementation of alarm and vibration)
iOS app development: Timer app (8. Implementation of progress bar)
iOS app development: Timer app (4. Countdown implementation)
iOS app development: Timer app (6. Creation of setting screen)
iOS app development: Timer app (2. Timer display)
iOS app development: Timer app (summary)
iOS app development: Timer app (1. Timer time setting)
iOS app development: Timer app (10. Create animation)
iOS app development: Timer app (9. Customize the color of the progress bar)
iOS app development: Timer app (3. Start / Stop button, Reset button)
Complete self-study IOS app development diary
iOS App Development Skill Roadmap (Introduction)
List of libraries useful for ios application development
SKStoreReviewController implementation memo in Swift UI of iOS14