SwiftUI Animation Bindings - swift

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.

Related

Is there a way to pass functions into views as optional parameters?

I am making an application in SwiftUI that involves answering yes or no questions. Because of this I have created a subview call YesOrNoView. This subview has two buttons, one labeled yes and the other labeled no. The view accepts a binding variable of question that returns true or false based on the users answer. It also accepts a binding variable of questionsAnswered and increments it whenever either button is pressed. This behavior works for most questions in the game, but for the final question I want it to execute custom logic to take the user to a different view. To do this I am trying to make the view accept a custom action/method that I can call from either buttons action logic. Ideally, the action would also be optional so that I don't have to pass it in the 99% percent of the time when I'm not using it.
How do I pass a function as an optional parameter into a view and then run that action when a button within said view is pressed?
I tried adding actions using
struct YesOrNoView<Content: Action>: View {
...
but it couldn't find action or Action within it's scope.
I also tried using
struct YesOrNoView<Content>: View {
But I got an error saying that the Content variables type could not be inferred.
Any help would be greatly appreciated.
It return clickAction back to the view
struct GenericButton<Title: StringProtocol>: View {
let title: Title
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
}.frame(width: 200, height: 40)
}
}
}
Usage:
GenericButton("Button") {
// Button tapped action
}
I managed to figure it out with help from #JoakimDanielson. The trick is to pass in an empty function as the default.
var myFunction: () -> Void = {}
Thanks for the help everyone.

Exclude button from disabling

I have custom view with different content inside including Button.
For example:
VStack {
Text("Some text")
Button() // 1-th
Button() // 2-th
.disabled(false) // This doesn't help, despite it logical
}
.disabled(true) // Line with disabling
I want 2-th button to be always enabled no matter what is set in "Line with disabling".
According to the documentation this does not work that way. It works the other way around. As you allready discovered the .disable on the parent overrides the value set in the child.
Possible solution would be to not use the parent .disable instead use it on each child element.

How does this SwiftUI binding + state example manage to work without re-invoking body?

A coworker came up with the following SwiftUI example which looks like it works just as expected (you can enter some text and it gets mirrored below), but how it works is surprising to me!
import SwiftUI
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
TextField("Change the string", text: $text)
WrappedText(text: $text)
}
}
}
struct WrappedText: View {
#Binding var text: String
var body: some View {
Text(text)
}
}
My newbie mental model of SwiftUI led me to think that typing in the TextField would change the $text binding, which would in turn mutate the text #State var. This would then invalidate the ContentView, triggering a fresh invocation of body. But interestingly, that's not what happens! Setting a breakpoint in ContentView's body only gets hit once, while WrappedText's body gets run every time the binding changes. And yet, as far as I can tell, the text state really is changing.
So, what's going on here? Why doesn't SwiftUI re-invoke ContentView's body on every change to text?
On State change SwiftUI rendering engine at first checks for equality of views inside body and, if some of them not equal, calls body to rebuild, but only those non-equal views. In your case no one view depends (as value) on text value (Binding is like a reference - it is the same), so nothing to rebuild at this level. But inside WrappedText it is detected that Text with new text is not equal to one with old text, so body of WrappedText is called to re-render this part.
This is declared rendering optimisation of SwiftUI - by checking & validating exact changed view by equality.
By default this mechanism works by View struct properties, but we can be involved in it by confirming our view to Eqatable protocol and marking it .equatable() modifier to give some more complicated logic for detecting if View should be (or not be) re-rendered.

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

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...

Why is my SwiftUI List row not always updating internally

I have a list of reminders grouped into sections by completion and date. With data coming from an ObservedObject DataStore called global. I pass a realmBinding to the cell. The cell can update this binding and it will trigger the data store to update.
List {
// Past Due
if self.global.pastDueReminders.count > 0 {
Section(header: SectionHeader {}){
ForEach(self.global.pastDueReminders) { reminder in
NavigationLink(destination: ReminderDetail( reminder: reminder.realmBinding())) {
GeneralReminderCell(reminder: reminder.realmBinding())
}
}
}
}
// Completed
if self.global.completeReminders.count > 0 {
// Same as PastDue but for Completed
}
}
The cell looks something like:
struct GeneralReminderCell: View {
#Binding var reminder:Reminder
var body: some View {
HStack(alignment:.top, spacing: 10) {
Image(systemName: reminder.completed ? "checkmark.circle.fill" : "circle")
.onTapGesture(perform:{ self.reminder.completed = !self.reminder.completed })
VStack(alignment: .leading, spacing: 2) {
Text("Follow up with \(reminder.client.fullName)").fontWeight(.semibold)
if reminder.title.count > 0 {
Text(reminder.title)
}
Text(reminder.date.formatted()).foregroundColor(.gray)
}
}.padding(.vertical, 10)
}
}
When tapping on an image it toggles the reminder completion state and its position changes in the List view. The image that was tapped should changed to a filled in check when completed.
This behaviour almost always happens as expected, but sometimes the checked image will get out of sync with the completed state of reminder. I've look at this for quite some time and have not made much headway. Why is the checked image not always matching the state of the data?
Even though this is a very old question, I may have been working on what appears to be this same problem. In my case, my App is running on macOS. At first, the problem also seemed to be very intermittent and had been difficult to reproduce consistently.
I also have a View with a ForEach supplying rows to a List. My row's View contains an #State for an Optional Image that gets updated several different ways via actions performed by that same row View (e.g. Continuity Camera, file picker or drag & drop). The issue is that sometimes the new Image is shown and sometimes it is not shown. Using Self._printChanges() I am able to see that the #State is changing and the row's body it is being redrawn, however, the rendered View does not change. The only pattern that I am able to observe is that this issue only seems to occur with the last row in the List. Based on the success of my workaround below, it seems to confirm that there is an issue with the way SwiftUI's List reuses table cells.
My solution/workaround is to replace:
List {
ForEach {
}
}
With:
ScrollView {
LazyVStack {
ForEach {
}
}
}