[SWIFT] iOS app development: Timer app (3. Start / Stop button, Reset button)

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

Contents

The points for creating a timer app are posted in multiple articles. In this article, I will provide the steps to implement the start, pause, and reset functions that are the main operations of the countdown timer.

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. Create a property in TimeManager that shows the status of the timer
  2. Create a method in TimeManager to change the status of the timer
  3. Create ButtonsView
  4. Create a start / stop button in ButtonsView
  5. Create a reset button in ButtonsView
  6. Place ButtonsView on MainView

1. Create a property in TimeManager that shows the status of the timer

Create a property in the TimeManager class that represents the status of the timer. The status is the following three.

--running: The timer is counting down --pause: The timer is paused and can be resumed --stop: The timer has finished counting down

As a preparation, create a new enum in the Data.swift file that represents the status of the timer.

Data.swift


//(Other enums omitted)

enum TimerStatus {
    case running
    case pause
    case stopped
}

Create a property in the TimeManager class with the created enum as the data type. The default value for this property should be .stopped, as the timer must be inactive until the user taps the start button.

TimeManager.swift


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

    //Timer status
    @Published var timerStatus: TimerStatus = .stopped

    //(Method omitted)

2. Create a method in TimeManager to change the status of the timer

We haven't created any buttons yet, but we'll create methods to start, pause, and completely terminate the timer the first time we tap each button. In other words, these methods change the value of the timer status property created earlier each time.

TimeManager.swift


class TimeManager: ObservableObject {
    //(Property omitted)

    //(Other methods omitted)

    //Method that is activated when the start button is tapped
    func start() {
        //Timer status.set to running
        timerStatus = .running
    }
    
    //Method that is activated when the pause button is tapped
    func pause() {
        //Timer status.pause
        timerStatus = .pause
    }
    
    //Method that is activated when the reset button is tapped
    func reset() {
        //Timer status.Stop
        timerStatus = .stopped
        //Forcibly set to 0 even if the remaining time is not 0 yet
        duration = 0
    }
}

3. Create ButtonsView

Create a new file named ButtonsView.swift. A struct with the same name will be generated.

Since it is necessary to link the button operation and the timerStatus property of the TimeManager class created in step 1, create an instance of the TimeManager class in this View as well. As usual, I'll also add a property wrapper for @EnvironmentObject.

ButtonsView.swift


import SwiftUI
    @EnvironmentObject var timeManager: TimeManager

struct ButtonsView: View {
    var body: some View {

    }
}

4. Create a start / stop button in ButtonsView

The start button and stop button will be displayed in the same place on the screen. Depending on the timer status, which button is displayed on the screen is conditional.

--Display the pause button when .running --Display start button when .pause or .stopped

The following two button icons from genuine Apple SF Symbols are used (commonly used for playing and pausing audio).

--play.circle.fill: Start button --pause.circle.fill: Pause button

When the remaining time display is 0, it means that you cannot start or pause, so make a conditional branch with the transparency .opacity modifier.

Specify the setTimer method so that when you tap the button, the timer is set when you tap the Start button when PickerView is displayed (and the time is set). When the PickerView is displayed, the timer status is .stopped, so describe this with an if statement in the onTapGesture modifier.

When this setTimer method is executed by tapping the button, the time set in the duration property of the remaining time and the maxValue property of the maximum time is assigned. Write an if statement in the onTapGesture modifier so that the start method is also executed when this duration is non-zero and the timer status is not .running.

As another condition, the if statement will be followed by the if else statement so that it can be paused only when the timer status is .running.

ButtonsView.swift


struct ButtonsView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        //running:Pause button/pause or stopped:Start button
        Image(systemName: self.timeManager.timerStatus == .running ? "pause.circle.fill" : "play.circle.fill")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 75, height: 75)
            //Space on the right side of the button and on the edge of the screen
            .padding(.trailing)
            //If the Picker's hours, minutes, and seconds are all 0, the button transparency is set to 0..1 to 1 otherwise 1 (opaque)
            .opacity(self.timeManager.hourSelection == 0 && self.timeManager.minSelection == 0 && self.timeManager.secSelection == 0 ? 0.1 : 1)
            //Action when tapping a button
            .onTapGesture {
                if timeManager.timerStatus == .stopped {
                    self.timeManager.setTimer()
                }
                //The remaining time is non-zero and the timer status is.Other than running
                if timeManager.duration != 0 && timeManager.timerStatus != .running {
                    self.timeManager.start()
                //The timer status is.For running
                } else if timeManager.timerStatus == .running {
                    self.timeManager.pause()
                }
            }
    }
}

