SwiftUI在iOS 17新增的部分API

985 阅读1分钟

前言

参考github项目iOS17_New_API, 总结了SwiftUI在iOS17新增的部分API,并以简单的示例展示效果。

scrollPosition(id: anchor:)

 func scrollPosition(
     id: Binding<(some Hashable)?>,
     anchor: UnitPoint? = nil
 ) -> some View

关联该视图中滚动视图滚动时要更新的绑定,通过更新绑定的id,即可滚动到对应view的位置

示例

ScrollView滑动到特定的View

scrollPosition.gif

 import SwiftUI
 ​
 struct ScrollViewDemo4: View {
     @State private var scrollPosition: Color?
     
     var body: some View {
         GeometryReader(content: { geometry in
             ScrollView(.horizontal) {
                 let colors: [Color] = [.red, .orange, .yellow, .green, .cyan, .blue, .purple]
                 
                 LazyHStack(spacing: 25, content: {
                     ForEach(colors, id: .self) { color in
                         RoundedRectangle(cornerRadius: 25.0, style: .continuous)
                             .fill(color.gradient)
                             //第三种分页。 固定宽度且居中
                             .frame(width: 300)
                     }
                 })
                 .padding(.horizontal, (geometry.size.width - 300) / 2)
                 .scrollTargetLayout()
             }
             .scrollTargetBehavior(.viewAligned)
             .scrollPosition(id: $scrollPosition)  //设置scrollPosition绑定的id
         })
         .frame(height: 250)
         .overlay(alignment: .bottom) {
             Button("Scroll To Yellow") {
                 withAnimation(.snappy) {
                     scrollPosition = .yellow    //通过改变绑定的scrollPosition,滚动到对应id的view的位置
                 }
             }
             .offset(y: 50)
         }
     }
 }

scrollTransition

 func scrollTransition(
     topLeading: ScrollTransitionConfiguration,
     bottomTrailing: ScrollTransitionConfiguration,
     axis: Axis? = nil,
     transition: @escaping @Sendable (EmptyVisualEffect, ScrollTransitionPhase) -> some VisualEffect
 ) -> some View

应用给定的过渡效果,使得该视图在包含滚动视图或其他使用coordinateSpace参数指定的容器的可见区域内出现和消失时,可以在过渡的各个阶段之间进行动画。

示例

ScrollView滑动时,View的透明度变化的动画效果

scrollTransition.gif

 import SwiftUI
 ​
 struct ScrollViewDemo5: View {
     
     var body: some View {
         ScrollView(.vertical, showsIndicators: false) {
             LazyVStack(spacing: 25, content: {
                 ForEach(1...20, id: .self) { _ in
                     RoundedRectangle(cornerRadius: 25.0, style: .continuous)
                         .fill(.red.gradient)
                         .frame(height: 155)
                         // 透明度变化的动画效果
                         .scrollTransition(topLeading: .animated, bottomTrailing: .interactive) { view, phase in
                             view.opacity(1 - (phase.value < 0 ? -phase.value : phase.value))
                         }
                 }
             })
             .padding(.horizontal, 25)
         }
         .navigationBarTitleDisplayMode(.inline)
     }
 }

Transition

 func transition(_ t: AnyTransition) -> some View

将过渡与视图关联起来。

示例

自定义转场动画

