I adapted this code for SwiftUI to display confetti particles, but sometimes the particle emitter does not work. I've noticed that this often happens after sending to background (not killing the app entirely) and reopening it, or simply letting the app sit for a while then trying again.
I've tried using beginTime as other answers have mentioned (on both the emitter and cells), but that fully breaks things. I've also tried toggling various other emitter properties (birthRate, isHidden). It might have to do with the fact that I'm adapting this with UIViewRepresentable. It seems like the emitter layer just disappears, even though the debug console says its still visible.
class ConfettiParticleView: UIView {
var emitter: CAEmitterLayer!
public var colors: [UIColor]!
public var intensity: Float!
private var active: Bool!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
colors = [UIColor(Color.red),
UIColor(Color.blue),
UIColor(Color.orange),
]
intensity = 0.7
active = false
emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: UIScreen.main.bounds.width / 2.0, y: 0) // emit from top of view
emitter.emitterShape = .line
emitter.emitterSize = CGSize(width: UIScreen.main.bounds.width, height: 100) // line spans the whole top of view
// emitter.beginTime = CACurrentMediaTime()
var cells = [CAEmitterCell]()
for color in colors {
cells.append(confettiWithColor(color: color))
}
emitter.emitterCells = cells
emitter.allowsGroupOpacity = false
self.layer.addSublayer(emitter)
}
func startConfetti() {
emitter.lifetime = 1
// i've tried toggling other properties here like birthRate, speed
active = true
}
func stopConfetti() {
emitter.lifetime = 0
active = false
}
func confettiWithColor(color: UIColor) -> CAEmitterCell {
let confetti = CAEmitterCell()
confetti.birthRate = 32.0 * intensity
confetti.lifetime = 15.0 * intensity
confetti.lifetimeRange = 0
confetti.name = "confetti"
confetti.color = color.cgColor
confetti.velocity = CGFloat(450.0 * intensity) // orig 450
confetti.velocityRange = CGFloat(80.0 * intensity)
confetti.emissionLongitude = .pi
confetti.emissionRange = .pi / 4
confetti.spin = CGFloat(3.5 * intensity)
confetti.spinRange = 300 * (.pi / 180.0)
confetti.scaleRange = CGFloat(intensity)
confetti.scaleSpeed = CGFloat(-0.1 * intensity)
confetti.contents = #imageLiteral(resourceName: "confetti").cgImage
confetti.beginTime = CACurrentMediaTime()
return confetti
}
func isActive() -> Bool {
return self.active
}
}
view representable
struct ConfettiView: UIViewRepresentable {
#Binding var isStarted: Bool
func makeUIView(context: Context) -> ConfettiParticleView {
return ConfettiParticleView()
}
func updateUIView(_ uiView: ConfettiParticleView, context: Context) {
if isStarted && !uiView.isActive() {
uiView.startConfetti()
print("confetti started")
} else if !isStarted {
uiView.stopConfetti()
print("confetti stopped")
}
}
}
swiftui view for testing
struct ConfettiViewTest: View {
#State var isStarted = false
var body: some View {
ZStack {
ConfettiView(isStarted: $isStarted)
.ignoresSafeArea()
Button(action: {
isStarted = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
isStarted = false
}
}) {
Text("toggle")
.padding()
.background(Color.white)
}
}
}
}
Related
I want to add confetti animation to a video so that when any animal/human appears then confetti animation should be running behind the human/animal's body. I used following code but here confetti is running as overlay in video.
import UIKit
import QuartzCore
class ConfettiView: UIView {
var emitter: CAEmitterLayer!
var colors: [UIColor]!
var intensity: Float!
private var active : Bool = false
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup() {
colors = [UIColor.red,
UIColor.green,
UIColor.blue
]
intensity = 0.2
active = false
}
func startConfetti() {
emitter = CAEmitterLayer()
emitter.emitterPosition = CGPoint(x: frame.size.width / 2.0, y: -20)
emitter.emitterShape = CAEmitterLayerEmitterShape.line
emitter.emitterSize = CGSize(width: frame.size.width, height: 1)
var cells = [CAEmitterCell]()
for color in colors {
cells.append(confettiWithColor(color: color))
}
emitter.emitterCells = cells
layer.addSublayer(emitter)
active = true
}
func stopConfetti() {
emitter?.birthRate = 0
active = false
}
func confettiWithColor(color: UIColor) -> CAEmitterCell {
let confetti = CAEmitterCell()
confetti.birthRate = 10.0 * intensity
confetti.lifetime = 180.0 * intensity
confetti.lifetimeRange = 0
confetti.color = color.cgColor
confetti.velocity = CGFloat(350.0 * intensity)
confetti.velocityRange = CGFloat(40.0 * intensity)
confetti.emissionLongitude = CGFloat(Double.pi)
confetti.emissionRange = 0.4
confetti.spin = CGFloat(3.5 * intensity)
confetti.spinRange = 0.0
confetti.scale = 0.1
confetti.contents = UIImage(named: "image")?.cgImage
return confetti
}
}
// then I add this confettiView as a subView of VidePlayerView
I need to show/export video with confetti like this.
Watch it. Here confetti is running behind the woman.
I have an image inside a HStack, which I scale using MagnificationGesture (This is working). I have also created the code to pan the image within HStack (This is also working). The issue I have is:
I would like to restrict the pan to not exceed the image bounds (ie not show any white space around the image). Even when I scale the image, the pan should be restricted to image bounds.
Here is my working code:
import SwiftUI
struct PanMove: View {
#State private var scale : CGFloat = 1.0
#State private var lastScaleValue : CGFloat = 1.0
#State private var minScale : CGFloat = 1.0
#State private var maxScale : CGFloat = 3.0
var body: some View {
VStack {
HStack {
SFImageVR()
.scaleEffect(self.scale)
.gesture(
MagnificationGesture()
.onChanged({ val in
let delta = val/self.lastScaleValue
self.lastScaleValue = val
let newScale = self.scale * delta
if (newScale >= self.minScale && newScale <= self.maxScale) {
self.scale = newScale
}
})
.onEnded({ scale in
self.lastScaleValue = 1.0
})
)
}
.frame(width: 300, height: 450, alignment: .center)
.border(Color.black, width: 1)
.clipped()
}
}
}
struct SFImageVR: UIViewRepresentable {
typealias UIViewType = SFImageView
func makeUIView(context: Context) -> SFImageView {
var imageView = SFImageView(image: UIImage(named: "Image1"))
return imageView
}
func updateUIView(_ uiView: SFImageView, context: Context) {
}
}
class SFImageView: UIImageView, UIGestureRecognizerDelegate {
override init(image: UIImage?) {
super.init(image: image)
self.isUserInteractionEnabled = true
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func setup(){
let gesture = UIPanGestureRecognizer(target: self, action: #selector(onPan))
gesture.minimumNumberOfTouches = 1
gesture.delegate = self
self.addGestureRecognizer(gesture)
}
#objc func onPan(sender: UIPanGestureRecognizer) {
guard sender.view != nil else { return }
if sender.state == .began || sender.state == .changed {
let translation = sender.translation(in: self)
sender.view!.center = CGPoint(x: sender.view!.center.x + translation.x, y: sender.view!.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self)
}
}
}
Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 years ago.
Improve this question
I to create my own custom progressbar ie:NSProgressIndicator using swift how do I do that?
You can just make your own progress indicator, e.g.:
#IBDesignable
open class ColorfulProgressIndicator: NSView {
#IBInspectable open var doubleValue: Double = 50 { didSet { needsLayout = true } }
#IBInspectable open var minValue: Double = 0 { didSet { needsLayout = true } }
#IBInspectable open var maxValue: Double = 100 { didSet { needsLayout = true } }
#IBInspectable open var backgroundColor: NSColor = .lightGray { didSet { layer?.backgroundColor = backgroundColor.cgColor } }
#IBInspectable open var progressColor: NSColor = .blue { didSet { progressShapeLayer.fillColor = progressColor.cgColor } }
#IBInspectable open var borderColor: NSColor = .clear { didSet { layer?.borderColor = borderColor.cgColor } }
#IBInspectable open var borderWidth: CGFloat = 0 { didSet { layer?.borderWidth = borderWidth } }
#IBInspectable open var cornerRadius: CGFloat = 0 { didSet { layer?.cornerRadius = cornerRadius } }
private lazy var progressShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = progressColor.cgColor
return shapeLayer
}()
public override init(frame: NSRect = .zero) {
super.init(frame: frame)
configure()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
// needed because IB doesn't don't honor `wantsLayer`
open override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
layer = CALayer()
configure()
}
open override func layout() {
super.layout()
updateProgress()
}
open func animate(to doubleValue: Double? = nil, minValue: Double? = nil, maxValue: Double? = nil, duration: TimeInterval = 0.25) {
let currentPath = progressShapeLayer.presentation()?.path ?? progressShapeLayer.path
// stop prior animation, if any
progressShapeLayer.removeAnimation(forKey: "updatePath")
// update progress properties
if let doubleValue = doubleValue { self.doubleValue = doubleValue }
if let minValue = minValue { self.minValue = minValue }
if let maxValue = maxValue { self.maxValue = maxValue }
// create new animation
let animation = CABasicAnimation(keyPath: "path")
animation.duration = duration
animation.fromValue = currentPath
animation.toValue = progressPath
progressShapeLayer.add(animation, forKey: "updatePath")
}
}
private extension ColorfulProgressIndicator {
func configure() {
wantsLayer = true
layer?.cornerRadius = cornerRadius
layer?.backgroundColor = backgroundColor.cgColor
layer?.borderWidth = borderWidth
layer?.borderColor = borderColor.cgColor
layer?.addSublayer(progressShapeLayer)
}
func updateProgress() {
progressShapeLayer.path = progressPath
}
var progressPath: CGPath? {
guard minValue != maxValue else { return nil }
let percent = max(0, min(1, CGFloat((doubleValue - minValue) / (maxValue - minValue))))
let rect = NSRect(origin: bounds.origin, size: CGSize(width: bounds.width * percent, height: bounds.height))
return CGPath(rect: rect, transform: nil)
}
}
You can either just set its doubleValue, minValue and maxValue, or if you want to animate the change, just:
progressIndicator.animate(to: 75)
For example, below I set the progressColor and borderColor to .red, set the borderWidth to 1, set the cornerRadius to 10. I then started animating to 75, and then, before it’s even done, triggered another animation to 100 (to illustrate that animations can pick up from wherever it left off):
There are tons of ways of implementing this (so get too lost in the details of the implementation, above), but it illustrates that creating our own progress indicators is pretty easy.
You can subclass it just like any other view. But for all you're doing that is likely unnecessary.
class CustomIndicator: NSProgressIndicator {
// ...
}
As far as setting the height goes, you can do this by initializing the view with a custom frame.
let indicator = NSProgressIndicator(frame: NSRect(x: 0, y: 0, width: 100, height: 20))
There is a property called controlTint that you can set on NSProgressIndicator, but this only allows you to set the color to the predefined ones under NSControlTint. For truly custom colors, I'd recommend this route using Quartz filters via the Interface Builder.
I want to add a UITapGestureRecognizer to my view named SetView. My setviews are created programmatically on another custom view called GridView.
This is what I have tried so far but I am not seeing any action while tapping my subvies.
import UIKit
#IBDesignable
class GridView: UIView {
private(set) lazy var deckOfCards = createDeck()
lazy var grid = Grid(layout: Grid.Layout.fixedCellSize(CGSize(width: 128.0, height: 110.0)), frame: CGRect(origin: CGPoint(x: bounds.minX, y: bounds.minY), size: CGSize(width: bounds.width, height: bounds.height)))
lazy var listOfSetCard = createSetCards()
private func createDeck() -> [SetCard] {
var deck = [SetCard]()
for shape in SetCard.Shape.allShape {
for color in SetCard.Color.allColor {
for content in SetCard.Content.allContent {
for number in SetCard.Number.allNumbers {
deck.append(SetCard(shape: shape, color: color, content: content, rank: number))
}
}
}
}
deck.shuffle()
return deck
}
private func createSetCards() -> [SetView] {
var cards = [SetView]()
for _ in 0..<cardsOnScreen {
let card = SetView()
let contentsToBeDrawn = deckOfCards.removeFirst()
card.combinationOnCard.shape = contentsToBeDrawn.shape
card.combinationOnCard.color = contentsToBeDrawn.color
card.combinationOnCard.content = contentsToBeDrawn.content
card.combinationOnCard.rank = contentsToBeDrawn.rank
/* print(contentsToBeDrawn.color) */
addSubview(card)
cards.append(card)
}
return cards
}
override func layoutSubviews() {
super.layoutSubviews()
for index in listOfSetCard.indices {
let card = listOfSetCard[index]
if let rect = grid[index] {
card.frame = rect.insetBy(dx: 2.5, dy: 2.5)
card.frame.origin = rect.origin
print(card.frame.origin)
}
}
}
Here is the function didTap(sender: UITapGestureRecognizer) that I wrote on SetView:
#objc func didTap(sender: UITapGestureRecognizer) {
switch sender.state {
case .changed,.ended:
let rect = UIBezierPath(rect: bounds)
fillBoundingRect(inRect: rect, color: UIColor.gray)
default:
break
}
And ViewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...12 {
let card = game.drawModelCard()
game.deck.append(card)
}
}
lazy var game = SetGame()
weak var setView : SetView! {
didSet {
let tapGestureRecognizer = UITapGestureRecognizer(target:
setView, action: #selector(SetView.didTap(sender:)))
setView.isUserInteractionEnabled = true
setView.addGestureRecognizer(tapGestureRecognizer)
}
}
}
My subviews(SetViews) should change the background color once tapped.
The following is my code. I want to add a little white tick to middle when i clicked the button. How can i do that by programming but not using image...
import Foundation
import UIKit
#IBDesignable
class SSRadioButton: UIButton {
private var circleLayer = CAShapeLayer()
private var fillCircleLayer = CAShapeLayer()
override var selected: Bool {
didSet {
toggleButon()
}
}
/**
Color of the radio button circle. Default value is UIColor red.
*/
#IBInspectable var circleColor: UIColor = UIColor.redColor() {
didSet {
circleLayer.strokeColor = circleColor.CGColor
self.toggleButon()
}
}
/**
Radius of RadioButton circle.
*/
#IBInspectable var circleRadius: CGFloat = 5.0
#IBInspectable var cornerRadius: CGFloat {
get {
return layer.cornerRadius
}
set {
layer.cornerRadius = newValue
layer.masksToBounds = newValue > 0
}
}
private func circleFrame() -> CGRect {
var circleFrame = CGRect(x: 0, y: 0, width: 2*circleRadius, height: 2*circleRadius)
circleFrame.origin.x = 0 + circleLayer.lineWidth
circleFrame.origin.y = bounds.height/2 - circleFrame.height/2
return circleFrame
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
private func initialize() {
circleLayer.frame = bounds
circleLayer.lineWidth = 2
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.strokeColor = circleColor.CGColor
layer.addSublayer(circleLayer)
fillCircleLayer.frame = bounds
fillCircleLayer.lineWidth = 2
fillCircleLayer.fillColor = UIColor.clearColor().CGColor
fillCircleLayer.strokeColor = UIColor.clearColor().CGColor
layer.addSublayer(fillCircleLayer)
self.titleEdgeInsets = UIEdgeInsetsMake(0, (4*circleRadius + 4*circleLayer.lineWidth), 0, 0)
self.toggleButon()
}
/**
Toggles selected state of the button.
*/
func toggleButon() {
if self.selected {
fillCircleLayer.fillColor = circleColor.CGColor
} else {
fillCircleLayer.fillColor = UIColor.clearColor().CGColor
}
}
private func circlePath() -> UIBezierPath {
return UIBezierPath(ovalInRect: circleFrame())
}
private func fillCirclePath() -> UIBezierPath {
return UIBezierPath(ovalInRect: CGRectInset(circleFrame(), 2, 2))
}
override func layoutSubviews() {
super.layoutSubviews()
circleLayer.frame = bounds
circleLayer.path = circlePath().CGPath
fillCircleLayer.frame = bounds
fillCircleLayer.path = fillCirclePath().CGPath
self.titleEdgeInsets = UIEdgeInsetsMake(0, (2*circleRadius + 4*circleLayer.lineWidth), 0, 0)
}
override func prepareForInterfaceBuilder() {
initialize()
}
}
Something like this?
You could do achieve by using UIBezierPath to draw a couple of lines that make a tick. Unless you were looking for something more fancy or curvy? The answer to this question: Draw a line with UIBezierPath
has a nice little function that simplifies the process of drawing the lines.