├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.swift ├── README.md └── Sources └── IrregularGradient ├── Blob.swift ├── IrregularGradient.swift ├── IrregularGradientView.swift └── Modifiers.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 João Gabriel 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 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "IrregularGradient", 8 | platforms: [ 9 | .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "IrregularGradient", 15 | targets: ["IrregularGradient"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "IrregularGradient", 26 | dependencies: []), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

IrregularGradient 2 | 3 | 4 | Project logo 5 | 6 |

7 | 8 |

9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | Twitter: @joogps 17 | 18 |

19 | 20 | A SwiftUI library for rendering beautiful, animated and _irregular_ gradient views. 21 | 22 | > [!NOTE] 23 | > This project implements this effect using pure SwiftUI, which can be computationally expensive. If you want something more efficient, please consider switching to [FluidGradient](https://github.com/Cindori/FluidGradient). 24 | 25 | ## Installation 26 | 27 | This repository is a Swift package, so just include it in your Xcode project and target under **File > Add package dependencies**. Then, `import IrregularGradient` to the Swift files where you'll be using it. 28 | 29 | ## Usage 30 | 31 | You can add an irregular gradient to your app with the following modifier: 32 | 33 | ```swift 34 | RoundedRectangle(cornerRadius: 24.0, style: .continuous) 35 | .irregularGradient(colors: [.orange, .pink, .yellow, .orange, .pink, .yellow], backgroundColor: .orange) 36 | ``` 37 | 38 | The other parameters go as follow: 39 | 40 | ```swift 41 | irregularGradient(colors: [Color], background: () -> View, shouldAnimate: Binding = .constant(true), speed: Double = 10) 42 | ``` 43 | 44 | - `colors` specifies the colors of each blob. Order and amount matters, so the colors will be stacked in the order of the array on the Z axis. Having two entries of the same color will create two completely distinct blobs of that color. 45 | - `background` defines the background of your gradient. It's a closure that returns a view. Not specifying this value it will make the background clear. 46 | - `shouldAnimate` is a boolean that specifies whether or not the gradient blobs should move. It can be enabled and disabled dinamically, and movement will always slow down to a stop. The default value is `true`. 47 | - `speed` accepts a Double and defines the speed of the movement — a 0.5 speed means the blobs will update every 2 seconds. The default value is 1. 48 | 49 | You can also use the `IrregularGradient` standalone view, which exists in its own container. 50 | 51 | ## How it's done 52 | The current implementation of this package is done through the creation of blobs (SwiftUI's [Ellipse](https://developer.apple.com/documentation/swiftui/ellipse) shape) of the specified colors that move and scale randomly in the container, and are then blurred to achieve the desired effect. 53 | 54 | ## Questions 55 | 56 | If you have any questions or suggestions, you can create an issue or pull request on this GitHub repository or even contact me via [Twitter](https://twitter.com/joogps) or [email](mailto:joogps@gmail.com). 57 | -------------------------------------------------------------------------------- /Sources/IrregularGradient/Blob.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Blob.swift 3 | // 4 | // 5 | // Created by Julian Schiavo on 5/4/2021. 6 | // 7 | 8 | import Combine 9 | import SwiftUI 10 | 11 | struct Blob: Identifiable, Equatable { 12 | let id = UUID() 13 | let color: Color 14 | 15 | var position: CGPoint = makePosition() 16 | var scale: CGSize = makeScale() 17 | var opacity: CGFloat = makeOpacity() 18 | 19 | static func makePosition() -> CGPoint { 20 | return CGPoint(x: CGFloat.random(in: 0...1), 21 | y: CGFloat.random(in: 0...1)) 22 | } 23 | 24 | static func makeScale() -> CGSize { 25 | return CGSize(width: CGFloat.random(in: 0.25...1), 26 | height: CGFloat.random(in: 0.25...1)) 27 | } 28 | 29 | static func makeOpacity() -> CGFloat { 30 | return CGFloat.random(in: 0.75...1) 31 | } 32 | } 33 | 34 | struct BlobView: View { 35 | var blob: Blob 36 | var geometry: GeometryProxy 37 | 38 | private var transformedPosition: CGPoint { 39 | let transform = CGAffineTransform(scaleX: geometry.size.width, y: geometry.size.height) 40 | return blob.position.applying(transform) 41 | } 42 | 43 | var body: some View { 44 | Ellipse() 45 | .foregroundColor(blob.color) 46 | .position(transformedPosition) 47 | .scaleEffect(blob.scale) 48 | .opacity(blob.opacity) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/IrregularGradient/IrregularGradient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IrregularGradient.swift 3 | // 4 | // 5 | // Created by João Gabriel Pozzobon dos Santos on 01/12/20. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | /// A view that displays an irregular gradient. 12 | public struct IrregularGradient: View { 13 | @State var blobs: [Blob] 14 | var background: Background 15 | var speed: Double 16 | var animate: Bool 17 | 18 | private let timer: Publishers.Autoconnect 19 | 20 | /// - Parameters: 21 | /// - colors: The colors of the blobs in the gradient. 22 | /// - background: The background view of the gradient. 23 | /// - speed: The speed at which the blobs move, if they're moving. 24 | /// - animate: Whether or not the blobs should move. 25 | public init(colors: [Color], 26 | background: @autoclosure @escaping () -> Background, 27 | speed: Double = 1, 28 | animate: Bool = true) { 29 | self._blobs = State(initialValue: colors.map({ Blob(color: $0) })) 30 | 31 | self.background = background() 32 | self.animate = animate 33 | self.speed = speed 34 | 35 | assert(self.speed > 0, "Speed should be greater than zero.") 36 | let interval = 1.0/self.speed 37 | self.timer = Timer.publish(every: interval, on: .main, in: .common).autoconnect() 38 | } 39 | 40 | private var animation: Animation { 41 | .spring(response: 3.0/speed, blendDuration: 1.0/speed) 42 | } 43 | 44 | public var body: some View { 45 | GeometryReader { geometry in 46 | ZStack { 47 | background 48 | ZStack { 49 | ForEach(blobs) { blob in 50 | BlobView(blob: blob, 51 | geometry: geometry) 52 | } 53 | }.compositingGroup() 54 | .blur(radius: pow(min(geometry.size.width, geometry.size.height), 0.65)) 55 | } 56 | .clipped() 57 | }.onAppear(perform: update) 58 | .onReceive(timer) { _ in 59 | update() 60 | } 61 | .animation(animation, value: blobs) 62 | } 63 | 64 | func update() { 65 | guard animate else { return } 66 | for index in blobs.indices { 67 | blobs[index].position = Blob.makePosition() 68 | blobs[index].scale = Blob.makeScale() 69 | blobs[index].opacity = Blob.makeOpacity() 70 | } 71 | } 72 | } 73 | 74 | public extension IrregularGradient where Background == Color { 75 | init(colors: [Color], 76 | backgroundColor: Color = .clear, 77 | speed: Double = 1, 78 | animate: Bool = true) { 79 | self.init(colors: colors, 80 | background: backgroundColor, 81 | speed: speed, 82 | animate: animate) 83 | } 84 | } 85 | 86 | struct IrregularGradient_Previews: PreviewProvider { 87 | static var previews: some View { 88 | PreviewWrapper() 89 | } 90 | 91 | struct PreviewWrapper: View { 92 | @State var animate = true 93 | 94 | var body: some View { 95 | VStack { 96 | RoundedRectangle(cornerRadius: 30.0, style: .continuous) 97 | .irregularGradient(colors: [.orange, .pink, .yellow, .orange, .pink, .yellow], 98 | background: Color.orange, 99 | animate: animate) 100 | Toggle("Animate", isOn: $animate) 101 | .padding() 102 | } 103 | .padding(25) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/IrregularGradient/IrregularGradientView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IrregularGradientView.swift 3 | // 4 | // 5 | // Created by Julian Schiavo on 5/4/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @available(*, deprecated, renamed: "IrregularGradient") 11 | public struct IrregularGradientView: View { 12 | var colors: [Color] 13 | var backgroundColor: Color 14 | var animate: Bool 15 | var speed: Double 16 | 17 | public init(colors: [Color], 18 | backgroundColor: Color = .clear, 19 | animate: Bool = true, 20 | speed: Double = 1) { 21 | self.colors = colors 22 | self.backgroundColor = backgroundColor 23 | self.animate = animate 24 | self.speed = speed 25 | } 26 | 27 | public var body: some View { 28 | IrregularGradient(colors: colors, 29 | background: backgroundColor, 30 | speed: speed, 31 | animate: animate) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/IrregularGradient/Modifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modifiers.swift 3 | // 4 | // 5 | // Created by Julian Schiavo on 5/4/2021. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | /// Replace a view's contents with a gradient. 12 | /// 13 | /// - Parameters: 14 | /// - colors: The colors of the blobs in the gradient. 15 | /// - background: The background view of the gradient. 16 | /// - speed: The speed at which the blobs move, if they're moving. 17 | /// - animate: Whether or not the blobs should move. 18 | public func irregularGradient(colors: [Color], 19 | background: @autoclosure @escaping () -> Background, 20 | animate: Bool = true, 21 | speed: Double = 1) -> some View { 22 | self 23 | .overlay(IrregularGradient(colors: colors, 24 | background: background(), 25 | speed: speed, 26 | animate: animate)) 27 | .mask(self) 28 | } 29 | 30 | @available(*, deprecated, message: "Replace \"backgroundColor\" with \"background\"") 31 | public func irregularGradient(colors: [Color], 32 | backgroundColor: Color = .clear, 33 | animate: Bool = true, 34 | speed: Double = 1) -> some View { 35 | self 36 | .irregularGradient(colors: colors, 37 | background: backgroundColor, 38 | animate: animate, 39 | speed: speed) 40 | } 41 | } 42 | 43 | extension Shape { 44 | /// Fill a shape with a gradient. 45 | /// 46 | /// - Parameters: 47 | /// - colors: The colors of the blobs in the gradient. 48 | /// - background: The background view of the gradient. 49 | /// - speed: The speed at which the blobs move, if they're moving. 50 | /// - animate: Whether or not the blobs should move. 51 | public func irregularGradient(colors: [Color], 52 | background: @autoclosure @escaping () -> Background, 53 | animate: Bool = true, 54 | speed: Double = 1) -> some View { 55 | self 56 | .overlay(IrregularGradient(colors: colors, 57 | background: background(), 58 | speed: speed, 59 | animate: animate)) 60 | .clipShape(self) 61 | } 62 | } 63 | --------------------------------------------------------------------------------