Dismissed view empties before animation starts when using SwiftUI navigation in combination with TCA - animation

We are struggling with SwiftUI navigation in combination with TCA and I am wondering is someone else did encounter similar issue.
The problem is that when we set parameters isPresented or isActive on .sheet or NavigationLink to false to dismiss it, then all content seems to be replaced with empty view before the animation starts (see attached gif).
In the code we store state (boolean value) indicating if child view is presented in parent view. When button to go back on child view is tapped, then we catch this action in parent view and change the boolean value to false to dismiss child view. It works like a charm instead of navigation animation.
I would be endlessly happy for any help or suggestions.

I have a similar problem. My example is a bit more involved but arguably more generic. Hope it helps.
My TCA setup looks like this:
struct State: Equatable {
var child: ChildState?
// ...
}
One can get the child store by scoping the parent store:
let childStore = parentStore.scope { $0.child ?? ChildState() }
And the child view’s body looks like this:
var body: some View {
WithViewStore(childStore) { viewStore in
// ...
}
}
Here is what happened:
Some reducer sets parent.child = nil.
This notifies every scoped store that the root state changes.
Each scoped store then applies the scope function to get a new child state, and compares it with the previous one. More likely than not, we get a different child state. Some objectWillChange is called.
The corresponding WithViewStore is marked dirty.
The content closure in WithViewStore is called, and generates a new child view tree.
SwiftUI then generates an animation from the old child view tree to the new child view tree, in addition to the dismissal animation for the whole child view tree.
My solution is to kill step 3. First, let's extend the child state with another property:
struct ChildState: Equatable {
var isBeingDismissed: Bool = false
}
Second, when scoping, let's do:
let childStore = parentStore.scope { x -> ChildState in
if let child = x.child {
return child
}
var child = ChildState()
child.isBeingDismissed = true
return true
}
Lastly, we can modify the WithViewStore with a custom removeDuplicates closure:
var body: some View {
WithViewStore(childStore) { old, new in
if old == new {
return true
}
// Key
if new.isBeingDismissed {
return true
}
return false
} content: { viewStore in
// ...
}
}
Here is how it works. The scoped child state can be nil in two cases, when the view is first created and the view is being dismissed. In the first case, the old is nil. In the second case, the new is nil. The custom removeDuplicates essentially does this: when a child state is being dismissed, we skip any update. So the only animation being played is the transition animation. The content stays the same.

Related

In SwiftUI how can I add scroll to refresh functionality to List?

If I have a SwiftUI List that gets its values from my server via API call, such as:
var body: some View {
List {
ForEach(viewModel.items) { item in
ListItemsView(item: item)
}
}
.onAppear {
self.viewModel.getAllItems()
}
}
The array of items is stored in the #Published var items, in viewModel
The func .getAllItems() calls a server API which returns an array of items and stores them under the #Published var items
How can I add the functionality to swipe down on this List and call .getAllItems() again, in order to update items with new values, and theoretically re-render the List?
Or is there a much better way to do this that I'm not seeing?
In Xcode 13 and iOS 15+ you can use the new refreshable() modifier
List(events) { event in
Text(event.name)
}
.refreshable {
print("Get New Events")
}
Here's an implementation that adds a proper UIRefreshControl to a SwiftUI List's table view: https://github.com/timbersoftware/SwiftUIRefresh

SwiftUI how to get reference to editing Textfield inside a List?

Is it possible to find a particular view in SwiftUI based on its tag ? Or is there another solution to my problem below ?
Based on string in a Textfield in a Row in List A, am populating search results in List B.
When a row on a List B is tapped, I want the textField on List A to be updated.
But the problem is I dont know how to get hold of the active textField or the index of the row in the List A.
Please refer this image for clarity
Im trying to emulate this behaviour, which is common in desktop. You enter text in a Textfield and either using mouse or up down arrow keys to select an option and the Textfield gets updated with that option.
Here instead of PopOver am using a separate List view.
In the code below, you can see how to accomplish most of the things you require.
You mention you want to know which field you are typing on. You can do that by using the onEdintingChanged closure. This closure receives a boolean value that indicates if the field became active or inactive. You can use it to set a variable like in the example: activeField.
Another of your requests, was being able to refresh the List with every keystroke. The onReceive modifier subscribes to the binding of the TextField and executes your closure for each. There is however, what I think it is a bug: When there is text in both fields, the closure executes for both fields at every keystroke. The if statement comparing self.activeField is there to prevent that.
So, now, from the closure you can trigger an update on your external model. Since your List should be bound to the same model, it will refresh automatically.
Another of your requirements was that tapping on the list should update your text field. That is simple. If your textfield is bound to the same model, you just update the corresponding model variable and the field will update.
I hope I've been clear.
struct ContentView : View {
#State private var field1 = ""
#State private var field2 = ""
#State private var activeField: Int = 0
var body: some View {
VStack {
TextField("", text: $field1, onEditingChanged: { if $0 { self.activeField = 0 } })
.textFieldStyle(.roundedBorder)
.onReceive(field1.publisher().last()) { if self.activeField == 0 { print("FIELD 1 -> \(self.field1): \($0)") } }
TextField("", text: $field2, onEditingChanged: { if $0 { self.activeField = 1 } })
.textFieldStyle(.roundedBorder)
.onReceive(field2.publisher().last()) { if self.activeField == 1 { print("FIELD 2 -> \(self.field2): \($0)") } }
}
}
}

