Как создать представление SwiftUI с помощью async
У меня есть небольшая программа swiftUI, которая отображает фотографии за определенный день в виде стопки фотографий за каждый год. Упрощенный код ниже. Я читаю фотографии в resultPhotos как словарь, где каждый ключ — это год, а каждое значение — PHFetchResult. Затем я перебираю каждый год в словаре и создаю myImageStack с фотографиями за этот год.
Протестировано около 100 фотографий, это очень медленно. Похоже, виновник создает стек изображений в myImageStack.
Я хотел бы попытаться создать myImageStack асинхронно, чтобы не замораживать пользовательский интерфейс. Любое руководство о том, как это сделать? Большинство примеров async/await, которые я нашел, основаны на возврате данных, но я пытаюсь вернуть представление, которое будет использоваться в цикле ForEach. Я чувствую, что мне здесь не хватает чего-то фундаментального.
(Я понимаю, что многие изображения полностью скрыты в myImageStack, поэтому, возможно, мне следует упростить и это представление. Но поскольку изображения расположены случайным образом, вы можете видеть части некоторых изображений в стеке, и я бы хотел поддерживать это.)
import SwiftUI
import Foundation
import Photos
struct PhotosOverviewView_simplified: View {
var body: some View {
let photos = PhotosModel()
let _ = photos.getPhotoAuthorizationStatus()
let resultPhotos = photos.getAllPhotosOnDay(monthAbbr:"Feb", day:16)
NavigationStack{
ScrollView(.vertical){
VStack(alignment: .center){
//For each year, create a stack of photos
ForEach(Array(resultPhotos.keys).sorted(), id: \.self) { key in
let stack = myImageStack(resultsPhotos: resultPhotos[key]!)
NavigationLink(destination: PhotosYearView(year: key, resultsPhotos: resultPhotos[key]!)){
stack
.padding()
}
}
}
.frame(maxWidth: .infinity)
}
}
}
}
struct myImageStack: View {
let resultsPhotos: PHFetchResult<PHAsset>
var body: some View {
let collection = PHFetchResultCollection(fetchResult: resultsPhotos)
ZStack{
ForEach(collection, id: \.localIdentifier){ p in
Image(uiImage: p.getAssetThumbnail())
.resizable()
.scaledToFit()
.border(Color.white, width:10)
.shadow(color:Color.black.opacity(0.2), radius:5, x:5, y:5)
.frame(width: 200, height: 200)
.rotationEffect(.degrees(Double.random(in:-25...25)))
}
}
}
}
1 ответ
Большинство примеров async/await, которые я нашел, основаны на возврате данных, но я пытаюсь вернуть представление, которое будет использоваться в цикле ForEach. Я чувствую, что мне здесь не хватает чего-то фундаментального.
Да, это именно ваша проблема. Суть SwiftUI в том, что он декларативен. Вы не можете выполнять асинхронную работу для создания представлений, поскольку представления описывают макет. Они не похожи на представления UIKit, которые представляют реальную вещь на экране.
У вас возникли проблемы, потому что вы не подумали, что отображать во время загрузки данных. Ваше представление должно учитывать все его возможные состояния. Эти состояния, что неудивительно, входят в свойства. При изменении состояния представление будет обновлено. Другой узор – это@StateObject
шаблон, так что когда ссылочный тип обновляет@Published
свойство, представление будет обновлено. Но главный урок таков: представление всегда отображает свое текущее состояние. Когда состояние изменяется, View перерисовывается.
Итак, для вашего примера вы можете ожидать что-то вроде:
struct Photo: Identifiable {
let id: String
}
@MainActor
class PhotosModel: ObservableObject {
@Published var photos: [Photo] = []
init() {}
func load(month: String, day: Int) async throws {
// Do things to update `photos`
}
}
struct PhotosOverviewView_simplified: View {
// Whenever a @Published property of model updates, body will re-render
// But the *value* of body does not actually change. It always describes
// the View in all possible states.
@StateObject var model = PhotosModel()
var body: some View {
NavigationStack{
ScrollView(.vertical){
VStack(alignment: .center){
// You should not think of this ForEach as a loop that
// executes. It just describes the View. It says "there
// is a YourCustomPhotoView for each element of photos."
// It does not actually make those Views. The renderer
// will do that at some future time.
ForEach(model.photos) { photo in
YourCustomPhotoView(photo)
}
}
}
}
.task { // When this is displayed, start a loading task
do {
try await model.load(month:"Feb", day: 16)
} catch {
// Error handling
}
}
}
}
Я немного помахал руками здесь с обработкой ошибок, чтобы все было просто. Обычно вам нужно иметь@State
переменная, чтобы указать, находитесь ли вы в состоянии ошибки. Внутриbody
, тогда у вас будет что-то вроде:
if let error {
CustomErrorView(error)
else {
... the rest of your current body ...
}
Опять же, дело в том, что тело описывает все возможные состояния. Он не мутирует в ErrorView или что-то в этом роде. if
здесь просто описываются разные состояния. Его не следует рассматривать как код, который выполняется во время выполнения.
(И не забудьте проверить AsyncImage . Он может делать многое из того, что вы хотите, автоматически.)