简体中文 繁體中文 English Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français Japanese

站内搜索

搜索

活动公告

通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31

使用Swift编程语言实现炫酷扫描动画效果的完整教程从基础到进阶让你的iOS应用界面更具吸引力

SunJu_FaceMall

3万

主题

153

科技点

3万

积分

大区版主

碾压王

积分
32103
发表于 2025-9-17 18:10:06 | 显示全部楼层 |阅读模式 [标记阅至此楼]

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
扫描动画效果是现代iOS应用中常见的UI元素,它可以用于表示数据加载、内容刷新、搜索过程或仅仅作为视觉装饰。这种动画效果能够吸引用户的注意力,提供视觉反馈,并增强用户体验。在本教程中,我们将学习如何使用Swift编程语言从基础到进阶实现各种炫酷的扫描动画效果。

1. 基础知识

在开始实现扫描动画之前,我们需要了解一些基础知识:

SwiftUI和UIKit

SwiftUI是Apple推出的现代UI框架,它使用声明式语法来构建用户界面。而UIKit是iOS开发的传统UI框架,它使用命令式编程方式。在本教程中,我们将主要使用SwiftUI,因为它更适合创建动画效果,但也会介绍一些UIKit的实现方式。

动画基础

在Swift中,动画通常通过以下方式实现:

• 使用withAnimation函数包装状态变化
• 使用animation修饰符指定动画类型
• 使用Animatable协议创建自定义动画

2. 简单扫描动画实现

让我们从最基础的扫描动画开始。我们将创建一个水平移动的扫描线效果。

使用SwiftUI创建基础扫描效果
  1. import SwiftUI
  2. struct BasicScanView: View {
  3.     @State private var scanPosition: CGFloat = -100
  4.     @State private var isScanning = false
  5.    
  6.     var body: some View {
  7.         ZStack {
  8.             // 背景内容
  9.             Rectangle()
  10.                 .fill(Color.gray.opacity(0.2))
  11.                 .frame(height: 200)
  12.                 .cornerRadius(10)
  13.             
  14.             // 扫描线
  15.             Rectangle()
  16.                 .fill(
  17.                     LinearGradient(
  18.                         gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
  19.                         startPoint: .leading,
  20.                         endPoint: .trailing
  21.                     )
  22.                 )
  23.                 .frame(width: 100, height: 200)
  24.                 .offset(x: scanPosition)
  25.                 .opacity(isScanning ? 1 : 0)
  26.         }
  27.         .onAppear {
  28.             startScanning()
  29.         }
  30.         .padding()
  31.     }
  32.    
  33.     private func startScanning() {
  34.         isScanning = true
  35.         withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
  36.             scanPosition = UIScreen.main.bounds.width + 100
  37.         }
  38.     }
  39. }
  40. struct BasicScanView_Previews: PreviewProvider {
  41.     static var previews: some View {
  42.         BasicScanView()
  43.     }
  44. }
复制代码

这段代码创建了一个简单的扫描动画效果。我们使用ZStack将背景和扫描线叠加在一起,然后通过动画改变扫描线的offset值,使其从左向右移动。扫描线使用渐变填充,使其看起来更加自然。

添加扫描效果

现在,让我们为扫描线添加一个发光效果,使其看起来更加炫酷:
  1. import SwiftUI
  2. struct GlowingScanView: View {
  3.     @State private var scanPosition: CGFloat = -100
  4.     @State private var isScanning = false
  5.    
  6.     var body: some View {
  7.         ZStack {
  8.             // 背景内容
  9.             Rectangle()
  10.                 .fill(Color.gray.opacity(0.2))
  11.                 .frame(height: 200)
  12.                 .cornerRadius(10)
  13.             
  14.             // 扫描线
  15.             Rectangle()
  16.                 .fill(
  17.                     LinearGradient(
  18.                         gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
  19.                         startPoint: .leading,
  20.                         endPoint: .trailing
  21.                     )
  22.                 )
  23.                 .frame(width: 100, height: 200)
  24.                 .offset(x: scanPosition)
  25.                 .opacity(isScanning ? 1 : 0)
  26.                 .shadow(color: .blue, radius: 20, x: 0, y: 0)
  27.         }
  28.         .onAppear {
  29.             startScanning()
  30.         }
  31.         .padding()
  32.     }
  33.    
  34.     private func startScanning() {
  35.         isScanning = true
  36.         withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
  37.             scanPosition = UIScreen.main.bounds.width + 100
  38.         }
  39.     }
  40. }
  41. struct GlowingScanView_Previews: PreviewProvider {
  42.     static var previews: some View {
  43.         GlowingScanView()
  44.     }
  45. }
复制代码

通过添加.shadow(color: .blue, radius: 20, x: 0, y: 0),我们为扫描线添加了一个蓝色发光效果,使其看起来更加炫酷。

3. 进阶扫描动画

现在我们已经掌握了基础的扫描动画,让我们尝试一些更复杂的效果。

圆形扫描动画
  1. import SwiftUI
  2. struct CircularScanView: View {
  3.     @State private var scanProgress: Double = 0
  4.     @State private var isScanning = false
  5.    
  6.     var body: some View {
  7.         ZStack {
  8.             // 背景圆
  9.             Circle()
  10.                 .stroke(Color.gray.opacity(0.2), lineWidth: 20)
  11.                 .frame(width: 200, height: 200)
  12.             
  13.             // 扫描圆弧
  14.             Circle()
  15.                 .trim(from: 0, to: scanProgress)
  16.                 .stroke(
  17.                     LinearGradient(
  18.                         gradient: Gradient(colors: [Color.blue, Color.purple]),
  19.                         startPoint: .topLeading,
  20.                         endPoint: .bottomTrailing
  21.                     ),
  22.                     lineWidth: 20
  23.                 )
  24.                 .frame(width: 200, height: 200)
  25.                 .rotationEffect(.degrees(-90))
  26.                 .shadow(color: .blue, radius: 10, x: 0, y: 0)
  27.         }
  28.         .onAppear {
  29.             startScanning()
  30.         }
  31.         .padding()
  32.     }
  33.    
  34.     private func startScanning() {
  35.         isScanning = true
  36.         withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
  37.             scanProgress = 1.0
  38.         }
  39.     }
  40. }
  41. struct CircularScanView_Previews: PreviewProvider {
  42.     static var previews: some View {
  43.         CircularScanView()
  44.     }
  45. }
