SwiftUI - Use .refreshable on ForEach to empower pull to refresh experience - list

I'm making use of the new .refreshable() function. For Testing purposes I'm having this simple sleep function:
func load() async {
await Task.sleep(2 * 1_000_000_000)
}
Appending this to a list works fine:
.refreshable {
await load()
}
However: When I'm trying to use it with a ForEach Loop it's not working. Do you know how to overcome this issue? Background: I need ForEach to ensure custom styling. In the List I'm too limited. I already tried to style it using some attributes like
.buttonStyle(.plain)
.listRowSeparator(.hidden)
But I'm not able to remove the badges or the distance in blue as well as impacting the spacing between the list objects.
Would be great if you can let me know about your thoughts on this. I wan to avoid building up my own pull to refresh view with coordinators etc. like it was done back in SwiftUI 2.0 times :D Really love the .refreshable action.
Many Thanks!

If you want to use refreshable on anything besides List, it's up to you to provide the user interface for it. The refreshable documentation only says it works for List:
When you apply this modifier to a view, you set the refresh value in the view’s environment to the specified action. Controls that detect this action can change their appearance and provide a way for the user to execute a refresh.
When you apply this modifier on iOS and iPadOS to a List, the list provides a standard way for the user to refresh the content. When the user drags the top of the scrollable content area downward, the view reveals a refresh control and executes the provided action. Use an await expression inside the action to refresh your data. The refresh indicator remains visible for the duration of the awaited operation.

To fully customize the look of a List, I did the following:
Use ForEach to iterate within the List.
Add .listStyle(.plain) to List.
Use .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) with ForEach to get rid of any inner paddings.
Use .listRowSeparator(.hidden, edges: .all) with ForEach to hide separators. I found it a little bit annoying to customize it to my needs. However, there are some way in iOS 16 to do that as mentioned here.
Finally use overlay with ForEach as in the code block below to get rid of arrows on the right.
You can now whatever design you want within the list and get benefits of its capabilities.
List {
ForEach(presenter.items) { item in
ItemView(item: item)
}
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowSeparator(.hidden, edges: .all)
.overlay {
NavigationLink(destination: { Text("Detail View") }, label: { EmptyView() })
.opacity(0)
}
}
.listStyle(.plain)

Related

SwiftUI: delete animation in List?

I have a SwiftUI View that shows a list of items. The view is pretty large so I won't paste the whole thing here. However, the List uses a ForEach:
ForEach(model.items, id: \.id) { item in
myCell(item: item)
}
Everything works fine. What I am trying to do is have a simple slide animation when an item is deleted in the model.
I looked at this answer on stack:
Insert, update and delete animations with ForEach in SwiftUI
However, my setup is different and I am not sure how to try to apply that to my case. In that link the items are stored as a #State property in the view.
My setup is the view uses as #StateObject:
#StateObject var model = MyViewModel()
The model has this:
#Published var items = [MyStruct]()
The List's cells allow the swipe action, the user taps on delete, and the view tells the model to delete that specific item.
The model then has async logic that kicks in and eventually (pretty much immediately) removes the related item from its items. When that happens the List updates, the cell goes away and all works.
I would like to add a cell delete animation. How can I do that given my setup?
EDIT
Trying to add more code to help with context.
The deletion is triggered from an alert like so:
return Alert(
title: Text(title),
message: Text("You cannot undo this action"),
primaryButton: .destructive(Text("Delete")) {
withAnimation {
model.deleteItem(id)
}
},
secondaryButton: .cancel()
)
I added that withAnimation but nothing has changed.
Ok,
so I found what was making things not work even withAnimation.
My model has a call:
model.deleteItem(id)
Internally, as part of the work needed to delete that item, deleteItem(id) uses a Task.init { ... } task. I was then jumping back on main to then update items #Published var items = [MyStruct]() in the model.
The async nature of it broke the withAnimation.
I pulled out the removal of the item from items and left it sync within the withAnimation call and now the default delete animation works.
So, as long as the removal is synchronous within the withAnimation call, the animation kicks in.

Implementing Swift UI Picker

What is the correct way to implement a Picker component with specific logic within a Section element?
I would like to have each type displayed in a separate row.
var types = ["Books", "Films", "Music"]
var body: some View {
Form {
Section(header: Text("Type")) {
TextField("Type", text: $newCategoryType)
// Picker
}
}
}
First you must have a #State property that can be updated based on what selection the user makes, say in this case we have this
#State private var selectedType = "Books"
Then you will implement a Picker SwiftUI struct as follows
Picker("Please choose a type", selection: $selectedType) {
ForEach(types, id: \.self) {
Text($0)
}
}
Note that the \.self is really important for ForEach to distinguish between each element inside the list, without which the Picker won't perform the selection action correctly.
The above is enough for doing the job of displaying each option as a row since that is the default behaviour of ForEach
Additionally if you want to customise the look and feel of the picker
you would like to see .pickerStyle() view modifier, for which the docs and examples are mentioned
Also
Tip: Because pickers in forms have this navigation behavior, it’s important you present them in a NavigationView on iOS otherwise you’ll find that tapping them doesn’t work. This might be one you create directly around the form, or you could present the form from another view that itself was wrapped in a NavigationView.

