I can't seem to find what I'm doing wrong.
Here is the setting:
- bundle identifiers stored as a Set through AppStorage
- new bundle identifiers added through drop
Here's how the issue looks like:
- normal case: item selected, highlighted, and contextual delete available (which shows the item is selected)
- normal case: item is not selected, not highlighted, and contextual delete unavailable
- the issue: item is selected but NOT highlighted, and contextual available (which shows the item is selected)
The issue happens maybe 80% of the time, not always. It happens when I add a new item to the list, and I select this item without clicking anywhere else within the List first. If I click on another item first, or in a space without item WITHIN the list, then selecting the new item after will work properly and this item will get highlighted. This makes me think that this could be a state issue? But I cannot find where I am doing wrong.
Here is the code:
import SwiftUI
// to save Sets through AppStorage
extension Set: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8) else { return nil }
guard let result = try? JSONDecoder().decode(Set<Element>.self, from: data) else { return nil}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self) else { return "[]" }
guard let result = String(data: data, encoding: .utf8) else { return "[]" }
return result
}
}
struct ContentView: View {
@AppStorage("apps") var apps: Set<String> = []
@State private var appsSelection: Set<String> = []
var body: some View {
Form {
List(Array(apps), id: \.self, selection: $appsSelection) { app in
Text(app)
}
.contextMenu {
Button("Delete") {
for selection in appsSelection {
apps.remove(selection)
}
}
.disabled(appsSelection.isEmpty)
}
.onDeleteCommand {
for selection in appsSelection {
apps.remove(selection)
}
}
.listStyle(.bordered(alternatesRowBackgrounds: true))
.onDrop(of: [.fileURL], delegate: AppsDropDelegate(apps: $apps))
}
}
}
// the DropDelegate
private struct AppsDropDelegate: DropDelegate {
@Binding var apps: Set<String>
func validateDrop(info: DropInfo) -> Bool {
guard info.hasItemsConforming(to: [.fileURL]) else { return false }
let providers = info.itemProviders(for: [.fileURL])
var result = false
for provider in providers {
if provider.canLoadObject(ofClass: URL.self) {
let group = DispatchGroup()
group.enter()
_ = provider.loadObject(ofClass: URL.self) { url, _ in
let itemIsAnApplicationBundle = try? url?.resourceValues(forKeys: [.contentTypeKey]).contentType == .applicationBundle
result = result || (itemIsAnApplicationBundle ?? false)
group.leave()
}
_ = group.wait(timeout: .now() + 0.5)
}
}
return result
}
func performDrop(info: DropInfo) -> Bool {
let providers = info.itemProviders(for: [.fileURL])
var result = false
for provider in providers {
if provider.canLoadObject(ofClass: URL.self) {
let group = DispatchGroup()
group.enter()
_ = provider.loadObject(ofClass: URL.self) { url, _ in
let itemIsAnApplicationBundle = (try? url?.resourceValues(forKeys: [.contentTypeKey]).contentType == .applicationBundle) ?? false
if itemIsAnApplicationBundle, let url = url, let app = Bundle(url: url), let bundleIdentifier = app.bundleIdentifier {
DispatchQueue.main.async {
apps.insert(bundleIdentifier)
}
result = result || true
}
group.leave()
}
_ = group.wait(timeout: .now() + 0.5)
}
}
return result
}
}
Thanks a ton in advance.
Ok so after more tests, it does seem that this is a SwiftUI bug. Probably something to do with the caching mechanism of @AppStorage, as it doesn't write/read from the disk on the fly. SwiftUI sometimes publishes the updates, sometimes it doesn't. This has been reported to Apple as FB10029588. Sample project here: https://github.com/godbout/ListDrop.
The way to go around this is to update the selection manually. To do that I've created an
@ObservableObject
that takes care of the items List through @AppStorage, and of the selection through a @Published property. I set the property to be empty manually when I delete the selection. This does solve the problem. Next time items are added, they are highlighted when selected.