复制代码

这段代码创建了一个圆形扫描动画,通过trim方法控制圆弧的显示进度,并使用渐变色和阴影效果增强视觉效果。

雷达扫描动画
  1. import SwiftUI
  2. struct RadarScanView: View {
  3.     @State private var scanAngle: Double = 0
  4.     @State private var isScanning = false
  5.    
  6.     var body: some View {
  7.         ZStack {
  8.             // 背景圆
  9.             Circle()
  10.                 .stroke(Color.green.opacity(0.3), lineWidth: 1)
  11.                 .frame(width: 300, height: 300)
  12.             
  13.             Circle()
  14.                 .stroke(Color.green.opacity(0.3), lineWidth: 1)
  15.                 .frame(width: 200, height: 200)
  16.             
  17.             Circle()
  18.                 .stroke(Color.green.opacity(0.3), lineWidth: 1)
  19.                 .frame(width: 100, height: 100)
  20.             
  21.             // 扫描线
  22.             Rectangle()
  23.                 .fill(
  24.                     LinearGradient(
  25.                         gradient: Gradient(colors: [Color.clear, Color.green.opacity(0.8), Color.clear]),
  26.                         startPoint: .leading,
  27.                         endPoint: .trailing
  28.                     )
  29.                 )
  30.                 .frame(width: 300, height: 2)
  31.                 .rotationEffect(.degrees(scanAngle))
  32.                 .shadow(color: .green, radius: 5, x: 0, y: 0)
  33.         }
  34.         .onAppear {
  35.             startScanning()
  36.         }
  37.         .padding()
  38.     }
  39.    
  40.     private func startScanning() {
  41.         isScanning = true
  42.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
  43.             scanAngle = 360
  44.         }
  45.     }
  46. }
  47. struct RadarScanView_Previews: PreviewProvider {
  48.     static var previews: some View {
  49.         RadarScanView()
  50.     }
  51. }
复制代码

这段代码创建了一个雷达扫描动画,通过旋转一个带有渐变效果的矩形来模拟雷达扫描的效果。我们还添加了几个同心圆作为背景,增强雷达的感觉。

多方向扫描动画
  1. import SwiftUI
  2. struct MultiDirectionalScanView: View {
  3.     @State private var horizontalScanPosition: CGFloat = -100
  4.     @State private var verticalScanPosition: CGFloat = -100
  5.     @State private var isScanning = false
  6.    
  7.     var body: some View {
  8.         ZStack {
  9.             // 背景内容
  10.             Rectangle()
  11.                 .fill(Color.gray.opacity(0.2))
  12.                 .frame(width: 300, height: 300)
  13.                 .cornerRadius(10)
  14.             
  15.             // 水平扫描线
  16.             Rectangle()
  17.                 .fill(
  18.                     LinearGradient(
  19.                         gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
  20.                         startPoint: .leading,
  21.                         endPoint: .trailing
  22.                     )
  23.                 )
  24.                 .frame(width: 100, height: 300)
  25.                 .offset(x: horizontalScanPosition)
  26.                 .opacity(isScanning ? 1 : 0)
  27.                 .shadow(color: .blue, radius: 10, x: 0, y: 0)
  28.             
  29.             // 垂直扫描线
  30.             Rectangle()
  31.                 .fill(
  32.                     LinearGradient(
  33.                         gradient: Gradient(colors: [Color.clear, Color.red, Color.clear]),
  34.                         startPoint: .top,
  35.                         endPoint: .bottom
  36.                     )
  37.                 )
  38.                 .frame(width: 300, height: 100)
  39.                 .offset(y: verticalScanPosition)
  40.                 .opacity(isScanning ? 1 : 0)
  41.                 .shadow(color: .red, radius: 10, x: 0, y: 0)
  42.         }
  43.         .onAppear {
  44.             startScanning()
  45.         }
  46.         .padding()
  47.     }
  48.    
  49.     private func startScanning() {
  50.         isScanning = true
  51.         
  52.         // 水平扫描
  53.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
  54.             horizontalScanPosition = UIScreen.main.bounds.width + 100
  55.         }
  56.         
  57.         // 垂直扫描
  58.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false).delay(1.5)) {
  59.             verticalScanPosition = UIScreen.main.bounds.height + 100
  60.         }
  61.     }
  62. }
  63. struct MultiDirectionalScanView_Previews: PreviewProvider {
  64.     static var previews: some View {
  65.         MultiDirectionalScanView()
  66.     }
  67. }
复制代码

这段代码创建了一个多方向扫描动画,包括水平和垂直两个方向的扫描线。通过使用不同的颜色和延迟启动,我们创建了一个更加复杂的扫描效果。

4. 自定义扫描动画

现在,让我们尝试创建一些独特的自定义扫描动画。

