普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月6日首页

#1 onLongPressGesture

作者 Neo_Arsaka
2025年12月6日 17:00

功能

为任意 View 添加长按手势识别。当用户持续按压且达到指定时长、同时手指偏移不超过阈值时,视为一次有效长按;可实时获取按压状态以驱动过渡动画。

参数说明

  • minimumDuration:触发所需最短按压时间(秒)。
  • maximumDistance:手指允许的最大偏移,单位为点;超限即判定为取消。
  • onPressingChanged:按压状态变化回调;true 表示按下,false 表示抬起或滑出。
  • action:满足时长与偏移条件后执行的一次性回调。

代码示例

struct LongPressGestureBootcamp: View {
    
    @State var isComplete: Bool = false
    @State var isSuccess: Bool = false
    var body: some View {
        
        VStack {
            Rectangle()
                .fill(isSuccess ? .green : .blue)
                .frame(maxWidth: isComplete ? .infinity : 0)
                .frame(height: 56)
                .frame(maxWidth: .infinity, alignment: .leading)
                .background(.gray)
            
            HStack {
                Text("CLICK HERE")
                    .foregroundStyle(.white)
                    .padding()
                    .background(.black)
                    .cornerRadius(8)
                    .onLongPressGesture(
                        minimumDuration: 1.0,
                        maximumDistance: 56) { (isPressing) in
                            // start of press -> min duration
                            if isPressing {
                                withAnimation(.easeInOut(duration: 1.0)) {
                                    isComplete = true
                                }
                            }
                            else {
                                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                                    if !isSuccess {
                                        withAnimation(.easeInOut) {
                                            isComplete = false
                                        }
                                    }
                                }
                            }
                        } perform: {
                            // at the min duration
                            withAnimation(.easeInOut) {
                                isSuccess = true
                            }
                        }
                
                Text("RESET")
                    .foregroundStyle(.white)
                    .padding()
                    .background(.black)
                    .cornerRadius(8)
                    .onTapGesture {
                        isComplete = false;
                        isSuccess = false;
                    }
            }
        }
        
        
//        Text(isComplete ? "COMPLETED" : "NOT COMPLETE")
//            .padding()
//            .background(isComplete ? .green : .gray)
//            .cornerRadius(8)
////            .onTapGesture {
////                withAnimation {
////                    isComplete.toggle()
////                }
////            }
//            .onLongPressGesture(minimumDuration: 1.0, maximumDistance: 50, perform: {
//                isComplete.toggle()
//            })
    }
}

注意事项

  1. 若同时附加 .onTapGesture,长按结束后可能额外触发一次点按,应通过状态标志互斥。
  2. onPressingChanged 中更新界面时,请使用 withAnimation 保证过渡流畅。
  3. 耗时操作请置于 action 的异步闭包内,避免阻塞主线程。

Swift 疑难杂想

作者 Neo_Arsaka
2025年12月6日 16:50

@State, @StateObject, @Published

@State

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 20) {
            Text("点了 (count) 次")   // 2. 读值
            Button("+1") {
                count += 1           // 3. 改值 → 自动刷新界面
            }
        }
        .font(.largeTitle)
    }
}

@State 是 SwiftUI 里最常用的属性包装器之一。

注意事项

  • 只能用于 当前 View 内部 的私有可变状态。
  • @State 的值改变时,SwiftUI 会 自动重新计算 body,把最新数据画到屏幕上。

@StateObject

import SwiftUI
import Combine

// 1. 先写一个可观察的模型
class TimerModel: ObservableObject {
    @Published var seconds = 0        // 2. 发布变化
    private var cancellable: AnyCancellable?
    
    init() {
        cancellable = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { _ in
                self.seconds += 1
            }
    }
}

// 3. 视图里“创建并持有”这个模型
struct TimerView: View {
    @StateObject private var model = TimerModel()   // ← 关键:@StateObject
    
    var body: some View {
        Text("(model.seconds) 秒")
            .font(.largeTitle)
    }
}

@StateObject 也是属性包装器,专门用来 创建并持有 一个 ObservableObject 实例。

注意事项

  • 对象里的 @Published 属性一旦变化,所有用到它的视图自动刷新
  • 只有第一次初始化时才会真正创建;后面 SwiftUI 重绘视图时不会反复 new 出新对象。

@Published

@Published 不是给 View 用的属性包装器,而是 写在 ObservableObject 里的“广播器”只要这个属性值一变,立刻通知所有正在监听它的视图

注意事项

  • 只能用在 ObservableObject 协议 的类里。
  • 标记为 @Published 的属性,SwiftUI 会自动生成 objectWillChange 发布事件。
  • 视图那一端用 @StateObject@ObservedObject 拿到这个对象后,就能 实时响应 这些变化。

Q&A

Q:@State 只能用在集成了 View 的struct或者class 中?

A:@State 只能出现在 SwiftUI 的 View 结构体(或极少数自己实现 View 协议的类)里,而且必须是 当前视图私有的可变状态

Q:@state只能修饰一些基础类型,无法修饰复杂的引用类型等

A:@State 完全可以修饰“复杂”的引用类型(class、嵌套 struct、数组、字典、自定义模型……),只要它满足两点:

  • 类型遵守 Equatable(SwiftUI 靠它 diff,官方文档推荐,不遵守也能跑,但可能多刷)。
  • 你在 View 里 把整个实例换掉(值语义)——SwiftUI 只关心“旧值 vs 新值”有没有变化,不关心内部字段。