transition.gif

 import SwiftUI
 ​
 struct CustomTransitionView: View {
     @State private var showView: Bool = false
     var body: some View {
         VStack {
             if showView {
                 Rectangle()
                     .fill(.red.gradient)
                     .frame(width: 250, height: 250)
                     .transition(MyTransition())
             }
             
             Button("Show View") {
                 //  spring  interactiveSpring  smooth  snappy  bouncy
                 //  弹簧 互动弹簧 光滑 敏捷 有弹性
                 withAnimation(.init(MyAnimation())) {
                     showView.toggle()
                 }
             }
         }
     }
 }
 ​
 struct MyTransition: Transition {
     func body(content: Content, phase: TransitionPhase) -> some View {
         content
             .rotation3DEffect(
                 .init(degrees: phase.value * (phase == .willAppear ? 90 : -90)),
                 axis: (x: 1.0, y: 0.0, z: 0.0)
             )
     }
 }
 ​
 struct MyAnimation: CustomAnimation {
     var duration: CGFloat = 1
     
     func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V : VectorArithmetic {
         if time > duration { return nil }
         return value.scaled(by: easeOutBounce(time / duration))
     }
     
     func easeOutBounce(_ x: TimeInterval) -> CGFloat {
         let n = 7.5625
         let d = 2.75
         var x: TimeInterval = x
         if (x < 1 / d) {
             return n * x * x
         } else if (x < 2 / d) {
             x -= 1.5 / d
             return n * x * x + 0.75
         } else if (x < 2.5 / d) {
             x -= 2.25 / d
             return n * x * x + 0.9375
         } else {
             x -= 2.625 / d
             return n * x * x + 0.984375
         }
     }
 }

withAnimation

 func withAnimation<Result>(
     _ animation: Animation? = .default,
     completionCriteria: AnimationCompletionCriteria = .logicallyComplete,
     _ body: () throws -> Result,
     completion: @escaping () -> Void
 ) rethrows -> Result

返回用提供的动画重新计算视图主体的结果,并在所有动画完成后运行completion。

示例

动画结束后执行

animation.gif

 import SwiftUI
 ​
 struct AnimationCallBackView1: View {
     @State private var isShow: Bool = false
     
     var body: some View {
         VStack {
             if isShow {
                 Text("Hello world!")
             }
             
             Button("Show View") {
                 withAnimation(.bouncy, completionCriteria: .logicallyComplete) {
                     isShow.toggle()
                 } completion: {
                     //动画完成后执行
                     print("Completed But View Not Removed")
                 }
 ​
             }
         }
     }
 }

@Bindable@Observable

 @dynamicMemberLookup @propertyWrapper
 struct Bindable<Value>
 ​
 @attached(member, names: named(_$observationRegistrar), named(access), named(withMutation), arbitrary) @attached(memberAttribute) @attached(extension)
 macro Observable()

@Bindable:一种属性包装器类型,支持为可观察对象的可变属性创建绑定。

@Observable: 定义并实现 Observable 协议的一致性

示例

数据观测和绑定

bindable和observable.gif

 import SwiftUI
 import SwiftData
 ​
 struct ObservableAndBindableDemo: View {
     //属性绑定
     @Bindable private var user: User = .init()
     
     var body: some View {
         VStack {
             TextField("请填写姓名", text: $user.name)
         }
         .onChange(of: user.name, initial: true) { oldValue, newValue in
             print("oldValue = (oldValue), newValue = (newValue)")
         }
         .padding(.horizontal, 20)
     }
 }
 ​
 @Observable
 class User {
     var name = ""
     var age = 0
 }

UnevenRoundedRectangle

不均匀的圆角矩形,具有不同值的圆角的矩形形状,在包含它的视图的框架内对齐。

示例

部分圆角

UnevenRoundedRectangle.png

 import SwiftUI
 ​
 struct UnevenRoundedRectangleDemo: View {
     var body: some View {
         VStack(spacing: 30, content: {
             Text("方式一")
             //不均匀的圆角矩形
             UnevenRoundedRectangle(topLeadingRadius: 35, bottomTrailingRadius: 35)
                 .fill(.red.gradient)
                 .frame(width: 200, height: 200)
             Text("方式二")
             Rectangle()
                 .fill(.red.gradient)
                 .frame(width: 200, height: 200)
                 .clipShape(.rect(topLeadingRadius: 35, bottomTrailingRadius: 35))
         })
     }
 }

SensoryFeedback

代表一种可播放的触觉和/或音频反馈。