网格扫描动画
  1. import SwiftUI
  2. struct GridScanView: View {
  3.     @State private var scanProgress: Double = 0
  4.     @State private var isScanning = false
  5.    
  6.     let gridSize = 10
  7.     let cellSize: CGFloat = 30
  8.    
  9.     var body: some View {
  10.         VStack(spacing: 0) {
  11.             ForEach(0..<gridSize, id: \.self) { row in
  12.                 HStack(spacing: 0) {
  13.                     ForEach(0..<gridSize, id: \.self) { column in
  14.                         Rectangle()
  15.                             .fill(cellColor(for: row, column: column))
  16.                             .frame(width: cellSize, height: cellSize)
  17.                             .border(Color.gray.opacity(0.3), width: 0.5)
  18.                     }
  19.                 }
  20.             }
  21.         }
  22.         .onAppear {
  23.             startScanning()
  24.         }
  25.         .padding()
  26.     }
  27.    
  28.     private func cellColor(for row: Int, column: Int) -> Color {
  29.         let distance = sqrt(pow(Double(row - gridSize/2), 2) + pow(Double(column - gridSize/2), 2))
  30.         let maxDistance = sqrt(pow(Double(gridSize/2), 2) + pow(Double(gridSize/2), 2))
  31.         let normalizedDistance = distance / maxDistance
  32.         
  33.         if abs(normalizedDistance - scanProgress) < 0.1 {
  34.             return Color.blue
  35.         } else if normalizedDistance < scanProgress {
  36.             return Color.blue.opacity(0.3)
  37.         } else {
  38.             return Color.gray.opacity(0.2)
  39.         }
  40.     }
  41.    
  42.     private func startScanning() {
  43.         isScanning = true
  44.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
  45.             scanProgress = 1.0
  46.         }
  47.     }
  48. }
  49. struct GridScanView_Previews: PreviewProvider {
  50.     static var previews: some View {
  51.         GridScanView()
  52.     }
  53. }
复制代码

这段代码创建了一个网格扫描动画,从网格中心向外扩散。每个单元格的颜色根据其与中心的距离和扫描进度来确定,创建了一个波纹扩散的效果。

粒子扫描动画
  1. import SwiftUI
  2. struct ParticleScanView: View {
  3.     @State private var particles: [Particle] = []
  4.     @State private var scanPosition: CGFloat = -100
  5.     @State private var isScanning = false
  6.    
  7.     struct Particle: Identifiable {
  8.         let id = UUID()
  9.         var x: CGFloat
  10.         var y: CGFloat
  11.         var size: CGFloat
  12.         var opacity: Double
  13.         var color: Color
  14.     }
  15.    
  16.     let particleCount = 50
  17.    
  18.     var body: some View {
  19.         ZStack {
  20.             // 背景内容
  21.             Rectangle()
  22.                 .fill(Color.black)
  23.                 .frame(height: 300)
  24.                 .cornerRadius(10)
  25.             
  26.             // 粒子
  27.             ForEach(particles) { particle in
  28.                 Circle()
  29.                     .fill(particle.color)
  30.                     .frame(width: particle.size, height: particle.size)
  31.                     .position(x: particle.x, y: particle.y)
  32.                     .opacity(particle.opacity)
  33.             }
  34.             
  35.             // 扫描线
  36.             Rectangle()
  37.                 .fill(
  38.                     LinearGradient(
  39.                         gradient: Gradient(colors: [Color.clear, Color.white, Color.clear]),
  40.                         startPoint: .leading,
  41.                         endPoint: .trailing
  42.                     )
  43.                 )
  44.                 .frame(width: 5, height: 300)
  45.                 .offset(x: scanPosition)
  46.                 .opacity(isScanning ? 1 : 0)
  47.                 .shadow(color: .white, radius: 10, x: 0, y: 0)
  48.         }
  49.         .onAppear {
  50.             initializeParticles()
  51.             startScanning()
  52.         }
  53.         .padding()
  54.     }
  55.    
  56.     private func initializeParticles() {
  57.         for _ in 0..<particleCount {
  58.             let particle = Particle(
  59.                 x: CGFloat.random(in: 0...UIScreen.main.bounds.width),
  60.                 y: CGFloat.random(in: 0...300),
  61.                 size: CGFloat.random(in: 2...6),
  62.                 opacity: 0.3,
  63.                 color: [Color.blue, Color.green, Color.yellow, Color.purple].randomElement() ?? .blue
  64.             )
  65.             particles.append(particle)
  66.         }
  67.     }
  68.    
  69.     private func startScanning() {
  70.         isScanning = true
  71.         
  72.         // 扫描线动画
  73.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
  74.             scanPosition = UIScreen.main.bounds.width + 100
  75.         }
  76.         
  77.         // 粒子动画
  78.         Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
  79.             updateParticles()
  80.         }
  81.     }
  82.    
  83.     private func updateParticles() {
  84.         for i in particles.indices {
  85.             // 检查粒子是否在扫描线附近
  86.             if abs(particles[i].x - scanPosition) < 20 {
  87.                 // 激活粒子
  88.                 particles[i].opacity = 1.0
  89.                 particles[i].size = particles[i].size * 1.5
  90.                
  91.                 // 添加随机运动
  92.                 particles[i].x += CGFloat.random(in: -2...2)
  93.                 particles[i].y += CGFloat.random(in: -2...2)
  94.             } else {
  95.                 // 逐渐恢复原始状态
  96.                 if particles[i].opacity > 0.3 {
  97.                     particles[i].opacity -= 0.01
  98.                 }
  99.                 if particles[i].size > 2 {
  100.                     particles[i].size -= 0.05
  101.                 }
  102.             }
  103.         }
  104.     }
  105. }
  106. struct ParticleScanView_Previews: PreviewProvider {
  107.     static var previews: some View {
  108.         ParticleScanView()
  109.     }
  110. }
复制代码

这段代码创建了一个粒子扫描动画,当扫描线经过粒子时,粒子会被激活并产生反应。这种效果可以用于表示数据扫描、安全检查等场景。

