SwiftUI 性能非常糟糕,许多矩形要着色
SwuftUI performance very bad with many Rectangles to be colored
我创建了一个像像素一样的矩形 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: [=10=], 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()
}
}
尝试用这个改变你的方法:
func startFillingAllAnimated() {
DispatchQueue.global().async {
for idx in self.pixels.indices {
Thread.sleep(forTimeInterval: 0.03)
DispatchQueue.main.async {
self.pixels[idx].isColored = true
}
}
}
}
部分问题是这段代码:
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")
}
在第一个网格方块变为已填充之前,您将在调试控制台中看到“正在返回”。
因此,所有对 .asycAfter
的调用都已排队,UI 更新被“阻塞”。
您可能想尝试这种方法...
我们将创建一个计时器,每次计时器重复时填充数组中的下一个网格方块。这样我们一次只在一个方块上设置 .isColored
,而且,作为一个额外的好处,它为我们提供了一种在“网格填充”之前 停止 过程的方法”已完成(例如添加一个“停止”按钮与“开始”按钮一起使用:
class Model: ObservableObject {
@Published var pixels: [Pixel]
@Published var myTimer: Timer? = nil
init(totalPixels: Int) {
pixels = (1...totalPixels).map{ Pixel(id: [=12=], 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()
}
}
}
编辑
如果以这种方式使用定时器太慢,您可以尝试这种方法。
为循环使用后台线程,具有(非常小的).sleep
延迟,可以使其更快。
这是您的 Model
class 的另一个版本。我加入了“开始/停止/恢复”按钮,这样填充就可以被中断,然后要么从头开始重新开始,要么从点击停止按钮时的位置恢复:
Button("start") {
model.startFillingAllAnimated()
}
Button("stop") {
model.stopFilling()
}
Button("resume") {
model.resumeFilling()
}
并且 Model
class 变为:
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: [=14=], 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: {[=14=].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
}
}
我创建了一个像像素一样的矩形 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: [=10=], 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()
}
}
尝试用这个改变你的方法:
func startFillingAllAnimated() {
DispatchQueue.global().async {
for idx in self.pixels.indices {
Thread.sleep(forTimeInterval: 0.03)
DispatchQueue.main.async {
self.pixels[idx].isColored = true
}
}
}
}
部分问题是这段代码:
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")
}
在第一个网格方块变为已填充之前,您将在调试控制台中看到“正在返回”。
因此,所有对 .asycAfter
的调用都已排队,UI 更新被“阻塞”。
您可能想尝试这种方法...
我们将创建一个计时器,每次计时器重复时填充数组中的下一个网格方块。这样我们一次只在一个方块上设置 .isColored
,而且,作为一个额外的好处,它为我们提供了一种在“网格填充”之前 停止 过程的方法”已完成(例如添加一个“停止”按钮与“开始”按钮一起使用:
class Model: ObservableObject {
@Published var pixels: [Pixel]
@Published var myTimer: Timer? = nil
init(totalPixels: Int) {
pixels = (1...totalPixels).map{ Pixel(id: [=12=], 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()
}
}
}
编辑
如果以这种方式使用定时器太慢,您可以尝试这种方法。
为循环使用后台线程,具有(非常小的).sleep
延迟,可以使其更快。
这是您的 Model
class 的另一个版本。我加入了“开始/停止/恢复”按钮,这样填充就可以被中断,然后要么从头开始重新开始,要么从点击停止按钮时的位置恢复:
Button("start") {
model.startFillingAllAnimated()
}
Button("stop") {
model.stopFilling()
}
Button("resume") {
model.resumeFilling()
}
并且 Model
class 变为:
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: [=14=], 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: {[=14=].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
}
}