iT邦幫忙

2022 iThome 鐵人賽

DAY 11
0
Mobile Development

使用 SwiftUI 讓有趣的點子變成 Apps系列 第 11

D11 - 葛麗絲逆走鐘: 角度計算器與單一職責原則 Single Responsibility Principle

  • 分享至 

  • xImage
  •  

前一天的文章裡,我們取得了當下的時間,現在需要把時分秒針的角度算出來。但…負責計算的程式碼,該放在哪裡比較好呢? 該不該另外寫一個物件出來處理?

如果「只」考慮 app 會不會動,那寫在哪裡真的沒有差別。寫在 ClockContainerView 的裡面,app 會運作,寫在 ClockContainerView 的外面,他也會運作,寫在 top-level 層級,每個物件都可以呼叫的 func, app 也是會運作。

但,如果考慮「單一職責」原則時,我會想把計算角度的方法寫在一個只會拿來計算角度的類別。

這是 SOLID 裡面的第一個 S - SRP 原則, Single Response Principle

維基解釋

https://zh.wikipedia.org/wiki/单一功能原则

分析需求

  • 這個時鐘目前沒有看到 年、月、日的需求
  • 三根針需要時間的 時、分、秒 才能轉換成角度

所以要計算角度前,需要先得到 時、分、秒,這邊可以用 Foundation 中 Date 相關的類別完成這樣的功能。

負責計算日期的 DateUtility

struct DateUtility {
  
  static let dateFormatter = DateFormatter()
  
  private var calendar: Calendar {
    return Calendar(identifier: .iso8601)
  }
  
  func getSecond(from timeInterval: TimeInterval) -> Int? {
    
    return getDateComponents(from: timeInterval, components: Set([.second])).second
  }
  
  func getMinute(from timeInterval: TimeInterval) -> Int? {
    
    return getDateComponents(from: timeInterval, components: Set([.minute])).minute
  }
  
  func getHour(from timeInterval: TimeInterval) -> Int? {
    return getDateComponents(from: timeInterval, components: Set([.hour])).hour
  }
  
  private func getDateComponents(from timeInterval: TimeInterval, components: Set<Calendar.Component>) -> DateComponents {
    
    let date = Date(timeIntervalSince1970: timeInterval)
    return calendar.dateComponents(components, from: date)
  }
}

這個 DateUtility 物件,可以從 getSecond(:), getMinute(:), getHour(:) 拿取時分秒。

負責計算角度的 AngleUtility

struct AngleUtility {
  
  private let secondToMinute: Double = 60
  
  private let minuteToHour: Double = 60
  
  private let hourToOneCircle: Int = 12
  
  private var dateUtility: DateUtility {
    return DateUtility()
  }
  
  func getSecondHandRadius(from timeInterval: TimeInterval) -> Double {

      guard let second = dateUtility.getSecond(from: timeInterval) else {
          return 0
      }

      return (Double(second) / secondToMinute) * 360
  }

  func getBackwardsSecondHandRadius(from timeInterval: TimeInterval) -> Double {
      return -getSecondHandRadius(from: timeInterval)
  }

  func getMinuteHandRadius(from timeInterval: TimeInterval) -> Double {

      guard let minute = dateUtility.getMinute(from: timeInterval) else {
                return 0
            }

      return (Double(minute) / minuteToHour) * 360
  }

  func getBackwardsMinuteHandRadius(from timeInterval: TimeInterval) -> Double {
      return -getMinuteHandRadius(from: timeInterval)
  }

  func getHourHandRadius(from timeInterval: TimeInterval) -> Double {

      guard let hour = dateUtility.getHour(from: timeInterval),
            let minute = dateUtility.getMinute(from: timeInterval) else {
                return 0
            }

      let hourMod = hour % hourToOneCircle
      let majorRadius = (Double(hourMod) / Double(hourToOneCircle)) * 360
      let minorRadius = getMinorHourRadius(from: minute)
      return majorRadius + minorRadius
  }

