SwiftUIでWeb上の通信が伴う画像を表示する方法
ウィジェットでURLから画像を表示する方法
はじめに
iOS14からウィジェットという機能が追加されました。
ウィジェット自体は以前からもあったのですが、好きなサイズのウィジェットをホーム画面の好きな場所におけるようになりました。
運用しているアプリでも採用しており、以下のようなイメージです。
Appleのガイドラインによると、ウィジェットの使い方は基本的に、アプリ本体を起動するきっかけになる仕組みとして用いられ、表示に変化があるべきです。
従って、固定の表示で、ただ起動するだけのショートカットのような使い方は相応しくありません。
ウィジェットの使用にはいくつか制限がありますが、有効性が高いと思われる特長を挙げます。
- ウィジェットの定期的な表示更新ができる
- ウィジェット内で簡単な通信ができる
- ディープリンクでアプリ起動ができる
- 3種類の表示方法をユーザーが選べる
3種類の表示方法について、標準のニュースアプリでは以下のようになっています。
※歪んで見えるのは、アニメーションのためです。表示はきちんとまっすぐになります。
対処方法
上記の標準ニュースアプリのウィジェットのとおり、画像を差し込みたいケースが多くあると思います。
筆者も、元々アプリ内で使用している、AlamofireImage、SDWebImage、Kingfisherのような画像キャッシュライブラリを使い回せば、簡単にできると考えていましたが、そう簡単にはいきませんでした。
そもそもウィジェットにはSwiftUIの対応が必須です。
従って、UIKitのUIImageではなく、SwiftUIのImageを使用する必要があり、そのまま使い回すことはできません。
また、SwiftUIのImageに非同期で画像を取得し次第表示してくれるようなキャッシュの機能は標準ではないため、自前で作成する必要があります。
そこで以下のような独自クラスを作成して対応しました。
import SwiftUI
struct NetworkImage: View {
private let url: URL?
private var size = CGSize(width: 50, height: 50)
init(withURL url:String) {
self.url = URL(string: url)
}
init(withURL url:String, size:CGSize) {
self.url = URL(string: url)
self.size = size
}
var body: some View {
Group {
if let url = url, let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage).resizable().scaledToFill().frame(width: size.width, height: size.height, alignment: .center).clipShape(ContainerRelativeShape())
} else {
Image("noimage").resizable().scaledToFill().frame(width: size.width, height: size.height, alignment: .center).clipShape(ContainerRelativeShape())
}
}
}
}
※「noimage」は事前に準備したデフォルト画像です。
使用するときは以下のNetworkImageの部分のとおりです。
…
struct xxwidgetEntryView : View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
ZStack(alignment: .topLeading, content: {
Color("AccentColor")
VStack(alignment: .leading, spacing: 6, content: {
...
let height = cellHeight(family: family)
Text("新着記事").font(.subheadline).foregroundColor(.white).bold()
let count = maxCount(family: family, size: entry.articles.articles.count)
ForEach(0..<count) { index in
let article = entry.articles.articles[index]
HStack {
NetworkImage(withURL: article.thumbnailUrl, size: CGSize(width: height, height: height))
VStack(alignment: .leading, spacing: nil, content: { ... })
}.frame(width: .infinity, height: height, alignment: .center)
}
})
})
}
...
}
ちなみに筆者は最初Objective-Cのプロジェクトに対してWidgetを採用しました。
画像以外にも記事を管理するモデルなど重複するものがあるため、使い回しを検討しました。
そして、Objective-Cで使用していた記事モデルをSwiftUIで使い回せるよう、SwiftからObjective-Cのコードを使用するためのBridgeファイルを作成して試みていたのですが、通信部分の処理などもろもろの都合により、Swiftで同じモデルクラスを書いたほうが速いと判断できたため、結局はウィジェット用にSwiftで新しく書き直しました。
画像表示が結局非同期ではないのですが、TableViewのようにいくつもコンテンツがある想定でなく、コンテンツ数が限られるため、このような対応でも良いとしました。
頑張れば使い回す方法があったり、探せば現時点では良いオープンソースなどあるかもしれません。一度探してみてもよいかもしれません。