问题
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