SwiftUI でデジタル時計のUIを実装してみる

SwiftUIの勉強がてらデジタルウォッチのUIを実装してみることにした。
チープな白黒液晶が懐かしくなり８０〜９０年代を席巻したカ⚪︎オ データバ⚪︎クなど所謂チープカシ⚪︎と呼ばれる腕時計のUIを参考に幾つかそのコンポーネントを再現することにした。

最終的にはそれをwatchOS上で動かせるようにしたいと思ったが、物理ボタンでの操作を想定したUIのはずなのでむずかしそう。
一種のアート作品として生暖かく見守っていただきたい。


# 📋 概要
どのようなアウトプットにすべきか考えたところ、懐かしいデジタルウォッチのUIコンポーネントを提供するレトロフューチャーなOSSにすることにした。需要はないだろうけど

現状のリポジトリは以下（まだPackage化は半端な状態）
https://github.com/JikeLab/DigitalClockKit

幾つか example として実装してみた画面はこの通り。
|||
|-|-|
|<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/65cdf90f-d1e8-7083-b5b7-55435ee1e2e7.png">|<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a98fb863-9e64-26a0-e174-907df33494c0.png">


# 🔎 コンポーネントの説明
## 数字と一部のアルファベット
[7セグメントディスプレイ](https://ja.wikipedia.org/wiki/7セグメントディスプレイ)というらしい。
主に日付や時刻を表現する部分を表現するためのコンポーネントでデジタル時計によっては曜日を表す際に一部のアルファベットもこれを使って表現されている。

まずは描画パスを指定するためのOptionSetを定義する。
```Swift:DigitView.swift#L87-L98
struct PathFlags: OptionSet {
    let rawValue: Int16
    static let top = PathFlags(rawValue: 1 << 0)    // 上辺
    static let rightTop = PathFlags(rawValue: 1 << 1)    // 右辺の上部
    static let rightBottom = PathFlags(rawValue: 1 << 2)    // 右辺の下部
    static let bottom = PathFlags(rawValue: 1 << 3)    // 下辺
    static let leftTop = PathFlags(rawValue: 1 << 4)    // 左辺の上部
    static let leftBottom = PathFlags(rawValue: 1 << 5)    // 左辺の下部
    static let middle = PathFlags(rawValue: 1 << 6)    // 真ん中の辺
    static let center = PathFlags(rawValue: 1 << 7)    // アルファベットのMやWを表現するための中央線
    static let outsideLeftTop = PathFlags(rawValue: 1 << 8)    // アルファベットのRを表現するためのパス
}
```

描画部分はこのOptionSetに従って指定されているPathを書く実装にする。
`componentSize` は数字一つの大きさ。
```Swift:DigitView.swift#L111-L231
private struct DigitContentView: View {
    let type: DigitType
    let componentSize: CGSize
    let color: Color

    var body: some View {
        ZStack {
            let inset = componentSize.width / 18
            let margin = componentSize.width / 18
            let lineWidth = componentSize.width / 3.5
            if type.paths.contains(.top) {
                // top
                Path { path in
                    path.move(to: CGPoint(x: (inset + margin), y: inset))
                    path.addLine(to: CGPoint(x: (componentSize.width - inset - margin), y: inset))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth - margin), y: lineWidth))
                    path.addLine(to: CGPoint(x: (lineWidth + margin), y: lineWidth))
                }
                .fill(color)
            }
            if type.paths.contains(.rightTop) {
                // rightTop
                Path { path in
                    path.move(to: CGPoint(x: (componentSize.width - lineWidth), y: (lineWidth + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - inset), y: (inset + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - inset), y: (componentSize.height / 2 - margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth / 2), y: (componentSize.height / 2 - margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth), y: (componentSize.height / 2 - lineWidth / 2 - margin)))
                }
                .fill(color)
            }
            if type.paths.contains(.rightBottom) {
                // rightBottom
                Path { path in
                    path.move(to: CGPoint(x: (componentSize.width - lineWidth), y: (componentSize.height / 2 + lineWidth / 2 + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth / 2), y: (componentSize.height / 2 + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - inset), y: (componentSize.height / 2 + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - inset), y: (componentSize.height - inset - margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth), y: (componentSize.height - lineWidth - margin)))
                }
                .fill(color)
            }
            if type.paths.contains(.bottom) {
                // bottom
                Path { path in
                    path.move(to: CGPoint(x: (lineWidth + margin), y: (componentSize.height - lineWidth)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth - margin), y: (componentSize.height - lineWidth)))
                    path.addLine(to: CGPoint(x: (componentSize.width - inset - margin), y: (componentSize.height - inset)))
                    path.addLine(to: CGPoint(x: (inset + margin), y: (componentSize.height - inset)))
                }
                .fill(color)
            }
            if type.paths.contains(.leftTop) {
                // leftTop
                Path { path in
                    path.move(to: CGPoint(x: inset, y: (inset + margin)))
                    path.addLine(to: CGPoint(x: lineWidth, y: (lineWidth + margin)))
                    path.addLine(to: CGPoint(x: lineWidth, y: (componentSize.height / 2 - lineWidth / 2 - margin)))
                    path.addLine(to: CGPoint(x: (lineWidth / 2), y: (componentSize.height / 2 - margin)))
                    path.addLine(to: CGPoint(x: inset, y: (componentSize.height / 2 - margin)))
                }
                .fill(color)
            }
            if type.paths.contains(.leftBottom) {
                // leftBottom
                Path { path in
                    path.move(to: CGPoint(x: inset, y: (componentSize.height / 2 + margin)))
                    path.addLine(to: CGPoint(x: (lineWidth / 2), y: (componentSize.height / 2 + margin)))
                    path.addLine(to: CGPoint(x: lineWidth, y: (componentSize.height / 2 + lineWidth / 2 + margin)))
                    path.addLine(to: CGPoint(x: lineWidth, y: (componentSize.height - lineWidth - margin)))
                    path.addLine(to: CGPoint(x: inset, y: (componentSize.height - inset - margin)))
                }
                .fill(color)
            }
            if type.paths.contains(.middle) {
                // middle
                Path { path in
                    path.move(to: CGPoint(x: (lineWidth / 2 + margin), y: (componentSize.height / 2)))
                    path.addLine(to: CGPoint(x: (lineWidth + margin), y: (componentSize.height / 2 - lineWidth / 2)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth - margin), y: (componentSize.height / 2 - lineWidth / 2)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth / 2 - margin), y: (componentSize.height / 2)))
                    path.addLine(to: CGPoint(x: (componentSize.width - lineWidth - margin), y: (componentSize.height / 2 + lineWidth / 2)))
                    path.addLine(to: CGPoint(x: (lineWidth + margin), y: (componentSize.height / 2 + lineWidth / 2)))
                }
                .fill(color)
            }
            if type.paths.contains(.center) {
                // center
                Path { path in
                    path.move(to: CGPoint(x: (componentSize.width / 2 - lineWidth / 2), y: (lineWidth + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 + lineWidth / 2), y: (lineWidth + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 + lineWidth / 2), y: (componentSize.height / 2 - lineWidth / 2)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 - lineWidth / 2), y: (componentSize.height / 2 - lineWidth / 2)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 - lineWidth / 2), y: (lineWidth + margin)))
                    
                }
                .fill(color)
                Path { path in
                    path.move(to: CGPoint(x: (componentSize.width / 2 - lineWidth / 2), y: (componentSize.height / 2 + lineWidth / 2 + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 + lineWidth / 2), y: (componentSize.height / 2 + lineWidth / 2 + margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 + lineWidth / 2), y: (componentSize.height - lineWidth - margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 - lineWidth / 2), y: (componentSize.height - lineWidth - margin)))
                    path.addLine(to: CGPoint(x: (componentSize.width / 2 - lineWidth / 2), y: (componentSize.height / 2 + lineWidth / 2 + margin)))
                    
                }
                .fill(color)
            }
            if type.paths.contains(.outsideLeftTop) {
                // outsideLeftTop
                Path { path in
                    path.move(to: CGPoint(x: 0, y: inset))
                    path.addLine(to: CGPoint(x: -(componentSize.width / 2), y: inset))
                    path.addLine(to: CGPoint(x: -(componentSize.width / 2 - lineWidth), y: (lineWidth + inset)))
                    path.addLine(to: CGPoint(x: 0, y: (lineWidth + inset)))
                }
                .fill(color)
            }
        }
        .frame(width: componentSize.width, height: componentSize.height)
    }
}
```

それぞれの描画結果はこんな感じ。描画結果は大体Previewに定義したつもり。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/a9641a34-bbea-f4b0-1342-1f687cb4953a.png">

```Swift:DigitView.swift#233-269
#Preview {
    VStack {
        HStack {
            DigitView(type: .zero, componentSize: defaultComponentSize)
            DigitView(type: .one, componentSize: defaultComponentSize)
            DigitView(type: .two, componentSize: defaultComponentSize)
            DigitView(type: .three, componentSize: defaultComponentSize)
            DigitView(type: .four, componentSize: defaultComponentSize)
        }
        HStack {
            DigitView(type: .five, componentSize: defaultComponentSize)
            DigitView(type: .six, componentSize: defaultComponentSize)
            DigitView(type: .seven, componentSize: defaultComponentSize)
            DigitView(type: .eight, componentSize: defaultComponentSize)
            DigitView(type: .nine, componentSize: defaultComponentSize)
        }
        HStack {
            DigitView(type: .hyphn, componentSize: defaultComponentSize)
            DigitView(type: .space, componentSize: defaultComponentSize)
        }
        HStack {
            DigitView(type: .a, componentSize: defaultComponentSize)
            DigitView(type: .e, componentSize: defaultComponentSize)
            DigitView(type: .f, componentSize: defaultComponentSize)
            DigitView(type: .h, componentSize: defaultComponentSize)
            DigitView(type: .m, componentSize: defaultComponentSize)
        }
        HStack {
            DigitView(type: .o, componentSize: defaultComponentSize)
            DigitView(type: .r, componentSize: defaultComponentSize)
            DigitView(type: .s, componentSize: defaultComponentSize)
            DigitView(type: .t, componentSize: defaultComponentSize)
            DigitView(type: .u, componentSize: defaultComponentSize)
            DigitView(type: .w, componentSize: defaultComponentSize)
        }
    }
}
```

## アルファベット・絵文字
7セグメントディスプレイで表現できないものに関しては `MatrixView` という 5 x 5 のタイルで表現するようにした。所謂ドット絵みたいな形。

表現するViewはこんな感じ。
```Swift:Coordinate.swift#16-23
struct Coordinate: Equatable {
    let x: Int
    let y: Int

    static func == (ldh: Coordinate, rdh: Coordinate) -> Bool {
        ldh.x == rdh.x && ldh.y == rdh.y
    }
}
```
```Swift:MatrixView.swift#10-43
struct MatrixView: View {
    let coordinates: [Coordinate]
    let maxMatrix: Coordinate?
    let componentSize: CGSize
    let color: Color
    var matrix: Coordinate {
        let maxX = maxMatrix?.x ?? coordinates.max { $0.x < $1.x }.map { $0.x } ?? 0
        let maxY = maxMatrix?.y ?? coordinates.max { $0.y < $1.y }.map { $0.y } ?? 0
        return .init(x: maxX + 1, y: maxY + 1)
    }
    let margin: CGFloat

    var body: some View {
        ZStack {
            let blockSize: (CGFloat, CGFloat) = (componentSize.width / CGFloat(matrix.x), componentSize.height / CGFloat(matrix.y))
            ForEach(0..<matrix.x, id: \.self) { x in
                ForEach(0..<matrix.y, id: \.self) { y in
                    if coordinates.contains(Coordinate(x: x, y: y)) {
                        Path { path in
                            path.move(to: CGPoint(x: (CGFloat(x) * blockSize.0 + margin / 2), y: (CGFloat(y) * blockSize.1 + margin / 2)))
                            path.addLine(to: CGPoint(x: (CGFloat(x + 1) * blockSize.0 - margin / 2), y: (CGFloat(y) * blockSize.1 + margin / 2)))
                            path.addLine(to: CGPoint(x: (CGFloat(x + 1) * blockSize.0 - margin / 2), y: (CGFloat(y + 1) * blockSize.1 - margin / 2)))
                            path.addLine(to: CGPoint(x: (CGFloat(x) * blockSize.0 + margin / 2), y: (CGFloat(y + 1) * blockSize.1 - margin / 2)))
                            path.addLine(to: CGPoint(x: (CGFloat(x) * blockSize.0 + margin / 2), y: (CGFloat(y) * blockSize.1 + margin / 2)))
                        }
                        .fill(color)
                    }
                    
                }
            }
        }
        .frame(width: componentSize.width, height: componentSize.height)
    }
}
```

例えば、↑に以下のようなCoodinateの定義を与えると
```Swift:WeekdayView.swift#19-85
    var coordinateGrid: [[Coordinate]] {
        switch self {
        case .sunday:
            return [
                // S
                [.init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 0, y: 1), .init(x: 1, y: 2), .init(x: 2, y: 2), .init(x: 3, y: 2), .init(x: 4, y: 3), .init(x: 3, y: 4), .init(x: 2, y: 4), .init(x: 1, y: 4), .init(x: 0, y: 4)],
                // U
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4), .init(x: 4, y: 3), .init(x: 4, y: 2), .init(x: 4, y: 1), .init(x: 4, y: 0)],
                // N
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 1), .init(x: 2, y: 2), .init(x: 3, y: 3), .init(x: 4, y: 4), .init(x: 4, y: 3), .init(x: 4, y: 2), .init(x: 4, y: 1), .init(x: 4, y: 0)],
            ]
        case .monday:
            return [
                // M
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 1), .init(x: 2, y: 2), .init(x: 3, y: 1), .init(x: 4, y: 0), .init(x: 4, y: 1), .init(x: 4, y: 2), .init(x: 4, y: 3), .init(x: 4, y: 4)],
                // O
                [.init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4), .init(x: 4, y: 3), .init(x: 4, y: 2), .init(x: 4, y: 1)],
                // N
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 1), .init(x: 2, y: 2), .init(x: 3, y: 3), .init(x: 4, y: 4), .init(x: 4, y: 3), .init(x: 4, y: 2), .init(x: 4, y: 1), .init(x: 4, y: 0)],
            ]
        case .tuesday:
            return [
                // T
                [.init(x: 0, y: 0), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 2, y: 1), .init(x: 2, y: 2), .init(x: 2, y: 3), .init(x: 2, y: 4)],
                // U
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4), .init(x: 4, y: 3), .init(x: 4, y: 2), .init(x: 4, y: 1), .init(x: 4, y: 0)],
                // E
                [.init(x: 0, y: 0), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 2), .init(x: 2, y: 2), .init(x: 3, y: 2), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4), .init(x: 4, y: 4)],
            ]
        case .wednesday:
            return [
                // W
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 1, y: 4), .init(x: 2, y: 1), .init(x: 2, y: 2), .init(x: 2, y: 3), .init(x: 3, y: 4), .init(x: 4, y: 0), .init(x: 4, y: 1), .init(x: 4, y: 2), .init(x: 4, y: 3)],
                // E
                [.init(x: 0, y: 0), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 2), .init(x: 2, y: 2), .init(x: 3, y: 2), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4), .init(x: 4, y: 4)],
                // D
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 1), .init(x: 4, y: 2), .init(x: 4, y: 3), .init(x: 3, y: 4), .init(x: 2, y: 4), .init(x: 1, y: 4)],
            ]
        case .thursday:
            return [
                // T
                [.init(x: 0, y: 0), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 2, y: 1), .init(x: 2, y: 2), .init(x: 2, y: 3), .init(x: 2, y: 4)],
                // H
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 2), .init(x: 2, y: 2), .init(x: 3, y: 2), .init(x: 4, y: 0), .init(x: 4, y: 1), .init(x: 4, y: 2), .init(x: 4, y: 3), .init(x: 4, y: 4)],
                // U
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4), .init(x: 4, y: 3), .init(x: 4, y: 2), .init(x: 4, y: 1), .init(x: 4, y: 0)],
            ]
        case .friday:
            return [
                // F
                [.init(x: 0, y: 0), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 2), .init(x: 2, y: 2), .init(x: 3, y: 2)],
                // R
                [.init(x: 0, y: 0), .init(x: 0, y: 1), .init(x: 0, y: 2), .init(x: 0, y: 3), .init(x: 0, y: 4), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 1), .init(x: 3, y: 2), .init(x: 2, y: 2), .init(x: 1, y: 2), .init(x: 4, y: 3), .init(x: 4, y: 4)],
                // I
                [.init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 2, y: 1), .init(x: 2, y: 2), .init(x: 2, y: 3), .init(x: 1, y: 4), .init(x: 2, y: 4), .init(x: 3, y: 4)],
            ]
        case .saturday:
            return [
                // S
                [.init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 0, y: 1), .init(x: 1, y: 2), .init(x: 2, y: 2), .init(x: 3, y: 2), .init(x: 4, y: 3), .init(x: 3, y: 4), .init(x: 2, y: 4), .init(x: 1, y: 4), .init(x: 0, y: 4)],
                // A
                [.init(x: 2, y: 0), .init(x: 1, y: 1), .init(x: 3, y: 1), .init(x: 1, y: 2), .init(x: 3, y: 2), .init(x: 0, y: 3), .init(x: 4, y: 3), .init(x: 2, y: 2), .init(x: 0, y: 4), .init(x: 4, y: 4)],
                // T
                [.init(x: 0, y: 0), .init(x: 1, y: 0), .init(x: 2, y: 0), .init(x: 3, y: 0), .init(x: 4, y: 0), .init(x: 2, y: 1), .init(x: 2, y: 2), .init(x: 2, y: 3), .init(x: 2, y: 4)],
            ]
        }
    }
```
このように描画される。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/89bee80f-8b9f-3056-dc4d-1b2fbe78c488.png">

絵文字だとこのようになる。
```Swift
    VStack {
        MatrixView(
            coordinates: Mode.dataBank.iconCoordinates,
            maxMatrix: .init(x: 5, y: 5),
            componentSize: CGSize(width: 24, height: 24), color: Color.black, margin: 1
        )
        MatrixView(
            coordinates: Mode.calculator.iconCoordinates,
            maxMatrix: .init(x: 5, y: 5),
            componentSize: CGSize(width: 24, height: 24), color: Color.black, margin: 1
        )
        MatrixView(
            coordinates: Mode.alarm.iconCoordinates,
            maxMatrix: .init(x: 5, y: 5),
            componentSize: CGSize(width: 24, height: 24), color: Color.black, margin: 1
        )
        MatrixView(
            coordinates: Mode.stopWatch.iconCoordinates,
            maxMatrix: .init(x: 5, y: 5),
            componentSize: CGSize(width: 24, height: 24), color: Color.black, margin: 1
        )
        MatrixView(
            coordinates: Mode.dualTime.iconCoordinates,
            maxMatrix: .init(x: 5, y: 5),
            componentSize: CGSize(width: 24, height: 24), color: Color.black, margin: 1
        )
    }
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/fe54b621-fcd7-3f17-72d8-52ae1b5451d7.png">

## 使い方
リポジトリの DigitalClockKitDemoApp 以下にある。
```Swift:ContentView.swift#10-115
struct ContentView: View {
    let timer = Timer.publish(every: 0.01, on: .current, in: .common).autoconnect()
    @State var currentDate:Date = Date()

    var body: some View {
        NavigationView {
            List {
                let year = Calendar.current.component(.year, from: currentDate)
                let month = Calendar.current.component(.month, from: currentDate)
                let day = Calendar.current.component(.day, from: currentDate)
                let weekday = Calendar.current.component(.weekday, from: currentDate)
                let hour = Calendar.current.component(.hour, from: currentDate)
                let minute = Calendar.current.component(.minute, from: currentDate)
                let second = Calendar.current.component(.second, from: currentDate)
                let miliSecond = Calendar.current.component(.nanosecond, from: currentDate) / 10000000

                // TODO: Components のプレゼン方法を変える
                Section {
                    HStack {
                        Text("Year")
                        Spacer()
                        AgeView(componentSize: mediumComponentSize, year: year)
                    }
                    HStack {
                        Text("Day")
                        Spacer()
                        DayView(componentSize: mediumComponentSize, month: month, day: day)
                    }
                    HStack {
                        Text("Time")
                        Spacer()
                        TimeView(componentSize: mediumComponentSize, timeComponentSize: nil, hasSecondDivider: true, hasSecond: true, hasMiliSecond: true, hour: hour, minute: minute, second: second, miliSecond: miliSecond, is24Hour: false)
                    }
                    HStack {
                        Text("Weekday")
                        Spacer()
                        HStack {
                            WeekdayView(edition: .matrix, componentSize: mediumComponentSize, weekday: weekday)
                                .padding(.trailing, 10)
                            WeekdayView(edition: .digit, componentSize: mediumComponentSize, weekday: weekday)
                        }
                    }
                    HStack {
                        Text("Telephone")
                        Spacer()
                        DigitContainerView(value: "012-3456-7890", componentSize: mediumComponentSize, spacing: 0.4)
                    }
                    HStack {
                        Text("Number")
                        Spacer()
                        DigitContainerView(value: "123.45", componentSize: defaultComponentSize, spacing: 1)
                    }
                    HStack {
                        Text("Icons")
                        Spacer()
                        HStack {
                            MatrixView(coordinates: Mode.dataBank.iconCoordinates, maxMatrix: nil, componentSize: CGSize(width:18, height: 18), color: Color.black, margin: 0)
                            MatrixView(coordinates: Mode.calculator.iconCoordinates, maxMatrix: nil, componentSize: CGSize(width:18, height: 18), color: Color.black, margin: 0)
                            MatrixView(coordinates: Mode.alarm.iconCoordinates, maxMatrix: nil, componentSize: CGSize(width:18, height: 18), color: Color.black, margin: 0)
                            MatrixView(coordinates: Mode.stopWatch.iconCoordinates, maxMatrix: nil, componentSize: CGSize(width:18, height: 18), color: Color.black, margin: 0)
                            MatrixView(coordinates: Mode.dualTime.iconCoordinates, maxMatrix: nil, componentSize: CGSize(width:18, height: 18), color: Color.black, margin: 0)
                        }
                    }
                } header: {
                    Text("COMPONENTS")
                }
                Section {
                    HStack {
                        Spacer()
                        ScrollView(.horizontal) {
                            LazyHStack(spacing: 0) {
                                ForEach(Mode.allCases, id: \.self) { mode in
                                    Watch1View(date: $currentDate, mode: mode, is24Hour: false)
                                        .frame(width: 220)
                                }
                            }
                            .scrollTargetLayout()
                        }
                        .scrollTargetBehavior(.paging)
                        .frame(width: 220)
                        Spacer()
                    }
                    HStack {
                        Spacer()
                        ScrollView(.horizontal) {
                            LazyHStack(spacing: 0) {
                                Watch2View(date: $currentDate, is24Hour: true)
                                    .frame(width: 250)
                            }
                            .scrollTargetLayout()
                        }
                        .scrollTargetBehavior(.paging)
                        .frame(width: 250)
                        Spacer()
                    }
                } header: {
                    Text("SAMPLES")
                }
            }
            .listStyle(InsetGroupedListStyle())
            .navigationTitle("DigitalClockKit")
        }.onReceive(timer){ value in
            currentDate = value
        }
    }
}
```

|||
|-|-|
|<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/be5281f8-5309-aa6d-0dcb-ce4a7848f04d.png">|<img width="180" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156085/889bf527-b056-f8c8-2793-879a767a3def.png">|

↑は画像だが本当は時計部分やストップウォッチが動いている


# 📝 あとがき
SwiftUIの勉強というよりただPath使った描画集になってしまった感。。
パッケージ公開についても中途半端なので時間作ってちゃんとやりたい