- 投稿日:2019-11-26T23:27:38+09:00
Swiftでビジネスロジックを実行するUseCaseのprotocolを作りたい話 2019
最初にこの文章の結論
この文章は、ロジックを処理する次のような型をアプリケーションの中で定義してみたらどうかな、と考えているのを文章にしてみたものです。
protocol UseCase { associatedtype Parameters associatedtype Success func execute( _ parameters: Parameters, completion: ((Result<Success, Error>) -> ())? ) func cancel() }なぜこんな事を考えているかというと、iOS VIPERアーキテクチャ研究読本(仮)という電子書籍を作ってみたいなと考えていて、まずはサンプルコードを作ろうとしているためです。
VIPER研究読本用サンプルコードのリポジトリ
https://github.com/yimajo/VIPERBook1SamplesUseCaseとは?
この記事でのUseCaseの前提として、
- システムを水平レイヤに分割したときのビジネスロジックを実装するもの
- 単一の目的を持ったコンポーネント
としてます。
この前提は、書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」での"ユースケース"から参考にしました。
注文入力システムに注文を追加するユースケースは、注文を削除するユースケースと比べると、明らかに異なる頻度と理由で変更される。ユースケースはシステムを分割する自然な方法である。また、ユースケースは、システムの水平レイヤーを薄く垂直にスライスしたものである。それぞれのユースケースは、UIの一部、アプリケーション特有のビジネスルールの一部、アプリケーションに依存しないビジネスルールの一部、データベース機能の一部を使用する。したがって、システムを水平レイヤーに分割するときには、それらを薄く垂直にユースケースとしても分割するのである。
ここまでで述べているのは、まず2つのユースケースがあるということ、そしてその分割方向についてです。
そして同書籍の中でユースケースの単位について「注文追加」「注文削除」を少し掘り下げています。それを読むと注文追加が
addOrder
で注文削除がdeleteOrder
とのことユースケースがお互いに切り離されていれば、addOrder(注文追加)のユースケースにフォーカスしたチームがdeleteOrder(注文削除)のユースケースにフォーカスしたチームの邪魔をする可能性は低い。
一応書いておくんですが、ユースケースは2つあるとは書いてあるもののそれらは2つのclassやstructとは書いてないんですね。むしろ
addOrder
とdeleteOrder
というのがメソッドにすら見える。そうなると「1つのclass/structに2つのメソッドがある」のかそれとも「2つのclass/structにそれぞれ1つのメソッドがある」のかということが気になってくるのですが、今回の話では後者の「2つのclass/structにそれぞれ1つのメソッドがある」ということでやっていきます。具体例
同書籍に書いてある内容をざっくりコードに落とすと、だいたい次のようなことだと思います
struct AddOrderUseCase { func execute(_ order: Order) } struct DeleteOrder { func execute(_ orderID: OrderID) }ここからテスト自動化できるようにしていくことや、その他のことを雑に考えると
protocol
を作ったりしたくなるというわけですよね。// AddOrder protocol AddOrderUseCase { func execute(_ order: Order) } struct AddOrderDefaultUseCase: AddOrderUseCase { func execute(_ order: Order) } struct テスト用AddOrderUseCase: AddOrderUseCase { func execute(_ order: Order) } // DeleteOrder protocol DeleteOrderUseCase { func execute(_ orderID: OrderID) } struct DeleteOrderDefaultUseCase: DeleteOrderUseCase { func execute(_ orderID: OrderID) } struct テスト用OrderDefaultUseCase: DeleteOrderUseCase { func execute(_ orderID: OrderID) }で、こういうことをやっていくともっと良いアプローチが無いかなと思うじゃないですか。...というのが発端です。
他の言語/プラットフォームではどうやってる?
Ruby on Railsでの例
まずは全然違う例としてサーバサイドを取り上げます。Ruby on RailsでオレオレUseCaseを考えている人たちも勿論いるので興味深いわけです。
A Case For UseCase
https://webuild.envato.com/blog/a-case-for-use-cases/上の記事でやりたいことを勝手に要約すると
- 写真を買うというユースケースがある
class PurchasePhoto
の内容- 購入時の細かい動作としては次の動作がある
- Purchaseテーブルにinsert
- 請求書の送付
- カウンタのインクリメント
- レビューの権利を付与する
class PurchasePhoto
の作り方として
- 抽象化したUseCaseというmoduleを作る
- PurchasePhotoはmoduleをimportする
コードとしては次のようにstaticにアクセスして上記3つの動作をさせたいわけです。
PurchasePhoto.perform(buyer, photo)写真購入のほかのパターンは書かれてませんが、
必要な情報はパラメータとしてメソッド実行時に揃ってますし、良さそうに見える。ただもともとデータベースに結果を入れていることもあり、テストコードはそのデータベースの値を見れば分かるようになっています。
しかし我々iOSアプリ開発するときはそういうことばっかりじゃないわけです。大抵は通信してJSONに色を付けないといけなかったりしますんで、その通信の結果を見るという作りからさらに改良してDBに保存してテストできるようにするというのは手間に感じます。
さらに非同期処理に対応しないといけない。というわけでこっからモバイルアプリのAndroidでのやり方を参考にしていきます。
Android Blue Print - todo-mvp-clean の場合
Android Blue Printを見てみます。
TODOアプリを様々な作り方でブランチごとに分けていて、MVPでClean Architectureっぽく作られてまして、そこでUseCaseは
abstract class
になってるわけです。public abstract class UseCase<Q extends UseCase.RequestValues, P extends UseCase.ResponseValue>もう少し細かい特徴は
- RequestとResponseの型を渡す
- 外からは
executeUseCase
メソッドで呼び出される
mUseCaseCallback
に成功か失敗を送るGetTaskのUseCaseを例にとった利用シーンとしては次のような感じ
mUseCaseHandler.execute(mGetTask, new GetTask.RequestValues(mTaskId), new UseCase.UseCaseCallback<GetTask.ResponseValue>() { @Override public void onSuccess(GetTask.ResponseValue response) { showTask(response.getTask()); } @Override public void onError() { showEmptyTaskError(); } });外部からはコールバックのハンドラーを登録して結果はそれが動作するイメージ。
同じパターンであることを示すためにSaveTaskも抜粋すると次のように共通点から、差分を見て具体的に何がやりたいかが分かるはずです。
mUseCaseHandler.execute(mSaveTask, new SaveTask.RequestValues(newTask), new UseCase.UseCaseCallback<SaveTask.ResponseValue>() { @Override public void onSuccess(SaveTask.ResponseValue response) { mAddTaskView.showTasksList(); } @Override public void onError() { showSaveError(); } });google/iosched の場合
同じAndroidで参考にしたいのは、Google IOアプリのコードgoogle/ioschedです。READMEでは「データレイヤーとプレゼンテーションレイヤーの間に、lightweight domain layer実装した。UIスレッドとは別にビジネスロジック処理する」と書いてUseCaseもあります。
invokeメソッドは大きく分けて2種類
- ObserverパターンのLiveDataを返す
operator fun invoke(parameters: P): LiveData<Result<R>>
- 即時実行してResultを返す
fun executeNow(parameters: P): Result<R>
これは前述のAndroid Blue Print - todo-mvp-clean で結果を全てコールバックで取得していたのと比較すると、「コールバックして返す必要のないものを戻り値としてすぐ取得できる」というメリットを感じます。
さて、実際の利用例として同期的に取得している
SearchUseCase
は次のような感じです。まずは簡単なexecuteNowから
val result = useCase.executeNow(parameters = "session 0")つぎにObserverパターンのLiveDataを返していたほう。
operator fun invoke
しているとおそらくobserveメソッドで呼び出すっていう感じになるんだと思います。val resultLiveData = useCase.observe() useCase.execute(FeedbackParameter( "userIdTest", TestData.userEvents[0], TestData.userEvents[0].id, mapOf("q1" to 1) )) // 結果を取り出して val result = LiveDataTestUtil.getValue(resultLiveData) // テストしてる assertEquals(Result.Success(Unit), result)でまあ何が言いたいかというと、結果をコールバックで返す処理/結果を戻り値で返す処理それぞれに応じたインタフェースを
abstract class
として用意していて、どちらかを実装していればそれが使えるし、実装していない方は使えないようになってるんじゃないでしょうか。本題: Swift でUseCaseをつくる
これをGitHubのWeb APIを利用したリポジトリ検索のビジネスロジックとしてUseCaseを作ってみようってのが本題です。
一番最初に述べたUseCaseをもう一度書いておきます。
protocol UseCase { associatedtype Parameters associatedtype Success func execute( _ parameters: Parameters, completion: ((Result<Success, Error>) -> ())? ) func cancel() }今回は1つのexecuteメソッドとcancelメソッドのみとし、executeメソッドはクロージャによって結果を取得します。できれば先述のexecuteNowのように単純なインタフェースを増やしたいところですが、OptionalなprotocolメソッドをSwiftで表現できないのが残念なところです。
なお、これまで紹介したUseCaseではcancelすることについて触れていませんでした。話がややこしくなるのでそこを掘り下げたりしないほうが良いかと思っています。RxSwiftなどを使ってObservableを戻り値に返せばそれを使って自動的にキャンセルもできるし、そもそもメソッドもコールバックなしのシンプルなものが用意できますが、一旦それは忘れましょう。
続いてこのUseCaseに準拠した具体的なclassについて考えます。
GithubRepoSearchInteractor
ではWeb APIにアクセスするGithubRepoSearchAPIRequest
を利用してその結果をクロージャで取得できるようにしています。class GithubRepoSearchInteractor: UseCase { var request: GithubRepoSearchAPIRequest? func execute(_ parameters: String, completion: ((Result<[GithubRepoEntity], Error>) -> ())?) { let request = GithubRepoSearchAPIRequest(word: parameters) request.perform { result in switch result { case .success(let response): completion?(.success(response.items)) case .failure(let error): completion?(.failure(error)) } } self.request = request } func cancel() { request?.cancel() } }実際に利用するのは次のようになると思えるでしょう。
let githubRepoSearch: UseCase = GithubRepoSearchInteractor()引数に使うんだったら
func なにかの関数(githubRepoSearch: UseCase) { // ... 省略 }しかしこれはコンパイルできません。
何が問題か
UseCaseのprotocolが
associatedtype
を使っていて、その型が解決していないことで次のようにエラーメッセージが表示されます。Protocol 'UseCase' can only be used as a generic constraint because it has Self or associated type requirements
これをなんとかしなければいけない。
Type Erasure: 継承 box 方式を使う
というわけでこのprotocolをそのまま使うのではなく、
AnyUseCase
classの引数にしつつ抽象的な扱いをするようにしたいわけです。let githubRepoSearch: AnyUseCase<String, [GithubRepoEntity]> = AnyUseCase(GithubRepoSearchInteractor())参考にさせてもらったのは次の記事の「type erasure: 継承 box 方式」
https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0実際にやってみると
// 実際の型情報として利用されるAnyUseCse final class AnyUseCase<Parameters, Success>: UseCase { // UseCaseの実体。Parameters, Successの型を合わせること private let box: AnyUseCaseBox<Parameters, Success> init<T: UseCase>(_ base: T) where T.Parameters == Parameters, T.Success == Success { box = UseCaseBox<T>(base) } func execute(_ parameters: Parameters, completion: ((Result<Success, Error>) -> ())?) { box.execute(parameters, completion: completion) } func cancel() { box.cancel() } } // MARK: - AnyUseCaseさえ知ってればいい情報 private extension AnyUseCase { class AnyUseCaseBox<Parameters, Success> { func execute(_ parameters: Parameters, completion: ((Result<Success, Error>) -> ())?) { fatalError() } func cancel() { fatalError() } } // Parameters, Success を UseCase のそれと合わせるために AnyUseCaseBox を継承する final class UseCaseBox<T: UseCase>: AnyUseCaseBox<T.Parameters, T.Success> { private let base: T init(_ base: T) { self.base = base } override func execute(_ parameters: T.Parameters, completion: ((Result<T.Success, Error>) -> ())?) { base.execute(parameters, completion: completion) } override func cancel() { base.cancel() } } }登場人物それぞれは次の役割となっています
- AnyUseCse
- 実際の型情報として外から利用する
- インタフェースを内部の AnyUseCaseBox に保持して利用する
- AnyUseCaseBox
- Parameters, Success を UseCase のそれと合わせるため
- UseCaseBox
- 内部に UseCase 自体を保持したい
具体的にこのAnyUseCseを利用するときは次のようになるはずです
func sample(_ githubRepoSearch: AnyUseCase<String, [GithubRepoEntity]>) { githubRepoSearch.execute("検索したいワード") { result in switch result { case .success(let items): // ... 省略 case .failure(let error): // ... 省略 } } } sample(AnyUseCase(GithubRepoSearchInteractor()))デメリット
全てコールバックでデータを取得することになる
同期的に戻り値で取得するようなデータでさえ、クロージャを使いコールバックされることになります。
最後に
もうちょっと他の良いやり方はないもんでしょうかね?他の案を比較できればもっと良くできるのではと思ってます...
みなさんはどんなUseCaseを作ってますか?
- 投稿日:2019-11-26T19:56:56+09:00
[はじめてのiOSアプリ]xcodeで地図アプリを作成(その3)
はじめに
iOSアプリを作ってみたいけど
何から始めて良いのかわからないとりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思うという記事の3回目です。
今回は、位置情報を取得します。
位置情報の取得
CoreLocationをインポート
- 画面左側のファイルツリーから[ViewController.swift]を選択し、画面中央に表示されるエディタで、以下のように修正
- 【なぜ?】
- 位置情報ライブラリ(CoreLocation)を使うことを宣言することで、位置情報取得のプログラムを記述できるようになる
ViewController.swiftimport UIKit import CoreLocation // この行を追加 class ViewController: UIViewController {CoreLocation 用の変数を追加
- エディタで、以下のように修正
- 【なぜ?】
- この変数を通してプログラムで位置情報を取り扱うため
ViewController.swiftclass ViewController: UIViewController { var locationManager: CLLocationManager! // この行を追加 override func viewDidLoad() {CoreLocation の delegate の使用を宣言(プロトコルの継承)
- エディタで、以下のように修正
- 【なぜ?】
- プロトコルを継承することで、位置情報更新などのイベントに対する処理をプログラムすることができるようになる
- 今回は ViewController.swift で継承したことで、クラスが増えずシンプルな実装とすることができた
- (今回はしないが)別クラスで継承・実装すれば再利用性の高いクラスとできると思う
ViewController.swiftclass ViewController: UIViewController, CLLocationManagerDelegate { //この行を修正 var locationManager: CLLocationManager! override func viewDidLoad() {CoreLocation 変数の初期化とCoreLocationの初期処理
- エディタで、以下のように修正
- 【なぜ?】
- 変数は、初期化しないと使えない
- 位置情報更新時のイベントを処理するプログラムを指定する必要がある
- 明示的に位置情報取得開始を指示する必要がある
- 利用者から、このアプリで位置情報を使う許可をもらう必要がある
ViewController.swiftoverride func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // この行↓から locationManager = CLLocationManager() // 変数を初期化 locationManager.delegate = self // delegateとしてself(自インスタンス)を設定 locationManager.startUpdatingLocation() // 位置情報更新を指示 locationManager.requestWhenInUseAuthorization() // 位置情報取得の許可を得る // この行↑までを追加 }位置情報更新時に呼び出される処理を記述
- エディタで、以下のように修正
- 【なぜ?】
- 位置情報更新時に呼び出される処理
- 最新(last)の位置情報から緯度経度を取り出している
- 地図と連携する場合は、最新の位置を用いて地図を更新すればよいはず
ViewController.swiftoverride func viewDidLoad() { // 表示の都合上、プログラムを省略しています } // この行↓から func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) { let longitude = (locations.last?.coordinate.longitude.description)! let latitude = (locations.last?.coordinate.latitude.description)! print("[DBG]longitude : " + longitude) print("[DBG]latitude : " + latitude) } // この行↑までを追加位置情報使用許可文字列を設定
- 画面左側のファイルツリーから[MyGpsMap]-[Info.plist]を選択
- Ctrl+クリックでコンテキストメニューを表示し[Open As]-[Source Code]を選択(↓のようにテキストエディタ形式で表示される)
- ファイルの後ろ付近に以下の行を追加
- 【なぜ?】
- 利用許可画面に表示される文字列
- この文字列設定が存在しないと、利用許可を完了できず位置情報が取得できない
Info.plist</array> <!-- ここ↓から追加 --> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <string>常に位置情報の利用を許可する理由/目的を書く</string> <key>NSLocationWhenInUseUsageDescription</key> <string>アプリ起動時に位置情報の利用を許可する理由/目的を書く</string> <!-- ここ↑まで追加 --> </dict> </plist>テスト実行
今回の到達点
- 位置情報(GPS)が取得できるようになった
参考情報
ViewController.swift// // ViewController.swift // MyGpsMap // // Created by shinobee on 2019/11/24. // Copyright © 2019 shinobee. All rights reserved. // import UIKit import CoreLocation class ViewController: UIViewController, CLLocationManagerDelegate { var locationManager: CLLocationManager! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. locationManager = CLLocationManager() locationManager.delegate = self locationManager.startUpdatingLocation() locationManager.requestWhenInUseAuthorization() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) { let longitude = (locations.last?.coordinate.longitude.description)! let latitude = (locations.last?.coordinate.latitude.description)! print("[DBG]longitude : " + longitude) print("[DBG]latitude : " + latitude) } }連載
- 投稿日:2019-11-26T18:37:21+09:00
MobileNetV2 サンプルコードを解読
概要
数回に渡って、CoreModelの学習モデルのiOSでの使い方について、サンプルコードのCoreMLに関わる部分を中心に解読していく。
・参考URL
https://developer.apple.com/jp/machine-learning/models/#text今回はMobileNetV2を解読する。
MobileNetV2
MLModelセットアップ
// MARK: - Image Classification /// - Tag: MLModelSetup lazy var classificationRequest: VNCoreMLRequest = { do { /* Use the Swift class `MobileNet` Core ML generates from the model. To use a different Core ML classifier model, add it to the project and replace `MobileNet` with that model's generated Swift class. */ let model = try VNCoreMLModel(for: MobileNet().model) let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in self?.processClassifications(for: request, error: error) }) request.imageCropAndScaleOption = .centerCrop return request } catch { fatalError("Failed to load Vision ML model: \(error)") } }()一箇所ずつ何をやっているか解読。
// MobileNet modelからMobileNetクラスを生成し、そのクラスからインスタンスを生成 let model = try VNCoreMLModel(for: MobileNet().model)// VNCoreMLRequestオブジェクトを生成、 // リクエスト後、モデルから結果を受け取るメソッドを指定するためにcompletionHandlerを使用。 let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in self?.processClassifications(for: request, error: error) })// 画像のスケール方法を決定 request.imageCropAndScaleOption = .centerCropリクエスト実行
/// - Tag: PerformRequests func updateClassifications(for image: UIImage) { classificationLabel.text = "Classifying..." let orientation = CGImagePropertyOrientation(image.imageOrientation) guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") } DispatchQueue.global(qos: .userInitiated).async { let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation) do { try handler.perform([self.classificationRequest]) } catch { /* This handler catches general image processing errors. The `classificationRequest`'s completion handler `processClassifications(_:error:)` catches errors specific to processing that request. */ print("Failed to perform classification.\n\(error.localizedDescription)") } } }// 画像の向きを取得 // imageからCIImageに変換 let orientation = CGImagePropertyOrientation(image.imageOrientation) guard let ciImage = CIImage(image: image) else { fatalError("Unable to create \(CIImage.self) from \(image).") }DispatchQueue.global(qos: .userInitiated).async { let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation) do { try handler.perform([self.classificationRequest]) } catch { /* This handler catches general image processing errors. The `classificationRequest`'s completion handler `processClassifications(_:error:)` catches errors specific to processing that request. */ print("Failed to perform classification.\n\(error.localizedDescription)") } }プロセス分類
/// Updates the UI with the results of the classification. /// - Tag: ProcessClassifications func processClassifications(for request: VNRequest, error: Error?) { DispatchQueue.main.async { guard let results = request.results else { self.classificationLabel.text = "Unable to classify image.\n\(error!.localizedDescription)" return } // The `results` will always be `VNClassificationObservation`s, as specified by the Core ML model in this project. let classifications = results as! [VNClassificationObservation] if classifications.isEmpty { self.classificationLabel.text = "Nothing recognized." } else { // Display top classifications ranked by confidence in the UI. let topClassifications = classifications.prefix(2) let descriptions = topClassifications.map { classification in // Formats the classification for display; e.g. "(0.37) cliff, drop, drop-off". return String(format: " (%.2f) %@", classification.confidence, classification.identifier) } self.classificationLabel.text = "Classification:\n" + descriptions.joined(separator: "\n") } } }上記は、分類結果をラベルに表示している。
- 投稿日:2019-11-26T18:05:37+09:00
実機iPhoneのSafariでWebインスペクタを使ってデバッグする方法
要望
実機のiPhoneで特定のWebページに対し、Webインスペクタで中身を書き換えたり、コンソール経由でスクリプトを叩いて実行した結果を見たい。
-> Android版
iPhoneのSafariにもWebインスペクタがある
iPhone単体ではView Sourceなどのアプリを使うことで、Webページのソース確認はできるものの、Webインスペクタやコンソールによるスクリプト実行が出来ない。
また、MacのSafariやiOSエミュレーターでもある程度の表示確認はできるが、やはり実機での操作感や見た目の確認は重要である。
少し調べてみたところ、MacとiPhoneを接続することで、PCと同様に実機での動作確認が可能なことが判明したため、実際に試してみた。
iPhone Safariの設定
事前準備としてiPhone側の設定を行う。「設定」を開き「Safari」をタップ。
「Webインスペクタ」をONにする。
Mac Safariの設定
Safariの「環境設定」を開き、「詳細」タブの「メニューバーに"開発"メニューを表示」にチェックを入れる。
MacとiPhoneをケーブル接続。Safariの「開発」から実機iPhoneを選択し、対象サイトを選択。
通常と同じ操作感で要素検証や書き換えが可能。
もちろんコンソールも使えるのでスクリプトを叩いて実行することも可能。
いえーい
Macに接続したiPhoneが認識されないことがあるけど、ケーブル抜き差ししてると出てくる。
- 投稿日:2019-11-26T17:55:58+09:00
AppStoreやAppleDeveloperProgramまわりの忘れがちなこと
対象
iOS開発に慣れて来た頃に、証明書の更新などたまにしかやらない故に忘れがちなことをまとめていきます。
一度は、証明書は完全に理解した!
と思った方向けです。仕様が頻繁に変わるので各項目ごとに最終確認日を掲載しています。
Apple Developer Programの場合です。Apple Enterprise Developer Programでは違う箇所があるのでご注意下さい。
iOS Distributionの期限が切れたときの心配事
確認日2019/05/07
- Adhoc/Productionでのビルドができなくなる
- ストア公開中のアプリには影響ない
- Adhoc配信したアプリは起動できなくなる
- Push通知は届く
証明書って何個まで作れる?
確認日2019/05/07
上限まで作成すると以下の文言が表示されます
(Maximum number of certificates generated)
Entity Typeが「Individual」 (個人または個人事業主/個人経営者)の場合
- iOS Distribution:2
- iOS Development:3
Entity Typeが「Company / Organization」(組織) の場合
- iOS Distribution:3
- iOS Development:6
Entity Typeが「In-House / Enterprise」の場合
- iOS Distribution:未確認
- iOS Development:未確認だが100くらいいけそう (情報求む)
Apple Developer Programの有効期限は?
確認日2019/06/04
- iOS Developer University Program
- 有効期限なし
証明書の有効期限は?
確認日2019/06/04
Entity Typeが「Individual」 (個人または個人事業主/個人経営者)の場合
- iOS Distribution:2
- iOS Development:3
Entity Typeが「Company / Organization」(組織) の場合
- iOS Distribution:3
- iOS Development:6
Entity Typeが「In-House / Enterprise」の場合
- iOS Distribution:未確認
- iOS Development:未確認だが100くらいいけそう (情報求む)
Push通知が届かない期間は?
Push通知証明書を別途作成してAppIDに設定 〜 外部サービスに登録、までは届かなくなる?
- Development / Production共に2つずつPush通知証明書を設定可能なのでスキマはない
AppIDsにはPush証明書Production/Development2つずつ割り当て可能
- とはいえ、Push通知の外部サービスに証明書を登録するので同時に使える証明書は1つ
Provisioning Profile
Provisioning Profileはアプリ内包なので、インストール済アプリには変更が効かない
ただ、基本、Distribution/Development証明書より1ヶ月期限が長いはずおわり
変更や間違っている箇所があればコメントお願いします!
年1の作業とか毎年忘れますね。
- 投稿日:2019-11-26T15:52:25+09:00
UITableViewController の タブボタンをカスタマイズしてみる
0.はじめに
UITableViewController のタブボタンを色々とカスタマイズしたかったので、やってみました。
試してみたのは、以下。
- タブボタンのサイズを個別に変更する
- タブボタンの背景色を個別に変更する
- どのタブも初期選択されない様にしてみる
ということで、どのタブも初期選択されない様にしてみた画像が、こんな感じ。
本当は、4つタブがあるんですが、表示されていません。
1.コード
TabBarController.swift// // TabBarController.swift // UITableViewController-Sample01 // // Created by Kusokamayarou on 2019/11/26. // Copyright © 2019 Makurazakiutoya. All rights reserved. // import UIKit class TabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() self.delegate = self // 「AutoLayoutをコードから指定する, アニメーションさせながらAutoLayoutを変更する - Qiita」 // <https://qiita.com/yokurin/items/4932ab488b5b503f2dd5> // 「ios - Swift | Adding constraints programmatically - Stack Overflow」 // <https://stackoverflow.com/questions/26180822/swift-adding-constraints-programmatically> let tabBarButtons = self.tabBarButtons() // item 1 tabBarButtons![0].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![0], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![0], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![0], attribute: .left, relatedBy: .equal, toItem: tabBar, attribute: .left, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![0], attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 0) ]) // item 2 tabBarButtons![1].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![1], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![1], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![1], attribute: .left, relatedBy: .equal, toItem: tabBarButtons![0], attribute: .right, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![1], attribute: .width, relatedBy: .equal, toItem: tabBar, attribute: .width, multiplier: 0.3333, constant: 0) ]) // item 3 tabBarButtons![2].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![2], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![2], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![2], attribute: .left, relatedBy: .equal, toItem: tabBarButtons![1], attribute: .right, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![2], attribute: .right, relatedBy: .equal, toItem: tabBarButtons![3], attribute: .left, multiplier: 1, constant: 0) ]) // item 4 tabBarButtons![3].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![3], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![3], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![3], attribute: .right, relatedBy: .equal, toItem: tabBar, attribute: .right, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![3], attribute: .width, relatedBy: .equal, toItem: tabBar, attribute: .width, multiplier: 0.3333, constant: 0) ]) // ios - How to definitively set UITabBar background color and UITabBar tint color - Stack Overflow // https://stackoverflow.com/questions/37626377/how-to-definitively-set-uitabbar-background-color-and-uitabbar-tint-color self.tabBar.barTintColor = .systemBackground } // 「小ネタ:UITabBarControllerに「モーダル表示するボタン」を追加する(Swift) - Qiita」 // <https://qiita.com/paming/items/a1413480358fa81728cf> override func viewDidLayoutSubviews() { let tabBarButtons = self.tabBarButtons() UIView.setAnimationsEnabled(false) // item 1 tabBarButtons![0].alpha = 0.0 tabBarButtons![0].backgroundColor = .red tabBarButtons![0].layoutIfNeeded() // item 2 tabBarButtons![1].backgroundColor = .cyan tabBarButtons![1].layoutIfNeeded() // item 3 tabBarButtons![2].backgroundColor = .green tabBarButtons![2].layoutIfNeeded() // item 4 tabBarButtons![3].backgroundColor = .yellow tabBarButtons![3].layoutIfNeeded() UIView.setAnimationsEnabled(true) } } extension TabBarController: UITabBarControllerDelegate { // 「swiftでUITabBarの特定のタブをタップした時にモーダル - Qiita」 // https://qiita.com/higan96/items/5ea742b59a48a34baa32 internal func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { self.viewDidLayoutSubviews() return true } // 「[Swift]タブ切り替え時に切り替え先のメソッドを実行する」 // https://nobuhiroharada.net/2018/04/06/change-tab/ internal func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { } } // 「Swift - UITabbarControllerにモーダル表示するボタンを追加(68046)|teratail」 // <https://teratail.com/questions/68046> extension TabBarController { func tabBarButtons() -> [UIView]? { return self.tabBar.subviews.reduce([], { (ret: [UIView], item:AnyObject) -> [UIView] in if let v = item as? UIView { if v.isKind(of: NSClassFromString("UITabBarButton")!) { return ret + [v] } } return ret }) } }■ タブボタンにアクセスするには?
これが無いと何も変更できません…。
- 以下の記事を参考に、
tabBarButtons()
を作成します。■ タブボタンのサイズを個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
で各タブボタンに Constraints を設定します。■ タブボタンの背景色を個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
でタブバーの背景色を設定します。- 以下の記事を参考に、
viewDidLayoutSubviews()
を作成し、初期表示時のタブボタンの背景色を設定します。- 以下の記事を参考に、
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
を作成し、タブ選択時のタブボタンの背景色を設定します。■ どのタブも初期選択されない様するには?
これまでの設定の応用になります。
- 初期選択される左端のタブの幅を0にして、他のタブボタンのサイズをうまいこと調整します。
- 背景色の設定と同様のやり方で、alpha 値を 0.0 にして、タブを見えなくします。
99.ハマりポイント
- 結構、ハマったと思いますが…、結構前にやったので覚えてません…。
???
XX.まとめ
他にも色々とカスタマイズ出来るかもしれませんね♪
以下、GitHub にも UP してますので、参考になれば♪
?♂️?♂️?♂️
- 投稿日:2019-11-26T15:52:25+09:00
UITabBarController の タブボタンをカスタマイズしてみる
0.はじめに
UITabBarController のタブボタンを色々とカスタマイズしたかったので、やってみました。
試してみたのは、以下。
- タブボタンのサイズを個別に変更する
- タブボタンの背景色を個別に変更する
- どのタブも初期選択されない様にしてみる
ということで、どのタブも初期選択されない様にしてみた画像が、こんな感じ。
本当は、4つタブがあるんですが、表示されていません。
1.コード
TabBarController.swift// // TabBarController.swift // UITableViewController-Sample01 // // Created by Kusokamayarou on 2019/11/26. // Copyright © 2019 Makurazakiutoya. All rights reserved. // import UIKit class TabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() self.delegate = self // 「AutoLayoutをコードから指定する, アニメーションさせながらAutoLayoutを変更する - Qiita」 // <https://qiita.com/yokurin/items/4932ab488b5b503f2dd5> // 「ios - Swift | Adding constraints programmatically - Stack Overflow」 // <https://stackoverflow.com/questions/26180822/swift-adding-constraints-programmatically> let tabBarButtons = self.tabBarButtons() // item 1 tabBarButtons![0].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![0], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![0], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![0], attribute: .left, relatedBy: .equal, toItem: tabBar, attribute: .left, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![0], attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 0) ]) // item 2 tabBarButtons![1].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![1], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![1], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![1], attribute: .left, relatedBy: .equal, toItem: tabBarButtons![0], attribute: .right, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![1], attribute: .width, relatedBy: .equal, toItem: tabBar, attribute: .width, multiplier: 0.3333, constant: 0) ]) // item 3 tabBarButtons![2].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![2], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![2], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![2], attribute: .left, relatedBy: .equal, toItem: tabBarButtons![1], attribute: .right, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![2], attribute: .right, relatedBy: .equal, toItem: tabBarButtons![3], attribute: .left, multiplier: 1, constant: 0) ]) // item 4 tabBarButtons![3].translatesAutoresizingMaskIntoConstraints = false self.view.addConstraints([ NSLayoutConstraint(item: tabBarButtons![3], attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![3], attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .bottom, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![3], attribute: .right, relatedBy: .equal, toItem: tabBar, attribute: .right, multiplier: 1, constant: 0), NSLayoutConstraint(item: tabBarButtons![3], attribute: .width, relatedBy: .equal, toItem: tabBar, attribute: .width, multiplier: 0.3333, constant: 0) ]) // ios - How to definitively set UITabBar background color and UITabBar tint color - Stack Overflow // https://stackoverflow.com/questions/37626377/how-to-definitively-set-uitabbar-background-color-and-uitabbar-tint-color self.tabBar.barTintColor = .systemBackground } // 「小ネタ:UITabBarControllerに「モーダル表示するボタン」を追加する(Swift) - Qiita」 // <https://qiita.com/paming/items/a1413480358fa81728cf> override func viewDidLayoutSubviews() { let tabBarButtons = self.tabBarButtons() UIView.setAnimationsEnabled(false) // item 1 tabBarButtons![0].alpha = 0.0 tabBarButtons![0].backgroundColor = .red tabBarButtons![0].layoutIfNeeded() // item 2 tabBarButtons![1].backgroundColor = .cyan tabBarButtons![1].layoutIfNeeded() // item 3 tabBarButtons![2].backgroundColor = .green tabBarButtons![2].layoutIfNeeded() // item 4 tabBarButtons![3].backgroundColor = .yellow tabBarButtons![3].layoutIfNeeded() UIView.setAnimationsEnabled(true) } } extension TabBarController: UITabBarControllerDelegate { // 「swiftでUITabBarの特定のタブをタップした時にモーダル - Qiita」 // https://qiita.com/higan96/items/5ea742b59a48a34baa32 internal func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { self.viewDidLayoutSubviews() return true } // 「[Swift]タブ切り替え時に切り替え先のメソッドを実行する」 // https://nobuhiroharada.net/2018/04/06/change-tab/ internal func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) { } } // 「Swift - UITabbarControllerにモーダル表示するボタンを追加(68046)|teratail」 // <https://teratail.com/questions/68046> extension TabBarController { func tabBarButtons() -> [UIView]? { return self.tabBar.subviews.reduce([], { (ret: [UIView], item:AnyObject) -> [UIView] in if let v = item as? UIView { if v.isKind(of: NSClassFromString("UITabBarButton")!) { return ret + [v] } } return ret }) } }■ タブボタンにアクセスするには?
これが無いと何も変更できません…。
- 以下の記事を参考に、
tabBarButtons()
を作成します。■ タブボタンのサイズを個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
で各タブボタンに Constraints を設定します。■ タブボタンの背景色を個別に変更するには?
- 以下の記事を参考に、
viewDidLoad()
でタブバーの背景色を設定します。- 以下の記事を参考に、
viewDidLayoutSubviews()
を作成し、初期表示時のタブボタンの背景色を設定します。- 以下の記事を参考に、
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
を作成し、タブ選択時のタブボタンの背景色を設定します。■ どのタブも初期選択されない様するには?
これまでの設定の応用になります。
- 初期選択される左端のタブの幅を0にして、他のタブボタンのサイズをうまいこと調整します。
- 背景色の設定と同様のやり方で、alpha 値を 0.0 にして、タブを見えなくします。
99.ハマりポイント
- 結構ハマったと様に思いますが…、ずいぶん前にやったので覚えてません…。
???
XX.まとめ
他にも色々とカスタマイズ出来るかもしれませんね♪
以下、GitHub にも UP してますので、参考になれば♪
?♂️?♂️?♂️
- 投稿日:2019-11-26T13:01:30+09:00
iOS 9 の端末でIn-Houseがダウンロードできない時メモ
iOSアプリをInHouseで配布したのでいざiOS9端末で取ろうとしたら
「アプリ名 は現在ダウンロードできません」 のダイアログが出たのでその原因メモ結論
InHouseのipa置き場に置く、plistファイルのbundleIDが実際のものと違っていた
原因
ipaのbundleId
jp.bizen.ios.exemple.app-enterprise
InHouse plist
jp.bizen.ios.exemple.app-e
開発途中でbundleIdの管理方法が変わってしまったため起きた。
Xcodeに繋いでログを見ながらインストールしないと気づかなかったですXcode 11 でインストール失敗のログを読む
- Xcodeに端末を接続
- 上部 [Window] -> [Devices and Simulators] -> [Open Console]
- 右上のフィルタに Failed と入れる
- ダウンロードを開始する
ずらずら出てくるが
BundleValidator: Failed bundleIdentifier: jp.bizen.ios.exemple.app-e does not match expected bundleIdentifier: jp.bizen.ios.exemple.app-enterprise [ApplicationWorkspace]: Failed to install application: jp.bizen.ios.exemple.app-e; /var/mobile/Media/Downloads/-..../-....; Error Domain=SSErrorDomain Code=143 "(null)"こんな感じで出てきます。
- 投稿日:2019-11-26T12:27:54+09:00
GameControllerを使ってDualshock4でUIViewを操作してみる
iOS13から、iPhoneでPS4やXboxのコントローラーが利用可能になりました。
コントローラーに対応したアプリで利用することができます。どんなふうに使うのか試しに触ってみました。
コントローラーと接続する
コントローラーの接続は以下の2つのNotificationでハンドリングすることができます。
GCControllerDidConnect - NSNotification.Name | Apple Developer Documentation
GCControllerDidDisconnect - NSNotification.Name | Apple Developer Documentationこれらを使うには
GameController
をimportする必要があります。import GameControllerNotificationCenterを登録します。
NotificationCenter.default.addObserver(self, selector: #selector(handleControllerDidConnect), name: .GCControllerDidConnect, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(handleControllerDidConnect), name: .GCControllerDidDisconnect, object: nil)これでコントローラーとの接続が確立したときと、切断されたときにそれぞれ通知を受け取ることができます。
コントローラーの入力をハンドリングする
コントローラの入力のハンドリングは GCExtendedGamepad - Game Controller | Apple Developer Documentation を使います。
これはNotification#object
から取得することができます。@objc func handleControllerDidConnect(notification: Notification) { guard let controller = notification.object as? GCController, let gamepad = controller.extendedGamepad else { return } ・・・ }
GCExtendedGamepad
はコントローラーの入力値や入力に対するハンドラを持っており、それぞれ以下のプロパティでアクセスできます。
※一部ボタンは省略しています。現在値を取得する場合は
// ×ボタンが押されているかどうか let isButtonAPressed = extendedGamepad.buttonA.isPressed // 右スティックの水平方向の傾き(-1~1) let rightThumbstickX = extendedGamepad.rightThumbstick.xAxis.value // 右スティックが上方向に傾けられているかどうか let isRightThumbstickUpPressed = extendedGamepad.rightThumbstick.up.isPressed入力を受け取ったタイミングでハンドリングしたい場合は
// ○ボタンが押された gamepad.buttonB.pressedChangedHandler = { (input, value, isPressed) in ・・・ } // OPTIONSボタンが押された gamepad.buttonMenu.pressedChangedHandler = { (input, value, isPressed) in ・・・ } // 右スティックが傾けられた gamepad.rightThumbstick.valueChangedHandler = { (pad, xAxis, yAxis) in ・・・ }というように使います。
UIViewを操作してみる
以下のようなサンプルを作ってみました。
- ランダムな
UIColor
のリストを表示し、セルをタップするとその色のHEX値をアラートで表示
- 左スティックでTableViewをスクロール
- 右スティックでポインターを動かす
- ○ボタンでセルを選択
- OPTIONSボタンで配列の中身を新しくする
https://github.com/yoshimin/DualshockSample
コントローラーとiPhoneの接続は以下のように行います。
- iPhoneのBluetoothをONにしておく
- SHAREボタンを長押ししながら、ライトバーが点滅するまでPSボタンを長押しする
- Bluetoothの設定画面上にDUALSHOCKが表示されるのでタップするとペアリングされます
この状態でアプリを起動するとすぐに
GCControllerDidConnect
が呼ばれ入力を受け取ることができます。簡単ですが実装してみた所感を以下にまとめます。
スクロールやポインターの移動など継続的な動きは現在値を使う
最初右スティックを使ってポインターを動かすのに
rightThumbstick.valueChangedHandler
を使ってみたのですが、これは値が変化したときにのみ発火するものなので、傾けたままの場合、最初に傾けたあとポインタが動かず止まってしまいました。
なので、GCControllerDidConnect
の通知で受け取ったGCExtendedGamepad
を保持しておきCADisplayLink
を使ってフレームごとにleftThumbstick.xAxis
leftThumbstick.yAxis
を見てポインタを動かすようにしました。逆に、ボタンの入力などその時々で処理したいイベントは
pressedChangedHandler
などのハンドラーを使います。ボタンの押下イベントは
isPressed
をみるclosure の引数に
isPressed
があるので容易に想像できるかも知れませんが、ボタンの入力は押したときと離したときの2回呼ばれます。
GCControllerButtonValueChangedHandler最初なにも気にせずに
pressedChangedHandler
の中に処理を書いていたら、処理が2回呼ばれていました...
ボタンが押し込まれたときに処理したい場合はisPressed == true
であることを確認する必要があります。まとめ
ゲームに使うものと思っていましたが、UIKit製のアプリでも普通に使えるのだなと思いました。
コントローラーの入力が受け取れるだけなので、それを受け取ってどうするかは実装次第で、何でもできます。
とはいえゲーム以外の需要はほとんどないと思いますが、普段PS4でAmazonビデオやNetflixで見ている私としてはiPadで動画アプリで視聴しているときに再生/一時停止/早送り/巻き戻しとかをコントローラーで操作できるのもありな気がしました。
- 投稿日:2019-11-26T11:52:10+09:00
iOS アプリ公開 スクリーンショットでハマった
- 投稿日:2019-11-26T11:51:23+09:00
iOS申請→拒否られApple Connectへ修正バージョンを再アップしたが反映されない
- 投稿日:2019-11-26T11:24:00+09:00
ホーム画面に追加したspaサイトで遷移すると上部にバーが出るのを消す
iPadでspaで簡単なサイトを作って「ホームに画面に追加」をして疑似アプリケーション化して使っていたのですが、いつのまにか遷移すると「上部にバー」が出るようになってしまいました。「完了」を押すとバー自体は消えるのですが、せっかくの専用端末化が台無しです。ということで調査しました。
環境
- iPadOS 13.1.3
たぶん、どこかのバージョンで治る気がしますが...。
方法
結論から書くとmanifestファイルを追加します。
index.html<!-- タイトルバーを消す設定 --> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <!-- 追加 --> <link rel="manifest" href="manifest.json">manifest.json{ "name": "サンプルアプリ", "short_name": "サンプル", "display": "standalone", "scope": "/", "start_url": "/" }nuxtの場合
- manifest.jsonをstaticに入れます
- nuxt.config.jsに以下を追加します。
nuxt.config.jshead: { ... meta: [ ... { name: 'apple-mobile-web-app-capable', content: 'yes' }, { name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' } ... ] ... link: [ { rel: 'manifest', href: '/manifest.json'}, ] } ...メモ
前はmanifest無くてもできていたと思うのですが...。ハマったのでメモしておきます。
- 投稿日:2019-11-26T01:23:00+09:00
SDWebImage5にcacheMemoryOnlyオプションがいない
概要
SDWebImageを4系から5系にアップデートする必要があったのですが、
ディスクキャッシュを使用しないcacheMemoryOnly
オプションがoptions
から消えたようです。
contextが使えない方向けのピンポイントなマイグレーションガイドです。経緯
StackOverFlowで秒殺だと思ったのですが、意外に簡潔なSwift用サンプルが見つからず。
公式GitHubで同じ質問をしてる人がいましたが、ちょっと分かりづらい気がしたので記事にしました。
https://github.com/SDWebImage/SDWebImage/issues/2729一応、issueにある回答としては以下になります。
imageView.sd_setImage(with: url, placeholderImage: nil, context: [.storeCacheType : SDImageCacheType.memory.rawValue])contextに辞書でいれるだけですね。
ただ、ホントにrawValue
まで記載するの?と気になることばかりです。reference
これだけだとPRを通す根拠に欠けるのでリファレンスを探しました。
SDWebImage - 5.0 Migration guide
https://github.com/SDWebImage/SDWebImage/wiki/5.0-Migration-guide
SDWebImageOptionsの項目から引用
SDWebImageCacheMemoryOnly removed, use @{SDWebImageContextStoreCacheType : @(SDImageCacheTypeMemory)} context option instead. For Swift user, using .storeCacheType: SDImageCacheType.memory.rawValue.
冗長に見えますが先程の回答の実装で問題なさそうです。
Migrationつまづきポイント
SDWebImage4系のときのコード▼
imageView.sd_setImage(with: url, placeholderImage: UIImage(named: "hoge"), options: [.cacheMemoryOnly, .retryFailed], completed: {(image, error, ctype, url) in5系だとこんな感じか?▼
imageView.sd_setImage(with: url, placeholderImage: UIImage(named: "hoge"), options: [.retryFailed], context: [.storeCacheType: SDImageCacheType.memory.rawValue], completed: {(image, error, ctype, url) in上記にするとコンパイルエラーで変更案が表示され、従うとcontextがprogressに...
→ progressに辞書を渡してももちろんダメ原因
ライブラリの中を追えば分かるのですが、
UIImageView+WebCache.h
を見ると
sd_setImage()
メソッドは、placeholderImage
,options
,progress
,completed
全ての組み合わせを網羅しているわけではありません。
一番近いオーバーライドが以下だったので、contextがprogressに提案されたのでしょう。- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock;以下を利用すれば大丈夫そうですね。
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock;修正したコード ▼
imageView.sd_setImage(with: url, placeholderImage: UIImage(named: "hoge"), options: [.retryFailed], context: [.storeCacheType: SDImageCacheType.memory.rawValue], progress: nil, completed: {(image, error, ctype, url) inおわり
完成されたライブラリだと思っていたので、ここにきてなかなかの変更ですね!
SDWebImageのポテンシャルをフル活用するのだけが目的のアプリつくるのも楽しそうですw