5. Create a reset button in ButtonsView

Create more reset buttons in the ButtonsView.

Tap this button to trigger the reset method of the TimeManager class created in step 2.

For the button icon, we adopted "stop.circle.fill" from SF Symbols.

Write an if statement in the onTapGesture modifier so that the reset method is triggered when the timer status is other than .stopped.

Regarding the button layout, I want to place the reset button on the left side of the screen and the start / pause button created earlier on the right side of the screen, so place both buttons inside the HStack.

If left at the default, both buttons will be centered on the screen, so put a Spacer between the buttons so that the buttons are on both sides of the screen.

If you move the button too close to the edge of the screen, it will look bad, so I adjusted it with padding.

ButtonsView.swift


struct ButtonsView: View {
    @EnvironmentObject var timeManager: TimeManager
    
    var body: some View {
        //Reset button on the left of the screen in HStack, start on the right/Pause button
        HStack {
            //Reset button
            Image(systemName: "stop.circle.fill")
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 75, height: 75)
                //Leave space on the left side of the button and on the edge of the screen
                .padding(.leading)
                //Transparency 0 when timer status ends.To 1, otherwise opaque
                .opacity(self.timeManager.timerStatus == .stopped ? 0.1 : 1)
                //Action when tapping a button
                .onTapGesture {
                    //The timer status is.Other than stopped
                    if timeManager.timerStatus != .stopped {
                    self.timeManager.reset()
                }
            
            //Space the buttons
            Spacer()
            
            //running:Pause button/pause or stopped:Start button
            Image(systemName: self.timeManager.timerStatus == .running ? "pause.circle.fill" : "play.circle.fill")
                //(Modifier omitted)
        } 
    }
}

Let's see what the ButtonsView looks like on Canvas. Below is the preview code.

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

It looks like the image below. Since the initial value of the timer status is .stopped, the button is grayed out and displayed as unresponsive when tapped. スクリーンショット 2020-10-28 10.32.02.png

6. Place ButtonsView on MainView

PickerView and TimerView were already added to MainView.

Write an if-else statement so that which View is displayed depends on whether the timer status is .stopped.

PickerView and TimerView are the two most important components of this timer app, so they should be centered on the screen (if you don't specify a placement, they will be centered horizontally and vertically by default).

Considering the operation with the finger of an iOS device such as iPhone, I want to place the ButtonsView that I want to add this time below PickerView / TimerView and also close to the bottom of the screen, so I made ZStack a layer separate from PickerView / TimerView and VStack Move the ButtonsView to the bottom by placing a Spacer on top of the ButtonsView. However, it doesn't look good at the edge of the screen, so make fine adjustments with the padding (.bottom) modifier.

MainView.swift


struct MainView: View {
    @EnvironmentObject var timeManager: TimeManager
    
     var body: some View {
        ZStack {
            if timeManager.timerStatus == .stopped {
                PickerView()
            } else {
                TimerView()
            }
            
            VStack {
                Spacer()
                ButtonsView()
                    .padding(.bottom)
            }
        }
        
    }
}

Check the display of MainView on Canvas. This is the preview code.

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

Since the initial value of the timer status is .stopped, the conditional branch of the if statement shows only the PickerView and hides the timerView that overlaps in ZStack. ButtonsView is pushed by Spacer in VStack and is located at the bottom of the screen. スクリーンショット 2020-10-28 10.32.20.png

Next time, we will implement a countdown display of the remaining time.

Recommended Posts

iOS app development: Timer app (3. Start / Stop button, Reset button)
iOS app development: Timer app (summary)
iOS app development: Timer app (4. Countdown implementation)
iOS app development: Timer app (1. Timer time setting)
iOS app development: Timer app (10. Create animation)
iOS app development: Timer app (6. Creation of setting screen)
iOS app development: Timer app (8. Implementation of progress bar)
iOS app development: Timer app (7. Implementation of alarm sound selection)
iOS app development: Timer app (5. Implementation of alarm and vibration)
Complete self-study IOS app development diary
iOS App Development Skill Roadmap (Introduction)
Let's finally use Docker: Start, stop, delete development environment