ios 通用框架_iOS通用搜索

ios 通用框架

In this tutorial we are going to implement a search that returns search results that will be used as filters. It can be very useful when narrowing or filtering down a list of content by applying multiple filters (chips). This is a common pattern seen when implementing Search. …

在本教程中,我们将实现一个搜索,该搜索返回将用作过滤器的搜索结果。 通过应用多个过滤器(芯片)来缩小或过滤内容列表时,此功能非常有用。 这是实现搜索时常见的模式。 …

Image for post

Let’s dive right in…

让我们潜入……

The idea is to show a list of filters when the use starts typing in the search bar. A filter will consist of some matching text and a category.

想法是当用户开始在搜索栏中输入内容时显示过滤器列表。 过滤器将包含一些匹配的文本和一个类别。

When the user taps on a search result we will create another chip as a filter. We will keep track of these with a SearchResult. A SearchResult contains the information we need later when applying them as filters to our list.

当用户点击搜索结果时,我们将创建另一个芯片作为过滤器。 我们将通过SearchResult跟踪这些信息。 SearchResult包含稍后将它们用作列表过滤器时所需的信息。

protocol SearchResult {    var searchText: String { get set }   
var
matchingText: String { get set }
var keyPath: AnyKeyPath { get set }
var categoryName: String { get } init(searchText: String,
matchingText: String,
keyPath: AnyKeyPath)
}

One little annoying thing is we have to wrap our SearchResult with another protocol. Otherwise later we get this compile-time error when we go to implement our concrete search:

一件令人讨厌的事情是,我们必须用另一种协议包装我们的SearchResult。 否则,稍后在执行具体搜索时会遇到此编译时错误:

Protocol ‘SearchResult’ can only be used as a generic constraint because it has Self or associated type requirements

协议“ SearchResult”只能用作一般约束,因为它具有“自我”或相关类型要求

protocol UniqueSearchResult: Hashable, SearchResult { }

Lets back our Generic Search with a protocol. This makes testing easier.

让我们通过协议返回通用搜索。 这使测试更加容易。

protocol SearchDefinition {    func linearSearch<Content, SearchResult: UniqueSearchResult     
(content: [Content],
searchString: String,
keyPaths: [AnyKeyPath],
resultType: SearchResult.Type,
completion: @escaping (_ results: [SearchResult]) -> Void)}

Here is the implementation of our SearchDefinition. We pass in an array of Content we want to be searched, the search string, the properties we want to search on that Content, and the result type.

这是我们SearchSearch的实现。 我们传入要搜索的Content数组,搜索字符串,我们要在该Content上搜索的属性以及结果类型。

struct GenericSearch: SearchDefinition {    func linearSearch<Content, SearchResult: UniqueSearchResult>.    
(content: [Content],
searchString: String,
keyPaths: [AnyKeyPath],
resultType: SearchResult.Type,
completion: @escaping ([SearchResult]) -> Void) { guard searchString.count > 0 else {
completion([])
return
} let searchStringLowercased = searchString.lowercased() var results: Set<SearchResult> = Set<SearchResult>() content.forEach { itemToSearch in keyPaths.forEach { prop in if let itemToSearch = itemToSearch[keyPath: prop]
as? String, itemToSearch.lowercased().contains(searchStringLowercased) { let result = SearchResult.init(searchText:
searchString, matchingText: itemToSearch,
keyPath: prop) results.insert(result)
}
}
} var resultsArray = Array(results) resultsArray.sort {
guard let first =
$0.matchingText.lowercased().range(of:
searchStringLowercased)?.lowerBound, let second =
$1.matchingText.lowercased().range(of:
searchStringLowercased)?.lowerBound else { return true
}
return first < second
} completion(resultsArray)
}
}

Now that we have our Generic Search we can create specific searches that specify the type of Content we want to perform the search on and the properties to search on that Content.

现在我们有了通用搜索,我们可以创建特定的搜索,以指定要在其上执行搜索的内容类型以及要在该内容上搜索的属性。

In this example we will use a list of Items that you can purchase:

在此示例中,我们将使用您可以购买的商品列表:

struct Item {
var name: String
var category: Category
var price: Double
var stores: [Store]
}struct Store {
var name: String
var categories: [Category]
}enum Category: String {
case toys
case clothes
case electronics
case housing
case jewelry
}

