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

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

Related

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

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.

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 :)

Select dynamically generated element by id in Titanium Appcelerator

I am working with the latest Tianium Appcelerator and my project is using Alloy.
I have a TableView with the id: tblResults
In my controller, I populate this table view with rows like this:
// Dummy data
var results = [];
results.push({
title: 'Hello World',
value: '123456'
});
results.push({
title: 'Bye World',
value: '654321'
});
// Build result data
var resultData = [];
for (var i = 0; i < results.length; i++) {
resultData.push(createResultRow(
results[i].title,
results[i].value
));
}
// Method to create result row
function createResultRow(myTitle, myValue) {
var tableRow = Titanium.UI.createTableViewRow({
height: 160
id: 'row-'+ myValue
});
var tableRowView = Titanium.UI.createView({
layout: 'horizontal'
});
var myButton = Titanium.UI.createButton({
title: myTitle,
btnValue: myValue
});
myButton.addEventListener('click', function(e) {
handleButtonClick(e);
});
tableRowView.add(myButton);
tableRow.add(tableRowView);
return tableRow;
}
// Set table data
$.tblResults.setData(resultData);
// Method to handle button click
function handleButtonClick(e) {
if (e.source && e.source.btnValue) {
// how to select row having a id: 'row-'+ e.source.btnValue ???
}
}
What this will do is, generate a dummy array of objects. Then using that, populate the table view with row that has a view, within it there is a button.
What I am trying to achieve is, when the button is clicked, I want to select the table row having the id like this:
'row-'+ e.source.btnValue
in pure javascript/jquery DOM style, I would have done something like this:
$('#row-'+ e.source.btnValue)
How can I achieve this in Titanium Appcelerator? Is there a element selector functionality of some sort like in jQuery?
This is a very often requested feature that we don't currently support, but should. Right now, you'd have to keep a hash of id -> view reference and look it up that way. However, I opened a Feature Request here https://jira.appcelerator.org/browse/TIMOB-20286
If you have a select method on rows, you can do like this:
$.table.addEventListener('click',function(e) {
if(e.row.select) e.row.select();
//or
if(rows[e.index]) rows[e.index].select();
});
For tables and lists views always use the click and itemclick event on the table/list. These events provide you with both the selected row (e.row) as well as the actual view clicked upon (e.source). It is also more efficient then having a listener on the buttons of all rows.
Your code would look like:
$.tblResults.addEventListener('click', handleButtonClick); // or bind in XML
// Method to handle button click
function handleButtonClick(e) {
// user tapped on button
if (e.source.btnValue) {
var row = e.row;
}
}

Is there a way to separate the two cases for rendering the emptyView?

Is there a way to separate the two cases for rendering the emptyView?
1. When the CollectionView is just created. The collection is still empty
2. After collection is fetched but the data is empty.
_CollectionView = Backbone.Marionette.CollectionView.extend({
emptyView: _EmptyView,
itemView: _ItemView,
initialize: function () {
this.collection = new Backbone.Collection ();
this.collection.fetch();
},//initialize
});
this is the way I have done this in the past. Set your 'emptyView' to be your loading view and then after the collection has synced, set the 'emptyView' to your actual EmptyView if required. I have also used this in 'onBeforeRender' as in the example below you may need to re-render your view if it has already been rendered with the 'EmptyView':
emptyView: LoadingView,
collectionEvents: {
'sync': 'onSync'
},
onSync: function () {
if(this.collection.length === 0) {
this.emptyView = EmptyView;
//may need to call 'this.render();' here if already rendered
}
}

Resources