How to forcefully redraw a swift ui view within a button action call - ios

So I'm doing something sort of jank, but I need to do it for a class that I am taking
I need to update the view after every step of a while loop that is inside of a button's action: callback, in order to create a really simple "animation"
is there any way to get a swift UI view to drop everything and redraw immediately?
code for my button:
Button(action: {
pass_again = true;
while(pass_again){
(right_text, pass_again) = twos.collapse(dir: Direction.right)
cur_id+=1
usleep(1000000)
// I need to redraw the UI here
}
twos.spawn()
}, label: {
Text(right_text)
}).padding(0.5)

You can bind an #State variable to any SwiftUI view i.e. Text(self.yourvar), so any time you change the #State value it will automatically update the text or any other property bonded with it.
About the animation you can use .withAnimation modifier to create a simple animation that swiftUI will perform
withAnimation {
your state var updates goes here
}

Related

How to manually reload any row in swiftUI

I wanted to create List in SwiftUI with following requirements:
Each row has some detail, and Toggle which represents its isEnabled status.
If someone enables or disables that toggle, then based on some logic, Either I have to allow its to be enabled, OR not allow it to be enabled OR allow it to be enabled, but have to disable other object from other row.
I have my ViewModel and Model as below
I have my model and View model as somewhat like this
class MyViewModel:ObservableObject{
#Published myObjArray:[MyObject]
....
}
class MyObject:Identifiable{
var id:..
var isEnabled:Bool
var title:...
....
....
}
As MyObject is used at multiple places in project, so, I dont want to touch It.
For implementing Toggle, we have to provide binding, so, I created one state object in its RowView and on enable and disable of that toggle, I get its event like this:
struct MyRowView: View {
var myObj:MyObject
#State var isEnabled: Bool
....
....
init(myObj:MyObject) {
self.myObj = myObj
self.isEnabled = myObj.isEnabled
}
var body: some View {
....
Toggle("", isOn: $isEnabled)
.labelsHidden()
.onChange(of: isEnabled) { value in
self.MyViewModelAsEnvironmentObject.toggleValueUpdated()
}
....
}
As value of isEnabled changes, I am passing that to above mentioned MyViewModel object, where it checks for some conflicts, if conflicting, I am showing some alert, asking user to to forcefully enable this or not, if he says yes, then I have to disable row in other row, if user says no, then I have to disable this row.
Question is, How I can reload individual cell to enable or disable given toggle in given rows? If I print value directly in Row, then it is updating properly, but here isEnabled variable is assigned when row initiated, now its not possible to change its value from view model.
How to deal with this situation?

SwiftUI - testing - simulate tap gesture?

Is it somehow possible to simulate a tap when testing (ex. snapshot tests) a tap or any other gesture in SwiftUI?
For UIKit we can do something like:
button.sendActions(for: .touchUpInside)
Is there any SwiftUI equivalent?
While it's not directly possible to "Simulate" in the fashion you're attempting to simulate, it is perfectly possible to simulate the actions behind the buttons. This is assuming that you're using an MVVM architecture. The reason for this is that if you "Simulate" via the backing methods that support the buttons, via the view model, then you will still get the same result. In addition to this, SwiftUI will update and recalculate the views upon any state change, meaning it doesn't matter if the button changes a state or if a method changes the state. You can then extend that functionality to the init() function of the view struct, and viola, you'll be simulating actions.
View Model Example
class VMExample: ObservableObject {
#Published var shouldNavigate = false
func simulateNavigate() {
shouldNavigate.toggle
}
}
View Example
struct MyView: View {
#ObservedObject var vm = VMExample()
var body: some View {
NavigationLink(
"Navigate",
destination: Text("New View"),
isActive: $vm.shouldNavigate)
.onAppear {
//If Debug
vm.simulateNavigate()
}
}
}
Simulating multiple actions
To do it with multiple actions, you could potentially create some function func beginSimulation() that begins running through all the actions you want to test. You might change some text, navigate to a view, etc...
TL;DR
Simulate the actions behind the buttons, not the buttons interactions themselves. The result will be the same due to View Binding.

Button handling in SwiftUI

In a SwiftUI app I have a few buttons (let us say 3 as an example). One of them is highlighted.
When I tap on a non-highlighted button, the previously highlighted button toggles to non-highlighted and the tapped button becomes highlighted. If I tap the already highlighted button, nothing happens.
This scenario is simple and I have a highlighBtn variable, when a button is tapped highlighBtn takes the value of the tapped button. This variable is then used when the next tap happens to toggle off the previously highlighted button.
This cycle is OK, but the problem is when I do the first tap. For some reasons, things don't work.
This is how I handle the creation of the highlighBtn variable:
class ActiveButton: ObservableObject {
#Published var highlighBtn = Button(....)
}
#StateObject var box = ActiveButton()
Here is the relevant code, when the button is tapped:
#EnvironmentObject var box: ActiveButton
....
Button(action: {
// Toggle off the previously highlighted button.
box.highlighBtn.highLight = false
.... some useful processing ....
box.highlighBtn = self
})
One detail I should give: if I tap the highlighted button to start, then all works as it should.
I have tried various method to solve this apparently simple problem but failed.
The first method was to try to initialize the highlighBtn variable.
The second was to try to simulate a tap on the highlighted button.
I must be missing something simple.
Any tip would be welcome.
After further investigation .....
I have created a demo app to expose my problem.
Since I lack practice using GitHub, it took me some time, but it is now available here.
For that I created a SwiftUI app in Xcode.
In SceneDelegate.swift, the four lines of code right after this one have been customized for the needs of this app:
// Create the SwiftUI view that provides the window contents.
Beside that all I did resides inside the file ContentView.swift.
To save some time to anyone who is going to take a look; here is the way to get to the point (i.e. the issue I am facing).
Run the app (I did it on an iPhone 7). You will see seven buttons appear. One of them (at random) will be highlighted. Starting with the highlighted button, tap on a few buttons one after another as many times as you want. You will see how the app is supposed to work.
(After switching it off) Run the app a second time. This time you will also tap on a few buttons one after another as many times as you want; but start by tapping one of the non-highlighted button. You will see the problem appear at this point.
Here is a solution for the first part of your question: Three buttons where the last one tapped gets highlighted with a background:
import SwiftUI
struct ContentView: View {
enum HighlightTag {
case none, first, second, third
}
#State private var highlighted = HighlightTag.none
var body: some View {
VStack {
Button("First") {
highlighted = .first
}.background(
Color.accentColor.opacity(
highlighted == .first ? 0.2 : 0.0
)
)
Button("Second") {
highlighted = .second
}.background(
Color.accentColor.opacity(
highlighted == .second ? 0.2 : 0.0
)
)
Button("Third") {
highlighted = .third
}.background(
Color.accentColor.opacity(
highlighted == .third ? 0.2 : 0.0
)
)
}
}
}
Update:
After reviewing your sample code on GitHub, I tried to understand your code, I tried to make some simplifications and tried to find a working solution.
Here are some opinions:
The Attribute "#State" in front of "var butnsPool" is not needed and confusing.
The Attribute "#State" in front of "var initialHilight" is not needed and confusing.
Your ActiveButton stores a copy of the selected Button View because it is a struct which is probably the main reason for the strange behaviour.
The needInit in your ObservableObject smells bad at least. If you really need to initialize something, you may consider doing it with some .onAppear() modifier in you ContentView.
There is probably no need to use .environmentObject and #EnvironmentObject. You could consider using a parameter and #ObsservedObject
There is probably no need for the ActiveButton at all, if you only use it internally. You could consider using a #State with the selected utton name
Your BtnTxtView is fine, but consider replacing the conditional (func if) with some animatable properties, if you want to animate the transition.
Based on your code I created a much simpler and working solution.
I removed the ActiveButton class and also the BttnView struct.
And I replaced the ContentView with this:
struct ContentView: View {
var butnsPool: [String]
var initialHilight: Int
#State var selectedBox: String = ""
var body: some View {
ForEach(butnsPool, id: \.self) { buttonName in
Button(action: {
selectedBox = buttonName
})
{
BtnTxtView(theLabel: buttonName,
highLight: buttonName == selectedBox)
}
}.onAppear {
selectedBox = butnsPool[initialHilight]
}
}
}

SwiftUI #State variables not getting deinitialized

I have a SwiftUI View where I declare a condition like this
#State var condition = UserDefaults.standard.bool(forKey: "JustKey")
When I first push this view into the navigation stack condition variable is getting the correct value. Then when I pop this View I change the value in UserDefaults but when I push this screen again condition variable remembers the old value which it got first time.
How to find a workaround for this because I want to reinitialize my condition variable each time I enter my custom view where I declared it?
In this case, #State is behaving exactly like it is supposed to: as a persistent store for the component it's attached to.
Fundamentally, pushing a view with a NavigationLink is like any other component in the view hierarchy. Whether or not the screen is actually visible is an implementation detail. While SwiftUI is not actually rendering your hidden UI elements after closing a screen, it does hold on to the View tree.
You can force a view to be completely thrown away with the .id(_:) modifier, for example:
struct ContentView: View {
#State var i = 0
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailView().id(i)) {
Text("Show Detail View")
}
Button("Toggle") {
self.i += 1
UserDefaults.standard.set(
!UserDefaults.standard.bool(forKey: "JustKey"),
forKey: "JustKey")
}
}
}
}
}
The toggle button both modifies the value for JustKey and increments the value that we pass to .id(i). When the singular argument to id(_:) changes, the id modifier tells SwiftUI that this is a different view, so when SwiftUI runs it's diffing algorithm it throws away the old one and creates a new one (with new #State variables). Read more about id here.
The explanation above provides a workaround, but it's not a good solution, IMO. A much better solution is to use an ObservableObject. It looks like this:
#ObservedObject condition = KeyPathObserver(\.JustKey, on: UserDefaults.standard)
You can find the code that implements the KeyPathObserver and a full working Playground example at this SO answer

How can I connect a variable value to a slider?

I am trying to insert a value that continuously changes into a slider. I want to see the slider move on its own as this value changes.
I have a slider structure set up and I tried inserting that value I want in it but it is not moving. I know that my variable value is changing as I want it to.
struct SliderCounter : View {
#State var timer1 : Float
var body: some View {
return Slider(value: $timer1, in : 0...100, step:1)
.padding()
}
}
SliderCounter(timer1: Float(changingValue))
I am calling this from another file but the slider is only showing up and not moving as the value changes
Since you are using SliderCounter as a reusable view, you need to user #Binding as this reusable view does not own the data.

Resources