[SWIFT] iOS app development: Timer app (6. Creation of setting screen)

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

article

The points for creating a timer app are posted in multiple articles. In this article, I will show you how to create a setting screen that includes turning on / off the alarm sound and turning on / off the progress bar.

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. List the setting items
  2. Create SettingView
  3. Create a list in SettingView
  4. Add the setting item to the list of SettingView
  5. Add properties required for various settings to TimeManager
  6. Associate the SettingView settings with TimeManager properties
  7. Make SettingView modal
  8. Add a setting button to MainView and display the setting screen modally

Detailed procedure

1. List the setting items

We will create a setting screen. The PickerView, TimerView, and ButtonsView that I have created so far are all placed in the MainView, and the timer app has no screen other than the MainView.

This time, we will create one setting screen separately from MainView. The following items will be displayed on this setting screen.

--Toggle switch for whether to sound an alarm --Toggle switch to enable vibration --Select an alarm sound from the list --Toggle switch to show progress bar --Toggle switch to show effect animation --Button to close the setting screen

2. Create a Setting View

From the SwiftUI template, create a new file named SettingView.

Again, the settings information will eventually be reflected in the properties of the TimeManager class, so first create an instance of TimeManager with the @EnvironmentObject property wrapper.

SettingView.swift


import SwiftUI

struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager

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

3. Create a list in SettingView

There are two main types of components used in list display in SwiftUI. One is List and the other is Form.

Form is used for the setting screen of a general iOS application, so this time we will use Form.

By the way, even if you divide some sections with a title, List will be displayed as a ruled line including the section division, so it is more suitable for arranging items such as reminders in a row. ..

Regarding the alarm sound selection item, we want to move from the setting screen to the sound selection screen, so put the Form component in the NavigationView. By doing this, it is possible to configure the transition from within the Form to another List or Form.

SettingView.swift


struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        NavigationView {
            Form {
            }
        }
    }
}

4. Add the setting item to the list of SettingView

The screen configuration is assumed as follows.

Section 1: Alarm related </ strong> --Alarm sound on / off toggle switch --Vibration on / off toggle switch --Alarm sound selection

Section 2: Animation related </ strong> --Progress bar display on / off toggle switch --Effect animation on / off toggle switch

  • The actual implementation of Section 2 will be done after the setting screen is created.

Section 3: Close the settings screen </ strong> --Button to close the setting screen

Put three Sections in the Form. The first Section contains two Toggle and one NavigationLink. Put one Toggle in the second Section. Put one Button in the third Section.

If you put the title of the section in Text in the argument header of Section, it will be easier to understand what section it is.

SettingView.swift


struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Alarm:")) {
                    Toggle(isOn: ) {
                    }
                    Toggle(isOn: ) {
                    }
                    NavigationLink(destination: )) {
                    }
                }
                Section(header: Text("Animation:")) {
                    Toggle(isOn: ) {
                    }
                    Toggle(isOn: ) {
                    }
                }
                Section(header: Text("Save:")) {
                    Button(action: ) { 
                    }
                }
            }
        }
    }
}

Now we need a property to reflect the on / off of the Toggle component. The property must be reflected in each component of MainView from the setting screen. For example, if you turn on the alarm on the settings screen, the setting will actually be triggered in MainView.

Therefore, the property that stores each setting information must be added to the TimeManager class with the @Published property wrapper.

5. Add properties required for various settings to TimeManager

Add four properties to TimeManager to store toggle switch configuration information. The following four items will be added.

--Alarm sound on / off setting --Vibration on / off setting --Progress bar display on / off setting --Effect animation display on / off setting

TimeManager.swift


class TimeManager: ObservableObject {
    //(Other properties omitted)

    //Stores the sound name corresponding to the value of the soundID property
    @Published var soundName: String = "Beat"

    //Alarm sound on/Off setting
    @Published var isAlarmOn: Bool = true
    //Vibration on/Off setting
    @Published var isVibrationOn: Bool = true
    //Progress bar display on/Off setting
    @Published var isProgressBarOn: Bool = true
    //Effect animation display on/Off setting
    @Published var isEffectAnimationOn: Bool = true

    //The publish method of the Timer class that fires every second
    var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    //(Method omitted)
}

6. Associate the SettingView settings with TimeManager properties

In the Toggle method argument isOn, specify the property of the TimeManager for which you want to store the settings. At this time, you need to prefix it with the $ symbol. Also, in the closure, enter the name you want to display as a setting item in the Form with Text.

Toggle(isOn: $timeManager.isAlarmOn) {
         Text("Alarm Sound")
 }

In this way, we will describe all Toggle arguments and closures for the time being.

Regarding the alarm sound selection item, there is no selection screen yet, so comment it out once.

The label for the last save button is provided with text and a checkmark icon. Since we want to place it in the center in the horizontal direction, we will bring it to the center by surrounding it with HStack and placing Spacer from the left and right. The action when you tap the button is still blank.

SettingView.swift


struct SettingView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Alarm:")) {
                    Toggle(isOn: $timeManager.isAlarmOn) {
                        Text("Alarm Sound")
                    }
                    Toggle(isOn: $timeManager.isVibrationOn) {
                        Text("Vibration")
                    }
//                    NavigationLink(destination: ) {
//
//                    }
                }
                Section(header: Text("Animation:")) {
                    Toggle(isOn: $timeManager.isProgressBarOn) {
                        Text("Progress Bar")
                    }
                    Toggle(isOn: $timeManager.isEffectAnimationOn) {
                        Text("Effect Animation")
                    }
                }
                Section(header: Text("Save:")) {
                    Button(action: ) {
                        HStack {
                            Spacer()
                            Text("Done")
                            Image(systemName: "checkmark.circle")
                            Spacer()
                        }
                    }
                }
            }
        }
    }
}