示例

 struct FeedBackDemo: View {
     @State private var feedBack: Bool = false
     var body: some View {
         Button("Send FeedBack") {
             feedBack.toggle()
         }
         //反馈
         .sensoryFeedback(.warning, trigger: feedBack)
     }
 }

VisualEffect

视觉效果更改视图的视觉外观,而不更改其祖先或后代。

示例

改变视图视觉外观

VisualEffect.gif

 import SwiftUI
 ​
 struct VisualEffectDemo: View {
     var body: some View {
         ScrollView(.vertical) {
             LazyVStack(spacing: 20, content: {
                 Rectangle()
                     .fill(.red.gradient)
                     .frame(height: 100)
                     .visualEffect { view, proxy in
                         view
                             .offset(y: proxy.bounds(of: .scrollView)?.minY ?? 0)
                     }
                     .zIndex(100)
                 
                 ForEach(1...30, id: .self) { count in
                     Rectangle()
                         .fill(.blue.gradient)
                         .frame(height: 80)
                 }
                 .padding(.horizontal, 20)
             })
         }
         .ignoresSafeArea(.container, edges: .top)
     }
 }

symbolEffect

 func symbolEffect<T, U>(
     _ effect: T,
     options: SymbolEffectOptions = .default,
     value: U
 ) -> some View where T : DiscreteSymbolEffect, T : SymbolEffect, U : Equatable
 ​
 struct PhaseAnimator<Phase, Content> where Phase : Equatable, Content : View

PhaseAnimator:一种容器,可通过自动循环您提供的阶段集合来为其内容制作动画,每个阶段定义动画中的一个离散步骤。

示例

symbol动画

symbolEffect.gif

 import SwiftUI
 ​
 struct AnimationSymbolDemo: View {
     var body: some View {
         VStack(spacing: 20) {
             AnimationSymbolDemo1()
             
             AnimationSymbolDemo2()
         }
     }
 }
 ​
 struct AnimationSymbolDemo1: View {
     @State private var isAnimation: Bool = false
     
     var body: some View {
         VStack(spacing: 20) {
             Image(systemName: "suit.heart.fill")
                 .font(.largeTitle)
                 .foregroundColor(.red)
                 .symbolEffect(.pulse, options: .repeating, value: isAnimation)  //symbol动画
                 .onTapGesture {
                     isAnimation.toggle()
                 }
             
             Image(systemName: "suit.heart.fill")
                 .font(.largeTitle)
                 .foregroundColor(.red)
                 .symbolEffect(.bounce, options: .repeating, value: isAnimation) //symbol动画
                 .onTapGesture {
                     isAnimation.toggle()
                 }
             
             Image(systemName: "suit.heart.fill")
                 .font(.largeTitle)
                 .foregroundColor(.red)
                 .symbolEffect(.variableColor, options: .repeating, value: isAnimation)  //symbol动画
                 .onTapGesture {
                     isAnimation.toggle()
                 }
         }
     }
 }
 ​
 struct AnimationSymbolDemo2: View {
     @State private var startSwitching: Bool = false
     
     var body: some View {
         VStack(spacing: 20) {
             //symbol动画
             PhaseAnimator(SFImage.allCases, trigger: startSwitching) { symbol in
                 ZStack {
                     Circle()
                         .fill(symbol.color.gradient)
                     
                     Image(systemName: symbol.rawValue)
                         .font(.largeTitle)
                         .foregroundColor(.white)
                 }
                 .frame(width: 100, height: 100)
             } animation: { symbol in
                 switch symbol {
                 case .heart:
                     return .bouncy(duration: 1)
                 case .house:
                     return .smooth(duration: 1)
                 case .iphone:
                     return .snappy(duration: 1)
                 }
             }
 ​
         }
         .onTapGesture {
             startSwitching.toggle()
         }
     }
 }
 ​
 enum SFImage: String, CaseIterable {
     case heart = "suit.heart.fill"
     case house = "house.fill"
     case iphone = "iphone"
     var color: Color {
         switch self {
         case .heart:
             return .red
         case .house:
             return .blue
         case .iphone:
             return .yellow
         }
     }
 }

