Published on

在SwiftUI中如何自定义一个ViewModifier及其代码语法解释

Authors
  • Name
    Twitter

在写 SwiftUI 的过程中,和 UIKi t组织 UI 的区别,感受最明显的SwiftUI中对 ViewModifier 的使用。

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

关于其使用方法,在 Apple 的 SwiftUI 官方文档中已经说得很清楚了
(SwiftUI/ViewModifier)[https://developer.apple.com/documentation/swiftui/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:

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

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:

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

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的类型必须提供的方法

@ViewBuilder:SwiftUI的DSL(领域特定语言)特性,允许在方法体内以声明式语法构建视图,自动将多个视图组合成单一视图。

疑惑的点

  1. @ViewBuilder 是否真正了解和掌控使用
  2. 为什么在 func body(content: Self.Content) -> Self.Body 中 入参中的 content 在类型是 Self.Content ,其采用 typealias 定义。 而返回结果 Self.Body 采用 associatedtype 定义。
  3. 当我们自定义一个符合 ViewModifier 协议的重复使用的 modifier的时候,我们的代码是这样的:
struct BorderedCaption: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.caption2)
            .padding(10)
            .overlay(
                RoundedRectangle(cornerRadius: 15)
                    .stroke(lineWidth: 1)
            )
            .foregroundColor(Color.blue)
    }
}

为什么只需要实现其定义的方法就可以了,对于 Body 和 Content 都不需要定义呢?

typealias:

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

associatedtype:

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

本质区别:

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

没有进行定义的原因是,Swift编译器能对类型进行自动推断,就像我们定义一个字符串一样:

let name = "duke"

不论是 Body 还是 Content 都会进行类型自动推断。因此我们只需要实现 func 方法即可。