Custom Cell Format ListView TornadoFx on delete item

I'm new to TornadoFx but am trying it out (also new to JavaFX by extension).
I have a listview defined as so:
private var colorList = mutableListOf<Color>
//other things in init block
colorpicker(mode = ColorPickerMode.MenuButton) {
valueProperty().onChange {
if (it != null) {
colorList.add(it)
}
}
}(Color.BLACK,Color.WHITE).observable()
listview(colorList) {
cellFormat {
text = it.toString()
style {
baseColor = it
}
}
contextmenu {
item("Delete").action {
if (selectedItem != null) {
colorList.remove(selectedItem)
}
}
}
}
//continue init block
Adding and taking away colors from the listview works just fine but the color inside the cell does not disapear if it is no longer occupied
Example of what is happening
The cellFormat function allows you to configure the list cell for each item in your list.
However, when there is no list item for a certain row, the callback is not run, so that you have no way of applying a style to an empty row using the cellFormat approach. One solution would be to implement your own ListCell and always clearing the style property of the cell, but I believe this might actually be fixed within the framework by always clearing the style property before a cell is reused. I just tried to make this change in the framework, and it fixes the issue with your code sample.
I will commit the change now, please try it out with tornadofx-1.7.17-SNAPSHOT :)

ExtJS: Clear a data binding from a viewController

I am using the admin-dashboard template and made the following modification to the MainController:
routes: {
':node': {
before : 'checkSession',
action : 'onRouteChange'
}
},
Thus, when I change a route the before route will happen first, so the following method will first get called and then the routing will proceed onward:
checkSession : function() {
var args = Ext.Array.slice(arguments),
action = args[args.length - 1],
hash = window.location.hash;
// TODO: use hash to clear patient header when appropriate
if (hash.indexOf('#new-patient') > -1) {
console.log(hash);
}
action.resume();
},
This works splendidly. However, when I get the condition of hash.indexOf('#new-patient') > -1 I would like to clear a comboBox that I have defined in a view as:
bind: {
store: '{patients}'
},
reference: 'patientCombo',
This is from a view in the same scope as the above method from the viewController is, that is they are both in the hierarchy of the same viewModel. I am just not sure exactly how I can clear this from the above method. I just want to set the combo box back to no selected value (if one had been selected) and then update the bindings. Merci!
When I tried using Ext.ComponentQuery, I initially referenced the itemId, but the xtype was wrong (the combobox and displaytext, among other sundries are in a container). Since this is the only combobox in my app now, I just tried it as:
combo = Ext.ComponentQuery.query('combobox')[0]
if (combo){
combo.setValue('')
}
and, lo' and behold, it worked!

change:source event in JointJS

Merry Christmas, everyone!
I want to do something when the source element or target element of a joint.dia.Link is changed. Firstly I tried to put the code in the callback function of 'change:source' and 'change:target' events. However, it turns out that those callback functions are called as soon as the link's position changes, instead of being called when the source element or target element changes. Then I tried to put the code in the LinkView.pointup() function, by adding a tag, which is set in the callback function of 'change:source' and 'change:target' events, to indicate the changed element. The resulted code looks like this:
link.on('change:source', function(){this.src_changed = true;});
link.on('change:target', function(){this.dest_changed = true;});
joint.shapes.custom.ModelLink = joint.dia.Link.extend({
defaults: joint.util.deepSupplement({
type: 'custom.ModelLink',
}, joint.dia.Link.prototype.defaults)
});
joint.shapes.custom.ModelLinkView = joint.dia.LinkView.extend({
pointerdown: function () {
joint.dia.LinkView.prototype.pointerdown.apply(this, arguments);
},
pointermove: function () {
joint.dia.LinkView.prototype.pointermove.apply(this, arguments);
},
pointerup: function (evt, x, y) {
var link = this.model;
if(link.src_changed) { // do something}
if(link.dest_changed) {// do something}
joint.dia.LinkView.prototype.pointerup.apply(this, arguments);
}
});
However, I found src_changed and dest_changed are both set to true sometimes when I am just dragging one end of the link. Why does this happen? How can I fix this? Or any new approach to do some response to the change of source element or target element?
Besides, After I reset the events of joint.shapes.uml.State using model.set('events', events), the text doesnot change on the graph? How can I refresh graph to show the changed state element?
Thanks very much!
The change:source and change:target events are indeed triggered also when the position of the arrowheads changes. In general, the source and target of a link can either be a point (an object with x and y properties) or an element (and in the near future also a link) - an object with id property pointing to the linked element. If you're only interested in source/target being an element, you can just check in your handlers for the change:source and change:target events whether the source/target of the link contains the id property:
if (this.get('source').id) { /*... do something ...*/ }

Resources