keyframeAnimator

 struct KeyframeAnimator<Value, KeyframePath, Content> where Value == KeyframePath.Value, KeyframePath : Keyframes, Content : View

通过关键帧对内容进行动画处理的容器。

示例

关键帧动画

keyframeAnimator.gif

 import SwiftUI
 ​
 struct KeyframeAnimationDemo: View {
     @State private var startKeyframeAnimation: Bool = false
     
     var body: some View {
         VStack {
             Spacer()
             
             Image(.xcodeBeta)
                 .resizable()
                 .aspectRatio(contentMode: .fill)
                 .frame(width: 200, height: 200)
                 //关键帧动画
                 .keyframeAnimator(initialValue: Keyframe(), trigger: startKeyframeAnimation) { view, frame in
                     view
                         .scaleEffect(frame.scale)
                         .rotationEffect(frame.rotation, anchor: .bottom)
                         .offset(y: frame.offsetY) 
                         .background {
                             view
                                 .blur(radius: 3.0)
                                 .rotation3DEffect(
                                     .init(degrees: 180),
                                     axis: (x: 1.0, y: 0.0, z: 0.0)
                                 )
                                 .mask({
                                     LinearGradient(colors: [
                                         .white.opacity(frame.reflectOpacity),
                                         .white.opacity(frame.reflectOpacity - 0.3),
                                         .white.opacity(frame.reflectOpacity - 0.45),
                                         .clear
                                     ], startPoint: .top, endPoint: .bottom)
                                 })
                                 .offset(y: 200 - frame.offsetY)
                         }
                 } keyframes: { frame in
                     //关键帧
                     KeyframeTrack(.offsetY) {
                         CubicKeyframe(20, duration: 0.15)
                         SpringKeyframe(-100, duration: 0.3, spring: .bouncy)
                         CubicKeyframe(-100, duration: 0.3)
                         SpringKeyframe(0, duration: 0.15, spring: .bouncy)
                     }
                     KeyframeTrack(.scale) {
                         CubicKeyframe(0.9, duration: 0.15)
                         CubicKeyframe(1.2, duration: 0.6)
                         CubicKeyframe(1.0, duration: 0.15)
                     }
                     KeyframeTrack(.rotation) {
                         CubicKeyframe(.zero, duration: 0.15)
                         CubicKeyframe(.zero, duration: 0.3)
                         CubicKeyframe(.init(degrees: -20), duration: 0.1)
                         CubicKeyframe(.init(degrees: 20), duration: 0.1)
                         CubicKeyframe(.init(degrees: -20), duration: 0.1)
                         CubicKeyframe(.init(degrees: 0), duration: 0.15)
                     }
                     KeyframeTrack(.reflectOpacity) {
                         CubicKeyframe(0.5, duration: 0.15)
                         CubicKeyframe(0.1, duration: 0.45)
                         CubicKeyframe(0.5, duration: 0.3)
                     }
                 }
 ​
             
             Spacer()
             
             Button("Keyframe Animation") {
                 startKeyframeAnimation.toggle()
             }
             .fontWeight(.bold)
         }
         .padding()
     }
 }
 ​
 struct Keyframe {
     var scale: CGFloat = 1
     var offsetY: CGFloat = 0
     var rotation: Angle = .zero
     var reflectOpacity: CGFloat = 0.5
 }

SectorMark

饼图或甜甜圈图的一个扇形,显示各个类别如何组成一个有意义的总数。

示例

扇形图

SectorMark.gif

 //部分代码
 SectorMark(
     angle: .value("Downloads", download.downloads),
     innerRadius: .ratio(graphType == .donut ? 0.61 : 0),
     angularInset: graphType == .donut ? 6 : 1
 )
 .cornerRadius(8)
 .foregroundStyle(by: .value("Month", download.month))
 .opacity(barSelection == nil ? 1 : (barSelection == download.month ? 1 : 0.4))

