Получение, кеширование, ошибка и просмотр загрузки
Обновление 2019/08/14: теперь доступно как пакет Swift 🚀
Я хочу реализовать кулон SwiftUI удаленного просмотра изображений UIKit, который мы создали у моего нынешнего работодателя. Я не могу поделиться кодом, но поверьте мне, что по сравнению со следующей реализацией SwiftUI наш код UIKit более сложен, использует внешнюю структуру и содержит больше строк кода.
Цель
Начнем с краткого описания цели статьи:
Наша цель - создать представление, которое извлекает изображение по заданному URL-адресу, показывает представление загрузки во время выборки, кэширует изображение, использует существующее представление изображения SwiftUI и при необходимости показывает представление ошибки.
Помня об этом, приступим к программированию.
Реализация
Код в основном состоит из трех компонентов.
1. RemoteImageState
Просмотр удаленного изображения может иметь три разных состояния: ошибка, изображение и загрузка. Он должен иметь возможность зависеть от своего текущего состояния.
enum RemoteImageState { case error(_ error: Error) case image(_ image: UIImage) case loading }
2. RemoteImageService
Нам нужен сервис, который позаботится о получении и кешировании изображения. В дополнение к этому, он должен управлять текущим состоянием связанного удаленного образа.
Получение изображения
HTTP-запрос выполняется с использованием стандартных инструментов (URLSession
и URLRequest
) в сочетании с простым издателем задачи данных и подписчиком приемника (объединить структуру ).
Кеширование изображения
Кеширование выполняется с помощью NSCache
. Все просто, правда? Все удаленные изображения в одном приложении SwiftUI должны использовать один и тот же кеш, поэтому кеш статичен. Кэш можно легко очистить с помощью функции removeAllObjects()
.
Изменения состояния
Служба соответствует протоколу ObservableObject
и распространяет изменения состояния, сделанные в функции fetchImage
, через простой PassthroughSubject
каждому подписчику.
final class RemoteImageService: ObservableObject { private var cancellable: AnyCancellable? static let cache = NSCache<NSURL, UIImage>() var state: RemoteImageState = .loading { didSet { objectWillChange.send() } } let objectWillChange = PassthroughSubject<Void, Never>() func fetchImage(atURL url: URL) { cancellable?.cancel() if let image = RemoteImageService.cache.object(forKey: url as NSURL) { state = .image(image) return } let urlSession = URLSession.shared let urlRequest = URLRequest(url: url) cancellable = urlSession.dataTaskPublisher(for: urlRequest) .map { UIImage(data: $0.data) } .receive(on: RunLoop.main) .sink(receiveCompletion: { completion in switch completion { case .failure(let failure): self.state = .error(failure) default: () } }) { image in if let image = image { RemoteImageService.cache.setObject(image, forKey: url as NSURL) self.state = .image(image) } else { self.state = .error(RemoteImageServiceError.couldNotCreateImage) } } } }
3. RemoteImage
Последний компонент - это само представление. Он использует экземпляр RemoteImageService
и становится зависимым от состояния в службе.
Инициализатор ожидает URL и блок ViewBuilder
для каждого состояния (ошибка, изображение и загрузка).
Получение изображения
Как только LoadingView
появляется на экране, запускается функция fetchImage
экземпляра RemoteImageService
.
Настройка внешнего вида изображения
Внутренне представление RemoteImage
использует существующее представление изображения SwiftUI. Чтобы иметь возможность настраивать внешний вид этого представления изображения, он отображается через связанный блок ViewBuilder
.
Произошла ошибка
Если возникает ошибка, вызывается окно просмотра ошибок ViewBuilder
. Ошибка передается в блок ViewBuilder
и может использоваться для создания представления ошибки.
AnyView
В этой реализации мне пришлось использовать обертку стертого типа AnyView
, чтобы стереть различные типы представлений, возвращаемых переключателем.
Обычно групповое представление может решить эту проблему. Но блок содержимого представления Group - это ViewBuilder
блок, и вы не можете использовать приведенный ниже оператор switch (перечисление со связанными значениями) в блоке ViewBuilder
.
struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>: View { private let url: URL private let errorView: (Error) -> ErrorView private let imageView: (Image) -> ImageView private let loadingView: () -> LoadingView @ObservedObject private var service: RemoteImageService = RemoteImageService() var body: AnyView { switch service.state { case .error(let error): return AnyView( errorView(error) ) case .image(let image): return AnyView( self.imageView(Image(uiImage: image)) ) case .loading: return AnyView( loadingView() .onAppear { self.service.fetchImage(atURL: self.url) } ) } } init(url: URL, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) { self.url = url self.errorView = errorView self.imageView = imageView self.loadingView = loadingView } }
Пример
И последнее, но не менее важное: мы рассмотрим пример использования.
Созданное представление удаленного изображения легко использовать. Просто передайте инициализатору URL-адрес, представление ошибок, изображение и представление загрузки.
struct ContentView: View { private let url = URL(string: "https://images.unsplash.com/photo-1524419986249-348e8fa6ad4a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80")! var body: some View { RemoteImage(url: url, errorView: { error in Text(error.localizedDescription) }, imageView: { image in image .resizable() .aspectRatio(contentMode: .fit) }, loadingView: { Text("Loading ...") }) } }
Взгляните на то, как я настраивал внешний вид изображения: я изменил его размер и изменил режим содержимого, чтобы он соответствовал.
Заключение
Поздравляю! Вы закончили этот урок о создании удаленного изображения в представлении SwiftUI.
Спасибо, что прочитали эту статью. Надеюсь, вы продолжите читать мои статьи. Будьте на связи.
Смотрите на GitHub по адресу https://github.com/crelies/RemoteImage-SwiftUI.