Date: 2015-11-20 Tags: [APPVENTURE] Categories: [Swift Advanced] Permalink advanced-practical-enum-examples
The original link = appventure. Me / 2015/10/17 /… Translation = PMST + small pan proofreading = Shanks final draft = Shanks
Translator’s Note: As a dedicated and strict translation group, we have verified all the codes in this article and made the playground into two parts, with detailed annotations in the code. You can download the code at github, provided by PPT, another member of the translation team.
This article is a detailed and practical tutorial on enumerations in Swift. It covers almost all the facts about enumerations and explains how to use them.
Like the Switch statement, enumerations in Swift at first glance look more like an advanced version of enumerations in C, allowing you to define a type to represent a use case for common things. However, after in-depth excavation, with the special design concept behind Swift, compared with C language enumeration, it is more widely used in the actual scene. In particular, enumerations in Swift are a powerful tool for articulating the intent of your code.
In this article, we’ll first look at basic syntax and the possibilities of using enumerations, then walk you through how and when to use enumerations. Finally, we’ll take a look at how enumerations are used in the Swift standard library.
Before we begin, let’s define enumerations. We’ll come back to that later.
The type of an enumeration declaration is a finite set of possible states and can have added value. Enumerations can define any organized data hierarchically by nesting, methods, associated values, and pattern matching.
They are Diving In.
A brief overview of how to define and use enumerations.
Defining Basic enumeration types (Defining Basic Enums)
Imagine we’re developing a game where the player can move in four directions. So, the player’s movement is limited. Obviously, we can use enumerations to express this:
enum Movement{
case Left
case Right
case Top
case Bottom
}Copy the code
Next, you can use multiple pattern matching structures to get the Movement enumeration value, or do something specific:
Let aMovement = movement. Left // switch aMovement{case.Left: Print ("left") default:()} if case. left = aMovement{print("left")} if aMovement ==.Left {print("left") default:()} }Copy the code
In this case, we do not have to specify the actual name of the enum (case move. Left:print(“Left”)). Because the type checker can automatically do the type calculation for this. This is especially useful for UIKit and the intricate enumerations in AppKit.
Enum Values
Of course, you might want to assign a value to each case in an enum. This is useful, for example, when enumerations themselves are actually tied to something or something that needs to be expressed in a different type. In C, you can only assign integer values to enumerated cases, whereas Swift offers more flexibility.
Movement: Int {case Left = 0 case Right = 1 case Top = 2 case Bottom = 3} String { case Baratheon = "Ours is the Fury" case Greyjoy = "We Do Not Sow" case Martell = "Unbowed, Unbent, Unbroken" case Stark = "Winter is Coming" case Tully = "Family, Duty, // Float double or Honor" case Tyrell = "Growing Strong"} enum Constants: Double {case e = 2.71828 case φ = 1.61803398874 case λ = 1.30357}Copy the code
For strings and ints, you can even ignore assigning values to cases in enumerations, and the Swift compiler will work fine.
// Mercury = 1, Venus = 2, ... Neptune = 8 enum Planet: Int { case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune } // North = "North", ... Enum CompassPoint: String {case North, South, East, West}Copy the code
The following four associative value types are supported in the Swift enumeration:
- Integer (Integer)
- Float Point
- String (String)
- Boolean type (Boolean)
Therefore, you cannot assign values of type CGPoint to enumerations.
If you want to read the value of an enumeration, you can do so using the rawValue property:
let bestHouse = House.Stark
print(bestHouse.rawValue)
// prints "Winter is coming"Copy the code
However, in some cases, you may want to create an enum case from an existing RAW value. In this case, enumerations provide a specified constructor:
enum Movement: Int {case Left = 0 case Right = 1 case Top = 2 case Bottom = 3} // create a movement rightMovement = Movement(rawValue: 1)Copy the code
If you use the rawValue constructor, remember that it is a failable initializer. In other words, the constructor returns a value of an optional type, because sometimes the value passed in May not match either case. Such as the Movement (rawValue: 42).
This is a very useful feature if you want to render something or something in low-level C binary encoding to make it more readable. For example, look at the encoding of VNode Flags in the BSD Kqeue library:
enum VNodeFlags : UInt32 {
case Delete = 0x00000001
case Write = 0x00000002
case Extended = 0x00000004
case Attrib = 0x00000008
case Link = 0x00000010
case Rename = 0x00000020
case Revoke = 0x00000040
case None = 0x00000080
}Copy the code
This allows you to make your Delete or Write use-case declarations clear at a glance and pass the RAW value into the C function later if needed.
Nesting Enums
You can nest enUms if you have requirements for a specific subtype. This allows you to include other explicit information for the actual enum. In an RPG, for example, every character can have a weapon, so all characters can acquire the same set of weapons. Other instances in the game don’t have access to these weapons (ogres, for example, use only sticks).
enum Character {
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
enum Helmet {
case Wooden
case Iron
case Diamond
}
case Thief
case Warrior
case Knight
}Copy the code
You can now describe the item bars that the role is allowed to access through a hierarchy.
let character = Character.Thief
let weapon = Character.Weapon.Bow
let helmet = Character.Helmet.IronCopy the code
Containing Enums
Similarly, you can embed enumerations in structs or classes. Following the example above:
struct Character {
enum CharacterType {
case Thief
case Warrior
case Knight
}
enum Weapon {
case Bow
case Sword
case Lance
case Dagger
}
let type: CharacterType
let weapon: Weapon
}
let warrior = Character(type: .Warrior, weapon: .Sword)Copy the code
Again, this will help us keep relevant information in one place.
Associated Value
Correlation values are an excellent way to attach additional information to an enum case. For example, if you are developing a trading engine, there may be two different types of trading: buy and sell. In addition to this, each lot of the transaction should make clear the name of the stock and the number of trades:
Simple Examples
enum Trade {
case Buy
case Sell
}
func trade(tradeType: Trade, stock: String, amount: Int) {}Copy the code
Yet the value and number of shares are clearly subordinate to the transaction, making them ambiguous as independent parameters. You might already want to embed an enumeration into a struct, but associated values provide a cleaner solution:
enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
func trade(type: Trade) {}Copy the code
Pattern Matching (Pattern Mathching)
If you want to access these values, pattern matching comes to the rescue again:
let trade = Trade.Buy(stock: "APPL", amount: 500)
if case let Trade.Buy(stock, amount) = trade {
print("buy \(amount) of \(stock)")
}Copy the code
Label (Labels)
Declaration that associated values do not require additional labels:
enum Trade {
case Buy(String, Int)
case Sell(String, Int)
}Copy the code
If you do, you will need to label these tags whenever you create an enumeration use case.
Tuple as Arguments
More importantly,Swift internal information is actually a tuple, so you can do something like this:
let tp = (stock: "TSLA", amount: 100)
let trade = Trade.Sell(tp)
if case let Trade.Sell(stock, amount) = trade {
print("buy \(amount) of \(stock)")
}
// Prints: "buy 100 of TSLA"Copy the code
The syntax allows you to treat a tuple as a simple data structure that will be automatically converted to a high-level type, such as enum case. Imagine an application that lets the user configure the computer:
typealias Config = (RAM: Int, CPU: String, GPU: String) // Each of these takes a config and returns an updated config func selectRAM(_ config: Config) -> Config {return (RAM: 32, CPU: config.CPU, GPU: config.GPU)} func selectCPU(_ config: Config) -> Config {return (RAM: config.RAM, CPU: "3.2ghz ", GPU: config.gpu)} func selectGPU(_ Config: Config) -> Config {return (RAM: config.RAM, CPU: "3.2ghz ", GPU: "NVidia")} enum Desktop { case Cube(Config) case Tower(Config) case Rack(Config) } let aTower = Desktop.Tower(selectGPU(selectCPU(selectRAM((0, "", "") as Config))))Copy the code
Each step of the configuration is updated by submitting a tuple to an enum. If we take our cue from Functional Programming 2, it gets even better.
infix operator <^> { associativity left }
func <^>(a: Config, f: (Config) -> Config) -> Config {
return f(a)
}Copy the code
Finally, we can concatenate the different configuration steps. This is useful when there are many configuration steps.
let config = (0, "", "") <^> selectRAM <^> selectCPU <^> selectGPU
let aCube = Desktop.Cube(config)Copy the code
Use Case Examples
Associated values can be used in a number of ways. As the saying goes: a code is worth a thousand words, here are a few simple code examples in no particular order.
// Case with different values enum UserAction {case OpenURL(URL: NSURL) case SwitchProcess(processId: UInt32) Case Restart(time: NSDate? IntoCommandLine: Bool)} // Suppose you are implementing a powerful editor that allows multiple selections, // as Sublime Text: https://www.youtube.com/watch?v=i2SVJa2EGIw enum Selection { case None case Single(Range<Int>) case Parameter Description Multiple([Range<Int>])} // Map different ids. Enum Barcode {case UPCA(numberSystem: Int, manufacturer: Int, product: Int, check: Int) case QRCode(productCode: String)} // https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2 enum KqueueEvent { case UserEvent(identifier: UInt, fflags: [UInt32], data: Int) case ReadFD(fd: UInt, data: Int) case WriteFD(fd: UInt, data: Int) case VnodeFD(fd: UInt, fflags: [UInt32], data: Int) case ErrorEvent(code: UInt, message: String)} // Finally, all wearables in an RPG can be mapped using an enumeration, // It is possible to add weight and durability to an item // It is now possible to add a "diamond" attribute with just one line of code, Enum Wearable {enum Weight: Int {case Light = 1 case Mid = 4 case Heavy = 10} enum Armor: Int { case Light = 2 case Strong = 8 case Heavy = 20 } case Helmet(weight: Weight, armor: Armor) case Breastplate(weight: Weight, armor: Armor) case Shield(weight: Weight, armor: Armor) } let woodenHelmet = Wearable.Helmet(weight: .Light, armor: .Light)Copy the code
Methods and Properties
You can also define methods in enum like this:
enum Wearable {
enum Weight: Int {
case Light = 1
}
enum Armor: Int {
case Light = 2
}
case Helmet(weight: Weight, armor: Armor)
func attributes() -> (weight: Int, armor: Int) {
switch self {
case .Helmet(let w, let a): return (weight: w.rawValue * 2, armor: w.rawValue * 4)
}
}
}
let woodenHelmetProps = Wearable.Helmet(weight: .Light, armor: .Light).attributes()
print (woodenHelmetProps)
// prints "(2, 4)"Copy the code
Methods in enumerations are “generated” for each enum case. So if you want to execute certain code in certain situations, you need to branch or use switch statements to specify the correct code path.
enum Device {
case iPad, iPhone, AppleTV, AppleWatch
func introduced() -> String {
switch self {
case AppleTV: return "\(self) was introduced 2006"
case iPhone: return "\(self) was introduced 2007"
case iPad: return "\(self) was introduced 2010"
case AppleWatch: return "\(self) was introduced 2014"
}
}
}
print (Device.iPhone.introduced())
// prints: "iPhone was introduced 2007"Copy the code
Attributes (Properties)
Although adding a storage property to an enumeration is not allowed, you can still create computed properties. Of course, the content of the calculated property is based on enumeration values or enumeration associations.
enum Device {
case iPad, iPhone
var year: Int {
switch self {
case iPhone: return 2007
case iPad: return 2010
}
}
}Copy the code
Static Methods
You can also create static methods for enumerations. In other words, create an enumeration from a non-enumeration type. In this example, we need to consider that users sometimes miscall apple devices (for example, AppleWatch as iWatch) and need to return an appropriate name.
enum Device {
case AppleWatch
static func fromSlang(term: String) -> Device? {
if term == "iWatch" {
return .AppleWatch
}
return nil
}
}
print (Device.fromSlang("iWatch"))Copy the code
Mutating Methods
Methods can be declared mutating. This allows changing the case value of the hidden argument self by 3.
enum TriStateSwitch { case Off, Low, High mutating func next() { switch self { case Off: self = Low case Low: Self = High case High: self = Off}}} var ovenLight = tristateswitch.low ovenlight.next () // ovenLight = On ovenlight.next () // ovenLight now equals. OffCopy the code
Summary (To Recap)
At this point, we have an overview of the basic use cases for the enumeration syntax in Swift. Before moving on, let’s revisit the definition given at the beginning of this article and see if it’s any clearer now.
The type of an enumeration declaration is a finite set of possible states and can have added value. Enumerations can define any organized data hierarchically by nesting, methods, associated values, and pattern matching.
Now we have a clearer definition of this. Indeed, if we add associative values and nesting, enum looks like a closed, simplified struct. Compared with struct, the former has the advantage of being able to encode classification and hierarchy.
// Struct Example
struct Point { let x: Int, let y: Int }
struct Rect { let x: Int, let y: Int, let width: Int, let height: Int }
// Enum Example
enum GeometricEntity {
case Point(x: Int, y: Int)
case Rect(x: Int, y: Int, width: Int, height: Int)
}Copy the code
The addition of methods and static methods allows us to add functionality to enums, which means that 4 can be implemented without relying on additional functions.
// C-Like example
enum Trade {
case Buy
case Sell
}
func order(trade: Trade)
// Swift Enum example
enum Trade {
case Buy
case Sell
func order()
}Copy the code
Advanced Enum Usage
Protocol (separate Protocols)
I’ve already mentioned the similarities between structs and Enums. In addition to the ability to attach methods, Swift also allows you to use Protocols and Protocol Extensions in enumerations.
The Swift protocol defines an interface or type for other data structures to follow. Enum is certainly no exception. Let’s start with an example from the Swift standard library.
CustomStringConvertible is a custom formatted output type for printing purposes.
protocol CustomStringConvertible {
var description: String { get }
}Copy the code
The protocol has only one requirement, which is a String of read-only (getter) type (String). We can easily implement this protocol for enUms.
enum Trade: CustomStringConvertible {
case Buy, Sell
var description: String {
switch self {
case Buy: return "We're buying something"
case Sell: return "We're selling something"
}
}
}
let action = Trade.Buy
print("this action is \(action)")
// prints: this action is We're buying somethingCopy the code
Some protocol implementations may need to handle requirements accordingly based on internal state. For example, define a protocol for managing bank accounts.
protocol AccountCompatible {
var remainingFunds: Int { get }
mutating func addFunds(amount: Int) throws
mutating func removeFunds(amount: Int) throws
}Copy the code
You might simply implement the protocol with a struct, but enum is a more sensible approach considering the context of the application. But you can’t add a storage property to enum, like var remainingFuns:Int. So how would you construct it? The answer is very simple, and you can solve it perfectly using correlation values:
enum Account {
case Empty
case Funds(remaining: Int)
enum Error: ErrorType {
case Overdraft(amount: Int)
}
var remainingFunds: Int {
switch self {
case Empty: return 0
case Funds(let remaining): return remaining
}
}
}Copy the code
To keep our code clean, we can define the required protocol functions in enum’s ProtoCL extension:
extension Account: AccountCompatible {
mutating func addFunds(amount: Int) throws {
var newAmount = amount
if case let .Funds(remaining) = self {
newAmount += remaining
}
if newAmount < 0 {
throw Error.Overdraft(amount: -newAmount)
} else if newAmount == 0 {
self = .Empty
} else {
self = .Funds(remaining: newAmount)
}
}
mutating func removeFunds(amount: Int) throws {
try self.addFunds(amount * -1)
}
}
var account = Account.Funds(remaining: 20)
print("add: ", try? account.addFunds(10))
print ("remove 1: ", try? account.removeFunds(15))
print ("remove 2: ", try? account.removeFunds(55))
// prints:
// : add: Optional(())
// : remove 1: Optional(())
// : remove 2: nilCopy the code
As you can see, we implement all of the protocol requirements by storing values in enum cases. There’s a nice twist to this: you now only need a pattern match on the entire code base to test for empty account entries. You don’t care if the remaining money is zero.
We have also embedded an enumeration following the ErrorType protocol in the Accout so that we can use the Swift2.0 syntax for error handling. A more detailed use case tutorial is provided here.
Extension (Extensions)
As you saw earlier, enumerations can also be extended. The most obvious use case is to separate the case and method of enumerations so that reading your code can easily and quickly digest the enum content and then move on to the method definition:
enum Entities {
case Soldier(x: Int, y: Int)
case Tank(x: Int, y: Int)
case Player(x: Int, y: Int)
}Copy the code
Now, we extend the method for enum:
extension Entities {
mutating func move(dist: CGVector) {}
mutating func attack() {}
}Copy the code
You can also follow a specific protocol by writing an extension:
extension Entities: CustomStringConvertible {
var description: String {
switch self {
case let .Soldier(x, y): return "\(x), \(y)"
case let .Tank(x, y): return "\(x), \(y)"
case let .Player(x, y): return "\(x), \(y)"
}
}
}Copy the code
Enumerate Generic Enums
Enumerations also support generic parameter definitions. You can use them to fit associated values in enumerations. Take a simple example directly from the Swift standard library, the Optional type. You can use it in several ways: Optional chaining(?) ), if-let can optionally bind, guard let, or switch, but syntactically you can also use Optional:
let aValue = Optional<Int>.Some(5)
let noValue = Optional<Int>.None
if noValue == Optional.None { print("No value") }Copy the code
This is Optional’s most direct use case and doesn’t use any syntax sugar, but there’s no denying that the addition of syntax sugar to Swift makes your job easier. If you looked at the example code above, you might have guessed that the Optional internal implementation looks like 5:
// Simplified implementation of Swift's Optional
enum MyOptional<T> {
case Some(T)
case None
}Copy the code
What’s so special about this place? Note that the associated value of the enumeration takes the generic parameter T as its own type, so the optional type constructs any return value you want.
Enumerations can have more than one generic parameter. Take the familiar Either class, which is not part of the Swift standard library but is implemented in many open source libraries and other functional programming languages such as Haskell or F#. The idea is that rather than just returning a value or no value (nee Optional), you want to return either a success value or some feedback (such as an error value).
// The well-known either type is, of course, an enum that allows you to return either
// value one (say, a successful value) or value two (say an error) from a function
enum Either<T1, T2> {
case Left(T1)
case Right(T2)
}Copy the code
Finally, all type constraints that apply to classes and structs in Swift apply to enUms as well.
// Totally nonsensical example. A bag that is either full (has an array with contents)
// or empty.
enum Bag<T: SequenceType where T.Generator.Element==Equatable> {
case Empty
case Full(contents: T)
}Copy the code
A recursive/Indirect type
Indirect types are a new type to Swift 2.0. They allow the associated value of a case in an enumeration to be defined as an enumeration again. As an example, suppose we want to define a file system that represents files and the directories that contain them. If files and directories are defined as enumerated cases, the associated value of the directory case should also contain an array of files as its associated value. Because this is a recursive operation, the compiler needs a special preparation for it. The Swift document reads as follows:
Enumerations and cases can be marked as indrect, which means that their associated values are held indirectly, allowing us to define recursive data structures.
So, if we were to define an enumeration of FileNode, it would look something like this:
enum FileNode {
case File(name: String)
indirect case Folder(name: String, files: [FileNode])
}Copy the code
The indrect keyword here tells the compiler to handle the enumerated case indirectly. You can also use this keyword for an entire enumerated type. As an example, let’s define a binary tree:
indirect enum Tree<Element: Comparable> {
case Empty
case Node(Tree<Element>,Element,Tree<Element>)
}Copy the code
This is a powerful feature that allows us to define a data structure with complex associations in a very concise way.
Use a custom type as the value of the enumeration
If we ignore associative values, enumeration values can only be integers, floating-point, strings, and Booleans. If you want to support additional types, you can do so by implementing the StringLiteralConvertible protocol, which allows us to support custom types in enumerations by serializing and deserializing strings.
As an example, suppose we wanted to define an enumeration to hold screen sizes for different iOS devices:
enum Devices: CGSize {
case iPhone3GS = CGSize(width: 320, height: 480)
case iPhone5 = CGSize(width: 320, height: 568)
case iPhone6 = CGSize(width: 375, height: 667)
case iPhone6Plus = CGSize(width: 414, height: 736)
}Copy the code
However, this code does not compile. Since CGPoint is not a constant, it cannot be used to define enumeration values. We need to add an extension for the custom types we want to support that implements the StringLiteralConvertible protocol. This protocol requires us to implement three constructors, each of which takes a String argument, and we need to convert the String to the type we want (CGSize in this case).
extension CGSize: StringLiteralConvertible {
public init(stringLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
public init(extendedGraphemeClusterLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
public init(unicodeScalarLiteral value: String) {
let size = CGSizeFromString(value)
self.init(width: size.width, height: size.height)
}
}Copy the code
We are now ready to implement the enumeration we need, but there is a drawback: the initialized value must be written as a String, because that is the type our enumeration needs to accept (remember, we implement StringLiteralConvertible, so String can be converted to CGSize)
enum Devices: CGSize {
case iPhone3GS = "{320, 480}"
case iPhone5 = "{320, 568}"
case iPhone6 = "{375, 667}"
case iPhone6Plus = "{414, 736}"
}Copy the code
Finally, we are ready to use enumerations of type CGPoint. Note that it is the rawValue property that we need to access when retrieving the actual CGPoint value.
let a = Devices.iPhone5 let b = a.rawValue print("the phone size string is \(a), width is \(b.width), Height is \(b.eight)") // Prints: The phone size string is iPhone5, width is 320.0, height is 568.0Copy the code
Using the serialized form of a string makes it difficult to use enumerations of custom types, but it can also be handy in certain cases (compared to NSColor/UIColor). Not only that, we can use this method for our own defined types.
Compare the associated values of enumerations
In general, enumerations are easy to make equality judgments about. A simple enum T {case a, b} implement default support for equality judgment T.a == T.b, T.b! = T.a
However, once we added an association value to the enumeration, Swift had no way of correctly determining the equality of the two enumerations and needed to implement the == operator ourselves. It’s not that difficult:
enum Trade {
case Buy(stock: String, amount: Int)
case Sell(stock: String, amount: Int)
}
func ==(lhs: Trade, rhs: Trade) -> Bool {
switch (lhs, rhs) {
case let (.Buy(stock1, amount1), .Buy(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
case let (.Sell(stock1, amount1), .Sell(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
default: return false
}
}Copy the code
As we can see, we judge the two enumerated cases through the switch statement, and only judge their true association values if their cases match (such as Buy and Buy).
Custom constructor
We mentioned in the static methods section that they can be a convenient form of constructing enumerations from different data. As shown in the previous example, return the correct name for an Apple device name that publishers often misuse:
enum Device {
case AppleWatch
static func fromSlang(term: String) -> Device? {
if term == "iWatch" {
return .AppleWatch
}
return nil
}
}Copy the code
We can also use custom constructors instead of static methods. The biggest difference between enumerations and constructors of structures and classes is that the constructor of enumerations requires the implicit self attribute to be set to the correct case.
enum Device { case AppleWatch init? (term: String) { if term == "iWatch" { self = .AppleWatch } return nil } }Copy the code
In this example, we use the failable constructor. However, ordinary constructors can work just fine:
enum NumberCategory {
case Small
case Medium
case Big
case Huge
init(number n: Int) {
if n < 10000 { self = .Small }
else if n < 1000000 { self = .Medium }
else if n < 100000000 { self = .Big }
else { self = .Huge }
}
}
let aNumber = NumberCategory(number: 100)
print(aNumber)
// prints: "Small"Copy the code
Iterate over enumerated cases
One particularly frequently asked question is how to iterate over cases in an enumeration. Unfortunately, enumerations do not adhere to the SequenceType protocol, so there is no official way to iterate over them. Depending on the type of enumeration, iterating over it can also be simple or difficult. There is a great discussion post on StackOverflow. There are so many different situations discussed in this post that it would be one-sided to just pick a few here, and too many to list all of them.
Objective-c support
Integer based enumeration, such as enum Bit: Int {case Zero = 0; Case One = 1} can be bridged to Objective-C with the @objc flag. However, once you use a type other than integer (like String) or start using associated values, you can’t use these enumerations in Objective-C.
There is a hidden protocol called _ObjectiveCBridgeable that allows the specification to define appropriate methods so that Swift can correctly convert enumerations to Objective-C, but I’m guessing that protocol is hidden for a reason. In theory, however, this protocol still allows us to bridge enumerations (including actual enumerations) correctly into Objective-C.
However, we don’t have to use the method mentioned above. By adding two methods to the enumeration and defining an alternative type using @objc, we are free to convert the enumeration in a way that does not require proprietary protocols:
enum Trade { case Buy(stock: String, amount: Int) case Sell(stock: String, amount: // This type can also be defined in objective-C code @objc class OTrade: NSObject {var type: Int var stock: String var amount: Int init(type: Int, stock: String, amount: Int) { self.type = type self.stock = stock self.amount = amount } } extension Trade { func toObjc() -> OTrade { switch self { case let .Buy(stock, amount): return OTrade(type: 0, stock: stock, amount: amount) case let .Sell(stock, amount): return OTrade(type: 1, stock: stock, amount: amount) } } static func fromObjc(source: OTrade) -> Trade? { switch (source.type) { case 0: return Trade.Buy(stock: source.stock, amount: source.amount) case 1: return Trade.Sell(stock: source.stock, amount: source.amount) default: return nil } } }Copy the code
One disadvantage of this approach is that we need to map enumerations to NSObject base types in Objective-C (we can also use NSDictionary directly), but when we run into enumerations that really need to fetch associated values in Objective-C, This is a method that can be used.
The enumeration of the underlying
Erica Sadun has written a very naughty blog about enumeration underbelly, covering all aspects of enumeration underbelly. You should never use these things in production code, but it’s fun to learn. I’m going to mention just one of the posts here, but if you want to know more, please go to the original:
Enumerations are usually one byte long. […]. If you are really naive, you can certainly define an enumeration with hundreds or thousands of cases, in which case the enumeration may take up two bytes or more depending on the minimum number of bits required.
Enumeration in the Swift standard library
Before we move on to explore the different use cases of enumerations in a project, it might be tempting to take a look at how enumerations are used in the Swift standard library, so let’s do that for now.
The enumeration Bit has two values, One and Zero. It is used as the Index type in CollectionOfOne
.
FloatingPointClassification this enumeration defines a series of IEEE 754 possible categories, such as NegativeInfinity PositiveZero or SignalingNaN.
Mirror. AncestorRepresentation and Mirror. DisplayStyle both enumerations are used in the context of Swift reflection API.
I don’t need to talk about Optional
The Process enumeration contains the command-line arguments (process.argc, process.arguments) for the current Process. This is a rather interesting enumerated type because it was implemented as a structure in Swift 1.0.
Practical cases
We’ve seen a number of useful enumerated types in the previous sections. Optional, Either, FileNode and binary trees. However, there are many situations where using enumerations is preferable to using structures and classes. In general, if the problem can be decomposed into a finite number of different categories, then using enumerations should be the right choice. Even with only two cases, this is a perfect scenario for using enumerations, as shown by the Optional and Either types.
Here are a few examples of enumeration types in action to ignite your creativity.
Error handling
When it comes to the practical use of enumerations, of course, there’s error handling, which is new in Swift 2.0. A function marked as throwable can throw any type that complies with the ErrorType null protocol. As Swift’s official documentation states:
Swift’s enumeration is particularly useful for building a set of related error states to which additional information can be added by associating values.
As an example, let’s look at the popular JSON parsing framework Argo. When JSON parsing fails, it can be for two main reasons:
- The JSON data is missing some key required for the final model (for example, your model has one)
username
Property, but missing from JSON) - There is a type mismatch, for example
username
You need String, and JSON containsNSNull
6.
In addition, Argo provides custom errors for errors that are not included in the above two categories. Their ErrorType enumeration looks something like this:
enum DecodeError: ErrorType {
case TypeMismatch(expected: String, actual: String)
case MissingKey(String)
case Custom(String)
}Copy the code
All cases have an associated value that contains additional information about the error.
A more generic ErrorType for full HTTP/REST API error handling would look something like this:
enum APIError : ErrorType {
// Can't connect to the server (maybe offline?)
case ConnectionError(error: NSError)
// The server responded with a non 200 status code
case ServerError(statusCode: Int, error: NSError)
// We got no data (0 bytes) back from the server
case NoDataError
// The server response can't be converted from JSON to a Dictionary
case JSONSerializationError(error: ErrorType)
// The Argo decoding Failed
case JSONMappingError(converstionError: DecodeError)
}Copy the code
ErrorType this ErrorType implements a complete REST stack resolution error, including all errors that occur when parsing structures and classes.
If you look closely, in JSONMappingError, we’ve encapsulated the DecodeError in Argo into our APIError type, because we’ll be using Argo for the actual JSON parsing.
More examples of ErrorType and this enumerated type can be found in the official documentation.
Observer model
In Swift, there are many ways to build observation patterns. If we use the @objC compatibility flag, we can use NSNotificationCenter or KVO. Even without this tag, didSet syntax can easily implement simple observation patterns. Enumerations can be used here to make the observed changes clearer. Imagine that we want to observe a set. If we think about it for a moment, we’ll see that there are only a few possible scenarios: one or more items are inserted, one or more items are deleted, and one or more items are updated. This sounds like what enumerations can do:
enum Change {
case Insertion(items: [Item])
case Deletion(items: [Item])
case Update(items: [Item])
}Copy the code
After that, the observer can use a very concise way to get detailed information about what has happened. This can also be extended by simply adding oldValue and newValue to it.
Status code
If we are using an external system that uses status codes (or error codes) to transmit error information, like HTTP status codes, enumerations are an obvious and good way to encapsulate information.
enum HttpError: String {
case Code400 = "Bad Request"
case Code401 = "Unauthorized"
case Code402 = "Payment Required"
case Code403 = "Forbidden"
case Code404 = "Not Found"
}Copy the code
Map Result Types
Enumerations are also often used to map JSON parsed results to Swift’s native types. Here’s a quick example:
enum JSON {
case JSONString(Swift.String)
case JSONNumber(Double)
case JSONObject([String : JSONValue])
case JSONArray([JSONValue])
case JSONBool(Bool)
case JSONNull
}Copy the code
Similarly, if we parse something else, we can use this method to convert the parse result to our Swift type.
UIKit logo
Enumerations can be used to map reuse identifiers of string types or storyboard identifiers to types that can be checked by the type system. Suppose we have a UITableView with many prototype cells:
enum CellType: String {
case ButtonValueCell = "ButtonValueCell"
case UnitEditCell = "UnitEditCell"
case LabelCell = "LabelCell"
case ResultLabelCell = "ResultLabelCell"
}Copy the code
unit
Units and unit conversions are another great place to use enumerations. You can map the units and their corresponding conversion rates, and then add methods to automatically convert the units. Here’s a fairly simple example:
Enum Liquid: Float {case ml = 1.0 case L = 1000.0 func convert(amount amount: Float, to: Liquid) -> Float { if self.rawValue < to.rawValue { return (self.rawValue / to.rawValue) * amount } else { return (self.rawValue * to.rawValue) * amount } } } // Convert liters to milliliters print (Liquid.l.convert(amount: 5, to: Liquid.ml))Copy the code
Another example is the conversion of money. And mathematical symbols such as angles and radians.
The game
Games are another pretty good use case for enumerations, as most of the entities on the screen belong to a specific race type (enemies, obstacles, textures…). . A game is more like a whiteboard than a native iOS or Mac app. To develop games we can create a whole new world with new objects and new associations, and iOS or OSX needs to use predefined UIButtons, UITableViews, UITableViewCells or NSStackView.
Not only that, but because enumerations are protocol compliant, we can leverage protocol extensions and protocol-based programming to add functionality to different enumerations defined for games. Here is a short example to illustrate this hierarchy:
enum FlyingBeast { case Dragon, Hippogriff, Gargoyle }
enum Horde { case Ork, Troll }
enum Player { case Mage, Warrior, Barbarian }
enum NPC { case Vendor, Blacksmith }
enum Element { case Tree, Fence, Stone }
protocol Hurtable {}
protocol Killable {}
protocol Flying {}
protocol Attacking {}
protocol Obstacle {}
extension FlyingBeast: Hurtable, Killable, Flying, Attacking {}
extension Horde: Hurtable, Killable, Attacking {}
extension Player: Hurtable, Obstacle {}
extension NPC: Hurtable {}
extension Element: Obstacle {}Copy the code
String typing
In a slightly larger Xcode project, we’ll soon have a whole bunch of resources accessed by strings. In the previous section, we talked about reuse logos and storyboard logos, but there are many other resources: images, Segues, Nibs, fonts, and more. Typically, these resources can be grouped into different collections. If so, a typed string would be a good way to get the compiler to do type checking for us.
enum DetailViewImages: String {
case Background = "bg1.png"
case Sidebar = "sbg.png"
case ActionButton1 = "btn1_1.png"
case ActionButton2 = "btn2_1.png"
}Copy the code
For iOS developers, r. swift, a third-party library, can automatically generate constructs for the above mentioned situations. But there are times when you may need to have more control (or you may be a Mac Developer 8).
API endpoint
The Rest API is an excellent use case for enumerations. They are grouped, they are a limited set of apis, and they may also have additional queries or named parameters, which can be implemented using associated values.
Here’s a simplified version of the Instagram API:
enum Instagram {
enum Media {
case Popular
case Shortcode(id: String)
case Search(lat: Float, min_timestamp: Int, lng: Float, max_timestamp: Int, distance: Int)
}
enum Users {
case User(id: String)
case Feed
case Recent(id: String)
}
}Copy the code
Ash Furrow’s Moya framework is based on this idea, using enumerations to map REST endpoints.
The list
Airspeed Velocity has an excellent article that shows how to use enumerations to implement a linked list. Most of the code in that article goes beyond enumerations and covers a host of other interesting topics 9, but the most basic definition of a linked list looks something like this (I’ve simplified it a bit) :
enum List {
case End
indirect case Node(Int, next: List)
}Copy the code
Each Node case points to the next case, and by using enumerations instead of other types, we can avoid using an optional Next type to indicate the end of the list.
Airspeed Velocity has also written a great blog on how to implement red-black trees using Swift’s indirect enumeration types, so if you’ve already read the blog on linked lists, you might want to continue reading this blog on red-black trees.
Setting the dictionary
This is a very, very clever solution by Erica Sadun. Simply put, any time we need a dictionary of attributes to set an item, we should use an enumeration of associated values instead. Using this method, the type checking system ensures that the configured values are of the correct type.
For more details, and appropriate examples, read her post.
limited
Like before, I’ll end this article with a list of limitations of enumerations.
Extracting associated values
David Owens has written an article about how the current method of extracting relational values is unwieldy. I recommend that you read his text, where I explain the gist: in order to get an association value from an enumeration, we must use pattern matching. However, an associative value is an efficient tuple associated in a particular enumerated case. Tuples, on the other hand, can be retrieved in simpler ways:.keyword or.0.
// Enums
enum Ex { case Mode(ab: Int, cd: Int) }
if case Ex.Mode(let ab, let cd) = Ex.Mode(ab: 4, cd: 5) {
print(ab)
}
// vs tuples:
let tp = (ab: 4, cd: 5)
print(tp.ab)Copy the code
If you think we should use the same approach to deconstruct enumerations, here is a rDAR: Rdar ://22704262 rDAR ://22704262 rDAR ://22704262
equality
Enumerations with associated values do not comply with the Equatable protocol. This is a shame because it adds unnecessary complexity and trouble to a lot of things. The underlying reason may be that the underlying use of associated values uses tuples that do not comply with the Equatable protocol. However, for a qualified subset of cases, I think the compiler should default to generating an Equatable extension for those associated values whose types all conform to the Equatable type.
} // Swift should be able to automatically generate this function func == (LHS: Ex.Mode, rhs: Ex.Mode) -> Bool { switch (lhs, rhs) { case (.Mode(let a, let b), .Mode(let c, let d)): return a == c && b == d default: return false } }Copy the code
Tuples (Tuples)
The biggest problem is support for tuples. I like using tuples, they make a lot of things easier, but they are currently undocumented and unavailable in many situations. In enumerations, we cannot use tuples as enumerations:
enum Devices: (intro: Int, name: String) {
case iPhone = (intro: 2007, name: "iPhone")
case AppleTV = (intro: 2006, name: "Apple TV")
case AppleWatch = (intro: 2014, name: "Apple Watch")
}Copy the code
This may not seem like the best example, but once we start using enumerations, we often end up in situations like the one above.
Iterate over all cases of the enumeration
We’ve already talked about this. There is currently no good way to get a collection of all the cases in the enumeration so that we can iterate over them.
Default associated value
Another thing we encounter is that the associated values of enumerations are always types, but we cannot specify default values for those types. Suppose we have a situation like this:
enum Characters {
case Mage(health: Int = 70, magic: Int = 100, strength: Int = 30)
case Warrior(health: Int = 100, magic: Int = 0, strength: Int = 100)
case Neophyte(health: Int = 50, magic: Int = 20, strength: Int = 80)
}Copy the code
We can still create new cases with different values, but the default Settings for the role will still be mapped.
change
10/26/2015
- Example of adding limitations (equality & Getting associated values)
- Added an example of Erica Sadun’s associative enumeration
10/22/2015
- Merge PR from # 6@mabidakun
- Add links at the bottom of the enumeration
- Break the account example into two more understandable pieces.
10/21/2015
- Merge from #4 @Blixt and #2 @Kandelvijayavolare and #3 @Sriniram and #5 @SixFivesoftware PR
- Add the calling code for the account example
- increase
ErrorType
The sample
explain
-
1. There are a few techniques you can use to achieve this goal, as described in the following article
-
2. The implementation of this example has been simplified for demonstration purposes. In real development, you would use optional types and arguments in reverse order. Take a look at popular functional programming libraries such as Swiftz and Dollar
-
3. This example directly adopts the example of Swift official documentation
-
4. Often making the locations they define difficult to find
-
5. This is a simplified version, but Swift, of course, adds a lot of grammatical sugar for us
-
6. If you’ve used JSON in your application, you’ve probably encountered this problem
-
7. By the way, you can’t just use numbers as names for enumerations, so 400 won’t do
-
8. That said, a Mac version of R.swift seems to be coming soon
-
Open the link and start reading the article