Moyaを使ってApiクライアント作成する
環境
- Swift 4.1.2
- Moya 11.0.2
Carthageでインストール
// Cartfile github "Moya/Moya"
carthage update --platform iOS
ビルドが終わったらプロジェクトに追加します
使い方
GithubのApiを使って以下の情報を取得してみます
- ユーザー情報 (/users/:username")
- ユーザーのリポジトリ情報 (/users/:username/repos")
モデルを作成
Codableを使用します。
※全部は大変なのでそれっぽいフィールドだけパースします。
// UserProfile.swift // ユーザー情報 struct UserProfile: Codable { let id: Int let name: String let email: String? let avatarUrl: String private enum CodingKeys: String, CodingKey { case id case name case email case avatarUrl = "avatar_url" } } // Repository.swift // ユーザーのリポジトリ情報 struct Repository: Codable { let id: Int let name: String let url: String private enum CodingKeys: String, CodingKey { case id case name case url = "html_url" } }
リクエストを作成
enumにエンドポイントを定義。TargetType
に準拠してリクエスト内容を記述します。
// GitHubAPI.swift enum GitHub { case profile(name: String) case repository(name: String, type: String) } extension GitHub: TargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var path: String { switch self { case let .profile(name): return "/users/\((name))" case let.repository(name, _): return "/users/\(name)/repos" } } var method: Moya.Method { return .get } var sampleData: Data { var path = "" switch self { case .profile: path = Bundle.main.path(forResource: "profile", ofType: "json")! case .repository: path = Bundle.main.path(forResource: "repository", ofType: "json")! } return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { switch self { case .profile: return .requestPlain case let .repository(_, type): return .requestParameters(parameters: ["type" : type], encoding: URLEncoding.default) } } var headers: [String : String]? { return ["Content-Type": "application/json"] } }
リクエスト
// ユーザー情報 let provider = MoyaProvider<GitHub>() provider.request(.profile(name: "y-hryk")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let profile = try! decoder.decode(UserProfile.self, from: response.data) print(profile) case let .failure(error): print(error) break } } // ユーザーのリポジトリ情報 let provider = MoyaProvider<GitHub>() provider.request(.repository(name: "y-hryk", type: "all")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let data = try! decoder.decode([Repository].self, from: response.data) print(data) case let .failure(error): print(error) break } }
使い方2
使い方1
の例だとAPIが増えるたびにリクエストの処理に分岐が増えていきます。
なのでenumを使用しない記述方法もみていきます。
TargetTypeに準拠したプロトコルを定義
TargetType
に準拠したプロトコルを定義して共通となる処理を記述します
// GitHubAPIService.swift class GitHubAPIService {} protocol GitHubAPITargetType: TargetType { } extension GitHubAPITargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var headers: [String : String]? { return ["Content-Type": "application/json"] } }
リクエストを作成
拡張したプロトコルGitHubAPITargetType
に準拠してリクエストを作成していきます。
// UserProfileRequest.swift extension GitHubAPIService { struct UserProfileRequest: GitHubAPITargetType { var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)" } var task: Task { return .requestPlain } var sampleData: Data { return Data() } let name: String init(name: String) { self.name = name } } }
// RepositoryRequest.swift extension GitHubAPIService { struct RepositoryRequest: GitHubAPITargetType { var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)/repos" } var sampleData: Data { let path = Bundle.main.path(forResource: "repository", ofType: "json")! return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { return .requestParameters(parameters: ["type" : self.type], encoding: URLEncoding.default) } let name: String let type: String init(name: String, type: String) { self.name = name self.type = type } } }
リクエスト
enumを使用した方法とほとんど変わらずリクエストできます。
// ユーザー情報 let provider = MoyaProvider<GitHubAPIService.UserProfileRequest>() provider.request(GitHubAPIService.UserProfileRequest(name: "y-hryk")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let data = try! decoder.decode(UserProfile.self, from: response.data) print(data) case let .failure(error): print(error) } } // ユーザーのリポジトリ情報 let provider = MoyaProvider<GitHubAPIService.RepositoryRequest>() provider.request(GitHubAPIService.RepositoryRequest(name: "y-hryk", type: "all")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let data = try! decoder.decode([Repository].self, from: response.data) print(data) case let .failure(error): print(error) } }
MoyaProviderの生成 + パース処理を共通化
MoyaProviderの生成とパース部分を隠蔽してシンプルな呼び出しに変更していきます
参考
- Moya/MultiTarget.md at master · Moya/Moya · GitHub
以下みたいに呼び出せるように修正します
GitHubAPIService().send(GitHubAPIService.UserProfileRequest(name: "y-hryk")) { (result) in switch result { case let .success(response): print(response) case let .failure(error): print(error) } }
TargetTypeに準拠したプロトコルを修正
// GitHubAPIService.swift protocol GitHubAPITargetType: TargetType { // レスポンスの型を定義できるように変更 // 追加 associatedtype Response: Codable } extension GitHubAPITargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var headers: [String : String]? { return ["Content-Type": "application/json"] } }
モデルを修正
// UserProfileRequest.swift extension GitHubAPIService { struct UserProfileRequest: GitHubAPITargetType { // 追加 typealias Response = UserProfile var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)" } var sampleData: Data { let path = Bundle.main.path(forResource: "profile", ofType: "json")! return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { return .requestPlain } let name: String init(name: String) { self.name = name } } } // RepositoryRequest.swift extension GitHubAPIService { struct RepositoryRequest: GitHubAPITargetType { // 追加 typealias Response = [Repository] var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)/repos" } var sampleData: Data { let path = Bundle.main.path(forResource: "repository", ofType: "json")! return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { return .requestParameters(parameters: ["type" : self.type], encoding: URLEncoding.default) } let name: String let type: String init(name: String, type: String) { self.name = name self.type = type } } }
リクエスト処理をラップする
これで最初に示したような呼び出し方が可能になります
// GitHubAPIService.swift protocol GitHubAPI { func send<T: GitHubAPITargetType>(_ request: T, completion: @escaping (Result<T.Response, Moya.MoyaError>) -> Void) } class GitHubAPIService: GitHubAPI { func send<T>(_ request: T, completion: @escaping (Result<T.Response, MoyaError>) -> Void) where T : GitHubAPITargetType { let provider = MoyaProvider<T>() provider.request(request) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() if let model = try? decoder.decode(T.Response.self, from: response.data) { completion(.success(model)) print(try! response.mapJSON()) } else { completion(.failure(.jsonMapping(response))) } case let .failure(error): completion(.failure(error)) } } } }
スタブを利用する
GitHubAPIService
,GitHubAPIStub
共にGitHubAPI
に準拠しているので
DIを用いてスタブに差し替えられる。
// GitHubAPIStub.swift class GitHubAPIStub: GitHubAPI { func send<T>(_ request: T, completion: @escaping (Result<T.Response, MoyaError>) -> Void) where T : GitHubAPITargetType { // スタブを返すように設定。このすることでリクエストに定義した`sampleData`が替えるようになります。 let provider = MoyaProvider<T>(stubClosure: MoyaProvider.immediatelyStub) provider.request(request) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() if let model = try? decoder.decode(T.Response.self, from: response.data) { completion(.success(model)) } else { completion(.failure(.jsonMapping(response))) } case let .failure(error): completion(.failure(error)) } } } }
【GitLab-CI】登録したrunnerが削除できない
以下のコマンドで登録をしたrunnerを削除しようとしたところ以下のエラーが発生した
現象
gitlab-ci-multi-runner unregister // 以下エラー WARNING: Running in user-mode. WARNING: Use sudo for system-mode: WARNING: $ sudo gitlab-runner... ERROR: Unregistering runner from GitLab error status=only http or https scheme supported FATAL: Failed to unregister runner
解決方法
url
とtoken
をオプションとして付与すれば良い。gitlab-ci-multi-runner register
は
コマンドを入力した後に情報を入れていく形だったので勘違いしていました。
gitlab-ci-multi-runner unregister --url https://xxxxxxxxxxxxxxxxxxx --token xxxxxxxxxxxxxxxxxxxxxxxxxx
登録しているrunnerはgitlab-ci-multi-runner list
で確認できるのでurl,tokenを調べられる。
すでにGitLabの設定画面からrunnerを削除している場合は上記のコマンドでもエラーが出るので
その場合は以下のコマンドで稼働していないrunnerを全て削除することができる。
gitlab-ci-multi-runner verify --delete
※稼働していないrunnerは全て削除されるので注意。以下のコマンドを実行する時は設定ファイルをバックアップしておいた方が良いと思います。 ちなみに設定ファイルを直接編集して削除しても問題なかったです。
設定ファイルのパス
~/.gitlab-runner/config.toml
設定ファイルの中身
以下のような感じ
concurrent = 1 check_interval = 0 [[runners]] name = "project1" url = "https://xxxxxxxx/" token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" executor = "shell" [runners.cache] [[runners]] name = "project2" url = "https://xxxxxxxx/" token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" executor = "shell" [runners.cache]
【Codable】特定の値を型変換する
レスポンスデータは文字列("12345"
)だけど、クライアントでは数値(12345
)で扱いたい時
// レスポンスデータ { id = "12345"; name = "y-hryk"; email = "test@gmail.com"; } struct User: Codable { var id: Int var name: String var email: String init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) // ここで明示的に変換する id = Int(try values.decode(Int.self, forKey: .id)) name = try values.decode(String.self, forKey: .name) email = try values.decode(String.self, forKey: .email) } }
R.swiftを使ってみる
便利とは聞いていたが、実務での使用経験がなかったため試してみる。
メリット
- コード補完でリソースファイルにアクセスできる
- 文字列による指定をしなくてよくなるのでtypoに気がつける
導入
R.swift(4.0.0)を導入していきます。
ドキュメント通りですが一応
CocoaPodsでインストール
pod 'R.swift'
Run Scriptの設定
Target > Build Phases を選択して Run Script を追加します。
"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT"
追加したRun ScriptはCompile Sources
とCheck Pods Manifest.lock
間にドラックして動かしましょう。
R.generated.swiftをプロジェクトに追加する
上記の作業を終えたら一回ビルドします。
そうするとR.generated.swift
がプロジェクトのルートディレクトリに追加されているので、このファイルをプロジェクトに追加します。
ビルドされるたびにリソースの状態をみてR.generated.swift
が更新されるので .gitignoreに以下を追加します。
*.generated.swift
使用方法
let string = NSLocalizedString("localize_text", comment: "") let image = UIImage(named: "sample") let vc = UIStoryboard(name: "main", bundle: nil).instantiateInitialViewController() tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCell") let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell // R.swiftを使用 let string = R.string.localizable.localize_text() let image = R.image.sample() let vc = R.storyboard.main.instantiateInitialViewController() tableView.register(R.nib.customCell) let cell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.customCell, for: indexPath)!
【RxSwift】Subjectまとめ
PublishSubject
let publishSubject = PublishSubject<Int>() publishSubject.asObservable().subscribe { (event) in print("publishSubject: 1, event:\(event)") } publishSubject.onNext(1) publishSubject.onNext(2) publishSubject.asObservable().subscribe { (event) in print("publishSubject: 2, event:\(event)") } publishSubject.onNext(3) publishSubject.onNext(4) publishSubject.onCompleted()
// 結果 publishSubject: 1, event:next(1) publishSubject: 1, event:next(2) publishSubject: 1, event:next(3) publishSubject: 2, event:next(3) publishSubject: 1, event:next(4) publishSubject: 2, event:next(4) publishSubject: 1, event:completed publishSubject: 2, event:completed
ReplaySubject
Subscribeしたタイミングで今の状態から遡って指定数または全て通知する。
let replaySubject = ReplaySubject<Int>.create(bufferSize: 2) replaySubject.asObservable().subscribe { (event) in print("replaySubject: 1, event:\(event)") } replaySubject.onNext(1) replaySubject.onNext(2) replaySubject.onNext(3) replaySubject.onNext(4) replaySubject.asObservable().subscribe { (event) in print("replaySubject: 2, event:\(event)") } replaySubject.onNext(5) replaySubject.onNext(6) replaySubject.onCompleted()
// 結果 replaySubject: 1, event:next(1) replaySubject: 1, event:next(2) replaySubject: 1, event:next(3) replaySubject: 1, event:next(4) // ※ 2つ目のsubscribeが実行されたタイミングで 指定した値(2)遡って通知する replaySubject: 2, event:next(3) replaySubject: 2, event:next(4) replaySubject: 1, event:next(5) replaySubject: 2, event:next(5) replaySubject: 1, event:next(6) replaySubject: 2, event:next(6) replaySubject: 1, event:completed replaySubject: 2, event:completed
BehaviorSubject
今の状態を一回通知した上で変化があった場合それを通知する
let behaviorSubject = BehaviorSubject<Int>(value: 0) behaviorSubject.asObservable().subscribe { (event) in print("behaviorSubject: 1, event:\(event)") } behaviorSubject.onNext(1) behaviorSubject.onNext(2) behaviorSubject.asObservable().subscribe { (event) in print("behaviorSubject: 2, event:\(event)") } behaviorSubject.onNext(3) behaviorSubject.onNext(4) behaviorSubject.onCompleted()
// 結果 // ※ 1つ目のsubscribeが実行されたタイミングで最後の値(0)が通知される behaviorSubject: 1, event:next(0) behaviorSubject: 1, event:next(1) behaviorSubject: 1, event:next(2) // ※ 2つ目のsubscribeが実行されたタイミングで最後の値(2)が通知される behaviorSubject: 2, event:next(2) behaviorSubject: 1, event:next(3) behaviorSubject: 2, event:next(3) behaviorSubject: 1, event:next(4) behaviorSubject: 2, event:next(4) behaviorSubject: 1, event:completed behaviorSubject: 2, event:completed
Variable
BehaviorSubjectのラッパー。Variableは明示的にonCompleted, onErrorになることはない。
解放時に自動的にonCompletedを発行する。
※ RxSwift 4.0.0-rc.0よりdeprecated
let variable = Variable(0) variable.asObservable().subscribe({ (event) in print("variable: 1, event:\(event)") }) variable.value = 1 variable.value = 2 variable.asObservable().subscribe({ (event) in print("variable: 2, event:\(event)") }) variable.value = 3 variable.value = 4
// 結果 variable: 1, event:next(0) variable: 1, event:next(1) variable: 1, event:next(2) variable: 2, event:next(2) variable: 1, event:next(3) variable: 2, event:next(3) variable: 1, event:next(4) variable: 2, event:next(4) // 変数が解放されたタイミングでonCompletedが発行される variable: 1, event:completed variable: 2, event:completed
PublishRelay
PublishSubjectのラッパー。onCompleted, onErrorが発行されることがない。
let publishRelay = PublishRelay<Int>() publishRelay.asObservable().subscribe { (event) in print("PublishRelay: 1, event:\(event)") } publishRelay.accept(1) publishRelay.accept(2) publishRelay.asObservable().subscribe { (event) in print("PublishRelay: 2, event:\(event)") } publishRelay.accept(3) publishRelay.accept(4)
// 結果 PublishRelay: 1, event:next(1) PublishRelay: 1, event:next(2) PublishRelay: 1, event:next(3) PublishRelay: 2, event:next(3) PublishRelay: 1, event:next(4) PublishRelay: 2, event:next(4)
BehaviorRelay
BehaviorSubjectのラッパー。onCompleted, onErrorが発行されることがない。
let behaviorRelay = BehaviorRelay<Int>(value: 0) behaviorRelay.asObservable().subscribe { (event) in print("behaviorRelay: 1, event:\(event)") } behaviorRelay.accept(1) behaviorRelay.accept(2) behaviorRelay.asObservable().subscribe { (event) in print("behaviorRelay: 2, event:\(event)") }.disposed(by: disposeBag) behaviorRelay.accept(3) behaviorRelay.accept(4)
// 結果 behaviorRelay: 1, event:next(0) behaviorRelay: 1, event:next(1) behaviorRelay: 1, event:next(2) behaviorRelay: 2, event:next(2) behaviorRelay: 1, event:next(3) behaviorRelay: 2, event:next(3) behaviorRelay: 1, event:next(4) behaviorRelay: 2, event:next(4)
参考
UIDatePickerまとめ
いつも調べている気がするのでまとめておく
端末の設定に関係なく、強制的に24時間表記にする
※以下の設定をすると端末の設定が英語の場合でも日本語 + 24時間表示になるので注意
let datePicker = UIDatePicker() datePicker.locale = Locale(identifier: "ja_JP")
端末の設定に関係なく、強制的に西暦表示にする
let datePicker = UIDatePicker() var calendar = Calendar(identifier: .gregorian) calendar.locale = Locale.current datePicker.calendar = calendar
【iOS】DateとStringの相互変換の注意点
今更ですが混乱することがあるのでまとめておく
String->Date
タイムゾーンなどの情報がない場合は現在の端末設定から自動的に
タイムゾーンを考慮したDateが返ってくる
※ 地域の設定が日本の場合
let dateString = "2018/01/01 12:00" let formatter = DateFormatter() formatter.dateFormat = "yyyy/MM/dd HH:mm" print(formatter.date(from: dateString)!) 結果 2018-01-01 03:00:00 +0000
基本的にDateFormatterにタイムゾーンを指定しない場合は
日本の場合UTC+9時間なので以下のようになる(DateはそもそもUTC+0)
Date -> String +9時間
String -> Date - 9時間
ちゃんとタイムゾーンが指定されていればそのタイムゾーンを考慮した時刻を得ることができる(RFC3339)
let dateString = "2018-01-01T12:00:00+09:00" let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" print(formatter.date(from: dateString)!) 結果 2018-01-01 03:00:00 +0000
let dateString = "2018-01-01T12:00:00+00:00" let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" print(formatter.date(from: dateString)!) 結果 2018-01-01 12:00:00 +0000
Date->String
DateFormatterのlocaleにen_US_POSIX
を指定すること。そうしないと和暦などの設定によっては正しく変換できない
let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") // これ