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() } }
方案核心优势
- 值类型优先:用
struct替代class,贴合SwiftUI状态管理机制,避免引用类型带来的意外状态共享问题。 - 声明式风格:通过
@ViewBuilder直接在数据定义中关联视图,代码逻辑更直观,符合SwiftUI设计理念。 - 类型安全:枚举方案在编译阶段即可确保所有数据类型都有对应视图实现;协议方案则支持灵活扩展新类型。
- 状态管理清晰:用
@State/@Binding自动管理选中状态,SwiftUI会自动处理视图刷新,无需手动维护状态变更逻辑。
内容的提问来源于stack exchange,提问作者Ailete619
相关产品推荐
相关产品推荐

