Text with Text.DateStyle in SwiftUI View in Complication alignment

323 views Asked by At

I want to create a complication rendered by a SwiftUI View that contains a label and a timer value.

I want the label to be on the complication background layer, and the timer value to be on the complication foreground layer so that they get tinted separately.

I would like this line of text, comprised of 2 parts, to be centered.

The trouble is, when using Text.DateStyle.timer, the Text behaves differently within a complication vs in a normal view.

In a normal view the Text frame behaves as any other text, only taking the space it needs.

When displayed in a complication, the Text frame expands to fill all the space it can, and the text within is left aligned.

This makes it so I cannot find a way to center the group of 2 Texts.

I tried a somewhat hacky approach with infinite spacers to try to steal the extra space from the Text that has the expanding frame. This works to center the content, but it causes the Text to truncate.

HStack {
    Text("T:")
        .foregroundColor(.accentColor)

    Text(Date(), style: .timer)
        .complicationForeground()
}

HStack {
    Spacer()
        .frame(maxWidth: .infinity)

    HStack {
        Text("T:")
            .foregroundColor(.accentColor)

        Text(Date(), style: .timer)
            .complicationForeground()
    }

    Spacer()
        .frame(maxWidth: .infinity)
}

A normal preview:

enter image description here

A preview of rendering within complication:

CLKComplicationTemplateGraphicExtraLargeCircularView(
    ExtraLargeStack()
)
.previewContext(faceColor: .multicolor)

enter image description here


Edit to show full code

import ClockKit
import SwiftUI

struct ExtraLargeStack: View {
    var body: some View {
        VStack(alignment: .center) {
            HStack {
                Text("T:")
                    .foregroundColor(.accentColor)

                Text(Date(), style: .timer)
                    .complicationForeground()
            }

            HStack {
                Spacer()
                    .frame(maxWidth: .infinity)

                HStack {
                    Text("T:")
                        .foregroundColor(.accentColor)

                    Text(Date(), style: .timer)
                        .complicationForeground()
                }

                Spacer()
                    .frame(maxWidth: .infinity)
            }
        }
        .font(.system(size: 18, weight: .regular))
        .lineLimit(1)
    }
}

struct ExtraLargeStack_Previews: PreviewProvider {
    static var previews: some View {
        /// Preview normal view
        // ExtraLargeStack()
        
        /// Preview as Complication
        CLKComplicationTemplateGraphicExtraLargeCircularView(
            ExtraLargeStack()
        )
        .previewContext(faceColor: .multicolor)
    }
}

Edit: Another partial solution

Based on suggestions from @Yrb, an overlay provides a partial solution that may be good enough for my use case.

The following does not fully center the 2 part line, but it is pretty close.

HStack {
    // Use placeholder text to create a view with the appropriate size for _most_ timer values that I need to support
    Text("L: 00:00 ").hidden()
}
.overlay(
    // overlay the real content, which is constrained to the frame created by the hidden placeholder.
    HStack(spacing: 5) {
        Text("L:")
            .foregroundColor(.accentColor)

        Text(Date() - 3599, style: .timer)
            .complicationForeground()
    }
)
1

There are 1 answers

3
Yrb On

So, I figured out what the issue with aligning Text(Date(), style: .timer) is. The timer format from hours on down. The document give this as an example: 2:59 36:59:01. It appears that .timer reserves all of the possible space it needs and then is formatted on that possible space, not the space actually used. There does not appear to be any way to change this behavior, even if your goal is a 5 minute countdown timer.

I think you need to consider slight UI change. I did find that you can change the alignment of the displayed Text with a .timer by using .multilineTextAlignment(), but that is about all you can do. The following code demonstrates this:

struct ExtraLargeStack: View {
    var body: some View {
        // I removed (alignment: .center) as it is redundant. VStacks default to center
        VStack {
            // I put the negative spacing to tighten the T: with the timer
            VStack(spacing: -6) {
                Text("T:")
                    .foregroundColor(.accentColor)
                
                Text(Date(), style: .timer)
                    // If you center with .multilineTextAlignment the timer
                    // will be centered
                    .multilineTextAlignment(.center)
                    .complicationForeground()
            }
            
            HStack {
                 HStack {
                    Text(Date(), style: .timer)
                        .multilineTextAlignment(.center)
                        .complicationForeground()
                        .overlay(
                            Text("T:")
                                .foregroundColor(.accentColor)
                                // This offset would need to be computed
                                .offset(x: -30, y: 0)
                        )
                }
            }
        }
        .font(.system(size: 18, weight: .regular))
    }
}

I left the second timer as an HStack, but I put your Text("T") as an .overlay() with a .offset(). I don't particularly like this as it will be fragile if you attempt to adjust the offset for the additional time units, but if you have a limited range, it may work well enough. Also, if you use .monospaced on the timer text, the computation should be a linear amount.