SwiftUI开发总结(三)

3,339 阅读5分钟

如何使用swiftUI绘制进度条

这个标题名起完之后,我就觉得小了,格局小了!因为在本篇文章的内容里,不仅仅有一个绘图的功能,还会穿插一下比较实用的开发tips。

本文代码来自于我个人研发的满满财表,感兴趣的小伙伴可以去下载一个体验一下,针对于个人的财务管理工具。

最终效果

w1n7q-ubcqk.gif

拆解问题

这是我们惯用的解题策略,一旦遇到一个相对复杂的问题时,算法题也好,数学题也好,程序开发也好,首先要想到的一定是化繁为简。

如果一个复杂问题,不能被拆解成为若干个或者是单个重复的简单问题,那么,我们的思路一定是错的。

上面这句话并非毫无根据的主观臆断,而是遵循于傅立叶变换的思维方法。

因此第一步,我们先将最终效果的样式拆分一下,首先是环形进度条,与之前的风格一样,不废话直接上代码:

struct XXRingShape: Shape {
    var progress: Double = 0.0
    var size: CGSize
    var animatableData: Double {
        get { progress }
        set { progress = newValue }
    }
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: size.width/2, y: size.height/2)
        let radius: CGFloat = (size.width - XXCircleProgressBar.spacing)/2
        path.addArc(center: center,
                    radius: radius,
                    startAngle: .degrees(-90),
                    endAngle: .degrees(-90 + (360 * progress)),
                    clockwise: false)
        return path
    }
}

其中的Shape可以理解为是Core Animation框架中的CAShapeLayer,如果不清楚CAShapeLayer是什么的,也没有问题,CAShapeLayer就像是一个画家,可以用来绘制和渲染2D形状。它可以创建复杂的形状,如圆形、矩形和曲线,并提供了许多属性来控制形状的外观和行为。它还可以与动画一起使用,以创建流畅的过渡效果,要结合path使用。

path的每个参数都十分简单,因此就不过多介绍了,这里需要注意的是animatableData这个属性。

Shape需要使用animatableData才能执行动画是因为Shape是基于路径绘制的,而animatableData可以帮助我们控制路径的变化,从而实现Shape的动画效果。

在往上一层,在使用XXRingShape的时候,需要明确给它一个size,那么如何给它一个明确的尺寸呢?

使用GeometryReader

之前挖了一个坑,在说到List上拉下拉的时候,我们提过,想要知道父视图的矩形框,swiftUI为我们提供了GeometryReader用来解决此类问题。

那么,应该如何使用呢?上代码:

struct XXCircleProgressBar: View {
    var progress: Double
    var color: Color
    @State var scaleValue: Double = 0
    static let spacing: Double = 8
    var lineWidth: Double
    init(progress: Double, color: Color, lineWidth: Double) {
        self.progress = progress
        self.color = color
        self.lineWidth = lineWidth
    }
    var body: some View {
        GeometryReader { geometry in
            ZStack() {
                XXRingShape(progress: 1, size: geometry.size)
                .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
                .foregroundColor(color.opacity(0.2))
              
                XXRingShape(progress: scaleValue, size: geometry.size)
                .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
                .foregroundColor(color)
                .animation(.easeOut(duration: 1), value: scaleValue)
                .onAppear {
                    scaleValue = progress
                }
                .onDisappear {
                    scaleValue = 0
                }
                .frame(width: geometry.size.width, height: geometry.size.width)
            }
        }
    }
}

GeometryReader就像是一个测量工具,用来测量你的swiftUI父视图的大小和位置,帮助你更好地理解和布局你的应用程序。其中的geometry可以理解为是你的父视图,通过对他的大小计算,从而确定包内视图的大小及位置。

一旦我们需要精确布局,实现一些自定义的动画效果,上拉下拉等效果,就离不开使用GeometryReader

在第一篇 《swiftUI总结》 中我们提到了属性修饰器-- @propertyWrapper,也简单地讲述了conbime是什么。而@State就是swiftUI提供的,用来观察属性变化的属性修饰器。swiftUI中的@State修饰符用于在视图中声明和管理状态。与其他修饰符不同,@State是用于管理视图内部状态的修饰符。可以使用@State来声明变量,并且可以在视图中使用它们。这些变量的值可以更改,并且当值更改时,视图将自动重新绘制。

完整代码

没错,从来不拿你们当外人,直接把开头代码的效果代码甩出来:

