[转] Safely Updating The View State

参考链接: https://swiftui-lab.com/state-changes/

6个月前 145次点击 来自 移动端

标签: SwiftUI

本文所有代码请跳转查阅 https://gist.github.com/swiftui-lab/ddbb145fb7397aa8923a4604e91f9b3f

需要用到的控件,显示CPU占用率

// MARK: - CPU Wheel
struct CPUWheel: View {
    @State private var cpu: Int = 0
    
    var timer = Timer.publish(every: 0.1, on: .current, in: .common).autoconnect()
    
    var body: some View {
        let gradient = AngularGradient(gradient: Gradient(colors: [Color.green, Color.yellow, Color.red]),
                                       center: .center, angle: Angle(degrees: 0))
        
        return Circle()
            .stroke(lineWidth: 3)
            .foregroundColor(.primary)
            .background(Circle().fill(gradient).clipShape(CPUClip(pct: Double(self.cpu))))
            .shadow(radius: 4)
            .overlay(CPULabel(pct: self.cpu))
            .onReceive(timer) { _ in
                withAnimation {
                    self.cpu = Int(CPUWheel.cpuUsage())
                }
            }
    }
    
    struct CPULabel: View {
        let pct: Int
        
        var body: some View {
            VStack {
                Text("\(self.pct) %")
                    .font(.largeTitle)
                    
                Text("CPU")
                    .font(.body)
            }.transaction({ $0.animation = nil })
        }
    }
    
    struct CPUClip: Shape {
        var pct: Double
        
        var animatableData: Double {
            get { pct }
            set { pct = newValue }
        }
        
        func path(in rect: CGRect) -> Path {
            var path = Path()

            let c = CGPoint(x: rect.midX, y: rect.midY)
            path.move(to: c)
            path.addArc(center: c,
                        radius: rect.width/2.0,
                        startAngle: Angle(degrees: 0),
                        endAngle: Angle(degrees: (pct/100.0) * 360), clockwise: false)
            
            path.closeSubpath()
            return path
        }
    }
    
    // Source for cpuUsage(): based on https://stackoverflow.com/a/44134397/7786555
    static func cpuUsage() -> Double {
        var kr: kern_return_t
        var task_info_count: mach_msg_type_number_t

        task_info_count = mach_msg_type_number_t(TASK_INFO_MAX)
        var tinfo = [integer_t](repeating: 0, count: Int(task_info_count))

        kr = task_info(mach_task_self_, task_flavor_t(TASK_BASIC_INFO), &tinfo, &task_info_count)
        if kr != KERN_SUCCESS {
            return -1
        }

        return [thread_act_t]().withUnsafeBufferPointer { bufferPointer in
            var thread_list: thread_act_array_t? = UnsafeMutablePointer(mutating: bufferPointer.baseAddress)
            var thread_count: mach_msg_type_number_t = 0
            defer {
                if let thread_list = thread_list {
                    vm_deallocate(mach_task_self_, vm_address_t(bitPattern: thread_list), vm_size_t(Int(thread_count) * MemoryLayout<thread_t>.stride) )
                }
            }

            kr = task_threads(mach_task_self_, &thread_list, &thread_count)

            if kr != KERN_SUCCESS {
                return -1
            }

            var tot_cpu: Double = 0

            if let thread_list = thread_list {

                for j in 0 ..< Int(thread_count) {
                    var thread_info_count = mach_msg_type_number_t(THREAD_INFO_MAX)
                    var thinfo = [integer_t](repeating: 0, count: Int(thread_info_count))
                    kr = thread_info(thread_list[j], thread_flavor_t(THREAD_BASIC_INFO),
                                     &thinfo, &thread_info_count)
                    if kr != KERN_SUCCESS {
                        return -1
                    }

                    let threadBasicInfo = convertThreadInfoToThreadBasicInfo(thinfo)

                    if threadBasicInfo.flags != TH_FLAGS_IDLE {
                        tot_cpu += (Double(threadBasicInfo.cpu_usage) / Double(TH_USAGE_SCALE)) * 100.0
                    }
                } // for each thread
            }
            
            return tot_cpu

        }
    }

    static func convertThreadInfoToThreadBasicInfo(_ threadInfo: [integer_t]) -> thread_basic_info {
        var result = thread_basic_info()

        result.user_time = time_value_t(seconds: threadInfo[0], microseconds: threadInfo[1])
        result.system_time = time_value_t(seconds: threadInfo[2], microseconds: threadInfo[3])
        result.cpu_usage = threadInfo[4]
        result.policy = threadInfo[5]
        result.run_state = threadInfo[6]
        result.flags = threadInfo[7]
        result.suspend_count = threadInfo[8]
        result.sleep_time = threadInfo[9]

        return result
    }

}

如下代码:

struct OutOfControlView: View {
    @State private var count: Int = 0

