{"id":18763749,"url":"https://github.com/dmytro-anokhin/autocomplete","last_synced_at":"2025-07-16T07:33:22.279Z","repository":{"id":134610652,"uuid":"408962986","full_name":"dmytro-anokhin/Autocomplete","owner":"dmytro-anokhin","description":"Autocomplete for a text field in SwiftUI using async/await","archived":false,"fork":false,"pushed_at":"2021-09-24T14:49:36.000Z","size":133,"stargazers_count":17,"open_issues_count":1,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-12T17:49:25.455Z","etag":null,"topics":["swift","swiftui","swiftui-demo","swiftui-example","swiftui-learning"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmytro-anokhin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-09-21T20:20:33.000Z","updated_at":"2024-09-01T09:54:55.000Z","dependencies_parsed_at":"2023-08-15T07:45:55.193Z","dependency_job_id":null,"html_url":"https://github.com/dmytro-anokhin/Autocomplete","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dmytro-anokhin/Autocomplete","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2FAutocomplete","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2FAutocomplete/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2FAutocomplete/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2FAutocomplete/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmytro-anokhin","download_url":"https://codeload.github.com/dmytro-anokhin/Autocomplete/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmytro-anokhin%2FAutocomplete/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265492353,"owners_count":23776043,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["swift","swiftui","swiftui-demo","swiftui-example","swiftui-learning"],"created_at":"2024-11-07T18:27:18.425Z","updated_at":"2025-07-16T07:33:22.242Z","avatar_url":"https://github.com/dmytro-anokhin.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Autocomplete for SwiftUI using async/await and actors\n\nWith [Swift 5.5 released](https://swift.org/blog/swift-5-5-released/) I want to offer a look how new [concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html) model can be used to create autocomplete feature in SwiftUI.\n\n---\n\nText autocomplete is a common feature that typically involves database lookup or networking. This operations must be asynchronous, not to block user input, and can include in-memory cache to speedup repeated lookups. This usecase is perfect to battle test new Swift concurrency model.\n\nLet's say we have an app that can show information about a city. When user types city in a `TextField` and we want to offer autocomplete suggestions.\n\nThis is our UI.\n\n\u003cimg src=\"https://user-images.githubusercontent.com/5136301/134250533-0c20f55c-b1b2-4b0b-9d57-8036d77cfb4b.png\" data-canonical-src=\"https://user-images.githubusercontent.com/5136301/134250533-0c20f55c-b1b2-4b0b-9d57-8036d77cfb4b.png\" width=\"375\"/\u003e\n\nHere is SwiftUI code that hardcodes suggestions for a prototype.\n\n```swift\nstruct ContentView: View {\n\n    private var suggestions = [\"Amstelveen\", \"Amsterdam\", \"Amsterdam-Zuidoost\", \"Amstetten\"]\n\n    @State var input: String = \"\"\n\n    var body: some View {\n        VStack {\n            TextField(\"\", text: $input)\n                .textFieldStyle(.roundedBorder)\n                .padding()\n        }\n        List(suggestions, id: \\.self) { suggestion in\n            ZStack {\n                Text(suggestion)\n            }\n            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)\n        }\n    }\n}\n```\n\n---\n\nSuggestions can come from a server or bundled with the app. For simplicity, in the example we store suggestions as a plain text (`cities` file), where each city name separated with a newline.\n\n```\n...\nAmstelveen\nAmsterdam\nAmsterdam-Zuidoost\nAmstetten\n...\n```\n\nTo load the file in memory we use `CitiesSource` protocol and `CitiesFile` object that implements it. You may choose not to declare a protocol and use an object directly. But I find that having a protocol creates simple to understand abstraction, further useful for unit testing.\n\n```swift\nprotocol CitiesSource {\n\n    func loadCities() -\u003e [String]\n}\n\nstruct CitiesFile: CitiesSource {\n\n    let location: URL\n\n    init(location: URL) {\n        self.location = location\n    }\n\n    /// Looks up for `cities` file in the main bundle\n    init?() {\n        guard let location = Bundle.main.url(forResource: \"cities\", withExtension: nil) else {\n            assertionFailure(\"cities file is not in the main bundle\")\n            return nil\n        }\n\n        self.init(location: location)\n    }\n\n    func loadCities() -\u003e [String] {\n        do {\n            let data = try Data(contentsOf: location)\n            let string = String(data: data, encoding: .utf8)\n            return string?.components(separatedBy: .newlines) ?? []\n        }\n        catch {\n            return []\n        }\n    }\n}\n```\n\nNext we need to build a cache. In our example `CitiesCache` keeps the complete list of cities in-memory. For a real app you should consider creating something smarter. We, instead, focus on concurrency. A good cache should be thread-safe. This is where new Swift concurrency model comes to life. \n\n`CitiesCache` is an `actor`. [Actor](https://github.com/apple/swift-evolution/blob/main/proposals/0306-actors.md) protects its own data, ensuring that only a single thread will access that data at a given time. Precisely what we need.\n\n```swift\nactor CitiesCache {\n\n    let source: CitiesSource\n\n    init(source: CitiesSource) {\n        self.source = source\n    }\n\n    var cities: [String] {\n        if let cities = cachedCities {\n            return cities\n        }\n\n        let cities = source.loadCities()\n        cachedCities = cities\n\n        return cities\n    }\n\n    private var cachedCities: [String]?\n}\n```\n\n`CitiesCache` stores the list of cities in `cachedCities`, loaded lazily on first access to computed `cities` property.\n\nCache lookup is a straight forward enumeration comparing prefixes. In the example we only do case-insensitive comparison. The real app may want more greedy algorithm.\n\n```swift\nextension CitiesCache {\n\n    func lookup(prefix: String) -\u003e [String] {\n        let lowercasedPrefix = prefix.lowercased()\n        return cities.filter { $0.lowercased().hasPrefix(lowercasedPrefix) }\n    }\n}\n```\n\nNotice a thing: so far there is not a line of synchronization code that we wrote. Actors allow only one task to access their state at a time. So we don't need to worry about.\n\n---\n\nPieces are almost ready to connect. One small autocomplete feature to consider is a slight delay between user input and autocomplete routine, to limit number of calls. This is especially useful if autocomplete extensively uses I/O, like database lookup or sending network requests.\n\n`AutocompleteObject` object implements autocomplete and notifies SwiftUI using `@Published var suggestions: [String]` property. To execute autocomplete asynchronously we use [`Task`](https://developer.apple.com/documentation/swift/task), new in Swift Standard Library. A `Task` can execute concurrent routines and supports cancellation.\n\nYou can also notice that `AutocompleteObject` uses [`@MainActor`](https://developer.apple.com/documentation/swift/mainactor) to always execute its code on the main thread.\n\nImportant that asyncronous calls, such as `Task.sleep` to add delay, and using `CitiesCache` actor, are marked with `await`. What it does, is indicates that the routine must stop and wait for asynchronous subroutine (marked with `async` keyword) to complete. You may previously used semaphores or `asyncAndWait` in GCD to achieve similar behaviour. The difference is that `await` won't block calling thread and simply return execution when `async` subroutine completes. Even that `AutocompleteObject` always uses the main thread, `await Task.sleep`  won't block it.\n\n```swift\n@MainActor\nfinal class AutocompleteObject: ObservableObject {\n\n    let delay: TimeInterval = 0.3\n\n    @Published var suggestions: [String] = []\n\n    init() {\n    }\n\n    private let citiesCache = CitiesCache(source: CitiesFile()!)\n\n    private var task: Task\u003cVoid, Never\u003e?\n\n    func autocomplete(_ text: String) {\n        guard !text.isEmpty else {\n            suggestions = []\n            task?.cancel()\n            return\n        }\n\n        task?.cancel()\n\n        task = Task {\n            await Task.sleep(UInt64(delay * 1_000_000_000.0))\n\n            guard !Task.isCancelled else {\n                return\n            }\n\n            let newSuggestions = await citiesCache.lookup(prefix: text)\n\n            if isSuggestion(in: suggestions, equalTo: text) {\n                // Do not offer only one suggestion same as the input\n                suggestions = []\n            } else {\n                suggestions = newSuggestions\n            }\n        }\n    }\n\n    private func isSuggestion(in suggestions: [String], equalTo text: String) -\u003e Bool {\n        guard let suggestion = suggestions.first, suggestions.count == 1 else {\n            return false\n        }\n\n        return suggestion.lowercased() == text.lowercased()\n    }\n}\n```\n\n---\n\nInside the view we create `AutocompleteObject` and observe its `suggestions` property. When `input` changes we call `autocomplete` function and the property will update.\n\n```swift\nstruct ContentView: View {\n\n    @ObservedObject private var autocomplete = AutocompleteObject()\n\n    @State var input: String = \"\"\n\n    var body: some View {\n        VStack {\n            TextField(\"\", text: $input)\n                .textFieldStyle(.roundedBorder)\n                .padding()\n                .onChange(of: input) { newValue in\n                    autocomplete.autocomplete(input)\n                }\n        }\n        List(autocomplete.suggestions, id: \\.self) { suggestion in\n            ZStack {\n                Text(suggestion)\n            }\n            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)\n            .onTapGesture {\n                input = suggestion\n            }\n        }\n    }\n}\n```\n\nI hope you find this example useful.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmytro-anokhin%2Fautocomplete","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmytro-anokhin%2Fautocomplete","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmytro-anokhin%2Fautocomplete/lists"}