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一样使用即可