Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 5 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
<img src="demo-videos/shower.gif" alt="Shower plume demo" width="32%" />
</p>

`Plume` is a Swift package for building particle effects with `CAEmitterLayer` and using them from SwiftUI or UIKit. The public API is centered around a single `Plume` value that combines an emitter and one or more particle cells.
`Plume` is a Swift package for building particle effects with `CAEmitterLayer` and using them from SwiftUI. The public API is centered around a single `Plume` value that combines an emitter and one or more particle cells.

## Requirements

- iOS 12+
- Swift Package Manager
- UIKit or SwiftUI
- SwiftUI

## Installation

Expand All @@ -36,7 +36,7 @@ import Plume
The package has three main pieces:

- `Plume`, the top-level effect model
- `PlumeView` and `PlumeUIView`, the SwiftUI and UIKit renderers
- `PlumeView`, the SwiftUI renderer
- a set of convenience extensions for quickly building emitters, cells, and preset motion values

## Type Tree
Expand Down Expand Up @@ -98,7 +98,7 @@ let plume = Plume(

## SwiftUI Usage

Use `PlumeView` when the effect belongs inside a SwiftUI hierarchy. The view is trigger-based: when the `trigger` value changes, the underlying `PlumeUIView` emits again.
Use `PlumeView` when the effect belongs inside a SwiftUI hierarchy. The view is trigger-based: when the `trigger` value changes, the underlying plume view emits again.

```swift
import SwiftUI
Expand Down Expand Up @@ -139,47 +139,6 @@ struct CelebrationView: View {

Use this approach when the plume effect should be attached to a specific screen or view tree.

## UIKit Usage

Use `PlumeUIView` when you want direct UIKit control. Create the view, size it to your container, add it to the hierarchy, and call `emit()`.

```swift
import UIKit
import Plume

final class CelebrationViewController: UIViewController {
private let plume = Plume(
emitter: .rectangle(birthRate: 20),
cells: .make(
from: [
UIImage(systemName: "sparkle")!,
UIImage(systemName: "circle.fill")!
],
lifetime: .normal,
spin: .normal,
scale: .small,
acceleration: .gravity,
velocity: .standard,
angle: .bottomHemisphere
)
)

private lazy var plumeView = PlumeUIView(plume: plume)

override func viewDidLoad() {
super.viewDidLoad()

plumeView.frame = view.bounds
plumeView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(plumeView)
}

func celebrate() {
plumeView.emit()
}
}
```

## Convenience Helpers

The package includes a few helpers to make common effects easier to express:
Expand Down Expand Up @@ -269,13 +228,12 @@ typealias CellVelocity = Plume.Cell.Velocity
## Choosing an API

- Use `PlumeView` for SwiftUI screens.
- Use `PlumeUIView` for UIKit screens and custom view hierarchies.
- Use direct `Plume` construction when you want precise control over emitter behavior.

## Notes

- `PlumeView` is trigger-driven and emits when the `trigger` input changes.
- `PlumeUIView` is non-interactive by default and is intended to sit on top of other content.
- The underlying UIKit view is internal, non-interactive by default, and intended to sit on top of other content.
- `Plume.Emitter.Mode` and `Plume.Emitter.Shape` are implementation details; the public entry point is the emitter factory API.
- Most motion/value types are currently intended to be used through their preset constants rather than direct initialization.
- Remote URL-backed cell creation is asynchronous and currently available on iOS 17 and later.
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@

import UIKit

// TODO: Consider if this should be a public facing component.
// TODO: Add support for haptics.
// TODO: Add support for audio.

/// A `UIView` responsible for rendering and emitting plume particles.
/// An internal `UIView` responsible for rendering and emitting plume particles.
///
/// `PlumeView` acts as a thin wrapper around `PlumeLayer`, handling
/// its lifecycle and attaching it to the view’s backing layer. Upon
/// initialization, it immediately begins emitting particles using the
/// provided plume.
public final class PlumeUIView: UIView {
final class PlumeUIView: UIView {

/// The plume definition rendered by this view.
private let plume: Plume

/// Creates a UIKit plume view from a plume definition.
/// Creates a plume view from a plume definition.
///
/// - Parameter plume: The plume rendered and emitted by the view.
public init(plume: Plume) {
init(plume: Plume) {
self.plume = plume
super.init(frame: .zero)

Expand All @@ -37,10 +36,10 @@ public final class PlumeUIView: UIView {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Public Actions
// MARK: - Actions

/// Emits the plume using the current view bounds as the emitter container.
public func emit() {
func emit() {
let emitter = makeCAEmitter(with: plume)

layer.addSublayer(emitter)
Expand Down
17 changes: 12 additions & 5 deletions Sources/Plume/Public/UI/PlumeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
//

import SwiftUI
import UIKit

/// A SwiftUI wrapper around `PlumeUIView`.
/// A SwiftUI view that renders and emits plume particles.
///
/// Use this view when a plume effect should be embedded in a SwiftUI hierarchy
/// and triggered by changes in view state.
public struct PlumeView: UIViewRepresentable {
public typealias UIViewType = UIView

/// The plume rendered by the underlying UIKit view.
var plume: Plume

Expand All @@ -28,13 +31,17 @@ public struct PlumeView: UIViewRepresentable {
self.trigger = trigger
}

/// Creates the underlying UIKit plume view.
public func makeUIView(context: Context) -> PlumeUIView {
/// Creates the underlying UIKit view.
public func makeUIView(context: Context) -> UIView {
PlumeUIView(plume: plume)
}

/// Updates the UIKit plume view and emits when the trigger changes.
public func updateUIView(_ uiView: PlumeUIView, context: Context) {
/// Updates the UIKit view and emits when the trigger changes.
public func updateUIView(_ uiView: UIView, context: Context) {
guard let uiView = uiView as? PlumeUIView else {
return
}

if context.coordinator.lastTrigger != trigger {
context.coordinator.lastTrigger = trigger
uiView.emit()
Expand Down
Loading