WidgetKit: .timer Text style expands it to fill the width, instead of taking space to fit the contained string
Asked Answered
A

4

26

.timer Text style expands it to fill the width, instead of taking space to fit the contained string. Is there a way to change this behavior?

Text(entry.timeFinished, style: .timer).multilineTextAlignment(.leading).opacity(0.5).background(Color.red)

enter image description here

Text("3:41").opacity(0.5).background(Color.blue)

enter image description here

Entire View:

struct TimePieceWidgetEntryView: View {
    
    var entry: Provider.Entry
    
    var body: some View {
        VStack {
            Text("Timer ⏱")
            Text(Date().addingTimeInterval(600), style: .timer).opacity(0.5)
        }.font(.title.bold().monospacedDigit())
        .padding(5)
        .widgetURL(entry.url)
    }
    
}
Agree answered 15/2, 2021 at 15:15 Comment(10)
Having the same problem :(Photozincography
Add your body code. By what you provided now, it works correctly if copied and pasted in the body as a standalone component.Gastro
@TomasJablonskis edited and added it to the original post.Agree
It works correctly with both of your examples and with your provided body... Create new project and paste your body inside of clean ContentView, you will see.Gastro
Provide body of the view which is using TimePieceWidgetEntryView Gastro
@TomasJablonskis it does indeed behave as expected inside of an app, but not in a widget.Agree
try to create formatted instead of style: https://mcmap.net/q/537576/-swiftui-change-output-format-of-text-using-as-timerEnvironmentalist
Still no solution on this issue?Fernandes
Having same issue in live activity in Dynamic Island just taking all the space...Hatchett
This is still an issue for me when using Dynamic IslandsAmphibrach
A
4

I have zero insight into how Apple is building Live Activities and widgets. My guess is that the widget is rendered and then cached. That is why things like standard timers/publishers don't update the UI. My guess is that these Text timers fill the space so that Apple can render a label on top of the cached view without requiring a new layout from the widget when the text size changes. I am saying this because I think the filling of the container is a feature and not a bug, so it probably won't change any time soon.

To get around this I force the size of the Text view to be the largest it should be. There are still some issues, however for the most part this solution looks ok.

struct TextTimer: View {
    // Return the largest width string for a time interval
    private static func maxStringFor(_ time: TimeInterval) -> String {
        if time < 600 { // 9:99
            return "0:00"
        }
        
        if time < 3600 { // 59:59
            return "00:00"
        }
        
        if time < 36000 { // 9:59:59
            return "0:00:00"
        }
        
        return "00:00:00"// 99:59:59
    }
    init(_ date: Date, font: UIFont, width: CGFloat? = nil) {
        self.date = date
        self.font = font
        if let width {
            self.width = width
        } else {
            let fontAttributes = [NSAttributedString.Key.font: font]
            let time = date.timeIntervalSinceNow
            let maxString = Self.maxStringFor(time)
            self.width = (maxString as NSString).size(withAttributes: fontAttributes).width
        }
    }
    
    let date: Date
    let font: UIFont
    let width: CGFloat
    var body: some View {
        Text(timerInterval: Date.now...date)
            .font(Font(font))
            .frame(width: width > 0 ? width : nil)
            .minimumScaleFactor(0.5)
            .lineLimit(1)
    }
}
Amphibrach answered 8/8, 2023 at 17:9 Comment(0)
G
1

This is my "solution" to this issue. Not perfect but maybe it helps someone

Text(timerInterval: dateRange, countsDown: false)
  .monospacedDigit()
  .frame(width: 40)
Gavrah answered 13/9, 2023 at 14:50 Comment(0)
N
0

Apple still didn't fix it.

I have remade @datinc answer and made it more clearer and flexible to use for SwiftUI.

It will show only mm:ss format but you can easy to change the format you want.

import SwiftUI

struct TextTimer: View {

    let dateRange: ClosedRange<Date>
    let font: UIFont
    let width: CGFloat

    init(_ dateRange: ClosedRange<Date>, font: UIFont, width: CGFloat? = nil) {
        self.dateRange = dateRange
        self.font = font
        self.width = width ?? TextTimer.defaultWidth(font: font, dateRange: dateRange)
    }

    var body: some View {
        Text(timerInterval: dateRange, showsHours: false)
            .font(Font(font))
            .frame(width: width > 0 ? width : nil)
            .minimumScaleFactor(0.5)
            .lineLimit(1)
    }

    private static func defaultWidth(font: UIFont, dateRange: ClosedRange<Date>) -> CGFloat {
        let maxString = maxStringFor(dateRange: dateRange)
        let fontAttributes = [NSAttributedString.Key.font: font]
        return (maxString as NSString).size(withAttributes: fontAttributes).width
    }

    private static func maxStringFor(dateRange: ClosedRange<Date>) -> String {
        let duration = dateRange.upperBound.timeIntervalSince(dateRange.lowerBound)
        let minutes = Int(duration) / 60
        let seconds = Int(duration) % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }

}
Nealon answered 11/2 at 15:38 Comment(0)
S
0

You can do the following trick below. As a general tip, if you ever want a view to affect layout differently, either due to a bug or design, have the view you want to affect layout hidden and overlay your desired view. I would also recommend playing around with the font/monospacing/alignment to get what you would like.

The good thing about this is that when Apple decides to fix it, it won't really cause any design bugs.

Text("00:00")
    .hidden()
    .overlay(alignment: .leading) {
        Text(date, style: .timer)
    }

This is also an issue for the dynamic island, and here are examples from that workaround:

enter image description here enter image description here

Steeve answered 19/4 at 20:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.