LoginSignup
24
8

More than 1 year has passed since last update.

[SwiftUI]AsyncImageを使用してURLから画像を表示する

Posted at

はじめに

iOS15ではAsync/Awaitが大きく注目されました。
その影で非同期処理で画像を読み込んで表示するAsyncImageも導入されました。

AsyncImageはSwiftUIで使用することができます。
SwiftUIでImageを読み込んで表示させるには、多くの場合、自前でFunctionを作成していると思いますが、今回追加されたAsyncImageを使用すればシンプルにImageを取得できるようになっています。
また、取得時のアニメーションや、ロード中に表示するindicator、取得に失敗した時に表示するImageまで指定する事ができます。
しかし、残念なところもあります。現時点ではAsyncImageではキャッシュされません。起動するたびにImageを再度DownLoadしています。
その辺りも含めて、この記事で触れていきたいと思います。

環境

・ macOS Monterey 12.0
・ iOS 15.0
・ Xcode : 13.1

従来のImage Downloadの例

@State var url: String
@State var image: UIImage?
func downloadImageAsync(url: URL, completion: @escaping (UIImage?) -> Void) {
    var request = URLRequest(url: url)
    request.timeoutInterval = 3
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        if let error = error {
            completion(nil)
            print(error)
            return
        }
        guard let response = response as? HTTPURLResponse,
              (200...299).contains(response.statusCode) else {
                  completion(nil)
                  print ("server error")
                  return
              }
        if response.statusCode == 200 {
            image = UIImage(data: data!)
            DispatchQueue.main.async {
                completion(image)
            }
        }
    }
    task.resume()
}
var body: some View {
    VStack {
        Text("従来のImageDL")
        if let image = image {
            Image(uiImage: image)
                .resizable()
                .scaledToFit()
                .scaleEffect(0.9)
        }
    }
    .onAppear {
        url = "https://images.unsplash.com/photo-1635408485959-9f56670da427?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1171&q=80"
        if !url.isEmpty {
            downloadImageAsync(url: URL(string: url)!) { image in
                self.image = image
            }
        }
    }
}
上記のCodeで取得した画面

AsyncImageの使用

let url = URL(string: "https://bit.ly/3nHS735")
var body: some View {
    VStack {
        Text("AsyncImageを使用したImageDL")
        AsyncImage(url: url)
    }
}
PreViewを再生するまではImageは表示されない
PreViewを再生して取得した画面

これだけです。ものすごくCodeがスッキリしました。
また画像の読み込み中に表示するindicatorやSizeの変更、エラー時の表示も指定できます。

VStack {
    AsyncImage(url: url) { phase in
        if let image = phase.image {
            image
                .resizable()
                .scaledToFit()
        } else if phase.error != nil {
            Text("no image")
        } else {
            ProgressView()
        }
    }
}
.scaleEffect(0.9)

上記ではif文で
・読み込んだ画像を表示
・error時は"no image"を表示
・ProgressViewの表示(ここでは読み込んだ画像を表示させるまでに表示させるものを指します)
を上から順に行っています。

またswitch文で書く事もできます。

AsyncImage(url: url) { phase in
    switch phase {
    case .success(let image):
        image
            .resizable()
            .aspectRatio(contentMode: .fit)
    case .failure(let error):
        Text(error.localizedDescription) 
    case .empty:
        ProgressView()
    @unknown default:
        EmptyView()
    }
}

1つ目は、画像の取得に成功したら、取得した画像を表示させています。
caseに関連付けられている値はImageであり、必要なImageのmodifierを使用する事ができます。

2つ目は、画像の取得に失敗した時、errorの説明をlocalizedDescriptionを使用してTextで表示しています。

3つ目は、読み込み中で、Imageを取得するまでに表示するものを指定できます。
先ほど紹介したif文でのProgressViewの部分と同じイメージを持ってもらえると分かりやすいかと思います。
ここではindicatorを表示させる為にProgressViewを使用しています。

最後に@unknown defaultですが、こちらは将来上記以外のcaseが追加される可能性があることを示しています。
今回はEmptyViewを追加しておきましたがここは必要に応じて書き換えてください。

アニメーションの追加

AsyncImage(url: url, transaction: Transaction(animation: .easeInOut(duration: 0.6)))

urlの後にtransactionを追加する事でImageを表示させる際にアニメーションを追加する事ができます。

transactionを追加して取得したImageを表示

上記のgifを見ていただくと分かるように、毎回ProgressViewから始まり、Imageを取得している事がわかります。つまりキャッシュはされていません。
また、timeoutIntervalのようなtimeoutできる機能もありません。
今のところキャッシュ機能付きのAsyncImageはないのでGitHubに公開されているライブラリを使用するしかなさそうですね。

Async/Awaitを使ってimageを取得してみる

ここからはおまけ的な要素ですがAsync/Awaitを使ってimageを取得してみます。

struct FetchImage: View {
    @State private var image: Image?
    var url: URL
    var body: some View {
        ZStack {
            if let fetchImage = image {
                fetchImage
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            } else {
                ProgressView()
            }
        }.task(id: url) {
            do {
                let (data, _) = try await URLSession.shared.data(from: url)
                if let fetchImage = UIImage(data: data) {
                    self.image = Image(uiImage: fetchImage)
                }
            } catch {
                self.image = Image(systemName: "xmark")
            }
        }
    }
}

imageが取得されれば、取得したimageを表示します。それまではProgressViewを表示させます。取得に失敗した場合はxmarkを表示します。
.task(id: url)により、ビューが表示されたとき、または指定された値が変更されたときに実行するタスクを追加するので、urlを変更するたびにimageを変更してくれます。

まとめ

ここまでCodeがシンプルに書けるのは純粋に感動しました!!
さらにエラーやロード画面が標準で設定できるのもすごくシンプルで書きやすいし、読みやすいです!!
またAsync/Awaitもすごく書きやすくCodeがシンプルに表現できる点が良かったです!!taskとの組み合わせも個人的には好きでした!!
とは言えiOS15〜という事で実際に使用するのはまだ先になりそうですねー。。。

おまけ

iOS15〜と言わずに、もっと幅広く使用できるようにSPMでimageを取得する簡単なライブラリ作ってみました(^^)
是非試して頂き、良ければ☆頂けると泣いて喜びます★

使い方など〜

公開の様子

参考記事

24
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
8