Is there a way to keep a SwiftUI text label from slightly moving while displaying a timer? - swift

I have a simple HStack containing 2 Text structs displaying a timer. It keeps changing position slightly while the timer value changes. Is there a way to avoid this behavior.
struct DurationLabel: View {
let majorDuration: String // the hours/minutes part "00:00"
let minorDuration: String // the seconds part "00"
var body: some View {
HStack(spacing: 0) {
Text(majorDuration + minorDuration)
.font(.system(size: 90))
Text(minorDuration)
.font(.body)
}
}
}

For displays like this I prefer to use a font with monospaced digits. You can use them in your Text like so:
Text("your Text here").font(Font.largeTitle.monospacedDigit())

I've done this a number of times for audio players, etc... If you want to keep the same font, you can do something like:
ZStack(alignment: .leading) {
Text("00:00").opacity(0.0)
Text("MM:SS") // Put your actual numbers in here with the same formatting.
}
The ZStack will size itself based on the largest subview, and 0 is the widest digit in basically any font.
With .center alignment this will keep surrounding views from shifting around each second. With .leading, you keep the label itself from shifting too much.
This technique works for any type of content that may change size. Just load up a "dummy" view with the largest possible version and hide it. Then keep your real content in the visible view. Then you can avoid hardcoding a frame size, etc...

Related

Better way of handling Tap Gesture on Lists

