- 投稿日:2020-03-26T22:20:29+09:00
[RxSwift]RxSwiftとは
RxSwiftとは
コードが新しいデータに反応、順次的に分離された方式で処理される、非同期プログラム開発が簡素化できる、データの変化に対応しやすい。
observable
asynchronous
functional
via schedulers
RxSwiftの基礎
Rxの3つの構成要素についてみてみる
Observables、Operators、Schedulers1.Observables
- Observable クラスはRxの基盤
T
タイプのデータを渡す、イベントを非同期に生成
- 他のクラスでemit(放出?)する値を購読できるようにする
- ObservableTypeのProtocol(Observable)はシンプルで、以下の3つのイベントのみemitし、observersはこの3つのみ受信できる
- next:最新/次のデータを渡すイベント
- completed:成功的にイベントを終了させるイベント。追加イベント生成しない
- error:Observableがエラー発生。追加イベント生成しない
Finite Observable Sequences (有限の観測可能なシーケンス)
例えば、ファイルをダウンロードするコードをイメージ
流れは
1. ファイルのダウンロードが始まって、データを監視する
2. 続けてファイルをダウンロードする
3. (エラーの場合)ネットワークが切れたらエラーになって停止する
4. (成功した場合)問題なくダウンロードが成功するAPI.download(file: "https://www...") .subscribe(onNext: { data in ... append data to temporary file }, onError: { error in ... display error to user }, onCompleted: { ... use downloaded file })
API.download
はダウンロードするデータObservable<Data>
インスタンスをreturnonNext
クロージャでnext
イベントを受け取ることができる。onError
クロージャではerror
イベントが受け取れる。- 最終的には
onCompleted
クロージャでcompleted
イベントを受け取るInfinite Observable Sequences(無限の観測可能なシーケンス)
ダウンロードのように自然に終了するものとは異なって、無限なsequenceがある。例えばUIイベントなどは無限に観測できる。
例えば、デバイスの横/縦モードに反応するコードをイメージ
流れは
1.UIDeviceOrientationDidChange
observerを追加
2. 方向転換が管理できるcallback methodが必要
3. UIDeviceの現在の方向を確認して、画面に表示する
4. 方向転換ができるデバイスが存在する以上、自然に終了することはない
5. 無限なsequenceなので一応初期値が必要UIDevice.rx.orientation .subscribe(onNext: { current in switch current { case .landscape: ... re-arrange UI for landscape case .portrait: ... re-arrange UI for portrait } })
- ここでは、
onError
onCompleted
は発生しないはずなので、記述も不要2. Operators
ObservableType
とObservable
クラスはより複雑な論理を実現するためにたくさんのメソッドが含まれている。それらをOperator
を呼ぶOperator
は非同期入力を受けて出力のみ生成するだけで、複数のOperator
を併せて使うことができる
Observable
に、より入ってきたのを処理して放出?する役割コードを見た方が分かりやすいかも
UIDevice.rx.orientation .filter { value in return value != .landscape } .map { _ in return "Only Portrait" } .subscribe(onNext: { string in showAlert(string) })
- 上記でみると分かるように
filter
は.landscape
ではないものだけ、return.portrait
の場合はmap
に入るはずで、map
ではStringに変換してreturn- 最後に
subscribe
では結果としてstringをshowAlertメソッドで表示しているこのように複数の
Operator
を使って、いろんなことができる。3.Schedulers
- SchedulersはRxではdispatch queueと同じだが、もっと使いやすくて強力
- RxSwiftではいろんなスクジューラーが既に定義されている。99%の状況で使えるので、開発者が自分だけのスケジューラーを生成することはない
RxCocoa
RxSwiftは一般的なRx APIで、Cocoaとか特定のUIKitクラスについて何の情報も持たない。RxCocoaはRxSwiftと一緒に使われながら、UIKitやCocoaフレームワーク基盤開発をサポートする全てのクラスを持っているらしい。
- 投稿日:2020-03-26T22:20:29+09:00
[RxSwift] RxSwiftとは
RxSwiftとは
コードが新しいデータに反応、順次的に分離された方式で処理される、非同期プログラム開発が簡素化できる、データの変化に対応しやすい。
observable
asynchronous
functional
via schedulers
RxSwiftの基礎
Rxの3つの構成要素についてみてみる
Observables、Operators、Schedulers1.Observables
- Observable クラスはRxの基盤
T
タイプのデータを渡す、イベントを非同期に生成
- 他のクラスでemit(放出?)する値を購読できるようにする
- ObservableTypeのProtocol(Observable)はシンプルで、以下の3つのイベントのみemitし、observersはこの3つのみ受信できる
- next:最新/次のデータを渡すイベント
- completed:成功的にイベントを終了させるイベント。追加イベント生成しない
- error:Observableがエラー発生。追加イベント生成しない
Finite Observable Sequences (有限の観測可能なシーケンス)
例えば、ファイルをダウンロードするコードをイメージ
流れは
1. ファイルのダウンロードが始まって、データを監視する
2. 続けてファイルをダウンロードする
3. (エラーの場合)ネットワークが切れたらエラーになって停止する
4. (成功した場合)問題なくダウンロードが成功するAPI.download(file: "https://www...") .subscribe(onNext: { data in ... append data to temporary file }, onError: { error in ... display error to user }, onCompleted: { ... use downloaded file })
API.download
はダウンロードするデータObservable<Data>
インスタンスをreturnonNext
クロージャでnext
イベントを受け取ることができる。onError
クロージャではerror
イベントが受け取れる。- 最終的には
onCompleted
クロージャでcompleted
イベントを受け取るInfinite Observable Sequences(無限の観測可能なシーケンス)
ダウンロードのように自然に終了するものとは異なって、無限なsequenceがある。例えばUIイベントなどは無限に観測できる。
例えば、デバイスの横/縦モードに反応するコードをイメージ
流れは
1.UIDeviceOrientationDidChange
observerを追加
2. 方向転換が管理できるcallback methodが必要
3. UIDeviceの現在の方向を確認して、画面に表示する
4. 方向転換ができるデバイスが存在する以上、自然に終了することはない
5. 無限なsequenceなので一応初期値が必要UIDevice.rx.orientation .subscribe(onNext: { current in switch current { case .landscape: ... re-arrange UI for landscape case .portrait: ... re-arrange UI for portrait } })
- ここでは、
onError
onCompleted
は発生しないはずなので、記述も不要2. Operators
ObservableType
とObservable
クラスはより複雑な論理を実現するためにたくさんのメソッドが含まれている。それらをOperator
を呼ぶOperator
は非同期入力を受けて出力のみ生成するだけで、複数のOperator
を併せて使うことができる
Observable
に、より入ってきたのを処理して放出?する役割コードを見た方が分かりやすいかも
UIDevice.rx.orientation .filter { value in return value != .landscape } .map { _ in return "Only Portrait" } .subscribe(onNext: { string in showAlert(string) })
- 上記でみると分かるように
filter
は.landscape
ではないものだけ、return.portrait
の場合はmap
に入るはずで、map
ではStringに変換してreturn- 最後に
subscribe
では結果としてstringをshowAlertメソッドで表示しているこのように複数の
Operator
を使って、いろんなことができる。3.Schedulers
- SchedulersはRxではdispatch queueと同じだが、もっと使いやすくて強力
- RxSwiftではいろんなスクジューラーが既に定義されている。99%の状況で使えるので、開発者が自分だけのスケジューラーを生成することはない
RxCocoa
RxSwiftは一般的なRx APIで、Cocoaとか特定のUIKitクラスについて何の情報も持たない。RxCocoaはRxSwiftと一緒に使われながら、UIKitやCocoaフレームワーク基盤開発をサポートする全てのクラスを持っているらしい。
- 投稿日:2020-03-26T18:03:49+09:00
Swiftでファイルを進捗付きでダウンロード
動画や大きな写真を端末にダウンロードさせる時に、時間がかかるので進捗を表示させたいことがありました
その時にやったことをまとめますコア
URLSession
のdownloadTask(with:)
を使います。
URLSessionDownloadDelegate
というデリゲートで進捗や完了/失敗を受け取ることができます。参考:
downloadTask(with:)
URLSessionDownloadDelegate実装サンプル
ファイルダウンロードができる簡単なアプリを作りました。
よければ参考にするなり流用するなりしてください。
https://github.com/Yaruki00/FileDownloader
テキストフィールドにファイルのURLを入力してGoボタンを押すとダウンロードします。
ダウンロード中は進捗を下のラベルに表示します。
エラー処理など作りは適当ですが、サンプルなのでご容赦ください・・・簡単に解説
最近は
RxSwift
を使うことが個人的に多いので、サンプルにもRxSwift
を使用しています。ダウンロード部分
downloadTask(with:)
を呼んで、URLSessionDownloadDelegate
に返ってくる状態がPublishRelay
に流れるようにします。Utility/FileDownloader.swift... /// ダウンロードの状態 enum DownloadState: Equatable { /// ダウンロード中(進捗) case downloading(Float) /// 完了(保存先URL) case done(URL) /// 失敗(エラー詳細) case error(Error) static func == (lhs: DownloadState, rhs: DownloadState) -> Bool { switch (lhs, rhs) { case (.downloading(let progressL), .downloading(let progressR)): return progressL == progressR case (.done, .done): return true case (.error, .error): return true default: return false } } } final class FileDownloader: NSObject { // ダウンロードの状態を流す用 // 繰り返し使う想定なのでPublishRelayにして、ObservableでいうonErrorは使わないようにする private var state = PublishRelay<DownloadState>() private var filename = "" private var task: URLSessionDownloadTask! /// ダウンロード開始 func download(_ url: URL, name: String? = nil) -> Observable<DownloadState> { filename = name ?? url.lastPathComponent return Observable.create { [weak self] observer in guard let self = self else { return Disposables.create() } // 関数呼び出されるたびにbindしてるけど大丈夫?教えてエラい人! let disposable = self.state.bind(to: observer) self.startDownload(url) return disposable } } /// ダウンロード中断 func cancel() { task.cancel() } } extension FileDownloader { private func startDownload(_ url: URL) { let config = URLSessionConfiguration.default let session = URLSession(configuration: config, delegate: self, delegateQueue: .main) let request = URLRequest(url: url) task = session.downloadTask(with: request) task.resume() } } extension FileDownloader: URLSessionDownloadDelegate { // ダウンロードが完了した時に呼ばれる func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { state.accept(.done(location)) } // ダウンロードの進捗が変化した時に呼ばれる func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { let current = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) state.accept(.downloading(current)) } // ダウンロードが失敗した時に呼ばれる func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { state.accept(.error(error)) } } }呼び出し側(今回はすごく適当なビュー)
ボタンが押されたらダウンロードを開始し、進捗をラベルに反映します。
App/Presentation/ViewController.swift... extension ViewController { private func bind() { ... // ボタンが押された button.rx.tap // テキストフィールドの文字列をURLに変換 .map { [weak self] _ -> URL? in let text = self?.textField.text ?? "" return URL(string: text) } // オプショナルのアンラップ .flatMap { $0.flatMap(Observable.just) ?? Observable.empty() } // URLをもとにダウンロード .flatMap { [weak self] url -> Observable<DownloadState> in return self?.fileDownloader.download(url) ?? .error(NSError(domain: "", code: 0, userInfo: nil)) } // 前回と同じ状態なら無視 .distinctUntilChanged() // ダウンロードの状態をもとにラベルに表示する文字列生成 .map { state -> String in switch state { case .downloading(let progress): return "downloading(\(String(format: "%.2f", progress * 100) + "%"))..." case .done(let url): return "done!\n\(url.absoluteString)" case .error(let error): return "error occurred\n\(error.localizedDescription)" } } // ラベルに流す .bind(to: label.rx.text) .disposed(by: disposeBag) } }おわりに
進捗がライブラリ等使わずに取得できるというのはありがたいですね
この記事では進捗状況をラベルに反映しましたが、プログレスバーなりアニメーションを使ったなにかなりにすれば見栄えもよくできると思います
RxSwift
の使い方についてはまだまだ自信ないので、アドバイス等あればぜひお願いします!
- 投稿日:2020-03-26T15:41:59+09:00
Youtubeのサムネイルを検索できるアプリをリリースしました!
はじめに簡単な自己紹介✍️
主にiOS開発をメインに勉強しております、私立文系の凡人大学生です、、、?
今年の目標は応用情報技術者試験に受かることです〜!リリースしたアプリ?
Youtubeのサムネイルを検索できるアプリになっています。
App Store
https://apps.apple.com/jp/app/%E5%8B%95%E7%94%BB%E5%88%86%E6%9E%90/id1502014149GitHub
https://github.com/geekshu625/YoutubeAPI
(改善点あればプルリクとか貰えますと泣いて喜びます)なぜ作ったか??
Youtubeのサムネイルを検索できるプラットフォームがない
最近、僕の友人にもYoutuberになる人が現れ、様々なツールやプラットフォームを駆使することで簡単に高クオリティの動画を制作できる時代になってきました。
僕自身はただの視聴者で、暇さえあればみてしまう中毒者です?
実際にYoutubeを見ている人はわかると思うのですが、動画を「見たい」か「見たくない」かはほぼサムネイルで選択していていると思います。
ですのでサムネイルを製作することは本編動画を作成するのとほぼ同等の重要性を持っていると考えられます。
では、実際に再生数を伸ばすサムネイルをどうやって作っているのかというと、再生数と好評価が多い動画のサムネイルを真似て作るらしいのです。
それらの条件にあったサムネイルをYoutubeで探すとなると、一覧で再生数はわかっても好評価は動画を開かないと見れず煩わしい仕様になっていることに気づきます。
それらの悩みをYoutuberから聞き、
「じゃあサムネイルに特化したプロットフォームを使ったら、、、」と思ったら1つもないことに気づきました。
その経緯があり、頻繁に使いたいからアプリで!ってことで作り始めました。開発期間?
しっかり取り掛かり始めたのが、今年に入ってからなので
2ヶ月弱ぐらいでメインの機能のみを作ってみました。使用した技術?
今回の開発にあたって技術的に課題としたのがRxSwift+MVVVMです。
技術書店で買った書籍や尊敬する方々の記事を参考にさせて頂きました(以下でまとめています)// -- API通信の一部 // Observable化したAPIレスポンスを返す static func thubnailRequest(searchText: String, nextPageToken: String) -> Observable<ThubnailRooter> { let baseUrl = APIEndpoint.thubnailURL(searchText: searchText, nextPageToken: nextPageToken) let url = URL(string: baseUrl) return Observable.create { observer -> Disposable in Alamofire.request(url!).response { (response) in guard let data = response.data else { return } do { if let errorModel = try? JSONDecoder().decode(APIErrorRooter.self, from: data) { let apierror = YoutubeAPIError.init(errorCode: errorModel.error.code) observer.onError(apierror) } let model = try JSONDecoder().decode(ThubnailRooter.self, from: data) observer.onNext(model) observer.onCompleted() } catch { let connectingError = ConnectionError(errorCode: error._code) observer.on(.error(connectingError)) } } return Disposables.create() } }Swiftの実践的な書き方がまだまだ身についていないことを自覚しており、自分が完全に理解できるコードのみで実装しました
(ジェネリクスなどをうまく活用してリファクタリングできる箇所がまだまだありますね、、、)
これまでMVCで構築することが多く、ロジックを分轄してもFatViewControllerになりがちで嫌気が指していました。
なので今回はMVVMを採用し、データバインディングにRxを用いることで処理の分担がスッキリさせることを意識しました。
しかし、まだまだRxの勉強は途中で悩む場面も多々ありました?
またUnit TestもAPI通信周り(Model)だけしかできていないのでUnit Test周辺も勉強していきたいです。その他の技術としては、デプロイ:fastlane、証明書管理:fastlane-match、ビルド:Xcodegenを導入しました。
CI/CD系も以後導入していきたいです。今回の開発を通して
今回の技術的なテーマであったRxSwiftですが、実用するのに難しい場面がたくさんありました、、、
記事や本を読んで理解したつもりでも、「いざ実践!」ってなった場合には「???」となることが多かったです。
またSwiftの書き方もまだまだなので、コーディングする機会を増やして知見を貯めていければなっと思ってます。参考文献
基本的なRxSwiftについて
・RxSwift研究読本1 入門編
・比較して学ぶRxSwift入門設計や実践的な使い方・記事構成
・あとで読むQiitaリーダーアプリをリリースしました今後追加していく機能
- いいね機能
- analyticsでサムネイル人気の統計図る
- 初期画面にジャンルタブバー設置
- リファクタリング etc...
- 投稿日:2020-03-26T04:06:28+09:00
Swift:Qiitaに投稿してある記事を全てMarkDownファイルとしてダウンロードするスクリプト
今自分にとって,とても需要のあるスクリプトを書きました.
使い方
まずは下記のSwiftスクリプトを
qiita_gyotaku.swift
という名前で保存します.次にTerminalを開いて上記のスクリプトがあるディレクトリにて,$ swift qiita_gyotaku.swift [Qiitaユーザ名]と叩けばそのディレクトリに
Articles
という名前のフォルダが生成され,その中に指定したユーザ名の全てのQiitaの記事がMarkDownとして保存されます.保存されるファイルの名前は記事の「タイトル名.md」です.画像を埋め込んでいる記事の場合、画像の引用がQiita頼り(もっといえばオンライン頼り)になってしまうので,画像も落としてきてローカルで読めるように書き換えています.なお,macOS Mojaveにて動作確認をしています.
スクリプトのソース
Gistにあげてあるので
clone
できますヽ(*゚д゚)ノ
https://gist.github.com/Kyome22/9525f59f3bdc8322cf18d0d1633575cbqiita_gyotaku.swiftimport Foundation class Article { let title: String var body: String init(_ title: String, _ body: String) { self.title = title self.body = body } var fileName: String { var name = title while name.hasPrefix(".") { name.removeFirst() } name = name.replacingOccurrences(of: ":", with: ":") name = name.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) return name + ".md" } } class Gyotaku { private static func createDirectory() { var dir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) dir.appendPathComponent("Articles") if !FileManager.default.fileExists(atPath: dir.path) { try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil) } dir.appendPathComponent("Images") if !FileManager.default.fileExists(atPath: dir.path) { try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true, attributes: nil) } } private static func getItemsCount(userName: String) -> (urlStr: String, itemsCount: Int)? { let semaphore = DispatchSemaphore(value: 0) let urlStr = "https://qiita.com/api/v2/users/\(userName)" guard let url = URL(string: urlStr) else { return nil } var itemsCount: Int = 0 let task = URLSession.shared.dataTask(with: url) { (data, response, error) in defer { semaphore.signal() } if let error = error { Swift.print("Failur: \(error.localizedDescription)") return } guard let data = data, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any], let count = json["items_count"] as? Int else { return } itemsCount = count } task.resume() semaphore.wait() if itemsCount == 0 { Swift.print("Failur: \(userName) has no article.") return nil } else if itemsCount == 1 { Swift.print("\(userName) has \(itemsCount) article") } else { Swift.print("\(userName) has \(itemsCount) articles") } return (urlStr, itemsCount) } private static func getArticle(_ urlStr: String, _ page: Int) -> [Article] { let semaphore = DispatchSemaphore(value: 0) var articles = [Article]() guard var components = URLComponents(string: "\(urlStr)/items") else { return articles } components.queryItems = [ URLQueryItem(name: "page", value: String(page)), URLQueryItem(name: "per_page", value: "100") ] let task = URLSession.shared.dataTask(with: components.url!) { (data, response, error) in defer { semaphore.signal() } if let error = error { Swift.print("Failur: \(error.localizedDescription)") return } guard let data = data, let items = try? JSONSerialization.jsonObject(with: data, options: []) as? NSArray else { return } articles = items.compactMap { (item) -> Article? in guard let obj = item as? [String : Any], let title = obj["title"] as? String, let body = obj["body"] as? String else { return nil } return Article(title, body) } } task.resume() semaphore.wait() return articles } static func renewalImages(body: inout String) { guard let regex = try? NSRegularExpression(pattern: "!\\[.+\\]\\((.+)\\)") else { return } let matches = regex.matches(in: body, range: NSRange(location: 0, length: body.count)) matches.reversed().forEach { (result) in let nsbody = NSString(string: body) let sentence = nsbody.substring(with: result.range) let imageURL = nsbody.substring(with: result.range(at: 1)) if Gyotaku.getImage(urlStr: imageURL) { let url = URL(string: imageURL)! let newImageURL = "./Images/\(url.lastPathComponent)" let newSentence = sentence.replacingOccurrences(of: imageURL, with: newImageURL) body = nsbody.replacingCharacters(in: result.range, with: newSentence) } } } static func getImage(urlStr: String) -> Bool { guard let url = URL(string: urlStr) else { return false } let semaphore = DispatchSemaphore(value: 0) var imageData: Data? let task = URLSession.shared.dataTask(with: url) { (data, response, error) in defer { semaphore.signal() } if let error = error { Swift.print("Failur: \(error.localizedDescription)") return } imageData = data } task.resume() semaphore.wait() guard let data = imageData else { return false } let saveUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent("Articles", isDirectory: true) .appendingPathComponent("Images", isDirectory: true) .appendingPathComponent(url.lastPathComponent) guard let _ = try? data.write(to: saveUrl, options: []) else { return false } return true } private static func saveArticles(_ articles: [Article]) { let dir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) .appendingPathComponent("Articles", isDirectory: true) articles.forEach { (article) in let saveURL = dir.appendingPathComponent(article.fileName) try? article.body.write(to: saveURL, atomically: true, encoding: .utf8) } Swift.print("Complete. Check Articles directory.") } static func main() { guard CommandLine.arguments.count == 2 else { Swift.print("$ swift qiita_gyotaku.swift [username]") return } guard let response = Gyotaku.getItemsCount(userName: CommandLine.arguments[1]) else { return } let articles = (0 ... Int(response.itemsCount / 100)).flatMap { (i) -> [Article] in return Gyotaku.getArticle(response.urlStr, i + 1) } Gyotaku.createDirectory() articles.forEach { (article) in Gyotaku.renewalImages(body: &article.body) } Gyotaku.saveArticles(articles) } } Gyotaku.main()