【Swift】iOSでSuggestキーワードを表示してみた


こんな感じです。

![SwiftSampleSuggestView.gif](https://qiita-image-store.s3.amazonaws.com/0/47302/1a40cbc1-a0a4-386a-c308-752a0fadb27c.gif "SwiftSampleSuggestView.gif")


とるの下手くそでごめんさない。

簡単な動作を説明すると

1. UISearchViewをNavigationBarに入れる
2. `delegate`で入力された値を取得する
3. YoutubeのSuggestAPIにアクセスする
4. 結果を`String`の配列にして`UITableView`に表示する

という感じです。

ファイルは以下におきました。

[ryokosuge/SampleSwiftSuggestView](https://github.com/ryokosuge/SampleSwiftSuggestView)

SuggestのAPIについては以下を参考にしました。

[http://logic.moo.jp/data/archives/792.html](http://logic.moo.jp/data/archives/792.html)

にすべて書いてあります。

ここの **Youtube** の部分を参考にさせていただきました。

特に難しいこともしていませんので、Suggestを表示するまでの流れを紹介します。

## ソースコード

ちょっと長いですが、こんな感じです。

```swift:SuggestViewController.swift
import UIKit
import BrightFutures

class SuggestViewController: UIViewController {
    
    dynamic var suggestItems: [String] = []
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var constTableViewHeight: NSLayoutConstraint!
    
    var searchBar: UISearchBar!

    override func viewDidLoad() {
        super.viewDidLoad()
        println(__FUNCTION__)
        setupSearchBar()
        setupTableView()
        navigationController?.setNavigationBarHidden(false, animated: true)
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
    }
    
    deinit {
        self.removeObserver(self, forKeyPath: "suggestItems")
        tableView.removeObserver(self, forKeyPath: "contentSize")
    }
}

// MARK : - set up

extension SuggestViewController {
    
    private func setupSearchBar() {
        if let navigationBarFrame = navigationController?.navigationBar.bounds {
            let searchBar: UISearchBar = UISearchBar(frame: navigationBarFrame)
            searchBar.delegate = self
            searchBar.showsCancelButton = true
            searchBar.keyboardAppearance = UIKeyboardAppearance.Dark
            searchBar.keyboardType = UIKeyboardType.Default
            navigationItem.titleView = searchBar
            navigationItem.titleView?.frame = searchBar.frame
            self.searchBar = searchBar
            searchBar.becomeFirstResponder()
        }
    }
    
    private func setupTableView() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.tableFooterView = UIView()
        tableView.hidden = true
        
        self.addObserver(self, forKeyPath: "suggestItems", options: NSKeyValueObservingOptions.New, context: nil)
        tableView.addObserver(self, forKeyPath: "contentSize", options: (NSKeyValueObservingOptions.New), context: nil)
    }
}

// MARK : - tableView height observer

extension SuggestViewController {
    
    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
        println(__FUNCTION__)
        if keyPath == "contentSize" {
            didChangeTableViewContentHeight()
        } else if keyPath == "suggestItems" {
            didChangeSuggestItems()
        }
    }
    
    private func didChangeTableViewContentHeight() {
        println(__FUNCTION__)
        constTableViewHeight.constant = tableView.contentSize.height
        tableView.setNeedsLayout()
        tableView.setNeedsUpdateConstraints()
        UIView.animateWithDuration(0.6, animations: {[weak self] () -> Void in
            self?.tableView.layoutIfNeeded()
        })
    }
    
    private func didChangeSuggestItems() {
        dispatch_async(dispatch_get_main_queue(), {[weak self] () -> Void in
            self?.tableView.hidden = false
            self?.tableView.reloadData()
        })
    }
}

// MARK : - tableview delegate

extension SuggestViewController: UITableViewDelegate {
    
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
}

// MARK : - tableview dataSource

extension SuggestViewController: UITableViewDataSource {
    
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let TableViewBasicCellIdentifier = "BasicCell"
        let cell = tableView.dequeueReusableCellWithIdentifier(TableViewBasicCellIdentifier, forIndexPath: indexPath) as! UITableViewCell
        cell.textLabel?.text = suggestItems[indexPath.row]
        return cell
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return suggestItems.count
    }
}

// MARK : - search bar delegate

extension SuggestViewController: UISearchBarDelegate {
    
    func searchBarCancelButtonClicked(searchBar: UISearchBar) {
        println(__FUNCTION__)
        searchBar.resignFirstResponder()
        navigationController?.setNavigationBarHidden(true, animated: true)
        dismissViewControllerAnimated(true, completion: nil)
    }
    
    func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
        println(__FUNCTION__)
        println("searchText = \(searchText)")
        self.searchSuggestKeyword(searchText)
    }
    
}

// MARK : - search suggest keyword

extension SuggestViewController {
    
    private func searchSuggestKeyword(keyword: String) {
        if count(keyword) < 1 {
            return
        }
        
        futureSuggestRequest(keyword).onSuccess { suggestItems in
            self.suggestItems = suggestItems
        }.onFailure { error in
            println("error = \(error)")
        }
        
    }
    
    private func futureSuggestRequest(searchKeywork: String) -> Future<[String]> {
        println(__FUNCTION__)
        let promiss = Promise<[String]>()
        
        Queue.global.async {
            if let URL = self.createSuggestURL(searchKeywoard: searchKeywork) {
                let request = self.requestURL(URL)
                self.sendRequest(request).flatMap { data in
                    self.futureSuggestItems(data: data)
                }.onSuccess { suggestItems in
                    promiss.success(suggestItems)
                }.onFailure { error in
                    promiss.failure(error)
                }
            }
        }
        
        return promiss.future
    }
    
    private func sendRequest(request: NSURLRequest) -> Future<NSData> {
        println(__FUNCTION__)
        let promiss = Promise<NSData>()
        let session = NSURLSession.sharedSession()
        let task = session.dataTaskWithRequest(request, completionHandler: { (responseData: NSData!, urlResponse: NSURLResponse?, connectError: NSError?) -> Void in
            if let error = connectError {
                promiss.failure(error)
            } else {
                promiss.success(responseData)
            }
        })
        task.resume()
        return promiss.future
    }
    
    private func futureSuggestItems(#data: NSData) -> Future<[String]> {
        println(__FUNCTION__)
        let promiss = Promise<[String]>()
        if let dataString = NSString(data: data, encoding: NSShiftJISStringEncoding), d = dataString.dataUsingEncoding(NSUTF8StringEncoding) {
            var error: NSError? = nil
            if let jsonArray = NSJSONSerialization.JSONObjectWithData(d, options: NSJSONReadingOptions.AllowFragments, error: &error) as? NSArray {
                let suggestItems = convertSuggestItems(jsonArray)
                promiss.success(suggestItems)
            } else {
                if let e = error {
                    promiss.failure(e)
                }
            }
        } else {
            let error = NSError(domain: "SampleSuggest", code: 9999, userInfo: nil)
            promiss.failure(error)
        }
        return promiss.future
    }
    
    private func convertSuggestItems(jsonArray: NSArray) -> [String] {
        println(__FUNCTION__)
        var newSuggestWords: [String] = []
        if let strs = jsonArray.lastObject as? [String] {
            for str in strs {
                newSuggestWords.append(str)
            }
        }
        
        return newSuggestWords
    }
    
    private func requestURL(URL: NSURL) -> NSURLRequest {
        println(__FUNCTION__)
        var request = NSMutableURLRequest(URL: URL)
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("applicatoin/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
        request.HTTPMethod = "GET"
        return request
    }
    
    private func createSuggestURL(#searchKeywoard: String) -> NSURL? {
        println(__FUNCTION__)
        let urlString = "http://clients1.google.com/complete/search?hl=ja&ds=yt&client=firefox&q=\(searchKeywoard.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)"
        return NSURL(string: urlString)
    }
    
}
```

以下ソースコードの要約になります。

### BrightFutures

[Thomvis/BrightFutures](https://github.com/Thomvis/BrightFutures)

とっても使ってみたかったライブラリについに手を出しました。

以下の記事が参考になりました。

[[Swift] 非同期処理フレームワークBrightFutures ~導入編~ | Developers.IO](http://dev.classmethod.jp/smartphone/swift-brightfutures-1/)

使い方などはGitHubを見れば大体わかるかと思います！

これに関しては別記事で書いた方がわかりやすいかと。

### View的になにをしているか

見た目の話をします。

以下のソースで **Suggest**の結果を表す`UITableView`の`contentSize`を監視しています。

```swift
tableView.addObserver(self, forKeyPath: "contentSize", options: (NSKeyValueObservingOptions.New), context: nil)
```

これで`tableView`の`contentSize`が変化したら

```swift
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {}
```

がコールされるので、それに`contentSize`の`height`に合わせて`tableView`の`HeightConstraint`に当たる`constTableViewHeight`を変化させています。

つまり`suggestItems:[String]`が空の場合、表示するものがなにもないので`constTableViewHeight.constant`は`0`になり、表示されなくなります。

最初は`Storyboard`で`hidden = true`にしています。（めんどくさかっただけです）

## 終わり

様々な処理を一つのクラスにまとめましたが、ちゃんと分けるように書かないとダメですね。

以上です。

## レポジトリ

- [ryokosuge/SampleSwiftSuggestView](https://github.com/ryokosuge/SampleSwiftSuggestView)