3D扫描动画
  1. import SwiftUI
  2. import SceneKit
  3. struct ThreeDScanView: UIViewRepresentable {
  4.     @Binding var scanProgress: Double
  5.    
  6.     func makeUIView(context: Context) -> SCNView {
  7.         let scnView = SCNView()
  8.         scnView.scene = createScene()
  9.         scnView.autoenablesDefaultLighting = true
  10.         scnView.allowsCameraControl = true
  11.         scnView.backgroundColor = UIColor.black
  12.         
  13.         return scnView
  14.     }
  15.    
  16.     func updateUIView(_ uiView: SCNView, context: Context) {
  17.         // 更新扫描进度
  18.         if let material = uiView.scene?.rootNode.childNodes.first?.geometry?.firstMaterial {
  19.             material.setValue(scanProgress, forKey: "scanProgress")
  20.         }
  21.     }
  22.    
  23.     private func createScene() -> SCNScene {
  24.         let scene = SCNScene()
  25.         
  26.         // 创建3D对象
  27.         let box = SCNBox(width: 2, height: 2, length: 2, chamferRadius: 0)
  28.         
  29.         // 创建自定义着色器材质
  30.         let material = SCNMaterial()
  31.         material.diffuse.contents = UIColor.blue
  32.         material.specular.contents = UIColor.white
  33.         
  34.         // 添加自定义着色器
  35.         let shader = """
  36.         uniform float scanProgress;
  37.         
  38.         #pragma transparent
  39.         #pragma body
  40.         
  41.         vec2 uv = _surface.diffuseTexcoord;
  42.         
  43.         // 计算扫描位置
  44.         float scanPos = scanProgress * 2.0 - 1.0;
  45.         
  46.         // 计算当前像素与扫描位置的距离
  47.         float dist = abs(uv.y - scanPos);
  48.         
  49.         // 创建扫描线效果
  50.         if (dist < 0.05) {
  51.             _output.color.rgb = mix(_output.color.rgb, vec3(0.0, 1.0, 1.0), 1.0 - dist / 0.05);
  52.             _output.color.a = 1.0;
  53.         }
  54.         
  55.         // 创建扫描过的区域效果
  56.         if (uv.y < scanPos) {
  57.             _output.color.rgb = mix(_output.color.rgb, vec3(0.0, 0.5, 1.0), 0.5);
  58.             _output.color.a = 0.8;
  59.         }
  60.         """
  61.         
  62.         material.shaderModifiers = [.surface: shader]
  63.         material.setValue(0.0, forKey: "scanProgress")
  64.         
  65.         box.materials = [material]
  66.         
  67.         let boxNode = SCNNode(geometry: box)
  68.         scene.rootNode.addChildNode(boxNode)
  69.         
  70.         // 添加相机
  71.         let cameraNode = SCNNode()
  72.         cameraNode.camera = SCNCamera()
  73.         cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
  74.         scene.rootNode.addChildNode(cameraNode)
  75.         
  76.         // 添加旋转动画
  77.         let rotation = SCNAction.rotateBy(x: 0, y: CGFloat.pi * 2, z: 0, duration: 10)
  78.         let repeatRotation = SCNAction.repeatForever(rotation)
  79.         boxNode.runAction(repeatRotation)
  80.         
  81.         return scene
  82.     }
  83. }
  84. struct ThreeDScanContainerView: View {
  85.     @State private var scanProgress: Double = 0
  86.     @State private var isScanning = false
  87.    
  88.     var body: some View {
  89.         VStack {
  90.             Text("3D扫描动画")
  91.                 .font(.title)
  92.                 .padding()
  93.             
  94.             ThreeDScanView(scanProgress: $scanProgress)
  95.                 .frame(height: 400)
  96.                 .onAppear {
  97.                     startScanning()
  98.                 }
  99.         }
  100.     }
  101.    
  102.     private func startScanning() {
  103.         isScanning = true
  104.         
  105.         Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
  106.             scanProgress += 0.01
  107.             if scanProgress >= 1.0 {
  108.                 scanProgress = 0.0
  109.             }
  110.         }
  111.     }
  112. }
  113. struct ThreeDScanView_Previews: PreviewProvider {
  114.     static var previews: some View {
  115.         ThreeDScanContainerView()
  116.     }
  117. }
复制代码

这段代码创建了一个3D扫描动画,使用SceneKit和自定义着色器来实现。扫描线从上到下移动,扫描过的区域会改变颜色,同时3D对象会缓慢旋转,展示全方位的扫描效果。

5. 性能优化

在实现复杂的扫描动画时,性能优化是非常重要的。以下是一些优化技巧:

使用CADisplayLink而不是Timer

