【问题标题】:Problem trying to add Search bar for simple SwiftUI app retrieving web data尝试为简单的 SwiftUI 应用程序检索 Web 数据添加搜索栏时出现问题
【发布时间】:2022-06-15 00:23:24
【问题描述】:

我有一个小项目,它是 Swift UI 练习的扩展,从 Greg Lim 的《Beginning Swift UI》一书中对 Github 进行网络调用: https://github.com/ethamoos/GitProbe

我一直在使用它来练习基本技能,并尝试添加其他可能在实际应用中有用的功能。

与最初的练习相比,我的主要变化是添加了选择要查找的用户的选项(这以前是硬编码的)并允许用户输入。因为这可以返回大量数据,所以我现在想让结果 List .searchable 以便用户可以过滤结果。

我一直在这里学习本教程: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-a-search-bar-to-filter-your-data

但我意识到这是基于返回的数据是字符串,因此搜索是一个字符串。

我返回的 JSON 解码为用户数据对象列表,因此无法直接搜索。我假设我可以调整它以匹配针对我的自定义对象的字符串搜索,但我不确定如何执行此操作。

为了让你明白我的意思是代码:

import SwiftUI
import URLImage


struct Result: Codable {
    let totalCount: Int
    let incompleteResults: Bool
    let items: [User]
    
    enum CodingKeys: String, CodingKey {
        case totalCount = "total_count"
        case incompleteResults = "incomplete_results"
        case items
    }
}

struct User: Codable, Hashable {
    let login: String
    let id: Int
    let nodeID: String
    let avatarURL: String
    let gravatarID: String
    
    enum CodingKeys: String, CodingKey {
        case login, id
        case nodeID = "node_id"
        case avatarURL = "avatar_url"
        case gravatarID = "gravatar_id"
    }
}


class FetchUsers: ObservableObject {
    @Published var users = [User]()
    
    func search(for user:String) {
        var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
        urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
        guard let url = urlComponents.url else {
            return
        }
        URLSession.shared.dataTask(with: url) {(data, response, error) in
            do {
                if let data = data {
                    let decodedData = try JSONDecoder().decode(Result.self, from: data)
                    DispatchQueue.main.async {
                        self.users = decodedData.items
                    }
                } else {
                    print("No data")
                }
            } catch {
                print("Error: \(error)")
            }
        }.resume()
    }
}

struct ContentView: View {
    @State var username: String = ""
    
    var body: some View {
        NavigationView {
            
            Form {
                Section {
                    Text("Enter user to search for")
                    TextField("Enter your username", text: $username).disableAutocorrection(true)
                        .autocapitalization(.none)
                }
                NavigationLink(destination: UserView(username: username)) {
                    Text("Show detail for \(username)")
                }
            }
        }
    }
}

struct UserView: View {
    
    @State var username: String
    @ObservedObject var fetchUsers = FetchUsers()
    @State var searchText = ""
    
    var body: some View {
        List {
            ForEach(fetchUsers.users, id:\.self) { user in
                NavigationLink(user.login, destination: UserDetailView(user:user))
            }
        }.onAppear {
            self.fetchUsers.search(for: username)
        }
        .searchable(text: $searchText)
        .navigationTitle("Users")
        
    }

/// With suggestion added 


    /// The search results
    private var searchResults: [User] {
        if searchText.isEmpty {
            return fetchUsers.users // your entire list of users if no search input
        } else {
            return fetchUsers.search(for: searchText) // calls your search method passing your search text
        }
    }
}

struct UserDetailView: View {
    
    var user: User
    
    var body: some View {
        Form {
            Text(user.login).font(.headline)
            Text("Git iD = \(user.id)")
            URLImage(URL(string:user.avatarURL)!){ image in
                image.resizable().frame(width: 50, height: 50)
            }
        }
    }
}

