iTranslated by AI
[Tips] Whether to use enums or structs for known values
Introduction
In Swift, I sometimes give feedback during code reviews on how to represent known values, so I decided to write an article about it.
In this article, I will introduce the following three methods for handling known values:
- Implementation using
enum - Implementation using
struct - Implementation using
enum+struct
While this might be common knowledge for those experienced with Swift, I felt there are still a fair number of people who might not know it, so I'm sharing it as a small tip.
Sample Code
For this article, let's consider a settings screen like the one below.

Assume this settings screen has an Item corresponding to each cell and is written using SwiftUI's List as follows:
let itmes: [Items] = ...
List(item) { item in
// Create a cell using the item
}
There are three types of cells:
- Account
- Contact Us
- Terms of Service
Also, each cell consists of the following three values:
- Title
- Icon Image (SF Symbols name in this case)
- Description
Now, let's see how to define Item in such a case.
1. Implementation using enum
First, let's focus on the characteristic "there are three types of cells."
When we do that, we might think of code like this using an enum:
enum Item: Identifiable {
case account, report, term
var id: String {
switch self {
case .account: return "account"
case .report: return "report"
case .term: return "term"
}
}
var title: String {
switch self {
case .account: return "Account Settings"
case .report: return "Contact Us"
case .term: return "Terms of Service"
}
}
var iconName: String {
switch self {
case .account: return "person"
case .report: return "exclamationmark.bubble"
case .term: return "doc"
}
}
var description: String? {
switch self {
case .account: return nil
case .report: return "Report bugs/requests here"
case .term: return nil
}
}
}
It's not bad, but I'm a bit concerned about the number of switch statements.
For example, when you want to check what values are set for the account cell, you need to read through all the switch statements for each property, which is a bit tedious.
2. Implementation using struct
Next, let's try implementing it using a struct.
struct Item: Identifiable {
let id: String
let title: String
let iconName: String
let description: String?
}
extension Item {
static let account = Item(
id: "account",
title: "Account Settings",
iconName: "person",
description: nil
)
static let report = Item(
id: "report",
title: "Contact Us",
iconName: "exclamationmark.bubble",
description: "Report bugs/requests here"
)
static let term = Item(
id: "term",
title: "Terms of Service",
iconName: "doc",
description: nil
)
}
Defining these as static let is done to achieve a similar usage feel as an enum. This allows you to write code in a way that is relatively similar to an enum.
// You can write it like this,
let item: Item = .account
// Or like this
if item == .account {
// ...
}
So, what is the benefit of using a struct? As mentioned earlier, it is that when you want to check the values for account, you can see at a glance what values each property holds.
static let account = Item(
id: "account",
title: "Account Settings",
iconName: "person",
description: nil
)
By reading just this code, you can check the values of the account Item all at once. Considering that in the enum case you had to read through every switch statement to confirm the same thing, you likely feel that the readability has improved significantly.
Weaknesses of struct
Now, looking at the struct example above, you might think "structs are the best!", but the above method isn't perfect. It also has its drawbacks.
Specifically, "it cannot guarantee that all cases are covered in a switch statement."
While with an enum you could write:
switch item {
case .account: //...
case .report: //...
case .term: //...
}
With a struct, you need to write a default case:
switch item {
case .account: //...
case .report: //...
case .term: //...
default: // ← This becomes necessary
}
Of course, whether this matters depends on the case, but it's a weakness that cannot be ignored.
If you cannot accept this disadvantage, a hybrid method using both enum and struct can be considered.
3. Implementation using enum + struct
This is a method where Item is defined as an enum, and the values for each case are defined as a struct called Item.Model.
enum Item: Identifiable {
case account, report, term
struct Model {
let id: String
let title: String
let iconName: String
let description: String?
}
var id: String { model.id }
var model: Model {
switch self {
case .account: return .account
case .report: return .report
case .term: return .term
}
}
}
extension Item.Model {
static let account = Item.Model(
id: "account",
title: "Account Settings",
iconName: "person",
description: nil
)
static let report = Item.Model(
id: "report",
title: "Contact Us",
iconName: "exclamationmark.bubble",
description: "Report bugs/requests here"
)
static let term = Item.Model(
id: "term",
title: "Terms of Service",
iconName: "doc",
description: nil
)
}
By doing this, you can ensure that the switch statement covers all cases while centralizing related values in one place.
switch item {
case .account: //...
case .report: //...
case .term: //...
} // No default needed!
// Account settings can be seen at a glance!
static let account = Item.Model(
id: "account",
title: "Account Settings",
iconName: "person",
description: nil
)
Of course, you need to access each value via model, but I feel the benefits are worth the extra step.
// Need to go through model
let title = item.model.title
Which method should you use?
When I write code, I think as follows:
- Start with an
enumfor now. - If the number of
switchstatements within theenum's computed properties increases, consider switching to astruct. - If you need to guarantee that all cases are covered by
switchstatements, use theenum+structapproach.
Conclusion
In this article, I introduced methods for representing known values.
I believe which method you use depends on the case.
However, knowing these methods will provide you with good options when writing code.
That's all for this small tip.
(Addition) 4. Implementation using struct + enum
This is content shared by Omochi Metal on Twitter.
I see, I think this is a very good idea.
Using the previous examples, it would look like this.
struct Item: Identifiable {
let id: String
let kind: Kind // kind has been added here
let title: String
let iconName: String
let description: String?
enum Kind {
case account, report, term
}
}
extension Item {
static let account = Item(
id: "account",
kind: .account,
title: "Account Settings",
iconName: "person",
description: nil
)
static let report = Item(
id: "report",
kind: .report,
title: "Contact Us",
iconName: "exclamationmark.bubble",
description: "Report bugs/requests here"
)
static let term = Item(
id: "term",
kind: .term,
title: "Terms of Service",
iconName: "doc",
description: nil
)
}
The great thing about this idea is that accessing the model, which was necessary in "3. Implementation using enum + struct", is no longer required.
// No need to access via model!
let title = item.title
This looks very good!
One thing to be careful about is that you might need to ensure the definitions of kind and Item maintain a 1:1 correspondence.
Also, it is slightly concerning that adding a case to kind cannot be detected as a compilation error.
That said, these are somewhat contrived concerns, and overall, it is an idea with significant benefits.
If you are really concerned, you could always write unit tests.
I had not tried this before, so I would like to keep it as an option for my future development!
Discussion
Modelへのアクセスを不要にしたいのでしたら「3. enum + structで実装する方法 & KeyPathによるDynamic Member Lookup」で可能ですね。modelを隠蔽できると思います。
みたいな感じでしょうか。
そうですね、dynamicMemberLookupもひとつの手だと思います。
ただ、function呼び出しには使えなかったり、アクセスしてるプロパティを辿るのに若干手間がかかったり、メリットとデメリットだと今回の場合、若干デメリットが勝つかなと思います。
もちろんケースバイケースなので、modelへのアクセスが多すぎる場合などは検討してみてもいいですが、若干敷居は高い気がします。
「既知の値に対して」ということでしたので提案しました。より複雑な関数呼びだしの場合はstaticでのModelインスタンスの保持の形は使いにくい可能性があるので、enumでの表現は避けるかもしれません。もしくはModelでのメソッドの実装を以下のようにすればDynamic Member Lookup でitem.a()とすることはできます。
確かに、簡単な関数ならクロージャー形式として持たせるのはアリかもしれませんね。
なるほど、ありがとうございます。
dynamicMemberLookupも選択肢として良さそうですね!