对于需要频繁更新的动画,使用CADisplayLink比Timer更高效:
  1. import SwiftUI
  2. struct OptimizedScanView: View {
  3.     @State private var scanPosition: CGFloat = -100
  4.     @State private var displayLink: CADisplayLink?
  5.    
  6.     var body: some View {
  7.         ZStack {
  8.             // 背景内容
  9.             Rectangle()
  10.                 .fill(Color.gray.opacity(0.2))
  11.                 .frame(height: 200)
  12.                 .cornerRadius(10)
  13.             
  14.             // 扫描线
  15.             Rectangle()
  16.                 .fill(
  17.                     LinearGradient(
  18.                         gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
  19.                         startPoint: .leading,
  20.                         endPoint: .trailing
  21.                     )
  22.                 )
  23.                 .frame(width: 100, height: 200)
  24.                 .offset(x: scanPosition)
  25.                 .shadow(color: .blue, radius: 10, x: 0, y: 0)
  26.         }
  27.         .onAppear {
  28.             startScanning()
  29.         }
  30.         .onDisappear {
  31.             stopScanning()
  32.         }
  33.         .padding()
  34.     }
  35.    
  36.     private func startScanning() {
  37.         displayLink = CADisplayLink(target: self, selector: #selector(updateScanPosition))
  38.         displayLink?.add(to: .current, forMode: .common)
  39.     }
  40.    
  41.     private func stopScanning() {
  42.         displayLink?.invalidate()
  43.         displayLink = nil
  44.     }
  45.    
  46.     @objc private func updateScanPosition() {
  47.         scanPosition += 5
  48.         
  49.         if scanPosition > UIScreen.main.bounds.width + 100 {
  50.             scanPosition = -100
  51.         }
  52.     }
  53. }
  54. struct OptimizedScanView_Previews: PreviewProvider {
  55.     static var previews: some View {
  56.         OptimizedScanView()
  57.     }
  58. }
复制代码

使用Metal着色器

对于更复杂的动画效果,使用Metal着色器可以显著提高性能:
  1. import SwiftUI
  2. import MetalKit
  3. struct MetalScanView: UIViewRepresentable {
  4.     func makeUIView(context: Context) -> MTKView {
  5.         let mtkView = MTKView()
  6.         mtkView.device = MTLCreateSystemDefaultDevice()
  7.         mtkView.delegate = context.coordinator
  8.         mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
  9.         mtkView.enableSetNeedsDisplay = true
  10.         
  11.         if let device = mtkView.device {
  12.             context.coordinator.setup(device: device)
  13.         }
  14.         
  15.         return mtkView
  16.     }
  17.    
  18.     func updateUIView(_ uiView: MTKView, context: Context) {
  19.         // 更新视图
  20.     }
  21.    
  22.     func makeCoordinator() -> Coordinator {
  23.         Coordinator()
  24.     }
  25.    
  26.     class Coordinator: NSObject, MTKViewDelegate {
  27.         private var device: MTLDevice!
  28.         private var commandQueue: MTLCommandQueue!
  29.         private var pipelineState: MTLRenderPipelineState!
  30.         private var vertices: [Float] = []
  31.         private var vertexBuffer: MTLBuffer!
  32.         private var scanPosition: Float = -1.0
  33.         private var time: Float = 0.0
  34.         
  35.         func setup(device: MTLDevice) {
  36.             self.device = device
  37.             commandQueue = device.makeCommandQueue()
  38.             
  39.             // 创建顶点数据
  40.             vertices = [
  41.                 -1.0, -1.0, 0.0, 1.0,
  42.                  1.0, -1.0, 1.0, 1.0,
  43.                 -1.0,  1.0, 0.0, 0.0,
  44.                  1.0,  1.0, 1.0, 0.0
  45.             ]
  46.             
  47.             vertexBuffer = device.makeBuffer(bytes: vertices, length: vertices.count * MemoryLayout<Float>.size, options: [])
  48.             
  49.             // 创建渲染管线
  50.             let library = device.makeDefaultLibrary()
  51.             let vertexFunction = library?.makeFunction(name: "vertexShader")
  52.             let fragmentFunction = library?.makeFunction(name: "fragmentShader")
  53.             
  54.             let pipelineDescriptor = MTLRenderPipelineDescriptor()
  55.             pipelineDescriptor.vertexFunction = vertexFunction
  56.             pipelineDescriptor.fragmentFunction = fragmentFunction
  57.             pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
  58.             
  59.             do {
  60.                 pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
  61.             } catch {
  62.                 print("Failed to create pipeline state: \(error)")
  63.             }
  64.         }
  65.         
  66.         func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
  67.             // 视图大小改变时调用
  68.         }
  69.         
  70.         func draw(in view: MTKView) {
  71.             guard let drawable = view.currentDrawable,
  72.                   let renderPassDescriptor = view.currentRenderPassDescriptor,
  73.                   let pipelineState = pipelineState else {
  74.                 return
  75.             }
  76.             
  77.             time += 0.01
  78.             scanPosition = sin(time) * 0.8
  79.             
  80.             let commandBuffer = commandQueue.makeCommandBuffer()
  81.             let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
  82.             
  83.             renderEncoder?.setRenderPipelineState(pipelineState)
  84.             renderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
  85.             
  86.             // 传递扫描位置和时间到着色器
  87.             var scanPos = scanPosition
  88.             var timeVal = time
  89.             renderEncoder?.setFragmentBytes(&scanPos, length: MemoryLayout<Float>.size, index: 0)
  90.             renderEncoder?.setFragmentBytes(&timeVal, length: MemoryLayout<Float>.size, index: 1)
  91.             
  92.             renderEncoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
  93.             renderEncoder?.endEncoding()
  94.             
  95.             commandBuffer?.present(drawable)
  96.             commandBuffer?.commit()
  97.         }
  98.     }
  99. }
  100. // Metal着色器代码
  101. #if os(iOS)
  102. #include <metal_stdlib>
  103. using namespace metal;
  104. struct VertexOut {
  105.     float4 position [[position]];
  106.     float2 texCoord;
  107. };
  108. vertex VertexOut vertexShader(const device packed_float4 *vertexArray [[buffer(0)], uint vertexId [[vertex_id]]]) {
  109.     VertexOut out;
  110.     out.position = float4(vertexArray[vertexId].xy, 0.0, 1.0);
  111.     out.texCoord = vertexArray[vertexId].zw;
  112.     return out;
  113. }
  114. fragment float4 fragmentShader(VertexOut in [[stage_in]], const device float &scanPosition [[buffer(0)]], const device float &time [[buffer(1)]]) {
  115.     float2 uv = in.texCoord;
  116.    
  117.     // 创建扫描线效果
  118.     float scanLine = smoothstep(0.02, 0.0, abs(uv.y - scanPosition));
  119.    
  120.     // 创建扫描过的区域效果
  121.     float scannedArea = smoothstep(scanPosition - 0.1, scanPosition, uv.y);
  122.    
  123.     // 创建网格效果
  124.     float grid = 0.0;
  125.     float2 gridUV = uv * 20.0;
  126.     grid += smoothstep(0.98, 1.0, abs(fract(gridUV.x) - 0.5));
  127.     grid += smoothstep(0.98, 1.0, abs(fract(gridUV.y) - 0.5));
  128.    
  129.     // 组合效果
  130.     float3 color = float3(0.0, 0.0, 0.0);
  131.     color = mix(color, float3(0.0, 0.5, 1.0), scannedArea * 0.5);
  132.     color = mix(color, float3(0.0, 1.0, 1.0), scanLine);
  133.     color += float3(0.0, 1.0, 0.5) * grid * 0.2;
  134.    
  135.     return float4(color, 1.0);
  136. }
  137. #endif
  138. struct MetalScanContainerView: View {
  139.     var body: some View {
  140.         VStack {
  141.             Text("Metal扫描动画")
  142.                 .font(.title)
  143.                 .padding()
  144.             
  145.             MetalScanView()
  146.                 .frame(height: 400)
  147.         }
  148.     }
  149. }
  150. struct MetalScanView_Previews: PreviewProvider {
  151.     static var previews: some View {
  152.         MetalScanContainerView()
  153.     }
  154. }
