I can't seem to find what I'm doing wrong.

Here is the setting:

  1. bundle identifiers stored as a Set through AppStorage
  2. new bundle identifiers added through drop

Here's how the issue looks like:

  1. normal case: item selected, highlighted, and contextual delete available (which shows the item is selected)

enter image description here

  1. normal case: item is not selected, not highlighted, and contextual delete unavailable

enter image description here

  1. the issue: item is selected but NOT highlighted, and contextual available (which shows the item is selected)

enter image description here

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.

1

There are 1 best solutions below

0
On

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.