LongPressGesture
import SwiftUI
struct LongPressGestureBootcamp: View {
@State var completed: Bool = false
@State var isSuccess: Bool = false
var body: some View {
VStack{
//长按简单例子
Text("Long Press Gesture")
.foregroundStyle(completed ? .white : .black)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(completed ? Color.blue : Color.gray)
.padding(.horizontal)
.cornerRadius(5.0)
//minimumDuration至少按几秒
//maximumDistance一直按的地,可滑动距离
.onLongPressGesture(minimumDuration: 1, maximumDistance: 50) {
completed = true
} onPressingChanged: { value in
}
Rectangle()
.fill(isSuccess ? Color.green : Color.blue)
.frame(height: 50)
.frame(maxWidth: completed ? .infinity : 0)
.frame(maxWidth: .infinity, alignment: .leading)
.cornerRadius(10.0)
//这样写无效
//.frame(width: 10, height: 30)
//.frame(width: .infinity, height: 30)
.background(Color.gray)
.padding()
HStack{
Text("Long Press")
.foregroundColor(.white)
.frame(width: 120, height: 50)
.background(.blue)
.cornerRadius(10)
.padding()
//minimumDuration至少按几秒
//maximumDistance一直按的地,可滑动距离
.onLongPressGesture(minimumDuration: 2.0, maximumDistance: 50) {
//按压完成之后
withAnimation(.easeInOut) {
isSuccess = true
}
} onPressingChanged: { isPressing in
if isPressing {//按压 到 按压时间设置(minimumDuration) 的中间状态
withAnimation(.easeInOut(duration: 2.0)){
completed = true
}
}else{//停止按压
//0.1秒后,去判断是否完成
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if !isSuccess {//没有完成,则回退
withAnimation(.easeInOut) {
completed = false
}
}
}
}
}
Button("Reset Click"){
completed = false
isSuccess = false
}
.foregroundColor(.white)
.frame(width: 120, height: 50)
.background(.blue)
.cornerRadius(10)
.padding()
}
}
}
}
#Preview {
LongPressGestureBootcamp()
}

哈哈,学到了录制git图,这可太棒了~~~
如何录呢?点个赞然后看最后
这块还是有一些疑问的
completed是一个Bool值,但是在加入到animation后,感觉它是一个0.1 0.2 0.3 ... 1的过度渐进值
查阅资料:
在 SwiftUI 中,@State 属性和动画结合使用时,即使 @State 属性本身是一个简单的布尔值,SwiftUI 也能够自动处理相关的动画过渡。这是因为 SwiftUI 内部有一个响应式的布局和动画系统,当状态值发生变化时,它会自动对视图进行重新计算和更新。
即使 completed 只是一个布尔值,SwiftUI 的动画系统会自动检测到这个状态变化会导致视图布局的更新。在这个例子中,当 completed 从 false 变为 true 时,SwiftUI 会检测到 Rectangle 的 maxWidth 从 0 变为 .infinity,这会触发视图宽度的变化。
由于 withAnimation 的包裹,SwiftUI 会对 Rectangle 的宽度变化进行动画处理。在动画过程中,SwiftUI 不是立即跳到最终状态,而是逐步插值,创建一个过渡动画,使得 Rectangle 的宽度从 0 缓慢增加到其父容器的最大宽度。
即使 completed 是一个简单的布尔值,SwiftUI 能够将状态变化与视图的几何变化关联起来,并自动生成过渡动画。这种动画是由 SwiftUI 的布局系统和动画系统自动管理的,不需要手动处理中间过渡值。动画的关键在于 withAnimation 包装的代码块,它指定了状态变化时应该使用的动画效果。
MagnificationGesture
import SwiftUI
struct MagnificationGestureBootcamp: View {
@State private var currentScale: CGFloat = 0.0
//由于放大完后,再次放大会从原始位置开始
//记录最后的缩放大小,然后在第二次缩放的时候,可以从当前时刻的大小去放大或缩小
@State private var lastScale: CGFloat = 0.0
@State private var imageCurrentScale: CGFloat = 0.0
var body: some View {
VStack{
Spacer()
Text("两指放大缩小")
.frame(width: 200, height: 80)
.background(.red)
.cornerRadius(4.0)
.scaleEffect(1 + currentScale + lastScale)
// .scaleEffect(CGSize(width: 1.0, height: 1.0))
//添加缩放手势
.gesture(
MagnificationGesture()
.onChanged({ value in
//value就是上面.scaleEffect填写的值
//其值是1 + xxx
//所以value也是 1 + xxx
//此处不能写成currentScale = value
//这样的话,currentScale = 1+xxx,传到上面直接成了 1 + (1 + xxx—)瞬间扩大了1倍
//所以,此处需要写:currentScale = value - 1
currentScale = value - 1
print("value1-\(value)")
print("currentScale-\(currentScale)")
})
.onEnded({ value in
print("value2-\(value)")
lastScale += currentScale
print("lastScale-\(lastScale)")
//lastScale = value - 1
//结束了,则当前就是0,last就是最后的scale
currentScale = 0.0
})
)
Spacer()
VStack(spacing: 10){
HStack {
Circle()
.frame(width: 35, height: 35)
Text("名字")
Spacer()
Image(systemName: "ellipsis")
}
.padding(.horizontal, 10)
Image("dinner")
.resizable()
.frame(maxWidth: 380, maxHeight: 380).cornerRadius(5.0)
.scaleEffect(1 + imageCurrentScale)
.gesture(
//非MagnifyGesture()
MagnificationGesture()
.onChanged({ value in
imageCurrentScale = value - 1
})
.onEnded({ value in
withAnimation(.spring) {
imageCurrentScale = 0
}
})
)
HStack {
Image(systemName: "heart.fill")
Image(systemName: "text.bubble.fill")
Spacer()
}
.padding(.horizontal, 10)
Text("这是内容内容")
// .frame(maxWidth: .infinity)
// .frame(alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
}
Spacer()
}
}
}
#Preview {
MagnificationGestureBootcamp()
}

