From 10f7e9db3388cf8641abfea3ec3b3f910a0d8173 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 6 Apr 2021 14:38:27 +0800 Subject: [PATCH 01/60] - RingText 1. Create RingText to show text on ring shape. - Swift Package Manager 1. Add platform information on Package.swift to solve compiling error cause by @available 2. Update dependencies --- Package.swift | 7 ++- Sources/Rings/RingText.swift | 81 +++++++++++++++++++++++++++++++ Sources/Rings/Rings.swift | 3 -- Tests/RingsTests/RingsTests.swift | 15 +++--- 4 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 Sources/Rings/RingText.swift delete mode 100644 Sources/Rings/Rings.swift diff --git a/Package.swift b/Package.swift index 139dcd0..f5df37c 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ 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( @@ -14,15 +15,17 @@ 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") ], 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: "Rings", - dependencies: []), + dependencies: ["CoreGraphicsExtension"]), .testTarget( name: "RingsTests", - dependencies: ["Rings"]), + dependencies: ["Rings", + "CoreGraphicsExtension"]), ] ) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift new file mode 100644 index 0000000..77c9b99 --- /dev/null +++ b/Sources/Rings/RingText.swift @@ -0,0 +1,81 @@ +import SwiftUI +import CoreGraphicsExtension + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +extension CGAngle { + func toAngle(offset radians: CGFloat = 0.0) -> Angle { + Angle(radians: Double(self.radians + radians)) + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +struct RingText : View { + var radius: Double + var textSize: CGFloat + var textColor: Color + var textUpsideDown: Bool + var textReversed: Bool + private var stringTable: [(offset: Int, element:String)] + private var textPoints: [CGPolarPoint] + + + @inlinable init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + self.radius = radius + self.textColor = color + self.textSize = textSize + self.textUpsideDown = upsideDown + self.textReversed = reversed + + let text = (words.count == 1) ? words[0] : words.reduce(String()) { result, element -> String in + result + element + " " + } + + let characters = Array(text.enumerated()).map { (e: EnumeratedSequence.Iterator.Element) -> (Int, String) in + (e.offset, String(e.element)) + } + + self.stringTable = characters + let gap = (textReversed ? -2.0 : 2.0)*Double.pi/Double(stringTable.count) + let beginOffset = 0.0 + textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in + return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) + }) + } + + @inlinable init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed) + } + + var body: some View { + GeometryReader { geo in + ZStack { + ForEach(stringTable, id: \.self.offset) { (offset, element) in + let pt = self.textPoints[offset].cgpoint + let textPt = CGPoint(x: pt.x, y: pt.y) + Text(element) + .rotationEffect(self.textPoints[offset].cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)) + .offset(x: textPt.x, y: textPt.y) + .font(.system(size: textSize)).foregroundColor(textColor) + + } + }.frame(width: geo.size.width, height: geo.size.height, alignment: .center) + } + } +} + +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +struct RingText_Previews: PreviewProvider { + static var previews: some View { + VStack { + ZStack { + RingText(radius: 20.0, text: "1234567890",textSize: 16.0, color: .red, upsideDown: false) + RingText(radius: 50.0, text: "12345678901234",textSize: 20.0, color: .blue, upsideDown: true, reversed: true) + RingText(radius: 90.0, text: "0987654321",textSize: 32.0, color: .green) + } + ZStack { + RingText(radius: 40.0, words: ["123", "456", "789"]) + RingText(radius: 80, words: ["1234567890"]) + } + } + } +} 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/Tests/RingsTests/RingsTests.swift b/Tests/RingsTests/RingsTests.swift index 86b9b5d..8c5a90f 100644 --- a/Tests/RingsTests/RingsTests.swift +++ b/Tests/RingsTests/RingsTests.swift @@ -1,15 +1,16 @@ 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), ] } From ae868a9d0204b65c052580d077078ad5593cd3e2 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 6 Apr 2021 14:57:22 +0800 Subject: [PATCH 02/60] Publishing RingText in SPM --- Sources/Rings/RingText.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 77c9b99..22601c2 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -2,24 +2,24 @@ import SwiftUI import CoreGraphicsExtension @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension CGAngle { +public extension CGAngle { func toAngle(offset radians: CGFloat = 0.0) -> Angle { Angle(radians: Double(self.radians + radians)) } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -struct RingText : View { - var radius: Double - var textSize: CGFloat - var textColor: Color - var textUpsideDown: Bool - var textReversed: Bool +public struct RingText : View { + public var radius: Double + public var textSize: CGFloat + public var textColor: Color + public var textUpsideDown: Bool + public var textReversed: Bool private var stringTable: [(offset: Int, element:String)] private var textPoints: [CGPolarPoint] - @inlinable init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { self.radius = radius self.textColor = color self.textSize = textSize @@ -42,11 +42,11 @@ struct RingText : View { }) } - @inlinable init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed) } - var body: some View { + public var body: some View { GeometryReader { geo in ZStack { ForEach(stringTable, id: \.self.offset) { (offset, element) in From 0d0fffa996288cde1a079e2889b71ebd7c037759 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 6 Apr 2021 15:00:05 +0800 Subject: [PATCH 03/60] publishing initializer of RingText --- Sources/Rings/RingText.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 22601c2..f7f18db 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -10,16 +10,16 @@ public extension CGAngle { @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct RingText : View { - public var radius: Double - public var textSize: CGFloat - public var textColor: Color - public var textUpsideDown: Bool - public var textReversed: Bool + var radius: Double + var textSize: CGFloat + var textColor: Color + var textUpsideDown: Bool + var textReversed: Bool private var stringTable: [(offset: Int, element:String)] private var textPoints: [CGPolarPoint] - init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { self.radius = radius self.textColor = color self.textSize = textSize @@ -42,7 +42,7 @@ public struct RingText : View { }) } - init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + public init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed) } From 4de27b11ed879d118bc20ff2ac7857e29bf07061 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 6 Apr 2021 15:49:14 +0800 Subject: [PATCH 04/60] Add begin argument to setup the beginning position --- Sources/Rings/RingText.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index f7f18db..69f651d 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -18,8 +18,7 @@ public struct RingText : View { private var stringTable: [(offset: Int, element:String)] private var textPoints: [CGPolarPoint] - - public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { + public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { self.radius = radius self.textColor = color self.textSize = textSize @@ -36,14 +35,14 @@ public struct RingText : View { self.stringTable = characters let gap = (textReversed ? -2.0 : 2.0)*Double.pi/Double(stringTable.count) - let beginOffset = 0.0 + let beginOffset = Double(begin.radians) textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) }) } - public init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false) { - self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed) + public init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { + self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed, begin: begin) } public var body: some View { @@ -68,8 +67,8 @@ struct RingText_Previews: PreviewProvider { static var previews: some View { VStack { ZStack { - RingText(radius: 20.0, text: "1234567890",textSize: 16.0, color: .red, upsideDown: false) - RingText(radius: 50.0, text: "12345678901234",textSize: 20.0, color: .blue, upsideDown: true, reversed: true) + RingText(radius: 20.0, text: "1234567890",textSize: 16.0, color: .red, upsideDown: false, begin: -CGAngle.pi/2) + RingText(radius: 50.0, text: "1234567890",textSize: 20.0, color: .blue, upsideDown: true, reversed: true) RingText(radius: 90.0, text: "0987654321",textSize: 32.0, color: .green) } ZStack { From 2baaa4ef5221294076df9eb6edf5fa69a5710843 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 7 Apr 2021 16:10:12 +0800 Subject: [PATCH 05/60] Add end position to setup text range. --- Sources/Rings/RingText.swift | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 69f651d..620c5da 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -18,7 +18,7 @@ public struct RingText : View { private var stringTable: [(offset: Int, element:String)] private var textPoints: [CGPolarPoint] - public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { + public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { self.radius = radius self.textColor = color self.textSize = textSize @@ -34,11 +34,18 @@ public struct RingText : View { } self.stringTable = characters - let gap = (textReversed ? -2.0 : 2.0)*Double.pi/Double(stringTable.count) let beginOffset = Double(begin.radians) - textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in - return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) - }) + if let endOffset = end?.radians, let range = (endOffset - begin.radians) as? CGFloat, range > 0 { + let gap = (textReversed ? -1.0 : 1.0)*Double(range)/Double(stringTable.count-1) + textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in + return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) + }) + } else { + let gap = (textReversed ? -2.0 : 2.0)*Double.pi/Double(stringTable.count) + textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in + return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) + }) + } } public init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { @@ -75,6 +82,9 @@ struct RingText_Previews: PreviewProvider { RingText(radius: 40.0, words: ["123", "456", "789"]) RingText(radius: 80, words: ["1234567890"]) } - } + ZStack { + RingText(radius: 80.0, words: ["123456789"], textSize: 16.0, color: .red, upsideDown: false, reversed: false, begin: CGAngle.degrees(-180), end: CGAngle.zero) + } + }.background(Color.black) } } From 58a0b6e0f6d5e2007aa39cca364b6da360e9feef Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 13 Apr 2021 10:43:49 +0800 Subject: [PATCH 06/60] 1. Add char_spacing, beginRadians, and endRadians to change appearance. 2. Extract functions * _createStringTable() -- to make it reusable when change reversed dynamic * _createTextPoints() -- to make it reusable when radius, char spacing, and begin changed. * also init textPoints with empty array to solve non-initialized error. --- Sources/Rings/RingText.swift | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 620c5da..9218e2f 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -8,6 +8,26 @@ public extension CGAngle { } } + +private func _createStringTable(origin: [String], reversed:Bool = false) -> [(Int, 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 = Array(text.enumerated()).map { (e: EnumeratedSequence.Iterator.Element) -> (Int, String) in + (e.offset, String(e.element)) + } + return characters +} + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct RingText : View { var radius: Double @@ -15,8 +35,12 @@ public struct RingText : View { var textColor: Color var textUpsideDown: Bool var textReversed: Bool + + var char_spacing: Double + var beginRadians: Double + var endRadians: Double private var stringTable: [(offset: Int, element:String)] - private var textPoints: [CGPolarPoint] + private var textPoints: [CGPolarPoint] = [] public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { self.radius = radius @@ -52,6 +76,12 @@ public struct RingText : View { self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed, begin: begin) } + private func _createTextPoints() -> [CGPolarPoint] { + return stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in + return CGPolarPoint(radius: radius, angle: CGAngle( beginRadians + char_spacing * Double(offset))) + }) + } + public var body: some View { GeometryReader { geo in ZStack { From 668ca41cf2c5d6a5da8d27454e59317b56b038c7 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 13 Apr 2021 10:49:52 +0800 Subject: [PATCH 07/60] Replace statements in init with _createStringTable and _createTextPoints --- Sources/Rings/RingText.swift | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 9218e2f..5ed71d3 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -49,27 +49,18 @@ public struct RingText : View { self.textUpsideDown = upsideDown self.textReversed = reversed - let text = (words.count == 1) ? words[0] : words.reduce(String()) { result, element -> String in - result + element + " " - } + stringTable = _createStringTable(origin: words, reversed: reversed) - let characters = Array(text.enumerated()).map { (e: EnumeratedSequence.Iterator.Element) -> (Int, String) in - (e.offset, String(e.element)) - } + beginRadians = Double(begin.radians) - self.stringTable = characters - let beginOffset = Double(begin.radians) - if let endOffset = end?.radians, let range = (endOffset - begin.radians) as? CGFloat, range > 0 { - let gap = (textReversed ? -1.0 : 1.0)*Double(range)/Double(stringTable.count-1) - textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in - return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) - }) - } else { - let gap = (textReversed ? -2.0 : 2.0)*Double.pi/Double(stringTable.count) - textPoints = self.stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in - return CGPolarPoint(radius: radius, angle: CGAngle( beginOffset + gap * Double(offset))) - }) - } + let count = stringTable.count-1 + let ratio = CGFloat(count)/CGFloat(stringTable.count) + + endRadians = Double(end?.radians ?? 2*CGFloat.pi*ratio+begin.radians) + + char_spacing = (endRadians - beginRadians)/Double(count) + + textPoints = _createTextPoints() } public init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { From 164c681de2bca22fa6a9b25576f72464d4b5e4b7 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 13 Apr 2021 11:40:47 +0800 Subject: [PATCH 08/60] 1. Add originwords to restore the original word list. 2. Add _setProperty to help copy and change property in constant structure. 3. Implement RingText extensions to change textColor, charSpacing, begin, end, upsideDown, reverse --- Sources/Rings/RingText.swift | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 5ed71d3..d4a10b6 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -39,6 +39,8 @@ public struct RingText : View { var char_spacing: Double var beginRadians: Double var endRadians: Double + + private var originwords: [String] private var stringTable: [(offset: Int, element:String)] private var textPoints: [CGPolarPoint] = [] @@ -48,6 +50,7 @@ public struct RingText : View { self.textSize = textSize self.textUpsideDown = upsideDown self.textReversed = reversed + self.originwords = words stringTable = _createStringTable(origin: words, reversed: reversed) @@ -90,6 +93,74 @@ public struct RingText : View { } } +func _setProperty(content: T, _ setBlock:(_ newContent: inout T) -> T) -> T { + var temp = content + return setBlock(&temp) +} + +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 charSpacing(_ spacing: Double) -> Self { + setProperty { tmp in + tmp.char_spacing = spacing + 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 + tmp.beginRadians = radians + + tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.stringTable.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.stringTable.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.stringTable = _createStringTable(origin: tmp.originwords, reversed: tmp.textReversed) + tmp.textPoints = tmp._createTextPoints() + } + } +} + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) struct RingText_Previews: PreviewProvider { static var previews: some View { From d91de1584fc9fd31e29da13eea07f54c08581957 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 13 Apr 2021 12:00:39 +0800 Subject: [PATCH 09/60] 1. Extract preview layout to RingTextPreviewWrapper -- to make it can change attributes dynamic on Live preview. 2. Add controls on preview to demonstrate dynamic attributes change. --- Sources/Rings/RingText.swift | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index d4a10b6..57ddcb4 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -83,7 +83,7 @@ public struct RingText : View { let pt = self.textPoints[offset].cgpoint let textPt = CGPoint(x: pt.x, y: pt.y) Text(element) - .rotationEffect(self.textPoints[offset].cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)) + .rotationEffect(self.textPoints[offset].cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)) .offset(x: textPt.x, y: textPt.y) .font(.system(size: textSize)).foregroundColor(textColor) @@ -164,18 +164,41 @@ extension RingText { @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) struct RingText_Previews: PreviewProvider { static var previews: some View { - VStack { + RingTextPreviewWrapper() + } +} + +struct RingTextPreviewWrapper: View { + @State var spacing: Double = 0.3 + @State var begin: Double = 0.0 + @State var end: Double = 360.0 + @State var upside_down: Bool = false + @State var reverse_text: Bool = false + var body: some View { + HStack { ZStack { RingText(radius: 20.0, text: "1234567890",textSize: 16.0, color: .red, upsideDown: false, begin: -CGAngle.pi/2) RingText(radius: 50.0, text: "1234567890",textSize: 20.0, color: .blue, upsideDown: true, reversed: true) RingText(radius: 90.0, text: "0987654321",textSize: 32.0, color: .green) } ZStack { - RingText(radius: 40.0, words: ["123", "456", "789"]) + RingText(radius: 40.0, words: ["a23", "b56", "c89"], reversed: true) RingText(radius: 80, words: ["1234567890"]) } ZStack { RingText(radius: 80.0, words: ["123456789"], textSize: 16.0, color: .red, upsideDown: false, reversed: false, begin: CGAngle.degrees(-180), end: CGAngle.zero) + RingText(radius: 40.0, words: ["12345", "67890"]).charSpacing(spacing).begin(degrees: begin) + .end(degrees: end) + .upsideDown(upside_down) + .reverse(reverse_text) + + } + VStack { + Slider(value: $spacing, in: 0.0...1.0) + Slider(value: $begin, in: 0.0...360.0) + Slider(value: $end, in: 0.0...360.0) + Toggle("Upside Down", isOn: $upside_down) + Toggle("Reverse Text", isOn: $reverse_text) } }.background(Color.black) } From 9ce807c9a874fe4ddec35b7c234f48972d6ba086 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 14 Apr 2021 14:54:42 +0800 Subject: [PATCH 10/60] To make RingText more flexible, use font instead of text size. --- Sources/Rings/RingText.swift | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 57ddcb4..17fd405 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -31,7 +31,6 @@ private func _createStringTable(origin: [String], reversed:Bool = false) -> [(In @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct RingText : View { var radius: Double - var textSize: CGFloat var textColor: Color var textUpsideDown: Bool var textReversed: Bool @@ -40,14 +39,15 @@ public struct RingText : View { var beginRadians: Double var endRadians: Double + private var font = Font.system(size: 20.0) + private var originwords: [String] private var stringTable: [(offset: Int, element:String)] private var textPoints: [CGPolarPoint] = [] - public init(radius: Double, words: [String], textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { + public init(radius: Double, words: [String], color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { self.radius = radius self.textColor = color - self.textSize = textSize self.textUpsideDown = upsideDown self.textReversed = reversed self.originwords = words @@ -66,8 +66,8 @@ public struct RingText : View { textPoints = _createTextPoints() } - public init(radius: Double, text: String, textSize:CGFloat = 20.0, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { - self.init(radius: radius, words: [text], textSize: textSize, color: color, upsideDown: upsideDown, reversed: reversed, begin: begin) + public init(radius: Double, 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] { @@ -85,8 +85,8 @@ public struct RingText : View { Text(element) .rotationEffect(self.textPoints[offset].cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)) .offset(x: textPt.x, y: textPt.y) - .font(.system(size: textSize)).foregroundColor(textColor) - + .font(font) + .foregroundColor(textColor) } }.frame(width: geo.size.width, height: geo.size.height, alignment: .center) } @@ -159,6 +159,12 @@ extension RingText { tmp.textPoints = tmp._createTextPoints() } } + + public func font(_ f: Font) -> Self { + setProperty { tmp in + tmp.font = f + } + } } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) From 0b6229f8e55540b70e8439f72d8d5828b3cc74b2 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 14 Apr 2021 14:56:35 +0800 Subject: [PATCH 11/60] update previews to show the effect of font --- Sources/Rings/RingText.swift | 51 ++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 17fd405..ac4ae36 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -180,31 +180,48 @@ struct RingTextPreviewWrapper: View { @State var end: Double = 360.0 @State var upside_down: Bool = false @State var reverse_text: Bool = false + @State var begin_0: Double = 0.0 + @State var font_size: Double = 20.0 var body: some View { HStack { - ZStack { - RingText(radius: 20.0, text: "1234567890",textSize: 16.0, color: .red, upsideDown: false, begin: -CGAngle.pi/2) - RingText(radius: 50.0, text: "1234567890",textSize: 20.0, color: .blue, upsideDown: true, reversed: true) - RingText(radius: 90.0, text: "0987654321",textSize: 32.0, color: .green) + VStack { + ZStack { + RingText(radius: 40.0, text: "1234567890", color: .blue, upsideDown: true, reversed: true).font(Font.custom("Apple Chancery", size: 16.0)).begin(degrees: begin_0) + RingText(radius: 80.0, text: "0987654321", color: .green).font(.system(size: font_size)) + } + 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) RingText(radius: 80, words: ["1234567890"]) } - ZStack { - RingText(radius: 80.0, words: ["123456789"], textSize: 16.0, color: .red, upsideDown: false, reversed: false, begin: CGAngle.degrees(-180), end: CGAngle.zero) - RingText(radius: 40.0, words: ["12345", "67890"]).charSpacing(spacing).begin(degrees: begin) - .end(degrees: end) - .upsideDown(upside_down) - .reverse(reverse_text) - - } VStack { - Slider(value: $spacing, in: 0.0...1.0) - Slider(value: $begin, in: 0.0...360.0) - Slider(value: $end, in: 0.0...360.0) - Toggle("Upside Down", isOn: $upside_down) - Toggle("Reverse Text", isOn: $reverse_text) + ZStack { + + RingText(radius: 60.0, words: ["12345", "67890"]).begin(degrees: begin) + .end(degrees: end) + .upsideDown(upside_down) + .reverse(reverse_text) + .textColor(.red) + RingText(radius: 40.0, words: ["1234567890"]).charSpacing(spacing) + .upsideDown(upside_down) + .reverse(reverse_text) + .textColor(.blue) + + } + VStack(alignment: .leading) { + Text("char spacing : \(spacing)") + Slider(value: $spacing, in: 0.0...1.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) } From 9a04f06be73ff6a688304b8dfa9f23d6e710e2e8 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 14 Apr 2021 14:59:15 +0800 Subject: [PATCH 12/60] Fix type error, cast font_size as CGFloat --- Sources/Rings/RingText.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index ac4ae36..f20b741 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -187,7 +187,7 @@ struct RingTextPreviewWrapper: View { VStack { ZStack { RingText(radius: 40.0, text: "1234567890", color: .blue, upsideDown: true, reversed: true).font(Font.custom("Apple Chancery", size: 16.0)).begin(degrees: begin_0) - RingText(radius: 80.0, text: "0987654321", color: .green).font(.system(size: font_size)) + RingText(radius: 80.0, text: "0987654321", color: .green).font(.system(size: CGFloat(font_size))) } Text("begin degrees: \(begin_0)") Slider(value: $begin_0, in: 0.0...360) From c67d322b60cb036b6ddc85f22c033223c672dc84 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 14 Apr 2021 15:03:31 +0800 Subject: [PATCH 13/60] Make the range between begin and end fixed when change begin. --- Sources/Rings/RingText.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index f20b741..ee64022 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -126,8 +126,9 @@ extension RingText { 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.stringTable.count-1) tmp.textPoints = tmp._createTextPoints() From be750bb906269a318b0c4f74f566a0e5c3bc84d1 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 14 Apr 2021 15:26:16 +0800 Subject: [PATCH 14/60] Update README.md --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e72e3b3..4e92433 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ # Rings -A description of this package. +**Rings** is a collection of controls which have similar shapes of ring, circle... + +It includes following controls: +* RingText +* ArchimedeanSpiralText (In-progress) +* SphericText (In-progress) +* Knob (In-planning) + +## RingText Preview +![RingTextPreview](https://user-images.githubusercontent.com/1284944/114670501-a4e17a80-9d35-11eb-955b-3a7b6c6e897f.gif) From 3b35299c3d2a9ef71449273bd9cbf38277c33741 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 15 Apr 2021 15:05:17 +0800 Subject: [PATCH 15/60] ### Refine text processing 1. Remove stringTable, and use characters instead. It does not need to transform words into (Index, String) tuple. 2. Replace _createStringTable with _createCharacters. 3. Update _createTextPoints based on characters. 4. When create body, use zip(characters.indeices, characters) in ForEach to make it safer ( refer to : https://stackoverflow.com/a/63145650/505763) --- Sources/Rings/RingText.swift | 45 +++++++++++++++--------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index ee64022..b492eec 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -8,23 +8,18 @@ public extension CGAngle { } } - -private func _createStringTable(origin: [String], reversed:Bool = false) -> [(Int, String)] { +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 + let text = (tmpWords.count == 1) ? tmpWords[0] : tmpWords.reduce(String()) { + result, element -> String in result + element + " " } - - let characters = Array(text.enumerated()).map { (e: EnumeratedSequence.Iterator.Element) -> (Int, String) in - (e.offset, String(e.element)) - } + let characters = text.map(String.init) return characters } @@ -42,7 +37,7 @@ public struct RingText : View { private var font = Font.system(size: 20.0) private var originwords: [String] - private var stringTable: [(offset: Int, element:String)] + private var characters: [String] private var textPoints: [CGPolarPoint] = [] public init(radius: Double, words: [String], color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { @@ -52,12 +47,12 @@ public struct RingText : View { self.textReversed = reversed self.originwords = words - stringTable = _createStringTable(origin: words, reversed: reversed) + characters = _createCharacters(origin: words, reversed: reversed) beginRadians = Double(begin.radians) - let count = stringTable.count-1 - let ratio = CGFloat(count)/CGFloat(stringTable.count) + let count = characters.count-1 + let ratio = CGFloat(count)/CGFloat(characters.count) endRadians = Double(end?.radians ?? 2*CGFloat.pi*ratio+begin.radians) @@ -71,23 +66,21 @@ public struct RingText : View { } private func _createTextPoints() -> [CGPolarPoint] { - return stringTable.map({ (offset: Int, element: String) -> CGPolarPoint in - return CGPolarPoint(radius: radius, angle: CGAngle( beginRadians + char_spacing * Double(offset))) - }) + return characters.enumerated().map { index, element -> CGPolarPoint in + return CGPolarPoint(radius: radius, angle: CGAngle(beginRadians + char_spacing * Double(index))) + } } public var body: some View { GeometryReader { geo in ZStack { - ForEach(stringTable, id: \.self.offset) { (offset, element) in - let pt = self.textPoints[offset].cgpoint + 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) - Text(element) - .rotationEffect(self.textPoints[offset].cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)) - .offset(x: textPt.x, y: textPt.y) - .font(font) - .foregroundColor(textColor) + Text(item).rotationEffect(polarPt.cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)).offset(x: textPt.x, y: textPt.y).font(font).foregroundColor(textColor) } + }.frame(width: geo.size.width, height: geo.size.height, alignment: .center) } } @@ -129,7 +122,7 @@ extension RingText { let range = tmp.endRadians - tmp.beginRadians tmp.beginRadians = radians tmp.endRadians = radians + range - tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.stringTable.count-1) + tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.characters.count-1) tmp.textPoints = tmp._createTextPoints() } @@ -142,7 +135,7 @@ extension RingText { public func end(radians: Double) -> Self { setProperty { tmp in tmp.endRadians = radians - tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.stringTable.count-1) + tmp.char_spacing = (tmp.endRadians - tmp.beginRadians)/Double(tmp.characters.count-1) tmp.textPoints = tmp._createTextPoints() } } @@ -156,7 +149,7 @@ extension RingText { public func reverse(_ yes: Bool) -> Self { setProperty { tmp in tmp.textReversed = yes - tmp.stringTable = _createStringTable(origin: tmp.originwords, reversed: tmp.textReversed) + tmp.characters = _createCharacters(origin: tmp.originwords, reversed: tmp.textReversed) tmp.textPoints = tmp._createTextPoints() } } From 87007b54626183ad8f91f356496aa27535ec2332 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Fri, 16 Apr 2021 15:23:33 +0800 Subject: [PATCH 16/60] 1. Encapsulate properties 2. Rename charSpacing to kerning to fit typography terminology. 3. Modify kerning adjustment process, use distance instead of radians between characters --- Sources/Rings/RingText.swift | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index b492eec..32bdd2a 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -25,14 +25,15 @@ private func _createCharacters(origin: [String], reversed:Bool = false) -> [Stri @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct RingText : View { - var radius: Double - var textColor: Color - var textUpsideDown: Bool - var textReversed: Bool + private var radius: Double + private var textColor: Color + private var textUpsideDown: Bool + private var textReversed: Bool - var char_spacing: Double - var beginRadians: Double - var endRadians: Double + 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) @@ -58,6 +59,8 @@ public struct RingText : View { char_spacing = (endRadians - beginRadians)/Double(count) + kerning_r = acos(1.0 - pow(20.0/self.radius, 2)/2.0) + textPoints = _createTextPoints() } @@ -106,9 +109,12 @@ extension RingText { } } - public func charSpacing(_ spacing: Double) -> Self { - setProperty { tmp in - tmp.char_spacing = spacing + 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() } } @@ -169,7 +175,7 @@ struct RingText_Previews: PreviewProvider { } struct RingTextPreviewWrapper: View { - @State var spacing: Double = 0.3 + @State var spacing: Double = 0.0 @State var begin: Double = 0.0 @State var end: Double = 360.0 @State var upside_down: Bool = false @@ -200,7 +206,7 @@ struct RingTextPreviewWrapper: View { .upsideDown(upside_down) .reverse(reverse_text) .textColor(.red) - RingText(radius: 40.0, words: ["1234567890"]).charSpacing(spacing) + RingText(radius: 40.0, words: ["1234567890"]).kerning(spacing) .upsideDown(upside_down) .reverse(reverse_text) .textColor(.blue) @@ -208,7 +214,7 @@ struct RingTextPreviewWrapper: View { } VStack(alignment: .leading) { Text("char spacing : \(spacing)") - Slider(value: $spacing, in: 0.0...1.0) + Slider(value: $spacing, in: 0.0...30.0) Text("begin degrees: \(begin)") Slider(value: $begin, in: 0.0...360.0) Text("end degrees: \(end)") From 912b8e61a42ae28a37eb09ff5ffa5f0487dffc36 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 22 Apr 2021 14:46:49 +0800 Subject: [PATCH 17/60] #### Add common extensions 1. Provide safe-access on collections. (https://stackoverflow.com/a/37225027/505763) 2. Create ViewBuilder to support conditionally view setup. (https://stackoverflow.com/a/57685253/505763) --- Package.swift | 7 ++++++- Sources/CommonExts/CollectionExts.swift | 15 +++++++++++++++ Sources/CommonExts/ViewExts.swift | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 Sources/CommonExts/CollectionExts.swift create mode 100644 Sources/CommonExts/ViewExts.swift diff --git a/Package.swift b/Package.swift index f5df37c..3bd7c03 100644 --- a/Package.swift +++ b/Package.swift @@ -8,6 +8,9 @@ let package = Package( 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"]), @@ -20,9 +23,11 @@ let package = Package( 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: ["CoreGraphicsExtension"]), + dependencies: ["CoreGraphicsExtension", "CommonExts"]), .testTarget( name: "RingsTests", dependencies: ["Rings", 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 + } + } +} From ccf0f4b94d5ff34414bc455d677b3979365557e3 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 22 Apr 2021 15:19:14 +0800 Subject: [PATCH 18/60] Implement blue print to show layout detail for debug purpose. 1. Create Sizing view to wrap content, and calculate size of content. 2. Store content sizes into ViewSizeKey 3. Refactor the view tree of RingText -- wrap Text with Sizing, and apply rotation and offset on Sizing rather than Text itself. 4. Apply border setup on Text itself to provide blue print layout. --- Sources/Rings/RingText.swift | 104 +++++++++++++++++++++-------------- Sources/Rings/Sizing.swift | 30 ++++++++++ 2 files changed, 94 insertions(+), 40 deletions(-) create mode 100644 Sources/Rings/Sizing.swift diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 32bdd2a..36b8b9f 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -1,5 +1,6 @@ import SwiftUI import CoreGraphicsExtension +import CommonExts @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public extension CGAngle { @@ -41,6 +42,9 @@ public struct RingText : View { private var characters: [String] private var textPoints: [CGPolarPoint] = [] + @State private var sizes: [CGSize] = [] + private var showBlueprint: Bool = false + public init(radius: Double, words: [String], color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { self.radius = radius self.textColor = color @@ -74,6 +78,10 @@ public struct RingText : View { } } + private func size(at index: Int) -> CGSize { + sizes[safe: index] ?? CGSize(width: 100, height: 0) + } + public var body: some View { GeometryReader { geo in ZStack { @@ -81,10 +89,16 @@ public struct RingText : View { let polarPt = self.textPoints[index] let pt = polarPt.cgpoint let textPt = CGPoint(x: pt.x, y: pt.y) - Text(item).rotationEffect(polarPt.cgangle.toAngle(offset: CGFloat.pi/2) + Angle.degrees(textUpsideDown ? 180 : 0)).offset(x: textPt.x, y: textPt.y).font(font).foregroundColor(textColor) + 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) + }.frame(width: geo.size.width, height: geo.size.height, alignment: .center).onPreferenceChange(ViewSizeKey.self, perform: { value in + sizes = value + }) } } } @@ -165,6 +179,12 @@ extension RingText { 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, *) @@ -180,49 +200,53 @@ struct RingTextPreviewWrapper: View { @State var end: Double = 360.0 @State var upside_down: Bool = false @State var reverse_text: Bool = false - @State var begin_0: Double = 0.0 + @State var begin_0: Double = -60.0 @State var font_size: Double = 20.0 + @State var blueprint: Bool = false var body: some View { - HStack { - VStack { - ZStack { - RingText(radius: 40.0, text: "1234567890", color: .blue, upsideDown: true, reversed: true).font(Font.custom("Apple Chancery", size: 16.0)).begin(degrees: begin_0) - RingText(radius: 80.0, text: "0987654321", color: .green).font(.system(size: CGFloat(font_size))) + 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))) + } + 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) } - 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) - RingText(radius: 80, words: ["1234567890"]) - } - VStack { ZStack { - - RingText(radius: 60.0, words: ["12345", "67890"]).begin(degrees: begin) - .end(degrees: end) - .upsideDown(upside_down) - .reverse(reverse_text) - .textColor(.red) - RingText(radius: 40.0, words: ["1234567890"]).kerning(spacing) - .upsideDown(upside_down) - .reverse(reverse_text) - .textColor(.blue) - + RingText(radius: 40.0, words: ["a23", "b56", "c89"], reversed: true) + RingText(radius: 80, words: ["1234567890"]) } - 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) + VStack { + ZStack { + + RingText(radius: 60.0, words: ["12345", "67890"]).begin(degrees: begin) + .end(degrees: end) + .upsideDown(upside_down) + .reverse(reverse_text) + .textColor(.red) + RingText(radius: 40.0, words: ["1234567890"]).kerning(spacing) + .upsideDown(upside_down) + .reverse(reverse_text) + .textColor(.blue) + + } + 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) + }.background(Color.black) + Toggle("Show Layout", isOn:$blueprint) + } } } 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]) + }) + } +} From d672a27a1ef129708c2adf6038baa7825951bb26 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sun, 25 Apr 2021 15:51:04 +0800 Subject: [PATCH 19/60] Update README.md 1. Update preview 2. Add Usages. --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e92433..9b653b5 100644 --- a/README.md +++ b/README.md @@ -8,5 +8,27 @@ It includes following controls: * SphericText (In-progress) * Knob (In-planning) -## RingText Preview -![RingTextPreview](https://user-images.githubusercontent.com/1284944/114670501-a4e17a80-9d35-11eb-955b-3a7b6c6e897f.gif) +## RingText + +### Preview +![RingDemo](https://user-images.githubusercontent.com/1284944/115984682-fb26a700-a5da-11eb-8a59-a1554ec41bdf.gif) + +### 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) +``` From 0ef5809d30b9b021a037a4d4f0c116ffecd33145 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Sun, 25 Apr 2021 16:27:38 +0800 Subject: [PATCH 20/60] update preview to demo blueprint --- Sources/Rings/RingText.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index 36b8b9f..4c1a5c0 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -209,7 +209,7 @@ struct RingTextPreviewWrapper: View { 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))) + 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) @@ -217,8 +217,8 @@ struct RingTextPreviewWrapper: View { Slider(value: $font_size, in: 10.0...40.0, step: 1) } ZStack { - RingText(radius: 40.0, words: ["a23", "b56", "c89"], reversed: true) - RingText(radius: 80, words: ["1234567890"]) + RingText(radius: 40.0, words: ["a23", "b56", "c89"], reversed: true).showBlueprint(blueprint) + RingText(radius: 80, words: ["1234567890"]).showBlueprint(blueprint) } VStack { ZStack { @@ -227,11 +227,11 @@ struct RingTextPreviewWrapper: View { .end(degrees: end) .upsideDown(upside_down) .reverse(reverse_text) - .textColor(.red) + .textColor(.red).showBlueprint(blueprint) RingText(radius: 40.0, words: ["1234567890"]).kerning(spacing) .upsideDown(upside_down) .reverse(reverse_text) - .textColor(.blue) + .textColor(.blue).showBlueprint(blueprint) } VStack(alignment: .leading) { From 378243f83134d92874fb51ba4dc7f67d26dc7be5 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 29 Apr 2021 14:28:00 +0800 Subject: [PATCH 21/60] Add Clock Index to show clock layout --- Package.swift | 2 +- Sources/Rings/ClockIndex.swift | 135 ++++++++++++++++++++++++++++++ Sources/Rings/RingText.swift | 2 +- Tests/RingsTests/RingsTests.swift | 1 + 4 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 Sources/Rings/ClockIndex.swift diff --git a/Package.swift b/Package.swift index 3bd7c03..eb918b4 100644 --- a/Package.swift +++ b/Package.swift @@ -31,6 +31,6 @@ let package = Package( .testTarget( name: "RingsTests", dependencies: ["Rings", - "CoreGraphicsExtension"]), + "CoreGraphicsExtension", "CommonExts"]), ] ) diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift new file mode 100644 index 0000000..93052db --- /dev/null +++ b/Sources/Rings/ClockIndex.swift @@ -0,0 +1,135 @@ +// +// ClockIndex.swift +// +// +// Created by Chen Hai Teng on 4/22/21. +// + +import SwiftUI +import CoreGraphicsExtension +import CommonExts + +enum ClockIndexError: Error { + case outOfBounds(String) +} + +let defaultTextMarker = ["1","2","3","4","5","6","7","8","9","10","11","12"] + +let defaultMarkers: [AnyView] = defaultTextMarker.map { num -> AnyView in + AnyView(Text(num)) +} + +let defaultRadius: CGFloat = 80.0 + +public struct ClockIndex: View { + + private var hourMarkers: [AnyView] = defaultMarkers + private var radius: CGFloat = defaultRadius + private var showBlueprint: Bool = false + + init(textMarkers: [String] = defaultTextMarker, surface: Surface? = nil) throws { + guard textMarkers.count == 12 else { + throw ClockIndexError.outOfBounds("The number of markers whould be 12.") + } + hourMarkers = textMarkers.map({ text -> AnyView in + AnyView(Text(text)) + }) + } + + init(_ markers: [AnyView], surface: Surface? = nil) 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 { + 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: CGFloat) -> Self { + setProperty { tmp in + tmp.radius = r + } + } + public func showBlueprint(_ show: Bool) -> Self { + setProperty { tmp in + tmp.showBlueprint = show + } + } +} + +//Previews +struct ClockPreviewClassic : View { + @State var showBlueprint: Bool = false + var body: some View { + VStack { + Spacer(minLength: 10.0) + Text("Classic Clocks") + HStack { + try? ClockIndex().radius(50.0).showBlueprint(showBlueprint) + try? ClockIndex(textMarkers: ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]).showBlueprint(showBlueprint) + } + 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: [".", "丑", ".", "寅", ".", "卯", ".", "辰", ".", "巳", ".", "子"]).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/RingText.swift b/Sources/Rings/RingText.swift index 4c1a5c0..b70e52d 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -103,7 +103,7 @@ public struct RingText : View { } } -func _setProperty(content: T, _ setBlock:(_ newContent: inout T) -> T) -> T { +internal func _setProperty(content: T, _ setBlock:(_ newContent: inout T) -> T) -> T { var temp = content return setBlock(&temp) } diff --git a/Tests/RingsTests/RingsTests.swift b/Tests/RingsTests/RingsTests.swift index 8c5a90f..f8214a3 100644 --- a/Tests/RingsTests/RingsTests.swift +++ b/Tests/RingsTests/RingsTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import Rings @testable import CoreGraphicsExtension From 75e25c45a8dedfc4d96d6e6eaf32df2aafaa20a3 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 29 Apr 2021 14:44:04 +0800 Subject: [PATCH 22/60] publish initializers --- Sources/Rings/ClockIndex.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift index 93052db..9eb9f8c 100644 --- a/Sources/Rings/ClockIndex.swift +++ b/Sources/Rings/ClockIndex.swift @@ -27,7 +27,7 @@ public struct ClockIndex: View { private var radius: CGFloat = defaultRadius private var showBlueprint: Bool = false - init(textMarkers: [String] = defaultTextMarker, surface: Surface? = nil) throws { + public init(textMarkers: [String] = defaultTextMarker, surface: Surface? = nil) throws { guard textMarkers.count == 12 else { throw ClockIndexError.outOfBounds("The number of markers whould be 12.") } @@ -36,7 +36,7 @@ public struct ClockIndex: View { }) } - init(_ markers: [AnyView], surface: Surface? = nil) throws { + public init(_ markers: [AnyView], surface: Surface? = nil) throws { guard markers.count == 12 else { throw ClockIndexError.outOfBounds("The number of markers whould be 12.") } From 2a67631e6bf3fafb74ee871b366c3d3b744634e0 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 29 Apr 2021 14:49:12 +0800 Subject: [PATCH 23/60] publish default values --- Sources/Rings/ClockIndex.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift index 9eb9f8c..58d8708 100644 --- a/Sources/Rings/ClockIndex.swift +++ b/Sources/Rings/ClockIndex.swift @@ -9,17 +9,17 @@ import SwiftUI import CoreGraphicsExtension import CommonExts -enum ClockIndexError: Error { +public enum ClockIndexError: Error { case outOfBounds(String) } -let defaultTextMarker = ["1","2","3","4","5","6","7","8","9","10","11","12"] +public let defaultTextMarker = ["1","2","3","4","5","6","7","8","9","10","11","12"] -let defaultMarkers: [AnyView] = defaultTextMarker.map { num -> AnyView in +public let defaultMarkers: [AnyView] = defaultTextMarker.map { num -> AnyView in AnyView(Text(num)) } -let defaultRadius: CGFloat = 80.0 +public let defaultRadius: CGFloat = 80.0 public struct ClockIndex: View { From df442920ef10c477fa2f9e8bcfd98fc33113a538 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 29 Apr 2021 18:26:18 +0800 Subject: [PATCH 24/60] Add customizable hour/minute index --- Sources/Rings/ClockIndex.swift | 76 +++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift index 58d8708..952cf56 100644 --- a/Sources/Rings/ClockIndex.swift +++ b/Sources/Rings/ClockIndex.swift @@ -21,12 +21,37 @@ public let defaultMarkers: [AnyView] = defaultTextMarker.map { num -> AnyView in 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: CGFloat) -> Self { + StrokeStyle(lineWidth: self.lineWidth, lineCap: self.lineCap, lineJoin: self.lineJoin, miterLimit: self.miterLimit, dash: [0, CGFloat.pi*radius/6], dashPhase: self.dashPhase) + } + + func minuteStyle(with radius: CGFloat) -> Self { + StrokeStyle(lineWidth: self.lineWidth, lineCap: self.lineCap, lineJoin: self.lineJoin, miterLimit: self.miterLimit, dash: [0, CGFloat.pi*radius/36], dashPhase: self.dashPhase) + } +} + public struct ClockIndex: View { private var hourMarkers: [AnyView] = defaultMarkers 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, surface: Surface? = nil) throws { guard textMarkers.count == 12 else { throw ClockIndexError.outOfBounds("The number of markers whould be 12.") @@ -46,6 +71,10 @@ public struct ClockIndex: View { 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 { @@ -86,19 +115,64 @@ extension ClockIndex { 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: CGFloat) -> Self { + setProperty { tmp in + tmp.hourIndexRadius = 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: CGFloat) -> Self { + setProperty { tmp in + tmp.minIndexRadius = 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 = false + @State var indexRadius: CGFloat = 60 var body: some View { VStack { Spacer(minLength: 10.0) Text("Classic Clocks") HStack { - try? ClockIndex().radius(50.0).showBlueprint(showBlueprint) + 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)") + } + Toggle("Show Index", isOn: $showIndex) + } try? ClockIndex(textMarkers: ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]).showBlueprint(showBlueprint) } + Spacer(minLength: 5.0) + Divider() Toggle("Blue Print", isOn: $showBlueprint) Spacer(minLength: 10.0) } From 51d2614575577c4994d37c111a507781ba2c531d Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Fri, 30 Apr 2021 14:25:30 +0800 Subject: [PATCH 25/60] 1. Remove Surface to make ClockIndex has simpler responsibility. 2. Add textColor as init argument. 3. Update preview to allow user to modify the radius of hour index and min index --- Sources/Rings/ClockIndex.swift | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift index 952cf56..0eb5868 100644 --- a/Sources/Rings/ClockIndex.swift +++ b/Sources/Rings/ClockIndex.swift @@ -35,9 +35,10 @@ public extension StrokeStyle { } } -public struct ClockIndex: View { +public struct ClockIndex: View { private var hourMarkers: [AnyView] = defaultMarkers + private var textColor: Color = .white private var radius: CGFloat = defaultRadius private var showBlueprint: Bool = false @@ -52,16 +53,17 @@ public struct ClockIndex: View { private var minIndexRadius: CGFloat = defaultRadius + 10.0 private var minIndexColor: Color = .white - public init(textMarkers: [String] = defaultTextMarker, surface: Surface? = nil) throws { + 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)) + AnyView(Text(text).foregroundColor(textColor)) }) } - public init(_ markers: [AnyView], surface: Surface? = nil) throws { + public init(_ markers: [AnyView]) throws { guard markers.count == 12 else { throw ClockIndexError.outOfBounds("The number of markers whould be 12.") } @@ -80,7 +82,6 @@ public struct ClockIndex: View { Sizing { hourMarkers[index].if(showBlueprint) { content in content.border(Color.blue, width: 1) - } }.offset(x: polarPt.cgpoint.x, y: polarPt.cgpoint.y) } @@ -152,25 +153,34 @@ extension ClockIndex { //Previews struct ClockPreviewClassic : View { @State var showBlueprint: Bool = false - @State var showIndex: 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) + 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)") } - Toggle("Show Index", isOn: $showIndex) } - try? ClockIndex(textMarkers: ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"]).showBlueprint(showBlueprint) + 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) @@ -186,9 +196,9 @@ struct ClockPreviewEarthlyBranches : View { Spacer(minLength: 10.0) Text("Earchly Branches Clocks") HStack { - try? ClockIndex(textMarkers: [".", "丑", ".", "寅", ".", "卯", ".", "辰", ".", "巳", ".", "子"]).showBlueprint(showBlueprint) + try? ClockIndex(textMarkers: [".", "丑", ".", "寅", ".", "卯", ".", "辰", ".", "巳", ".", "子"], color: .red).showBlueprint(showBlueprint) - try? ClockIndex(textMarkers: [".", "未", ".", "申", ".", "酉", ".", "戌", ".", "亥", ".", "午"]).showBlueprint(showBlueprint) + try? ClockIndex(textMarkers: [".", "未", ".", "申", ".", "酉", ".", "戌", ".", "亥", ".", "午"]).showBlueprint(showBlueprint) } Toggle("Blue Print", isOn: $showBlueprint) Spacer(minLength: 10.0) From 3e93318fbeb0bf8282bfc67570125753d78a6ef9 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Fri, 30 Apr 2021 15:59:00 +0800 Subject: [PATCH 26/60] Update README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 9b653b5..d30435a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ It includes following controls: * RingText +* ClockIndex * ArchimedeanSpiralText (In-progress) * SphericText (In-progress) * Knob (In-planning) @@ -32,3 +33,25 @@ It includes following controls: // Show blueprint of each text (For debug purpose) RingText(radius: 40.0, text: "1234567890").showBlueprint(true) ``` +## ClockIndex + +### Preview +![ClockIndex Demo Classic](https://user-images.githubusercontent.com/1284944/116664495-26d6d200-a9cb-11eb-906c-7ffe659dcfbc.gif) + +earchly_clock_demo + +### 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) +``` From 3ec78a7eaac660dc3033e7f353cbd120b4e05584 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Fri, 30 Apr 2021 15:59:37 +0800 Subject: [PATCH 27/60] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d30435a..4b0c3b8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ It includes following controls: earchly_clock_demo -### Usage +### Usage: ```Swift // Default clock index with radius 50.0 From 4dfe206562a746992b011019a8a2ba92016195fe Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 5 May 2021 14:27:19 +0800 Subject: [PATCH 28/60] Add HandAiguille to easy create hand aiguille. 1. Link time data to hand aiguille to change its rotation effect 2. Allow developer to create its own hand aiguille style by simply combine different views. 3. Add factory function to create apple watch style hand aiguille. --- Sources/Rings/HandAiguille.swift | 166 +++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 Sources/Rings/HandAiguille.swift diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift new file mode 100644 index 0000000..4bc661e --- /dev/null +++ b/Sources/Rings/HandAiguille.swift @@ -0,0 +1,166 @@ +// +// 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: Double + @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 + + public init(size: CGSize, offset: CGFloat, time: Binding = .constant(0), unit: TimeUnit = .second, @ViewBuilder content: @escaping () -> Content) { + self.handSize = size + self.offset = 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) + } + content().frame(width: handSize.width, height: handSize.height, alignment: .center).offset(y: -yoffset).rotationEffect(angleOfTime(time)) + } + } + } + + private func angleOfTime(_ time: Double) -> Angle { + switch timeUnit { + case .hour: + let t = time.truncatingRemainder(dividingBy: 12.0) + return Angle(degrees: t * 30.0) + case .minute, .second: + let t = + time.truncatingRemainder(dividingBy: 60.0) + return Angle(degrees: t * 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 + } + + func blueprint(_ isOn:Bool = true) -> Self { + setProperty { tmp in + tmp.showBlueprint = isOn + } + } +} + +public struct HandFactory { + private let rectRatio: CGFloat = 0.2 + public func makeWatchStyleHand(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 AppleStyleHandPreview: View { + @State var emulateTime: Double = 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().makeWatchStyleHand(size: CGSize(width: 5.0, height: 80.0),timeProvider: $emulateTime, unit: .hour).frame(width: 120, height: 120, alignment: .center) + HandFactory().makeWatchStyleHand(size: CGSize(width: 4.0, height: 60.0),timeProvider: $emulateTime, unit: .minute).frame(width: 120, height: 120, alignment: .center) + } + } + HStack { + Spacer(minLength: 10) + Text("time:") + Slider(value: $emulateTime, in: 0.0...60.0) + Text("\(emulateTime)") + Spacer(minLength: 10) + } + HStack { + Spacer(minLength: 10) + Text("offset:") + Slider(value: $offset, in: 0.0...20.0, step: 0.5) + Text("\(offset)") + Spacer(minLength: 10) + } + HStack { + Spacer(minLength: 10) + Picker("Time Unit", selection: $unit) { + Text("hour").tag(TimeUnit.hour) + Text("minute").tag(TimeUnit.minute) + Text("second").tag(TimeUnit.second) + }.pickerStyle(SegmentedPickerStyle()) + 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) + } +} From bd183fba5d28d2bdb9d685a71913bd03c9fdbf89 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 5 May 2021 15:52:48 +0800 Subject: [PATCH 29/60] Update README.md Add document for HandAiguille --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 4b0c3b8..8796415 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,31 @@ It includes following controls: // Show/Hide hour index and minutes track ClockIndex().showIndex(shouldShowIndex) ``` + +## 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) +``` + + From a981a49d3b602aed401671cf3dde28b19c68375e Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 5 May 2021 15:58:15 +0800 Subject: [PATCH 30/60] 1. Add handBackground for a. Simply show range of Hand Aiguille, when there has no content. b. Provide more flexibility for designer. 2. Refactor HandFactory, provide standard instance. --- Sources/Rings/HandAiguille.swift | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift index 4bc661e..43eb7f7 100644 --- a/Sources/Rings/HandAiguille.swift +++ b/Sources/Rings/HandAiguille.swift @@ -35,8 +35,9 @@ public struct HandAiguille : View { private var timeUnit: TimeUnit private var showBlueprint: Bool = false + private var handBackground: AnyView = AnyView(Color.clear) - public init(size: CGSize, offset: CGFloat, time: Binding = .constant(0), unit: TimeUnit = .second, @ViewBuilder content: @escaping () -> Content) { + public init(size: CGSize = CGSize(width: 3.0, height: 50.0), offset: CGFloat = 1.5, time: Binding = .constant(0), unit: TimeUnit = .second, @ViewBuilder content: @escaping () -> Content) { self.handSize = size self.offset = offset _time = time @@ -59,7 +60,11 @@ public struct HandAiguille : View { }.if(showBlueprint) { p in p.stroke(Color.blue) } - content().frame(width: handSize.width, height: handSize.height, alignment: .center).offset(y: -yoffset).rotationEffect(angleOfTime(time)) + 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)) + } } } } @@ -91,11 +96,18 @@ extension HandAiguille { tmp.showBlueprint = isOn } } + + 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 makeWatchStyleHand(size: CGSize = CGSize(width: 4.0, height: 60.0), timeProvider: Binding, unit: TimeUnit = .second) -> some View { + 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) @@ -127,8 +139,8 @@ struct AppleStyleHandPreview: View { }.blueprint(showBlueprint).frame(width: 100, height: 100, alignment: .center) } ZStack { - HandFactory().makeWatchStyleHand(size: CGSize(width: 5.0, height: 80.0),timeProvider: $emulateTime, unit: .hour).frame(width: 120, height: 120, alignment: .center) - HandFactory().makeWatchStyleHand(size: CGSize(width: 4.0, height: 60.0),timeProvider: $emulateTime, unit: .minute).frame(width: 120, height: 120, alignment: .center) + 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 { From cb8c78dc9f0cbe911a2cf90459afa78de1fa04d3 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 5 May 2021 16:02:25 +0800 Subject: [PATCH 31/60] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8796415..071924a 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ It includes following controls: * RingText * ClockIndex +* HandAiguille * ArchimedeanSpiralText (In-progress) * SphericText (In-progress) * Knob (In-planning) From 70d7aface9bd17d76b903623f7943f582ca6495b Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 6 May 2021 17:28:05 +0800 Subject: [PATCH 32/60] Update preview to avoid build fail on watchOS --- Sources/Rings/HandAiguille.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift index 43eb7f7..d7ed3be 100644 --- a/Sources/Rings/HandAiguille.swift +++ b/Sources/Rings/HandAiguille.swift @@ -159,11 +159,19 @@ struct AppleStyleHandPreview: View { } 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) From 9003fd6fb04a7bf9065d21b366443327e9583bcb Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 6 May 2021 17:31:15 +0800 Subject: [PATCH 33/60] publish extensions to setup HandAiguille --- Sources/Rings/HandAiguille.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift index d7ed3be..95720b9 100644 --- a/Sources/Rings/HandAiguille.swift +++ b/Sources/Rings/HandAiguille.swift @@ -91,13 +91,13 @@ extension HandAiguille { return result } - func blueprint(_ isOn:Bool = true) -> Self { + public func blueprint(_ isOn:Bool = true) -> Self { setProperty { tmp in tmp.showBlueprint = isOn } } - func handBackground(_ background: Background) -> Self where Background : View { + public func handBackground(_ background: Background) -> Self where Background : View { setProperty{ tmp in tmp.handBackground = AnyView(background) } From a879d568538c5a8fe0958a92e72e7d8d038c2bfc Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 6 May 2021 17:25:48 +0800 Subject: [PATCH 34/60] [Refactor] Replace Double with generic type of BinaryFloatingPoint to reduce casting. --- Sources/Rings/RingText.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Rings/RingText.swift b/Sources/Rings/RingText.swift index b70e52d..9e592b7 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -44,9 +44,9 @@ public struct RingText : View { @State private var sizes: [CGSize] = [] private var showBlueprint: Bool = false - - public init(radius: Double, words: [String], color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero, end: CGAngle? = nil) { - self.radius = radius + + 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 @@ -68,7 +68,7 @@ public struct RingText : View { textPoints = _createTextPoints() } - public init(radius: Double, text: String, color: Color = Color.white, upsideDown: Bool = false, reversed: Bool = false, begin: CGAngle = CGAngle.zero) { + 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) } @@ -218,7 +218,7 @@ struct RingTextPreviewWrapper: View { } ZStack { RingText(radius: 40.0, words: ["a23", "b56", "c89"], reversed: true).showBlueprint(blueprint) - RingText(radius: 80, words: ["1234567890"]).showBlueprint(blueprint) + RingText(radius: 80.0, words: ["1234567890"]).showBlueprint(blueprint) } VStack { ZStack { From e9ed7982fe88e518dead22e8f1552e477ee941c8 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 6 May 2021 17:51:57 +0800 Subject: [PATCH 35/60] [Refactor] Replace Double with generic type of BinaryFloatingPoint to reduce casting for HandAiguille --- Sources/Rings/HandAiguille.swift | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift index 95720b9..0ce2dc8 100644 --- a/Sources/Rings/HandAiguille.swift +++ b/Sources/Rings/HandAiguille.swift @@ -24,9 +24,9 @@ extension TimeUnit { } } -public struct HandAiguille : View { +public struct HandAiguille : View { - @Binding private var time: Double + @Binding private var time: T @State private var angle: Angle = Angle() private let content: () -> Content @@ -37,9 +37,9 @@ public struct HandAiguille : View { private var showBlueprint: Bool = false private var handBackground: AnyView = AnyView(Color.clear) - public init(size: CGSize = CGSize(width: 3.0, height: 50.0), offset: CGFloat = 1.5, time: Binding = .constant(0), unit: TimeUnit = .second, @ViewBuilder content: @escaping () -> Content) { + 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 = offset + self.offset = CGFloat(offset) _time = time timeUnit = unit self.content = content @@ -69,15 +69,16 @@ public struct HandAiguille : View { } } - private func angleOfTime(_ time: Double) -> Angle { + private func angleOfTime(_ time: T) -> Angle { + let t = Double(time) switch timeUnit { case .hour: - let t = time.truncatingRemainder(dividingBy: 12.0) - return Angle(degrees: t * 30.0) + let r = t.truncatingRemainder(dividingBy: 12.0) + return Angle(degrees: r * 30.0) case .minute, .second: - let t = - time.truncatingRemainder(dividingBy: 60.0) - return Angle(degrees: t * 6.0) + let r = + t.truncatingRemainder(dividingBy: 60.0) + return Angle(degrees: r * 6.0) } } } @@ -107,7 +108,7 @@ extension HandAiguille { 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 { + 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) @@ -119,7 +120,7 @@ public struct HandFactory { } struct AppleStyleHandPreview: View { - @State var emulateTime: Double = 0.0 + @State var emulateTime: CGFloat = 0.0 @State var showBlueprint: Bool = false @State var offset: CGFloat = 1.5 @State var unit: TimeUnit = .second From 681895c16f9cc15175f930c19639460c4d3c71f3 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 6 May 2021 17:55:47 +0800 Subject: [PATCH 36/60] [Refactor] Replace Double with generic type of BinaryFloatingPoint to reduce casting for ClockIndex --- Sources/Rings/ClockIndex.swift | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/Rings/ClockIndex.swift b/Sources/Rings/ClockIndex.swift index 0eb5868..0b4be9f 100644 --- a/Sources/Rings/ClockIndex.swift +++ b/Sources/Rings/ClockIndex.swift @@ -26,12 +26,12 @@ public let classicHourStyle: StrokeStyle = StrokeStyle(lineWidth: 5.0, lineCap: 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: CGFloat) -> Self { - StrokeStyle(lineWidth: self.lineWidth, lineCap: self.lineCap, lineJoin: self.lineJoin, miterLimit: self.miterLimit, dash: [0, CGFloat.pi*radius/6], dashPhase: self.dashPhase) + 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: CGFloat) -> Self { - StrokeStyle(lineWidth: self.lineWidth, lineCap: self.lineCap, lineJoin: self.lineJoin, miterLimit: self.miterLimit, dash: [0, CGFloat.pi*radius/36], 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) } } @@ -106,9 +106,9 @@ extension ClockIndex { return result } - public func radius(_ r: CGFloat) -> Self { + public func radius(_ r: T) -> Self { setProperty { tmp in - tmp.radius = r + tmp.radius = CGFloat(r) } } public func showBlueprint(_ show: Bool) -> Self { @@ -124,9 +124,9 @@ extension ClockIndex { } } - public func hourIndexRadius(_ r: CGFloat) -> Self { + public func hourIndexRadius(_ r: T) -> Self { setProperty { tmp in - tmp.hourIndexRadius = r + tmp.hourIndexRadius = CGFloat(r) } } @@ -137,9 +137,9 @@ extension ClockIndex { } } - public func minIndexRadius(_ r: CGFloat) -> Self { + public func minIndexRadius(_ r: T) -> Self { setProperty { tmp in - tmp.minIndexRadius = r + tmp.minIndexRadius = CGFloat(r) } } From 40f149ce3ba7e9c379fcb68561e91adce8fe0827 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Fri, 7 May 2021 19:28:29 +0800 Subject: [PATCH 37/60] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE 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. From 6e47cc9dada53865bc8d3eceac4767acb6f3efd4 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Mon, 10 May 2021 16:40:28 +0800 Subject: [PATCH 38/60] [Refactor] 1. Extract internal function _setProperty to independent file. 2. Create protocol Adjustable to implement the logic to update members in constant structure. --- Sources/Rings/InternalUtilities.swift | 26 ++++++++++++++++++++++++++ Sources/Rings/RingText.swift | 5 ----- 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 Sources/Rings/InternalUtilities.swift 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.swift b/Sources/Rings/RingText.swift index 9e592b7..4e64e08 100644 --- a/Sources/Rings/RingText.swift +++ b/Sources/Rings/RingText.swift @@ -103,11 +103,6 @@ public struct RingText : View { } } -internal func _setProperty(content: T, _ setBlock:(_ newContent: inout T) -> T) -> T { - var temp = content - return setBlock(&temp) -} - extension RingText { func setProperty(_ setBlock: (_ text: inout Self) -> Void) -> Self { let result = _setProperty(content: self) { (tmp :inout Self) in From ee1b81f31d6482102703f1a68ab22e202b15cdde Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Mon, 10 May 2021 16:45:36 +0800 Subject: [PATCH 39/60] Import ArchimedeanSpiral to calculate ArchimedeanSpiral --- Package.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index eb918b4..7911df9 100644 --- a/Package.swift +++ b/Package.swift @@ -18,7 +18,8 @@ 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: "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. @@ -27,7 +28,7 @@ let package = Package( dependencies: []), .target( name: "Rings", - dependencies: ["CoreGraphicsExtension", "CommonExts"]), + dependencies: ["CoreGraphicsExtension", "CommonExts", "ArchimedeanSpiral"]), .testTarget( name: "RingsTests", dependencies: ["Rings", From 6efda8ed4465537a992ad7128f89015bf03606d7 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Mon, 10 May 2021 16:46:30 +0800 Subject: [PATCH 40/60] Implement ArchimedeanSpiralPath to draw path along an archimedean spiral --- Sources/Rings/ArchimedeanSpiralPath.swift | 120 ++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 Sources/Rings/ArchimedeanSpiralPath.swift 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() + } +} From fbeeed764758c1941aee67a43ba950aa17b49b1c Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Mon, 10 May 2021 16:47:20 +0800 Subject: [PATCH 41/60] Implement ArchimedeanSpiralText to layout string along a Archimedean spiral. --- Sources/Rings/ArchimedeanSpiralText.swift | 128 ++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 Sources/Rings/ArchimedeanSpiralText.swift diff --git a/Sources/Rings/ArchimedeanSpiralText.swift b/Sources/Rings/ArchimedeanSpiralText.swift new file mode 100644 index 0000000..0e62cfc --- /dev/null +++ b/Sources/Rings/ArchimedeanSpiralText.swift @@ -0,0 +1,128 @@ +// +// ArchimedeanSpiralText.swift +// Rings +// +// Created by Chen Hai Teng on 3/5/21. +// + +import SwiftUI +import ArchimedeanSpiral +import CoreGraphicsExtension + +public struct ArchimedeanSpiralText: View { + private var radiusSpacing: Double + private var innerRadius: Double + private var gap: Double + 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.radians(CGFloat.pi*0.5), 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) + Text(String(element)) + .rotationEffect(self.textPoints[offset].cgangle.toAngle()) + .offset(x: textPt.x, y: textPt.y) + .font(.system(size: 13)) + + } + }.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() + } + } +} + +struct ArchimedeanSpiralTextDemo : View { + private let demoText = "1234567890abcdefgABCDEFG♩♪♫♬" + @State var radiusSpacing: Double = 10.0 + @State var innerR: Double = 5.0 + @State var gap: Double = 25.0 + @State var textLength: Double = 10.0 + 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) + 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)) + + Slider(value: $innerR, in: 0.0...30.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) { + 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) { + Text("Gap:\(gap)") + }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0)) + } + } +} + +struct ArchimedeanSpiralText_Previews: PreviewProvider { + static var previews: some View { + ArchimedeanSpiralTextDemo() + } +} From b4d5a7837b8a5322790b6f11075f2a87dfee44af Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Mon, 10 May 2021 17:42:43 +0800 Subject: [PATCH 42/60] Add text direction setting on Archimedean Spiral Text --- Sources/Rings/ArchimedeanSpiralText.swift | 60 ++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/Sources/Rings/ArchimedeanSpiralText.swift b/Sources/Rings/ArchimedeanSpiralText.swift index 0e62cfc..cd2ff79 100644 --- a/Sources/Rings/ArchimedeanSpiralText.swift +++ b/Sources/Rings/ArchimedeanSpiralText.swift @@ -9,10 +9,30 @@ 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 startAngle: CGAngle = CGAngle.zero + + private var textDirection: TextDirection = .Top + private var text: String { didSet { chars = Array(text.enumerated()) @@ -27,18 +47,19 @@ public struct ArchimedeanSpiralText: View { private var textPoints: [CGPolarPoint] = [] - public init(_ innerRadius: Double = 12.0, spacing: Double = 10.0, gap: Double = 5.0, text: String = "") { + public init(_ innerRadius: Double = 12.0, spacing: Double = 10.0, gap: Double = 5.0, text: String = "", angle: CGAngle = CGAngle.zero) { self.radiusSpacing = spacing self.innerRadius = innerRadius self.gap = gap self.text = text self.chars = Array(text.enumerated()) + self.startAngle = angle updateTextPoints() } private mutating func updateTextPoints() { let spiral = ArchimedeanSpiral(innerRadius: self.innerRadius, radiusSpacing: self.radiusSpacing, spacing: self.gap) - textPoints = spiral.equidistantPoints(start: CGAngle.radians(CGFloat.pi*0.5), num: self.chars.count) + textPoints = spiral.equidistantPoints(start: startAngle, num: self.chars.count) } public var body: some View { @@ -47,8 +68,9 @@ public struct ArchimedeanSpiralText: View { 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(self.textPoints[offset].cgangle.toAngle()) + .rotationEffect(rotation) .offset(x: textPt.x, y: textPt.y) .font(.system(size: 13)) @@ -87,27 +109,53 @@ extension ArchimedeanSpiralText: Adjustable { tmp.updateTextPoints() } } + + public func textDirection(_ direction: TextDirection) -> Self { + setProperty { tmp in + tmp.textDirection = direction + } + } } struct ArchimedeanSpiralTextDemo : View { private let demoText = "1234567890abcdefgABCDEFG♩♪♫♬" @State var radiusSpacing: Double = 10.0 - @State var innerR: Double = 5.0 + @State var innerR: Double = 25.0 @State var gap: Double = 25.0 @State var textLength: Double = 10.0 + @State var angle: Double = 90.0 + @State var direction: TextDirection = TextDirection.Top var body: some View { VStack { let enabled = String(demoText.prefix(Int(textLength))) let disabled = String(demoText.suffix(demoText.count - Int(textLength))) - ArchimedeanSpiralText() + ArchimedeanSpiralText(angle: CGAngle.degrees(angle)) .gap(gap) .innerRadius(innerR) .spacing(radiusSpacing) .text(enabled) + .textDirection(direction) 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)) - + #if os(watchOS) + Picker("direct to center", selection: $direction) { + Text("up").tag(TextDirection.UpToCenter) + Text("down").tag(TextDirection.DownToCenter) + Text("right").tag(TextDirection.RightToCenter) + Text("left").tag(TextDirection.LeftToCenter) + } + #else + Picker("head to center", selection: $direction) { + Text("top").tag(TextDirection.Top) + Text("bottom").tag(TextDirection.Bottom) + Text("right").tag(TextDirection.Right) + Text("left").tag(TextDirection.Left) + }.pickerStyle(SegmentedPickerStyle()) + #endif + Slider(value: $angle, in: 0.0...360.0) { + Text("start at: \(angle)") + }.padding(EdgeInsets(top: 0, leading: 10.0, bottom: 0, trailing: 10.0)) Slider(value: $innerR, in: 0.0...30.0) { Text("Inner Radius: \(innerR)") }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0)) From daee676e8b50eafda05f7de8819636d8d6e6dd4a Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Tue, 11 May 2021 11:12:59 +0800 Subject: [PATCH 43/60] Update enum name on watchOS --- Sources/Rings/ArchimedeanSpiralText.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Rings/ArchimedeanSpiralText.swift b/Sources/Rings/ArchimedeanSpiralText.swift index cd2ff79..d27ac5c 100644 --- a/Sources/Rings/ArchimedeanSpiralText.swift +++ b/Sources/Rings/ArchimedeanSpiralText.swift @@ -140,10 +140,10 @@ struct ArchimedeanSpiralTextDemo : View { }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0)) #if os(watchOS) Picker("direct to center", selection: $direction) { - Text("up").tag(TextDirection.UpToCenter) - Text("down").tag(TextDirection.DownToCenter) - Text("right").tag(TextDirection.RightToCenter) - Text("left").tag(TextDirection.LeftToCenter) + Text("top").tag(TextDirection.Top) + Text("bottom").tag(TextDirection.Bottom) + Text("right").tag(TextDirection.Right) + Text("left").tag(TextDirection.Left) } #else Picker("head to center", selection: $direction) { From a03cd9315c97f84a33dc3b1b2b08ddc8957c8cb0 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 12 May 2021 17:06:55 +0800 Subject: [PATCH 44/60] 1. Remove start angle, since there already has rotationEffect. 2. Add font, textColor support 3. Update preview to show adjustable font and text color --- Sources/Rings/ArchimedeanSpiralText.swift | 138 +++++++++++++++------- 1 file changed, 98 insertions(+), 40 deletions(-) diff --git a/Sources/Rings/ArchimedeanSpiralText.swift b/Sources/Rings/ArchimedeanSpiralText.swift index d27ac5c..1240e40 100644 --- a/Sources/Rings/ArchimedeanSpiralText.swift +++ b/Sources/Rings/ArchimedeanSpiralText.swift @@ -29,9 +29,9 @@ public struct ArchimedeanSpiralText: View { private var radiusSpacing: Double private var innerRadius: Double private var gap: Double - private var startAngle: CGAngle = CGAngle.zero - private var textDirection: TextDirection = .Top + private var font = Font.system(size: 20.0) + private var textColor: Color = Color.red private var text: String { didSet { @@ -47,19 +47,18 @@ public struct ArchimedeanSpiralText: View { private var textPoints: [CGPolarPoint] = [] - public init(_ innerRadius: Double = 12.0, spacing: Double = 10.0, gap: Double = 5.0, text: String = "", angle: CGAngle = CGAngle.zero) { + 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()) - self.startAngle = angle updateTextPoints() } private mutating func updateTextPoints() { let spiral = ArchimedeanSpiral(innerRadius: self.innerRadius, radiusSpacing: self.radiusSpacing, spacing: self.gap) - textPoints = spiral.equidistantPoints(start: startAngle, num: self.chars.count) + textPoints = spiral.equidistantPoints(start: CGAngle.zero, num: self.chars.count) } public var body: some View { @@ -72,7 +71,8 @@ public struct ArchimedeanSpiralText: View { Text(String(element)) .rotationEffect(rotation) .offset(x: textPt.x, y: textPt.y) - .font(.system(size: 13)) + .foregroundColor(textColor) + .font(font) } }.frame(width: geo.size.width, height: geo.size.height, alignment: .center) @@ -115,62 +115,120 @@ extension ArchimedeanSpiralText: Adjustable { 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 { +public struct ArchimedeanSpiralTextDemo : View { private let demoText = "1234567890abcdefgABCDEFG♩♪♫♬" - @State var radiusSpacing: Double = 10.0 + @State var radiusSpacing: Double = 20.0 @State var innerR: Double = 25.0 @State var gap: Double = 25.0 - @State var textLength: Double = 10.0 - @State var angle: Double = 90.0 + @State var textLength: Double = 15.0 @State var direction: TextDirection = TextDirection.Top - var body: some View { + @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(angle: CGAngle.degrees(angle)) + 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)) - #if os(watchOS) - Picker("direct to center", selection: $direction) { - Text("top").tag(TextDirection.Top) - Text("bottom").tag(TextDirection.Bottom) - Text("right").tag(TextDirection.Right) - Text("left").tag(TextDirection.Left) + 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)) + } } - #else - Picker("head to center", selection: $direction) { - Text("top").tag(TextDirection.Top) - Text("bottom").tag(TextDirection.Bottom) - Text("right").tag(TextDirection.Right) - Text("left").tag(TextDirection.Left) - }.pickerStyle(SegmentedPickerStyle()) - #endif - Slider(value: $angle, in: 0.0...360.0) { - Text("start at: \(angle)") - }.padding(EdgeInsets(top: 0, leading: 10.0, bottom: 0, trailing: 10.0)) - Slider(value: $innerR, in: 0.0...30.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) { - 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) { - Text("Gap:\(gap)") - }.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0)) } } } -struct ArchimedeanSpiralText_Previews: PreviewProvider { - static var previews: some View { +public struct ArchimedeanSpiralText_Previews: PreviewProvider { + public static var previews: some View { ArchimedeanSpiralTextDemo() } } From 1cc2567aba285e10f25471e8e5deca9e991745ed Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 12 May 2021 18:14:56 +0800 Subject: [PATCH 45/60] Update README.md --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 071924a..b480648 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It includes following controls: * RingText * ClockIndex * HandAiguille -* ArchimedeanSpiralText (In-progress) +* ArchimedeanSpiralText * SphericText (In-progress) * Knob (In-planning) @@ -63,7 +63,7 @@ It includes following controls: https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8e82-d77d9569dcca.mov ### Usage: -```Swift +```swift // Create empty hand aiguille with default size, and set the hand aiguille background red HandAiguille() { }.handBackground(Color.red) @@ -83,4 +83,27 @@ https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8 HandFactory.standard.makeAppleWatchStyleHand(time: $secsProvider) ``` +## ArchimedeanSpiralText + +### Preview: +![ArchimedeanSpiralTextDemo](https://user-images.githubusercontent.com/1284944/117950922-3ef10e80-b346-11eb-9da1-50b0f87990a2.gif) + +### 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 +``` From d55dfb30282319e86afa61495295e6e247770d57 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 12 May 2021 18:22:57 +0800 Subject: [PATCH 46/60] Extract document from README.md --- Sources/Rings/ArchimedeanSpiralText.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Sources/Rings/ArchimedeanSpiralText.md 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: + +![ArchimedeanSpiralTextDemo](https://user-images.githubusercontent.com/1284944/117950922-3ef10e80-b346-11eb-9da1-50b0f87990a2.gif) + +### 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 +``` From 403cea3943c8254c3bdfd8e0a0a5160bdf09c0fe Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Wed, 12 May 2021 18:27:40 +0800 Subject: [PATCH 47/60] Update README.md --- README.md | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b480648..83c9a8f 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ **Rings** is a collection of controls which have similar shapes of ring, circle... It includes following controls: -* RingText -* ClockIndex -* HandAiguille -* ArchimedeanSpiralText +* **RingText** +* **ClockIndex** +* **HandAiguille** +* **ArchimedeanSpiralText** * SphericText (In-progress) * Knob (In-planning) @@ -83,27 +83,9 @@ https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8 HandFactory.standard.makeAppleWatchStyleHand(time: $secsProvider) ``` -## ArchimedeanSpiralText +## ![ArchimedeanSpiralText](Sources/Rings/ArchimedeanSpiralText.md) ### Preview: - ![ArchimedeanSpiralTextDemo](https://user-images.githubusercontent.com/1284944/117950922-3ef10e80-b346-11eb-9da1-50b0f87990a2.gif) -### 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 -``` +### ![Document](Sources/Rings/ArchimedeanSpiralText.md) From d68e999cf4528ee24210fe8d0babe94a620d8841 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 12:52:35 +0800 Subject: [PATCH 48/60] Extract ClockIndex, HandAiguille, and RingText document from READ ME --- README.md | 72 ++++++----------------------------- Sources/Rings/ClockIndex.md | 22 +++++++++++ Sources/Rings/HandAiguille.md | 25 ++++++++++++ Sources/Rings/RingText.md | 23 +++++++++++ 4 files changed, 81 insertions(+), 61 deletions(-) create mode 100644 Sources/Rings/ClockIndex.md create mode 100644 Sources/Rings/HandAiguille.md create mode 100644 Sources/Rings/RingText.md diff --git a/README.md b/README.md index 83c9a8f..8c3d832 100644 --- a/README.md +++ b/README.md @@ -12,80 +12,30 @@ It includes following controls: ## RingText -### Preview +### What it looks like ![RingDemo](https://user-images.githubusercontent.com/1284944/115984682-fb26a700-a5da-11eb-8a59-a1554ec41bdf.gif) -### 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) -``` +### ![How to use it](Sources/Rings/RingText.md) + ## ClockIndex -### Preview +### What it looks like ![ClockIndex Demo Classic](https://user-images.githubusercontent.com/1284944/116664495-26d6d200-a9cb-11eb-906c-7ffe659dcfbc.gif) earchly_clock_demo -### 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) -``` +### ![How to use it](Sources/Rings/ClockIndex.md) ## 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) -``` +### What it looks like: + +### ![How to use it](Sources/Rings/HandAiguille.md) + ## ![ArchimedeanSpiralText](Sources/Rings/ArchimedeanSpiralText.md) -### Preview: +### What it looks like: ![ArchimedeanSpiralTextDemo](https://user-images.githubusercontent.com/1284944/117950922-3ef10e80-b346-11eb-9da1-50b0f87990a2.gif) -### ![Document](Sources/Rings/ArchimedeanSpiralText.md) +### ![How to use it](Sources/Rings/ArchimedeanSpiralText.md) 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 + +![ClockIndex Demo Classic](https://user-images.githubusercontent.com/1284944/116664495-26d6d200-a9cb-11eb-906c-7ffe659dcfbc.gif) + +earchly_clock_demo + +### 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/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/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 + +![RingDemo](https://user-images.githubusercontent.com/1284944/115984682-fb26a700-a5da-11eb-8a59-a1554ec41bdf.gif) + +### 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) +``` From 393eadaf777737f1f8473f36f138189116ff0270 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:13:36 +0800 Subject: [PATCH 49/60] This is a test commit to verified ssh setup --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8c3d832..25df3a5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ It includes following controls: ### ![How to use it](Sources/Rings/HandAiguille.md) - ## ![ArchimedeanSpiralText](Sources/Rings/ArchimedeanSpiralText.md) ### What it looks like: From 101a29fcfea7f45af73bc337c6c4a2cf2b24be15 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:19:30 +0800 Subject: [PATCH 50/60] test commit for ssh setup --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25df3a5..95940b0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Rings** is a collection of controls which have similar shapes of ring, circle... It includes following controls: -* **RingText** +* **[RingText](#ringtext)** * **ClockIndex** * **HandAiguille** * **ArchimedeanSpiralText** From 1493f5aeececc61e6e9e598b8a48471cdb254152 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:23:41 +0800 Subject: [PATCH 51/60] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 95940b0..2a38391 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ It includes following controls: ## HandAiguille ### What it looks like: +https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8e82-d77d9569dcca.mov ### ![How to use it](Sources/Rings/HandAiguille.md) From 9ff83b22b5e3bb51b66250e53e0c009beea6578c Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:24:58 +0800 Subject: [PATCH 52/60] add link in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a38391..adb1124 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ It includes following controls: * **[RingText](#ringtext)** -* **ClockIndex** +* **[ClockIndex](#clockindex)** * **HandAiguille** * **ArchimedeanSpiralText** * SphericText (In-progress) From 46aab35a8d4c78b6ec8177117266c049137f8798 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:41:18 +0800 Subject: [PATCH 53/60] update link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index adb1124..9f31c10 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It includes following controls: * **[RingText](#ringtext)** * **[ClockIndex](#clockindex)** -* **HandAiguille** +* **[HandAiguille](#gandaiguille)** * **ArchimedeanSpiralText** * SphericText (In-progress) * Knob (In-planning) From c38c0a6ae02ba24b2f90acf3d21bd9cbbf7aa335 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:44:24 +0800 Subject: [PATCH 54/60] [Test signed commit] Update Readme link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f31c10..50f6c7b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It includes following controls: * **[RingText](#ringtext)** * **[ClockIndex](#clockindex)** * **[HandAiguille](#gandaiguille)** -* **ArchimedeanSpiralText** +* **[ArchimedeanSpiralText](#archimedeanspiraltext)** * SphericText (In-progress) * Knob (In-planning) From 227c117d07db381e34d79b89c52c6d53c9c3cde0 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 13:51:04 +0800 Subject: [PATCH 55/60] [Signed commit test] Update document --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 50f6c7b..158fd67 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,15 @@ **Rings** is a collection of controls which have similar shapes of ring, circle... It includes following controls: + * **[RingText](#ringtext)** * **[ClockIndex](#clockindex)** * **[HandAiguille](#gandaiguille)** * **[ArchimedeanSpiralText](#archimedeanspiraltext)** -* SphericText (In-progress) -* Knob (In-planning) + +and following functions are in progress: +* SphericText +* Knob ## RingText From 6517beb0b984b12910711b46b1bd67d5747fcafb Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 14:04:58 +0800 Subject: [PATCH 56/60] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 158fd67..1eda9e2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It includes following controls: * **[RingText](#ringtext)** * **[ClockIndex](#clockindex)** -* **[HandAiguille](#gandaiguille)** +* **[HandAiguille](#handaiguille)** * **[ArchimedeanSpiralText](#archimedeanspiraltext)** and following functions are in progress: @@ -36,7 +36,7 @@ https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8 ### ![How to use it](Sources/Rings/HandAiguille.md) -## ![ArchimedeanSpiralText](Sources/Rings/ArchimedeanSpiralText.md) +## ArchimedeanSpiralText ### What it looks like: ![ArchimedeanSpiralTextDemo](https://user-images.githubusercontent.com/1284944/117950922-3ef10e80-b346-11eb-9da1-50b0f87990a2.gif) From fd9e0663519f5fe159717a5096200e1ac513a25f Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 16:34:13 +0800 Subject: [PATCH 57/60] 1. Ignore documents in SPM 2. Refine preview for HandAiguille --- Package.swift | 6 +++- Sources/Rings/ArchimedeanSpiralText.swift | 4 +-- Sources/Rings/HandAiguille.swift | 35 +++++++++++++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 7911df9..79d3d26 100644 --- a/Package.swift +++ b/Package.swift @@ -28,7 +28,11 @@ let package = Package( dependencies: []), .target( name: "Rings", - dependencies: ["CoreGraphicsExtension", "CommonExts", "ArchimedeanSpiral"]), + dependencies: ["CoreGraphicsExtension", "CommonExts", "ArchimedeanSpiral"], + exclude: ["RingText.md", + "ClockIndex.md", + "ArchimedeanSpiralText.md", + "HandAiguille.md"]), .testTarget( name: "RingsTests", dependencies: ["Rings", diff --git a/Sources/Rings/ArchimedeanSpiralText.swift b/Sources/Rings/ArchimedeanSpiralText.swift index 1240e40..d7d80fd 100644 --- a/Sources/Rings/ArchimedeanSpiralText.swift +++ b/Sources/Rings/ArchimedeanSpiralText.swift @@ -164,7 +164,7 @@ extension Picker { } } -public struct ArchimedeanSpiralTextDemo : View { +struct ArchimedeanSpiralTextDemo : View { private let demoText = "1234567890abcdefgABCDEFG♩♪♫♬" @State var radiusSpacing: Double = 20.0 @State var innerR: Double = 25.0 @@ -227,7 +227,7 @@ public struct ArchimedeanSpiralTextDemo : View { } } -public struct ArchimedeanSpiralText_Previews: PreviewProvider { +struct ArchimedeanSpiralText_Previews: PreviewProvider { public static var previews: some View { ArchimedeanSpiralTextDemo() } diff --git a/Sources/Rings/HandAiguille.swift b/Sources/Rings/HandAiguille.swift index 0ce2dc8..5d7818a 100644 --- a/Sources/Rings/HandAiguille.swift +++ b/Sources/Rings/HandAiguille.swift @@ -119,6 +119,26 @@ public struct HandFactory { } } + +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 @@ -144,18 +164,21 @@ struct AppleStyleHandPreview: View { 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) - Text("time:") - Slider(value: $emulateTime, in: 0.0...60.0) - Text("\(emulateTime)") + 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) - Text("offset:") - Slider(value: $offset, in: 0.0...20.0, step: 0.5) - Text("\(offset)") + 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 { From 1cf8f39ac6693dfad80c466ad13f86605ee30107 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 16:45:34 +0800 Subject: [PATCH 58/60] Update README.md replace demo video with gif --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1eda9e2..8d490ae 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ and following functions are in progress: ## HandAiguille ### What it looks like: -https://user-images.githubusercontent.com/1284944/117106480-83aeff80-adb2-11eb-8e82-d77d9569dcca.mov +![HandAguille](https://user-images.githubusercontent.com/1284944/118101511-47128200-b40a-11eb-870f-90ac2f2a302a.gif) ### ![How to use it](Sources/Rings/HandAiguille.md) From 4f724804f2be1f5778ffa7016ced2f8a9443baef Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 16:56:28 +0800 Subject: [PATCH 59/60] Update README.md Add badges to show Rings' status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d490ae..d2e9ff3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Rings +# Rings ![GitHub](https://img.shields.io/github/license/chenhaiteng/Rings?style=plastic) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/chenhaiteng/rings) **Rings** is a collection of controls which have similar shapes of ring, circle... From c6045d2851693f826b880eaf1fcffcc709484a69 Mon Sep 17 00:00:00 2001 From: Chen-Hai Teng Date: Thu, 13 May 2021 17:06:53 +0800 Subject: [PATCH 60/60] Update README.md Add installation and license documents --- README.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2e9ff3..74a47e3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Rings** is a collection of controls which have similar shapes of ring, circle... -It includes following controls: +It includes following controls, click to see what it looks like: * **[RingText](#ringtext)** * **[ClockIndex](#clockindex)** @@ -13,6 +13,40 @@ 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 @@ -42,3 +76,7 @@ and following functions are in progress: ![ArchimedeanSpiralTextDemo](https://user-images.githubusercontent.com/1284944/117950922-3ef10e80-b346-11eb-9da1-50b0f87990a2.gif) ### ![How to use it](Sources/Rings/ArchimedeanSpiralText.md) + +--- +# License +Rings is released under the [MIT License](LICENSE).