Chào các bạn, dạo này tư tưởng của mình là kiến thức đi liền thực tiễn nên mình sẽ không viết những bài về thuần kiến thức nữa mà sẽ tạo ra những sản phẩm thực tế luôn. Hôm nay, chúng ta sẽ cùng nhau tạo ra một ứng dụng nho nhỏ là cái đồng hồ kim treo tường kiểu cổ điển, sự chuyển động của ba cây kim để hiển thị các mốc thời gian trong vòng một ngày. 😨
Trong iOS, khi nói đến chuyển động thì ta sẽ nghĩ ngay đến animation , nó là một phần của iOS bên mảng giao diện người dùng, giúp cho ứng dụng trở nên mượt mà, thú vị và thân thiện hơn. Tất nhiên là chúng ta có thể tự tính toán và làm các cây kim chuyển động mà không cần đến animation, nhưng nếu thế thì ứng dụng sẽ khó được mượt mà và phức tạp hơn.

Trong bài có thể vừa tiếng Anh vừa tiếng Việt, mọi người thông cảm bỏ qua nhé .
Link có che đây ạ: Classical clock

Bắt đầu nào

1. Tạo new project

Open Xcode -> File -> New -> Project... -> Auto select Single View Application -> Next and fill info là xong phần tạo mới một project, quá nhanh :))

2. Custom một cái view hiển thị đồng hồ

Đầu tiên, chúng ta tạo một custom view hiển thị cái đồng hồ là ClockView.
Vào thư mục chứa source code trong Finder để tạo trước một folder là ClockView để dễ quản lý
File -> New -> File... : Chọn file định dạng Swift để tạo một file .swift, file này chứa code mình viết, đặt tên là ClockView, nó sẽ có dạng full là ClockView.swift nhớ lưu nó trong folder mới tạo ở trên.
File -> New -> File... : Chọn file định dạng View để tạo một file .xib, file này thực chất là một file chứa các thẻ html elements, thường thì mình sẽ kéo views, layouts nhờ sự trợ giúp của Xcode Interface Builder, chứ ít khi code tay. Đặt tên nó giống với ClockView luôn cho dễ làm việc, ClockView.xib , và nhớ bỏ nó trong folder tạo lúc trước.

Hoàn hảo, bây giờ mình select file .xib này vào kéo các views vào, vì chỗ này khó nói chi tiết được nên các bạn download cái source mình để link ở trên ấy., mở ra xem trực tiếp cho dễ. Trong file .xib này nhớ chú ý một điểm là chúng ta sẽ có ba cái hình, tương ứng với ba cây kim hiển thị giờ, phút và giây, nhớ là canh chúng nó ở giữa cái khung của đồng hồ, chứ đừng canh cái gốc của nó ở giữa khung theo thực tế, lý do thì lát nữa sẽ nói sau 🤓.

Cái giao diện trong file .xib sẽ có hình dạng như thế này
https://viblo.asia/uploads/600a7ec8-dda1-428c-a7f2-c195d38de721.png

Phần tạo view như thế là xong rồi, giờ mình vô trọng tâm của bài này là vẽ cái đồng hồ và làm cho nó chạy như cách mà một cái đồng hồ kim hoạt động ngoài đời thực.

Vẽ đồng hồ:
Mở file ClockView.swift và khai báo ba cái properties biểu thị cho ba cây kim giờ, phút và giây.

class ClockView: UIView {

    @IBOutlet fileprivate weak var hourImageView: UIImageView?
    @IBOutlet fileprivate weak var minuteImageView: UIImageView?
    @IBOutlet fileprivate weak var secondImageView: UIImageView?
    
}

Nhớ "kéo outlet" cho nó nha, mình nói nôm na theo cái cách mà các lập trình viên hay dùng, không hiểu thì chịu 🤭