其中几个小知识点:
在写.scaleEffect()的时候,Xcode输入.scaleEffect后面会提示三个提示函数:

其中,第一个函数的参数是一个CGSize类型,但是我们传的时候,却可以传:
.scaleEffect(1)
怎么将1赋值给了CGSize类型?
其实并不是,这个其实是.scaleEffect的重载,重载的时候,有一个函数:
public func scaleEffect(_ scale: CGFloat, anchor: UnitPoint = .center) -> some VisualEffect
所以我们可以使用:
.scaleEffect(1)
其实,我们可以看到提升函数的右边有一个箭头,这就表示里面还有其他函数,遇到这种,可以点击箭头去看下里面还有其他可以使用的重载函数:

原来还隐藏了这么多函数~
MagnificationGesture()
MagnificationGesture()与MagnifyGesture()不同,写的时候注意,别写错了
.frame
Text("这是内容内容")
//.frame(maxWidth: .infinity)
//.frame(alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
文字左对齐的时候,使用.frame(maxWidth: .infinity, alignment: .leading),好使
使用
.frame(maxWidth: .infinity)
.frame(alignment: .leading)
无效
GPT的解释:
该问题实际上涉及的是 SwiftUI 布局系统和修饰符的作用顺序。尽管两种方法看起来相似,但它们在本质上有不同的作用机制,因此结果也完全不一样。
方法一:.frame(maxWidth: .infinity) 和 .frame(alignment: .leading)
Text("这是内容内容")
.frame(maxWidth: .infinity)
.frame(alignment: .leading)
.padding(.horizontal, 10)
在这种方法中,修饰符的作用顺序如下:
- .frame(maxWidth: .infinity):这个修饰符首先作用于
Text视图,使其在父视图中占满可用宽度。这里的Text视图已经决定了它的尺寸,但还没有应用对齐方式。 - .frame(alignment: .leading):这个修饰符作用于先前生成的框架,对其视图内容进行定位(默认情况下是居中)。但由于这个
frame修饰符本身没有指定尺寸,它不会改变任何尺寸特性。因此,内层Text视图仍然会保持其原始尺寸(从maxWidth继承来),框架大小未变。 - .padding(.horizontal, 10):在水平方向上给
Text视图添加了 10 点的内边距。
由于内层的 .frame(alignment: .leading) 修改内容的位置,但实际宽度是由外层的 .frame(maxWidth: .infinity) 决定,因此第一个 .frame(alignment: .leading) 不会产生预期的效果。
方法二:.frame(maxWidth: .infinity, alignment: .leading)
Text("这是内容内容")
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10)
在这种方法中,修饰符的作用顺序如下:
- .frame(maxWidth: .infinity, alignment: .leading):这个一体化的修饰符将宽度设置为最大,并同时将内容对齐设置为
leading。这意味着Text视图会占满父视图的宽度,同时内容会进行左对齐。 - .padding(.horizontal, 10):在水平方向上给
Text视图添加了 10 点的内边距。
由于这两个操作在一个修饰符中完成,SwiftUI 确保 Text 视图在改变宽度的同时设置对齐方式,因此达到了预期的效果。
总结
- 方法一 中的
.frame(alignment:)只改变内部内容的位置,而不会改变整个视图框架的尺寸,因此在maxWidth修改后它产生的效果并不明显,无法正确地左对齐文本。 - 方法二 将宽度和对齐方式合并在一个修饰符内,使得框架的尺寸变化和内容对齐同时生效,因此可以正确地使文本左对齐。
RotationGesture
import SwiftUI
struct RotationGestureBootcamp: View {
@State private var angle: Angle = Angle(degrees: 0.0)
var body: some View {
Text("Hello, World!")
//.frame(width: 300, height: 100)
.padding(50)
.foregroundStyle(.white)
.background(.blue)
.cornerRadius(10.0)
.font(.largeTitle)
.rotationEffect(angle)
//注意:切圆角不要放在rotationEffect后面
// .cornerRadius(10.0)
.gesture(
RotationGesture()
.onChanged({ value in
angle = value
})
.onEnded({ value in
withAnimation(.spring) {
angle = Angle(degrees: 0.0)
}
})
)
}
}
#Preview {
RotationGestureBootcamp()
}
animation的时候,
.spring与.spring()效果一样
注意:切圆角不要放在rotationEffect后面
.cornerRadius(10.0)
.rotationEffect(angle)
//.cornerRadius(10.0)