How to get rid of SwifTUI List weird animation and spacing triggered by EditMode?

If you create a new project on Xcode 12.5 and use a SwiftUI List combined to a foreach and .onmove, you will notice 2 annoying things:
1/ Even though ondelete is unset, and even though you explicitly specify .deleteDisabled(true), a space appears on the left side of the list row when in EditMode. Is it possible to get rid of it?
2/ The entrance of the move icon creates some kind of an animation glitch on each list row. The divider is highlighting that. Why is there a space after the divider when EditMode is triggered? It seems that back in the past the divider width stay unchanged during this transition.
If you ask me, the previous behavior was making me think that the move button was embedded in a ZStack wrapping both the move button and the list. Whereas now it looks like it's in an HStack. Whatever, none of that explains the glitch which doesn't appear on the left side of the row even though it gets pushed too.
I am so confused.
I am surprised I am not able to find anything about that issue via Google. I am the one doing something wrong?
A video of the problem: Glitch and here is a simple code to try it out yourself.
Thank you for your time.
struct ContentView: View {
#State var listItems = ["Item 1", "Item 2", "Item 3"]
var body: some View {
NavigationView {
List {
ForEach(listItems, id: \.self) { (item) in
Text(item)
}.onMove { (indexSet, index) in
self.listItems.move(fromOffsets: indexSet,
toOffset: index)
}
}
.navigationBarTitle(Text("Nav Title"))
.deleteDisabled(true)
.navigationBarItems(trailing: EditButton())
}
}
}

SwiftUI - Left squeeze views in a ForEach loop

I am trying to loop through a list of 2-6 items. They are all Text() views for this example, and I want them to all squeeze to the left. This code below is as close as I have gotten
Why? I am simplifying the actual problem to make solving easier
HStack{
ForEach(array, id: \.self) { pageItem in
HStack{
PageDataItemView(pageItem: pageItem)
}.fixedSize()
}
VStack(){
Spacer()
}
}
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
.frame(height: 40.0)
PageDataItemView for this example is just Text().
Anyway what I can't seem to solve is to make PageDataItemView's width based on the text content. In the code above they squeeze, but they all overlap each other, rather than taking their own space. Removing .fixedSize() above causes the items to fill the full width evenly.
In Android's XML I could just use "wrap_content" to produce this. While I understand this is not the ideal way to build UI here, it has to do with a complex client request.
Update:
GeometryReader { geometry in
Seems to be causing my views to break as this is inside PageDataItemView. Even when geometry isn't even used at all. Once removed everything worked as expected, curious why this effects a view indirectly. Let me know if I should remove this question.

How Can I Wrap Text in a SwiftUI NavigationButton?

SwiftUI's Text type has a modifier that allows text to wrap if the text is too long to fit horizontally within its container. To achieve text wrapping, simply pass nil as the argument to the lineLimit modifier:
Text("This text is too long to fit horizontally within its non-NavigationButton container. Therefore, it should wrap to fit, and it does.")
.font(.body)
.lineLimit(nil)
The above works as expected, except for when used inside of a SwiftUI NavigationButton. All Text instances that I have nested within NavigationButton instances do not wrap:
NavigationButton(destination: DestinationView()) {
Text("This text is too long to fit horizontally within its NavigationButton container. Therefore, it should wrap to fit, but it does not.")
.font(.body)
.lineLimit(nil)
}
Is there anything that I am missing from the code above that would allow Text instances to wrap within NavigationButton instances?
Edit to add more context:
The initial view is a List that is wrapped in a NavigationView. The List contains instances of MeasurableItemsListItem, which are wrapped in NavigationButton instances that trigger navigation to a secondary view that is added to the navigation stack:
struct MeasurableItemsList : View {
private let measurableItems = MeasurableItem.allCases
var body: some View {
NavigationView {
List(measurableItems.identified(by: \.self)) { measurableItem in
NavigationButton(destination: DosableFormsList(measurableItem: measurableItem)) {
MeasurableItemsListItem(measurableItem: measurableItem)
}
}
}
}
}
Each list item that is wrapped in a NavigationButton is made from the following structure:
struct MeasurableItemsListItem : View {
let measurableItem: MeasurableItem
var body: some View {
Text(measurableItem.name)
.font(.body)
.foregroundColor(.primary)
.lineLimit(nil)
}
}
You might be helped with this answer: https://stackoverflow.com/a/56604599/30602
The summary is that inside other Builders you need to add .fixedSize(horizontal: false, vertical: true) to your Text() to get it to wrap.
You don't need to use NavigationButton to achieve navigation. You can achieve it by using NavigationLink easily, wherein you don't need to wrap your views inside NavigationButton.
Check this ANSWER which is explaining the usage of NavigationLink without wrapping view inside it. I hope it helps.
P.S.- i don't have enough reputation points to add this as a comment. Thanks