[iOS]RxSwift + APIKit + Decodableでインクリメンタルサーチ
こんにちは。
モバイルアプリサービスの太田です。
今さながらRxSwiftを初めて触っています。
RxSwiftの使い方も少しずつわかってきましたので、通信ライブラリAPIKitを使って、
wikiのapiをコールして、記事をインクリメンタルサーチしてみたいと思います。
環境
Xcode 10
Swift 4.2
RxSwift 4.4.1
APIKit 3.2.1
APIKit.DataParser
APIKit
は標準だとレスポンスがData
->JSON(Dictionary
やArray
)にパースされるJSONDataParser
が利用されます。
そのままではDecodable
によるデコードができません。
そこで、APIKit.DataParser
を継承したクラスを作り、Decodable
によるデコードを行います。
final class EntityDataParser<T: Decodable>: APIKit.DataParser { var contentType: String? { return "application/json" } func parse(data: Data) throws -> Any { // デコードする return try JSONDecoder().decode(T.self, from: data) } }
EntityDataParser
を使うことで、デコードされたEntityが返ってくるようになります。
Entity
wikiの検索結果を格納するCodableに準拠したEntityを定義します。
APIから返ってくるJSONは以下のような感じです。
{ "continue" : { "sroffset" : 30, "continue" : "-||" }, "query" : { "searchinfo" : { "totalhits" : 427418 }, "search" : [ { "pageid" : 13618, "ns" : 0, "snippet" : "(<span class=\"searchmatch\">A<\/span>+, <span class=\"searchmatch\">A<\/span>-)。 劇場や競技場、業績等ではS(特別席)に次ぐよいランクを表す)に次ぐよいランクを表す\343。 スポーツなどの競技での、リーグ等のグループの段階を表す。Sが「特<span class=\"searchmatch\">A<\/span>」として<span class=\"searchmatch\">A<\/span>(即ち「平<span class=\"searchmatch\">A<\/span>」)より上で最上級の場合と、<span class=\"searchmatch\">A<\/span>が「平<span class=\"searchmatch\">A<\/span>」でも最上級の場合がある。 会社の経営状態に関するランク付け等で<span class=\"searchmatch\">A<\/span>", "title" : "A", "size" : 13654, "wordcount" : 1453, "timestamp" : "2019-01-15T14:19:36Z" }, { "pageid" : 1892553, "ns" : 0, "snippet" : "o または <span class=\"searchmatch\">a<\/span> を標識としてつけて、序数を表す。o と <span class=\"searchmatch\">a<\/span> の2種類があるのは、ロマンスロマンス\350\252語の形容詞に男性形と女性ロマンス\350\252語の形容詞に男性形と女性\345\275形が存在するためである。標識のロマンス\350\252語の形容詞に男性形と女性\345\275形が存在するためである。標識の\344下に線が引かれロマンス\350\252語の形容詞に男性形と女性\345\275形が存在するためである。標識の\344下に線が引かれ\343\202る場合もある。ポルトガル語では数字のあとにピリオドをつけてから o \/ <span class=\"searchmatch\">a<\/span> をつける。 例:1 (uno) の場合", "title" : "序数標識", "size" : 6304, "wordcount" : 727, "timestamp" : "2018-10-26T09:44:07Z" }, ... ] }, "batchcomplete" : "" }
欲しい情報は、search
の中のtitile
とpageid
なのでそれだけを保持するようにします。
struct WikiEntity: Decodable { let query: Query? } struct Query: Decodable { let search: [Search] } struct Search: Decodable { let title: String let pageid: Int }
APIKit.Request
APIKit.Requestに準拠したWikiRequest
を作成します。
struct WikiRequest: APIKit.Request { typealias Response = WikiEntity let query: String var baseURL: URL { let urlStr = "https://ja.wikipedia.org/w/api.php?format=json&action=query&list=search&srlimit=30&srsearch=\(self.query)" return URL(string: urlStr.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! } let method: HTTPMethod = .post let path: String = "" }
baseURLのパラメータについてはここを参考にしました。
APIKit.Request.Response
がDecodable
に準拠している場合に、EntityDataParser
を使うように変更します。
extension APIKit.Request where Self.Response: Decodable { var dataParser: APIKit.DataParser { // 作成したEntityDataParserを使用する return EntityDataParser<Response>() } func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response { // objectの型をチェック // Responseと型が一致していなければエラーを投げる guard let entity = object as? Response else { throw ResponseError.unexpectedObject(object) } return entity } }
さらに、レスポンスをObservable
でラップするメソッドを用意。
extension APIKit.Request { func rx_send() -> Observable<Self.Response> { return Observable.create { observer in let task = Session.shared.send(self) { result in switch result { case .success(let res): observer.on(.next(res)) observer.on(.completed) case .failure(let err): observer.onError(err) } } return Disposables.create { task?.cancel() } } } }
UI側とRxSwiftで接続
Storyboard上で以下のようなUIを準備
WikiViewController
検索文字を入力するUISearchBar
と、検索結果表示用のUITableView
を設置。
UITableView
にはUITableViewCell
を設置して、Identifier
に適当な名前を設定してます。(ここでは、"Cell"と設定してます。)
class WikiViewController: UIViewController { @IBOutlet weak var searchBar: UISearchBar! @IBOutlet weak var tableView: UITableView! // Observableの登録解除用 private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() // UISearchBarの入力を検知してWikiのAPIをコール self.searchBar.rx.text.orEmpty .flatMapLatest { WikiRequest(query: $0).rx_send() } .map { $0.query?.search ?? [] } // APIのレスポンスとUITableViewをバインド // Identifierで設定した"Cell"でUITableViewCellを使いまわし .bind(to: self.tableView.rx.items(cellIdentifier: "Cell")) { // 検索結果のタイトルとURLを表示に反映 $2.textLabel?.text = $1.title $2.detailTextLabel?.text = "https://ja.wikipedia.org/w/index.php?curid=\($1.pageid)" } .disposed(by: self.disposeBag) } }
完成
以上で、インクリメンタルサーチの完成です。
完成品
最後に
RxSwiftに触り出したばかりなので、どういったときにどれを使うのが良いのかがわからず、色々調べならが試行錯誤しております。
様々な便利な機能がありますので、活用していければと思います。