Skip to content
On this page

实现一个简单的倒计时钟表

说明

本次想通过实现一个倒计时的时钟来练习TimelineView以及Canvas的使用。首先时间一个倒计时的选择页面,选择需要倒计时的时间。然后实现一个时钟的样式,用来倒计式。最后实现倒计时结束的提示页面。

倒计时选择页面

整个倒计时选择界面比较简单,如下图所示

倒计时

整个页面是在一个NavigationView中,因为SwiftUI中的DatePicker不支持精确到秒,所以需要自己实验一个,这里直接从网上抄来一个,具体的代码可以参考这里

swift
 VStack{
              PickerView(seconds: $countDownSec)
              Button("start"){
                self.startDate = .now
              }.disabled(countDownSec==0)
        }

倒计计选择页面是overlay到时钟页面上面的,当点击start后,设置了开始时间,刚overlay界面消失

时钟界面及结束页面

时间页面比较简单,如下图所示,一个文字的倒计时加上一个时钟。时钟上面倒计时的区域以红色为底,指针会随着倒计时的时间而移动,这里为了简单起见,只实现了分针的移动,整个页面的更新通过TimelineView来驱动。 clock

具体的代码如下,时间耗尽前显示当前剩余的秒数以及时钟,耗尽后显示结果界面。

