You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何以类多态方式构建展示不同数据模型的视图?

SwiftUI 多类型视图网格展示的优化实现

问题背景

时隔数年再次尝试SwiftUI,受OOP思路影响,在将UI构想映射到SwiftUI时遇到困难。需求是在主视图的垂直网格中,渲染展示一系列对应不同数据模型的视图。目前采用让模型生成自定义视图的方案,但对架构不满意,希望了解最新SwiftUI中更规范、更优的实现方式。

当前实现代码

1. 基础模型“抽象类”

import SwiftUI

class BaseModel: Identifiable, Equatable {
    static func == (lhs: BaseModel, rhs: BaseModel) -> Bool {
        lhs.id == rhs.id
    }
    
    var id : String
    var isSelected: Bool = false 
    init(_ id: String = "") {
        self.id = id
    }
    func toggleSelection() {
        self.isSelected = !self.isSelected
    }
    func view() -> any View {
        return EmptyView()
    }
}

2. 文本展示模型

import SwiftUI

class TextModel: BaseModel {
    let text: String
    override init(_ text: String) {
        self.text = text
        super.init(self.text.description)
    }
    override func view() -> any View {
        return Button(action: self.toggleSelection) {
            Text(self.text)
                .frame( maxWidth: .infinity,maxHeight: .infinity)
                .padding()
                .foregroundStyle(self.isSelected ? .white : .blue)
                .background(self.isSelected ? .blue : .white)
                .frame(width:.infinity)
                .cornerRadius(10)
                .overlay(
                        RoundedRectangle(cornerRadius: 10).stroke(Color(.blue), lineWidth: 2)
                    )
        }
    }
}

3. 图片展示模型

import SwiftUI

class ImageModel: BaseModel {
    let resource: String
    override init(_ resource: String) {
        self.resource = resource
        super.init(self.resource.description)
    }
    override func view() -> any View {
        return Button(action: self.toggleSelection) {
            Image(self.resource)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(maxWidth: .infinity, alignment: .center)
                .clipped()
                .padding()
                .foregroundStyle(self.isSelected ? .white : .blue)
                .background(self.isSelected ? .blue : .white)
                .frame(width:.infinity)
                .cornerRadius(10)
                .overlay(
                        RoundedRectangle(cornerRadius: 10).stroke(Color(.blue), lineWidth: 2)
                    )
        }
    }
}

4. 主视图

import SwiftUI

struct MainView: View {
    var models:[BaseModel] = [
        TextModel("ABCDEFG"),
        ImageModel("my_image")
    ]

    var body: some View {
        VStack {
                LazyVGrid(columns: [
                    GridItem(.flexible()), GridItem(.flexible())
                ]) {
                    ForEach(self.models) {model in
                        AnyView(model.view()
                            .frame(maxWidth: .infinity)).frame(maxWidth: .infinity)
                    }
                }
            }
        }.padding()
    }
}

优化方案

方案1:值类型+枚举(贴合SwiftUI声明式风格)

SwiftUI偏好值类型(struct)而非引用类型(class),用枚举统一管理不同数据类型,结合@ViewBuilder实现视图映射:

import SwiftUI

// 枚举统一管理所有网格项类型
enum GridItemContent: Identifiable {
    case text(String, isSelected: Bool)
    case image(String, isSelected: Bool)
    
    var id: String {
        switch self {
        case .text(let text, _): return text
        case .image(let resource, _): return resource
        }
    }
    
    // 切换选中状态
    mutating func toggleSelection() {
        switch self {
        case .text(let text, let isSelected): self = .text(text, isSelected: !isSelected)
        case .image(let resource, let isSelected): self = .image(resource, isSelected: !isSelected)
        }
    }
    
