做 iOS 开发的,谁没在 UIKit 里享受过"继承的快乐"?比如写个 BaseViewController,把导航栏样式、加载动画、空白页统一封装好,后面所有页面直接 : BaseViewController,一顿操作猛如虎,不用重复写代码------主打一个"父债子还"(不是),"父功子享"才对!
可等咱们兴冲冲转到 SwiftUI,想依葫芦画瓢写个 BaseView,再让 HomeView: BaseView 时,Xcode 直接给你泼一盆冷水:"兄弟,你怕不是喝多了?View 是协议,不是类,不能继承!"
那一刻,多少开发者的内心是崩溃的:"SwiftUI 你玩我呢?UIKit 能行的事,你凭啥不行?我就想省点劲,有错吗?"
别急别急,今天就用唠嗑的方式,扒一扒 SwiftUI 为啥"反骨"不支持 View 继承,以及它到底藏了啥"骚操作",能比 UIKit 的继承更省心(偶尔也更闹心)。
先吐槽:UIKit 的继承有多香,SwiftUI 的"拒绝"就有多离谱?
咱们先回味下 UIKit 的"继承爽点":
- 「一脉相承」:BaseVC 写好导航栏隐藏、返回按钮自定义,所有子类自动继承,不用重复写一行代码;
- 「按需修改」:子类想改个导航栏颜色?重写个方法就行,不影响其他子类,主打一个"个性化不破坏全局";
- 「新人友好」:新人接手项目,只要懂 BaseVC 的封装,所有页面的基础逻辑一目了然,不用到处找重复代码。
反观 SwiftUI,一上来就断了"继承"这条路------核心原因很简单(虽然听着有点绕):SwiftUI 的 View 是"协议",不是"类" ,而 Swift 里的协议,本身就不支持"继承"(只能遵循);再加上 SwiftUI 里的 View 载体都是 Struct(值类型),值类型也不能继承(只有类是引用类型,能继承)。
苹果爸爸的心思其实很歪:"我就是要逼你们放弃'继承依赖',值类型+协议的组合,线程安全又轻量,不香吗?" 香是香,但刚开始确实浑身不自在,就像习惯了用筷子吃饭,突然让你用叉子,怎么都觉得别扭。
重点来了:SwiftUI 没有继承,怎么实现"复用+扩展"?
别慌,SwiftUI 虽然堵死了"继承"这一条路,但开了 N 条"后门",每一条都比继承更灵活(就是得适应适应),咱们一条条唠,结合吐槽讲明白。
方案1:协议扩展 ------ 给所有 View 发"通用福利"(最省心)
UIKit 里 BaseVC 的"全局统一样式",在 SwiftUI 里用「协议扩展」就能实现,相当于给所有遵循 View 协议的"打工人",统一发福利,不用一个个单独给。
举个栗子:咱们想让所有按钮都有统一的圆角、背景色,不用每个按钮都写 .cornerRadius(8).background(Color.blue),直接给 View 写个协议扩展:
scss
// 自定义协议(可选,也可以直接扩展 View)
protocol CommonButtonStyle: View {}
// 给协议写扩展,实现统一样式(相当于 BaseVC 的统一配置)
extension CommonButtonStyle {
func commonButton() -> some View {
self
.cornerRadius(8) // 统一圆角
.background(Color.blue) // 统一背景色
.foregroundColor(.white) // 统一文字色
.padding(.horizontal, 16) // 统一水平内边距
.padding(.vertical, 8)
}
}
// 让所有 View 都能"领取"这个福利(遵循协议)
extension View: CommonButtonStyle {}
// 使用时,一句话搞定,比继承还简单!
Button("我是统一样式按钮") {
print("点击啦")
}
.commonButton() // 直接调用扩展方法
吐槽点:这种方式确实香,但是!只能加"通用样式/通用方法",不能加"个性化状态"------比如你想让某个子类按钮有个专属的加载动画,光靠协议扩展就不够了,得搭配其他方案。
优点:零耦合、全局可用,改一处,所有用到的地方都同步改,比 UIKit 继承还省心(不用维护 BaseView 子类)。
方案2:组合封装 ------ 把"重复 View"做成"乐高零件"(最常用)
UIKit 里,我们继承 BaseVC 是为了复用"导航栏、空白页"这些重复组件;而 SwiftUI 里,更推荐"组合优于继承"------把重复的 View 抽成一个独立的 Struct,用到的时候直接"拼"上去,就像搭乐高,想要哪个零件就放哪个,不用继承整个"底座"。
举个栗子:APP 所有页面都有统一的"标题栏"(左边返回按钮,中间标题),UIKit 里我们会在 BaseVC 里写好标题栏;SwiftUI 里,直接把标题栏做成一个独立 View:
scss
// 封装通用标题栏(相当于 BaseVC 里的标题栏逻辑)
struct CommonNavigationBar: View {
let title: String // 可配置标题(个性化参数)
let onBack: () -> Void // 可配置返回事件(个性化回调)
var body: some View {
HStack {
// 返回按钮
Button(action: onBack) {
Image(systemName: "chevron.left")
.foregroundColor(.black)
}
Spacer()
// 标题
Text(title)
.font(.title2)
.fontWeight(.bold)
Spacer()
// 占位(和返回按钮对称,美观)
Color.clear.frame(width: 24)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
}
// 页面使用:直接组合,不用继承,想改就改
struct HomeView: View {
var body: some View {
VStack {
// 组合标题栏,传入个性化参数
CommonNavigationBar(title: "首页") {
print("返回上一页")
}
Spacer()
Text("首页内容")
Spacer()
}
}
}
struct MineView: View {
var body: some View {
VStack {
// 同一个标题栏,换个标题和回调,就是自己的样式
CommonNavigationBar(title: "我的") {
print("返回首页")
}
Spacer()
Text("我的内容")
Spacer()
}
}
}
吐槽点:这种方式比继承更灵活,但如果重复组件太多(比如标题栏、加载框、空白页、错误页),每个页面都要手动"拼",确实有点繁琐------不过总比重复写代码强,而且可以自由组合,不想用某个零件就直接删掉,比继承的"捆绑销售"舒服多了。
优点:高度解耦,每个组件都是独立的,修改一个组件不会影响其他组件;可定制性强,传入不同参数就能实现不同效果,比 UIKit 继承的"重写方法"更简单。
方案3:Modifier 修饰器 ------ 给 View 贴"个性化标签"(最灵活)
如果说协议扩展是"全局统一福利",组合封装是"乐高零件",那 Modifier 就是"个性化贴纸"------可以给任意 View 贴不同的贴纸,实现不同的样式/功能,而且可以叠加使用,比继承的"重写"灵活一百倍。
其实 SwiftUI 自带的 .cornerRadius()、.background() 都是 Modifier,我们也可以自定义 Modifier,实现自己的"扩展逻辑",相当于给 View 加"专属技能"。
举个栗子:我们想给某些 View 加一个"加载中遮罩",UIKit 里可能要在 BaseVC 里写个 showLoading() 方法,子类调用;SwiftUI 里,自定义一个 Modifier 就行:
swift
// 自定义 Modifier:加载中遮罩
struct LoadingModifier: ViewModifier {
let isLoading: Bool // 控制是否显示(个性化参数)
func body(content: Content) -> some View {
content
.overlay {
if isLoading {
// 遮罩+加载动画
ZStack {
Color.black.opacity(0.3)
.ignoresSafeArea()
ProgressView("加载中...")
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.5))
.cornerRadius(8)
}
}
}
}
}
// 扩展 View,让所有 View 都能使用这个 Modifier
extension View {
func loading(isLoading: Bool) -> some View {
self.modifier(LoadingModifier(isLoading: isLoading))
}
}
// 使用时,任意 View 都能加加载遮罩,不用继承!
struct DetailView: View {
@State private var isLoading = true
var body: some View {
Text("详情页内容")
.loading(isLoading: isLoading) // 直接贴"加载贴纸"
.onAppear {
// 模拟加载完成
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
isLoading = false
}
}
}
}
吐槽点:Modifier 确实灵活,但写多了容易乱,比如一个 View 叠加了五六个 Modifier,可读性就变差了------不过比起 UIKit 里继承层层嵌套、重写方法混乱的问题,这点乱真不算啥。
优点:可叠加、可复用、可定制,任意 View 都能使用,不用受继承关系限制;而且 Modifier 是"无侵入"的,不会改变 View 本身的结构,比继承更安全。
方案4:@ViewBuilder ------ 封装"可变内容"的组合(进阶骚操作)
有时候,我们想封装一个"容器 View",里面的内容是可变的(比如 BaseVC 里的 contentView),这时候就可以用 @ViewBuilder,相当于给"乐高底座"留了个"自定义凹槽",想放什么内容就放什么内容,比继承更灵活。
举个栗子:封装一个"带标题栏+底部按钮"的容器 View,中间内容由子类(页面)自定义:
scss
// 封装容器 View,用 @ViewBuilder 接收可变内容
struct ContainerView<Content: View>: View {
let title: String
let bottomButtonTitle: String
let onBottomButtonClick: () -> Void
// 用 @ViewBuilder 接收自定义内容
@ViewBuilder let content: () -> Content
var body: some View {
VStack {
// 通用标题栏
CommonNavigationBar(title: title) {
print("返回")
}
// 自定义内容(页面自己的内容)
content()
.flexibleFrame(maxWidth: .infinity, maxHeight: .infinity)
// 通用底部按钮
Button(action: onBottomButtonClick) {
Text(bottomButtonTitle)
.commonButton() // 复用之前的协议扩展
}
.padding(.bottom, 16)
}
}
}
// 页面使用:传入自定义内容,不用继承
struct EditView: View {
var body: some View {
ContainerView(
title: "编辑页面",
bottomButtonTitle: "保存",
onBottomButtonClick: {
print("保存成功")
}
) {
// 自定义内容,想放什么就放什么
VStack(spacing: 20) {
TextField("请输入内容", text: .constant(""))
.padding()
.border(Color.gray)
Text("编辑页面的自定义内容")
}
.padding()
}
}
}
吐槽点:这个方案稍微有点进阶,刚开始写的时候容易搞混 @ViewBuilder 的用法,比如忘记加 () -> Content,Xcode 报错能让你怀疑人生------但一旦学会,封装复杂容器 View 简直爽到飞起,比 UIKit 里继承 BaseVC 再重写 contentView 简单多了。
最后总结:别再执念于继承了,SwiftUI 的"套路"更香!
其实 SwiftUI 不是"反继承",而是它的设计思路和 UIKit 完全不同:UIKit 是"面向类的继承",主打一个"一脉相承";SwiftUI 是"面向协议的组合",主打一个"灵活拼接"。
用一句话吐槽总结:
UIKit 里的继承,就像"继承家产",好处是省心,但容易被"家产"绑定,想改点东西还要顾及祖宗规矩;SwiftUI 里的扩展,就像"搭乐高",虽然每个零件都要自己拼,但想怎么搭就怎么搭,拆了重拼也不心疼,灵活到飞起!
最后给大家一个小建议:刚从 UIKit 转到 SwiftUI 时,别总想着"怎么继承",而是多想想"怎么组合、怎么封装"------用协议扩展做全局统一,用组合封装做重复组件,用 Modifier 做个性化扩展,用 @ViewBuilder 做灵活容器,慢慢你就会发现,SwiftUI 的扩展方式,比 UIKit 的继承香多了!