复制代码

这段代码使用Metal和着色器创建了一个高效的扫描动画效果。通过在GPU上执行渲染逻辑,我们可以实现更复杂的视觉效果,同时保持高性能。

6. 实际应用案例

现在,让我们看看如何将这些扫描动画应用到实际的应用场景中。

文件扫描应用
  1. import SwiftUI
  2. struct DocumentScannerView: View {
  3.     @State private var scanProgress: Double = 0
  4.     @State private var isScanning = false
  5.     @State private var scannedDocument: UIImage?
  6.     @State private var showResults = false
  7.    
  8.     var body: some View {
  9.         VStack {
  10.             Text("文档扫描")
  11.                 .font(.largeTitle)
  12.                 .padding()
  13.             
  14.             ZStack {
  15.                 // 相机预览区域
  16.                 Rectangle()
  17.                     .fill(Color.black)
  18.                     .frame(height: 400)
  19.                     .cornerRadius(10)
  20.                     .overlay(
  21.                         RoundedRectangle(cornerRadius: 10)
  22.                             .stroke(Color.white, lineWidth: 2)
  23.                     )
  24.                
  25.                 // 扫描线
  26.                 if isScanning {
  27.                     Rectangle()
  28.                         .fill(
  29.                             LinearGradient(
  30.                                 gradient: Gradient(colors: [Color.clear, Color.green, Color.clear]),
  31.                                 startPoint: .leading,
  32.                                 endPoint: .trailing
  33.                             )
  34.                         )
  35.                         .frame(width: UIScreen.main.bounds.width, height: 2)
  36.                         .offset(y: scanPosition)
  37.                         .shadow(color: .green, radius: 10, x: 0, y: 0)
  38.                     
  39.                     // 扫描进度指示器
  40.                     VStack {
  41.                         Spacer()
  42.                         ProgressView(value: scanProgress)
  43.                             .progressViewStyle(LinearProgressViewStyle(tint: .green))
  44.                             .padding()
  45.                     }
  46.                 }
  47.                
  48.                 // 扫描结果
  49.                 if showResults, let image = scannedDocument {
  50.                     Image(uiImage: image)
  51.                         .resizable()
  52.                         .aspectRatio(contentMode: .fit)
  53.                         .frame(height: 400)
  54.                         .cornerRadius(10)
  55.                 }
  56.             }
  57.             .padding()
  58.             
  59.             HStack {
  60.                 Button(action: {
  61.                     if isScanning {
  62.                         stopScanning()
  63.                     } else {
  64.                         startScanning()
  65.                     }
  66.                 }) {
  67.                     Text(isScanning ? "停止扫描" : "开始扫描")
  68.                         .font(.headline)
  69.                         .padding()
  70.                         .background(isScanning ? Color.red : Color.green)
  71.                         .foregroundColor(.white)
  72.                         .cornerRadius(10)
  73.                 }
  74.                
  75.                 Spacer()
  76.                
  77.                 Button(action: {
  78.                     saveDocument()
  79.                 }) {
  80.                     Text("保存文档")
  81.                         .font(.headline)
  82.                         .padding()
  83.                         .background(Color.blue)
  84.                         .foregroundColor(.white)
  85.                         .cornerRadius(10)
  86.                 }
  87.                 .disabled(scannedDocument == nil)
  88.             }
  89.             .padding()
  90.         }
  91.     }
  92.    
  93.     private var scanPosition: CGFloat {
  94.         let progress = CGFloat(scanProgress)
  95.         return -200 + (400 * progress)
  96.     }
  97.    
  98.     private func startScanning() {
  99.         isScanning = true
  100.         showResults = false
  101.         
  102.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
  103.             scanProgress = 1.0
  104.         }
  105.         
  106.         // 模拟扫描完成
  107.         DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  108.             completeScan()
  109.         }
  110.     }
  111.    
  112.     private func stopScanning() {
  113.         isScanning = false
  114.         withAnimation {
  115.             scanProgress = 0
  116.         }
  117.     }
  118.    
  119.     private func completeScan() {
  120.         isScanning = false
  121.         
  122.         // 在实际应用中,这里会处理扫描的图像
  123.         // 这里我们使用一个示例图像
  124.         scannedDocument = UIImage(systemName: "doc.fill")
  125.         
  126.         withAnimation {
  127.             showResults = true
  128.         }
  129.     }
  130.    
  131.     private func saveDocument() {
  132.         // 在实际应用中,这里会保存扫描的文档
  133.         print("文档已保存")
  134.     }
  135. }
  136. struct DocumentScannerView_Previews: PreviewProvider {
  137.     static var previews: some View {
  138.         DocumentScannerView()
  139.     }
  140. }
复制代码

这段代码创建了一个文档扫描应用的界面,包含扫描动画、进度指示器和结果展示。这种界面可以用于实际的文档扫描应用,如扫描纸质文件、收据等。