distortionEffect

 public func distortionEffect(_ shader: Shader, maxSampleOffset: CGSize, isEnabled: Bool = true) -> some View

扭曲效果

示例

pixellate 像素化

distortionEffect-pixellate.gif

 import SwiftUI
 ​
 struct PixellateView: View {
     @State private var pixellate: CGFloat = 1.0
     var body: some View {
         VStack {
             Image(.xcodeBeta)
                 .resizable()
                 .aspectRatio(contentMode: .fit)
                 .frame(height: 200)
                 //像素化
                 .distortionEffect(.init(function: .init(library: .default, name: "pixellate"), arguments: [.float(pixellate)]), maxSampleOffset: .zero)   
             
             Slider(value: $pixellate, in: 1...30)
             
             Text("Hello World!")
                 .font(.largeTitle)
                 .frame(height: 100)
                 //像素化
                 .distortionEffect(.init(function: .init(library: .default, name: "pixellate"), arguments: [.float(pixellate)]), maxSampleOffset: .zero)   
             
             Spacer()
         }
         .padding()
         .navigationTitle("Pixellate")
     }
 }

wave 波浪

distortionEffect-wave.gif

 import SwiftUI
 ​
 struct WavesView: View {
     @State private var speed: CGFloat = 6
     @State private var amplitude: CGFloat = 10
     @State private var frequency: CGFloat = 25
     let startDate: Date = .init()
     
     var body: some View {
         List {
             TimelineView(.animation) {
                 let time = $0.date.timeIntervalSince1970 - startDate.timeIntervalSince1970
                 
                 Image(.xcodeBeta)
                     .resizable()
                     .aspectRatio(contentMode: .fit)
                     .frame(height: 200)
                     //波浪
                     .distortionEffect(.init(function: .init(library: .default, name: "wave"), arguments: [
                         .float(time),
                         .float(speed),
                         .float(frequency),
                         .float(amplitude)
                     ]), maxSampleOffset: .zero)
             }
             
             Section("Speed") {
                 Slider(value: $speed, in: 1...15)
             }
             Section("Frequemcy") {
                 Slider(value: $frequency, in: 1...50)
             }
             Section("Amplitude") {
                 Slider(value: $amplitude, in: 1...35)
             }
             
             TimelineView(.animation) {
                 let time = $0.date.timeIntervalSince1970 - startDate.timeIntervalSince1970
                 
                 Text("Hello World!")
                     .font(.largeTitle)
                     .frame(height: 100)
                     //波浪
                     .distortionEffect(.init(function: .init(library: .default, name: "wave"), arguments: [
                         .float(time),
                         .float(speed),
                         .float(frequency),
                         .float(amplitude)
                     ]), maxSampleOffset: .init(width: .zero, height: 100))
             }
         }
         .padding()
         .navigationTitle("Wave")
     }
 }

layerEffect

 func layerEffect(
     _ shader: Shader,
     maxSampleOffset: CGSize,
     isEnabled: Bool = true
 ) -> some View

图层效果

示例

灰度图

layerEffect.gif

 import SwiftUI
 ​
 struct GrayScaleView: View {
     @State private var enableLayerEffect: Bool = false
     var body: some View {
         VStack {
             Image(.xcodeBeta)
                 .resizable()
                 .aspectRatio(contentMode: .fit)
                 .frame(height: 200)
                 .layerEffect(.init(function: .init(library: .default, name: "grayscale"), arguments: []), maxSampleOffset: .zero, isEnabled: enableLayerEffect)
             
             Toggle("Enable Grayscale Layer Effect", isOn: $enableLayerEffect)
             
             Spacer()
         }
         .padding()
         .navigationTitle("GrayScale")
     }
 }

参考资料

iOS17_New_API