【问题标题】:SwuftUI performance very bad with many Rectangles to be coloredSwiftUI 性能非常糟糕,许多矩形要着色
【发布时间】:2021-03-06 13:23:03
【问题描述】:

我创建了一个像像素一样的矩形 LazyVGrid。我想延迟部分或全部着色,以便在填充期间执行类似动画,但性能非常糟糕,我认为每次更新都会刷新所有矩形。

行为

代码

struct Pixel: Identifiable, Hashable {
    var id: Int
    var isColored: Bool
}


class Model: ObservableObject {
    
    @Published var pixels: [Pixel]
    
    init(totalPixels: Int) {
        pixels = (1...totalPixels).map{ Pixel(id: $0, isColored: false)}
    }
    
    func pixelsRange(num:Int, clusterDimension:Int) -> [Pixel]{
        return Array(pixels[(num-1)*clusterDimension..<clusterDimension*num])
    }

    func startFillingAllAnimated() {
        for idx in pixels.indices {
            let addTime = idx
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(addTime) * 0.1) {
                self.pixels[idx].isColored = true
            }
        }
    }
}
struct TotalView: View {
    
    static var totalPixels = 1280
    
    @StateObject var model = Model(totalPixels: totalPixels)
    
    var clusterDimension = 16
    
    static let bigSpacing:CGFloat = 2
    
    let bigColumns = [
        GridItem(.flexible(), spacing: bigSpacing),
        GridItem(.flexible(), spacing: bigSpacing),
        GridItem(.flexible(), spacing: bigSpacing),
        GridItem(.flexible(), spacing: bigSpacing),
        GridItem(.flexible(), spacing: bigSpacing),
        GridItem(.flexible(), spacing: bigSpacing),
        GridItem(.flexible(), spacing: bigSpacing),
        
        GridItem(.flexible(), spacing: bigSpacing)
    ]
    
    @State var numToBeColored: Int = 8
    
    var body: some View {
        
        VStack {

            Button("start") {
                model.startFillingAllAnimated()
            }
            ScrollView {
                LazyVGrid(columns: bigColumns, alignment: .center, spacing: 2){
                    ForEach(0..<TotalView.totalPixels/clusterDimension, id: \.self) { num in
                        ClusterView(pixels: $model.pixels, clusterNumber: num, clusterDimension: clusterDimension, color: .red)
                    }
                }
            }
            .padding(.horizontal, 4)
        }
        
    }
}





struct ClusterView: View {
    
    
    @Binding var pixels: [Pixel]
    let clusterNumber: Int
    let clusterDimension: Int
    let color: Color
    
    static let spacing:CGFloat = 2
    static let boxDimension:CGFloat = 9

    let columns = [
        GridItem(.fixed(boxDimension), spacing: spacing),
        GridItem(.fixed(boxDimension), spacing: spacing),
        GridItem(.fixed(boxDimension), spacing: spacing),
        GridItem(.fixed(boxDimension), spacing: spacing)
    ]

    var body: some View {
  
        LazyVGrid(columns: columns, alignment: .center, spacing: ClusterView.spacing) {
            ForEach(pixels[clusterNumber*clusterDimension..<clusterDimension*(clusterNumber+1)], id: \.self) { pixel in
                Rectangle()
                    .aspectRatio(1.0, contentMode: .fit)
                    .border(color)
                    .foregroundColor(pixel.isColored ? color:.clear)
            }
        }
        
    }
}

struct TotalView_Previews: PreviewProvider {
    static var previews: some View {
        TotalView()
    }
}

