这是一篇来自 Majid 的文章的翻译,并且添加了部分内容,便于了解。

本文介绍了现在已知的部分 SwiftUI 新特性。因为今天是 WWDC 的第一天,现在官方只放出了 Platforms State of the Union 这一支技术视频,因而本文只掩盖到了视频内提到的所有新特性。后续新特性会在之后几天中连续介绍(假如有时间的话)。

本文中呈现的新 API 需求调配 Xcode 15 beta 运用,并且部分 API 还没有实装。

WWDC 23按期而至,许多内容也随之更新并被添加到 SwiftUI 结构中。阅览本文,你能够了解到 SwiftUI 结构第5次迭代中新增的几个最重要的特性。

数据流

与 UIKit 不同,SwiftUI 运用新的调查结构(Observation framework)作为其数据流。调查结构供给了一个 Observable 协议,咱们经过它来订阅更改和更新 SwiftUI 视图。不过之前 SwiftUI 的数据流 API 比较杂乱,需求了解的细节许多,上手本钱颇高。

不过跟着 Swift 5.9 引入了宏特性,Observation framework 运用宏优化了许多 API ,精简和躲藏了部分逻辑。

@Observable

现在不需求让类(ViewModel)遵循 Observable 协议,直接运用 @Observer 宏符号就行,这个宏会主动让该类遵循 Observable 协议。现在也不需求给特点前加 @Published,因为 SwiftUI 视图会主动盯梢任何可调查类型的可用特点的更改。

// before
final class Store: ObservableObject {
    @Published var products: [String] = []
    @Published var favorites: [String] = []
    func fetch() async {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        // load products
        products = [
            "Product 1",
            "Product 2"
        ]
    }
}
// now
@Observable
final class Store {
    var products: [String] = []
    var favorites: [String] = []
    func fetch() async {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        // load products
        products = [
            "Product 1",
            "Product 2"
        ]
    }
}

此处开发人员还提到:

Observable lets SwiftUI track access at a per-field level, so your view’s body is only re-evaluated when the specific properties used by your view change. If you modify a field not used by your view, no invalidation will happen at all.

新的监听方式会让 UI 刷新的范围限定在更改正的特点上,因而不会呈现之前一个 ViewModel 中的某一个特点更改,所有引证到 ViewModel 的 View 都会刷新的问题,算是大大提升了优化功率。

@State

此前咱们有许多特点包装器,比如 StateStateObjectObservedObjectEnvironmentObject,开发者需求清晰在什么时分运用什么类型的包装器。现在,状况管理变得更简略了。关于值类型(StringInt 等)以及契合 Observable 协议的引证类型(如 ViewModel ),无脑运用 @State 符号就行了。

struct ProductsView: View {
    // @State private var text = ""
    // @StateObject private var store = Store()
    let store = Store()
    var body: some View {
        List(store.products, id: .self) { product in
            Text(verbatim: product)
        }
        .task {
            if store.products.isEmpty {
                await store.fetch()
            }
        }
    }
}

因为 @State 完全抹平了 StateStateObjectObservedObjectEnvironmentObject 的差异,因而 @State private var store = Store() 这段代码定义的 store 不仅仅是一个 StateObject,也是 EnvironmentObject。简略来讲,之前 Apple 把杂乱的内部实现露出出来的,可是从新版 SwiftUI 开端,你不需求再了解这些细节,只需求了解一种状况类型 State 即可。既然如此,咱们能够玩一个小技巧。下面的代码, store 不仅仅是 ProductsView 内部的状况类,也被 .environment(store) 直接变为了 EnvironmentObject 注入到了所有子视图了。EnvironmentViewExample 就能够经过 Environment API 拿到 store 实例。

struct EnvironmentViewExample: View {
    @Environment(Store.self) private var store
    var body: some View {
        Button("Fetch") {
            Task {
                await store.fetch()
            }
        }
    }
}
struct ProductsView: View {
    @State private var store = Store()
    var body: some View {
        List(store.products, id: .self) { product in
            Text(verbatim: product)
        }
        .task {
            if store.products.isEmpty {
                await store.fetch()
            }
        }
        .toolbar {
            NavigationLink {
                EnvironmentViewExample()
            } label: {
                Text(verbatim: "Environment")
            }
        }
        .environment(store)
    }
}

@ObservedObject

这个比如中,咱们有一个视图,它有一个从外部传进来的参数 store。在此之前,咱们需求运用 @ObservedObject 特点包装器来符号它,以订阅它的更改。现在不需求了,因为 SwiftUI 视图会主动盯梢契合 Observable 协议的类型的更改。

struct FavoriteProductsView: View {
    // @ObservedObject let store: Store
    let store: Store
    var body: some View {
        List(store.favorites, id: .self) { product in
            Text(verbatim: product)
        }
    }
}

@Bindable

struct BindanbleViewExample: View {
    @Bindable var store: Store
    var body: some View {
        List($store.products, id: .self) { $product in
            TextField(text: $product) {
                Text(verbatim: product)
            }
        }
    }
}

关于双向绑定(Binding),之前只能将基础类型包装成 Binding 传递给子视图,现在新增了一个 @Bindable,能够用来符号引证类型,这样就能直接把 ViewModel 传递给子视图了。

动画

