diff --git a/SwiftUI-Particles.xcodeproj/project.pbxproj b/Example/SwiftUI-Particles.xcodeproj/project.pbxproj similarity index 100% rename from SwiftUI-Particles.xcodeproj/project.pbxproj rename to Example/SwiftUI-Particles.xcodeproj/project.pbxproj diff --git a/SwiftUI-Particles.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/SwiftUI-Particles.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from SwiftUI-Particles.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Example/SwiftUI-Particles.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/SwiftUI-Particles.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/SwiftUI-Particles.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from SwiftUI-Particles.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Example/SwiftUI-Particles.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/SwiftUI-Particles/AppDelegate.swift b/Example/SwiftUI-Particles/AppDelegate.swift similarity index 100% rename from SwiftUI-Particles/AppDelegate.swift rename to Example/SwiftUI-Particles/AppDelegate.swift diff --git a/SwiftUI-Particles/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/SwiftUI-Particles/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from SwiftUI-Particles/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Example/SwiftUI-Particles/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/SwiftUI-Particles/Assets.xcassets/Contents.json b/Example/SwiftUI-Particles/Assets.xcassets/Contents.json similarity index 100% rename from SwiftUI-Particles/Assets.xcassets/Contents.json rename to Example/SwiftUI-Particles/Assets.xcassets/Contents.json diff --git a/SwiftUI-Particles/Assets.xcassets/spark.imageset/Contents.json b/Example/SwiftUI-Particles/Assets.xcassets/spark.imageset/Contents.json similarity index 100% rename from SwiftUI-Particles/Assets.xcassets/spark.imageset/Contents.json rename to Example/SwiftUI-Particles/Assets.xcassets/spark.imageset/Contents.json diff --git a/SwiftUI-Particles/Assets.xcassets/spark.imageset/spark.png b/Example/SwiftUI-Particles/Assets.xcassets/spark.imageset/spark.png similarity index 100% rename from SwiftUI-Particles/Assets.xcassets/spark.imageset/spark.png rename to Example/SwiftUI-Particles/Assets.xcassets/spark.imageset/spark.png diff --git a/SwiftUI-Particles/Base.lproj/LaunchScreen.storyboard b/Example/SwiftUI-Particles/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from SwiftUI-Particles/Base.lproj/LaunchScreen.storyboard rename to Example/SwiftUI-Particles/Base.lproj/LaunchScreen.storyboard diff --git a/SwiftUI-Particles/ContentView.swift b/Example/SwiftUI-Particles/ContentView.swift similarity index 100% rename from SwiftUI-Particles/ContentView.swift rename to Example/SwiftUI-Particles/ContentView.swift diff --git a/SwiftUI-Particles/Info.plist b/Example/SwiftUI-Particles/Info.plist similarity index 100% rename from SwiftUI-Particles/Info.plist rename to Example/SwiftUI-Particles/Info.plist diff --git a/SwiftUI-Particles/ParticlesEmitter.swift b/Example/SwiftUI-Particles/ParticlesEmitter.swift similarity index 100% rename from SwiftUI-Particles/ParticlesEmitter.swift rename to Example/SwiftUI-Particles/ParticlesEmitter.swift diff --git a/SwiftUI-Particles/Preview Content/Preview Assets.xcassets/Contents.json b/Example/SwiftUI-Particles/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from SwiftUI-Particles/Preview Content/Preview Assets.xcassets/Contents.json rename to Example/SwiftUI-Particles/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/SwiftUI-Particles/SceneDelegate.swift b/Example/SwiftUI-Particles/SceneDelegate.swift similarity index 100% rename from SwiftUI-Particles/SceneDelegate.swift rename to Example/SwiftUI-Particles/SceneDelegate.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..067f798 --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SwiftUI-Particles", + platforms: [ + .iOS(.v13) + ], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "SwiftUI-Particles", + targets: ["SwiftUI-Particles"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.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: "SwiftUI-Particles", + dependencies: []) + ] +) diff --git a/README.md b/README.md index b399963..9cc349a 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,15 @@ Playing with particles with SwiftUI ✨ +## Installation via Swift Package Manager +1. In your Xcode project, navigate to File → Swift Packages → Add Package Dependency... +2. Copy and paste `https://github.com/ArthurGuibert/SwiftUI-Particles` into the search bar and click Next. +3. For Rules, select "Branch" and set it to **master**. +4. Click Finish. ## How to use -Just use the the `ParticlesEmitter` class in your project as follow: +Just use the `ParticlesEmitter` class in your project as follow: ```swift struct ContentView: View { diff --git a/Sources/SwiftUI-Particles/ParticlesEmitter.swift b/Sources/SwiftUI-Particles/ParticlesEmitter.swift new file mode 100644 index 0000000..465c015 --- /dev/null +++ b/Sources/SwiftUI-Particles/ParticlesEmitter.swift @@ -0,0 +1,223 @@ +// +// ParticlesEmitter.swift +// SwiftUI-Particles +// +// Created by Arthur Guibert on 28/10/2019. +// Copyright © 2019 Arthur Guibert. All rights reserved. +// + +import SwiftUI +import UIKit + + +/// Class that wraps the CAEmitterCell in a class compatible with SwiftUI +public struct ParticlesEmitter: UIViewRepresentable { + var center: CGPoint = .zero + var emitterSize: CGSize = .init(width: 1, height: 1) + var shape: CAEmitterLayerEmitterShape = .line + var cells: [CAEmitterCell] = [] + + public func updateUIView(_ uiView: InternalParticlesView, context: UIViewRepresentableContext) { + uiView.emit(from: center, + size: emitterSize, + shape: shape, + cells: cells) + } + + public func makeUIView(context: Context) -> InternalParticlesView { + let view = InternalParticlesView() + view.emit(from: center, + size: emitterSize, + shape: shape, + cells: cells) + return view + } +} + +extension ParticlesEmitter { + func emitterSize(_ size: CGSize) -> Self { + return ParticlesEmitter(center: self.center, emitterSize: size, shape: shape, cells: self.cells) + } + + func emitterPosition(_ position: CGPoint) -> Self { + return ParticlesEmitter(center: position, emitterSize: self.emitterSize, shape: shape, cells: self.cells) + } + + func emitterShape(_ shape: CAEmitterLayerEmitterShape) -> Self { + return ParticlesEmitter(center: self.center, emitterSize: self.emitterSize, shape: shape, cells: self.cells) + } +} + + +/// The container view class for the particles, as the project is using a CAEmitterLayer +public final class InternalParticlesView: UIView { + private var particleEmitter: CAEmitterLayer? + + /// Function that adds the emitter cells to the layer + /// - Parameter center: center of the emitter + /// - Parameter size: size of the emitter + /// - Parameter cells: all the CAEmitterCell + func emit(from center: CGPoint, size: CGSize, shape: CAEmitterLayerEmitterShape, cells: [CAEmitterCell]) { + if particleEmitter == nil { + particleEmitter = CAEmitterLayer() + layer.addSublayer(particleEmitter!) + } + + particleEmitter?.emitterPosition = center + particleEmitter?.emitterShape = shape + particleEmitter?.emitterSize = size + particleEmitter?.emitterCells = cells + } +} + +@_functionBuilder +struct EmitterCellBuilder { + static func buildBlock(_ cells: CAEmitterCell...) -> [CAEmitterCell] { + Array(cells) + } +} + +extension ParticlesEmitter { + init(@EmitterCellBuilder _ content: () -> [CAEmitterCell]) { + self.init(cells: content()) + } + + init(@EmitterCellBuilder _ content: () -> CAEmitterCell) { + self.init(cells: [content()]) + } +} + +class EmitterCell: CAEmitterCell { + override init() { + super.init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func copyEmitter() -> EmitterCell { + return super.copy() as! EmitterCell + } +} + + +extension EmitterCell { + /// Content for the emitter cell, it is either an image, or a circle. + /// NB: It could easily be extended for other shapes. + public enum Content { + case image(UIImage) + case circle(CGFloat) + } + + @inlinable func content(_ content: Content) -> Self { + self.contents = content.image.cgImage + return self + } + + @inlinable func birthRate(_ birthRate: Float) -> Self { + self.birthRate = birthRate + return self + } + + @inlinable func lifetime(_ lifetime: Float) -> Self { + self.lifetime = lifetime + return self + } + + @inlinable func scale(_ scale: CGFloat) -> Self { + self.scale = scale + return self + } + + @inlinable func scaleRange(_ scaleRange: CGFloat) -> Self { + self.scaleRange = scaleRange + return self + } + + @inlinable func scaleSpeed(_ scaleSpeed: CGFloat) -> Self { + self.scaleSpeed = scaleSpeed + return self + } + + @inlinable func velocity(_ velocity: CGFloat) -> Self { + self.velocity = velocity + return self + } + + @inlinable func velocityRange(_ velocityRange: CGFloat) -> Self { + self.velocityRange = velocityRange + return self + } + + @inlinable func emissionLongitude(_ emissionLongitude: CGFloat) -> Self { + self.emissionLongitude = emissionLongitude + return self + } + + @inlinable func emissionLatitude(_ emissionLatitude: CGFloat) -> Self { + self.emissionLatitude = emissionLatitude + return self + } + + @inlinable func emissionRange(_ emissionRange: CGFloat) -> Self { + self.emissionRange = emissionRange + return self + } + + @inlinable func spin(_ spin: CGFloat) -> Self { + self.spin = spin + return self + } + + @inlinable func spinRange(_ spinRange: CGFloat) -> Self { + self.spinRange = spinRange + return self + } + + @inlinable func color(_ color: UIColor) -> Self { + self.color = color.cgColor + return self + } + + @inlinable func xAcceleration(_ xAcceleration: CGFloat) -> Self { + self.xAcceleration = xAcceleration + return self + } + + @inlinable func yAcceleration(_ yAcceleration: CGFloat) -> Self { + self.yAcceleration = yAcceleration + return self + } + + @inlinable func zAcceleration(_ zAcceleration: CGFloat) -> Self { + self.zAcceleration = zAcceleration + return self + } + + @inlinable func alphaSpeed(_ alphaSpeed: Float) -> Self { + self.alphaSpeed = alphaSpeed + return self + } + + @inlinable func alphaRange(_ alphaRange: Float) -> Self { + self.alphaRange = alphaRange + return self + } +} + +fileprivate extension EmitterCell.Content { + var image: UIImage { + switch self { + case let .image(image): + return image + case let .circle(radius): + let size = CGSize(width: radius * 2, height: radius * 2) + return UIGraphicsImageRenderer(size: size).image { context in + context.cgContext.setFillColor(UIColor.white.cgColor) + context.cgContext.addPath(CGPath(ellipseIn: CGRect(origin: .zero, size: size), transform: nil)) + context.cgContext.fillPath() + } + } + } +}