DragGesture
import SwiftUI
struct DragGestureBootcamp: View {
@State var offset: CGSize = .zero
var body: some View {
VStack{
Text("移动的宽度: offset = \(offset.width)")
Spacer()
Rectangle()
.cornerRadius(20.0)
.frame(width: 300, height: 500)
//大小缩放
.scaleEffect(getScaleAmount())
//角度旋转
.rotationEffect(Angle(degrees: getRotationAmount()))
.offset(offset)
.foregroundColor(.green)
.gesture(
DragGesture()
.onChanged({ value in
//translation:类型CGSize
/// This is equivalent to `location.{x,y} - startLocation.{x,y}`.
offset = value.translation
})
.onEnded({ value in
withAnimation(.spring){
offset = .zero
}
})
)
}
}
///左滑、右滑缩小
func getScaleAmount() -> CGFloat {
let maxValue = UIScreen.main.bounds.width / 2.0
let currentAmount = abs(offset.width)
let percentage = currentAmount/maxValue
return 1 - (min(percentage, 0.5) * 0.5)
}
///左滑、优化旋转
func getRotationAmount() -> Double {
let maxValue = UIScreen.main.bounds.width / 2.0
let currentAmount = offset.width
let percentage = currentAmount/maxValue
//转换为Double类型
let percentageAsDouble = Double(percentage)
let maxAngle: Double = 10
return percentageAsDouble * maxAngle
}
}
#Preview {
DragGestureBootcamp()
}

import SwiftUI
struct DragGestureBootcamp2: View {
public let kScreenWidth = UIScreen.main.bounds.width
public let kScreenHeight = UIScreen.main.bounds.height
///开始的地方
@State var staringOffsetY: CGFloat = UIScreen.main.bounds.height * 0.82
@State var currentDragOffsetY: CGFloat = 0
@State var endDragOffsetY: CGFloat = 0
var body: some View {
ZStack{
Color.green.ignoresSafeArea()
MySignUpView()
// .offset(CGSize(width: 0, height: staringOffsetY))
.offset(y: staringOffsetY)
.offset(y: currentDragOffsetY)
.offset(y: endDragOffsetY)
//影响是.offset累加的结果
.gesture(
DragGesture()
.onChanged({ value in
currentDragOffsetY = value.translation.height
})
.onEnded({ value in
///可以停上、中、下三个地方
var totalOffsetY = staringOffsetY + currentDragOffsetY + endDragOffsetY
///如果小于0.5,松手后直接到顶部
///如果大于0.5,但是小于0.82,松手后露半截
///如果大于0.82,松手后,只露一点点
if (totalOffsetY < kScreenHeight * 0.5){
endDragOffsetY = -staringOffsetY
currentDragOffsetY = 0
} else if(totalOffsetY > kScreenHeight * 0.5 && totalOffsetY < kScreenHeight * 0.7){
endDragOffsetY = -UIScreen.main.bounds.height * 0.4
currentDragOffsetY = 0.0
}else{
endDragOffsetY = 0.0
currentDragOffsetY = 0.0
}
///可以停上、下两个地方
///往上推150,就到顶
// if (currentDragOffsetY < -150){
// endDragOffsetY = -staringOffsetY
// currentDragOffsetY = 0
// } else if(endDragOffsetY != 0 && currentDragOffsetY > 150){//往下拉150,就到低
// endDragOffsetY = 0
// currentDragOffsetY = 0.0
// }else{
// currentDragOffsetY = 0.0
// }
})
)
Text("\(currentDragOffsetY)")
}
.ignoresSafeArea(edges: .bottom)
}
}
#Preview {
DragGestureBootcamp2()
}
struct MySignUpView: View {
var body: some View {
VStack(spacing: 20){
Image(systemName: "chevron.up")
.padding(.top)
Text("登录")
.font(.title)
.fontWeight(.semibold)
Image(systemName: "flame.fill")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
Text("这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述这里是描述")
.multilineTextAlignment(.center)
Text("Create an account")
.foregroundStyle(.white)
.padding()
.background(.black)
.cornerRadius(10)
Spacer()
}
.frame(maxWidth: .infinity)
.background(.white)
//切角在背景色后面写
.cornerRadius(20.0)
}
}

