Published on

如何自定义一个ViewModifier

Authors
  • Name
    Twitter

在使用 SwiftUI 的过程中,对比 UIKit 组织 UI 的方式,最明显的区别感受是,SwiftUI中对 ViewModifier 的链式调用。

我们可以通过在符合 View 协议的视图上使用一连串的 ViewModifier 来改变其外观和进行一些操作。

关于其使用方法,在 Apple 的 SwiftUI 官方文档中已经说得很清楚了
(SwiftUI/ViewModifier)[https://developer.apple.com/documentation/swiftui/viewmodifier]

如何自定义一个 ViewModifier

1.创建一个结构体,实现 ViewModifier 协议

Adopt the ViewModifier protocol when you want to create a reusable modifier that you can apply to any view. The example below combines several modifiers to create a new modifier that you can use to create blue caption text surrounded by a rounded rectangle:

中文翻译:

当您想创建一个可重复使用的修改器,并将其应用于任何视图时,请采用 ViewModifier 协议。下面的示例结合了多个修改器,创建了一个新的修改器,您可以用它来创建由圆角矩形包围的蓝色标题文本:

struct BorderedCaption: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.caption2)
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 1)
            )
            .foregroundColor(Color.blue)
    }
}

2.(可选)为 View 添加扩展方法,方便调用,这样可以像调用内置修饰器一样使用你的自定义修饰器。

You can apply modifier(:) directly to a view, but a more common and idiomatic approach uses modifier(:) to define an extension to View itself that incorporates the view modifier:

中文翻译:

您可以直接对视图应用 modifier(:),但更常见、更习惯的方法是使用 modifier(:) 来定义一个包含视图修改器的 View 扩展:

extension View {
    func borderedCaption() -> some View {
        modifier(BorderedCaption())
    }
}

3.像使用内置调用器一样使用自定义修饰器

You can then apply the bordered caption to any view, similar to this:

Image(systemName: "bus")
    .resizable()
    .frame(width:50, height:50)
Text("Downtown Bus")
    .borderedCaption()
SwiftUI-View-ViewModifier

代码解释

我在自定义的过程中,有一些比较疑惑的点
我们先看下 ViewModifier 协议的定义

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@MainActor @preconcurrency public protocol ViewModifier {

    /// The type of view representing the body.
    associatedtype Body : View

    /// Gets the current body of the caller.
    ///
    /// `content` is a proxy for the view that will have the modifier
    /// represented by `Self` applied to it.
    @ViewBuilder @MainActor @preconcurrency func body(content: Self.Content) -> Self.Body

    /// The content view type passed to `body()`.
    typealias Content
}

  • @MainActor 表明该协议和方法必须在主线程(Main Actor 环境)中执行

  • @preconcurrency Swift 5.5引入的并发相关注解,用于在引入严格并发检查之前,允许该协议在非严格并发环境中使用,以向后兼容旧代码。

  • associatedtype 定义了一个关联类型 Body ,其必须符合 View 类型协议,作为方法
    func body(content: Self.Content) -> Self.Body 的返回类型,Body 表示修饰器应用后返回的视图类型,也就是最终的视图内容。

  • typealias 定义了一个类型别名 Content,表示传递给 body 方法的输入视图类型。Content 是修饰器将要修改的原始视图的类型。

  • @ViewBuilder @MainActor @preconcurrency func body(content: Self.Content) -> Self.Body 遵循 ViewModifier 协议的类型(即我们自定义的 ViewModifier) 必须要实现的方法。

知识点

  1. @ViewBuilder 是否真正了解和掌控使用
    Anwser: @ViewBuilder:SwiftUI的DSL(领域特定语言)特性,允许在方法体内以声明式语法构建视图,自动将多个视图组合成单一视图。

  2. 为什么只需要实现其定义的方法就可以了,对于 Body 和 Content 都不需要定义呢?
    Anwser:不论是 Body 还是 Content 都会进行自动类型推断。因此我们只需要实现 body 方法即可。就像我们定义一个字符串一样:

let name = "duke"

会进行自动类型推断

  1. 为什么在 func body(content: Self.Content) -> Self.Body 中 入参中的 content 在类型是 Self.Content ,其采用 typealias 定义。 而返回结果 Self.Body 采用 associatedtype 定义。

typealias
用于为现有类型创建别名,固定绑定,编译时确定。 适合简化类型名称或提高可读性,无需动态类型选择。 在ViewModifier中,typealias Content表示输入视图,灵活且由上下文推断。

associatedtype
用于协议中定义抽象类型,动态绑定,需实现者指定类型。 适合协议中需要灵活类型关系并施加约束的场景。 在ViewModifier中,associatedtype Body : View 表示返回视图需符合View约束。

本质区别
typealias是静态的类型重命名,associatedtype是动态的类型占位符。 typealias无约束能力,associatedtype支持协议约束和动态类型绑定。