前言
参考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
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的透明度变化的动画效果
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
将过渡与视图关联起来。
示例
自定义转场动画
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。
示例
动画结束后执行
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 协议的一致性
示例
数据观测和绑定
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
不均匀的圆角矩形,具有不同值的圆角的矩形形状,在包含它的视图的框架内对齐。
示例
部分圆角
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
视觉效果更改视图的视觉外观,而不更改其祖先或后代。
示例
改变视图视觉外观
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动画
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
通过关键帧对内容进行动画处理的容器。
示例
关键帧动画
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(
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 像素化
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 波浪
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
图层效果
示例
灰度图
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")
}
}