安全验证应用
  1. import SwiftUI
  2. import LocalAuthentication
  3. struct SecurityScanView: View {
  4.     @State private var scanProgress: Double = 0
  5.     @State private var isScanning = false
  6.     @State private var isAuthenticated = false
  7.     @State private var showAlert = false
  8.     @State private var alertMessage = ""
  9.    
  10.     var body: some View {
  11.         VStack {
  12.             Text("安全验证")
  13.                 .font(.largeTitle)
  14.                 .padding()
  15.             
  16.             ZStack {
  17.                 // 指纹图标
  18.                 Image(systemName: "touchid")
  19.                     .font(.system(size: 100))
  20.                     .foregroundColor(isAuthenticated ? .green : .gray)
  21.                     .scaleEffect(isAuthenticated ? 1.2 : 1.0)
  22.                     .animation(.spring())
  23.                
  24.                 // 扫描圆环
  25.                 if isScanning {
  26.                     Circle()
  27.                         .trim(from: 0, to: scanProgress)
  28.                         .stroke(
  29.                             LinearGradient(
  30.                                 gradient: Gradient(colors: [Color.blue, Color.purple]),
  31.                                 startPoint: .topLeading,
  32.                                 endPoint: .bottomTrailing
  33.                             ),
  34.                             lineWidth: 5
  35.                         )
  36.                         .frame(width: 200, height: 200)
  37.                         .rotationEffect(.degrees(-90))
  38.                         .shadow(color: .blue, radius: 10, x: 0, y: 0)
  39.                 }
  40.             }
  41.             .frame(height: 300)
  42.             .padding()
  43.             
  44.             Text(isAuthenticated ? "验证成功" : "请进行身份验证")
  45.                 .font(.title2)
  46.                 .foregroundColor(isAuthenticated ? .green : .primary)
  47.             
  48.             Spacer()
  49.             
  50.             Button(action: {
  51.                 authenticateUser()
  52.             }) {
  53.                 Text("开始验证")
  54.                     .font(.headline)
  55.                     .padding()
  56.                     .frame(maxWidth: .infinity)
  57.                     .background(Color.blue)
  58.                     .foregroundColor(.white)
  59.                     .cornerRadius(10)
  60.             }
  61.             .padding()
  62.             .disabled(isScanning || isAuthenticated)
  63.         }
  64.         .alert(isPresented: $showAlert) {
  65.             Alert(title: Text("验证结果"), message: Text(alertMessage), dismissButton: .default(Text("确定")))
  66.         }
  67.     }
  68.    
  69.     private func authenticateUser() {
  70.         isScanning = true
  71.         
  72.         // 启动扫描动画
  73.         withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
  74.             scanProgress = 1.0
  75.         }
  76.         
  77.         // 使用生物识别验证
  78.         let context = LAContext()
  79.         var error: NSError?
  80.         
  81.         if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
  82.             let reason = "请使用Touch ID进行身份验证"
  83.             
  84.             context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in
  85.                 DispatchQueue.main.async {
  86.                     self.isScanning = false
  87.                     
  88.                     if success {
  89.                         self.isAuthenticated = true
  90.                         self.alertMessage = "身份验证成功!"
  91.                         self.showAlert = true
  92.                     } else {
  93.                         self.alertMessage = "身份验证失败,请重试。"
  94.                         self.showAlert = true
  95.                     }
  96.                     
  97.                     // 重置扫描进度
  98.                     self.scanProgress = 0
  99.                 }
  100.             }
  101.         } else {
  102.             // 无法使用生物识别
  103.             DispatchQueue.main.async {
  104.                 self.isScanning = false
  105.                 self.scanProgress = 0
  106.                 self.alertMessage = "此设备不支持生物识别。"
  107.                 self.showAlert = true
  108.             }
  109.         }
  110.     }
  111. }
  112. struct SecurityScanView_Previews: PreviewProvider {
  113.     static var previews: some View {
  114.         SecurityScanView()
  115.     }
  116. }
复制代码

这段代码创建了一个安全验证应用的界面,使用扫描动画来表示生物识别验证的过程。这种界面可以用于需要安全验证的应用,如银行应用、密码管理器等。

