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.
You can see the sample code from the URL of the Git repository below. https://github.com/msnsk/Qiita_Timer.git
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)
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
}
}
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 {
}
}
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()
}
}
}
}
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.
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.
Next time, we will implement a countdown display of the remaining time.
Recommended Posts