SwiftUI и чрезмерная перерисовка

TL; DR:

Применение визуальных эффектов к содержимому ScrollView вызывает тысячи запросов на одно и то же (неизменное) изображение для каждого жеста перетаскивания. Могу я уменьшить это? (В моем реальном приложении у меня 50 с лишним изображений в представлении, и, соответственно, прокрутка медленная.)

Суть

Чтобы немного оживить прокрутку HStack изображений, я применил несколько преобразований для эффекта круговой «карусели». (Подсказки к образцу кода от Джона М. и Пол Хадсон)

Код можно запускать с копированием и вставкой, как указано. (Вам необходимо предоставить изображение.) Без двух строк, отмеченных /* 1 */ и /* 2 */, объект Slide сообщает о шести запросах изображения, независимо от того, сколько вы перетаскиваете и прокручиваете. Включите две строки и посмотрите, как количество запросов увеличивается до 1000 одним движением пальца.

Примечания

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

Но ... обязательно ли это требует постоянного повторного извлечения статических изображений? Случайное перетаскивание мизинца вперед и назад вызовет десятки тысяч запросов изображения. Это кажется чрезмерным. Есть ли способ уменьшить накладные расходы в этом примере?

Конечно, это примитивный дизайн, который постоянно выкладывает все свое содержимое, вместо того, чтобы использовать подход повторного использования ячеек, как, например, UITableView. Можно подумать о применении преобразований только к трем видимым в данный момент представлениям. В Интернете есть некоторая дискуссия об этом, но в мои попытки, компилятор не смог сделать вывод типа.

Код

import SwiftUI

// Comment out lines marked 1 & 2 and watch the request count go down.
struct ContentView: View {
  var body: some View {
    GeometryReader { outerGeo in
      ScrollView(.horizontal, showsIndicators: false) {
        HStack {
          ForEach(Slide.all) { slide in
            GeometryReader { innerGeo in
              Image(uiImage: slide.image).resizable().scaledToFit()
/* 1 */       .rotation3DEffect(.degrees(Double(innerGeo.localOffset(in: outerGeo).width) / 10), axis: (x: 0, y: 1, z: 0))
/* 2 */       .scaleEffect(1.0 - abs(innerGeo.localOffset(in: outerGeo).width) / 800.0)
            }
            .frame(width:200)
          }
        }
      }
    }
    .clipped()
    .border(Color.red, width: 4)
    .frame(width: 400, height: 200)
  }
}

// Provides images for the ScrollView. Tracks and reports image requests.
struct Slide : Identifiable {
  let id: Int
  static let all = (1...6).map(Self.init)
  static var requestCount = 0
  var image: UIImage {
    Self.requestCount += 1
    print("Request # \(Self.requestCount)")
    return UIImage(named: "blueSquare")!  // Or whatever image
  }
}

// Handy extension for finding local coords.
extension GeometryProxy {
  func localOffset(in outerGeo: GeometryProxy) -> CGSize {
    let innerFrame = self.frame(in: .global)
    let outerFrame = outerGeo.frame(in: .global)
    return CGSize(
      width : innerFrame.midX - outerFrame.midX,
      height: innerFrame.midY - outerFrame.midY
    )
  }
}

person Andrew Duncan    schedule 17.04.2020    source источник
comment
глупый вопрос: почему поиск ваших изображений обходится дорого? если вы его кешируете, он должен быть невероятно быстрым ...   -  person Chris    schedule 17.04.2020
comment
Я так и сделал, и это помогло. Просто не хватит.   -  person Andrew Duncan    schedule 17.04.2020
comment
К тому же: ваша идея подходит для многих случаев. В моем текущем проекте у меня есть (пока) пять разных кешей для дорогих активов. Например. все мои статические наборы данных (пути, координаты, макеты) нормализованы до содержащего прямоугольника размером 1x1. Я кэширую денормализованные значения, так как они не изменятся во время выполнения. Но часто кажется, что я защищаюсь от SwiftUI вместо того, чтобы плыть по течению.   -  person Andrew Duncan    schedule 18.04.2020


Ответы (1)


Думаю, вы могли бы попробовать вот так:

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

class ImageCache {
    static let slides = Slide.all

    // do prefetch your images here....
    static let cachedImage = UIImage(named: "blueSquare")!

    struct Slide : Identifiable {
        let id: Int
        static let all = (1...6).map(Self.init)
        static var requestCount = 0

        var image: UIImage {
            Self.requestCount += 1
            print("Request # \(Self.requestCount)")
            //  return ImageCache.image!  // Or whatever image
            return ImageCache.cachedImage  // Or whatever image
        }
    }

}
// Comment out lines marked 1 & 2 and watch the request count go down.
struct ContentView: View {
  var body: some View {
    GeometryReader { outerGeo in
      ScrollView(.horizontal, showsIndicators: false) {
        HStack {
            ForEach(ImageCache.slides) { slide in
            GeometryReader { innerGeo in
              Image(uiImage: slide.image).resizable().scaledToFit()
/* 1 */       .rotation3DEffect(.degrees(Double(innerGeo.localOffset(in: outerGeo).width) / 10), axis: (x: 0, y: 1, z: 0))
/* 2 */       .scaleEffect(1.0 - abs(innerGeo.localOffset(in: outerGeo).width) / 800.0)
            }
            .frame(width:200)
          }
        }
      }
    }
    .clipped()
    .border(Color.red, width: 4)
    .frame(width: 400, height: 200)
  }
}

// Provides images for the ScrollView. Tracks and reports image requests.

// Handy extension for finding local coords.
extension GeometryProxy {
  func localOffset(in outerGeo: GeometryProxy) -> CGSize {
    let innerFrame = self.frame(in: .global)
    let outerFrame = outerGeo.frame(in: .global)
    return CGSize(
      width : innerFrame.midX - outerFrame.midX,
      height: innerFrame.midY - outerFrame.midY
    )
  }
}
person Chris    schedule 17.04.2020
comment
На самом деле это не отвечает на мой вопрос, а не как я могу ускорить поиск изображений? но как я могу уменьшить количество ненужных изображений? - person Andrew Duncan; 18.04.2020
comment
в этой статье (наполовину) хорошо объясняется использование идентификатора представления (_) для повышения производительности представления. Может быть, это поможет в твоей ситуации. swiftui-lab.com/swiftui-id - person workingdog; 18.04.2020
comment
Это было интересно, спасибо, собачка. Проблема в том, где разместить обновление значения id? В моем примере кода нет места, где раскрывается поток управления. Это чисто декларативный подход, который должен позволить мне забыть обо всем, что было до этого. Может быть, если я сломаю обработчик перетаскивания ... кстати, я все равно пытался вставить некоторые .id модификаторы, но безрезультатно. Возможно, это просто подчеркивает преимущества предварительной выборки и источников данных, как в старые времена UITableView. Я вижу, что нужно выполнить тысячи операций, но не тысячи повторных выборок. - person Andrew Duncan; 18.04.2020
comment
Хотя я считаю, что SwiftUI (или я) должен (буду?) Быть умнее в этом отношении, ощутимые задержки были неприемлемы. Я написал тот же код для UIScrollView и заключил его в UIViewRepresentable. Ни глюков, ни тысяч повторных загрузок. Самым сложным было выяснить, как настроить CATransform3D. Вкратце: измените компонент m14 на ± 0,001. - person Andrew Duncan; 18.04.2020