├── .gitignore
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── SwiftUIReplicator
│ ├── ActivityIndicator.swift
│ ├── Extensions
│ ├── CGAffineTransform+SyntaxSugar.swift
│ └── Color+Static.swift
│ ├── Indicators
│ ├── CircleBounceIndicator.swift
│ ├── CircleFedeIndicator.swift
│ ├── CircleRotateIndicator.swift
│ ├── CircleScaleIndicator.swift
│ ├── ClassicalActivityIndicator.swift
│ └── RectangleScaleIndicator.swift
│ ├── Modifiers
│ └── CirculateModifier.swift
│ ├── Replicator.swift
│ └── SwiftUIReplicatorContent.swift
└── Tests
└── SwiftUIReplicatorTests
└── SwiftUIReplicatorTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Gcc Patch
26 | /*.gcno
27 |
28 | .DS_Store
29 | IDEWorkspaceChecks.plist
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Kazuhiro Hayashi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | import PackageDescription
3 |
4 | let package = Package(
5 | name: "SwiftUIReplicator",
6 | platforms: [
7 | .iOS(.v13),
8 | .macOS(.v11)
9 | ],
10 | products: [
11 | .library(name: "SwiftUIReplicator", targets: ["SwiftUIReplicator"])
12 | ],
13 | targets: [
14 | .target(name: "SwiftUIReplicator"),
15 | .testTarget(
16 | name: "SwiftUIReplicatorTests",
17 | dependencies: ["SwiftUIReplicator"])
18 | ]
19 | )
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUIReplicator
2 |
3 | SwiftUIReplicator is a container view that creates a specified number of view copies with varying geometric, temporal, and color transformations.
4 | The specification refers to CAReplicatorLayer.
5 |
6 | You can copy a view with ```Replicator``` specifying transformation rules.
7 |
8 | # Feature
9 | - [x] Pure SwiftUI library
10 | - [x] CAReplicatorLayer like container view
11 | - [x] UIKit like loading indicator
12 |
13 | # Requirements
14 |
15 | - iOS 14.0+
16 | - Xcode 12.0+
17 | - Swift 5.3
18 |
19 | # Installation
20 | SwiftUIReplicator supports only SwiftPM.
21 |
22 | ## Swift Package Manager
23 | Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift compiler. It is in early development, but Alamofire does support its use on supported platforms.
24 |
25 | Once you have your Swift package set up, adding SwiftUIReplicator as a dependency is as easy as adding it to the dependencies value of your Package.swift.
26 |
27 | ```swift
28 | dependencies: [
29 | .package(url: "https://github.com/kazuhiro4949/SwiftUIReplicator.git", .upToNextMajor(from: "1.0.0"))
30 | ]
31 | ```
32 |
33 | # Usage
34 |
35 | You can use a this library to build complex layouts based on a single source view that is replicated with transformation rules that can affect the position, rotation color, and time.
36 |
37 |
38 | ## Replicator
39 | ```Replicator``` is the most important in SwiftUIReplicator.
40 |
41 | The following code shows a simple example: a red square is added to a replicator view with an instance count of 5. The position of each replicated instance is offset along the x axis so that it appears to the right of the previous instance. The blue and green color channels are offset so that their values reach 0 at the final instance.
42 |
43 | ```swift
44 | Replicator(
45 | Rectangle() // repeated view
46 | .fill(Color.white)
47 | .frame(width: 40, height: 40)
48 | )
49 | .repeatCount(5) // how many views
50 | .repeatTransform(.init(translationX: 42, y: 0)) // repeats the transformation
51 | .instanceBlueOffset(-0.2) // the offset added to the blue component of the color
52 | .instanceGreenOffset(-0.2) // the offset added to the green component of the color
53 | .offset(x: -84)
54 | ```
55 |
56 | The result of the code above is a row of five squares, with colors graduating from white to red, as shown in the figure.
57 |
58 |
59 |
60 |
61 |
62 | ## UseCase: Loading Indicator
63 | SwiftUIReplicator has many usecases. One of them is "Loading Indicator".
64 |
65 | SwiftUI doesn't have UIActivityIndicator like view. Instead, SwiftUIReplicator provides some loading indicators.
66 |
67 | | classical | circle bounce | circle rotation | circle scaling | rectangle scaling |
68 | |:------------:|:------------:|:------------:|:------------:|:------------:|
69 | |  |  |  |  |  |
70 |
71 | If you want to use a classical indicator, set ```ActivityIndicator``` with ```.classicalLarge``` in body.
72 |
73 | ```swift
74 | struct ContentView: View {
75 | var body: some View {
76 | ActivityIndicator(style: .classicalLarge)
77 | .accentColor(.gray)
78 | }
79 | }
80 | ```
81 |
82 | # License
83 |
84 | Copyright (c) 2021 Kazuhiro Hayashi
85 |
86 | 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:
87 |
88 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
89 |
90 | 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.
91 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/ActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActivityIndicator.swift
3 | //
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/30.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | public struct ActivityIndicator: View {
12 | public enum Style {
13 | case classicalMedium
14 | case classicalLarge
15 | case circleBounce
16 | case circleRotate
17 | case circleScale
18 | case rectangleScale
19 | }
20 |
21 | public init(style: ActivityIndicator.Style) {
22 | self.style = style
23 | }
24 |
25 | private let style: Style
26 |
27 | public var body: some View {
28 | switch style {
29 | case .classicalMedium:
30 | ClassicalActivityIndicator(style: .medium)
31 | case .classicalLarge:
32 | ClassicalActivityIndicator(style: .large)
33 | case .circleBounce:
34 | CircleBounceIndicator()
35 | case .circleRotate:
36 | CircleRotateIndicator()
37 | case .circleScale:
38 | CircleScaleIndicator()
39 | case .rectangleScale:
40 | RectangleScaleIndicator()
41 | }
42 | }
43 | }
44 |
45 | struct ActivityIndicator_Previews: PreviewProvider {
46 | static var previews: some View {
47 | ActivityIndicator(style: .classicalLarge)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Extensions/CGAffineTransform+SyntaxSugar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGAffineTransform+SyntaxSugar.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension CGAffineTransform {
12 | static func rotateWithDividing(_ number: Double) -> CGAffineTransform {
13 | CGAffineTransform(
14 | rotationAngle: CGFloat(
15 | (2.0*Double.pi)/number
16 | )
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Extensions/Color+Static.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Color+Static.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension Color {
12 | static var activityIndicator: Color {
13 | Color(red: 158/255,
14 | green: 157/255,
15 | blue: 162/255,
16 | opacity: 1
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Indicators/CircleBounceIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleBounceIndicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// An activity indicator with bouncing circle
12 | public struct CircleBounceIndicator: View {
13 | @State private var offsetY: CGFloat = 0
14 |
15 | public init() {}
16 |
17 | public var body: some View {
18 | Replicator(
19 | Circle()
20 | .fill(Color.accentColor)
21 | .frame(width: 10, height: 10)
22 | .offset(x: 0, y: offsetY)
23 | )
24 | .repeatCount(4)
25 | .repeatTransform(.init(translationX: 18, y: 0))
26 | .repeatDelay(0.6/4)
27 | .animation(.linear(duration: 0.3)
28 | .delay(0.3)
29 | .repeatForever())
30 | .onAppear(perform: {
31 | self.offsetY = -5
32 | })
33 | .offset(x: -26, y: 0)
34 | }
35 | }
36 |
37 | struct CircleActivityIndicator_Previews: PreviewProvider {
38 | static var previews: some View {
39 | CircleBounceIndicator()
40 | .previewDevice("iPhone 12 Pro Max")
41 | .accentColor(.gray)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Indicators/CircleFedeIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleFedeIndicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/26.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// An activity indicator with fading circle
12 | public struct CircleFedeIndicator: View {
13 | @State private var foregroundColor = Color.clear
14 |
15 | public init() {}
16 |
17 | public var body: some View {
18 | Replicator(
19 | Circle()
20 | .strokeBorder(Color.accentColor, lineWidth: 0)
21 | .background(
22 | Circle()
23 | .foregroundColor(foregroundColor)
24 | )
25 | .frame(
26 | width: 12,
27 | height: 12
28 | )
29 | .transformEffect(
30 | .init(
31 | translationX: -5,
32 | y: -30
33 | )
34 | )
35 | )
36 | .repeatCount(10)
37 | .repeatDelay(0.1)
38 | .repeatTransform(.rotateWithDividing(10))
39 | .animation(
40 | .linear(duration: 1)
41 | .delay(0.9)
42 | .repeatForever(autoreverses: false)
43 |
44 | )
45 | .onAppear(perform: {
46 | self.foregroundColor = .accentColor
47 | })
48 | }
49 | }
50 |
51 | struct RotatingFillAcitivityIndicator_Previews: PreviewProvider {
52 | static var previews: some View {
53 | CircleFedeIndicator()
54 | .accentColor(.gray)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Indicators/CircleRotateIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleRotateIndicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/25.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// An activity indicator with rotating circle
12 | public struct CircleRotateIndicator: View {
13 | @State private var scale: CGFloat = 0.8
14 |
15 | public init() {}
16 |
17 | public var body: some View {
18 | Replicator(
19 | Circle()
20 | .fill(
21 | Color.accentColor
22 | )
23 | .frame(
24 | width: 6,
25 | height: 6
26 | )
27 | .scaleEffect(CGSize(width: scale, height: scale))
28 | .transformEffect(
29 | .init(translationX: -5, y: 16)
30 | )
31 | )
32 | .repeatCount(8)
33 | .repeatDelay(2/8)
34 | .repeatTransform(.rotateWithDividing(8))
35 | .animation(
36 | .linear(duration: 0.4)
37 | .delay(0.5)
38 | .repeatForever()
39 | )
40 | .onAppear(perform: {
41 | self.scale = 1.4
42 | })
43 | }
44 | }
45 |
46 | struct RotatedCircleActivityIndicator_Previews: PreviewProvider {
47 | static var previews: some View {
48 | CircleRotateIndicator()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Indicators/CircleScaleIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CircleScaleIndicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// An activity indicator with scaling circle
12 | public struct CircleScaleIndicator: View {
13 | @State private var scale: CGFloat = 1
14 |
15 | private let count: Int = 4
16 | private let size: CGFloat = 10
17 |
18 | public init() {}
19 |
20 | public var body: some View {
21 | Replicator(
22 | Circle()
23 | .fill(Color.accentColor)
24 | .frame(width: size, height: size)
25 | .scaleEffect(scale)
26 | )
27 | .repeatCount(count)
28 | .repeatTransform(.init(translationX: size + 5, y: 0))
29 | .repeatDelay(0.8/Double(count))
30 | .animation(.linear(duration: 0.5)
31 | .delay(0.3)
32 | .repeatForever())
33 | .onAppear(perform: {
34 | self.scale = 0.3
35 | })
36 | .offset(x: -22.5, y: 0)
37 | }
38 | }
39 |
40 | struct CircleInLineIndicator_Previews: PreviewProvider {
41 | static var previews: some View {
42 | CircleScaleIndicator()
43 | .previewDevice("iPhone 12 Pro Max")
44 | .accentColor(.red)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Indicators/ClassicalActivityIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ClassicalActivityIndicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// An activity indicator like UIActivtyIndicator.
12 | public struct ClassicalActivityIndicator: View {
13 | public enum Style {
14 | case medium
15 | case large
16 |
17 | public var scale: CGFloat {
18 | switch self {
19 | case .medium: return 1
20 | case .large: return 1.5
21 | }
22 | }
23 | }
24 |
25 | @State private var fillColor = Color(white: 0.9, opacity: 1)
26 |
27 | private let style: Style
28 | private let count = 8
29 |
30 |
31 | /// Creates an indicator with style
32 | /// - Parameter style: medium or large
33 | public init(style: Style) {
34 | self.style = style
35 | }
36 |
37 | public var body: some View {
38 | Replicator(
39 | Capsule(style: .circular)
40 | .fill(fillColor)
41 | .frame(
42 | width: 2.5 * style.scale,
43 | height: 6 * style.scale
44 | )
45 | .transformEffect(
46 | .init(
47 | translationX: -1.25 * style.scale,
48 | y: -9.5 * style.scale
49 | )
50 | )
51 | )
52 | .repeatCount(count)
53 | .repeatDelay(1/Double(CGFloat(count)))
54 | .repeatTransform(
55 | .init(
56 | rotationAngle:
57 | (2.0*CGFloat.pi)/CGFloat(count)
58 | )
59 | )
60 | .animation(
61 | .linear(duration: 0.2)
62 | .delay(0.3)
63 | .repeatForever()
64 | )
65 | .onAppear(perform: {
66 | self.fillColor = Color.activityIndicator
67 | })
68 | }
69 | }
70 |
71 | struct ClassicalActivityIndicator_Previews: PreviewProvider {
72 | static var previews: some View {
73 | ClassicalActivityIndicator(style: .large)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Indicators/RectangleScaleIndicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RectangleScaleIndicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// An activity indicator with scaling rectangle
12 | public struct RectangleScaleIndicator: View {
13 | @State private var scale: CGFloat = 1
14 | private let count: Int = 3
15 |
16 | public init() {}
17 |
18 | public var body: some View {
19 | Replicator(
20 | Rectangle()
21 | .fill(Color.accentColor)
22 | .frame(width: 8, height: 26)
23 | .scaleEffect(CGSize(width: 1.0, height: scale))
24 | )
25 | .repeatCount(count)
26 | .repeatTransform(.init(translationX: 12, y: 0))
27 | .repeatDelay(0.5/Double(count))
28 | .animation(.easeInOut(duration: 0.5)
29 | .delay(0.2)
30 | .repeatForever())
31 | .onAppear(perform: {
32 | self.scale = 1.5
33 | })
34 | .offset(x: -12, y: 0)
35 | }
36 | }
37 |
38 | struct StickActivityIndicator_Previews: PreviewProvider {
39 | static var previews: some View {
40 | RectangleScaleIndicator()
41 | .accentColor(.gray)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Modifiers/CirculateModifier.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CirculateModifier.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | extension View {
12 | func circulate(width: CGFloat, height: CGFloat, ratio: CGFloat = 1) -> some View {
13 | modifier(CirculateModifier(width: width, height: height, ratio: ratio))
14 | }
15 | }
16 |
17 | struct CirculateModifier: ViewModifier {
18 | let width: CGFloat
19 | let height: CGFloat
20 | let ratio: CGFloat
21 |
22 | func body(content: Content) -> some View {
23 | content
24 | .offset(x: -width/2, y: -height * ratio * 2)
25 | .frame(width: width, height: height)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/Replicator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Replicator.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 |
12 | /// A view that creates a specified number of subview copies with varying geometric, temporal, and color transformations.
13 | public struct Replicator: View {
14 | private let content: Content
15 |
16 | @ObservedObject private var viewModel = ReplicatorViewModel()
17 |
18 | public init(_ content: Content) {
19 | self.content = content
20 | }
21 |
22 | public var body: some View {
23 | var incrementer = Incrementer(viewModel: viewModel)
24 |
25 | ZStack {
26 | ForEach(0.. Replicator {
45 | viewModel.repeatCount = repeatCount
46 | return self
47 | }
48 |
49 |
50 | /// Specifies the aniation delay, in seconds, between replicated copies
51 | /// - Parameter repeatDelay: repeat delay
52 | /// - Returns: self
53 | public func repeatDelay(_ repeatDelay: TimeInterval) -> Replicator {
54 | viewModel.repeatDelay = repeatDelay
55 | return self
56 | }
57 |
58 |
59 | /// sets the animation to each view.
60 | /// - Parameter animation: animation
61 | /// - Returns: self
62 | public func animation(_ animation: Animation) -> Replicator {
63 | viewModel.animation = animation
64 | return self
65 | }
66 |
67 |
68 | /// The transform matrix applied to the previous instance to produce the current instance.
69 | /// - Parameter repeatTransform: transform
70 | /// - Returns: self
71 | public func repeatTransform(_ repeatTransform: CGAffineTransform) -> Replicator {
72 | viewModel.repeatTransform = repeatTransform
73 | return self
74 | }
75 |
76 |
77 | /// Defines the offset added to the red component of the color for each replicated instance..
78 | /// - Parameter instanceRedOffset: red offset
79 | /// - Returns: self
80 | public func instanceRedOffset(_ instanceRedOffset: Double) -> Replicator {
81 | viewModel.instanceRedOffset = instanceRedOffset
82 | return self
83 | }
84 |
85 | /// Defines the offset added to the green component of the color for each replicated instance..
86 | /// - Parameter instanceGreenOffset: green offset
87 | /// - Returns: self
88 | public func instanceGreenOffset(_ instanceGreenOffset: Double) -> Replicator {
89 | viewModel.instanceGreenOffset = instanceGreenOffset
90 | return self
91 | }
92 |
93 | /// Defines the offset added to the blue component of the color for each replicated instance..
94 | /// - Parameter instanceBlueOffset: blue offset
95 | /// - Returns: self
96 | public func instanceBlueOffset(_ instanceBlueOffset: Double) -> Replicator {
97 | viewModel.instanceBlueOffset = instanceBlueOffset
98 | return self
99 | }
100 | }
101 |
102 | //MARK:- ViewModel
103 |
104 | extension Replicator {
105 | private class ReplicatorViewModel: ObservableObject {
106 | @Published var repeatCount: Int = 0
107 | @Published var repeatDelay: TimeInterval = 0.0
108 | @Published var repeatTransform: CGAffineTransform = .identity
109 | @Published var instanceRedOffset: Double = 0
110 | @Published var instanceBlueOffset: Double = 0
111 | @Published var instanceGreenOffset: Double = 0
112 | @Published var animation: Animation?
113 | }
114 | }
115 |
116 | // MARK:- Utility
117 |
118 | extension Replicator {
119 | private struct Incrementer {
120 | var repeatTransform: RepeatTransform
121 | var repeatDelay: RepeatDelay
122 | var repeatColor: RepeatColorOffset
123 |
124 | init(viewModel: ReplicatorViewModel) {
125 | repeatTransform = RepeatTransform(value: viewModel.repeatTransform)
126 | repeatDelay = RepeatDelay(value: viewModel.repeatDelay)
127 | repeatColor = RepeatColorOffset(
128 | value: .init(
129 | red: viewModel.instanceRedOffset,
130 | blue: viewModel.instanceBlueOffset,
131 | green: viewModel.instanceGreenOffset
132 | )
133 | )
134 | }
135 |
136 | mutating func color() -> Color {
137 | let color = repeatColor.increment()
138 | return Color(
139 | red: color.red,
140 | green: color.green,
141 | blue: color.blue, opacity: 1)
142 | }
143 |
144 | mutating func transform() -> CGAffineTransform {
145 | repeatTransform.increment()
146 | }
147 |
148 | mutating func delay() -> TimeInterval {
149 | repeatDelay.increment()
150 | }
151 | }
152 |
153 | private struct RepeatTransform {
154 | var value: CGAffineTransform
155 | var pointer: CGAffineTransform?
156 |
157 | mutating func increment() -> CGAffineTransform {
158 | let pointer = pointer?.concatenating(value) ?? .identity
159 | self.pointer = pointer
160 | return pointer
161 | }
162 | }
163 |
164 | private struct RepeatDelay {
165 | var value: TimeInterval
166 | var pointer: TimeInterval?
167 |
168 | mutating func increment() -> TimeInterval {
169 | let pointer = pointer.flatMap { $0 + value } ?? 0
170 | self.pointer = pointer
171 | return pointer
172 | }
173 | }
174 |
175 | private struct RepeatColorOffset {
176 | var value: ColorComponents
177 | var pointer: ColorComponents?
178 |
179 | mutating func increment() -> ColorComponents {
180 | let pointer = pointer.flatMap { $0.appending(value) } ?? .white
181 | self.pointer = pointer
182 | return pointer
183 | }
184 | }
185 |
186 | struct ColorComponents {
187 | let red: Double
188 | let blue: Double
189 | let green: Double
190 |
191 | static var white: ColorComponents {
192 | ColorComponents(red: 1, blue: 1, green: 1)
193 | }
194 |
195 | func appending(_ components: ColorComponents) -> ColorComponents {
196 | let red = min(max(0, self.red + components.red), 1)
197 | let blue = min(max(0, self.blue + components.blue), 1)
198 | let green = min(max(0, self.green + components.blue), 1)
199 | return .init(red: red, blue: blue, green: green)
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReplicator/SwiftUIReplicatorContent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIReplicatorContent.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/24.
6 | //
7 | //
8 |
9 | import SwiftUI
10 |
11 | @available(iOS 14.0, *)
12 | public struct SwiftUIReplicatorContent: LibraryContentProvider {
13 | public var views: [LibraryItem] {
14 | LibraryItem(
15 | ActivityIndicator(style: .classicalLarge),
16 | title: "Indicator: rectangle & scale"
17 | )
18 | LibraryItem(
19 | ClassicalActivityIndicator(style: .large),
20 | title: "An activity indicator like UIActivtyIndicator."
21 | )
22 | LibraryItem(
23 | CircleRotateIndicator(),
24 | title: "An activity indicator with rotating circle"
25 | )
26 | LibraryItem(
27 | CircleBounceIndicator(),
28 | title: "An activity indicator with bouncing circle"
29 | )
30 | LibraryItem(
31 | CircleFedeIndicator(),
32 | title: "An activity indicator with fading circle"
33 | )
34 | LibraryItem(
35 | RectangleScaleIndicator(),
36 | title: "An activity indicator with scaling rectangle"
37 | )
38 | LibraryItem(
39 | Replicator(Circle().frame(width: 5, height: 5))
40 | .repeatCount(3)
41 | .repeatTransform(CGAffineTransform(translationX: 10, y: 0)),
42 | title: "Replicator"
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/SwiftUIReplicatorTests/SwiftUIReplicatorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIReplicatorTests.swift
3 | // SwiftUIReplicator
4 | //
5 | // Created by Kazuhiro Hayashi on 2021/05/23.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftUIReplicator
11 |
12 | class SwiftUIReplicatorTests: XCTestCase {
13 |
14 | override func setUpWithError() throws {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDownWithError() throws {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() throws {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() throws {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------