SwiftUI and excessive redrawing

和自甴很熟 提交于 2021-01-28 09:50:55

问题


TL;DR:

Applying visual effects to the contents of a ScrollView causes thousands of requests for the same (unchanging) image for each drag gesture. Can I reduce this? (In my real app, I have 50-odd images in the view, and the scrolling is correspondingly sluggish.)

Gist

To give a little life to a scrolling HStack of images, I applied a few transforms for a circular "carousel" effect. (Tips of the hat to sample code from John M. and Paul Hudson)

The code is copy-paste-runnable as given. (You do need to provide an image.) Without the two lines marked /* 1 */ and /* 2 */ the Slide object reports six image requests, no matter how much you drag and scroll. Enable the two lines, and watch the request count zoom to 1000 with a single flick of your finger.

Remarks

SwiftUI is predicated on the inexpensive re-drawing of lightweight Views based on current state. Careless management of state dependency can improperly invalidate parts of the view tree. And in this case, the constant rotation and scaling while scrolling makes the runtime re-render the content.

But... should this necessarily require the continual re-retrieval of static images? Casual dragging back-and-forth of my little finger will trigger tens of thousands of image requests. This seems excessive. Is there a way to reduce the overhead in this example?

Of course this is a primitive design, which lays out all its contents all of the time, instead of taking the cell-reuse approach of, say, UITableView. One might think to apply the transformations only on the three currently-visible views. There is some discussion about this online, but in my attempts, the compiler couldn't do the type inference.

Code

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
    )
  }
}

回答1:


i think you could try it like this:

no, it is not a full solution, i just took one cached image (instead of an array of cached images, which you have to preload beforehand) but the concept should be clear and so this should be fast...i think

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
    )
  }
}


来源:https://stackoverflow.com/questions/61275457/swiftui-and-excessive-redrawing

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!