- 投稿日:2020-03-16T22:41:51+09:00
【iOS】SDWebImageで画像のリサイズと角丸を実現
URLから取得した画像を編集して表示したい
LINEやinstagramのDMように、アップロードしたオリジナル画像をリサイズして角丸をつけて表示する、画像送信機能付きのチャット画面のようなものを作りたいことがあります。
手動でもやれると思いますが、URLの画像への変換まで一気通貫でやりたいケースを想定し、ライブラリを使います。いくつかの代表的なものの中から
今回はSDWebImageを使います。特に角丸の実装方法が、SDWebImageの公式ドキュメントからは見つかりにくかったので書いておきます。
前提条件
画像を表示するための基本的なしつらえ(tableViewのレイアウトやdelegateメソッドの用意、imageViewの用意など)は完了している
tranformerを使って画像のリサイズと角丸実装
例えばこんな感じで書きます。
viewController.swiftfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = // cellの生成は省略。imageViewが乗っかったcellを想定。 let url = // 省略。固定URLや、データベースなどから取得したURLを変数に代入。 // 画像のリサイズ。任意のサイズを設定。 let sizeTransformer = SDImageResizingTransformer(size: CGSize(width: 200, height: 200),scaleMode: .aspectFill) // 角丸をつける。任意の数値や色を設定。 let roundCornerTransformer = SDImageRoundCornerTransformer(radius: 18.5, corners: .allCorners, borderWidth: 2.0, borderColor: UIColor.clear) // 上記2つのtransformerをまとめて1つにする let pipeLineTransformer = SDImagePipelineTransformer(transformers: [sizeTransformer, roundCornerTransformer]) cell.imageView.sd_setImage(with: url, placeholderImage: nil, context: [.imageTransformer: pipeLineTransformer]) }公式の読み込みも大変だと思うのでご参考になれば幸いです。
最近の話
この前母親が週に2回だけママをやっているスナックに行きました。スナックって味があっていいですね。母の作るハイボールは濃くて美味しかったです。
今回初投稿でしたが、自分が苦しんだものを中心にまた書きたいと思います。
- 投稿日:2020-03-16T20:18:50+09:00
SwiftUIのSubViewは画面更新ごとに生成と破壊を繰り返す
この記事の目的
SwiftUIのSubViewはその親Viewを更新されるタイミングで繰り返し生成と破棄されます。画面を更新されるたびにstructであるSubViewは破棄されて作り変えられているわけです。これを
数字
で理解するのがこの記事の目的です。SubViewは描画が必要なタイミングで生まれ変わっている、というのを言葉ではなく数字で分かりたいわけです。
具体的な前提
前提を説明すると、とあるContentViewが
@ObservedObject
を持つ場合にその@Published
なプロパティが更新された場合、ContentViewは更新のためにvar body: some View {}
プロパティが呼び出され、そこに記述されているSubViewはその都度生成されているわけです(もちろんこれは@State
もしくは@Binding
が更新された場合にも同じです)。上記SwiftUIのレンダリングシステムについてはBOOTHで電子書籍として販売している内容にその情報源を詳しく書いています。
SwiftUIガイドブック - レンダリングシステムの考察とデータの使い分け
https://booth.pm/ja/items/1829015ここまでが前提の確認です。
記事の目的のための本題
このSwiftUIのSubViewの更新はどこまでのスピードまで耐えられるのかというのが本題です。例えば
@Published
なプロパティの更新時間が例えば0.0001秒間隔ならその間隔に併せてViewも更新するのでしょうか。しないでしょ。無駄です。なぜならそんな間隔で画面を更新されても目で見て分からないからです。この予想としては60fpsもしくは120fpsだろうと考えます。この数字は2019年までに発売されたiPhoneが60fpsで、iPad Proが120fpsだからです。
60fpsの場合、つまり1秒間あたり60回画面を更新しているなら、1回の画面更新秒数は 1 / 60 で 0.001666 ... 秒となります。そのためこれを検証するのに 0.001666 ... の更新タイミングで
@ObservedObject
の@Published
プロパティを書き換え、1秒間に60回SubViewがinitメソッドを呼んでいるということが分かれば、つまり60fpsで画面更新できるということが確認できます。
- 0.001666 ... 秒ごとに
@ObservedObject
の@Published
プロパティを書き換える
- SubViewが更新される
- SubViewのinit時の回数を数える
- 1秒後 にinit回数が60であればインターバル0.001666 ... 秒でViewは作り変えられている
もちろん1秒後よりも10秒後にするほうが精度的には良いと思います。が、精度はあんまり求めていないので今回は1秒にしています。
さらにインターバルを小さくしていき、1秒間に最大何回initが実行されているか分かれば面白いところです。
実験
まずはPlaygroundで試す
- Xcdoe 11.4 beta
- Playground
- SubViewを青い背景としてTextでinitの回数を表示
- init回数は何度かやると56か57になる
import SwiftUI import UIKit import PlaygroundSupport // SubViewがinitされるたびにそのタイミングを保持 var dates = [Date]() struct ContentView: View { @ObservedObject var myTimer = MyTimer() var body: some View { VStack(alignment: .center) { Text("interval: \(myTimer.interval.description)") Text("limit: \(myTimer.limit)") SubView(myTimer: self.myTimer) .background(Color.blue) .foregroundColor(Color.white) } } } struct SubView: View { @ObservedObject var myTimer: MyTimer init(myTimer: MyTimer) { self.myTimer = myTimer dates.append(Date()) } var body: some View { VStack { Text("SubView: init count \(dates.count)") } } } class MyTimer: ObservableObject { // これを更新するとViewがreloadされる @Published var text: String = "" // タイマーを更新する間隔 let interval: TimeInterval = (1 / 60) // 0.0166... // タイマーを止めるlimit時間 let limit = 1.0 private lazy var timer: Timer = { Timer.scheduledTimer( withTimeInterval: self.interval, repeats: true ) { timer in self.text = timer.fireDate.description } }() init() { timer.fire() Timer.scheduledTimer( withTimeInterval: limit, repeats: false ) { _ in self.timer.invalidate() } } } let viewController = UIHostingController(rootView: ContentView()) PlaygroundPage.current.liveView = viewControllerPlaygroundのシミュレータ上では1/60の0.001666 ... 秒のインターバルにすると56か57あたりになります。60近ければ想定通りなのでまあそんなところでしょう。
ちなみに1秒間あたりのinit上限を探っていったところ、インターバルを240(0.0041秒間隔)より細かくすることはできませんでした。OS的にはゲーミングディスプレイの240fpsを想定しているのでしょう。Macbookのディスプレイで240fpsなんて描画してるわけないですが、1秒間にSwiftUIのinitは240回までできます。
実機でやってみる
PlaygroundからプロジェクトをSwiftUI用の[Single View App]に作り直し、iPhone 11Pro実機でインターバルを240(0.0041秒間隔)で試すと、だいたい115~125あたりです。
iPhone 11 Proのディスプレイは120Hzで駆動すると書かれた公式仕様がないため、60fpsがハードウェア的な上限だとは思いますが、システム的な上限は120に近い値が出せるということがわかります。
結論
やはりViewは
@ObservedObject
の@Published
なプロパティが更新されるとSubViewは再生と破壊を繰り返す。そしてリミッターが存在しそうでありそれはシミュレータと端末では違いがある。
- Playground(というかシミュレータでは)
- 1秒間に60回近くプロパティを更新すればViewはその都度initされている
- インターバルは240回の更新が限界
- (もちろん画面更新が1秒間に240回やってるとは思えない)
- 240はゲーミングディスプレイの更新周波数と同じでキリが良すぎる...
- 実機のiPhone 11 Pro
- インターバルは120回付近の更新が限界
- (もちろん実機画面更新が1秒間に140回やってるとは思えない)
念の為書いておくこと
SwiftUIのレンダリングシステムが描画をサボっているかどうかはわからない
Viewが0.001666 ... 秒ごとに作り変えられていたとしても、本当に画面が更新されているのかSwiftUIのレンダリングシステムによってスキップされているかはわかりません。わかるのはinitの回数だけです。initが動作すると
body
が更新されますが、処理的に重かったりすると次のinitタイミングが動作してしまい前のbody
更新する前に次の処理が入る、とかもあるでしょう。今回はそんなに重い処理ではないのでそうならないとは思いますが。おそらく基本的にはOSの描画タイミングに合わせて画面を更新しており、その時間までに
setNeedsDisplay
のようにフラグを立てられているものを回収して描画しているのだと思います。本当の描画回数が分かればもっと興味深い結果になるかもしれません...