swift
 TimelineView(.animation) { context in
          VStack{
           
            if startDate != .distantPast {
              let timeRemain = (Double(countDownSec) - context.date.timeIntervalSince(startDate))/60
              if timeRemain > 0{
                Text("\(Int(timeRemain*60))").padding(.vertical).font(.largeTitle)
               
                  
                  CountdownAnalogView(countDown: timeRemain)
                  
                
              }else{
                Text("time is over").font(.largeTitle.bold())
                Button("reset"){
                  self.startDate = .distantPast
                  self.countDownSec = 0
                }.buttonStyle(.borderedProminent).padding(.vertical)
              }
            }
          }

时钟界面的绘制

整个CountdownAnalogView是是一个Canvas,首先以整个画面为中心,以长宽中小边取一个正方形,同时时钟留一定的边距,以绘制时的offset。

swift
 Canvas { gContext, size in
        // 1
        let clockSize = min(size.width, size.height) * 0.9
        // 2
        let centerOffsetX = min(size.width, size.height) * 0.05 +  (size.width -  min(size.width, size.height))/2
        let centerOffsetY = min(size.width, size.height) * 0.05 +  (size.height -  min(size.width, size.height))/2
        // 3
        let clockCenterX = size.width / 2.0
        let clockCenterY = size.height / 2.0

然后确定时钟的内外边,在内外边两个正方形内分别画圆

swift
 // 4
        let frameRect = CGRect(
          x: centerOffsetX,
          y: centerOffsetY,
          width: clockSize,
          height: clockSize)
        let outframeRect = CGRect(
          x: centerOffsetX-10,
          y: centerOffsetY-10 ,
          width: clockSize+20,
          height: clockSize+20)
        gContext.withCGContext { cgContext in
          
          // 2
          cgContext.setStrokeColor(UIColor.black.cgColor)
          // 3
          cgContext.setFillColor(UIColor.brown.cgColor)
          cgContext.setLineWidth(5.0)
          // 4
          cgContext.addEllipse(in: outframeRect)
          // 5
          cgContext.drawPath(using: .fillStroke)
          
          cgContext.setFillColor(dayColor)
          cgContext.setLineWidth(2.0)
          
          cgContext.addEllipse(in: frameRect)
          // 5
          cgContext.drawPath(using: .fillStroke)

然后将画布的原点设置到时钟的中心方便我们后续操作,然后分别绘制剩余时间的扇形区域,以及刻度表和时针以及分针

swift
 cgContext.setFillColor(UIColor.black.cgColor)
          // 2
          cgContext.translateBy(x: clockCenterX, y: clockCenterY)
          
          // 3
          let angle = 12 / 12.0 * 2 * Double.pi
          let hourRadius = clockSize * 0.65 / 2.0
          
          let minuteRadius = clockSize * 0.75 / 2.0
          let minuteAngle = countDown / 60.0 * 2 * Double.pi
          
          
          addCountDownArc(context: cgContext, angle: minuteAngle, width: 0, length: clockSize/2*0.85)
          
          drawTickMarks(
            gcontext: gContext,
            context: cgContext,
            size: clockSize,
            offset: CGPoint(x: 0, y: 0))
          
          // 4
          drawClockHand(
            context: cgContext,
            angle: angle,
            width: 7.5,
            length: hourRadius)
         
          drawClockHand(
            context: cgContext,
            angle: minuteAngle,
            width: 5.0,
            length: minuteRadius)
          

扇形区域比较简单,通过cgcontext绘制addArc来确定扇形的边,然后填充一个半透明的红色

swift
func addCountDownArc( context: CGContext,
                        angle: Double,
                        width: Double,
                        length: Double){
    context.saveGState()
    context.setFillColor(UIColor.red.cgColor)
    context.setAlpha(0.7)
    context.move(to:  CGPoint(x: 0, y: 0))
    context.rotate(by: -Double.pi/2)
    context.addArc(center: CGPoint(x: 0, y: 0), radius: length, startAngle: 0, endAngle: angle, clockwise: false)
    context.fillPath()
    context.restoreGState()
    
  }

绘制刻度表,通过三角函数分别计算需要刻度的坐标,每5分钟一个长刻度,每1分钟一个短刻度。在绘制长刻度时,同时把文字也绘制上

swift
func drawTickMarks(gcontext: GraphicsContext,context: CGContext, size: Double, offset : CGPoint) {
    // 1
    let clockCenterX = offset.x
    let clockCenterY = offset.y
    let clockRadius = size / 2.0
    context.saveGState()
    context.setStrokeColor(UIColor.gray.cgColor)
    context.setLineWidth(1.5)
    for minitMark in 0..<60 {
      // 3
      let angle = Double(minitMark) / 60.0 * 2.0 * Double.pi
      // 4
      let startX = cos(angle) * clockRadius + clockCenterX
      let startY = sin(angle) * clockRadius + clockCenterY
      // 5
      let endX = cos(angle) * clockRadius * 0.95 + clockCenterX
      let endY = sin(angle) * clockRadius * 0.95 + clockCenterY
      // 6
      context.move(to: CGPoint(x: startX, y: startY))
      // 7
      context.addLine(to: CGPoint(x: endX, y: endY))
      
      // 8
      context.strokePath()
    }
    context.restoreGState()
    // 2
    for hourMark in 0..<12 {
      // 3
      let angle = Double(hourMark) / 12.0 * 2.0 * Double.pi
      // 4
      let startX = cos(angle) * clockRadius + clockCenterX
      let startY = sin(angle) * clockRadius + clockCenterY
      // 5
      let endX = cos(angle) * clockRadius * 0.9 + clockCenterX
      let endY = sin(angle) * clockRadius * 0.9 + clockCenterY
      // 6
      context.move(to: CGPoint(x: startX, y: startY))
      // 7
      context.addLine(to: CGPoint(x: endX, y: endY))
      
      // 8
      context.strokePath()
      let tX = cos(angle) * clockRadius * 0.8
      let tY = sin(angle) * clockRadius * 0.8
      var hour = (hourMark+3)%12
      hour = (hour == 0 ? 12 : hour) * 5
      /*gcontext.draw(Text("\(hour)").font(.title.bold()).foregroundColor(.secondary), at: CGPoint(x: tX,y: tY))*/
      draw("\(hour)",in: context, at: CGPoint(x: tX,y: tY))
    }
  }

然后是绘制时针分针,先旋转坐标系,然后另边上横坐标上下文绘制两条线

swift
 func drawClockHand(
    context: CGContext,
    angle: Double,
    width: Double,
    length: Double
  ) {
    // 1
    context.saveGState()
    // 2
    context.rotate(by: angle)
    // 3
    context.move(to: CGPoint(x: 0, y: 0))
    context.addLine(to: CGPoint(x: -width, y: -length * 0.67))
    context.addLine(to: CGPoint(x: 0, y: -length))
    context.addLine(to: CGPoint(x: width, y: -length * 0.67))
    context.closePath()
    // 4
    context.fillPath()
    // 5
    context.restoreGState()
  }

至此整个工作就完成了,因为实现的比较粗糙,所以还有很多细节没有完善。理解情况下,选择时间可以通过拨动时间上的分针来实现,待后续再进行补充。