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.
You can see the sample code from the URL of the Git repository below. https://github.com/msnsk/Qiita_Timer.git
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
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
}
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)"))
}
}
}
}
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)
}
}
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())
}
}
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.
Next time, I will post about the implementation of a visually pleasing progress bar.
Recommended Posts