diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f4cbbce
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Chen-Hai Teng
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Package.swift b/Package.swift
index 139dcd0..79d3d26 100644
--- a/Package.swift
+++ b/Package.swift
@@ -5,8 +5,12 @@ import PackageDescription
let package = Package(
name: "Rings",
+ platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
+ .library(
+ name: "CommonExts",
+ targets: ["CommonExts"]),
.library(
name: "Rings",
targets: ["Rings"]),
@@ -14,15 +18,24 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
+ .package(name: "CoreGraphicsExtension", url: "https://github.com/chenhaiteng/CoreGraphicsExtension.git", from: "0.2.0"),
+ .package(name: "ArchimedeanSpiral", url: "https://github.com/chenhaiteng/ArchimedeanSpiral.git", from: "1.0.12")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
+ .target(name: "CommonExts",
+ dependencies: []),
.target(
name: "Rings",
- dependencies: []),
+ dependencies: ["CoreGraphicsExtension", "CommonExts", "ArchimedeanSpiral"],
+ exclude: ["RingText.md",
+ "ClockIndex.md",
+ "ArchimedeanSpiralText.md",
+ "HandAiguille.md"]),
.testTarget(
name: "RingsTests",
- dependencies: ["Rings"]),
+ dependencies: ["Rings",
+ "CoreGraphicsExtension", "CommonExts"]),
]
)
diff --git a/README.md b/README.md
index e72e3b3..74a47e3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,82 @@
-# Rings
+# Rings  
-A description of this package.
+**Rings** is a collection of controls which have similar shapes of ring, circle...
+
+It includes following controls, click to see what it looks like:
+
+* **[RingText](#ringtext)**
+* **[ClockIndex](#clockindex)**
+* **[HandAiguille](#handaiguille)**
+* **[ArchimedeanSpiralText](#archimedeanspiraltext)**
+
+and following functions are in progress:
+* SphericText
+* Knob
+
+---
+## Installation:
+### Install with Swift Package Manager
+#### - Add to Xcode:
+
+1. File > Swift Packages > Add Package Dependency...
+2. Choose Project you want to add Rings
+3. Paste repository https://github.com/chenhaiteng/Rings.git
+4. Rules > Version: Up to Next Major 0.1.0
+It's can also apply Rules > Branch : main to access latest code.
+If you want try some experimental features, you can also apply Rules > Branch : develop
+
+**Note:** It might need to link Rings to your target maunally.
+
+1. Open *Project Editor* by tap on root of project navigator
+2. Choose the target you want to use Rings.
+3. Choose **Build Phases**, and expand **Link Binary With Libraries**
+4. Tap on **+** button, and choose Rings to add it.
+
+#### - Add to SPM package:
+```swift
+dependencies: [
+ .package(name: "Rings", url: "https://github.com/chenhaiteng/Rings.git", from: "0.1.0")
+ // To specify branch, use following statement to instead of.
+ // .package(name: "Rings", url: "https://github.com/chenhaiteng/Rings.git", .branch("branch_name"))
+],
+targets: [
+ .target(
+ name: "MyPackage",
+ dependencies: ["Rings"]),
+]
+```
+---
+
+## RingText
+
+### What it looks like
+
+
+### 
+
+## ClockIndex
+
+### What it looks like
+
+
+
+
+### 
+
+## HandAiguille
+
+### What it looks like:
+
+
+### 
+
+## ArchimedeanSpiralText
+
+### What it looks like:
+
+
+### 
+
+---
+# License
+Rings is released under the [MIT License](LICENSE).
diff --git a/Sources/CommonExts/CollectionExts.swift b/Sources/CommonExts/CollectionExts.swift
new file mode 100644
index 0000000..e6a5020
--- /dev/null
+++ b/Sources/CommonExts/CollectionExts.swift
@@ -0,0 +1,15 @@
+//
+// CollectionExts.swift
+//
+// Refer to https://stackoverflow.com/a/37225027/505763
+// Created by Chen Hai Teng on 4/22/21.
+//
+
+import Foundation
+
+public extension Collection where Indices.Iterator.Element == Index {
+ subscript (safe index: Index) -> Iterator.Element? {
+ return indices.contains(index) ? self[index] : nil
+ }
+}
+
diff --git a/Sources/CommonExts/ViewExts.swift b/Sources/CommonExts/ViewExts.swift
new file mode 100644
index 0000000..67b76e1
--- /dev/null
+++ b/Sources/CommonExts/ViewExts.swift
@@ -0,0 +1,19 @@
+//
+// ViewExts.swift
+//
+// Refer to https://stackoverflow.com/a/57685253/505763
+// Created by Chen Hai Teng on 4/22/21.
+//
+
+import SwiftUI
+
+public extension View {
+ @ViewBuilder
+ func `if`(_ conditional: Bool, content: (Self)->Content) -> some View {
+ if(conditional) {
+ content(self)
+ } else {
+ self
+ }
+ }
+}
diff --git a/Sources/Rings/ArchimedeanSpiralPath.swift b/Sources/Rings/ArchimedeanSpiralPath.swift
new file mode 100644
index 0000000..ecf8086
--- /dev/null
+++ b/Sources/Rings/ArchimedeanSpiralPath.swift
@@ -0,0 +1,120 @@
+//
+// ArchimedeanSpiralPath.swift
+// Rings
+//
+// Created by Chen Hai Teng on 3/5/21.
+//
+
+import SwiftUI
+import ArchimedeanSpiral
+import CoreGraphicsExtension
+
+public struct ArchimedeanSpiralPath: View {
+ var radiusSpacing: Double
+ var innerRadius: Double
+ var gap: Double
+ var ptCount: Int
+ var spiralDesc :ArchimedeanSpiral
+ var startAngle: CGAngle = CGAngle.zero
+
+ private lazy var points: [CGPoint] = spiralDesc.equidistantPoints(start: startAngle, num: ptCount).map { polar in
+ polar.cgpoint
+ }
+
+ init(_ innerRadius: Double = 12.0, spacing: Double = 10.0, gap: Double = 5.0, count: Int = 100) {
+ self.radiusSpacing = spacing
+ self.innerRadius = innerRadius
+ self.gap = gap
+ self.ptCount = count
+ spiralDesc = ArchimedeanSpiral(innerRadius: self.innerRadius, radiusSpacing: self.radiusSpacing, spacing: self.gap)
+
+ }
+
+ func getPoints()-> [CGPoint] {
+ var mutateSelf = self
+ return mutateSelf.points
+ }
+
+ public var body: some View {
+ GeometryReader { geo in
+ Path { path in
+ let midX = geo.size.width/2
+ let midY = geo.size.height/2
+ path.move(to: CGPoint(
+ x: midX,
+ y: midY
+ ))
+ getPoints().forEach { pt in
+ let next = CGPoint(x: pt.x + midX, y: pt.y + midY)
+ path.addLine(to: next)
+ }
+ }.stroke(Color.red)
+ }
+ }
+}
+
+extension ArchimedeanSpiralPath : Adjustable {
+ func start(_ angle: CGAngle) -> Self {
+ setProperty { tmp in
+ tmp.startAngle = angle
+ }
+ }
+}
+
+struct ArchimedeanSpiralPathDemo : View {
+ @State var radiusSpacing: Double = 10.0
+ @State var innerR: Double = 5.0
+ @State var spacing: Double = 25.0
+ @State var count: Double = 100.0
+ @State var startAt: Double = 0.0
+ @State var showInner: Bool = false
+ @State var showRadius: Bool = false
+ var body: some View {
+ VStack {
+ ZStack {
+ GeometryReader { geo in
+ let midX = geo.size.width/2
+ let midY = geo.size.height/2
+ ArchimedeanSpiralPath(innerR, spacing: radiusSpacing, gap: spacing, count: Int(count)).start(CGAngle.degrees(startAt))
+ Group {
+ if (showInner) {
+ Path { p in
+ p.move(to: CGPoint(x: midX, y: midY))
+ p.addLine(to: CGPoint(x: midX + CGFloat(innerR), y: midY))
+ }.stroke(style: StrokeStyle(lineWidth: 2.0, lineCap: .butt, lineJoin: .miter, miterLimit: 0, dash: [3,1], dashPhase: 0))
+ }
+ if(showRadius) {
+ Path { p in
+ p.move(to: CGPoint(x: midX + CGFloat(innerR), y: midY))
+ p.addLine(to: CGPoint(x: midX + CGFloat(innerR) + CGFloat(radiusSpacing), y: midY))
+ }.stroke(Color.blue, style: StrokeStyle(lineWidth: 2.0, lineCap: .butt, lineJoin: .miter, miterLimit: 0, dash: [3,1], dashPhase: 0))
+ }
+ }
+ }
+ }
+ Slider(value: $startAt, in: 0.0...360.0) {
+ Text("start at (\(startAt))")
+ }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
+ Slider(value: $radiusSpacing, in: 10.0...50.0) {
+ Toggle("", isOn: $showRadius)
+ Text("radius spacing (\(radiusSpacing))")
+ }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
+ Slider(value: $innerR, in: 10.0...50.0) {
+ Toggle("", isOn: $showInner)
+ Text("inner radius(\(innerR))")
+ }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
+ Slider(value: $spacing, in: 10.0...50.0) {
+ Text("points spacing(\(spacing))")
+ }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
+ Slider(value: $count, in: 100.0...200.0, step: 10.0) {
+ Text("count(\(Int(count)))")
+ }.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
+ }
+ }
+}
+
+struct SwiftUIView_Previews: PreviewProvider {
+ static var previews: some View {
+ ArchimedeanSpiralPathDemo()
+ }
+}
diff --git a/Sources/Rings/ArchimedeanSpiralText.md b/Sources/Rings/ArchimedeanSpiralText.md
new file mode 100644
index 0000000..620c849
--- /dev/null
+++ b/Sources/Rings/ArchimedeanSpiralText.md
@@ -0,0 +1,24 @@
+# ArchimedeanSpiralText
+
+## Preview:
+
+
+
+### Usage:
+```swift
+// Create text along archimedean spiral
+ArchimedeanSpiralText("My Archimedean Spiral")
+
+// Setup Archimedean Spiral parameters
+ArchimedeanSpiralText("My Archimedean Spiral")
+ .gap(10.0) // To setup the distance between two calculated points.
+ .innerRadius(15.0) // To specify the start radius of an archimedean spiral
+ .spacing(radiusSpacing) // To adjust the constant separation distance between intersection points measured from the origin.
+
+// Update the text appearance
+ArchimedeanSpiralText()
+ .text("My Archimedean Spiral") // Modifing text content
+ .textDirection(direction) // Specifing the direction of text.
+ .textColor(color) // Change text color
+ .font(font) // Change font and text size
+```
diff --git a/Sources/Rings/ArchimedeanSpiralText.swift b/Sources/Rings/ArchimedeanSpiralText.swift
new file mode 100644
index 0000000..d7d80fd
--- /dev/null
+++ b/Sources/Rings/ArchimedeanSpiralText.swift
@@ -0,0 +1,234 @@
+//
+// ArchimedeanSpiralText.swift
+// Rings
+//
+// Created by Chen Hai Teng on 3/5/21.
+//
+
+import SwiftUI
+import ArchimedeanSpiral
+import CoreGraphicsExtension
+
+public enum TextDirection {
+ case Top, Bottom, Left, Right
+ var cgangle: CGAngle {
+ switch self {
+ case .Top:
+ return CGAngle.degrees(270.0)
+ case .Bottom:
+ return CGAngle.degrees(90.0)
+ case .Left:
+ return CGAngle.degrees(0.0)
+ case .Right:
+ return CGAngle.degrees(180.0)
+ }
+ }
+}
+
+public struct ArchimedeanSpiralText: View {
+ private var radiusSpacing: Double
+ private var innerRadius: Double
+ private var gap: Double
+ private var textDirection: TextDirection = .Top
+ private var font = Font.system(size: 20.0)
+ private var textColor: Color = Color.red
+
+ private var text: String {
+ didSet {
+ chars = Array(text.enumerated())
+ }
+ }
+
+ private var chars: [(offset: Int, element:Character)] {
+ didSet {
+
+ }
+ }
+
+ private var textPoints: [CGPolarPoint] = []
+
+ public init(_ innerRadius: Double = 12.0, spacing: Double = 10.0, gap: Double = 5.0, text: String = "") {
+ self.radiusSpacing = spacing
+ self.innerRadius = innerRadius
+ self.gap = gap
+ self.text = text
+ self.chars = Array(text.enumerated())
+ updateTextPoints()
+ }
+
+ private mutating func updateTextPoints() {
+ let spiral = ArchimedeanSpiral(innerRadius: self.innerRadius, radiusSpacing: self.radiusSpacing, spacing: self.gap)
+ textPoints = spiral.equidistantPoints(start: CGAngle.zero, num: self.chars.count)
+ }
+
+ public var body: some View {
+ GeometryReader { geo in
+ ZStack {
+ ForEach(chars, id: \.self.offset) { (offset, element) in
+ let pt = self.textPoints[offset].cgpoint
+ let textPt = CGPoint(x: pt.x, y: pt.y)
+ let rotation = (self.textPoints[offset].cgangle + textDirection.cgangle).toAngle()
+ Text(String(element))
+ .rotationEffect(rotation)
+ .offset(x: textPt.x, y: textPt.y)
+ .foregroundColor(textColor)
+ .font(font)
+
+ }
+ }.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
+ }
+ }
+}
+
+extension ArchimedeanSpiralText: Adjustable {
+ public func spacing(_ space: T) -> Self {
+ setProperty { tmp in
+ tmp.radiusSpacing = Double(space)
+ tmp.updateTextPoints()
+ }
+ }
+
+ public func innerRadius(_ radius: T) -> Self {
+ setProperty { tmp in
+ tmp.innerRadius = Double(radius)
+ tmp.updateTextPoints()
+ }
+ }
+
+ public func gap(_ gap: T) -> Self {
+ setProperty { tmp in
+ tmp.gap = Double(gap)
+ tmp.updateTextPoints()
+ }
+ }
+
+ public func text(_ text: String) -> Self {
+ setProperty { tmp in
+ tmp.text = text
+ tmp.chars = Array(text.enumerated())
+ tmp.updateTextPoints()
+ }
+ }
+
+ public func textDirection(_ direction: TextDirection) -> Self {
+ setProperty { tmp in
+ tmp.textDirection = direction
+ }
+ }
+
+ public func font(_ font: Font) -> Self {
+ setProperty { tmp in
+ tmp.font = font
+ }
+ }
+
+ public func textColor(_ color: Color) -> Self {
+ setProperty { tmp in
+ tmp.textColor = color
+ }
+ }
+}
+
+struct SegmentedPicker: ViewModifier {
+ func body(content: Content) -> some View {
+ #if os(watchOS)
+ content
+ #else
+ content.pickerStyle(SegmentedPickerStyle()).padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 7))
+ #endif
+ }
+}
+
+struct ColoredPicker: ViewModifier {
+ @Binding var selection: Color
+ func body(content: Content) -> some View {
+ #if os(macOS) || os(iOS)
+ if #available(macOS 11.0, *) {
+ ColorPicker("", selection: _selection)
+ } else {
+ content.modifier(SegmentedPicker())
+ }
+ #else
+ content.modifier(SegmentedPicker())
+ #endif
+ }
+}
+
+extension Picker {
+ func segmented() -> some View {
+ modifier(SegmentedPicker())
+ }
+
+ func colorPicker(_ selection: Binding) -> some View {
+ modifier(ColoredPicker(selection: selection))
+ }
+}
+
+struct ArchimedeanSpiralTextDemo : View {
+ private let demoText = "1234567890abcdefgABCDEFG♩♪♫♬"
+ @State var radiusSpacing: Double = 20.0
+ @State var innerR: Double = 25.0
+ @State var gap: Double = 25.0
+ @State var textLength: Double = 15.0
+ @State var direction: TextDirection = TextDirection.Top
+ @State var color: Color = .white
+ @State var font: Font = .system(.body)
+
+ public var body: some View {
+ VStack {
+ let enabled = String(demoText.prefix(Int(textLength)))
+ let disabled = String(demoText.suffix(demoText.count - Int(textLength)))
+ ArchimedeanSpiralText()
+ .gap(gap)
+ .innerRadius(innerR)
+ .spacing(radiusSpacing)
+ .text(enabled)
+ .textDirection(direction)
+ .textColor(color)
+ .font(font)
+ Slider(value: $textLength, in: 1.0...28.0, step: 1.0) {
+ Text(enabled).foregroundColor(.white) + Text(disabled).foregroundColor(.gray)
+ }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
+ HStack {
+ VStack {
+ Text("direction forward to center")
+ Picker("", selection: $direction) {
+ Text("top").tag(TextDirection.Top)
+ Text("bottom").tag(TextDirection.Bottom)
+ Text("right").tag(TextDirection.Right)
+ Text("left").tag(TextDirection.Left)
+ }.segmented()
+ Text("text color")
+ Picker("", selection: $color) {
+ Text("White").tag(Color.white)
+ Text("Red").tag(Color.red)
+ Text("Blue").tag(Color.blue)
+ Text("Green").tag(Color.green)
+ }.colorPicker($color)
+ Picker("Font", selection: $font) {
+ Text(".body").tag(Font.system(.body))
+ Text(".caption").tag(Font.system(.caption))
+ Text("Zapfino(10)").tag(Font.custom("Zapfino", size: 10.0))
+ }.segmented()
+ }
+ VStack {
+ Slider(value: $innerR, in: 0.0...30.0, step: 5.0) {
+ Text("Inner Radius: \(innerR)")
+ }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
+ Slider(value: $radiusSpacing, in: 10.0...80.0, step: 5.0) {
+ Text("Radius Spacing: \(radiusSpacing)")
+ }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
+ Slider(value: $gap, in: 10.0...40.0, step: 3.0) {
+ Text("Gap:\(gap)")
+ }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
+ }
+ }
+ }
+ }
+}
+
+struct ArchimedeanSpiralText_Previews: PreviewProvider {
+ public static var previews: some View {
+ ArchimedeanSpiralTextDemo()
+ }
+}
diff --git a/Sources/Rings/ClockIndex.md b/Sources/Rings/ClockIndex.md
new file mode 100644
index 0000000..648e9f0
--- /dev/null
+++ b/Sources/Rings/ClockIndex.md
@@ -0,0 +1,22 @@
+## ClockIndex
+
+
+
+
+
+### Usage:
+
+```Swift
+ // Default clock index with radius 50.0
+ ClockIndex().radius(50.0)
+
+ // Modify hour index style with radius.
+ ClockIndex().hourIndexStyle(StrokeStyle(lineWidth: 5.0).hourStyle(with: indexRadius))
+
+ // Custom hour marker text
+ ClockIndex(textMarkers: ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"])
+
+ // Show/Hide hour index and minutes track
+ ClockIndex().showIndex(shouldShowIndex)
+```
+
diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift
new file mode 100644
index 0000000..0b4be9f
--- /dev/null
+++ b/Sources/Rings/ClockIndex.swift
@@ -0,0 +1,219 @@
+//
+// ClockIndex.swift
+//
+//
+// Created by Chen Hai Teng on 4/22/21.
+//
+
+import SwiftUI
+import CoreGraphicsExtension
+import CommonExts
+
+public enum ClockIndexError: Error {
+ case outOfBounds(String)
+}
+
+public let defaultTextMarker = ["1","2","3","4","5","6","7","8","9","10","11","12"]
+
+public let defaultMarkers: [AnyView] = defaultTextMarker.map { num -> AnyView in
+ AnyView(Text(num))
+}
+
+public let defaultRadius: CGFloat = 80.0
+
+public let classicHourStyle: StrokeStyle = StrokeStyle(lineWidth: 5.0, lineCap: CGLineCap.butt, lineJoin: CGLineJoin.miter, miterLimit: 0.0, dash: [0, CGFloat.pi*100/6], dashPhase: 0.0)
+
+public let classicMinStyle: StrokeStyle = StrokeStyle(lineWidth: 2.0, lineCap: CGLineCap.butt, lineJoin: CGLineJoin.miter, miterLimit: 0.0, dash: [0, CGFloat.pi*100/36], dashPhase: 0.0)
+
+public extension StrokeStyle {
+ func hourStyle(with radius: T) -> Self {
+ StrokeStyle(lineWidth: self.lineWidth, lineCap: self.lineCap, lineJoin: self.lineJoin, miterLimit: self.miterLimit, dash: [0, CGFloat.pi*CGFloat(radius)/6], dashPhase: self.dashPhase)
+ }
+
+ func minuteStyle(with radius: T) -> Self {
+ StrokeStyle(lineWidth: self.lineWidth, lineCap: self.lineCap, lineJoin: self.lineJoin, miterLimit: self.miterLimit, dash: [0, CGFloat.pi*CGFloat(radius)/36], dashPhase: self.dashPhase)
+ }
+}
+
+public struct ClockIndex: View {
+
+ private var hourMarkers: [AnyView] = defaultMarkers
+ private var textColor: Color = .white
+ private var radius: CGFloat = defaultRadius
+ private var showBlueprint: Bool = false
+
+ private var showIndex: Bool = true
+ private var indexColor: Color = .white
+ // hours index
+ private var hourIndexStyle: StrokeStyle = StrokeStyle()
+ private var hourIndexRadius: CGFloat = defaultRadius + 15.0
+ private var hourIndexColor: Color = .white
+ // minutes index
+ private var minIndexStyle: StrokeStyle = StrokeStyle()
+ private var minIndexRadius: CGFloat = defaultRadius + 10.0
+ private var minIndexColor: Color = .white
+
+ public init(textMarkers: [String] = defaultTextMarker, color: Color = .white) throws {
+ guard textMarkers.count == 12 else {
+ throw ClockIndexError.outOfBounds("The number of markers whould be 12.")
+ }
+ textColor = color
+ hourMarkers = textMarkers.map({ text -> AnyView in
+ AnyView(Text(text).foregroundColor(textColor))
+ })
+ }
+
+ public init(_ markers: [AnyView]) throws {
+ guard markers.count == 12 else {
+ throw ClockIndexError.outOfBounds("The number of markers whould be 12.")
+ }
+ hourMarkers = markers
+ }
+
+ public var body: some View {
+ GeometryReader { geo in
+ ZStack {
+ if(showIndex) {
+ Circle().stroke(style: hourIndexStyle).frame(width: 2*hourIndexRadius, height: 2*hourIndexRadius, alignment: .center).foregroundColor(hourIndexColor)
+ Circle().stroke(style: minIndexStyle).frame(width: 2*minIndexRadius, height: 2*minIndexRadius, alignment: .center).foregroundColor(minIndexColor)
+ }
+ ForEach(0..<12) { index in
+ let polarPt = CGPolarPoint(radius: radius, angle: CGAngle.pi/6*CGFloat(index) - CGAngle.pi/3)
+ Sizing {
+ hourMarkers[index].if(showBlueprint) { content in
+ content.border(Color.blue, width: 1)
+ }
+ }.offset(x: polarPt.cgpoint.x, y: polarPt.cgpoint.y)
+ }
+ if(showBlueprint) {
+ Path { path in
+ path.addEllipse(in: CGRect(origin: CGPoint(x: geo.size.width/2 - radius, y: geo.size.height/2 - radius), size: CGSize(width: 2*radius, height: 2*radius)), transform: CGAffineTransform.identity)
+ }.stroke(Color.blue, lineWidth: 1.0)
+ }
+ }.frame(width: geo.size.width, height: geo.size.height, alignment: .center).if(showBlueprint){ content in
+ content.border(Color.blue, width: 1)
+ }
+ }
+ }
+}
+
+extension ClockIndex {
+ func setProperty(_ setBlock: (_ clockIndex: inout Self) -> Void) -> Self {
+ let result = _setProperty(content: self) { (tmp :inout Self) in
+ setBlock(&tmp)
+ return tmp
+ }
+ return result
+ }
+
+ public func radius(_ r: T) -> Self {
+ setProperty { tmp in
+ tmp.radius = CGFloat(r)
+ }
+ }
+ public func showBlueprint(_ show: Bool) -> Self {
+ setProperty { tmp in
+ tmp.showBlueprint = show
+ }
+ }
+
+ public func hourIndexStyle(_ style: StrokeStyle, color: Color? = nil) -> Self {
+ setProperty { tmp in
+ tmp.hourIndexStyle = style
+ tmp.hourIndexColor = color ?? tmp.hourIndexColor
+ }
+ }
+
+ public func hourIndexRadius(_ r: T) -> Self {
+ setProperty { tmp in
+ tmp.hourIndexRadius = CGFloat(r)
+ }
+ }
+
+ public func minIndexStyle(_ style: StrokeStyle, color: Color? = nil) -> Self {
+ setProperty { tmp in
+ tmp.minIndexStyle = style
+ tmp.minIndexColor = color ?? tmp.minIndexColor
+ }
+ }
+
+ public func minIndexRadius(_ r: T) -> Self {
+ setProperty { tmp in
+ tmp.minIndexRadius = CGFloat(r)
+ }
+ }
+
+ public func showIndex(_ show: Bool = true) -> Self {
+ setProperty { tmp in
+ tmp.showIndex = show
+ }
+ }
+}
+
+//Previews
+struct ClockPreviewClassic : View {
+ @State var showBlueprint: Bool = false
+ @State var showIndex: Bool = true
+ @State var indexRadius: CGFloat = 60
+ @State var indexRadius2: CGFloat = 100
+ var body: some View {
+ VStack {
+ Spacer(minLength: 10.0)
+ Text("Classic Clocks")
+ HStack {
+ VStack {
+ try? ClockIndex().radius(50.0).showBlueprint(showBlueprint).hourIndexStyle(StrokeStyle(lineWidth: 5.0).hourStyle(with: indexRadius)).hourIndexRadius(indexRadius)
+ .minIndexStyle(StrokeStyle().minuteStyle(with: indexRadius)).minIndexRadius(indexRadius)
+ .showIndex(showIndex)
+ HStack {
+ Slider(value: $indexRadius, in: 30...100, step: 5.0)
+ Text("\(indexRadius)")
+ }
+ }
+ VStack {
+ try? ClockIndex(textMarkers: ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]).showBlueprint(showBlueprint).hourIndexStyle(StrokeStyle(lineWidth: 5.0).hourStyle(with: indexRadius2+5), color: .red).hourIndexRadius(indexRadius2+5)
+ .minIndexStyle(StrokeStyle().minuteStyle(with: indexRadius2-5), color: .blue).minIndexRadius(indexRadius2-5)
+ .showIndex(showIndex)
+ HStack {
+ Slider(value: $indexRadius2, in: 60...150, step: 5.0)
+ Text("\(indexRadius2)")
+ }
+ }
+ }
+ Toggle("Show Index", isOn: $showIndex)
+ Spacer(minLength: 5.0)
+ Divider()
+ Toggle("Blue Print", isOn: $showBlueprint)
+ Spacer(minLength: 10.0)
+ }
+ }
+}
+
+struct ClockPreviewEarthlyBranches : View {
+ @State var showBlueprint: Bool = false
+ var body: some View {
+ VStack {
+ Spacer(minLength: 10.0)
+ Text("Earchly Branches Clocks")
+ HStack {
+ try? ClockIndex(textMarkers: [".", "丑", ".", "寅", ".", "卯", ".", "辰", ".", "巳", ".", "子"], color: .red).showBlueprint(showBlueprint)
+
+ try? ClockIndex(textMarkers: [".", "未", ".", "申", ".", "酉", ".", "戌", ".", "亥", ".", "午"]).showBlueprint(showBlueprint)
+ }
+ Toggle("Blue Print", isOn: $showBlueprint)
+ Spacer(minLength: 10.0)
+ }
+ }
+}
+
+struct ClockIndex_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ ClockPreviewClassic()
+ }
+ Group {
+ ClockPreviewEarthlyBranches()
+ }
+ }
+}
+
diff --git a/Sources/Rings/HandAiguille.md b/Sources/Rings/HandAiguille.md
new file mode 100644
index 0000000..0f6354c
--- /dev/null
+++ b/Sources/Rings/HandAiguille.md
@@ -0,0 +1,25 @@
+## HandAiguille
+
+### Preview
+https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8e82-d77d9569dcca.mov
+
+### Usage:
+```swift
+ // Create empty hand aiguille with default size, and set the hand aiguille background red
+ HandAiguille() {
+ }.handBackground(Color.red)
+
+ // Create empty hand aiguille with time provider, and specify its time unit.
+ @State var hourProvider: Double = 0.0
+ HandAiguille(time: $hourProvider, unit: .hour) {
+ }.handBackgroudn(Color.red)
+
+ // Create hand aiguille with Image
+ @State var secsProvider: Double = 0.0
+ HandAiguille(time: $secsProvider) {
+ Image("SpadeHand")
+ }
+
+ // Create apple watch style hand
+ HandFactory.standard.makeAppleWatchStyleHand(time: $secsProvider)
+```
diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift
new file mode 100644
index 0000000..5d7818a
--- /dev/null
+++ b/Sources/Rings/HandAiguille.swift
@@ -0,0 +1,210 @@
+//
+// HandAiguille.swift
+//
+//
+// Created by Chen Hai Teng on 5/4/21.
+//
+
+import SwiftUI
+import Foundation
+
+public enum TimeUnit {
+ case hour, minute, second
+}
+
+extension TimeUnit {
+ public typealias RawValue = Calendar.Component
+ public init?(rawValue: RawValue) {
+ switch rawValue {
+ case .hour: self = Self.hour
+ case .minute: self = Self.minute
+ case .second: self = Self.second
+ default: return nil
+ }
+ }
+}
+
+public struct HandAiguille : View {
+
+ @Binding private var time: T
+ @State private var angle: Angle = Angle()
+
+ private let content: () -> Content
+ private var handSize: CGSize
+ private var offset: CGFloat
+ private var timeUnit: TimeUnit
+
+ private var showBlueprint: Bool = false
+ private var handBackground: AnyView = AnyView(Color.clear)
+
+ public init(size: CGSize = CGSize(width: 3.0, height: 50.0), offset: T = 1.5, time: Binding = .constant(0), unit: TimeUnit = .second, @ViewBuilder content: @escaping () -> Content) {
+ self.handSize = size
+ self.offset = CGFloat(offset)
+ _time = time
+ timeUnit = unit
+ self.content = content
+ }
+
+ public var body: some View {
+ GeometryReader { geo in
+ ZStack {
+ let yoffset:CGFloat = handSize.height/2.0 - offset
+ if(showBlueprint) {
+ Circle().stroke().frame(width:geo.size.width, height: geo.size.height)
+ }
+ Path { p in
+ p.move(to: CGPoint(x: 0.0, y: geo.size.height/2.0))
+ p.addLine(to: CGPoint(x: geo.size.width, y: geo.size.height/2.0))
+ p.move(to: CGPoint(x: geo.size.width/2, y: 0.0))
+ p.addLine(to: CGPoint(x: geo.size.width/2, y: geo.size.height))
+ }.if(showBlueprint) { p in
+ p.stroke(Color.blue)
+ }
+ if(content() is EmptyView) {
+ handBackground.frame(width: handSize.width, height: handSize.height, alignment: .center).offset(y: -yoffset).rotationEffect(angleOfTime(time))
+ } else {
+ content().frame(width: handSize.width, height: handSize.height, alignment: .center).offset(y: -yoffset).rotationEffect(angleOfTime(time))
+ }
+ }
+ }
+ }
+
+ private func angleOfTime(_ time: T) -> Angle {
+ let t = Double(time)
+ switch timeUnit {
+ case .hour:
+ let r = t.truncatingRemainder(dividingBy: 12.0)
+ return Angle(degrees: r * 30.0)
+ case .minute, .second:
+ let r =
+ t.truncatingRemainder(dividingBy: 60.0)
+ return Angle(degrees: r * 6.0)
+ }
+ }
+}
+
+extension HandAiguille {
+ func setProperty(_ setBlock: (_ handAiguille: inout Self) -> Void) -> Self {
+ let result = _setProperty(content: self) { (tmp :inout Self) in
+ setBlock(&tmp)
+ return tmp
+ }
+ return result
+ }
+
+ public func blueprint(_ isOn:Bool = true) -> Self {
+ setProperty { tmp in
+ tmp.showBlueprint = isOn
+ }
+ }
+
+ public func handBackground(_ background: Background) -> Self where Background : View {
+ setProperty{ tmp in
+ tmp.handBackground = AnyView(background)
+ }
+ }
+}
+
+public struct HandFactory {
+ public static let standard = HandFactory()
+ private let rectRatio: CGFloat = 0.2
+ public func makeAppleWatchStyleHand(size: CGSize = CGSize(width: 4.0, height: 60.0), timeProvider: Binding, unit: TimeUnit = .second) -> some View {
+ HandAiguille(size: size, offset: 1.5, time: timeProvider, unit: unit) {
+ VStack(spacing: 0) {
+ Capsule().stroke().frame(width: size.width)
+ Rectangle().frame(width: 2, height: 10, alignment: .center)
+ Circle().stroke().frame(width: 3, height: 3, alignment: .center)
+ }
+ }
+ }
+}
+
+
+struct RoundedBorder: ViewModifier {
+ var cornerRadius:CGFloat = 5
+ var color = Color.white
+ var lineWidth:CGFloat = 1
+ func body(content: Content) -> some View {
+ content.padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 5))
+ .overlay(
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .stroke(color, lineWidth: lineWidth)
+ )
+ }
+}
+
+extension View {
+ func rounded(_ cornerRadius:CGFloat = 5, color:Color = .white, width:CGFloat = 1) -> some View {
+ modifier(RoundedBorder(cornerRadius: cornerRadius, color: color, lineWidth: width))
+ }
+}
+
+struct AppleStyleHandPreview: View {
+ @State var emulateTime: CGFloat = 0.0
+ @State var showBlueprint: Bool = false
+ @State var offset: CGFloat = 1.5
+ @State var unit: TimeUnit = .second
+
+ var body: some View {
+ VStack {
+ HStack {
+ ZStack {
+ HandAiguille(size: CGSize(width: 4.0, height: 50.0), offset: offset, time: $emulateTime, unit: unit) {
+ GeometryReader { geo in
+ VStack(spacing: 0) {
+ Capsule().stroke()
+ Rectangle().frame(width: 2, height: 10, alignment: .center)
+ Circle().stroke().frame(width: 3, height: 3, alignment: .center)
+ }
+ }
+ }.blueprint(showBlueprint).frame(width: 100, height: 100, alignment: .center)
+ }
+ ZStack {
+ HandFactory.standard.makeAppleWatchStyleHand(size: CGSize(width: 5.0, height: 80.0),timeProvider: $emulateTime, unit: .hour).frame(width: 120, height: 120, alignment: .center)
+ HandFactory.standard.makeAppleWatchStyleHand(size: CGSize(width: 4.0, height: 60.0),timeProvider: $emulateTime, unit: .minute).frame(width: 120, height: 120, alignment: .center)
+ }
+ }
+
+ HStack {
+ Spacer(minLength: 10)
+ Slider(value: $emulateTime, in: 0.0...60.0, minimumValueLabel: Text("0.0"), maximumValueLabel: Text("60.0")) {
+
+ Text("time")
+ .rounded()
+ }
+ Spacer(minLength: 10)
+ }
+ HStack {
+ Spacer(minLength: 10)
+ Slider(value: $offset, in: 0.0...20.0, step: 0.5, minimumValueLabel: Text("0.0"), maximumValueLabel: Text("20.0")) {
+ Text("offset").rounded()
+ }
+ Spacer(minLength: 10)
+ }
+ HStack {
+ Spacer(minLength: 10)
+ #if os(watchOS)
+ Picker("Time Unit", selection: $unit) {
+ Text("hour").tag(TimeUnit.hour)
+ Text("minute").tag(TimeUnit.minute)
+ Text("second").tag(TimeUnit.second)
+ }
+ #else
+ Picker("Time Unit", selection: $unit) {
+ Text("hour").tag(TimeUnit.hour)
+ Text("minute").tag(TimeUnit.minute)
+ Text("second").tag(TimeUnit.second)
+ }.pickerStyle(SegmentedPickerStyle())
+ #endif
+ Spacer(minLength: 10)
+ }
+ Toggle("show blueprint", isOn: $showBlueprint)
+ }
+ }
+}
+
+struct HandAiguille_Previews: PreviewProvider {
+ static var previews: some View {
+ AppleStyleHandPreview().frame(width: 350, height: 350, alignment: .center)
+ }
+}
diff --git a/Sources/Rings/InternalUtilities.swift b/Sources/Rings/InternalUtilities.swift
new file mode 100644
index 0000000..c967800
--- /dev/null
+++ b/Sources/Rings/InternalUtilities.swift
@@ -0,0 +1,26 @@
+//
+// InternalUtilities.swift
+//
+//
+// Created by Chen Hai Teng on 5/9/21.
+//
+
+import Foundation
+
+internal func _setProperty(content: T, _ setBlock:(_ newContent: inout T) -> T) -> T {
+ var temp = content
+ return setBlock(&temp)
+}
+
+
+protocol Adjustable {}
+
+extension Adjustable {
+ func setProperty(_ setBlock: (_ text: inout Self) -> Void) -> Self {
+ let result = _setProperty(content: self) { (tmp :inout Self) in
+ setBlock(&tmp)
+ return tmp
+ }
+ return result
+ }
+}
diff --git a/Sources/Rings/RingText.md b/Sources/Rings/RingText.md
new file mode 100644
index 0000000..92905ed
--- /dev/null
+++ b/Sources/Rings/RingText.md
@@ -0,0 +1,23 @@
+## RingText
+
+
+
+### Usage:
+
+```swift
+ // To layout the text "1234567890" along a circle with radius 40.
+ RingText(radius: 40.0, text: "1234567890")
+
+ // To layout a list to words along a circle with radius 40.
+ RingText(radius: 40.0, words: ["1","2","3","4","5","6"])
+
+ // To create and setup RingText with rich features:
+ RingText(radius: 40.0, text: "1234567890")
+ .font(Font.custom("Apple Chancery", size: 16.0)) // Setup font and size
+ .begin(degrees: -90.0) // Modify the begining degrees
+ .end(degress: 90.0) // Modify the end degrees.
+ .textColor(.red) // Change the color of text, default is white.
+
+ // Show blueprint of each text (For debug purpose)
+ RingText(radius: 40.0, text: "1234567890").showBlueprint(true)
+```
diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift
new file mode 100644
index 0000000..4e64e08
--- /dev/null
+++ b/Sources/Rings/RingText.swift
@@ -0,0 +1,247 @@
+import SwiftUI
+import CoreGraphicsExtension
+import CommonExts
+
+@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
+public extension CGAngle {
+ func toAngle(offset radians: CGFloat = 0.0) -> Angle {
+ Angle(radians: Double(self.radians + radians))
+ }
+}
+
+private func _createCharacters(origin: [String], reversed:Bool = false) -> [String] {
+ var tmpWords = reversed ? origin.reversed() : origin
+ if(reversed) {
+ tmpWords = tmpWords.map({ word -> String in
+ String(word.reversed())
+ })
+ }
+ let text = (tmpWords.count == 1) ? tmpWords[0] : tmpWords.reduce(String()) {
+ result, element -> String in
+ result + element + " "
+ }
+ let characters = text.map(String.init)
+ return characters
+}
+
+@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
+public struct RingText : View {
+ private var radius: Double
+ private var textColor: Color
+ private var textUpsideDown: Bool
+ private var textReversed: Bool
+
+ private var char_spacing: Double
+ private var kerning_r: Double
+ private var beginRadians: Double
+ private var endRadians: Double
+
+ private var font = Font.system(size: 20.0)
+
+ private var originwords: [String]
+ private var characters: [String]
+ private var textPoints: [CGPolarPoint] = []
+
+ @State private var sizes: [CGSize] = []
+ private var showBlueprint: Bool = false
+
+ public init(radius: T, words: [String], color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) {
+ self.radius = Double(radius)
+ self.textColor = color
+ self.textUpsideDown = upsideDown
+ self.textReversed = reversed
+ self.originwords = words
+
+ characters = _createCharacters(origin: words, reversed: reversed)
+
+ beginRadians = Double(begin.radians)
+
+ let count = characters.count-1
+ let ratio = CGFloat(count)/CGFloat(characters.count)
+
+ endRadians = Double(end?.radians ?? 2*CGFloat.pi*ratio+begin.radians)
+
+ char_spacing = (endRadians - beginRadians)/Double(count)
+
+ kerning_r = acos(1.0 - pow(20.0/self.radius, 2)/2.0)
+
+ textPoints = _createTextPoints()
+ }
+
+ public init(radius: T, text: String, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) {
+ self.init(radius: radius, words: [text], color: color, upsideDown: upsideDown, reversed: reversed, begin: begin)
+ }
+
+ private func _createTextPoints() -> [CGPolarPoint] {
+ return characters.enumerated().map { index, element -> CGPolarPoint in
+ return CGPolarPoint(radius: radius, angle: CGAngle(beginRadians + char_spacing * Double(index)))
+ }
+ }
+
+ private func size(at index: Int) -> CGSize {
+ sizes[safe: index] ?? CGSize(width: 100, height: 0)
+ }
+
+ public var body: some View {
+ GeometryReader { geo in
+ ZStack {
+ ForEach(Array(zip(characters.indices, characters)), id: \.0) { index, item in
+ let polarPt = self.textPoints[index]
+ let pt = polarPt.cgpoint
+ let textPt = CGPoint(x: pt.x, y: pt.y)
+ Sizing {
+ Text(item).font(font).foregroundColor(textColor).if(showBlueprint) { content in
+ content.border(Color.blue.opacity(0.5), width:1)
+ }
+ }.rotationEffect(polarPt.cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)).offset(x: textPt.x, y: textPt.y)
+ }
+
+ }.frame(width: geo.size.width, height: geo.size.height, alignment: .center).onPreferenceChange(ViewSizeKey.self, perform: { value in
+ sizes = value
+ })
+ }
+ }
+}
+
+extension RingText {
+ func setProperty(_ setBlock: (_ text: inout Self) -> Void) -> Self {
+ let result = _setProperty(content: self) { (tmp :inout Self) in
+ setBlock(&tmp)
+ return tmp
+ }
+ return result
+ }
+
+ public func textColor(_ color: Color) -> Self {
+ setProperty { tmp in
+ tmp.textColor = color
+ }
+ }
+
+ public func kerning(_ pt: Double) -> Self {
+ let r = acos(1.0 - pow(pt/self.radius, 2)/2.0)
+ let adjusted = self.char_spacing - kerning_r + r
+ return setProperty { tmp in
+ tmp.kerning_r = pt
+ tmp.char_spacing = adjusted
+ tmp.textPoints = tmp._createTextPoints()
+ }
+ }
+
+ public func begin(degrees: Double) -> Self {
+ return begin(radians: Double(CGAngle.degrees(degrees)))
+ }
+
+ public func begin(radians: Double) -> Self {
+ setProperty { tmp in
+ let range = tmp.endRadians - tmp.beginRadians
+ tmp.beginRadians = radians
+ tmp.endRadians = radians + range
+ tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.characters.count-1)
+
+ tmp.textPoints = tmp._createTextPoints()
+ }
+ }
+
+ public func end(degrees: Double) -> Self {
+ return end(radians: Double(CGAngle.degrees(degrees)))
+ }
+
+ public func end(radians: Double) -> Self {
+ setProperty { tmp in
+ tmp.endRadians = radians
+ tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.characters.count-1)
+ tmp.textPoints = tmp._createTextPoints()
+ }
+ }
+
+ public func upsideDown(_ yes: Bool) -> Self {
+ setProperty { tmp in
+ tmp.textUpsideDown = yes
+ }
+ }
+
+ public func reverse(_ yes: Bool) -> Self {
+ setProperty { tmp in
+ tmp.textReversed = yes
+ tmp.characters = _createCharacters(origin: tmp.originwords, reversed: tmp.textReversed)
+ tmp.textPoints = tmp._createTextPoints()
+ }
+ }
+
+ public func font(_ f: Font) -> Self {
+ setProperty { tmp in
+ tmp.font = f
+ }
+ }
+
+ public func showBlueprint(_ show: Bool) -> Self {
+ setProperty { tmp in
+ tmp.showBlueprint = show
+ }
+ }
+}
+
+@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
+struct RingText_Previews: PreviewProvider {
+ static var previews: some View {
+ RingTextPreviewWrapper()
+ }
+}
+
+struct RingTextPreviewWrapper: View {
+ @State var spacing: Double = 0.0
+ @State var begin: Double = 0.0
+ @State var end: Double = 360.0
+ @State var upside_down: Bool = false
+ @State var reverse_text: Bool = false
+ @State var begin_0: Double = -60.0
+ @State var font_size: Double = 20.0
+ @State var blueprint: Bool = false
+ var body: some View {
+ VStack {
+ HStack {
+ VStack {
+ ZStack {
+ RingText(radius: 40.0, words: ["1","2","3","4","5","6","7","8","9","10","11","12"], color: .blue, upsideDown: false, reversed: false).font(Font.custom("Apple Chancery", size: 16.0)).begin(degrees: begin_0).showBlueprint(blueprint)
+ RingText(radius: 80.0, text: "0987654321", color: .green).font(.system(size: CGFloat(font_size))).showBlueprint(blueprint)
+ }
+ Text("begin degrees: \(begin_0)")
+ Slider(value: $begin_0, in: 0.0...360)
+ Text("font size: \(font_size)")
+ Slider(value: $font_size, in: 10.0...40.0, step: 1)
+ }
+ ZStack {
+ RingText(radius: 40.0, words: ["a23", "b56", "c89"], reversed: true).showBlueprint(blueprint)
+ RingText(radius: 80.0, words: ["1234567890"]).showBlueprint(blueprint)
+ }
+ VStack {
+ ZStack {
+
+ RingText(radius: 60.0, words: ["12345", "67890"]).begin(degrees: begin)
+ .end(degrees: end)
+ .upsideDown(upside_down)
+ .reverse(reverse_text)
+ .textColor(.red).showBlueprint(blueprint)
+ RingText(radius: 40.0, words: ["1234567890"]).kerning(spacing)
+ .upsideDown(upside_down)
+ .reverse(reverse_text)
+ .textColor(.blue).showBlueprint(blueprint)
+
+ }
+ VStack(alignment: .leading) {
+ Text("char spacing : \(spacing)")
+ Slider(value: $spacing, in: 0.0...30.0)
+ Text("begin degrees: \(begin)")
+ Slider(value: $begin, in: 0.0...360.0)
+ Text("end degrees: \(end)")
+ Slider(value: $end, in: 0.0...360.0)
+ Toggle("Upside Down", isOn: $upside_down)
+ Toggle("Reverse Text", isOn: $reverse_text)
+ }
+ }
+ }.background(Color.black)
+ Toggle("Show Layout", isOn:$blueprint)
+ }
+ }
+}
diff --git a/Sources/Rings/Rings.swift b/Sources/Rings/Rings.swift
deleted file mode 100644
index 32fdc9e..0000000
--- a/Sources/Rings/Rings.swift
+++ /dev/null
@@ -1,3 +0,0 @@
-struct Rings {
- var text = "Hello, World!"
-}
diff --git a/Sources/Rings/Sizing.swift b/Sources/Rings/Sizing.swift
new file mode 100644
index 0000000..687c9d8
--- /dev/null
+++ b/Sources/Rings/Sizing.swift
@@ -0,0 +1,30 @@
+//
+// Sizing.swift
+//
+// Refer to:
+// 1. https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
+// 2. https://git.kabellmunk.dk/prototyping-custom-ui-in-swiftui-talk/custom-ui-prototype-in-swiftui/-/blob/master/Custom%20UI%20Prototype/Prototype/CurvedText.swift
+// 3. https://www.youtube.com/watch?v=1BHHybRnHFE
+// 4. https://www.fivestars.blog/articles/preferencekey-reduce/
+//
+// Created by Chen Hai Teng on 4/22/21.
+//
+
+import SwiftUI
+
+struct ViewSizeKey : PreferenceKey {
+ static var defaultValue: [CGSize] = []
+ static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
+ value.append(contentsOf: nextValue())
+ }
+ typealias Value = [CGSize]
+}
+
+struct Sizing : View {
+ var content: ()->V
+ var body: some View {
+ content().background(GeometryReader { geo in
+ Color.clear.preference(key: ViewSizeKey.self, value: [geo.size])
+ })
+ }
+}
diff --git a/Tests/RingsTests/RingsTests.swift b/Tests/RingsTests/RingsTests.swift
index 86b9b5d..f8214a3 100644
--- a/Tests/RingsTests/RingsTests.swift
+++ b/Tests/RingsTests/RingsTests.swift
@@ -1,15 +1,17 @@
import XCTest
+
@testable import Rings
+@testable import CoreGraphicsExtension
-final class RingsTests: XCTestCase {
- func testExample() {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct
- // results.
- XCTAssertEqual(Rings().text, "Hello, World!")
+final class RingTextTests: XCTestCase {
+ func testCGAngleExt() {
+ let zeroAngle = CGAngle.zero.toAngle()
+ let rightAngle = CGAngle.degrees(90.0).toAngle()
+ XCTAssertEqual(zeroAngle.degrees, 0)
+ XCTAssertEqual(rightAngle.degrees, 90)
}
static var allTests = [
- ("testExample", testExample),
+ ("testCGAngleExt", testCGAngleExt),
]
}