对此的任何帮助将不胜感激。

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    您的 UserListView 构造不正确。我不明白为什么你需要一个里面有空文本的 ScrollView?我删除了它。

    所以我从 View 中删除了 searchText 到 FetchUsers 类,这样我们就可以延迟服务器请求,从而避免不必要的多次调用。请根据您的需要调整它(检查Apple's Debounce documentation。现在一切都应该按预期工作。

    import Combine
    
    class FetchUsers: ObservableObject {
        
        @Published var users = [User]()
        
        @Published var searchText = ""
        
        var subscription: Set<AnyCancellable> = []
        
        init() {
            $searchText
                .debounce(for: .milliseconds(500), scheduler: RunLoop.main) // debounces the string publisher, delaying requests and avoiding unnecessary calls.
                .removeDuplicates()
                .map({ (string) -> String? in
                    if string.count < 1 {
                        self.users = [] // cleans the list results when empty search
                        return nil
                    }
                    return string
                }) // prevents sending numerous requests and sends nil if the count of the characters is less than 1.
                .compactMap{ $0 } // removes the nil values
                .sink { (_) in
                    //
                } receiveValue: { [self] text in
                    search(for: text)
                }.store(in: &subscription)
        }
        
        func search(for user:String) {
            var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
            urlComponents.queryItems = [URLQueryItem(name: "q", value: user.lowercased())]
            guard let url = urlComponents.url else {
                return
            }
            
            URLSession.shared.dataTask(with: url) {(data, response, error) in
                
                guard error == nil else {
                    print("Error: \(error!.localizedDescription)")
                    return
                }
                
                guard let data = data else {
                    print("No data received")
                    return
                }
                
                do {
                    let decodedData = try JSONDecoder().decode(Result.self, from: data)
                    DispatchQueue.main.async {
                        self.users = decodedData.items
                    }
                } catch {
                    print("Error: \(error)")
                }
            }.resume()
        }
    }
    
    struct UserListView: View {
        
        @State var username: String
        @ObservedObject var fetchUsers = FetchUsers()
    
        var body: some View {
            
            NavigationView {
                List {
                    ForEach(fetchUsers.users, id:\.self) { user in
                        NavigationLink(user.login, destination: UserDetailView(user:user))
                    }
                }
                .searchable(text: $fetchUsers.searchText) // we move the searchText to fetchUsers
                .navigationTitle("Users")
            }
        }
    }
    

    我希望这会有所帮助! :)

    【讨论】:

    • 非常感谢您的回答。不过,我认为我的代码中仍有一些问题。为了更容易,我将整个代码压缩到一个屏幕中。我已经更新了原帖。
    • 我返回的错误是:Cannot convert return expression of type '()' to return type '[User]' 所以我认为搜索的构建方式仍然存在问题?
    • 嗯……您的搜索是直接更新用户列表,而不是返回值。更改方法以返回用户数组。
    • 您介意说明一下您的意思吗?当我尝试让搜索功能返回用户时,我得到的是:Unexpected non-void return value in void function
    • 嘿,我更新了我的答案,并包含了 Combine 以延迟您对服务器的请求并仍然正确接收您的数据。与搜索相关的所有内容都在 FetchUsers 中,应该可以正常工作。
    【解决方案2】:

    最后,我想我已经解决了这个问题 - 感谢 Andre 的建议。

    我需要正确过滤我的数据,然后返回剩余部分。

    这是更正(删节)的版本:

    import SwiftUI
    import URLImage
    
    
    struct Result: Codable {
        let totalCount: Int
        let incompleteResults: Bool
        let items: [User]
        
        enum CodingKeys: String, CodingKey {
            case totalCount = "total_count"
            case incompleteResults = "incomplete_results"
            case items
        }
    }
    
    struct User: Codable, Hashable {
        let login: String
        let id: Int
        let nodeID: String
        let avatarURL: String
        let gravatarID: String
        
        enum CodingKeys: String, CodingKey {
            case login, id
            case nodeID = "node_id"
            case avatarURL = "avatar_url"
            case gravatarID = "gravatar_id"
        }
    }
    
    class FetchUsers: ObservableObject {
        @Published var users = [User]()
        
        func search(for user:String) {
            var urlComponents = URLComponents(string: "https://api.github.com/search/users")!
            urlComponents.queryItems = [URLQueryItem(name: "q", value: user)]
            guard let url = urlComponents.url else {
                return
    //            print("error")
            }
            URLSession.shared.dataTask(with: url) {(data, response, error) in
                do {
                    if let data = data {
                        let decodedData = try JSONDecoder().decode(Result.self, from: data)
                        DispatchQueue.main.async {
                            self.users = decodedData.items
                        }
                    } else {
                        print("No data")
                    }
                } catch {
                    print("Error: \(error)")
                }
            }.resume()
        }
    }
    
    struct ContentView: View {
        @State var username: String = ""
        
        var body: some View {
            NavigationView {
                
                Form {
                    Section {
                        Text("Enter user to search for")
                        TextField("Enter your username", text: $username).disableAutocorrection(true)
                            .autocapitalization(.none)
                    }
                    NavigationLink(destination: UserView(username: username)) {
                        Text("Show detail for \(username)")
                    }
                }
            }
        }
    }
    
    struct UserView: View {
        
        @State var username: String
        @ObservedObject var fetchUsers = FetchUsers()
        @State var searchText = ""
        
        var body: some View {
            List {
                ForEach(searchResults, id:\.self) { user in
                    NavigationLink(user.login, destination: UserDetailView(user:user))
                }
            }.onAppear {
                self.fetchUsers.search(for: username)
            }
            .searchable(text: $searchText)
            .navigationTitle("Users")
        }
        
        var searchResults: [User] {
             if searchText.isEmpty {
                 print("Search is empty")
                 return fetchUsers.users
             } else {
                 print("Search has a value - is filtering")
                 return fetchUsers.users.filter { $0.login.contains(searchText) }
             }
         }
    }
    
    struct UserDetailView: View {
        
        var user: User
        
        var body: some View {
            Form {
                Text(user.login).font(.headline)
                Text("Git iD = \(user.id)")
                URLImage(URL(string:user.avatarURL)!){ image in
                    image.resizable().frame(width: 50, height: 50)
                }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-01-23
      • 1970-01-01
      • 1970-01-01
      • 2016-06-18
      • 2010-11-15
      • 2010-12-07
      • 1970-01-01
      相关资源
      最近更新 更多