Make a list row selectable in SwiftUI - ios

I'm creating a list with custom rows in SwiftUI. When I tap one of them, I want the row to turn light gray until another tap is detected.
This doesn't happen with simple non-custom lists either.
Here's my list:
List{
ForEach(blocks){ block in
BlockRow(block: block)
}
.onDelete(perform: delete)
.onMove(perform: day.move)
}
When I tap on one of the items, nothing happens. If I create a simple list with storyBoards, I get the behavior I want:

Hey so you asked this 3 months ago so I hope you got an answer somewhere or figured it out since then, but to get to the good stuff to make a button tappable I was able to get it working using this,
List {
Button (action: {
//Whatever action you want to perform
}) {
//Code to present as the cell
}
}
I would maybe try the following based on your code,
List (blocks) { block in
Button (action: {
//perform button action
}) {
//How the cell should look
BlockRow(block: block)
}
}
.onAppear()
.onDelete()

Related

What is "stable identifier" for SwiftUI Cell?

While listening Use SwiftUI with UIKit (16:54), I heard, that the reporter said:
"When defining swipe actions, make sure your buttons perform their actions using a stable identifier for the item represented.
Do not use the index path, as it may change while the cell is visible, causing the swipe actions to act on the wrong item."
- what??? All these years I was fighting with prepareForReuse() and indexPath in cells trying to somehow fix bugs related to cell reusing.
What is this "stable identifier"?
Why does no one talk about it?
On stackoverflow you can find answers only related to prepareForReuse() function. No "stable identifier".
Is it reuseIdentifier?
If so, how I suppose to use it?
Creating for each cell its own reuseIdentifier, like this:
for index in 0..<dataSourceArray.count {
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "ReuseIdForMyCell" + "\(index)")
}
She made a mistake, if you download the sample associated with the video you'll see the deleteHandler captures the item from outside already, it doesn't look it up again when the handler is invoked. She was trying to say that if you look up an item in the handler then there is a chance if other rows have been added or removed then its index will have changed so if you use the rows old index to look up the item then you would delete the wrong one. But since no lookup is required that will never happen, so she shouldn't have even mentioned it. Here is the code in question:
// Configures a list cell to display a medical condition.
private func configureMedicalConditionCell(_ cell: UICollectionViewListCell, for item: MedicalCondition) {
cell.accessories = [.delete()]
// Create a handler to execute when the cell's delete swipe action button is triggered.
let deleteHandler: () -> Void = { [weak self] in
// Make sure to use the item itself (or its stable identifier) in any escaping closures, such as
// this delete handler. Do not capture the index path of the cell, as a cell's index path will
// change when other items before it are inserted or deleted, leaving the closure with a stale
// index path that will cause the wrong item to be deleted!
self?.dataStore.delete(item)
}
// Configure the cell with a UIHostingConfiguration inside the cell's configurationUpdateHandler so
// that the SwiftUI content in the cell can change based on whether the cell is editing. This handler
// is executed before the cell first becomes visible, and anytime the cell's state changes.
cell.configurationUpdateHandler = { cell, state in
cell.contentConfiguration = UIHostingConfiguration {
MedicalConditionView(condition: item, isEditable: state.isEditing)
.swipeActions(edge: .trailing) {
Button(role: .destructive, action: deleteHandler) {
Label("Delete", systemImage: "trash")
}
}
}
}
}

SwiftUI: backwards infinite scroll

