- Published on
在SwiftUI中如何自定义一个ViewModifier及其代码语法解释
- Authors
- Name
在写 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()

代码解释
我在自定义的过程中,有一些比较疑惑的点
我们先看下 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(领域特定语言)特性,允许在方法体内以声明式语法构建视图,自动将多个视图组合成单一视图。
疑惑的点
- @ViewBuilder 是否真正了解和掌控使用
- 为什么在
func body(content: Self.Content) -> Self.Body
中 入参中的 content 在类型是 Self.Content ,其采用typealias
定义。 而返回结果 Self.Body 采用associatedtype
定义。 - 当我们自定义一个符合 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 方法即可。