I'm trying to implement a 1 count tap gesture and 2 count tap gesture on rows of a list view.
Currently I'm achieving this by expanding the contents of the rows to fill the entire width and height, by changing the row insets with .listRowInsets(EdgeInsets()) and adding an HStack with a Spacer inside. As seen below:
My ListRow View, which serves to attempt to fill out the the entire rows' width and height. (With a red border for debugging):
struct ListRow<Content: View> : View {
let content: () -> Content
var body: some View {
HStack {
content()
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.border(Color.red)
}
}
And here's a snippet of one of the two places I'm using a list with my ListRow:
List(selection: $selectedItem) {
Section(header: Text("Favourites")) {
ForEach($sidebarItems, id: \.self) { $item in
ListRow {
FileLabel(URL(string: item.path)!, size: 14, text: item.text)
}.gesture(TapGesture().onEnded {
selectedItem = item
fileState.updatePath(path: item.path)
}).listRowInsets(EdgeInsets())
}
}
}.listStyle(SidebarListStyle())
A few issues happen here, which both feels hacky and incorrect to do.
I have to manually select the row when the content is tapped.
I actually manually have to focus the list, if it's not actually focused when clicking a row, otherwise the selection is a very faint color.
The contents aren't actually expanding to the leading and trailing edges of the row, no matter if I give my EdgeInsets negative values (This just results in the contents getting clipped, rather than expanding to the edge).
Clicking the areas outside of the red border (seen below) results in the row being selected, but the tap action not firing.
Preferably I'd love if there was a way to listen for tap gestures on a list and get the row that was tapped. It would fix having to manually reimplement features that the List already has built-in and lowering the risk of introducing bugs + it just feels like the right way to do it, unfortunately I haven't been able to find other ways of implementing this, other than creating an HStack with a Spacer.
EDIT:
I'm curious as to how NavigationLink works as no matter where you click, or if you navigate using your keyboard, it'll actually trigger the navigation. Makes me think there's some kind of binding or event fired?

SwiftUI Animation Bindings

struct ContentView: View {
#State private var animationAmount = 1.0
var body: some View {
VStack
{
Stepper("Scale amount", value: $animationAmount.animation(.linear), in: 1...10)
Spacer()
Button("Tap Me")
{
animationAmount += 1
}
.padding(50)
.background(.red)
.foregroundColor(.white)
.clipShape(Circle())
.scaleEffect(animationAmount)
}
}
}
So I have a tiny question, here I made a Stepper view with value being some way two binding of a variable and then I called the .animation method on that binding which from what I understood, if any changes happen to that binding they simply get animated.
My question is, is it specifically only changes that relate to the binding value that get animated? Or if some other changes happen to this view but coincidentally they happened a bit before the binding changed would those changes get animated too?
And another super super tiny question, why is it exactly that I can't put an if statement in this VStack that will increment animationAmount?
like
if animationAmount > 1.0
{
animationAmount += 0.25
}
Just says that () doesn't conform to View.
By definition, .animation(_:value:):
Applies the given animation to this view when the specified value changes.
So yes, the animation applies only when the value you passed changes, see more: .animation(_:value:)
To answer your second question,VStack, HStack,... & body are ViewBuilders. Meaning they require you to pass in a View, whereas animationAmount += 0.25 is of type Void != View. So what you should do is use .onChange:
.onChange(of: animationAmount) { newValue in
if animationAmount > 1.0 {
animationAmount += 0.25
}
}
First question: .animation works by animating a specific component whenever the value of this component changes.
eg: foregroundColor(color.animation) meaning that if the value of variable color changes, animation will happen BUT only to this foregroundColor component, unless you give another component color.animation too. Any other changes in view/variable beside this variable color will not apply animation to this foregroundColor. Any change even a very small change of variable color will animate the foregroundColor.
There is another different type of animation called withAnimation.
eg: withAnimation { isViewed = true }, now all the views or modifiers that relate to this variable isViewed will be animated.
Second question: To make it easy to understand, you simply can’t make any calculation/logic process inside a SwiftUI View Bracket. An ifelse{} statement inside a VStack or var body expects Views, so you can’t make any calculation there because it is not a view. Any code inside this ifelse bracket {. . .} shall be views only.
However, an ifelse{} statement inside a Button Action or onTapGesture is an opposite story. You can make any calculation/logic process there except displaying views. To understand formally about these concepts you should explore more about the basic of SwiftUI.

SwiftUI question about .padding view modifier from a lecture I saw

So I was watching a lecture and I saw this:
Modifiers Remember that viewmodifier functions(like .padding)
themselves return a View. That view, conceptually anyway, "contains"
the View it's modifying.
Many of them just pass the size offered to them along(like .font or
.foregroundColor). But it is possible for a modifier to be involved in
the layout process itself.
For example the view returned by .padding(10) will offer the view that
it is modifying a space that that is the same size as it was offered,
but reduced by 10 points on each side. The view returned by
.padding(10) would then choose a size for itself which is 10 points
larger on all sides than the View it is modifying ended up choosing.
I don't fully understand this description because if I have some View I call T and I wanna add padding with T.padding(10) What is this saying will happen because this description doesn't fully make sense to me could someone maybe try to explain it a bit better..? Even though this does seem like a simple explanation to me it doesn't really click.
Edit for screenshot:
Think about this as calling .padding(10) on T, creates T1 view having 10pt larger size at each edge, places T at the center of this new T1 view, and returns new T1 view.
Thus every modifier can be considered as a factory of new container view which encloses original view and can affect it on purpose.
In the example you provide, the HStack is a View that is made up of Card Views. The .foregroundColor(Color.orange) modifier takes the HStack View and returns a new View. Finally, the .padding(10) modifier takes that orange View and returns a new View adding padding of size 10 on all sides.
This slide is then explaining how the layout works, which is in the reverse direction. The first View in the chain of Views to be offered space is the last one, that is the View that results from adding padding. For instance, this View might be offered the entire screen if this were the top View in the ContentView. The padding view for layout purposes would then subtract 10 from all sides and offer the smaller space to the .foregroundColor() modifier. Since .foregroundColor() doesn't affect layout, it would offer this same size to the HStack View. The HStack would divide the offered space by the number of cards in the ForEach and offer this smaller view size to each of the Card Views.
An Example With Numbers
It might help your understanding to look at an example with real numbers.
Suppose this total view will fit into a space 200 wide by 240 tall, and there are 2 cards in the HStack. How big will each Card be? This is the question Layout is figuring out.
If you have 200 by 240 to work with, then the padding is going to take up space on all sides leaving a space of only 180 by 220 for the HStack to lay out its views. The HStack determines that it has 2 views laid out horizontally, so it offers half the space to each of the card views (90 by 220).
So, the padding is actually going to make the Card Views smaller because it is taking up some of the existing space.
Example to Play With in an iOS App
Try the following code in an iOS app. Run it as is and see what it looks like. Then change the padding to 100 and run it again. Notice that as the padding gets larger, the red and blue views get smaller.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack {
Color.red
Color.blue
}
.padding(10)
}
}
More Extensive Example with Slider to Adjust Padding
Here is a interactive example that shows the padding in Color.green that can be adjusted with the slider. The dimensions of the red and blue views are shown using GeometryReader.
struct ContentView: View {
#State private var pad: CGFloat = 50
var body: some View {
VStack {
HStack {
GeometryReader { geo in
ZStack {
Color.red
Text("\(geo.size.width, specifier: "%.1f")x\(geo.size.height, specifier: "%.1f")").foregroundColor(.white)
}
}
GeometryReader { geo in
ZStack {
Color.blue
Text("\(geo.size.width, specifier: "%.1f")x\(geo.size.height, specifier: "%.1f")").foregroundColor(.white)
}
}
}
.background(Color.yellow)
.padding(pad)
}
.background(Color.green)
HStack {
Text("Padding =")
HStack {
Text("\(pad, specifier: "%.1f")")
Spacer()
}.frame(width: 50)
}.foregroundColor(.green)
Slider(value: $pad, in: 0...100).padding(.horizontal)
}
}