数据加载应用
  1. import SwiftUI
  2. struct DataLoadingScanView: View {
  3.     @State private var scanProgress: Double = 0
  4.     @State private var isLoading = false
  5.     @State private var dataItems: [DataItem] = []
  6.     @State private var showData = false
  7.    
  8.     struct DataItem: Identifiable {
  9.         let id = UUID()
  10.         let title: String
  11.         let subtitle: String
  12.         let icon: String
  13.     }
  14.    
  15.     var body: some View {
  16.         VStack {
  17.             Text("数据加载")
  18.                 .font(.largeTitle)
  19.                 .padding()
  20.             
  21.             ZStack {
  22.                 // 数据加载区域
  23.                 RoundedRectangle(cornerRadius: 10)
  24.                     .fill(Color.gray.opacity(0.2))
  25.                     .frame(height: 400)
  26.                
  27.                 // 扫描线
  28.                 if isLoading {
  29.                     VStack(spacing: 0) {
  30.                         ForEach(0..<5, id: \.self) { index in
  31.                             Rectangle()
  32.                                 .fill(
  33.                                     LinearGradient(
  34.                                         gradient: Gradient(colors: [Color.clear, Color.blue, Color.clear]),
  35.                                         startPoint: .leading,
  36.                                         endPoint: .trailing
  37.                                     )
  38.                                 )
  39.                                 .frame(height: 80)
  40.                                 .offset(x: scanPosition)
  41.                                 .shadow(color: .blue, radius: 5, x: 0, y: 0)
  42.                                 .opacity(scanOpacity(for: index))
  43.                         }
  44.                     }
  45.                 }
  46.                
  47.                 // 加载的数据
  48.                 if showData {
  49.                     ScrollView {
  50.                         LazyVStack(spacing: 15) {
  51.                             ForEach(dataItems) { item in
  52.                                 HStack {
  53.                                     Image(systemName: item.icon)
  54.                                         .font(.title)
  55.                                         .foregroundColor(.blue)
  56.                                         .frame(width: 40)
  57.                                     
  58.                                     VStack(alignment: .leading) {
  59.                                         Text(item.title)
  60.                                             .font(.headline)
  61.                                        
  62.                                         Text(item.subtitle)
  63.                                             .font(.subheadline)
  64.                                             .foregroundColor(.secondary)
  65.                                     }
  66.                                     
  67.                                     Spacer()
  68.                                 }
  69.                                 .padding()
  70.                                 .background(Color.white)
  71.                                 .cornerRadius(10)
  72.                                 .shadow(radius: 2)
  73.                             }
  74.                         }
  75.                         .padding()
  76.                     }
  77.                 }
  78.             }
  79.             .padding()
  80.             
  81.             HStack {
  82.                 Button(action: {
  83.                     if isLoading {
  84.                         stopLoading()
  85.                     } else {
  86.                         startLoading()
  87.                     }
  88.                 }) {
  89.                     Text(isLoading ? "停止加载" : "开始加载")
  90.                         .font(.headline)
  91.                         .padding()
  92.                         .background(isLoading ? Color.red : Color.green)
  93.                         .foregroundColor(.white)
  94.                         .cornerRadius(10)
  95.                 }
  96.                
  97.                 Spacer()
  98.                
  99.                 Button(action: {
  100.                     refreshData()
  101.                 }) {
  102.                     Text("刷新数据")
  103.                         .font(.headline)
  104.                         .padding()
  105.                         .background(Color.blue)
  106.                         .foregroundColor(.white)
  107.                         .cornerRadius(10)
  108.                 }
  109.                 .disabled(isLoading || dataItems.isEmpty)
  110.             }
  111.             .padding()
  112.         }
  113.     }
  114.    
  115.     private var scanPosition: CGFloat {
  116.         let progress = CGFloat(scanProgress)
  117.         return -UIScreen.main.bounds.width + (UIScreen.main.bounds.width * 2 * progress)
  118.     }
  119.    
  120.     private func scanOpacity(for index: Int) -> Double {
  121.         let progress = scanProgress
  122.         let segmentProgress = progress * 5
  123.         let segmentIndex = Int(segmentProgress)
  124.         let segmentOffset = segmentProgress - Double(segmentIndex)
  125.         
  126.         if segmentIndex == index {
  127.             return 1.0 - segmentOffset
  128.         } else if segmentIndex == index - 1 {
  129.             return segmentOffset
  130.         } else {
  131.             return 0.0
  132.         }
  133.     }
  134.    
  135.     private func startLoading() {
  136.         isLoading = true
  137.         showData = false
  138.         dataItems = []
  139.         
  140.         withAnimation(Animation.linear(duration: 3).repeatForever(autoreverses: false)) {
  141.             scanProgress = 1.0
  142.         }
  143.         
  144.         // 模拟数据加载
  145.         DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  146.             completeLoading()
  147.         }
  148.     }
  149.    
  150.     private func stopLoading() {
  151.         isLoading = false
  152.         withAnimation {
  153.             scanProgress = 0
  154.         }
  155.     }
  156.    
  157.     private func completeLoading() {
  158.         isLoading = false
  159.         
  160.         // 生成示例数据
  161.         dataItems = [
  162.             DataItem(title: "项目 1", subtitle: "这是第一个项目的描述", icon: "doc.fill"),
  163.             DataItem(title: "项目 2", subtitle: "这是第二个项目的描述", icon: "folder.fill"),
  164.             DataItem(title: "项目 3", subtitle: "这是第三个项目的描述", icon: "star.fill"),
  165.             DataItem(title: "项目 4", subtitle: "这是第四个项目的描述", icon: "heart.fill"),
  166.             DataItem(title: "项目 5", subtitle: "这是第五个项目的描述", icon: "flag.fill")
  167.         ]
  168.         
  169.         withAnimation {
  170.             showData = true
  171.         }
  172.     }
  173.    
  174.     private func refreshData() {
  175.         showData = false
  176.         
  177.         // 模拟数据刷新
  178.         DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
  179.             // 重新生成示例数据
  180.             dataItems = [
  181.                 DataItem(title: "新项目 1", subtitle: "这是第一个新项目的描述", icon: "doc.fill"),
  182.                 DataItem(title: "新项目 2", subtitle: "这是第二个新项目的描述", icon: "folder.fill"),
  183.                 DataItem(title: "新项目 3", subtitle: "这是第三个新项目的描述", icon: "star.fill"),
  184.                 DataItem(title: "新项目 4", subtitle: "这是第四个新项目的描述", icon: "heart.fill"),
  185.                 DataItem(title: "新项目 5", subtitle: "这是第五个新项目的描述", icon: "flag.fill")
  186.             ]
  187.             
  188.             withAnimation {
  189.                 showData = true
  190.             }
  191.         }
  192.     }
  193. }
  194. struct DataLoadingScanView_Previews: PreviewProvider {
  195.     static var previews: some View {
  196.         DataLoadingScanView()
  197.     }
  198. }
复制代码

这段代码创建了一个数据加载应用的界面,使用多条扫描线来表示数据加载的过程。这种界面可以用于需要从网络或数据库加载数据的应用,如新闻应用、社交媒体应用等。

7. 总结与展望

在本教程中,我们学习了如何使用Swift编程语言实现各种炫酷的扫描动画效果,从基础的线性扫描到复杂的3D扫描。我们探讨了如何使用SwiftUI、UIKit、SceneKit和Metal等技术来创建这些效果,并讨论了性能优化的方法。

扫描动画效果不仅可以增强应用的视觉吸引力,还可以提供有用的反馈,表示系统正在处理数据或执行任务。通过将这些动画应用到实际的应用场景中,如文档扫描、安全验证和数据加载,我们可以创建更加生动和直观的用户界面。

随着iOS开发技术的不断发展,我们可以期待更多新的动画技术和工具的出现。例如,SwiftUI的不断更新可能会带来更多简洁而强大的动画API,而Metal的持续发展将使我们能够创建更加复杂和高效的图形效果。

无论你是初学者还是有经验的开发者,掌握这些扫描动画技术都将有助于你创建更加吸引人的iOS应用。希望本教程能够为你提供有用的指导和灵感,让你在iOS开发的道路上取得更大的成功。

现在,你已经掌握了使用Swift实现炫酷扫描动画效果的知识,可以开始将这些技术应用到自己的项目中,创造出令人印象深刻的用户界面了!
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

加入Discord频道

加入Discord频道

加入QQ社群

加入QQ社群

联系我们|小黑屋|TG频道|RSS |网站地图

Powered by Pixtech

© 2025-2026 Pixtech Team.