Files
skills/ios-application-dev/references/graphics-animation.md
shihao 6487becf60 Initial commit: add all skills files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 16:52:49 +08:00

351 lines
9.0 KiB
Markdown

# Graphics & Animation
iOS graphics and animation guide covering CAShapeLayer, CAGradientLayer, UIBezierPath, and Core Animation.
## CAShapeLayer
For custom shapes, paths, and animations:
```swift
class CircularProgressView: UIView {
private let trackLayer = CAShapeLayer()
private let progressLayer = CAShapeLayer()
var progress: CGFloat = 0 {
didSet { updateProgress() }
}
override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
}
private func setupLayers() {
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - 10
let startAngle = -CGFloat.pi / 2
let endAngle = startAngle + 2 * CGFloat.pi
let circularPath = UIBezierPath(
arcCenter: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true
)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.systemGray5.cgColor
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineWidth = 10
trackLayer.lineCap = .round
layer.addSublayer(trackLayer)
progressLayer.path = circularPath.cgPath
progressLayer.strokeColor = UIColor.systemBlue.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = 10
progressLayer.lineCap = .round
progressLayer.strokeEnd = 0
layer.addSublayer(progressLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
setupLayers()
}
private func updateProgress() {
progressLayer.strokeEnd = progress
}
func animateProgress(to value: CGFloat, duration: TimeInterval = 0.5) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = progressLayer.strokeEnd
animation.toValue = value
animation.duration = duration
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
progressLayer.strokeEnd = value
progressLayer.add(animation, forKey: "progressAnimation")
}
}
```
## UIBezierPath
### Common Shapes
```swift
let roundedRect = UIBezierPath(
roundedRect: bounds,
cornerRadius: 12
)
let customCorners = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(width: 16, height: 16)
)
let triangle = UIBezierPath()
triangle.move(to: CGPoint(x: bounds.midX, y: 0))
triangle.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
triangle.addLine(to: CGPoint(x: 0, y: bounds.maxY))
triangle.close()
let circle = UIBezierPath(
arcCenter: CGPoint(x: bounds.midX, y: bounds.midY),
radius: bounds.width / 2,
startAngle: 0,
endAngle: .pi * 2,
clockwise: true
)
```
### Custom Paths
```swift
let customPath = UIBezierPath()
customPath.move(to: CGPoint(x: 0, y: bounds.height))
customPath.addCurve(
to: CGPoint(x: bounds.width, y: 0),
controlPoint1: CGPoint(x: bounds.width * 0.3, y: bounds.height),
controlPoint2: CGPoint(x: bounds.width * 0.7, y: 0)
)
```
## CAGradientLayer
### Linear Gradient Button
```swift
class GradientButton: UIButton {
private let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupGradient()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupGradient()
}
private func setupGradient() {
gradientLayer.colors = [
UIColor.systemBlue.cgColor,
UIColor.systemPurple.cgColor
]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
gradientLayer.cornerRadius = 12
layer.insertSublayer(gradientLayer, at: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
```
### Gradient Background View
```swift
class GradientBackgroundView: UIView {
private let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupGradient()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupGradient()
}
private func setupGradient() {
gradientLayer.colors = [
UIColor.systemBackground.cgColor,
UIColor.secondarySystemBackground.cgColor
]
gradientLayer.locations = [0.0, 1.0]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 1)
layer.insertSublayer(gradientLayer, at: 0)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
gradientLayer.colors = [
UIColor.systemBackground.cgColor,
UIColor.secondarySystemBackground.cgColor
]
}
}
```
### Gradient Types
| Type | Configuration |
|------|---------------|
| Linear (horizontal) | `startPoint: (0, 0.5)`, `endPoint: (1, 0.5)` |
| Linear (vertical) | `startPoint: (0.5, 0)`, `endPoint: (0.5, 1)` |
| Diagonal | `startPoint: (0, 0)`, `endPoint: (1, 1)` |
| Radial | Use `CAGradientLayer.type = .radial` |
## Core Animation
### Basic Animation
```swift
func animateScale() {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 1.2
animation.duration = 0.3
animation.autoreverses = true
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
layer.add(animation, forKey: "scaleAnimation")
}
func animatePosition() {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = layer.position
animation.toValue = CGPoint(x: 200, y: 200)
animation.duration = 0.5
layer.add(animation, forKey: "positionAnimation")
}
```
### Keyframe Animation
```swift
func animateAlongPath() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addCurve(
to: CGPoint(x: 250, y: 250),
controlPoint1: CGPoint(x: 150, y: 50),
controlPoint2: CGPoint(x: 50, y: 250)
)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path.cgPath
animation.duration = 2.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
layer.add(animation, forKey: "pathAnimation")
}
```
### Animation Group
```swift
func animateMultiple() {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.0
scaleAnimation.toValue = 1.5
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 1.0
opacityAnimation.toValue = 0.0
let group = CAAnimationGroup()
group.animations = [scaleAnimation, opacityAnimation]
group.duration = 0.5
group.fillMode = .forwards
group.isRemovedOnCompletion = false
layer.add(group, forKey: "multipleAnimations")
}
```
### Spring Animation
```swift
func springAnimation() {
let spring = CASpringAnimation(keyPath: "transform.scale")
spring.fromValue = 0.8
spring.toValue = 1.0
spring.damping = 10
spring.stiffness = 100
spring.mass = 1
spring.initialVelocity = 5
spring.duration = spring.settlingDuration
layer.add(spring, forKey: "springAnimation")
}
```
## UIView Animation
### Basic UIView Animation
```swift
UIView.animate(withDuration: 0.3) {
self.view.alpha = 1.0
self.view.transform = .identity
}
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseInOut]) {
self.cardView.frame.origin.y = 100
} completion: { _ in
self.didFinishAnimation()
}
```
### Spring Animation
```swift
UIView.animate(
withDuration: 0.6,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: []
) {
self.popupView.transform = .identity
}
```
### Keyframe Animation
```swift
UIView.animateKeyframes(withDuration: 1.0, delay: 0) {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25) {
self.view.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25) {
self.view.transform = CGAffineTransform(rotationAngle: .pi / 4)
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
self.view.transform = .identity
}
}
```
## Timing Functions
| Name | Description |
|------|-------------|
| `.linear` | Constant speed |
| `.easeIn` | Slow start |
| `.easeOut` | Slow end |
| `.easeInEaseOut` | Slow start and end |
| `.default` | System default |
---
*UIKit, Core Animation, and Apple are trademarks of Apple Inc.*