    var body: some View {
        self.count += 1

        return Text("计算次数:\(self.count)")
            .multilineTextAlignment(.center)
    }
}

Xcode报警信息:

[SwiftUI] Modifying state during view update, this will cause undefined behavior.

OK,假设我们使用以下代码尝试消除报警

DispatchQueue.main.async {
    self.counter += 1
}

struct ContentView: View {
    @State private var showOutOfControlView = false
    
    var body: some View {
        VStack(spacing: 10) {
            CPUWheel().frame(height: 150)

            VStack {
                if showOutOfControlView { OutOfControlView() }
            }.frame(height: 80)

            Button(self.showOutOfControlView ? "Hide": "Show") {
                self.showOutOfControlView.toggle()
            }
        }
    }
}

struct OutOfControlView: View {
    @State private var counter = 0
    
    var body: some View {

        DispatchQueue.main.async {
            self.counter += 1
        }
        
        return Text("Computed Times\n\(counter)").multilineTextAlignment(.center)
    }
}

没有报警,但是CPU疯了,OutOfControlView在一直刷新???死循环原因如下:

  • DispatchQueue.main.async是一个异步函数,就跟按钮的点击事件一样,在计算body的时候,并不会直接执行
  • 当body计算完成后才会执行DispatchQueue.main.async中的代码,这时候状态修改了,又触发了View的刷新
  • 一直重复循环上边两个过程

Breaking The Loop

原作者使用给出了一个新的例子,用于讲解如何处理以上类似情况下的死循环

struct ExampleView2: View {
    @State private var flag = false
    @State private var cardinalDirection = ""
    
    var body: some View {
        return VStack(spacing: 30) {
            CPUWheel().frame(height: 150)
            
            Text("\(cardinalDirection)").font(.largeTitle)
            Image(systemName: "location.north")
                .resizable()
                .frame(width: 100, height: 100)
                .foregroundColor(.red)
                .modifier(RotateNeedle(cardinalDirection: self.$cardinalDirection, angle: self.flag ? 0 : 360))
            
            
            Button("Animate") {
                withAnimation(.easeInOut(duration: 3.0)) {
                    self.flag.toggle()
                }
            }
        }
    }
}

struct RotateNeedle: GeometryEffect {
    @Binding var cardinalDirection: String
    
    var angle: Double
    
    var animatableData: Double {
        get { angle }
        set { angle = newValue }
    }
    
    func effectValue(size: CGSize) -> ProjectionTransform {
        DispatchQueue.main.async {
            self.cardinalDirection = self.angleToString(self.angle)
        }
        
        let rotation = CGAffineTransform(rotationAngle: CGFloat(angle * (Double.pi / 180.0)))
        let offset1 = CGAffineTransform(translationX: size.width/2.0, y: size.height/2.0)
        let offset2 = CGAffineTransform(translationX: -size.width/2.0, y: -size.height/2.0)
        return ProjectionTransform(offset2.concatenating(rotation).concatenating(offset1))
    }
    
    func angleToString(_ a: Double) -> String {
        switch a {
        case 315..<405:
            fallthrough
        case 0..<45:
            return "North"
        case 45..<135:
            return "East"
        case 135..<225:
            return "South"
        default:
            return "West"
        }
    }
}

重点:

  • @Binding var cardinalDirection: String 在RotateEffect中,我们通过Binding的方式直接修改状态
  • angleToString函数用于计算某个角度下的方向

当进行旋转的时候,self.direction一直都在改变,但为什么没有造成CPU的过度消耗呢?

如你所见,SwiftUI 足够聪明,知道 body 不需要每次都重新计算,只有当状态真正改变时才需要重新计算。 这意味着除非您在状态中设置不同的值,否则视图不会失效。

在上述情况下:只有当基本方向不同时才会请求刷新视图。

这就是为什么 CPU 不会发疯的原因。 通过分配与之前相同的值,我们打破了在上一个示例中看到的死循环情况。

Unexpected Loops

struct ContentView: View {
    @State private var width: CGFloat = 0.0
    
    var body: some View {
        Text("Width = \(width)")
            .font(.largeTitle)
            .background(WidthGetter(width: self.$width))
    }
    
    struct WidthGetter: View {
        @Binding var width: CGFloat
        
        var body: some View {
            GeometryReader { proxy -> AnyView in
                DispatchQueue.main.async {
                    self.width = proxy.frame(in: .local).width
                }
                return AnyView(Color.clear)
            }
        }
    }
}

以上代码会导致如下情况

当我们在WidthGetter中修改状态width的时候,都需要重新刷新body,由于数字的宽度都不一样,造成了死循环
解决方案是固定字体大小

.font(.custom("Menlo", size: 32))

One More Thing

原作者最后提供了一个EnvironmentObject扩展示例用于开发调试的技巧

Made with in Shangrao,China By Devler.

Copyright © Devler 2012 - 2022

赣ICP备19009883号-1