【SwiftUI】URLからImageを表示する

2022.08.30

SwiftUIではImage(url:)のようなAPIは無い為、URLからImageを表示する方法を調べました。

環境

  • Xcode 13.3
  • iOS 15.5

URLからImageを表示する

iOS 15以上

iOS 15以上からはAsyncImageを使うことで容易に実現出来ます。

AsyncImage

今回はinit(url:scale:content:placeholder:)を使用してみます。

init<I, P>(
    url: URL?,
    scale: CGFloat = 1,
    content: @escaping (Image) -> I,
    placeholder: @escaping () -> P
) where Content == _ConditionalContent<I, P>, I : View, P : View
  • url
    • 表示する画像のURL
  • scale
    • 画像に使用するスケール
  • content
    • 読み込まれた画像を表示するViewを返すクロージャー
  • placeholder
    • ロードが完了するまでに表示するViewを返すクロージャー

使用例

import SwiftUI

struct ContentView: View {

    let imageUrl = URL(string: "https://cdn-ssl-devio-img.classmethod.jp/wp-content/uploads/2022/08/swiftui-image-from-url-eyecatch-960x504.png")

    var body: some View {
        AsyncImage(url: imageUrl) { image in
            image.resizable()
        } placeholder: {
            ProgressView()
        }
        .frame(width: 240, height: 126)
    }
}

今回はロードが完了するまでに表示するViewはprogressViewにしています。まだ、URLから画像が取得出来ていない場合は、下記にような表示になります。

ロードが完了すると無事に意図した画像が表示されました。

iOS 15 未満

iOS 15では容易に実現できることが分かったのですが、iOS 15未満での対応方法も試行錯誤してみました。

AsyncImageView

URLplaceholderとなるContentを受け取り、Imageまたはplaceholderを表示するViewです。

import SwiftUI

struct AsyncImageView<Content: View>: View {

    @StateObject var viewModel: AsyncImageViewModel
    let placeholder: Content

    init(url: URL?,
         @ViewBuilder placeholder: () -> Content)  {
        _viewModel = StateObject(wrappedValue: AsyncImageViewModel(url: url))
        self.placeholder = placeholder()
    }

    var body: some View {

        ZStack {

            if let uiImage = viewModel.uiImage {
                Image(uiImage: uiImage)
                    .resizable()
            } else {
                placeholder
            }
        }
        .onAppear {
            viewModel.downloadImage()
        }
    }
}

viewModel.uiImagenilの場合はplaceholderを表示して、nilでない場合はそのUIImageからImageを生成して表示しています。

AsyncViewModel

初期化時にURLを受け取っています。

URLからDataを取得する関数downloadImageDataを作成しました。またPublisheduiImageの値を更新することでUIが更新される為、@MainActorをつけたdownloadImage関数を作成しています。

class AsyncImageViewModel: ObservableObject {

    @Published var uiImage: UIImage? = nil
    let url: URL?

    init(url: URL?) {
        self.url = url
    }

    @MainActor
    func downloadImage() {

        guard let data = downloadImageData() else { return }
        uiImage = UIImage(data: data)
    }

    private func downloadImageData() -> Data? {

        guard let url = url else { return nil }
        let data = try? Data(contentsOf: url)
        return data
    }
}

uiImageの値を変更することで、AsyncImageViewのUIが変更される

ContentView

import SwiftUI

struct ContentView: View {

    let imageUrl = URL(string: "https://cdn-ssl-devio-img.classmethod.jp/wp-content/uploads/2022/08/swiftui-image-from-url-eyecatch-960x504.png")

    var body: some View {
        AsyncImageView(url: imageUrl) {
            ProgressView()
        }
        .frame(width: 240, height: 126)
    }
}

iOS 15未満でもURLから画像を表示することができました!

おわりに

iOS 15からはかなりサクッとURLからImageを表示できることが分かりました。まだローディング最中のUIも決めることが出来るので視覚的にも伝えやすくユーザーに優しいですね。 iOS 15未満でもURLからImageを表示することが出来たのですが、より良い方法等ありましたら優しく教えていただけたら嬉しいです。

参考