Search code examples
swiftstringmacosfilteridentifiable

Filter elements of a String variable contained in an Identifiable


I'm coding a Swift application for macOS. I'm a beginner in Swift so my problem might seem stupid to you, I apologize for that...

I have a type containing the following two String variables:

struct ShortcutInfo: Identifiable {
    let id: String
    let name: String
}

The variable I use the most is "name", containing a list of texts.

I tried to filter the list with a TextField. The filtered list should appear in the Picker.

@State var filteredShortcuts = ShortcutInfo(id: "", name: "").name.filter($0.lowercased().contains(searchText.lowercased()))

// body
TextField("", text: $searchText)
                        
Picker("", selection: $selection) {
    ForEach(filteredShortcuts) { item in
        Text(item)
    }
}

But I get the error "Cannot use instance member 'searchText' within property initializer; property initializers run before 'self' is available". What is wrong in my code?

Full code:

@State var shortcuts = [ShortcutInfo]()
@State var selection = ""
@State var searchText: String = ""
@State var filteredShortcuts = ShortcutInfo(id: "", name: "").name.filter($0.lowercased().contains(searchText.lowercased()))

// body
TextField("", text: $searchText)
                        
Picker("", selection: $action1a) {
    ForEach(filteredShortcuts) { item in
        Text(item)
    }
}
.task {
     let shortcuts = (try? await loadShortcutNames()) ?? []
     self.shortcuts = shortcuts
     selection = shortcuts.first?.id ?? ""
}
//

func loadShortcutNames() async throws -> [ShortcutInfo] {
    let process = Process()
    process.executableURL = URL(filePath: "/usr/bin/shortcuts")

    process.arguments = ["list", "--show-identifiers"]
    let pipe = Pipe()
    process.standardOutput = pipe
    return try await withTaskCancellationHandler {
        try process.run()
        return try await pipe.fileHandleForReading.bytes.lines.compactMap { line in
            guard let (_, name, id) = line.wholeMatch(of: /(.*) \(([A-Z0-9-]*)\)/)?.output else {
                return nil
            }
            return ShortcutInfo(id: String(id), name: String(name))
        }
        .reduce(into: []) { $0.append($1) }
    } onCancel: {
        process.terminate()
    }
}

struct ShortcutInfo: Identifiable {
    let id: String
    let name: String
}

extension Binding where Value == Int {
    var toString: Binding<String> {
        Binding<String>(
            get: {
                "\(wrappedValue)"
            },
            set: {
                wrappedValue = Int($0) ?? 0
            }
        )
    }  
}

In addition, I am not sure of the validity of my code regarding the display of the list in the Picker.

Thanks in advance.


Solution

  • You cannot filter the text on the top level, you have to filter the text for example in the onChange modifier

    struct ContentView: View {
        
        @State private var shortcuts = [ShortcutInfo]()
        @State private var selection = "None"
        @State private var searchText: String = ""
        @State private var filteredShortcuts = [ShortcutInfo]()
        
        var body: some View {
            VStack {
                TextField("", text: $searchText)
                
                Picker("", selection: $selection) {
                    Text("Nothing selected").tag("None")
                    ForEach(filteredShortcuts) { Text($0.name) }                   
                }
                .onChange(of: searchText) { _, newValue in
                    filteredShortcuts = shortcuts.filter{$0.name.localizedStandardContains(searchText)}
                    selection = filteredShortcuts.first?.id ?? "None"
                }
            }
            .task {
                self.shortcuts = (try? await loadShortcutNames()) ?? []
                selection = "None"
            }
        }
        
        func loadShortcutNames() async throws -> [ShortcutInfo] {
            let process = Process()
            process.executableURL = URL(filePath: "/usr/bin/shortcuts")
            
            process.arguments = ["list", "--show-identifiers"]
            let pipe = Pipe()
            process.standardOutput = pipe
            return try await withTaskCancellationHandler {
                try process.run()
                return try await pipe.fileHandleForReading.bytes.lines.compactMap { line in
                    guard let (_, name, id) = line.wholeMatch(of: /(.*) \(([A-Z0-9-]*)\)/)?.output else {
                        return nil
                    }
                    return ShortcutInfo(id: String(id), name: String(name))
                }
                .reduce(into: []) { $0.append($1) }
            } onCancel: {
                process.terminate()
            }
        }
    }