ScrollViewReaderBootcamp
import SwiftUI
struct ScrollViewReaderBootcamp: View {
@State var proxy2: ScrollViewProxy?
@State var textString: String
var body: some View {
VStack{
TextField("input number index", text: $textString){}
.frame(height: 50)
.border(Color.gray)
.cornerRadius(5.0)
.padding()
.keyboardType(.numberPad)
ScrollView {
ScrollViewReader(content: { proxy in
ForEach(0..<50) { index in
Text("This is item #\(index)")
.font(.title)
.frame(height: 200)
.frame(maxWidth: .infinity)
.background(.white)
.cornerRadius(10)
.padding()
.shadow(radius: 10)
.id(index)//加上id
}
.onChange(of: textString) { oldValue, newValue in
proxy.scrollTo(Int(newValue), anchor: .center)
}
})
}
}
}
}
struct ScrollViewReaderBootcamp2: View {
@State var proxy2: ScrollViewProxy?
@State var textString: String
var body: some View {
VStack{
TextField("input number index", text: $textString){}
.frame(height: 50)
.border(Color.gray)
.cornerRadius(5.0)
.padding()
.keyboardType(.numberPad)
Button("Select " + textString + " Click Me") {
if let proxy = proxy2 {
withAnimation(.spring){
proxy.scrollTo(Int(textString), anchor: .center)
}
}
}
ScrollView {
ScrollViewReader(content: { proxy in
ForEach(0..<50) { index in
Text("This is item #\(index)")
.font(.title)
.frame(height: 200)
.frame(maxWidth: .infinity)
.background(.white)
.cornerRadius(10)
.padding()
.shadow(radius: 10)
.id(index)//加上id
}
.onAppear(perform: {
self.proxy2 = proxy
})
})
}
}
}
}
struct ScrollViewReaderBootcamp_Previews: PreviewProvider {
static var previews: some View {
Group{
ScrollViewReaderBootcamp( textString: "2")
ScrollViewReaderBootcamp2( textString: "2")
}
}
}

GeometryReaderBootcamp
import SwiftUI
struct GeometryReaderBootcamp: View {
var body: some View {
VStack(spacing: 0){
Rectangle()
.fill(.blue)
.frame(height: kScreenHeight * 0.3)
Rectangle()
.fill(.yellow)
.frame(height: kScreenHeight * 0.7)
}
.ignoresSafeArea()
}
}
struct GeometryReaderBootcamp2: View {
var body: some View {
GeometryReader(content: { geometry in
VStack(spacing: 0){
Rectangle()
.fill(.blue)
.frame(height: geometry.size.height * 0.3)
Rectangle()
.fill(.yellow)
// .frame(height: geometry.size.height * 0.7)
}
.ignoresSafeArea()
})
}
}
struct GeometryReaderBootcamp3: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack{
ForEach(0..<20) { index in
GeometryReader(content: { geometry in
ZStack{
RoundedRectangle(cornerRadius: 20)
.fill(Color.green)
.rotation3DEffect(
Angle(degrees: getPercentage(geometry: geometry) * 10),
axis: (x: 0.0, y: 1.0, z: 0.0)
)
Text("\(index)")
.font(.largeTitle)
}
})
.frame(width: 300, height: 250)
.padding()
}
}
}
}
func getPercentage(geometry: GeometryProxy) -> Double {
//屏幕的一半
let maxDistance = kScreenWidth / 2
//获取每个 RoundedRectangle 的中点位置(X轴),相对于整个屏幕坐标系(global)。
let currentX = geometry.frame(in: .global).midX
//计算当前视图距离屏幕中心点的相对距离
//正中间的时候,下面的值正好是:1-1 = 0
//最左边的时候,下面的值是:1 - 0 = 1
return Double(1 - (currentX/maxDistance))
}
}
struct GeometryReaderBootcamp_Previews: PreviewProvider {
static var previews: some View {
Group{
GeometryReaderBootcamp()
GeometryReaderBootcamp2()
GeometryReaderBootcamp3()
}
}
}

