SwiftUI View 继承扩展:别再执着于 UIKit 的“子承父业”啦!

做 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 的继承香多了!

相关推荐
东坡肘子3 天前
Xcode 迈入 Agent 时代 -- 肘子的 Swift 周报 #122
人工智能·swiftui·swift
文件夹__iOS5 天前
AsyncStream 进阶实战:SwiftUI 全局消息流极简实现
ios·swiftui·swift
CYpdpjRnUE8 天前
光伏电池PV建模及其基于Boost Buck电路的最大功率追踪MPPT算法研究及仿真效果探究
swiftui
初级代码游戏9 天前
iOS开发 SwiftUI 15:手势 拖动 缩放 旋转
ios·swiftui·swift
zhyongrui11 天前
SnipTrip 菜单 Liquid Glass 实现方案:结构、材质、交互与深浅色策略
ios·性能优化·swiftui·交互·开源软件·材质
zhyongrui11 天前
SnipTrip 不发烫的实现路径:局部刷新 + 合成缓存 + 峰值削减
ios·swiftui
初级代码游戏12 天前
iOS开发 SwiftUI 14:ScrollView 滚动视图
ios·swiftui·swift
初级代码游戏12 天前
iOS开发 SwitftUI 13:提示、弹窗、上下文菜单
ios·swiftui·swift·弹窗·消息框
zhyongrui12 天前
托盘删除手势与引导体验修复:滚动冲突、画布消失动画、气泡边框
ios·性能优化·swiftui·swift