Reputation: 1282
Here's an interesting quandary: I want to make a timer that "ticks" reliably but, also, renders symbols in predictable places so that I could, for instance, decorate the timer by adding a background. Because of WidgetKit limitations, I cannot reliably render my own text every second and have to rely on special views, such as Text(Date(), style: .timer). However, this view can render time as, both, XX:XX and X:XX depending on how much time is left, which would be OK, except, it also, both, takes the whole width of the container and aligns to the left, which makes the last :XX move depending on time left.
Here's an illustration:
And code that produced it:
struct MyWidgetEntryView : View {
var body: some View {
VStack {
Text(Date().addingTimeInterval(1000), style: .timer)
.font(.body.monospacedDigit())
.background(Color.red)
Text(Date().addingTimeInterval(100), style: .timer)
.background(Color.green)
.font(.body.monospacedDigit())
}
}
}
Question: is there a way to make a reliably updating time display in a WidgetKit widget in such a way that symbols for minutes and seconds are always rendered in the same places and not move depending on time left?
I can't figure it out, please help me!
–Baglan
Upvotes: 12
Views: 1673
Reputation: 564
I would just add to the amazing Adam's answer:
.monospacedDigit()
This way we avoid all the numbers moving every second that the last digit is a different size.
So it would be something like:
Text(Date(), style: .timer)
.monospacedDigit()
.multilineTextAlignment(.trailing)
Upvotes: 2
Reputation: 5135
Set the multi-line text alignment:
Text(Date(), style: .timer)
.multilineTextAlignment(.trailing)
It’s not multiple lines of text but it works!
Upvotes: 18
Reputation: 1282
Still looking for a better answer, but here's a "proof of concept" hack to achieve my goal:
struct _T1WidgetEntryView : View {
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
@State private var digitSize: CGSize = .zero
@State private var semicolonSize: CGSize = .zero
var body: some View {
ZStack {
Text("0")
.overlay(
GeometryReader { proxy in
Color.green
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
.onPreferenceChange(SizePreferenceKey.self) { digitSize = $0 }
)
.hidden()
Text(":")
.overlay(
GeometryReader { proxy in
Color.green
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
.onPreferenceChange(SizePreferenceKey.self) { semicolonSize = $0 }
)
.hidden()
Color.clear
.frame(width: digitSize.width * 4 + semicolonSize.width, height: digitSize.width * 4 + semicolonSize.width)
.overlay(
Text(Date().addingTimeInterval(100 + 3600 * 200), style: .timer)
.frame(width: digitSize.width * 7 + semicolonSize.width * 2)
,
alignment: .trailing
)
.clipped()
}
.font(.body.monospacedDigit())
}
}
And the result is:
This code assumes that all the digits are the same width (hence the .monospacedDigit() font modifier).
Here's what it does:
Again, if there is a better solution, I'd love to learn about it!
–Baglan
Upvotes: 3