MultipleSheets
import SwiftUI
///方法三:使用sheet的item
struct MultipleSheetsCorrect3Bootcamp: View {
@State var randomSheetStructModel: MultipleSheetStructModel?
@State var isShowSheet = false
var body: some View {
VStack{
VStack{
ScrollView {
ForEach(0..<50) { index in
Button("click me show sheet\(index)") {
isShowSheet.toggle()
randomSheetStructModel = MultipleSheetStructModel(title: "sheet\(index)")
}
.padding()
}
}
}
.sheet(item: $randomSheetStructModel, content: { item in
NextStructSheets(randomSheetStructModel:item)
})
}
}
}
private struct NextStructSheets: View {
var randomSheetStructModel: MultipleSheetStructModel
var body: some View {
Text("structType:\(randomSheetStructModel.title)")
}
}
struct MultipleSheetsCorrect3Bootcamp_Previews: PreviewProvider {
static var previews: some View {
Group{
MultipleSheetsCorrect3Bootcamp()
}
}
}

MaskBootcamp
import SwiftUI
struct MaskBootcamp: View {
///选中几个星星⭐️
@State var selectedStarCount: Int = 0
private var StarView: some View {
HStack{
ForEach(1..<6) { index in
Image(systemName: "star.fill")
.font(.largeTitle)
.onTapGesture {
selectedStarCount = index
}
}
}
}
private var overlayView: some View {
GeometryReader(content: { geometry in
ZStack(alignment: .leading){
Rectangle()
.foregroundColor(.yellow)
///单个星星的宽度 * 几个星星
.frame(width: geometry.size.width/5.0 * CGFloat(selectedStarCount), height: geometry.size.height)
.allowsHitTesting(false)
//放在这错误
// .mask {
// StarView
// }
}
})
}
var body: some View {
VStack(){
Text("选中了\(selectedStarCount)星星⭐️")
StarView
.overlay(){
overlayView
//只能放在这
.mask {
StarView
}
}
}
}
}
#Preview {
MaskBootcamp()
}

SoundsBootcamp
import SwiftUI
import AVKit
import AVFoundation
class SoundManage{
static let instance = SoundManage()
var player: AVAudioPlayer?
var netPlayer : AVPlayer?
//播放音乐
func playSound(urlString: String) {
guard !urlString.isEmpty else {
return
}
stopPlaySound()
let url: URL?
if urlString.contains("www") || urlString.contains("http"){//网络mp3
url = URL(string: urlString)
if let url = url {
let playerItem = AVPlayerItem.init(url: url)
netPlayer = AVPlayer.init(playerItem: playerItem)
netPlayer?.play()
}
}else{//本地mp3
url = Bundle.main.url(forResource: urlString, withExtension: ".mp3")
do {
if let url = url {
player = try AVAudioPlayer(contentsOf: url)
player?.play()
}
}catch let error {
print(error)
}
}
}
func stopPlaySound(){
player?.stop()
netPlayer?.pause()
}
}
struct SoundsBootcamp: View {
var body: some View {
Button("play local sound1") {
SoundManage.instance.playSound(urlString: "voip_call")
}
Button("play local sound2") {
SoundManage.instance.playSound(urlString: "voip_calling_ring")
}
Button("play network sound") {
SoundManage.instance.playSound(urlString: "https://music.163.com/song/media/outer/url?id=1809815407.mp3")
}
Button("stop play sound") {
SoundManage.instance.stopPlaySound()
}
}
}
#Preview {
SoundsBootcamp()
}

1438

被折叠的 条评论
为什么被折叠?