Rồi, ý tưởng là chúng ta sẽ xoay các "cây kim" này giống theo một cái đồng hồ ngoài đời thực.
Chia cái khung thành một vòng tròn, thì mỗi 1s, cây kim giây sẽ xoay 1 góc là 6 độ = 1/60 của 360 độ. Khi kim giây xoay xong 1 vòng tròn thì kim phút sẽ xoay cũng 1 góc 6 độ. Khi kim phút xoay xong 1 vòng tròn thì kim giờ sẽ xoay 1 góc 30 độ.
Cái này thì ai cũng biết rồi, mình nhắc lại thôi, và đến lúc này, nếu bạn suy nghĩ là chúng ta sẽ tính ra góc, rồi áp dụng sin, cos, tan vào để tính toạ độ, xong cái cho các cây kim chỉ tới toạ độ đó thì mình xin lỗi, bạn nghĩ xa quá rồi, quay về với Swift thôi, thời đại công nghiệp hoá - hiện đại hoá rồi không còn thời thủ công, lúa nước nữa đâu. 😁
Với Swift chúng ta không nên quá quan tâm đến việc sử dụng các phép toán thông thường để làm những việc liên quan đến việc xoay, dịch chuyển các views. Mà chúng ta nên nghĩ ngay đến thứ mà mình đã đề cập ở đầu bài, đó là animation. Một thứ đầy sức mạnh cho việc làm những thứ liên quan đến hoạt hình, đúng với ý nghĩa tên gọi của nó.
Lan man quá, mình vô luôn đây. Như đã biết, theo đơn vị radian thì một vòng tròn thì là 2π, vậy tại một thời điểm thì:

  • giây = giây * 2 * π / 60
  • phút = phút * 2 * π / 60
  • giờ = giờ * 2 * π / 12

Áp dụng điều đó vào trong code thì chúng ta sẽ làm từng bước như sau:
Đầu tiên là get các date components tại thời điểm hiện tại Date().

let calendar = Calendar(identifier: Calendar.Identifier.gregorian)
let units = [Calendar.Component.hour, 
             Calendar.Component.minute, 
             Calendar.Component.second,
             Calendar.Component.nanosecond]
let components = calendar.dateComponents(Set<Calendar.Component>(units), from: Date())

Ở đây chúng ta để ý đến 1 component là nanosecond. Nó chính là cái khoảng dư thật sự của giây hiện tại, ví dụ: second chúng ta get được là giây thứ 10, nhưng thực tế là đang ở khoảng giữa 10s và 11s thì sẽ có thêm 1 khoảng dư nanosecond nữa,nghe tên là chúng ta biết nó tỉ lệ đến 1/1.000.000.000 giây. Nên khi mà tính giây thì ta cộng thêm nano giây này vào thì sẽ chính xác nhất, nếu sai thì lỗi không phải do mình mà do hệ điều hành rồi, :))).
Code tiếp sẽ như sau:

guard let hour = components.hour,
    let minute = components.minute,
    let second = components.second,
    let nanoSecond = components.nanosecond else {
        return
}

let realSecond = Double(second) + pow(Double(nanoSecond),Double(-9))
let realMinute = Double(minute) + realSecond / 60.0
let realHour = Double(hour) + realMinute / 60.0
let secondAngle = realSecond / 60.0 * .pi * 2.0
let minuteAngle = realMinute / 60.0 * .pi * 2.0
let hourAngle = realHour / 12.0 * .pi * 2.0

Ta lấy được 3 cái góc thực tế của nó rồi, bây giờ là lúc aniamtion thể hiện.
Trong class UIView, thì nó có 1 properties tên là transform kiểu dữ liệu của nó là CGAffineTransform. Theo gu gồ nó dịch thì là một ma trận chuyển đổi dùng trong đồ hoạ 2D 😰. Theo như mình hiểu nôm na thì nó là cái thứ mà có thể xoay cái view của mình theo một góc nhất định theo mặt phẳng 2D. Mình chỉ hiểu đơn giản như thế thôi, có xoáy vô nữa thì chịu, chưa tìm hiểu tới tận cùng.

Rồi xoay thôi:

