LoginSignup
6
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-01-16

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も初期化しないと、2回目以降の検索がうまくいかなくなる。

                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をフィルタリングするです。
検索文字2つと銘打っていますが、3つでも4つでも10個でもこの方法なら対応できると思います。

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

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初心者は多少面食らうわけです。
そもそもこれは少し簡略化されています。簡略化せずに書くと、

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に簡略して書くことができるということです。

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

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に格納し、

self.searchArticleArray = self.searchArticleArrayfilter { article in
    self.conditions.reduce(true) { $0 && $1 (article) }

と上書きさえるような形をとっても上手く動作するとは思います。配列を分けたほうがわかりやすいかと思って今回は分けました。

以上です!

参考記事
[Swift] 複数のOR条件をAND検索してArrayをフィルタリングする
Swiftの配列、辞書関連メソッドの基本
Swiftのmap, filter, reduce(などなど)はこんな時に使う!

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4