この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは。
モバイルアプリサービスの太田です。
今さながら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に触り出したばかりなので、どういったときにどれを使うのが良いのかがわからず、色々調べならが試行錯誤しております。
様々な便利な機能がありますので、活用していければと思います。