Check the display of SettingView on Canvas. Below is the preview code.

struct SettingView_Previews: PreviewProvider {
    static var previews: some View {
        SettingView().environmentObject(TimeManager())
    }
}

It should look like the image below. スクリーンショット 2020-10-28 11.14.35.png

7. Make SettingView modal

Display the SettingView modally. Modal is a shortened name for a modal window, which is like being incapable of doing anything else while the window is open.

Prepare a variable with a property wrapper called @Environment (\ .presentationMode) in SettingView.

Next, write the code to close the modal in the closure of the action argument of the Save button that is tapped when closing the setting screen.

self.presentationMode.wrappedValue.dismiss()

SettingView.swift


struct SettingView: View {
    //Properties for using modal sheets
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        NavigationView {
            Form {
                //(Other Sections omitted)

                Section(header: Text("Save:")) {
                    Button(action: { 
                        //Tap to close modal
                        self.presentationMode.wrappedValue.dismiss()
                    }) {
                        HStack {
                            Spacer()
                            Text("Done")
                            Image(systemName: "checkmark.circle")
                            Spacer()
                        }
                    }
                }
            }
        }
    }
}

This time, on the contrary, we will prepare a button in MainView to open the setting screen. With just one button, it creates a View that is easy to understand. Now let's create a new SwiftUI file named SettingButton.

SettingButtonView.swift


import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
    }
}

The button icon uses "ellipsis.circle.fill" from SF Symbols.

SettingButtonView.swift


import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
    }
}

The size of the setting button is slightly smaller than that of the start / pause button and reset button, so use the frame modifier to set the vertical and horizontal sizes.

SettingButtonView.swift


import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40, height: 40)
    }
}

Now, create a Bool type property in the TimeManager class that shows / hides the modal. Name it isSetting.

TimeManager.swift


class TimeManager: ObservableObject {
    //(Other properties omitted)

    //Display of setting screen/Hide
    @Published var isSetting: Bool = false

    //(Method omitted)
}

And finally, go back to the SettingButtonView and add a .onTapGesture so that the TimeManager's isSetting property is true inside the closure {}.

SettingButtonView.swift


import SwiftUI

struct SettingButtonView: View {
    @EnvironmentObject var timeManager: TimeManager

    var body: some View {
        Image(systemName: "ellipsis.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 40, height: 40)
            .onTapGesture {
                self.timeManager.isSetting = true
    }
}

Let's check the screen with Canvas. Below is the preview code.

struct SettingButton_Previews: PreviewProvider {
    static var previews: some View {
        SettingButtonView()
            .environmentObject(TimeManager())
            .previewLayout(.sizeThatFits)
    }
}

It should look like the image below. スクリーンショット 2020-10-28 10.56.37.png

8. Add a setting button to MainView and display the setting screen modally

Now let's add SettingButtonView to ManiView.

The place to add is the same bottom of the screen as the start / pause and reset buttons below PickerView and TimerView. Therefore, in ZStack {}, ButtonsView and SettingButtonView are layered back and forth. It doesn't matter which is the front or the back.

SettingBottonView adds a padding (.bottom) modifier as well as ButtonsView, as it's better to have all the buttons aligned in the vertical direction.

Then add a .sheet () modifier to display the modal window. The isPresented argument specifies a Bool type property whose modal is displayed when true. That is, the isSetting property of the TimeManager class we prepared earlier.

In the closure {}, describe the View you want to display modally. Here it is SettingView (). At this time, be sure to also describe the property .environmentObject (self.timeManager).

MainView.swift


struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        ZStack {
            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }
            
            VStack {
                Spacer()
                //Layer Buttons View and Setting Button View
                ZStack {
                    ButtonsView()
                        .padding(.bottom)
                    //Added setting button
                    SettingButtonView()
                        .padding(.bottom)
                        //Display SettingView modally when isSetting property is true
                        .sheet(isPresented: $timeManager.isSetting) {
                            SettingView()
                                            .environmentObject(self.timeManager)
                        }
                }
            }
        }
        //(.osReceive modifier part omitted)
    }
}

Since we have added alarm sound and vibration setting items, we will sound an alarm sound when the timer in the closure of the onReceive modifier of MainView becomes 0, and make the description to activate the vibration as if the setting is ON. This is simply conditional branching in the if statement depending on the value of the isAlarmOn property and the isVibrationOn property of the TimeManager class.

MainView.swift


struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        ZStack {
            //(View description omitted)
        }
        //Executes the code in the closure triggered by a timer that is activated every specified time (1 second)
        .onReceive(timeManager.timer) { _ in
            //The timer status is.Do nothing except running
            guard self.timeManager.timerStatus == .running else { return }
            //If the remaining time is greater than 0
            if self.timeManager.duration > 0 {
                //From the remaining time-0.05
                self.timeManager.duration -= 0.05
                //When the remaining time is 0 or less
            } else {
                //Timer status.Change to stopped
                self.timeManager.timerStatus = .stopped
                //Sound an alarm
                if timeManager.isAlarmOn {
                    AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)
                }
                //Activate vibration
                if timeManager.isVibrationOn {
                    AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) {}
                }
            }
        }
    }
}

Let's check the MainView with Canvas. Below is the preview code.

struct MainView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            MainView().environmentObject(TimeManager())
        }
    }
}

It should look like the image below. The setting button is placed between the start button and the reset button with the same height. スクリーンショット 2020-10-28 10.57.44.png

Now, when you tap the setting button, the setting screen will be displayed modally. Please check it with Canvas.

This time, I created a View with only setting buttons as a SettingButtonView, but I think it's okay to put it together in the ButtonsView created earlier.

Recommended Posts