Layout text vertically so the text orientation is upright - swiftui

I would like to have text placed vertically on top of an image, so the text is displayed upright. I have written code to do that, but it feels like a hack. I am hoping someone has a better way to do this. My code:
import SwiftUI
struct VerticalText: View {
let input = "12.4"
let myFont = Font.system(size: 22.0).bold()
var body: some View {
ZStack {
Rectangle().fill(Color.yellow).frame(width: 100, height: 100, alignment: .center)
getText()
}
}
func getText() -> some View {
let chars = input.map { String($0) }
return VStack {
ForEach(chars, id: \.self) { char in
if char == "." {
Text(char)
.font(myFont)
.frame(width: .infinity, height: 2, alignment: .center)
.offset(x: 0.0, y: -6.0)
}else {
Text(char)
.font(myFont)
}
}
}
}
}
struct VerticalText_Previews: PreviewProvider {
static var previews: some View {
VerticalText()
}
}
Some notes:
I've replaced the image with a rectangle and simplified the input to a constant for the purposes of this question.
The input will be a number between 0 and 100. There may optionally be up to two decimal places.
The if block for the "." is there to tighten up the spacing when the input is a decimal number. Without it the result looks like:
In the above image, there is a rather large gap between the 2 and the decimal. I want to shrink that space. To do that I set the frame size on the decimal. This results in:
However, the above code feels like a hack. The offset is hard coded and the if statement feels wrong. Is there a better way to layout text vertically?

Related

Dynamically set frame dimensions in a view extension

