├── .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 | | ![sample_5](https://user-images.githubusercontent.com/18320004/120912569-4bf5e900-c6cb-11eb-9066-a983683de8bb.gif) | ![sample_4](https://user-images.githubusercontent.com/18320004/120912583-68922100-c6cb-11eb-810d-3d7b61efdbe4.gif) | ![sample6](https://user-images.githubusercontent.com/18320004/120912593-7e074b00-c6cb-11eb-85b3-999b749b5211.gif) | ![sample7](https://user-images.githubusercontent.com/18320004/120912601-8eb7c100-c6cb-11eb-9f15-6936c4b5b097.gif) | ![sample8](https://user-images.githubusercontent.com/18320004/120912615-b0b14380-c6cb-11eb-837f-012a52e99f88.gif) | 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 | --------------------------------------------------------------------------------