SwiftUI 开发技巧
初识SwiftUI。
Environment变量都有些什么?
这是一些swift默认环境变量,不需要显性传值。
// 1. presentationMode - 关闭模态视图
@Environment(\.presentationMode) var presentationMode
presentationMode.wrappedValue.dismiss()
// 2. colorScheme - 深色/浅色模式
@Environment(\.colorScheme) var colorScheme
if colorScheme == .dark { ... }
// 3. dismiss - iOS 15+ 新的关闭方式
@Environment(\.dismiss) var dismiss
dismiss()
// 4. locale - 语言区域
@Environment(\.locale) var locale
print(locale.identifier) // zh_CN
// 5. scenePhase - 应用生命周期
@Environment(\.scenePhase) var scenePhase
if scenePhase == .active { ... }
// 6. openURL - 打开链接
@Environment(\.openURL) var openURL
openURL(URL(string: "https://...")!)
// 7. layoutDirection - 布局方向
@Environment(\.layoutDirection) var layoutDirection
if layoutDirection == .rightToLeft { ... }其中,presentationMode的作用是关闭view,返回上一个视图,而dismiss的作用同样是关闭视图返回上一个视图,是因为presentationMode是比较旧的API,为了兼容旧版代码,直接使用dismiss即可
什么是 @escaping?
// Non-escaping(默认):闭包在函数返回前执行完成
func doSomething(completion: () -> Void) {
completion() // 立即调用
} // 函数返回,completion 生命周期结束
// Escaping:闭包在函数返回后仍可能被调用
func doSomethingLater(completion: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion() // 1秒后调用(函数已经返回)
}
}异步函数默认支持escapting,所以对于async修饰的函数,不需要额外加@escaping
那要如何把函数赋值给属性变量?
要赋值给属性?必须是 escaping!
同步闭包:默认 non-escaping,需要加 @escaping
异步闭包:默认 escaping,不需要加 @escaping
Non-escaping = 函数返回前执行完
Escaping = 可以在函数返回后执行
如果在init函数中,若参数存在函数即()→void,那一定要添加@escaping,因为no-escaping的闭包不能赋值给属性,同时闭包需要在函数返回前执行完,所以在init中将函数赋值给属性的情况下,一定要是用escaping
包装属性的赋值方式
对一个包装类的属性,_属性,属于包装器本身State<T>、ObjectState<T>等,属性(无_),是包装后的值
// 等价于(伪代码)
private var _count: State<Int> // 属性包装器
private var count: Int { // 计算属性
get { _count.wrappedValue }
set { _count.wrappedValue = newValue }
}所以在init函数中,只能通过包装器来赋值,也就是_属性来设置值,首先通过参数构架出一个包装器State(initialValue:val),然后在赋值给_属性
但是在init之外,可以直接使用=来赋值
AnyView的使用技巧
用来兼容多个类型的view,一个func可能返回多个不同类型的view
var view3: some View {
if condition {
return Text("Hello") // Text
} else {
return Image("icon") // Image - 类型不同!
}
}这种就会报错,因为Text和Image不是用一种类型,所以编译就会不通过,这种情况就能使用AnyView来兼容多个view,统一返回的都是AnyView,
var content: some View {
switch type {
case .text:
return AnyView(Text("文本"))
case .image:
return AnyView(Image("图片"))
case .button:
return AnyView(Button("按钮") {})
}
}
// 或者下面这种用法
let text: Text = Text("Hello")
let image: Image = Image("icon")
// 不同类型,无法放入同一个数组
let views: [???] = [text, image] // 什么类型?
// ✅ 使用 AnyView 统一类型
let views: [AnyView] = [
AnyView(text),
AnyView(image)
]更推荐替换的方式,使用@ViewBuilder
@ViewBuilder是什么
是一个结果构建器,允许在闭包中使用条件语句和多个视图,自动处理解决类型问题
// 不使用 @ViewBuilder
var content: some View {
if condition {
return AnyView(Text("A")) // 需要 AnyView
} else {
return AnyView(Text("B"))
}
}
// 使用 @ViewBuilder
@ViewBuilder
var content: some View {
if condition {
Text("A") // 不需要 AnyView
} else {
Text("B")
}
// 也不需要 return
}可以将多个view组合成一个View返回,或者使用hstack视图包裹住全部要返回的视图
如果返回的视图中存在多种类型的View,不使用@ViewBuild则会出现变异报错
dismiss() 函数的妙用
dismiss()的作用是返回父视图,关闭当前视图,会立即将isPresented改为false,swiftUI在本帧就知道该收起视图,关闭当前页面。而有时时候,在执行dismiss()的时候,会一起执行其他函数,而dissmiss()往往都是第一个执行
因为这是相相对保险的做法,其他函数(比如刷新父视图数据、清空状态、触发 analytics)如果在 dismiss() 之前就执行,有可能在还没切断绑定前又触发展示逻辑,造成闪烁或二次展示。
所以把 dismiss() 放在最前,等于是先告诉swiftUI“我已经关闭了”,随后做的任何副作用都不会被误解为仍在显示的视图要更新
@discardableResult的用处
对一个函数来说,分为有返回值和没有返回值两种类型
有返回值,一般使用 → object 来定义,这种函数在调用的时候,需要定义一个let变量来承接,如果不定义,则会出现编译异常,
无返回值,一般使用 → Void 来定义,表明这个函数无返回值
但一些函数虽然有返回值,但是在代码中实际不会使用到其返回值,那么就可以在函数上面用@discardableResult来装饰,这样就不用再使用let定义变量来承接, 像Void一样使用即可