I am showing the conversation in the view, initially only the end of the conversation is loaded. To simplify it's something like this:
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(items) { item in
itemView(item)
.onAppear { prependItems(item) }
}
.onAppear {
if let id = items.last?.id {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
}
}
func prependItems(item: Item) {
// return if already loading
// or if the item that fired onAppear
// is not close to the beginning of the list
// ...
let moreItems = loadPreviousItems(items)
items.insert(contentsOf: moreItems, at: 0)
}
The problem is that when the items are prepended to the list, the list view position remains the same relative to the new start of the list, and trying to programmatically scroll back to the item that fired loading previous items does not work if scrollbar is moving at the time...
A possible solution I can think of would be to flip the whole list view upside down, reverse the list (so that the new items are appended rather than prepended), then flip each item upside down, but firstly it is some terrible hack, and, more importantly, the scrollbar would be on the left...
Is there a better solution for backwards infinite scroll in SwiftUI?
EDIT: it is possible to avoid left scrollbar by using scaleEffect(CGSize(width: 1, height: -1)) instead of rotationEffect(.degrees(180)), but in either case item contextMenu is broken one way or another, so it is not a viable option, unfortunately, as otherwise scaleEffect works reasonably well...
EDIT2: The answer that helps fixing broken context menu, e.g. with a custom context menu in UIKit or in some other way, can also be acceptable, and I posted it to freelancer in case somebody is interested to help with that: https://www.freelancer.com/projects/swift/Custom-UIKit-context-menu-SwiftUI/details
Have you tried this?
self.data = []
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01)
{
self.data = self.idx == 0 ? ContentView.data1 : ContentView.data2
}
Basically what this does is first empty the array and then set it again to something, but with a little delay. That way the ScrollView empties out, resets the scroll position (as it's empty) and repopulates with the new data and scrolled to the top.
So far the only solution I could find was to flip the view and each item:
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(items.reversed()) { item in
itemView(item)
.onAppear { prependItems(item) }
.scaleEffect(x: 1, y -1)
}
.onAppear {
if let id = items.last?.id {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
}
.scaleEffect(x: 1, y -1)
}
func prependItems(item: Item) {
// return if already loading
// or if the item that fired onAppear
// is not close to the beginning of the list
// ...
let moreItems = loadPreviousItems(items)
items.insert(contentsOf: moreItems, at: 0)
}
I ended up maintaining reversed model, instead of reversing on the fly as in the above sample, as on long lists the updates become very slow.
This approach breaks swiftUI context menu, as I wrote in the question, so UIKit context menu should be used. The only solution that worked for me, with dynamically sized items and allowing interactions with the item of the list was this one.
What it is doing, effectively, is putting a transparent overlay view with an attached context menu on top of SwiftUI list item, and then putting a copy of the original SwiftUI item on top - not doing this last step makes an item that does not allow tap interactions, so if it were acceptable - it would be better. The linked answer allows to briefly see the edges of the original SwiftUI view during the interaction; it can be avoided by making the original view hidden.
The full code we have is here.
The downside of this approach is that copying the original item prevents its partial updates in the copy, so for every update its view ID must change, and it is more visible to the user when the full update happens... So I believe making a fully custom reverse lazy scroll would be a better (but more complex) solution.
We would like to sponsor the development of reverse lazy scroll as an open-source component/library - we would use it in SimpleX Chat and it would be available for any other messaging applications. Please approach me if you are able and interested to do it.
I found a link related to your question: https://dev.to/gualtierofr/infinite-scrolling-in-swiftui-1p3l
it worked for me so you can implement parts of that in ur code
struct StargazersViewInfiniteScroll: View {
#ObservedObject var viewModel:StargazersViewModel
var body: some View {
List(viewModel.stargazers) { stargazer in
StargazerView(stargazer: stargazer)
.onAppear {
self.elementOnAppear(stargazer)
}
}
}
private func elementOnAppear(_ stargazer:User) {
if self.viewModel.isLastStargazer(stargazer) {
self.viewModel.getNextStargazers { success in
print("next data received")
}
}
}
you can take what you need from here

Adding a cell to another TableView. (selecting an item from a tableview, showing it in another tableview)

I'm building an app which which has built in with 2 different tabs. First tab is is "Home" which basically has a tableview with cells that configured from an api.(The api gets me country names for now)
Those cells also have a "Star" button which prints the data of the specific cell for now.
Second tab is "Saved" tab(SavedViewController), where I want to show the "starred" countries, using a tableview.
You can see the image below in order to get an idea for the app.
App simulation Image
The star button has a function in my CountriesTableViewCell. I'm using a saveButtonDelegate in order to let the SavedViewController know about an item is going to be saved. The code in CountriesTableViewCell for star button is as below.
#objc func buttonTapped() {
//If Button is selected fill the image. Else unfill it.
if !isSaveButtonSelected {
saveButton.setImage(UIImage(systemName: "star.fill"), for: .normal)
isSaveButtonSelected = true
saveButtonDelegate?.saveButtonClicked(with: countryData) //Checking if save button is clicked
}
}
countryData is the data that I get from the api, and this is the data I want to pass to SavedViewController.
struct CountryData: Codable {
let name : String
}
So on the SavedViewController, I'm handling the data using the SaveButtonProtocol conformance as below:
extension SavedViewController: SaveButtonProtocol {
func saveButtonClicked(with data: CountryData) {
countryDataArray.append(data)
print("saveButtonClicked")
print("countryData in savevc is \(countryDataArray)")
DispatchQueue.main.async {
self.countriesTableView.reloadData()
}
}
}
Whenever I click the star button on the first tab, this function is getting called on SavedViewController. So whenever I click to button, those print statements above work fine.
The problem is, whenever the star button is clicked, it should append the data of the current clicked cell to countryDataArray in SavedViewController. But the array is not populating as it should.
Let's say I pressed the first cell's star button, my print("countryData in savevc is (countryDataArray)") statement prints : ["Vatican City"], then I press the second cell's star button it only prints ["Ethiopia"] while it should print ["Vatican City", "Ethiopia"]
Why this issue is happening? My best guess is I'm delegating SavedViewController from the cell class so it behaves differently for every other cell. If that is the problem what should I do to solve it?
Many Thanks.
You should store your data in a shared (static array) object so you only have one source and add the saved indicator in your country struct so you do not rely on what is displayed in one view controller.

SwiftUI tap gesture blocks item deleting action in List

So I have a view with List, also this view has side menu. I added tapGesture to my VStack to dismiss side menu when it's open, but then I face issue, tapGesture is blocking onDelete method of List. Any ideas how to fix that??
Here is code example:
VStack {
.....
List {
ForEach(){
//list elements here
}
.onDelete {
// delete action here
}
}
}
.onTapGesture {
// action here
}
Also, if while deleting I swipe once till the end, it's working. But if I swipe just a little and try to press Delete button nothing happens.
Replace your .onTapGesture with the simultaneousGesture modifier.
.simultaneousGesture(TapGesture().onEnded {
// action here
})

Context Menu & Haptic touch in SwiftUI

I'm working on a new app using SwiftUI and I need some help in the context menu.
I want to know how I can add a custom preview for the context menu in SwiftUI?
& how I can group menu items in multiple groups & add children for any item in the menu?
also how I can make the delete button in red color? or change the colors for them?
another thing, how I can add a menu on the app icon to open a specific View or make an action like this:
In order to add a custom preview, you can use this https://developer.apple.com/documentation/swiftui/view/contextmenu(menuitems:preview:)
The preview should be something that conforms to View.
To split the items in multiple groups, just add a Divider() between the items.
In order to change the color to red for a Delete item, change the button role to .destructive as in the example below.
To add children to one item, use a Menu as below, but I don't think this approach is encouraged.
Here is an example that includes all the above.
.contextMenu {
Menu("This is a menu") {
Button {
doSomething()
} label: {
Text("Do something")
}
}
Button {
doSomethingAgain()
} label: {
Text("Something")
}
Divider()
Button(role: .destructive) {
performDelete()
} label: {
Label("Delete", systemImage: "trash")
}
} preview: {
Text("This is the preview") // you can add anything that conforms to View here
}

Resources