#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
- основной цикл
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))
}
}
}
- модели (обратите внимание — ваши
Image
конфликты со стандартами SwiftUIImage
— избегайте таких конфликтов, иначе вы можете столкнуться с очень неясными ошибками. Для демонстрации я переименовал его везде, где это было необходимо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]
}