Q:就是说比如我定义了一个 @State var person = Person(name: "Xiaomi") ,然后当我更换引用对象比如,person = Person(name:"Sam")也会触发界面的刷新?

A:没错,会刷新——前提是你把“整颗对象”换掉

Q:那么如果我想让类的成员可视化到界面上,并实时显示最新的值,那么我就得遵守 ObservableObject 协议 ,并且使用 @Published 修饰对应的成员?

A:是的,想让“类的某个成员”一变化就自动刷新界面,必须同时满足:

  • 类遵守 ObservableObject 协议;
  • 那个成员用 @Published 修饰;
  • 视图通过 @StateObject(或 @ObservedObject)持有这个类实例。

避坑小贴士

  1. 不要init 里直接修改 @State / @StateObject 包装的值,SwiftUI 会忽略这次写入。
  2. 不要@StateObject 用在“不是真正拥有者”的视图里,否则可能出现“同一模型被重复创建”或“野指针”问题;子视图只观察时用 @ObservedObject
  3. 不要ObservableObject 里手动调用 objectWillChange.send(),除非你有特殊时机需求;@Published 已经帮你做了。
  4. 不要把值类型(struct)也加上 @Published,它照样工作,但会失去“共享引用”的意义,容易陷入“改了副本、界面没反应”的错觉。

界面背景色

Q&A

Q:最新的swift ui语法中,如何设置界面的背景色,并且忽略安全边界? Color.blue.ignoreSageAre() 这么写是不是不是最佳开发实践了?

A: 你写的 ignoreSageAre() 拼写错了,应该是
.ignoresSafeArea()(iOS 14+ 的名字,以前叫 .edgesIgnoringSafeArea(.all))。

另外,直接把 Color.blue 当根视图 虽然能跑,但属于“偷懒写法”——
一旦后面要加别的控件,就得再包 ZStack,不如一开始就写成“背景层 + 内容层”分离的形式,可读性、可维护性都更好。

推荐写法(iOS 14+ 官方风格):

struct ContentView: View {
    var body: some View {
        ZStack {
            Color.blue                // 1. 背景层
                .ignoresSafeArea()    // 2. 忽略安全边界
            
            Text("Hello, SwiftUI")    // 3. 内容层
                .foregroundColor(.white)
        }
    }
}

Q:我记得还可以直接 ZStack{..}.background(.blue).ignore啥的,是不是也可以?

A:可以,但顺序得换一下:
background 只是给 ZStack 刷一层颜色,本身不会自动撑满屏幕,所以要把 .ignoresSafeArea() 贴在真正“能撑满”的那个视图(也就是 Color.blue)上,而不是贴在 ZStack 后面。

ZStack {
    Text("Hello, SwiftUI").foregroundColor(.white)
}
.background(.blue.ignoresSafeArea())   // iOS 15+ 语法糖

Q:和Color.blue.ignoresSafeArea() 这种方式比,那种方式更好一些?

A:两种写法在最终视觉效果上几乎一样:整屏蓝底、无视安全区。差异主要在可读性、扩展性、习惯上,而不是性能。

Color.blue.ignoresSafeArea() 是最佳实践。

两种写法在最终视觉效果上几乎一样:整屏蓝底、无视安全区。
差异主要在可读性、扩展性、习惯上,而不是性能。

  • 快速原型、单页极简界面:用 .background(Color.blue.ignoresSafeArea()) 完全没问题。
  • 正式项目、多人协作、后续会迭代:优先 ZStack 里单独放 Color,扩展、阅读都更舒服。

字体颜色

Q&A

Q:最新的swift ui 中 .fontColor 好像已经不推荐了?

A:在 SwiftUI 里并没有 .fontColor 这个修饰器,从 iOS 13 开始官方就统一用
.foregroundColor(_:) 或 iOS 15 起更通用的 .foregroundStyle(_:) 来给 Text、Image、Shape 等设置“前景色”。

Text("Hello")
    .foregroundColor(.red)      // iOS 13+ 专用颜色
// 或者
Text("Hello")
    .foregroundStyle(.red)      // iOS 15+ 接受任何 ShapeStyle(纯色、渐变、材质)

圆角

Q&A

Q:Text("CLICK HERE") .foregroundStyle(.white) .padding() .background(.black).cornerRadius(8) 这么设置圆角好像也不推荐了在新的swiftui中

A:cornerRadius(8) 并没有被废弃,代码现在依旧能编译、能跑;
但在 iOS 17 的 Apple Sample 和 WWDC 视频里,官方更推荐用 .clipShape(.rect(cornerRadius: 8)) (或者 .clipShape(RoundedRectangle(cornerRadius: 8)))来裁切,语义更清晰——“先画完再裁”,而不是“把边框圆角当成修饰器”。

// 1. 老写法(依旧可用)
Text("CLICK HERE")
    .foregroundStyle(.white)
    .padding()
    .background(.black)
    .cornerRadius(8)

// 2. iOS 17 推荐写法
Text("CLICK HERE")
    .foregroundStyle(.white)
    .padding()
    .background(.black)
    .clipShape(.rect(cornerRadius: 8))   // ← 语义:把整体裁成圆角矩形
❌
❌