动画是 SwiftUI 结构中最重要的部分,在 SwiftUI 中给任何东西添加动画都是垂手可得的。此次更新为 SwiftUI 动画添加了一些新特性。

withAnimation completion

正如比如中看到的,withAnimation API 新增了 completion 回调,便利咱们对动画进行操控。

struct AnimationExample: View {
    @State private var value = false
    var body: some View {
        Text(verbatim: "Hello")
            .scaleEffect(value ? 2 : 1)
            .onTapGesture {
                withAnimation {
                    value.toggle()
                } completion: {
                    print("Animation have finished")
                }
            }
    }
}

PhaseAnimator

SwiftUI 结构引入了新的 PhaseAnimator 类,答应为每个阶段供给不同的动画,并在阶段更改时更新内容。这个其实属于一个工具类,便利咱们做一些拥有不同状况或阶段的动画。没有这个类也能做,便是稍微麻烦点。

enum Phase: CaseIterable {
    case start
    case loading
    case finish
}
struct PhasedAnimationExample: View {
    @State private var value = false
    var body: some View {
        PhaseAnimator(Phase.allCases, trigger: value) { phase in
            switch phase {
            case .start:
                StartPhaseView()
                    .onTapGesture {
                        value.toggle()
                    }
            case .loading:
                LoadingPhaseView()
            case .finish:
                FinishPhaseView()
            }
        } animation: { phase in
            switch phase {
            case .start: .easeIn(duration: 0.3)
            case .loading: .easeInOut(duration: 0.5)
            case .finish: .easeOut(duration: 0.1)
            }
        }
    }
}

KeyframeAnimator

本年 SwiftUI 也带来了关键帧动画,应该能在动画自由度上再上一层楼。可是现在没有看到相关 API,官方演示的 demo 是 MapKit 中的运用案例。

【译】WWDC 2023 带来的 SwiftUI 部分新特性

ScrollView

ScrollView 本年有很好的补充。首先,咱们能够运用 scrollPosition 视图修饰符来操控和调查内容偏移。

struct ContentView: View {
    @State private var scrollPosition: Int? = 0
    var body: some View {
        ScrollView {
            Button("Scroll") {
                scrollPosition = 80
            }
            ForEach(1..<100, id: .self) { number in
                Text(verbatim: number.formatted())
            }
            .scrollTargetLayout()
        }
        .scrollPosition(id: $scrollPosition)
    }
}

scrollTargetBehavior

新增 scrollTargetBehavior, 支撑分页模式。

struct ContentView: View {
    var body: some View {
        ScrollView {
            ForEach(1..<100, id: .self) { number in
                Text(verbatim: number.formatted())
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.paging)
    }
}

查找

现在能够运用 searchable 视图修饰符的 isPresent 参数来显现/躲藏查找字段。还能够运用 searchScope 来限定查找范围。

struct ProductsView: View {
    @State private var store = Store()
    @State private var query = ""
    @State private var scope: Scope = .default
    var body: some View {
        List(store.products, id: .self) { product in
            Text(verbatim: product)
        }
        .task {
            if store.products.isEmpty {
                await store.fetch()
            }
        }
        .searchable(text: $query, isPresented: .constant(true), prompt: "Query")
        .searchScopes($scope, activation: .onTextEntry) {
            Text(verbatim: scope.rawValue)
        }
    }
}

手势

新增 RotateGestureMagnifyGesture, 答应咱们盯梢视图的旋转和放大。

struct RotateGestureView: View {
    @State private var angle = Angle(degrees: 0.0)
    var rotation: some Gesture {
        RotateGesture()
            .onChanged { value in
                angle = value.rotation
            }
    }
    var body: some View {
        Rectangle()
            .frame(width: 200, height: 200, alignment: .center)
            .rotationEffect(angle)
            .gesture(rotation)
    }
}

其他改善

ContentUnavailableView

新增 ContentUnailableView 视图,能够在需求的时分显现空视图,展现一个图片和一行文字。算是一个很小的改善吧。

struct ProductsView: View {
    ContentUnavailableView("Products list is empty", systemImage: "list.dash")
}

#Preview

新增 #Preview 宏,替代原来杂乱的 preview 代码。不过 preview 模版代码基本上都是新建 View 的时分模板主动生成的,影响不大。

// before
struct PlayerView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
// now
#Preview {
    ContentView()
}

其他

新增一对新的视图修饰符,答应咱们调整列表中的距离。您能够运用 listRowSpaceslistSectionSpaces 视图修饰符来设置列表中所需的距离。

EnvironmentValues 结构包括一系列与最新渠道更新相关的新特点,比如 isActivityFullscreen 和 showsWidgetContainerBack。

Swift Charts 支撑滚动。

SF Symbols 添加动画作用。

总结

每一年的 SwiftUI 迭代都会让我感觉,这些新特性分明应该早就有才对。像是 @State 这样的能力,假如一开端就有,会大大下降 SwiftUI 学习的门槛。不过这次 API 上的改动许多都依赖于 Swift 宏,而宏直到 4.9 才正式发布,也算是能够了解吧。不过依照这样的节奏下去,不知道何年马月才能真实大面积用上 SwiftUI 。听说本年 Platforms State of the Union 上一共就提了 3 次 UIKit,其间一次还是讲的 #Preview 支撑预览 UIKit 。看来咱们将会在很长一段时间内卡在两代技术中间。或许,投入另一个新技术:realityOS?