In part 1 of building a widget extension for iOS, we got a very basic Widget built that has some custom UI that shows the beginnings of a pomodoro timer. You can download part 1 below, although if you are new to this tutorial I would recommend starting at Part 1 to follow along each step so you are more familiar with how we got here.
In this part of the tutorial, we’ll look at cleaning up the widget ready for making it work in part 3.
Improving the Widget View
In the last turorial we left with a simple UI for the widget. To begin with today we’ll improve on that a little, and then work on making the widget functional.
In PomodoroWidgetView.swift create a new struct as follows:
struct ClockHand: View {
let angle: Angle
let length: CGFloat
let width: CGFloat
var body: some View {
Rectangle()
.fill(Color.black)
.frame(width: width, height: length)
.offset(y: -length / 2)
.rotationEffect(angle)
.animation(.easeInOut(duration: 0.3), value: angle)
}
}
The widget will have four clocks in a grid allowing you to tap one of them to start a timer of the specified duration. This small view creates the hand of the clock face and will be used elsewhere in code.
struct PomodoroClockButton: View {
let minutes: Int?
let timeRemaining: Int?
var body: some View {
ZStack {
Circle()
.stroke(Color.gray.opacity(0.5), lineWidth: 2)
.frame(width: 60, height: 60)
ClockHand(angle: angleForTime(), length: 18, width: 3)
Text(displayText())
.font(.caption)
.foregroundColor(.black)
.offset(y: 35)
}
.widgetURL(URL(string: "pomodoro://start?minutes=\(minutes ?? 25)"))
}
func angleForTime() -> Angle {
let totalMinutes = minutes ?? (timeRemaining != nil ? timeRemaining! / 60 : 25)
let elapsedMinutes = timeRemaining != nil ? totalMinutes - (timeRemaining! / 60) : minutes!
let degreesPerMinute = 360.0 / 60.0
return Angle(degrees: Double(elapsedMinutes) * degreesPerMinute)
}
func displayText() -> String {
if let timeRemaining = timeRemaining {
return "\(timeRemaining / 60)m"
}
return "\(minutes ?? 25)m"
}
}
This view accepts minutes as a parameter. This allows us to use this view and pass in the duration of a timer, and then create as many timers as needed.
This view creates the clock face, calls in the hand of the clock, and provides a lable.
Line 18 creates a deep link that allows the widget to communicate with the main app. When a clock is tapped, we can pass in the minutes so that the deep link is used with the correct duration. We’ll see more of this later on.
The angleForTime method calculates the angle that the hand will need to be on the clock face. You might consider refactoring this and putting the logic of this method into the ClockHand view, and then just passing that view the minutes and have it return the Angle.
We also create a method called displayText that returns the time remaining as a string. This is used for running timers.
For the next part of the view, we want to put four clocks in a grid of two x two.
struct ClockGridView: View {
let columns = [
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
]
var body: some View {
LazyVGrid(columns: columns, spacing: 16) {
ForEach([10, 15, 20, 25], id: \.self) { minutes in
PomodoroClockButton(minutes: minutes, timeRemaining: nil)
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
}
This struct contains columns which contain two GridItem’s. In our view, we create a LazyVGrid, passing in the columns, and then put four clocks with a different duration for each.
This particular view is used for when a timer is not running. It puts four clocks on the widget with intervals of 10, 15, 20, 25 minutes. Pomodoro is based on 25 minute stretches, but many people find them useful in shorter durations, even if just to get them started on something.
struct RunningPomodoroView: View {
let timeRemaining: Int
var body: some View {
VStack {
Text("\(timeRemaining / 60):\(String(format: "%02d", timeRemaining % 60)) left")
.font(.headline)
.multilineTextAlignment(.center)
PomodoroClockButton(minutes: nil, timeRemaining: timeRemaining)
}
.padding()
}
}
This code creates a view for when a timer is running. Widgets can only update in 1 minute intervals, so the minute hand would change once a minute rather than slowly sweeping (we can save that part of the live activity).
The code isn’t particularly complex here. We have an integer that stores the time remaining. We have a VStack with a timer, followed by the reusable PomodoroClockButton view which passed in nil for the minutes, but a time remaining for timeRemaining. This will be the widget view if there is an active timer.
The widget UI is now complete. Although far more polish can be added, it’s functional for our needs.
In part 3 we’ll look at making this work. When tapping the widget the app will open, the timer will start, and when going back to the widget, you’ll see a clock counting down.
Leave a Reply
You must be logged in to post a comment.