Swift Enum Properties
Introduction
Enumerations in Swift are more powerful than in many other programming languages. One of the features that makes Swift enums so versatile is their ability to have properties, including computed properties, methods, and initializers. This enables enums to go beyond simply being a collection of related values and instead function as full-fledged types with behavior and functionality.
In this tutorial, we'll explore how to add properties to Swift enums, how they work, and practical use cases that demonstrate their power in real-world applications.
What are Enum Properties?
Enum properties allow you to associate functionality with enumeration types. Unlike stored properties (which enums cannot have), computed properties in enums calculate values rather than storing them. Enums can have:
- Computed properties
- Methods
- Initializers
- Conform to protocols
These features make enums in Swift much more powerful than traditional C-style enumerations.
Computed Properties in Enums
Since enums can't have stored properties (because they represent a single value, not a container of values), we use computed properties to add functionality.
Basic Syntax
enum Direction {
case north, south, east, west
var description: String {
switch self {
case .north:
return "Heading North"
case .south:
return "Heading South"
case .east:
return "Heading East"
case .west:
return "Heading West"
}
}
}
let heading = Direction.north
print(heading.description) // Output: "Heading North"
In this example, the description
property computes a string value based on the enum case. When we access heading.description
, the property's getter runs the switch statement and returns the appropriate string.
Methods in Enums
You can also add methods to enums, which allows them to have behaviors.
enum Measurement {
case centimeters(Double)
case inches(Double)
func toCentimeters() -> Double {
switch self {
case .centimeters(let value):
return value
case .inches(let value):
return value * 2.54
}
}
func toInches() -> Double {
switch self {
case .centimeters(let value):
return value / 2.54
case .inches(let value):
return value
}
}
}
let length = Measurement.inches(10)
print(length.toCentimeters()) // Output: 25.4
print(length.toInches()) // Output: 10.0
let width = Measurement.centimeters(30)
print(width.toInches()) // Output: 11.811023622047244
In this example, the Measurement
enum has methods that convert between different units of measurement, making unit conversion intuitive and type-safe.
Mutating Methods
Since enums are value types, methods that need to modify self
must be marked as mutating
.
enum ToggleSwitch {
case on
case off
mutating func toggle() {
switch self {
case .on:
self = .off
case .off:
self = .on
}
}
}
var lightSwitch = ToggleSwitch.off
print("Light is \(lightSwitch)") // Output: Light is off
lightSwitch.toggle()
print("Light is \(lightSwitch)") // Output: Light is on
lightSwitch.toggle()
print("Light is \(lightSwitch)") // Output: Light is off
The toggle()
method changes the enum value from .on
to .off
or vice versa. Without the mutating
keyword, this code would not compile.
Static Properties and Methods
Enums can also have static properties and methods that apply to the enum type itself rather than instances.
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
static var allPlanets: [Planet] {
return [.mercury, .venus, .earth, .mars,
.jupiter, .saturn, .uranus, .neptune]
}
static func isPlanet(name: String) -> Bool {
switch name.lowercased() {
case "mercury", "venus", "earth", "mars",
"jupiter", "saturn", "uranus", "neptune":
return true
default:
return false
}
}
var distanceFromSun: Double {
switch self {
case .mercury: return 57.9
case .venus: return 108.2
case .earth: return 149.6
case .mars: return 227.9
case .jupiter: return 778.3
case .saturn: return 1427.0
case .uranus: return 2871.0
case .neptune: return 4497.1
}
}
}
print(Planet.allPlanets.count) // Output: 8
print(Planet.isPlanet(name: "Pluto")) // Output: false
print(Planet.earth.distanceFromSun) // Output: 149.6
In this example, allPlanets
is a static computed property that returns an array of all planet enum cases. The isPlanet(name:)
method is a static method that checks if a string represents a valid planet name.
Initializers in Enums
Enums can have initializers that create enum instances based on input parameters.
enum Direction {
case north, south, east, west
init?(degree: Int) {
switch degree {
case 0, 360:
self = .north
case 90:
self = .east
case 180:
self = .south
case 270:
self = .west
default:
return nil
}
}
}
if let direction = Direction(degree: 90) {
print("Heading: \(direction)") // Output: Heading: east
} else {
print("Invalid direction")
}
if let direction = Direction(degree: 45) {
print("Heading: \(direction)")
} else {
print("Invalid direction") // Output: Invalid direction
}
Here, the initializer is failable (marked with ?
), which means it can return nil
if the provided degree doesn't correspond to one of the cardinal directions.
Raw Values and Computed Properties
You can combine raw values with computed properties to create powerful enumerations.
enum HTTPStatus: Int {
case ok = 200
case created = 201
case accepted = 202
case badRequest = 400
case unauthorized = 401
case forbidden = 403
case notFound = 404
case serverError = 500
var isSuccess: Bool {
return self.rawValue < 300
}
var description: String {
switch self {
case .ok: return "OK"
case .created: return "Created"
case .accepted: return "Accepted"
case .badRequest: return "Bad Request"
case .unauthorized: return "Unauthorized"
case .forbidden: return "Forbidden"
case .notFound: return "Not Found"
case .serverError: return "Internal Server Error"
}
}
var responseMessage: String {
if isSuccess {
return "Success: \(description) (\(rawValue))"
} else {
return "Error: \(description) (\(rawValue))"
}
}
}
let status = HTTPStatus.ok
print(status.isSuccess) // Output: true
print(status.responseMessage) // Output: Success: OK (200)
let errorStatus = HTTPStatus.notFound
print(errorStatus.isSuccess) // Output: false
print(errorStatus.responseMessage) // Output: Error: Not Found (404)
This example shows how computed properties can derive values from the raw value and provide additional functionality to make the enum more usable.
Real-world Applications
Let's look at some practical examples of using enum properties in real-world scenarios.
Example 1: Currency Converter
enum Currency {
case usd(Double)
case eur(Double)
case gbp(Double)
case jpy(Double)
var usdValue: Double {
switch self {
case .usd(let amount):
return amount
case .eur(let amount):
return amount * 1.09 // Conversion rate as of 2023
case .gbp(let amount):
return amount * 1.25 // Conversion rate as of 2023
case .jpy(let amount):
return amount * 0.0067 // Conversion rate as of 2023
}
}
func convert(to currency: CurrencyType) -> Currency {
let usdAmount = self.usdValue
switch currency {
case .usd:
return .usd(usdAmount)
case .eur:
return .eur(usdAmount / 1.09)
case .gbp:
return .gbp(usdAmount / 1.25)
case .jpy:
return .jpy(usdAmount / 0.0067)
}
}
var formattedString: String {
switch self {
case .usd(let amount):
return "$\(String(format: "%.2f", amount))"
case .eur(let amount):
return "€\(String(format: "%.2f", amount))"
case .gbp(let amount):
return "£\(String(format: "%.2f", amount))"
case .jpy(let amount):
return "¥\(String(format: "%.0f", amount))"
}
}
enum CurrencyType {
case usd, eur, gbp, jpy
}
}
let euros = Currency.eur(100)
print(euros.formattedString) // Output: €100.00
let dollars = euros.convert(to: .usd)
print(dollars.formattedString) // Output: $109.00
let yen = euros.convert(to: .jpy)
print(yen.formattedString) // Output: ¥16269
This currency enum demonstrates how methods, computed properties, and nested enums can create a comprehensive domain model with useful functionality.
Example 2: Card Game
enum Card {
enum Suit: String, CaseIterable {
case hearts, diamonds, clubs, spades
var symbol: String {
switch self {
case .hearts: return "♥️"
case .diamonds: return "♦️"
case .clubs: return "♣️"
case .spades: return "♠️"
}
}
var color: String {
switch self {
case .hearts, .diamonds:
return "Red"
case .clubs, .spades:
return "Black"
}
}
}
enum Rank: Int, CaseIterable {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
var symbol: String {
switch self {
case .jack: return "J"
case .queen: return "Q"
case .king: return "K"
case .ace: return "A"
default: return "\(self.rawValue)"
}
}
}
case card(rank: Rank, suit: Suit)
var description: String {
switch self {
case .card(let rank, let suit):
return "\(rank.symbol)\(suit.symbol)"
}
}
var value: Int {
switch self {
case .card(let rank, _):
return rank.rawValue
}
}
static var fullDeck: [Card] {
var deck = [Card]()
for suit in Suit.allCases {
for rank in Rank.allCases {
deck.append(.card(rank: rank, suit: suit))
}
}
return deck
}
}
let myCard = Card.card(rank: .ace, suit: .spades)
print(myCard.description) // Output: A♠️
print(myCard.value) // Output: 14
print("Number of cards in a deck: \(Card.fullDeck.count)") // Output: 52
This card game example shows how nested enums with properties can create a rich domain model. The Suit
and Rank
nested enums have their own properties, while the main Card
enum combines them and adds additional functionality.
Summary
Swift enum properties significantly enhance the power and flexibility of enumerations:
- Computed properties allow enums to derive values based on cases
- Methods provide behavior related to the enum
- Mutating methods can change the enum value
- Static properties and methods apply to the enum type itself
- Initializers enable flexible enum instance creation
- Combining raw values with computed properties creates powerful API
By leveraging these features, Swift enums can be transformed from simple value types to comprehensive domain models that encapsulate both data and behavior. This makes your code more expressive, type-safe, and maintainable.
Exercises
-
Create a
TrafficLight
enum with casesred
,yellow
, andgreen
. Add a mutating methodadvance()
that changes the light to the next color in the cycle. -
Create a
Shape
enum with cases for different shapes (circle, square, triangle). Add computed properties to calculate area and perimeter for each shape. -
Implement a
DayOfWeek
enum with cases for each day. Add computed properties to check if it's a weekday or weekend, and methods to get the next and previous days. -
Create a
Temperature
enum with cases for Celsius and Fahrenheit. Add methods to convert between the two and a computed property to determine if it's freezing or not.
Further Resources
- Swift Documentation - Enumerations
- Apple Developer - Swift Standard Library
- Swift by Sundell - Enum-driven API Design
- Ray Wenderlich - Swift Enumerations Tutorial
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)