secondImageView?.transform = CGAffineTransform(rotationAngle: CGFloat(secondAngle))
minuteImageView?.transform = CGAffineTransform(rotationAngle: CGFloat(minuteAngle))
hourImageView?.transform = CGAffineTransform(rotationAngle: CGFloat(hourAngle))

Sau đó đặt schedule mỗi một giây cho cái timer để lấy thời gian rồi thực hiện lại việc như trên là sẽ xoay được 3 cây kim đó đúng như giờ thực tế.
https://viblo.asia/uploads/b303d857-7518-4396-ac61-e160a1e32bb6.png

Xoay thì đúng rồi, vấn đề là nhìn nó chán quá, việc hiển thị các cây kim chẳng giống với thực tế gì cả. Đến lúc này những người chưa bao giờ làm với animation sẽ nghĩ đến việc dịch chuyển các cây kim này sao cho gốc của chúng nó ở ngay tâm bằng cách điều chỉnh origin hoặc center của view, awsome!, nghĩ hay lắm nhưng trật rồi. Nếu ta move view như vậy thì ai đi đường nấy luôn, nó sẽ xoay bậy ngay vì transform nó xoay bằng tâm của view mà.
Vì vậy, chúng ta sẽ làm việc với layer của view, có kiểu dữ liệu là CALayer. Dịch theo định nghĩa của Apple thì nó là một đối tượng quản lý nội dung dựa theo hình ảnh và cho phép bạn thực hiện những chuyển động hoạt hình trên nội dung đó, nghe nó hàn lâm quá. Hiểu nôm na thì nó như là một lớp màng che phủ trên cái UIView, và chúng ta có thể làm việc trực tiếp với nó như là backgroundColor, position, size, border, etc.... Trong trường hợp của chúng ta thì ta sẽ quan tâm tới anchorPoint property của layer.

anchorPoint là một điểm mà nó thể hiện mối liên hệ giữa layer và superlayer. Hiểu như thế này, toạ độ (coordinate) của 1 layer thì có giá trị từ 0 -> 1. Góc dưới bên trái thì là (0, 0), góc trên bên phải thì là (1, 1). Thì giá trị mặc định của anchorPoint là (0.5, 0.5), ngay chính giữa tâm, trùng với điểm position của layer và center của view. Bây giờ ta chỉ việc dời cái điểm "anchor" này đi thì layer nó cũng dời theo mà k làm ảnh hưởng đến cái center của view, nên việc xoay vẫn bình thường không có bất thường cả.
Theo thực tế code thì:

        hourImageView?.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
        minuteImageView?.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
        secondImageView?.layer.anchorPoint = CGPoint(x: 0.5, y: 0.7)

Kim giờ và phút sẽ được dịch cái image của chúng lên trên đến ngay chỗ bottom của cái hình thì trùng với center của bounds đó, kim giây thì dịch lên 1 chút là 0.2 nhìn nó sẽ hài hoà hơn chứ dịch như mấy cái kim kia thì sẽ xấu vì nó có cái "đuôi" lòi ra nữa.

Xong rồi, vậy là cơ bản xong cái view đồng hồ. Còn phải thêm vài dòng code để cái đồng hồ có thể chạy được, thì các bạn có thể mở code ra xem, nó rất đơn giản và dễ đọc hiểu.

Hình thật

Kết

Mình rất thích việc custom những thứ mà mình có thể làm được trước khi đi search trên mạng. Nên việc mà làm những ứng dụng kiểu như này đã tạo một niềm vui không nhỏ cho mình trong những ngày làm việc nhàm chán và mệt mỏi. Mình cũng hi vọng mọi người, ngoài công việc ra thì nên ứng dụng kiến thức, sự hiểu biết của mình để tạo ra những sản phẩm thực tế để đời sống tinh thần phong phú hơn. Nghiệp dev là một nghiệp mà gắn liền với sự cô độc, thức đêm và những con số, đoạn mã khô khan, nhàm chán.
Cảm ơn các bạn đã đọc tới tận đây. Chào tạm biệt và hẹn gặp lại quý vị trong những chương trình tiếp theo.
Link full HD: Classical clock