The points for creating a timer app are posted in multiple articles. In this article, I will write about creating a circular progress bar that shows the remaining time 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 new file with the name ProgressBar.swift. This View also refers to the property value from the TimeManager class, so create an instance of the TimeManager class with the @EnvironmentObject property wrapper before var.
ProgressBar.swift
import SwiftUI
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
Text("Hello, World!")
}
}
The progress bar requires two circles, one that shortens over time and the other that is the background. The background circle is the same size and does not change in length over time. First of all, we will create from the background circle.
Place the circle inside the body {}. SwiftUI provides a graphic component called Circle (), so we will use it.
ProgressBar.swift
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
Circle()
}
}
A figure is composed of contour lines and faces, so to make a hollow circle, display only the contour lines without displaying the faces, and adjust the thickness, length, and color of the contour lines.
Add a Circle () modifier to get the shape you want.
In the .stroke modifier, put Color () in the argument and make the argument .darkGray to make it a gray color that seems to be the background.
In the .stroke modifier, set the style argument lineWidth to 20 to specify the thickness of the progress bar.
Use the .scaledToFit modifier to fit the size of the circle to fill the screen size, and the .padding modifier to adjust the margins with the screen edges.
ProgressBar.swift
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
Circle()
.stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
.scaledToFit()
.padding(25)
}
}
The progress bar circle that you will create will overlap the background circle created in step 2 in a layered manner, so if you add another Circle () component for the progress bar, enclose the two circles with ZStack {}. I will.
ProgressBar.swift
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
ZStack {
//Circle for background
Circle()
.stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
.scaledToFit()
.padding(15)
//Circle for progress bar
Circle()
}
}
}
Like the background circle, the progress bar circle will be shaped by adding modifiers.
In the .stroke modifier, I put Color () in the argument and specified .cyan as the color of the progress bar for the time being.
In the .stroke modifier, put a finer specification for StrokeStyle in the style argument. Specify a width of 20 with lineWidth, specify .round with lineCap to round the corners of the line ends, and specify .round with lineJoin to exceed the end of the line by half the length of the line width. Make it round.
Add a .rotationEffect modifier and enter Angle (degrees: -90) as an argument. This allows you to change the starting position of the default circle outline from the 3 o'clock direction to the 12 o'clock direction.
Others are the same as the background circle.
ProgressBar.swift
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
ZStack {
//Circle for background
Circle()
.stroke(Color(.darkGray), style: StrokeStyle(lineWidth: 20))
.scaledToFit()
.padding(15)
//Circle for progress bar
Circle()
.stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .bevel))
.scaledToFit()
//Set the start position of the contour line to the 12 o'clock direction
.rotationEffect(Angle(degrees: -90))
.padding(15)
}
}
}
Add more modifiers to the progress bar circle so that the progress bar gets shorter as the countdown timer elapses.
Add a .trim modifier. This allows you to trim the progress bar to the required length.
The starting position is always fixed at 12 o'clock, so put 0 in the from argument. As the progress bar needs to get shorter over time, the end position should always be tied to the remaining time. Also, the value for both the from and to arguments must be between 0 and 1.
Here is a little math. The TimeManager class has a property maxValue that stores the maximum time set in Picker and a property duration that stores the remaining time. Using these two values, the formula that has a maximum value of 1 at the start of the countdown and a minimum value of 0 at the end of the countdown is duration / maxValue.
Since the data type of the value of the argument to must be CGFloat, the value to be finally put in the argument to is as follows.
CGFloat(self.timeManager.duration / self.timeManager.maxValue)
So the code looks like this:
ProgressBar.swift
struct ProgressBarView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
ZStack {
//Circle for background
Circle()
//(Modifier omitted)
//Circle for progress bar
Circle()
.trim(from: 0, to: CGFloat(self.timeManager.duration / self.timeManager.maxValue))
.stroke(Color(.cyan), style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
.scaledToFit()
//Set the start position of the contour line to the 12 o'clock direction
.rotationEffect(Angle(degrees: -90))
.padding(15)
}
}
}
Check the ProgressBarView in Canvas. Below is the preview code.
struct ProgressBarView_Previews: PreviewProvider {
static var previews: some View {
ProgressBarView()
.environmentObject(TimeManager())
.previewLayout(.sizeThatFits)
}
}
It should look like the image below.
Add an instance of ProgressBarView to the top of the outermost ZStack {} inside the body {} of the MainView. The top of the ZStack code is the back of the UI component layer hierarchy. As an image, the display of the remaining time and the Picker of the time setting are in the foreground on the screen.
In addition, since a toggle switch for showing / hiding the progress bar is prepared in the SettingView item of the setting screen first, the progress bar is displayed only when the isProgressBaron property of the TimeManager class linked with that setting is true. Describe it with an if statement so that it will be displayed.
MainView.swift
struct MainView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
ZStack {
if timeManager.isProgressBarOn {
ProgressBarView()
}
if timeManager.timerStatus == .stopped {
PickerView()
} else {
TimerView()
}
VStack {
Spacer()
ZStack {
ButtonsView()
.padding(.bottom)
SettingButtonView()
.padding(.bottom)
.sheet(isPresented: $timeManager.isSetting) {
SettingView()
.environmentObject(self.timeManager)
}
}
}
}
.onReceive(timeManager.timer) { _ in
//(Omission of description in onReceive)
}
}
}
Check the MainView in Canvas. It should look like the image below.
I was able to implement the progress bar, but when I checked the actual movement of MainView with Xcode Canvas and Simulator, the shorter the timer setting time, the shorter the progress bar became every second (it was jerky). You can see it. It's not a failure as a progress bar, but the visually smoother one gives the impression of sophistication, so I'll fix it a little.
There are two causes for this jerky movement, so we will correct each one.
The first is the timer property of the TimeManager class. This property contains the publish method of the Timer class, but the value of its argument every is 1. This means that it will be activated every second. Change this value to about 0.05. As a result of verification, there will be an error between the actual passage of time and the update of the remaining time of the timer application from around 0.01, so I think that about 0.05 is the limit.
Update in the TimeManager class as follows:
TimeManager.swift
class TimeManager: ObservableObject {
//(Other properties omitted)
//The publish method of the Timer class that fires every second
var timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect()
//(Method omitted)
The second is the description in the OnReceive modifier of MainView. This modifier is triggering the timer property of the TimeManager class that we modified earlier to execute the code inside the closure {}. Since we updated the trigger for 0.05 seconds, the update of the duration property (remaining time) of the TimeManager class described in the closure {} of the onReceive modifier must also be deducted by 0.05 seconds.
Update in MainView as follows.
MainView.swift
struct MainView: View {
@EnvironmentObject var timeManager: TimeManager
var body: some View {
ZStack {
//(abridgement)
}
//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 //Update here!
//When the remaining time is 0 or less
} else {
//Timer status.Change to stopped
self.timeManager.timerStatus = .stopped
//Sound an alarm
AudioServicesPlayAlertSoundWithCompletion(self.timeManager.soundID, nil)
//Activate vibration
AudioServicesPlayAlertSoundWithCompletion(SystemSoundID(kSystemSoundID_Vibrate)) {}
}
}
}
}
Now, for example, even if you set the timer to 5 seconds, the progress bar will move relatively smoothly. Next time, I will display the color of the progress bar more beautifully, which is a little extra element.
Recommended Posts