Next we will wrap our generic search with a concrete search class. This will define our result type as well as all the keypaths we want to search within our content.

接下来,我们将用具体的搜索类包装通用搜索。 这将定义我们的结果类型以及我们要在内容中搜索的所有关键路径。

protocol SearchService: class {    func search<Content>(content: [Content], 
with string: String,
completion: @escaping (_ results:
[SearchResult]) -> Void)}class ItemSearch: SearchService { private lazy var search: GenericSearch = {
GenericSearch()
}() func search<Content>(content: [Content],
with string: String,
completion: @escaping (_ results:
[SearchResult]) -> Void) { search.linearSearch(content: content,
searchString: string,
keyPaths: [\Item.name,
\Item.category.rawValue],
resultType: ItemSearchResult.self) {
results in
completion(results) }
}
}struct ItemSearchResult: UniqueSearchResult { var searchText: String
var matchingText: String
var keyPath: AnyKeyPath
var categoryName: String {
switch keyPath {
case \Item.name:
return "Item"
case \Item.category.rawValue:
return "Category"
default:
return ""
}
} init(searchText: String,
matchingText: String,
keyPath: AnyKeyPath) { self.searchText = searchText
self.matchingText = matchingText
self.keyPath = keyPath
}
}

Now let’s use our Search:

现在,让我们使用搜索:

class SearchViewController: UITableViewController {    private var searchController: UISearchController!

private let search = ItemSearch() weak var dataSource: SearchDataSource? weak var delegate: SearchResultsDelegate? ...}extension SearchViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { guard let searchString = searchController.searchBar.text,
let dataSource = self.dataSource else { return } self.search.search(content: dataSource.items,
with: searchString,
completion: { [weak self] results in /// Update Chips and apply filters aka results
self
.delegate.reload(searchResults: results) })
}
}

This all works great! If and only if the properties we are including in our keyPaths are Strings. If we want to search inside an array or another type that contains an array we need to do a little more work.

这一切都很棒! 当且仅当我们包含在keyPaths中的属性是字符串。 如果要在数组或包含数组的其他类型中进行搜索,我们需要做更多的工作。

Lets update our SearchResult by replacing our KeyPath with a recursive enum:

让我们通过用递归枚举替换KeyPath来更新SearchResult:

indirect enum SearchPath {    case path(currentLevel: AnyKeyPath, nestedLevel: SearchPath?)    var currentLevel: AnyKeyPath {
switch self {
case .path(let currentLevel, _):
return currentLevel
}
} var nestedLevel: SearchPath? {
switch self {
case .path(_, let nestedLevel):
return nestedLevel
}
}
}extension SearchPath: Hashable {
static func == (lhs: SearchPath, rhs: SearchPath) -> Bool {
return lhs.currentLevel == rhs.currentLevel
&& lhs.nestedLevel == rhs.nestedLevel
}
}protocol SearchResult { var searchText: String { get set }
var matchingText: String { get set }
var searchPath: SearchPath { get set }
var categoryName: String { get } init(searchText: String,
matchingText: String,
searchPath: SearchPath)
}

Above we used an indirect enum which allows us to have nested key paths! Shout out to my co-worker Ben Hakes for suggesting the indirect enum.

上面我们使用了一个间接枚举,该枚举允许我们拥有嵌套的键路径! 向我的同事本·哈克斯(Ben Hakes)大声疾呼建议间接枚举。

Now we can update our Generic Search with our new SearchPath:

现在,我们可以使用新的SearchPath更新通用搜索:

