From 2fbcf029a6575e44d01f139d4aeb0123ebd177c7 Mon Sep 17 00:00:00 2001 From: Will-thom <116388885+Will-thom@users.noreply.github.com> Date: Mon, 25 May 2026 17:37:07 -0300 Subject: [PATCH] Internalize UIKit plume view --- README.md | 52 ++----------------- .../{Public => Internal}/UI/PlumeUIView.swift | 13 +++-- Sources/Plume/Public/UI/PlumeView.swift | 17 ++++-- 3 files changed, 23 insertions(+), 59 deletions(-) rename Sources/Plume/{Public => Internal}/UI/PlumeUIView.swift (93%) diff --git a/README.md b/README.md index e7b4a86..52dc3b1 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ Shower plume demo

-`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 @@ -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 @@ -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 @@ -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: @@ -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. diff --git a/Sources/Plume/Public/UI/PlumeUIView.swift b/Sources/Plume/Internal/UI/PlumeUIView.swift similarity index 93% rename from Sources/Plume/Public/UI/PlumeUIView.swift rename to Sources/Plume/Internal/UI/PlumeUIView.swift index da4d874..866f977 100644 --- a/Sources/Plume/Public/UI/PlumeUIView.swift +++ b/Sources/Plume/Internal/UI/PlumeUIView.swift @@ -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) @@ -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) diff --git a/Sources/Plume/Public/UI/PlumeView.swift b/Sources/Plume/Public/UI/PlumeView.swift index 50f3d68..1a19dfc 100644 --- a/Sources/Plume/Public/UI/PlumeView.swift +++ b/Sources/Plume/Public/UI/PlumeView.swift @@ -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 @@ -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()