I have a CALayer and I'd like to animate a property on it. But, the property that triggers the animation change is different to the one that is being changed. A basic example of what I'm trying to do is below. I'm trying to create an animation on count by changing triggerProperty. This example is simplified (in my project, the triggerProperty is not an Int, but a more complex non-animatable type. So, I'm trying to animate it by creating animations for some of it's properties that can be matched to CABasicAnimation - and rendering a version of that class based on the interpolated values).
@objc
class AnimatableLayer: CALayer {
@NSManaged var triggerProperty: Int
@NSManaged var count: Int
override init() {
super.init()
triggerProperty = 1
setNeedsDisplay()
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override class func needsDisplay(forKey key: String) -> Bool {
return key == String(keypath: \AnimatableLayer.triggerProperty) || super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> (any CAAction)? {
if event == String(keypath: \AnimatableLayer.triggerProperty) {
if let presentation = self.presentation() {
let keyPath = String(keypath: \AnimatableLayer.count)
let animation = CABasicAnimation(keyPath: keyPath)
animation.duration = 2.0
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.fromValue = presentation.count
animation.toValue = 10
return animation
}
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
print("draw")
NSGraphicsContext.saveGraphicsState()
let nsctx = NSGraphicsContext(cgContext: ctx, flipped: true) // create NSGraphicsContext
NSGraphicsContext.current = nsctx // set current context
let renderText = NSAttributedString(string: "\(self.presentation()?.count ?? self.count)", attributes: [.font: NSFont.systemFont(ofSize: 30)])
renderText.draw(in: bounds)
NSGraphicsContext.restoreGraphicsState()
}
func animate() {
print("animate")
self.triggerProperty = 10
}
}
With this code, the animation isn't triggered. It seems to get triggered only if the animation's keypath matches the one on the event (in the action func).
Is it possible to do something like this?
Core Animation
RSS for tagRender, compose, and animate visual elements using Core Animation.
Posts under Core Animation tag
47 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
I am working on a SceneKit project where I use a CAShapeLayer as the content for SCNMaterial's diffuse.contents to display a progress bar. Here's my initial code:
func setupProgressWithCAShapeLayer() {
let progressLayer = createProgressLayer()
progressBarPlane?.firstMaterial?.diffuse.contents = progressLayer
DispatchQueue.main.async {
var progress: CGFloat = 0.0
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
progress += 0.01
if progress > 1.0 {
progress = 0.0
}
progressLayer.strokeEnd = progress // Update progress
}
}
}
// MARK: - ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
progressBarPlane = SCNPlane(width: 0.2, height: 0.2)
setupProgressWithCAShapeLayer()
let planeNode = SCNNode(geometry: progressBarPlane)
planeNode.position = SCNVector3(x: 0, y: 0.2, z: 0)
node.addChildNode(planeNode)
}
This works fine, and the progress bar updates smoothly. However, when I change the code to use a class property (self.progressLayer) instead of a local variable, the rendering starts flickering on the screen:
func setupProgressWithCAShapeLayer() {
self.progressLayer = createProgressLayer()
progressBarPlane?.firstMaterial?.diffuse.contents = progressLayer
DispatchQueue.main.async { [weak self] in
var progress: CGFloat = 0.0
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in
progress += 0.01
if progress > 1.0 {
progress = 0.0
}
self?.progressLayer?.strokeEnd = progress // Update progress
}
}
}
After this change, the progressBarPlane in SceneKit starts flickering while being rendered on the screen.
My Question:
Why does switching from a local variable (progressLayer) to a class property (self.progressLayer) cause the flickering issue in SceneKit rendering?
I plan to create a simple motion graphics software for macOS that animates text, basic shapes, and handles audio. I'll use SwiftUI for the UI.
What are the commonly used technologies for rendering animated graphics? Core Animation is suitable for UI animations but not for exporting and controlling UI animations.
Basic requirements:
Timeline user interface
Animation of text and basic shapes
Viewer in SwiftUI GUI with transport control (play, pause, scrub, …)
Export to video file
Is Metal or Core Graphics typically used directly? I want to keep it as simple as possible.
I'm new to developing with SwiftUI and I created a Pomodoro app for macOS that runs in the menu bar. I added 4 animations and when the user selects the snow animation, it starts snowing on the screen. But the app uses 20%-30% of the CPU and has high energy consumption. I can't reduce it and I couldn't find a solution.
// snow animation
import SwiftUI
struct SnowflakeView: View {
@State private var flakeYPosition: CGFloat = -100
@State private var isAnimating = false
private let flakeSize: CGFloat = CGFloat.random(in: 10...30)
private let flakeColor: Color = Color(
red: Double.random(in: 0.8...1),
green: Double.random(in: 0.9...1),
blue: Double.random(in: 1...1),
opacity: Double.random(in: 0.6...0.8)
)
private let animationDuration: Double = Double.random(in: 1...3)
private let flakeXPosition: CGFloat = CGFloat.random(in: 0...310)
var body: some View {
Text("❄️")
.font(.system(size: flakeSize))
.foregroundColor(flakeColor)
.position(x: flakeXPosition, y: flakeYPosition)
.onAppear {
if !isAnimating {
withAnimation(Animation.linear(duration: animationDuration).repeatForever(autoreverses: false)) {
flakeYPosition = 280 + 50
}
isAnimating = true
}
}
}
}
I also have how I run the animation below.
ZStack {
ForEach(0..<10, id: \.self) { index in
if selectedAnimal == "Snow" {
SnowflakeView()
} else if selectedAnimal == "Rain" {
RainDropAnimation()
}else if selectedAnimal == "Leaf"{
LeafFallAnimation()
}else if selectedAnimal == "Confetti"{
ConfettiAnimation()
}
}
}
I have facing an above crash for many users device running on iOS 17.6.1 mostly on iPad devices. I'm not sure why this happening only in 17.X. In Xcode Organizer unable to see this crash in any devices running on OS 18.x. Our app crashes got spiked due to this. I am unable to fix or reproduce the same. The crash log is not pointing to our app code to find the root cause and fix this issue. Have attached the crash log in this post also the crash log roles have mixed values Background &amp; Foreground. But most of the crash is in background.
Is this any crash related to system and that only solved by OS update? I have updated the app using Xcode 16 and 16.1 still facing this crash unable to symbolicate the crash report as well.
Any ideas/solution how to solve this or how to proceed further. Have attached the entire crash log below.
RoleBackgroundCrash.crash
RoleForeGroundCrash.crash
I created a simple animation of a circle that changes sizes. The circle pulses like a heartbeat in the center of the screen. My expectation was for the CPU use to be very low, but that is not the case. In addition, even if the CPU use isn't as low as I would expect, I did not expect the CPU use to increase over time because nothing else is happening in the app. Here is the code:
import SwiftUI
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
SplashScreenView()
}
}
}
import SwiftUI
struct SplashScreenView: View {
var body: some View {
ZStack {
SplashNucleusView(minSize: 50, maxSize: 100)
}
}
}
import SwiftUI
struct SplashNucleusView: View {
let minSize: Double
let maxSize: Double
@State private var nucleusColor: Color = .primary
@State private var nucleusRadius: Double = 10
@State private var nucleusOpacity: Double = 1.0
private var nucleusAnimation: Animation {
.easeInOut(duration: 0.25)
.repeatForever(autoreverses: true)
}
let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
Circle()
.fill(nucleusColor)
.frame(width: nucleusRadius)
.opacity(nucleusOpacity)
.onReceive(timer) { _ in
withAnimation(nucleusAnimation) {
nucleusRadius = Double.random(in: minSize...maxSize)
}
}
}
}
This is how the animation looks:
The animation is snappy until the CPU use reaches 95%, at which point there is visible stuttering. Here is how the CPU looks when the animation duration value is 0.5 seconds and the timer publishing interval is 3 seconds:
Changing the animation duration value to 0.25 seconds and the timer publishing interval to 0.5 seconds changes the CPU use as follows:
The complete view has many other moving parts, which make the issue much worse. The issue is evident with only the circle view. I spent hours working with the debugger, reading about animations, and testing new approaches, but the result is always the same.
Why is this happening?
I am looking to implement CAMetalDisplayLink on a separate thread on a macOS application. I am basing my implementation on the following example project:
Achieving Smooth Frame Rates with Metal Display Link
This project allows you to configure whether a separate thread is used for rendering by setting RENDER_ON_MAIN_THREAD in GameConfig to 0. However, when I set it to use a separate thread nothing is rendered. Stepping through the code shows that a separate thread is created, but a CAMetalDisplayLinkUpdate is never received. Does anyone know why this does not work?
I found that there is such a click-to-expand horizontally and smoothly effect in the system application called "message", which is good. I wonder if I can add a similar effect to my own app. If possible, are there any implementation ideas or examples that I can refer to? Thanks!
Hi.
Since Xcode 16 and/or iOS 18.0 (I upgraded at the same time), I have an strange effect in the lower (let's say) 20% section of the Navigation Bar when changing to another tab, and this independently if large titles are used or not. Mentioned section is brighter or darker than the rest of the Navigation Bar background, depending on which background tint is used. This effect lasts about 0.3 seconds, but is clearly visible, quite disturbing and new as of Xcode 16 and/or iOS 18.0.
I use the code below in AppDelegate to get a gradient coloured Navigation Bar background.
let appearance = UINavigationBarAppearance()
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().compactAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
UINavigationBar.appearance().compactScrollEdgeAppearance = appearance
If I don't use above code., the background color is filled and without gradient. Subject effect doesn't show in this case.
The effect basically looks like when changing tab, the new Navigation Bar background doesn't clear right away, and keeps the background from the previous Navigation Bar for 0.3 seconds before new one Navigation Bar background is rendered.
I spent quite some time on changing every possible setting, in code as well as storyboard ... no success so far.
Any ideas how to disable this undesired animation?
Hi Friends!
I’m facing an issue with SceneKit.
I’m developing a 3D mobile game. I have a character 3D model and several skeletal animations CAAnimation. I import both the model and the animations from Maya in *.dae format. The character’s animations play continuously one after the other, with each new animation being triggered randomly. The transition between animations makes smoothly by setting the fadeInDuration and fadeOutDuration properties. Here’s an example of the code:
import UIKit import QuartzCore import SceneKit
class TestAnimationController: UIViewController {
var bodyNode: SCNNode?
override func viewDidLoad() {
super.viewDidLoad()
let scnView = SCNView(frame: self.view.bounds)
scnView.backgroundColor = .black // Set your desired background color
scnView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let scene = SCNScene(named: "art.scnassets/scene/Base_room/ROOM5.scn")!
bodyNode = collada2SCNNode(filepath: "art.scnassets/female/girl_body_races.dae")!
bodyNode?.renderingOrder = 10
scene.rootNode.addChildNode(bodyNode!)
playIdleAnimation()
scnView.scene = scene // Assign the scene to the SCNView
self.view.addSubview(scnView) // Add the SCNView to your main view)
}
func collada2SCNNode(filepath:String) -> SCNNode? { if let scene = SCNScene(named: filepath) { let node = scene.rootNode.childNodes[0] return node } else { return nil } }
func playIdleAnimation() {
let array = [
"art.scnassets/female/animations/idle/girl_idle_4.dae",
"art.scnassets/female/animations/idle/girl_idle1.dae",
"art.scnassets/female/animations/idle/girl_idle2.dae",
"art.scnassets/female/animations/idle/Girl_idle3.dae",
]
let animation = CAAnimation.animationWithSceneNamed(array.randomElement() ?? "")!
self.setAnimationAdd(
fadeInDuration: 1.0,
fadeOutDuration: 1.0,
keyTime: 0.99,
animation,
isLooped: false
) { [weak self] in
guard let self = self else { return }
try? self.playBoringAnimations()
}
}
func playBoringAnimations() {
let array = [
"art.scnassets/female/animations/boring/girl_boring1.dae",
"art.scnassets/female/animations/boring/girl_boring2.dae",
"art.scnassets/female/animations/boring/girl_boring3.dae",
"art.scnassets/female/animations/boring/girl_boring4.dae",
"art.scnassets/female/animations/boring/girl_boring5.dae",
"art.scnassets/female/animations/boring/girl_boring6.dae",
"art.scnassets/female/animations/boring/girl_boring8.dae"
]
let animation = CAAnimation.animationWithSceneNamed(array.randomElement() ?? "")!
self.setAnimationAdd(
fadeInDuration: 1.0,
fadeOutDuration: 1.0,
keyTime: 0.99,
animation,
isLooped: false
) { [weak self] in
guard let self = self else { return }
try? self.playIdleAnimation()
}
}
func setAnimationAdd(fadeInDuration : CGFloat, fadeOutDuration : CGFloat, keyTime : CGFloat, _ animation: CAAnimation, isLooped: Bool, completion: (() -> Void)?) {
animation.fadeInDuration = fadeInDuration
animation.fadeOutDuration = fadeOutDuration
if !isLooped {
animation.repeatCount = 1
} else {
animation.repeatCount = Float.greatestFiniteMagnitude
}
animation.animationEvents = [
SCNAnimationEvent(keyTime: keyTime, block: { _, _, _ in completion?() })
]
bodyNode?.addAnimation(animation, forKey: "avatarAnimation")
}
}
Everything worked perfectly until I updated to iOS 18. On a physical device, the animations now transition abruptly without the smooth blending that was present in earlier iOS versions. The switch between them is very noticeable, as if the fadeInDuration and fadeOutDuration parameters are being ignored.
However, in the iOS 18 simulator, the animations still transition smoothly as before.
Here two example videos - IOS 17.5 and IOS 18 https://youtube.com/shorts/jzoMRF4skAQ - IOS 17,5 smooth https://youtube.com/shorts/VJXrZzO9wl0 - IOS 18 not smooth
I try this code in IOS 17.5, everything works fine
Does anyone have any ideas on how to fix this issue?
Hello,
I want to create a painting app for iOS and I saw many examples use a CAShapeLayer to draw a UIBezierPath.
As I understand CoreAnimation uses the GPU so I was wondering how is this implemented on the GPU? Or in other words, how would you do it with Metal or OpenGL?
I can only think of continuously updating a texture in response to the user's drawing but that would be a very resource intensive operation...
Thanks
UI:
Attachment(id: "tooptip") {
if isRecording {
TooltipView {
HStack(spacing: 8) {
Image(systemName: "waveform")
.font(.title)
.frame(minWidth: 100)
}
}
.transition(.opacity.combined(with: .scale))
}
}
Trigger:
Button("Toggle") {
withAnimation{
isRecording.toggle()
}
}
The above code did not show the animation effect when running. When I use isRecording to drive an element in a common SwiftUI view, there is an animation effect.
Hello, I noticed that the CADisplayLink seems to emit incorrect targetTimestamp and timestamp in the iOS 18 simulator. If you compute the actual duration of a frame, the duration is always a negative number.
This only occurs in the iOS 18 simulator.
I have a 3х3 Matrix which I need to apply to UIImage and save it in Documents folder. I successfully converted the 3x3 Matrix (represented as [[Double]]) to CATrasform3D and then I have broken my head with trying to figure out how to apply it to UIImage.
The only property where I can I apply it is UIView(or UIImageView in case with working with UIImage) transform property. But it has nothing to do with UIImage itself. I can't save the UIImage from transformed the UIImageView with all the transformations.
And all the CoreGraphic methods (like concatenate for CGContext) only work with affine transformations which not suits for me.
Please give me a hint what direction I should look.
Does Apple has native methods or I have to use 3rd party frameworks for this functionality?
I have a game for iOS where I use CADisplayLink to animate a simulation, and for some reason the animation is not getting the full 120hz on capable devices (like iPhone 15 Pro). When I enable a 120hz refresh target, the animation is capped at only 90hz. This looks terrible because the animation works best when doubled (30, 60, 120, 240, etc).
The really bizarre thing is that when I turn on Screen Recording, my frame rate instantly jumps to 120, and everything looks perfectly smooth. My game has never looked better on iPhone! When recording is stopped, the animation drops back down to 90 fps. What in the world is going on?
[displayLink setPreferredFrameRateRange:CAFrameRateRangeMake(100,240,120)]; //Min. Max, Preferred [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
(Also, CADisableMinimumFrameDurationOnPhone is set to True in info.plist)
In iOS 18.0, UICollectionView/scrollToItem(at:at:animated:) causes UICollectionView to scroll with an awkward stuttered animation at about 4 frames per second on the device.
This behavior is not observed in iOS 15, 16, or 17.
I submitted FB13986989 about this issue 6 weeks ago when I first observed it in iOS 18 beta 1, but I have not received a response.
The issue has not been fixed in iOS 18.0 beta 2, 3, or 4.
Is this a known issue? Does Apple intend to fix it?
Assuming Apple has not prioritized fixing this bug, could a UIKit engineer suggest a workaround?
My app relies on scrollTo working properly and now looks awful on iOS 18.
Thank you for any assistance you can provide.
I want to achieve Fold animation when the user scrolls UICollectionView. I have UICollectionView with full-screen size cell and vertically scrolling with paging enabled. For that I've created sub-class of UICollectionViewFlowLayout which is as described below.
class FoldingFlowLayout: UICollectionViewFlowLayout {
private let logger = Logger(subsystem: bundleIdentifier, category: "FlowLayout")
override func prepare() {
super.prepare()
scrollDirection = .vertical
minimumLineSpacing = 0
minimumInteritemSpacing = 0
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
attributes?.forEach { attribute in
transformLayoutAttributes(attribute)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
private func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = collectionView else { return }
let contentOffsetY = collectionView.contentOffset.y
let cellOffsetY = attributes.frame.origin.y - contentOffsetY
let cellHeight = attributes.frame.height
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 500.0 // Apply perspective
if cellOffsetY < cellHeight && cellOffsetY > -cellHeight {
let angle = (cellOffsetY / cellHeight) * .pi / 2
transform = CATransform3DRotate(transform, angle, -1, 0, 0)
attributes.transform3D = transform
attributes.alpha = 1.0 - (abs(cellOffsetY) / cellHeight)
} else {
attributes.transform3D = CATransform3DIdentity
attributes.alpha = 1.0
}
}
}
But this is not working as I expected. I want to create replica of this kind of animation.
What am I missing here?
I'm making a loading screen, but I can't figure out how to make the loading indicator animate smoothly while work is being performed. I've tried a variety of tactics, including creating confining the animation to a .userInitiated Task, or downgrading the loading Task to .background, and using TaskGroups. All of these resulted in hangs, freezes, or incredibly long load times.
I've noticed that standard ProgressViews work fine when under load, but the documentation doesn't indicate why this is the case. Customized ProgressViews don't share this trait (via .progressViewStyle()) also choke up. Finding out why might solve half the problem.
Note: I want to avoid async complications that come with using nonisolated functions. I've used them elsewhere, but this isn't the place for them.
I want to achieve Fold animation when the user scrolls UICollectionView. I have UICollectionView with full-screen size cell and vertically scrolling with paging enabled. For that I've created sub-class of UICollectionViewFlowLayout which is as described below.
class FoldingFlowLayout: UICollectionViewFlowLayout {
private let logger = Logger(subsystem: bundleIdentifier, category: "FlowLayout")
override func prepare() {
super.prepare()
scrollDirection = .vertical
minimumLineSpacing = 0
minimumInteritemSpacing = 0
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributes = super.layoutAttributesForElements(in: rect)
attributes?.forEach { attribute in
transformLayoutAttributes(attribute)
}
return attributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
private func transformLayoutAttributes(_ attributes: UICollectionViewLayoutAttributes) {
guard let collectionView = collectionView else { return }
let contentOffsetY = collectionView.contentOffset.y
let cellOffsetY = attributes.frame.origin.y - contentOffsetY
let cellHeight = attributes.frame.height
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 500.0 // Apply perspective
if cellOffsetY < cellHeight && cellOffsetY > -cellHeight {
let angle = (cellOffsetY / cellHeight) * .pi / 2
transform = CATransform3DRotate(transform, angle, -1, 0, 0)
attributes.transform3D = transform
attributes.alpha = 1.0 - (abs(cellOffsetY) / cellHeight)
} else {
attributes.transform3D = CATransform3DIdentity
attributes.alpha = 1.0
}
}
}
But this is not working as I expected. I want to create replica of this kind of animation.
What am I missing here?
I am trying to debug a crash due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSFrozenDictionary layoutSublayers]: unrecognized selector sent to instance.
I see 2 things that I find interesting about it.
The fact that the instance is a __NSFrozenDictionary tells me that the reference to a CALayer that has since been evicted from memory and re-written.
The call to layoutSublayers tells me that the CALayer was dealloc-ed at some point between the call to setNeedsLayout (or layoutIfNeeded)
This seemingly occurs as part of a call to -[UITableView reloadData]
Furthermore, each cell created by the UITableView has a UIStackView.
As part of the call to cellForRowAtIndexPath the code adds an instance of "custom view" to the stack view's subviews.
As part of the call to prepareForReuse the code removes the "custom view" from the stack view's subviews.
Therefore as part of the prepareForReuse the "custom view" (and its layer) is evicted from memory.
My theory is that the tableview does a layout pass on the visible cell which has since had its subview removed which causes the crash.
My question is what are the constraints on when/where to call reloadData and/or when/where you should definitely avoid it as it relates to this context?
This is code that modifies the view hierarchy of the cell as part of its lifecycle which AFAIK this is "not supported" since prepareForReuse is meant to be used for resetting view state and cellForRowAtIndexPath to reset content.
In that sense, another question, are you not allowed to modify the cell view hierarchy period as part of the cycle to draw the visible cells or is it more of a case, do not call reloadData?