protocol SearchDefinition {    func search<Content, SearchResult: UniqueSearchResult>(content: [Content], searchString: String,
searchPaths: [SearchPath],
resultType: SearchResult.Type,
completion: @escaping (_ results: [SearchResult]) -> Void)}struct GenericSearch: SearchDefinition { func search<Content, SearchResult: UniqueSearchResult>(content: [Content], searchString: String,
searchPaths: [SearchPath],
resultType: SearchResult.Type,
completion: @escaping ([SearchResult]) -> Void) { if searchString == "" {
completion([])
return
} let searchStringLowercased = searchString.lowercased() var results: Set<SearchResult> = Set<SearchResult>() content.forEach { itemToSearch in searchPaths.forEach { prop in self.search(itemToSearch: itemToSearch,
searchString: searchString,
originalSearchPath: prop,
searchPath: prop,
results: &results)
}
} var resultsArray = Array(results)
resultsArray.sort {
guard let first =
$0.matchingText.lowercased().range(of:
searchStringLowercased)?.lowerBound, let second =
$1.matchingText.lowercased().range(of:
searchStringLowercased)?.lowerBound else { return true
}
return first < second
} completion(resultsArray)
} private func search<Content, SearchResult: UniqueSearchResult>
(itemToSearch: Content,
searchString: String,
originalSearchPath: SearchPath,
searchPath: SearchPath,
results: inout Set<SearchResult>) { let searchStringLowercased = searchString.lowercased() guard let nestedLevel = searchPath.nestedLevel else {
if let itemToSearch = itemToSearch[keyPath:
searchPath.currentLevel] as? String,
itemToSearch.lowercased().contains(searchStringLowercased){ let result = SearchResult.init(searchText:
searchString,
matchingText: itemToSearch,
searchPath: originalSearchPath) results.insert(result)
}
return
} guard let nextItems = itemToSearch[keyPath:
searchPath.currentLevel] as? [Any] else { return } nextItems.forEach { nextItemToSearch in
return
search(itemToSearch: nextItemToSearch,
searchString: searchString,
originalSearchPath: originalSearchPath,
searchPath: nestedLevel,
results: &results)
}
}
}

We will need new filter logic to handle the nested SearchPaths when applying our results (chips):

应用结果(芯片)时,我们将需要新的过滤器逻辑来处理嵌套的SearchPath:

protocol FilterService: class {    func apply<Content>(filter: SearchResult, 
to content: [Content]) -> [Content]
}class TextFilter: FilterService { typealias SearchFilter = SearchResult func apply<Content>(filter: SearchFilter,
to content: [Content]) -> [Content] { let filteredContent = content.filter { item in
return
applyFilter(path: filter.searchPath,
value: filter.matchingText.lowercased(),
itemToSearch: item)
} return filteredContent
} private func applyFilter<Content>(path: SearchPath,
value: String,
itemToSearch: Content) -> Bool { guard let nestedLevel = path.nestedLevel else {
if let itemToSearch = itemToSearch[keyPath:
path.currentLevel] as? String,
itemToSearch.lowercased() == value {
return true
}
return false
} guard let nextItems = itemToSearch[keyPath:
path.currentLevel] as? [Any] else { return false } return nextItems.contains { nextItemToSearch -> Bool in
return
applyFilter(path: nestedLevel,
value: value,
itemToSearch: nextItemToSearch)
}
}
}

Now we handle nested structures like ‘Item’ above. We have everything we need to search through stores. We can update our ItemSearch to the following:

现在,我们处理上面的“ Item”之类的嵌套结构。 我们拥有搜索商店所需的一切。 我们可以将ItemSearch更新为以下内容:

class ItemSearch: SearchService {    private lazy var search: GenericSearch = {
GenericSearch()
}() func search<Content>(content: [Content],
with string: String,
completion: @escaping (_ results:
[SearchResult]) -> Void) { search.linearSearch(content: content,
searchString: string,
keyPaths: [.path(currentLevel: \Item.name,
nestedLevel: nil),
.path(currentLevel:
\Item.category.rawValue],
nestedLevel: nil),
.path(currentLevel: \Item.stores,
nestedLevel: \Store.name),
resultType: ItemSearchResult.self) { results in
completion(results) }
}
}

As you can see in the above example we can access collections via our new SearchPath enum:

如上例所示,我们可以通过新的SearchPath枚举访问集合:

.path(currentLevel: AnyKeyPath, nestedLevel: SearchPath?)

And that's it! We now have a generic, reusable Search that can be used to create multiple filters on a list. Simply wrap the Generic Search with your own concrete struct or class and define they keypaths and SearchResult for your data. Please feel free to take this and modify it to fit your needs in your next project.

就是这样! 现在,我们有了一个通用的,可重复使用的搜索,该搜索可用于在列表上创建多个过滤器。 只需使用您自己的具体结构或类包装通用搜索,然后为您的数据定义它们的关键路径和SearchResult。 请随意接受并对其进行修改,以适合您的下一个项目。

翻译自: https://medium.com/swlh/ios-generic-search-5e3c29589708

ios 通用框架

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值