【问题讨论】:

    标签: ios swift iphone swiftui


    【解决方案1】:

    尝试用这个改变你的方法:

    func startFillingAllAnimated() {
        DispatchQueue.global().async {
            for idx in self.pixels.indices {
                Thread.sleep(forTimeInterval: 0.03)
                DispatchQueue.main.async {
                    self.pixels[idx].isColored = true
                }
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      部分问题在于这段代码:

      func startFillingAllAnimated() {
          
          for idx in pixels.indices {
              let addTime = idx
              DispatchQueue.main.asyncAfter(deadline: .now() + Double(addTime) * 0.1) {
                  self.pixels[idx].isColored = true
              }
          }
      }
      

      在一个非常紧凑的循环中运行,并且几乎是瞬间完全执行。

      您可以通过在末尾添加print() 语句来确认:

      func startFillingAllAnimated() {
          
          for idx in pixels.indices {
              let addTime = idx
              DispatchQueue.main.asyncAfter(deadline: .now() + Double(addTime) * 0.1) {
                  self.pixels[idx].isColored = true
              }
          }
      
          print("returning")
      }
      

      在第一个方格变为填充之前,您将在调试控制台中看到“returning”打印。

      因此,所有对 .asycAfter 的调用都已排队,并且 UI 更新“阻塞”了。

      您可能想尝试这种方法...

      我们将创建一个 Timer,每次 Timer 重复时填充数组中的下一个方格。这样,我们一次只在一个方格上设置.isColored,而且,作为一个额外的好处,它为我们提供了一种在“网格填充”完成之前停止该过程的方法(例如作为添加“停止”按钮与“开始”按钮一起使用:

      class Model: ObservableObject {
          
          @Published var pixels: [Pixel]
          
          @Published var myTimer: Timer? = nil
      
          init(totalPixels: Int) {
              pixels = (1...totalPixels).map{ Pixel(id: $0, isColored: false)}
          }
          
          func pixelsRange(num:Int, clusterDimension:Int) -> [Pixel]{
              return Array(pixels[(num-1)*clusterDimension..<clusterDimension*num])
          }
          
          func startFillingAllAnimated() {
              // local index var
              var idx: Int = 0
              // create and start a Timer
              myTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
                  // if we've reached the end of the array, Stop the Timer
                  if idx == self.pixels.count {
                      timer.invalidate()
                      return
                  }
                  self.pixels[idx].isColored = true
                  idx += 1
              }
          }
      
          // call this if we want to Stop the Timer
          //  before the grid has been completely filled
          func stopFilling() {
              if let t = myTimer {
                  t.invalidate()
              }
          }
          
      }
      

      编辑

      如果以这种方式使用 Timer 太慢,您可以尝试这种方法。

      为循环使用后台线程,延迟(非常小).sleep,可以使其更快。

      这是您的 Model 课程的另一个版本。我添加了“开始/停止/恢复”按钮,因此可以中断填充,然后从头开始重新开始,或者从点击停止按钮时的位置恢复:

          Button("start") {
              model.startFillingAllAnimated()
          }
          Button("stop") {
              model.stopFilling()
          }
          Button("resume") {
              model.resumeFilling()
          }
      

      Model 类变为:

      class Model: ObservableObject {
          
          @Published var pixels: [Pixel]
          
          // so we can interrupt the filling loop
          @Published var keepRunning: Bool = false
          
          init(totalPixels: Int) {
              pixels = (1...totalPixels).map{ Pixel(id: $0, isColored: false)}
          }
          
          func pixelsRange(num:Int, clusterDimension:Int) -> [Pixel]{
              return Array(pixels[(num-1)*clusterDimension..<clusterDimension*num])
          }
          
          func startFillingAllAnimated() {
              // if "start" button tapped,
              //  "un-color" all the pixels
              for idx in self.pixels.indices {
                  self.pixels[idx].isColored = false
              }
              // start filling them
              resumeFilling()
          }
          
          func resumeFilling() {
              // if ALL pixels are already "colored" don't do anything (just return)
              guard let i = pixels.firstIndex(where: {$0.isColored == false}) else { return }
              // set the running flag
              keepRunning = true
              DispatchQueue.global().async {
                  // start at the first non-colored pixel
                  for idx in i..<self.pixels.count {
                      // insert a slight delay
                      //  based on quick testing...
                      //  0.0020 will take about 3 seconds to fill them all
                      //  0.0010 will take about 1.5 seconds to fill them all
                      //  0.0002 will take about 0.3 seconds to fill them all
                      //  anything shorter pretty much fills them all instantly
                      //  so, you probably want somewhere between
                      Thread.sleep(forTimeInterval: 0.0010)
                      DispatchQueue.main.async {
                          self.pixels[idx].isColored = true
                      }
                      // if keepRunning was set to false, break out of the loop
                      if !self.keepRunning {
                          break
                      }
                  }
              }
          }
      
          // call this if we want to Stop
          //  before the grid has been completely filled
          func stopFilling() {
              self.keepRunning = false
          }
          
      }
      

      【讨论】:

      • 它正在工作,但如果我减少时间间隔,则需要大量时间来填充所有内容,有什么想法可以快速完成吗?
      • @SimonePistecchia - 请参阅我的回答中的 编辑
      猜你喜欢
      • 2011-12-19
      • 1970-01-01
      • 1970-01-01
      • 2012-05-06
      • 2016-06-29
      • 2018-01-16
      • 2021-04-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多