На момент написания этого поста WWDC21 все еще находится в разработке. И, как оказалось, анонсы этого года открывают для разработчиков действительно отличные и захватывающие новости. Среди них SwiftUI, похоже, хорошо оснащен новыми API, представлениями и инструментами, которые, несомненно, сделают создание приложений с его помощью еще более простой и увлекательной задачей.

Новое и довольно интересное представление, представленное в этом третьем выпуске SwiftUI, - это AsyncImage. Поскольку название делает это довольно очевидным, это представление отображает изображения после их получения с удаленного URL-адреса. Традиционно это выполнялось вручную, но теперь AsyncImage выполняет всю работу за кулисами, пока изображение не будет представлено в представлении. API AsyncImage прост, но достаточно гибок; он предоставляет параметры для отображения изображения-заполнителя во время ожидания удаленного изображения, обработки потенциальных ошибок, отображения загруженного изображения в анимации и, конечно же, стилизации изображения, как нам нравится, с использованием модификаторов представления.

Есть только один недостаток: полученные изображения не кэшируются для использования в будущем. Представление AsyncImage загружает удаленное изображение всякий раз, когда оно будет отображаться. Это нормально для изображений, которые будут показаны пользователям всего несколько раз. Но для удаленных изображений, которые приложение часто представляет, не рекомендуется всегда получать их в режиме реального времени. По-прежнему требуется ручная реализация для выборки и кеширования изображений.

Практическое руководство к AsyncImage

Пора немного поиграться с AsyncImage и посмотреть, что он делает. Самый простой способ использовать это так:

AsyncImage(url: url)

Аргумент url - это объект URL, указывающий на удаленное изображение:

let url = URL(string: "https://images.pexels.com/photos/8016369/pexels-photo-8016369.jpeg")

Если вы добавите указанное выше в представление SwiftUI и запустите его в симуляторе или просто выполните предварительный просмотр в реальном времени, вы увидите, что изображение отображается через несколько секунд. Однако это не так удобно для пользователя; поле серого цвета по умолчанию временно отображается как заполнитель. Нам нужно указать нашим пользователям, что выполняется асинхронная операция, возможно, с представлением о ходе выполнения и некоторым сообщением, или отобразить более подходящее изображение-заполнитель, пока не будет извлечено удаленное.

Давайте сначала сосредоточимся на использовании изображения-заполнителя. Для этого можно использовать другой инициализатор: init(url:scale:content:placeholder:).

Первый аргумент - это URL-адрес изображения, а второй - масштаб изображения. Значение масштаба по умолчанию - 1. Два других аргумента - это замыкания, о которых я расскажу подробнее после того, как увидю этот новый инициализатор в действии:

AsyncImage(url: url) { image in
    image
        .resizable()
        .aspectRatio(contentMode: .fit)
        .clipShape(RoundedRectangle(cornerRadius: 15))
        .padding()
} placeholder: {
    placeholderImage()
}

Я не указываю здесь значение масштаба изображения; Я пропускаю его, поэтому вместо него будет использоваться значение по умолчанию.

А теперь поговорим о двух закрывающих операциях. Аргументом первого закрытия является представление SwiftUI Image, содержащее изображение после его загрузки. Мы представляем его в теле закрытия, и, как вы могли заметить, мы можем использовать любой модификатор вида, который соответствующим образом стилизует изображение.

Второе закрытие - это место для добавления представления изображения с изображением-заполнителем. Этот будет отображаться до тех пор, пока мы ждем, пока будет извлечен удаленный. Он будет продолжать отображаться, если фактическое изображение не может быть загружено по какой-либо причине. В этом примере я вызываю метод placeholderImage(), который является следующим:

@ViewBuilder
func placeholderImage() -> some View {
    Image(systemName: "photo")
        .renderingMode(.template)
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 150, height: 150)
        .foregroundColor(.gray)
}

Метод placeholderImage() возвращает представление SwiftUI, поэтому он помечен атрибутом @ViewBuilder. В качестве изображения-заполнителя я использую символ SF.

Источники: Фото Люси из Pexels.

AsyncImage и AsyncImagePhase

AsyncImage имеет еще один инициализатор, который позволяет получать результаты загрузки изображения поэтапно: init(url:scale:transaction:content:).

См. Следующий пример:

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

Здесь я указываю только URL и аргумент content, который является закрытием. Аргумент закрытия - это значение AsyncImagePhase, где AsyncImagePhase - это перечисление с наблюдениями, которые представляют определенные фазы загрузки.

Фактически, вы можете увидеть все доступные варианты AsyncImagePhase в операторе switch прямо выше. Первый - это случай, когда изображение было успешно получено. Связанное значение case - это представление SwiftUI Image, которое мы можем украсить, используя любые необходимые модификаторы представления.

Второй случай содержит потенциальную ошибку, которая могла произойти. В этом примере я просто представляю описание ошибки в текстовом виде.

Последний случай, называемый .empty, представляет состояние представления AsyncImage, когда еще нет загруженного изображения. Здесь мы можем добавить представление заполнителя, представление прогресса или что-нибудь еще, подходящее для приложения. В приведенном выше примере я вызываю метод waitView(), который выглядит следующим образом:

@ViewBuilder
func waitView() -> some View {
    VStack {
        ProgressView()
            .progressViewStyle(CircularProgressViewStyle(tint: .indigo))
        
        Text("Fetching image...")
    }
}

Его цель - показать прогресс вместе с текстом.

Обратите внимание, что в операторе switch также есть @unknown default регистр; это необходимо для обработки любых новых значений, которые могут быть добавлены в перечисление AsyncImagePhase в будущем. EmptyView - лучший кандидат для обработки такого случая.

Приведенная выше реализация AsyncImage приводит к следующему:

В общем, обратите внимание, что описанный выше способ инициализации представления AsyncImage обеспечивает больший контроль над результатами представления. В частности, мы действительно можем определить, когда произошла ошибка с этим, и, следовательно, отобразить другое представление; то, что мы не можем сделать с помощью инициализатора заполнителя, описанного в предыдущей части.

Но это не единственное преимущество этого инициализатора. С помощью аргумента transaction мы можем передать анимацию, которая будет приведена в движение при изменении фазы. Сразу после этого вы можете увидеть тот же пример, что и раньше, но на этот раз аргументу transaction предоставляется только экземпляр анимации:

AsyncImage(url: url,
           transaction: Transaction(animation: .easeInOut(duration: 2.5))
) { phase in
    switch phase {
        case .success(let image):
            image
                .resizable()
                .aspectRatio(contentMode: .fit)
                .clipShape(RoundedRectangle(cornerRadius: 15))
                .padding()
 
        case .failure(let error):
            Text(error.localizedDescription)
 
        case .empty:
            waitView()
 
        @unknown default:
            EmptyView()
    }
}

Резюме

Это почти все, что касается использования представления AsyncImage для загрузки и отображения удаленных изображений. Есть две вещи, которые я не упомянул во введении; во-первых, AsyncImage использует общий экземпляр URLSession для получения изображения с заданного URL-адреса. Вторая и, вероятно, неприятная новость заключается в том, что AsyncImage доступен в iOS 15 и выше. Тем не менее, это отличный новый вид, который будет полезен вне всяких сомнений, и прекрасное новое дополнение к арсеналу SwiftUI. Спасибо за чтение!

Первоначально опубликовано на https://serialcoder.dev 10 июня 2021 г.