Сделать так, чтобы вид не перерисовывался после обновления ObservableObject

#swift #swiftui #combine

#swift #swiftui #объединить

Вопрос:

У меня ситуация, когда я использую LazyVGrid для отображения изображений из библиотеки фотографий пользователей. В сетке есть разделы, и каждый раздел содержит массив изображений, которые должны отображаться, а также некоторые метаданные о разделе.

Проблема, с которой я сталкиваюсь, заключается в том, что каждый раз, когда пользователь нажимает на плитку, которая переключается markedForDeletion на соответствующем изображении, все виды в сетке (которые видны) перерисовываются. Это не идеально, так как в «реальной» версии примера кода существует реальный штраф за перерисовкукаждое Tile , поскольку изображения должны быть извлечены и отображены.

Я попытался сделать Tile соответствие Equatable , а затем использовать .equatable() модификатор, чтобы уведомить SwiftUI о том, что плитка не должна перерисовываться, но это не работает.

Размещение другого .onAppear вне Section подсказок о том, что весь раздел перерисовывается каждый раз, когда что-либо меняется, но я не уверен, как структурировать мой код, чтобы свести к минимуму влияние дорогостоящего перерисовывания Tiles .

 import SwiftUI

struct ContentView: View {
    
    @ObservedObject var viewModel: ViewModel = ViewModel()
    
    let columns = [
        GridItem(.flexible(minimum: 40), spacing: 0),
        GridItem(.flexible(minimum: 40), spacing: 0),
    ]
    
    var body: some View {
        
        GeometryReader { gr in
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(viewModel.imageSections.indices, id: .self) { imageSection in
                        Section(header: Text("Section!")) {
                            ForEach(viewModel.imageSections[imageSection].images.indices, id: .self) { imageIndex in
                                Tile(image: viewModel.imageSections[imageSection].images[imageIndex])
                                    .equatable()
                                    .onTapGesture {
                                        viewModel.imageSections[imageSection].images[imageIndex].markedForDeletion.toggle()
                                    }
                                    .overlay(Color.blue.opacity(viewModel.imageSections[imageSection].images[imageIndex].markedForDeletion ? 0.2 : 0))
                                
                                    .id(UUID())
                            }
                        }
                    }
                }
            }
        }
        
    }
}


struct Image {
    
    var id: String = UUID().uuidString
    var someData: String
    var markedForDeletion: Bool = false
    
}

struct ImageSection {
    
    var id: String = UUID().uuidString
    var images: [Image]
    
    
}

class ViewModel: ObservableObject {
    
    @Published var imageSections: [ImageSection] = generateFakeData(numSections: 10, numImages: 10)
    
}

func generateFakeData(numSections: Int, numImages: Int) -> [ImageSection] {
    
    var sectionsToReturn: [ImageSection] = []
    
    for _ in 0 ..< numSections {
        var imageArray: [Image] = []
        
        for i in 0 ..< numImages {
            imageArray.append(Image(someData: "Data (i)"))
        }
        
        sectionsToReturn.append(ImageSection(images: imageArray))
    }
    
    return sectionsToReturn
}

struct Tile: View, Equatable {
    
    static func == (lhs: Tile, rhs: Tile) -> Bool {
        
        return lhs.image.id == rhs.image.id
    }
    
    
    var image: Image
    
    var body: some View {
        
        Text(image.someData)
            .background(RoundedRectangle(cornerRadius: 20).fill(Color.blue))
        
            .onAppear(perform: {
                NSLog("Appeared - (image.id)")
            })
        
    }
    
}

 

Ответ №1:

Основная причина глобального перерисовывания заключается .id(UUID()) в том, что при каждом вызове он принудительно восстанавливает просмотр. Но чтобы удалить его и сохранить ForEach работоспособность, нам нужно явно идентифицировать модель.

Здесь исправлены части кода. Протестировано с Xcode 12.1 / iOS 14.1

  1. основной цикл
 ForEach(Array(viewModel.imageSections.enumerated()), id: .1) { i, imageSection in
    Section(header: Text("Section!")) {
        ForEach(Array(imageSection.images.enumerated()), id: .1) { j, image in
            Tile(image: image)
                .onTapGesture {
                    viewModel.imageSections[i].images[j].markedForDeletion.toggle()
                }
                .overlay(Color.red.opacity(image.markedForDeletion ? 0.2 : 0))
        }
    }
}
 
  1. модели (обратите внимание — ваши Image конфликты со стандартами SwiftUI Image — избегайте таких конфликтов, иначе вы можете столкнуться с очень неясными ошибками. Для демонстрации я переименовал его везде, где это было необходимо ImageM )
 struct ImageM: Identifiable, Hashable {
    
    var id: String = UUID().uuidString
    var someData: String
    var markedForDeletion: Bool = false
}

struct ImageSection: Identifiable, Hashable {
    
    var id: String = UUID().uuidString
    var images: [ImageM]
}