    // 生成对应视图
    @ViewBuilder
    func view() -> some View {
        Button(action: { var copy = self; copy.toggleSelection(); self = copy }) {
            switch self {
            case .text(let text, let isSelected):
                Text(text)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .padding()
                    .foregroundStyle(isSelected ? .white : .blue)
                    .background(isSelected ? .blue : .white)
                    .cornerRadius(10)
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 2))
            case .image(let resource, let isSelected):
                Image(resource)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(maxWidth: .infinity, alignment: .center)
                    .clipped()
                    .padding()
                    .foregroundStyle(isSelected ? .white : .blue)
                    .background(isSelected ? .blue : .white)
                    .cornerRadius(10)
                    .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 2))
            }
        }
    }
}

// 优化后主视图
struct OptimizedMainView: View {
    @State private var items: [GridItemContent] = [
        .text("ABCDEFG", isSelected: false),
        .image("my_image", isSelected: false)
    ]

    var body: some View {
        VStack {
            LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())]) {
                ForEach($items) { $item in
                    item.view()
                        .frame(maxWidth: .infinity)
                }
            }
        }
        .padding()
    }
}

方案2:协议+关联类型(支持灵活扩展)

如果需要后续新增更多视图类型,可采用协议定义数据与视图的关联规则:

import SwiftUI

// 协议定义网格项的核心能力
protocol GridItemModel: Identifiable {
    var isSelected: Bool { get set }
    mutating func toggleSelection()
    @ViewBuilder func view() -> some View
}

// 文本网格项实现
struct TextGridItem: GridItemModel {
    let id: String
    var isSelected: Bool
    let text: String
    
    init(text: String, isSelected: Bool = false) {
        self.id = text
        self.isSelected = isSelected
        self.text = text
    }
    
    mutating func toggleSelection() {
        isSelected.toggle()
    }
    
    @ViewBuilder func view() -> some View {
        Button(action: { toggleSelection() }) {
            Text(text)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .padding()
                .foregroundStyle(isSelected ? .white : .blue)
                .background(isSelected ? .blue : .white)
                .cornerRadius(10)
                .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 2))
        }
    }
}

// 图片网格项实现
struct ImageGridItem: GridItemModel {
    let id: String
    var isSelected: Bool
    let resource: String
    
    init(resource: String, isSelected: Bool = false) {
        self.id = resource
        self.isSelected = isSelected
        self.resource = resource
    }
    
    mutating func toggleSelection() {
        isSelected.toggle()
    }
    
    @ViewBuilder func view() -> some View {
        Button(action: { toggleSelection() }) {
            Image(resource)
                .resizable()
                .aspectRatio(contentMode: .fill)
                .frame(maxWidth: .infinity, alignment: .center)
                .clipped()
                .padding()
                .foregroundStyle(isSelected ? .white : .blue)
                .background(isSelected ? .blue : .white)
                .cornerRadius(10)
                .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.blue, lineWidth: 2))
        }
    }
}

// 协议式主视图
struct ProtocolBasedMainView: View {
    @State private var items: [any GridItemModel] = [
        TextGridItem(text: "ABCDEFG"),
        ImageGridItem(resource: "my_image")
    ]

    var body: some View {
        VStack {
            LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())]) {
                ForEach($items) { $item in
                    item.view()
                        .frame(maxWidth: .infinity)
                }
            }
        }
        .padding()
    }
}

方案核心优势

  1. 值类型优先:用struct替代class,贴合SwiftUI状态管理机制,避免引用类型带来的意外状态共享问题。
  2. 声明式风格:通过@ViewBuilder直接在数据定义中关联视图,代码逻辑更直观,符合SwiftUI设计理念。
  3. 类型安全:枚举方案在编译阶段即可确保所有数据类型都有对应视图实现;协议方案则支持灵活扩展新类型。
  4. 状态管理清晰:用@State/@Binding自动管理选中状态,SwiftUI会自动处理视图刷新,无需手动维护状态变更逻辑。

内容的提问来源于stack exchange,提问作者Ailete619

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 02:35:09