SwiftUI on Mac: Help Text Always Visible Even within View with Zero Opacity

I have run into some unexpected behavior while using SwiftUI in a macOS app. I filed a Feedback with Apple in case it's a bug, but it might actually be designed to work this way, so I'm looking for a workaround.
I rely heavily on the use of .opacity() to show and hide different sections of my app with tabs. I don't use if clauses because each time the user changes the tab, you have to wait for the entire view to rebuild and that is pretty slow.
Here's a basic example that demonstrates the problem:
struct ContentView: View {
#State var viewAVisible = false
var body: some View {
VStack{
ZStack{
Text("View A Visible")
.frame(width: 500, height: 500)
.background(Color.blue)
.help("This is View A's help text. It should be invisible when View A is invisible.")
.opacity(viewAVisible ? 1 : 0)
Text("View B Visible")
.frame(width: 500, height: 500)
.background(Color.gray)
.opacity(viewAVisible ? 0 : 1)
}
Button("Toggle"){
viewAVisible.toggle()
}
}.padding()
}
}
The default app state is to hide the "View A" Text() and only show the "View B" Text(). But if you hover over View B, you still see View A's .help text:
In my opinion, if a view has .opacity(0) then its help text shouldn't show up. But regardless, I need to find a way around this.
I thought about doing something like this:
.help(viewAVisible ? "This is View A's help text..." : "")
...but that doesn't scale across dozens of views in my app--particularly among child views that don't know if their parent view is shown or hidden. As I mouse across my app, I see the help text of tons of views all over the place even though they are invisible. 😅
Has anyone run into this or have any suggestions on how to handle it?
Looks like a bug (they do not remove tracking rects), here is a demo of workaround - move help tag into background and remove it manually (tested with macOS 12.0.1)
Text("View A Visible")
.frame(width: 500, height: 500)
.background(Group {
if viewAVisible {
Color.blue.help("This is View A's help text. It should be invisible when View A is invisible.")
} else {
Color.clear
}
})
.opacity(viewAVisible ? 1 : 0)

SwiftUI Wrapping HStack with Images

I am trying to make a HStack which contains x number of images and will display them in a wrapping HStack manner (i.e if only 4 of the 7 images fit on the line, then spill the 3 remaining over onto the next line).
I am trying to use the library WrappingHStack (https://github.com/dkk/WrappingHStack) but the result isn't as expected, the images are not wrapping around.
Here is my code:
#State var numEarths : Int = 7;
var body: some View {
VStack{
Text("If everyone were to live like you, we would need \(numEarths) Earths").font(.title)
WrappingHStack{
ForEach(0 ..< numEarths){_ in
Image("logo")
}
}
}
}
Here is the result on the simulator (see, no wrapping)...we should see 7 Earth logo images but you can only see 4 (and a tiny bit of number 5).
screenshot of image
Please can someone help me fix this issue? I am new to SwiftUI dev so please go lightly on me if this is a silly mistake. Thanks.
According to the WrappingHStack library, if you want to achieve what you above-mentioned, you need to do two things:
Give your Image a frame so that the WrappingHStack can know how many elements to put in per line.
Change your ForEach loop to WrappingHStack, because WrappingHStack can be used as a ForEach to loop over items.
WrappingHStack(0..<numEarths, id:\.self) { _ in
Image("logo")
.resizable()
.frame(width: 100, height: 100)
}