  func getBackwardsHourHandRadius(from timeInterval: TimeInterval) -> Double {
      return -getHourHandRadius(from: timeInterval)
  }

  private func getMinorHourRadius(from minute: Int) -> Double {

      return Double(minute) / minuteToHour * 30 //(360 / 12)
  }
}

打完還沒收工,先寫個測試,讓自己晚上能夠安心睡覺

開個 UnitTesting target

https://ithelp.ithome.com.tw/upload/images/20220911/20140622wvA9Do4nlJ.png

然後開始測試 DateUtility 和 AngleUtility 物件

測試 DateUtility

//
//  DateUtilityTests.swift
//  DemoBackwardsClockTests
//
//

import XCTest

class DateUtilityTests: XCTestCase {
  
  private var dateUtility: DateUtility?
  //   Sun May 29 2022 19:33:43 GMT+0800 (台北標準時間)
  private let date = Date(timeIntervalSince1970: 1653824023)
  
  override func setUpWithError() throws {
    super.setUp()
    dateUtility = DateUtility()
  }
  
  override func tearDownWithError() throws {
    dateUtility = nil
    super.tearDown()
  }
  
  func test_getSecound() {
    
    let second = dateUtility?.getSecond(from: date.timeIntervalSince1970)
    XCTAssertEqual(second, 43)
  }
  
  func test_getMinute() {
    
    let minute = dateUtility?.getMinute(from: date.timeIntervalSince1970)
    XCTAssertEqual(minute, 33)
  }
  
  func test_getHour() {
    let hour = dateUtility?.getHour(from: date.timeIntervalSince1970)
    XCTAssertEqual(hour, 19)
  }
}

測 AngleUtility

//
//  AngleUtilityTests.swift
//  DemoBackwardsClockTests
//
//

import XCTest

class AngleUtilityTests: XCTestCase {
  
  private var angleUtility: AngleUtility?
  
  //   Sun May 29 2022 19:33:43 GMT+0800 (台北標準時間)
  private let date = Date(timeIntervalSince1970: 1653824023)
  
  override func setUpWithError() throws {
    super.setUp()
    angleUtility = AngleUtility()
  }
  
  override func tearDownWithError() throws {
    angleUtility = nil
    super.tearDown()
  }
  
  func test_43SecondAngle() {
    
    let radius = angleUtility?.getSecondHandRadius(from: date.timeIntervalSince1970) ?? 0
    let answer = Double(43) / Double(60) * 360
    print("angle: \(radius)")
    print("answer: \(answer)")
    XCTAssertEqual(radius, answer, accuracy: 0.1)
  }
  
  func test_33MinuteAngle() {
    
    let radius = angleUtility?.getMinuteHandRadius(from: date.timeIntervalSince1970) ?? 0
    let answer = Double(33) / Double(60) * 360
    print("angle: \(radius)")
    print("answer: \(answer)")
    XCTAssertEqual(radius, answer, accuracy: 0.1)
  }
  
  func test_19_33HourAngle() {
    
    let radius = angleUtility?.getHourHandRadius(from: date.timeIntervalSince1970) ?? 0
    
    print("angle: \(radius)")
    XCTAssertEqual(radius, 226.5, accuracy: 0.1)
  }
}

測完後,都是綠燈,可以安心了,下一篇: 結合進 View

https://ithelp.ithome.com.tw/upload/images/20220911/20140622eyxNqgDloI.png


上一篇
D10 - 用 SwiftUI 讓有趣的點子變成 Apps{葛麗絲逆走鐘:讓時鐘動起來}
下一篇
D12 - 使用 SwiftUI 讓有趣的點子變成 Apps{葛麗絲逆走鐘: 把角度計算器放進 View 裡面}
系列文
使用 SwiftUI 讓有趣的點子變成 Apps30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言