[转] 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
扩展示例用于开发调试的技巧