Как создать представление 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 . Он может делать многое из того, что вы хотите, автоматически.)

Другие вопросы по тегам