複数の単語の検索をSwiftで実装する

SmartNews風のアプリを作成しているのですが、複数の単語での記事検索機能を実装したので、そのコードの解説を紹介します。
ポイントはreduceとfilterの組み合わせです。


```SearchViewController.swift

var preArticleArray:[ArticleQueryData] = [] //データベースからfetchした全記事情報を入れる配列
var searchArticleArray:[ArticleQueryData] = [] //検索によるフィルタリング後の記事情報を入れる配列
var conditions = [(ArticleQueryData) -> Bool]() //複数単語の検索で使用

//省略
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    searchArticleArray.removeAll()
    self.view.endEditing(true)
        
    if let word = searchBar.text {
        if(word == "") {
            return
        } else if word.contains(" ") { //検索窓に入力された文字列を、半角スペース分割する。
            let words:[String] = word.components(separatedBy: " ")
            searchFetchArticleData(searchWords:words)
            SVProgressHUD.show()

        } else {
            searchFetchArticleData(searchWords:[word]) 
            SVProgressHUD.show()
        }
    }
}



func searchFetchArticleData(searchWords:[String]) {
    if let user = Auth.auth().currentUser {
        //Firestoreの初期化、reference先を変数に格納
        let ref = Firestore.firestore().collection("articleData")
        let uid = user.uid
        
        //.addSnapshotListenerを使うと"いいね"などの更新をキャッチできます。
        ref.addSnapshotListener { querySnapshot, err in
            if let err = err {
                print("Error fetching documents: \(err)")
            } else {
                
                self.searchArticleArray = [] //fetchする前に空にする
                self.preArticleArray = [] //こっちも空にしないと、fetchするたびに増えていってしまう。
                self.conditions = [(ArticleQueryData) -> Bool]() //conditionsも初期化しないと、２回目以降の検索がうまくいかなくなる。

                for document in querySnapshot!.documents {
                    let articleData = ArticleQueryData(snapshot: document, myId: uid)
                    //全記事データがpreArticleArrayに格納される
                    self.preArticleArray.insert(articleData, at: 0)
                }

                //$0にはconditions:[(ArticleQueryData) -> Bool]のelement、すなわちArticleQueryData型のelementが入ることになります。
                //なお、summaryもtagsもtitleStrも記事情報のpropertyで、String型です。
                for word in searchWords {
                    self.conditions.append { $0.titleStr!.contains(word) || $0.summary!.contains(word) || $0.tags!.contains(word) }
                }
                
                //$0には初めtrueが初期値として設定されています。$1にはconditionsの戻り値の要素（Bool値）が順々に入っていくことになります。
                //conditionsにはsearchWords.countの数だけBool値があり、それがreduceによって畳み込まれます。つまり、全てtrueならtrueとなりfilterを通過し、1つでもfalseならfalseとなり弾かれます。
                self.searchArticleArray = self.preArticleArray.filter { article in
                    self.conditions.reduce(true) { $0 && $1 (article) }
                }
                
                if self.searchArticleArray.isEmpty {
                        self.resultLabel.text = "検索条件に一致する記事は見つかりませんでした。"
                }
                
                //中略
                
                SVProgressHUD.dismiss()                
            }
        }    
    }
}      
```

複数検索の実装にあたり、参考にしたのは、こちらの記事[[Swift] 複数のOR条件をAND検索してArrayをフィルタリングする](https://dev.classmethod.jp/smartphone/iphone/swift-filter-by-multiple-array-conditions/)です。
検索文字２つと銘打っていますが、３つでも４つでも10個でもこの方法なら対応できると思います。

Swiftのfilterとreduceについて理解が浅かったため、少々理解と応用に時間がかかりました。
よく書かれている形以下のような形だと思います。

```swift
let numbers = [1, 2, 3, 4, 5]

//filter
let newNumbers = numbers.filter { $0 < 3 }
print(newNumbers) //[1, 2]

//reduce
let sum = numbers.reduce(0) {$0 + $1}
print(sum) //15
```
ただ、これだと他の言語とかでreduceやfilter使ったりしたことがないswift初心者は多少面食らうわけです。
そもそもこれは少し簡略化されています。簡略化せずに書くと、

```swift
let numbers = [1, 2, 3, 4, 5]

//filter
let newNumbers = numbers.filter { (value: Int) -> Bool in
    return value < 3
}
print(newNumbers) //[1, 2]

//reduce
let sum = numbers.reduce(0) { (sum: Int, value: Int) -> Int in
    return total + value
}
print(sum) //15
```
という形になります。
つまり、filterの方では第一引数が\$0に、
reduceの方では第一引数が\$0、第二引数が\$1に簡略して書くことができるということです。

これらを踏まえて先のコードを、順に辿っていくと、

```swift
for word in searchWords {
    self.conditions.append { $0.titleStr!.contains(word) || $0.summary!.contains(word) || $0.tags!.contains(word) }
}

self.searchArticleArray = self.preArticleArray.filter { article in
    self.conditions.reduce(true) { $0 && $1 (article) }
}
```
まず、preArticleArrayに格納された一つ一つの記事情報articleを取り出します。※filterのクロージャーの中。
articleはconditionsの引数として取り込まれ、searchWordsの数だけBool値を返します。
そのBool値はreduceによって畳み込まれます。
初期値をtrueとして、全てtrueならtrue、一つでもfalseがあればfalseになります。
そのBool値がfilterの結果となり、trueだったarticleのみが抽出されます。

※preArticleArrayを宣言せずに、最初にfetchした記事データをそのままsearchArticleArrayに格納し、

```swift
self.searchArticleArray = self.searchArticleArrayfilter { article in
    self.conditions.reduce(true) { $0 && $1 (article) }
```
と上書きさえるような形をとっても上手く動作するとは思います。配列を分けたほうがわかりやすいかと思って今回は分けました。

以上です！


参考記事
[[Swift] 複数のOR条件をAND検索してArrayをフィルタリングする](https://dev.classmethod.jp/smartphone/iphone/swift-filter-by-multiple-array-conditions/)
[Swiftの配列、辞書関連メソッドの基本](https://re-engines.com/2017/06/09/swiftの配列、辞書関連メソッドの基本/)
[Swiftのmap, filter, reduce（などなど）はこんな時に使う！](https://qiita.com/motokiee/items/cf83b22cb34921580a52#filterは条件に合う要素を絞り込む時に使う)