SwiftUIの勉強がてらデジタルウォッチのUIを実装してみることにした。
チープな白黒液晶が懐かしくなり80〜90年代を席巻したカ⚪︎オ データバ⚪︎クなど所謂チープカシ⚪︎と呼ばれる腕時計のUIを参考に幾つかそのコンポーネントを再現することにした。
最終的にはそれをwatchOS上で動かせるようにしたいと思ったが、物理ボタンでの操作を想定したUIのはずなのでむずかしそう。
一種のアート作品として生暖かく見守っていただきたい。
📋 概要
どのようなアウトプットにすべきか考えたところ、懐かしいデジタルウォッチのUIコンポーネントを提供するレトロフューチャーなOSSにすることにした。需要はないだろうけど
現状のリポジトリは以下(まだPackage化は半端な状態)
https://github.com/JikeLab/DigitalClockKit
幾つか example として実装してみた画面はこの通り。
🔎 コンポーネントの説明
数字と一部のアルファベット
7セグメントディスプレイというらしい。
主に日付や時刻を表現する部分を表現するためのコンポーネントでデジタル時計によっては曜日を表す際に一部のアルファベットもこれを使って表現されている。
まずは描画パスを指定するためのOptionSetを定義する。
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
は数字一つの大きさ。
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に定義したつもり。
#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はこんな感じ。
struct Coordinate: Equatable {
let x: Int
let y: Int
static func == (ldh: Coordinate, rdh: Coordinate) -> Bool {
ldh.x == rdh.x && ldh.y == rdh.y
}
}
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の定義を与えると
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)],
]
}
}
絵文字だとこのようになる。
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
)
}
使い方
リポジトリの DigitalClockKitDemoApp 以下にある。
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
}
}
}
↑は画像だが本当は時計部分やストップウォッチが動いている
📝 あとがき
SwiftUIの勉強というよりただPath使った描画集になってしまった感。。
パッケージ公開についても中途半端なので時間作ってちゃんとやりたい