How to manually reload any row in swiftUI - ios

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?

Related

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

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
}

SwiftUI selectable lists where selection variable is ordered

I'll keep it simple, I have a List with UUID identified items. This list is selectable, when EditMode() is entered user can tap items, and these get added to the selection variable:
And the selection variable is:
#State var selection = Set<UUID?>()
However, the issue is that I care about the order in which the items are selected. And in the selection array, the items are not updated in a FIFO or LIFO way, they seem to be random or perhaps it depends on the value of the UUID but the point is it doesn't conserve the order in which they are added.
I considered using a stack to keep track of what is added, but the List + selection combination in SwiftUI doesn't appear to have built in methods to inform of new additions e.g. UUID 1234 has been added. I can thing of "not clean" ways to make it work like iterating through the whole selection Set every time selection.count changes and add the "new item" to the stack but I wouldn't want to come down to that.
So after some research, the List constructor that enables selection in .editMode does specify that the selection variable has to be a Set and nothing but a Set:
public init(selection: Binding<Set<SelectionValue>>?, #ViewBuilder content: () -> Content)
So I decided to implement my own array that keeps track of the selection order. I'll show how in case it helps anyone, but it is quite rudimentary:
#State var selectedItemsArray=[UUID]()
...
List(selection: $selection) {
...
}
.onChange(of: selection) { newValue in
if newValue.count>selectedItemsArray.count {
for uuid in newValue {
if !selectedItemsArray.contains(uuid) {
selectedItemsArray.append(uuid)
break
}
}
}
if newValue.count<selectedItemsArray.count {
for uuid in selectedItemsArray {
if !newValue.contains(uuid) {
selectedItemsArray.remove(at: selectedItemsArray.firstIndex(of: uuid)!)
break
}
}
}
}

How to handle onChange of Text event in SwiftUI?

I have a ContentView where I have a variable:
#State private var textToShow = "Some Text"
That I show in:
Text(textToShow)
I have a button where when I click it, it changes textToShow to equal "Changed Text". What is the right way to attach some kind of event that triggers when the Text changes? I am looking for something like a Text(textToShow).onChange(print("Text Changed")).
Note that I do not have any IBAction and I am not using any storyboards.
I would trigger any side-effects as early as possible. That would be the button-action. If you trigger side-effects from side-effects from side-effects it will become hard to track all changes that may occur when you tap the button. I used to implement such a chain of multiple bindings. It was horrible to maintain.
If you still want to observe the Text-View, then you may just observe the state itself and not the Text-View. Side-effects can be trigged by the View‘s onChange-modifier. https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)
Text(textToShow)
.onChange(of: textToShow) { newValue in
print(...)
}
You could also achieve this using an ObservableObject.
First, declare a model class that conforms to the ObservableObject protocol. This will store the text and apply a didSet property observer.
class Model: ObservableObject {
#Published var textValue: String = "" {
didSet {
print("Text changed!")
}
}
}
Then use it in the relevant view.
struct ContentView: View {
// Create observed object
#ObservedObject var model = Model()
var body: some View {
Text($model.textValue)
}
}

SwiftUI tracking "selected" (tapped on) TextField from an outside view

I have a card view which contains a list of TextFields that are drawn from CoreData, something like:
var body: some View {
ZStack {
ForEach(textsArray, id:\.self) { text in
TextFieldView(textBlock: text, editing: editing)
.onTapGesture(count: 1) {
selected.selectedText = text
}
}
}
}
The textfield stores its contents and color in CoreData.
This card is displayed by an Edit view. The edit view also contains a ColorPicker, which should allow you to modify the color of the selected text field. So, if the user taps on a text field and starts editing it, a color picker will appear in the corner of their screen to allow them to modify the color of that field.
I attempted to create an observable object to keep track of the selected text field:
class Selection:ObservableObject {
#Published var selectedText : TextBlock?
}
Then my edit view would simply keep track of the selected text:
#ObservedObject var selected : Selection = Selection()
It also passes it down into the card view.
The issue is that the ColorPicker view requires a binding to a CGColor. I'm not sure how to pass this binding to the ColorPicker: I tried this:
ColorPicker("", selection: self.selected.selectedText.$color)
But XCode tells me there's no member of selected text called $color, which I guess is because color is #NSManaged rather than a #State property.
How can I pass a binding of the Color property to the color picker? Am I even approaching this the right way? I'm brand new to iOS development so I have no idea what the idiomatic ways of doing things are.
You can separate it into other view and call like
if self.selected.selectedText != nil {
MyColorPicker(selection: self.selected.selectedText!)
}
where
struct MyColorPicker: View {
#ObservedObject var selection: TextBlock
var body: some View {
ColorPicker("", selection: self.$selection.color)
}
}

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

Resources