import SwiftUI
extension Color {
    init(hexString: String) {
        let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
        var int = UInt64()
        Scanner(string: hex).scanHexInt64(&int)
        let r, g, b: UInt64
        switch hex.count {
        case 3: // RGB (12-bit)
            (r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
        case 6: // RGB (24-bit)
            (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
        case 8: // ARGB (32-bit)
            (r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
        default:
            (r, g, b) = (0, 0, 0)
        }
        self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255)
    }
}
​
public struct XXChartTargetCircleModel {
    var outProgress: Double = 0.3
    var midProgress: Double = 0.2
    var inProgress: Double = 0.5
    var progress: Int = 81
    let outColor: Color = Color(hexString: "#1F8A70")
    let midColor: Color = Color(hexString: "#BFDB38")
    let inColor: Color = Color(hexString: "#FC7300")
    var target: String = ""
​
}
​
struct XXAimCircleView: View {
    @Environment(.colorScheme) var colorScheme: ColorScheme
    var title: String
    var data: XXChartTargetCircleModel
    public var formSize:CGSize
    @State var progress: Int = 0
    
    init(title: String, data: XXChartTargetCircleModel) {
        self.title = title
        self.data = data
        self.formSize = CGSize(width: 180, height: 180)
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(Color(hexString: "#F5F5F5"))
                .cornerRadius(20)
            VStack() {
                ZStack {
                    XXCircleProgressBar(progress: data.outProgress, color: data.outColor, lineWidth: 16)
                        .frame(width: self.formSize.width - 30,
                               height: self.formSize.width - 30)
                    XXCircleProgressBar(progress: data.midProgress, color: data.midColor, lineWidth: 12)
                        .frame(width: self.formSize.width - 66,
                               height: self.formSize.width - 66)
                    XXCircleProgressBar(progress: data.inProgress, color: data.inColor, lineWidth: 10)
                        .frame(width: self.formSize.width - 94,
                               height: self.formSize.width - 94)
                    HStack(alignment: .lastTextBaseline) {
                        Text("(progress)")
                            .foregroundColor(.black)
                            .font(.title3) + Text("%")
                            .foregroundColor(.black)
                            .font(.subheadline)
                            
                    }
                    .onAppear {
                        Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { timer in
                            if progress < Int(data.progress) {
                                progress += 1
                            } else {
                                timer.invalidate()
                            }
                        }
                    }
                }
                .padding([.top])
                HStack {
                    Text("(Image(systemName: "target"))")
                    Spacer()
                    Text("(title)·挑战")
                        .font(.bold(.body)())
                        .foregroundColor(.black)
                }
                .padding([.bottom, .leading, .trailing])
            }
        }.frame(minWidth:self.formSize.width,
                maxWidth:self.formSize.width,
                minHeight:self.formSize.height,
                maxHeight:self.formSize.height)
    }
}
struct XXCircleProgressBar: View {
    var progress: Double
    var color: Color
    @State var scaleValue: Double = 0
    static let spacing: Double = 8
    var lineWidth: Double
    init(progress: Double, color: Color, lineWidth: Double) {
        self.progress = progress
        self.color = color
        self.lineWidth = lineWidth
    }
    var body: some View {
        GeometryReader { geometry in
            ZStack() {
                XXRingShape(progress: 1, size: geometry.size)
                .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
                .foregroundColor(color.opacity(0.2))
​
                XXRingShape(progress: scaleValue, size: geometry.size)
                .stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
                .foregroundColor(color)
                .animation(.easeOut(duration: 1), value: scaleValue)
                .onAppear {
                    scaleValue = progress
                }
                .onDisappear {
                    scaleValue = 0
                }
                .frame(width: geometry.size.width, height: geometry.size.width)
            }
        }
    }
}
​
struct XXRingShape: Shape {
    var progress: Double = 0.0
    var size: CGSize
    var animatableData: Double {
        get { progress }
        set { progress = newValue }
    }
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: size.width/2, y: size.height/2)
        let radius: CGFloat = (size.width - XXCircleProgressBar.spacing)/2
        path.addArc(center: center,
                    radius: radius,
                    startAngle: .degrees(-90),
                    endAngle: .degrees(-90 + (360 * progress)),
                    clockwise: false)
        return path
    }
}
struct XXAimCircleView_Previews: PreviewProvider {
    static var previews: some View {
        XXAimCircleView(title: "今日", data: XXChartTargetCircleModel())
    }
}
​

结束语

近期chatGPT的爆火,让一个有趣的问题又重新进入人们的视野——AI是否可以代替人类工作?

这个问题看看chatGPT是怎么回答的。

截屏2023-03-25 18.56.12.png

很巧的是,在我问它这个问题之前,我能想到的答案与它的回答大致相同。没错,随着科技的发展,我们的生产工具的确取得日新月异的进步,因此,一些工具威胁论会产生也是十分自然的可能,人类对于自己无法控制的力量总是充满敬畏。

但我相信,斧子不会自己杀人,能杀死人的只有会用斧子的人。

所以,与其担心会不会斧子砍死,不如先学会使用斧子。

最后,感谢所有为了科学进步,为了社会进步,为了人类进步的伟大科学家。Respect!!!

封面是使用Midjourney绘制。再次感谢那些所有推动科技发展的人!