I've been playing around with giving views a gradient shadow (taken from here and here) and while these achieve most of what I need, they seem to have a flaw: the extension requires you to set a .frame height, otherwise the gradient looks really desaturated (as it's taking up the entire height of the device screen). It's a little hard to describe, so here's the code:
struct RainbowShadowCard: View {
#State private var cardGeometryHeight: CGFloat = 0.0
#State private var cardGeometryWidth: CGFloat = 0.0
var body: some View {
VStack {
Text("This is a card, it's pretty nice. It has a couple of lines of text inside it. Here are some more lines to see how it scales.")
.font(.system(.body, design: .rounded).weight(.medium))
}
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background {
GeometryReader { geo in
Color.black
.onAppear {
cardGeometryHeight = geo.size.height
cardGeometryWidth = geo.size.width
print("H: \(cardGeometryHeight), W: \(cardGeometryWidth)")
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.padding()
.multicolorGlow(cardHeight: cardGeometryHeight, cardWidth: cardGeometryWidth)
}
}
extension View {
func multicolorGlow(cardHeight: CGFloat, cardWidth: CGFloat) -> some View {
ZStack {
ForEach(0..<2) { i in
Rectangle()
.fill(
LinearGradient(colors: [
.red,
.green
], startPoint: .topLeading, endPoint: .bottomTrailing)
)
// The height of the frame dictates the saturation of
// the linear gradient. Without it, the gradient takes
// up the full width and height of the screen, resulting in
// a washed out / desaturated gradient around the card.
.frame(height: 300)
// My attempt at making the height and width of this view
// be based off the parent view
/*
.frame(width: cardWidth, height: cardHeight)
*/
.mask(self.blur(radius: 10))
.overlay(self.blur(radius: 0))
}
}
}
}
struct RainbowShadowCard_Previews: PreviewProvider {
static var previews: some View {
RainbowShadowCard()
}
}
I've managed to successfully store the VStack height and width in cardGeometryHeight and cardGeometryWidth states respectfully, but I can't figure out how to correctly pass that into the extension.
In the extension, if I uncomment:
.frame(width: cardWidth, height: cardHeight)
The VStack goes to a square of 32x32.
Edit
For the sake of clarity, the above solution "works" if you don't use a frame height value for the extension, but it doesn't work very nicely. Compare the saturation of the shadow in this image to the original, and you'll see a big difference between a non framed approach and a framed approach. The reason for this muddier gradient is the extension is using the screen bounds for the linear gradient, so our shadow gradient isn't getting the benefit of the "start" and "end" saturation of the red and green, but the middle blending of the two.

In SwiftUI, how to fill Path with text color on top of a material?

I'm drawing icons on a toolbar with a material background. The Text and symbol Images are white, but if I draw my own Path, it's gray.
struct ContentView: View {
var body: some View {
HStack {
Text("Hi")
Image(systemName: "square.and.arrow.up.fill")
Path { p in
p.addRect(CGRect(origin: .zero, size: .init(width: 20, height: 30)))
}.fill()
.frame(width: 20, height: 30)
}
.padding()
.background(.regularMaterial)
}
}
I get the same result with .fill(), .fill(.foreground), or .fill(.primary).
Why is it gray? How do I get it to match the white text color?
I find it weird that .white or .black work, but .primary doesn't.
Upon discovering the Material documentation, I found this interesting snippet:
When you add a material, foreground elements exhibit vibrancy, a context-specific blend of the foreground and background colors that improves contrast. However using foregroundStyle(_:) to set a custom foreground style — excluding the hierarchical styles, like secondary — disables vibrancy.
Seems like you have to force a different color (see previous edit which I used the environment color scheme), since hierarchical styles such as .primary won't work by design.
Luckily there is a way around this - you can use colorMultiply to fix this problem. If you set the rectangle to be .white, then the color multiply will make it the .primary color.
Example:
struct ContentView: View {
var body: some View {
HStack {
Text("Hi")
Image(systemName: "square.and.arrow.up.fill")
Path { p in
p.addRect(CGRect(origin: .zero, size: .init(width: 20, height: 30)))
}
.foregroundColor(.white)
.colorMultiply(.primary)
.frame(width: 20, height: 30)
}
.padding()
.background(.regularMaterial)
}
}
There is no issue with code, your usage or expecting is not correct! Text and Image in that code has default Color.primary with zero code! So this is you, that messing with .fill() you can delete that one!
struct ContentView: View {
var body: some View {
HStack {
Text("Hi")
Image(systemName: "square.and.arrow.up.fill")
Path { p in
p.addRect(CGRect(origin: .zero, size: .init(width: 20, height: 30)))
}
.fill(Color.primary) // You can delete this line of code as well! No issue!
.frame(width: 20, height: 30)
}
.padding()
.background(Color.secondary.cornerRadius(5))
}
}

Right-aligning Text(Date(), style: .timer) text in an iOS WidgetKit widget

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
Set the multi-line text alignment:
Text(Date(), style: .timer)
.multilineTextAlignment(.trailing)
It’s not multiple lines of text but it works!
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:
Calculates the sizes of a digit symbol and the semicolon;
"Normalizes" the time string by adding 200 hours to ensure the XXX:XX:XX formatting;
Sets the size of the text to accommodate strings formatted as XXX:XX:XX;
Sets the size of the container to accommodate strings formatted as XX:XX;
Aligns the text .trailing in an overlay;
Clips the whole thing to the size of the container.
Again, if there is a better solution, I'd love to learn about it!
–Baglan

Is there a way in SwiftUI to fulfill scrolling animations between 2 numbers

I'm looking for a similar way https://github.com/stokatyan/ScrollCounter in SwiftUI
This is a very rudimentary build of the same thing. I'm not sure if you're wanting to do it on a per-digit basis, however this should give you a solid foundation to work off of. The way that I'm handling it is by using a geometry reader. You should be able to easily implement this view by utilizing an HStack for extra digits/decimals. The next thing I would do would be to create an extension that handles returning the views based on the string representation of your numeric value. Then that string is passed as an array and views created for each index in the array, returning a digit flipping view. You'd then have properties that are having their state observed, and change as needed. You can also attach an .opacity(...) modifier to give it that faded in/out look, then multiply the opacity * n where n is the animation duration.
In this example you can simply tie your Digit value to the previewedNumber and it should take over from there.
struct TestView: View {
#State var previewedNumber = 0;
var body: some View {
ZStack(alignment:.bottomTrailing) {
GeometryReader { reader in
VStack {
ForEach((0...9).reversed(), id: \.self) { i in
Text("\(i)")
.font(.system(size: 100))
.fontWeight(.bold)
.frame(width: reader.size.width, height: reader.size.height)
.foregroundColor(Color.white)
.offset(y: reader.size.height * CGFloat(previewedNumber))
.animation(.linear(duration: 0.2))
}
}.frame(width: reader.size.width, height: reader.size.height, alignment: .bottom)
}
.background(Color.black)
Button(action: {
withAnimation {
previewedNumber += 1
if (previewedNumber > 9) {
previewedNumber = 0
}
}
}, label: {
Text("Go To Next")
}).padding()
}
}
}

SwiftUI Frame width issue [duplicate]

I would like to underline a title with a rectangle that should have the same width as the Text.
First I create an underlined text as below:
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}
}
}
So I get the following result:
Now I want to get this result:
So I would like to know if it's possible to bind Text width and apply it to Rectangle by writing something like :
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(width: Text.width, height: (5.0))
}
}
}
By doing so, I could change text and it will be dynamically underlined with correct width.
I tried many options but I can't find how to do it. I also checked this question but it's seems to not be the same issue.
Just specify that container has fixed size and it will tight to content, like
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}.fixedSize() // << here !!
}