在日常开发中,我们经常需要为「状态」或「配置」建模。Swift 提供了两种最常见的值类型:
enum
(枚举)struct
(结构体)
它们都能表达"一组相关的数据",但设计理念完全不同。选错工具往往会让后续迭代寸步难行
核心区别速览
维度 | enum | struct |
---|---|---|
值集合 | 有限、封闭(编译期已知全部 case) | 开放、可扩展(运行时可任意实例化) |
穷举检查 | ✅ 编译器强制 switch 覆盖全部 case |
❌ 永远需要 default |
关联值 | ✅ 每个 case 可附带不同类型的数据 | ❌ 属性统一声明 |
可扩展性 | ❌ 新增 case 必须改源码 | ✅ 随时创建新实例 |
表达意图 | 描述多选一的状态 | 描述配置/样式的组合值 |
Enum:为"有限状态"而生
典型场景:LoadingState
swift
/// 业务模型
struct SomeModel { var title: String }
/// 1️⃣ 定义有限且互斥的加载状态
enum LoadingState {
case idle
case loading
case success(SomeModel) // 关联值
case failure(Error)
}
/// 2️⃣ 在 View 中穷举所有可能
struct ContentView: View {
@State var loadingState: LoadingState = .idle
var body: some View {
VStack {
Text("Details")
switch loadingState { // 编译器会检查是否全覆盖
case .idle:
Text("Ready to load")
case .loading:
ProgressView("Loading...")
case .success(let model):
Text("Success: \(model.title)")
case .failure(let error):
Text("Error: \(error.localizedDescription)")
.foregroundColor(.red)
}
}
.task { loadData() }
}
private func loadData() {
loadingState = .loading
do {
let model = try ... // 实际网络请求
loadingState = .success(model)
} catch {
loadingState = .failure(error)
}
}
}
把 Enum 本身变成 View(扩展)
swift
extension LoadingState: View {
@ViewBuilder
var body: some View {
switch self {
case .idle: Text("Ready to load")
case .loading: ProgressView("Loading...")
case .success(let m): Text("Success: \(m.title)")
case .failure(let e): Text("Error: \(e.localizedDescription)").foregroundColor(.red)
}
}
}
/// 使用:直接把 state 当 View 用
VStack {
Text("Details")
loadingState // <- 这里
}
个人建议:把
LoadingState
做成独立LoadingStateView(state:)
会更清晰
Struct:为"可扩展配置"而生
典型场景:MessageStyle
swift
/// 1️⃣ 定义一个开放的可配置样式
struct MessageStyle {
let backgroundColor: Color
let foregroundColor: Color
// 可继续添加字体、圆角、阴影......
}
/// 2️⃣ 预置常用样式
extension MessageStyle {
static let primary = MessageStyle(
backgroundColor: .blue,
foregroundColor: .white
)
static let secondary = MessageStyle(
backgroundColor: .gray,
foregroundColor: .primary
)
static let destructive = MessageStyle(
backgroundColor: .red,
foregroundColor: .white
)
}
/// 3️⃣ 扩展计算属性
extension MessageStyle {
var cornerRadius: Double { 10 }
}
/// 4️⃣ 注入到 Environment
extension EnvironmentValues {
@Entry var messageStyle = MessageStyle.primary
}
extension View {
func messageStyle(_ style: MessageStyle) -> some View {
environment(\.messageStyle, style)
}
}
/// 5️⃣ 消费样式的自定义 View
struct Message: View {
let title: String
@Environment(\.messageStyle) var style
var body: some View {
Text(title)
.foregroundColor(style.foregroundColor)
.background(
style.backgroundColor,
in: .rect(cornerRadius: style.cornerRadius)
)
}
}
/// 6️⃣ 使用:系统预设 + 自定义
struct MessageStyleExample: View {
var body: some View {
VStack(spacing: 16) {
Message(title: "Primary").messageStyle(.primary)
Message(title: "Secondary").messageStyle(.secondary)
Message(title: "Delete").messageStyle(.destructive)
// 完全自定义
Message(title: "Custom")
.messageStyle(
MessageStyle(
backgroundColor: .purple,
foregroundColor: .white
)
)
}
}
}
Struct 的"陷阱":无法穷举
swift
struct NetworkState: Equatable {
let connectionType: String
let speed: Double?
static let offline = NetworkState(connectionType:"Offline", speed:nil)
static let wifi = NetworkState(connectionType:"WiFi", speed:100)
static let cellular = NetworkState(connectionType:"Cellular", speed:25)
}
/// ❌ switch 必须写 default,否则编译不过
struct NetworkStatusView: View {
@State private var networkState = NetworkState.offline
var body: some View {
switch networkState {
case .offline: ...
case .wifi: ...
case .cellular: ...
default: // 无法避免
Label("Unknown", systemImage: "questionmark")
}
}
}
控制 Struct 的"开放性":私有构造器
若既想要 struct 的扩展能力,又想限制"只有我们定义的值合法",可把构造器设为 private
:
swift
struct Theme: Equatable {
let primary: Color
let secondary: Color
let accent: Color
private init(primary: Color, secondary: Color, accent: Color) {
self.primary = primary; self.secondary = secondary; self.accent = accent
}
static let light = Theme(primary: .black, secondary: .gray, accent: .blue)
static let dark = Theme(primary: .white, secondary: .gray, accent: .orange)
static let highContrast = Theme(primary: .black, secondary: .black, accent: .yellow)
}
注意:即使限制了构造器,
switch
仍需default
,因为编译器无法证明未来不会新增static let
。
混合使用:各取所长
enum 太"重"?嵌套 struct 解耦视图样式
swift
enum Theme {
case light, dark, highContrast
/// 把与 View 相关的属性打包成 struct,避免枚举到处 switch
struct ViewStyle {
let primaryColor: Color
let secondaryColor: Color
let accentColor: Color
init(primary: Color, secondary: Color, accent: Color) {
self.primaryColor = primary
self.secondaryColor = secondary
self.accentColor = accent
}
}
var viewStyle: ViewStyle {
switch self {
case .light: return .init(primary: .black, secondary: .gray, accent: .blue)
case .dark: return .init(primary: .white, secondary: .gray, accent: .orange)
case .highContrast: return .init(primary: .black, secondary: .black, accent: .yellow)
}
}
}
struct 太"散"?用 enum 归类
swift
struct NetworkState {
let name: String
let speed: Double?
enum ConnectionType { case none, slow, fast }
var type: ConnectionType {
guard let s = speed else { return .none }
return s > 25 ? .fast : .slow
}
var color: Color {
switch type {
case .none: .red
case .slow: .orange
case .fast: .green
}
}
}
选型决策树
-
状态是否有限且不会频繁新增?
✅ 用 enum(LoadingState、Result、AuthenticationStatus)
-
是否允许业务方随时自定义新值?
✅ 用 struct(MessageStyle、Theme、Padding)
-
是否需要关联不同类型数据?
✅ enum 的关联值是唯一选择
-
是否需要编译期穷举检查?
✅ 只有 enum 能做到
实战扩展场景
场景 | 推荐方案 | 理由 |
---|---|---|
表单校验状态 | enum ValidationState { case idle, validating, valid(String), invalid(String) } |
状态机清晰,穷举安全 |
暗黑模式/主题包 | struct Theme + 私有构造器 |
业务方可创建自定义主题,但受限于预设属性 |
网络层抽象 | enum HTTPMethod { case get, post(body: Data) } |
关联值表达能力好 |
富文本样式 | struct RichTextStyle |
可组合、无限扩展 |
Redux Store 中的 Action | enum AppAction |
穷举检查避免遗漏 reducer |
组件库 Token | struct DesignToken + 私有构造器 |
既保证一致性,又允许用户自定义 |
个人总结
- enum 像"单选题":选项在编译期钉死,换来极致安全。
- struct 像"填空题":运行时可随意构造,换来极致灵活。
真正决定使用场景的,不是语法能力,而是需求对"封闭"还是"开放"的偏好。
在大型项目里,两者往往并存:
- 用 enum 描述业务流程与状态机;
- 用 struct 暴露给业务方做主题/样式配置;
- 通过 嵌套 或 组合 消除各自短板。
记住一句话:"有限选 enum,无限选 struct;既要又要,就混用。"