Swift Enum Advance | mobidevtalk, Learning through case study
On our previous blog post we had some talk, which covers the surface of enum
. Here we will dive a bit deeper for familiarizing ourself with some more advanced concepts of enum
. And Finally on the next case study blog post, tic tac toe through enum, we will wrap it up by using our gained enum
knowledge to implement a real life problem. So let's get started on our swift enum advance talk.
For a better experience we can alway go back to the original blog post on mobidevtalk.com.
Background
- Generic enum
- Error definition
- Why to avoid mutation on enum
- Recursive enum, when and how to use? Should be used? simplicity
- allCases
Generic enum
Generic itself is a big topic. We will cover generic on some future blog series. But here we can have a glimpse of Generic usage on enum
. So what is the generic concepts? Generic basically broader the boundary of types, ie String
Int
etc, So that a larger number of types can be target of some task. In other words we can use multiple types all together when we put those types under Generic. We can also shorter the boundary by applying some constraints. On the following code T
itself is generic, but we are setting a constraint CustomStringConvertible
.
enum Status<T: CustomStringConvertible>{
case success(T)
case failed
} extension Status{
var description: String{
switch self {
case .success(let text):
return text.description
case .failed:
return "Failed...."
}
}
} Status.success("Good to go").description /"Good to go" Status.success(5).description // "5"
Error definition through enum
On the Swift
ecosystem enum
are the best candidate for defining Error
. The Error
itself does not have any requirements. And this Error
instance can be thrown by Swift error handling system.
Say we have an enum
Mismatched
enum Mismatched : String{
case notFound
case undefined
case invalid
}
We can confirm the Error
protocol to make Mismatched
throwable.
extension Mismatched: Error{}
So our throwable function:
func find(text: String?, on sentence: String?) throws {
guard let text = text, text.count > 0 else {
throw Mismatched.undefined
} guard let sentence = sentence, sentence.count > 0 else {
throw Mismatched.invalid
} if !sentence.contains(text) {
throw Mismatched.notFound
}
}
As we can see, if the target text is not set, then we will throw an undefined
error. Similarly for the invalid sentence we will throw an invalid
error. And finally notFound
if not found.
do {
try find(text: nil, on: "Some input")
}
catch Mismatched.undefined {
Mismatched.undefined.rawValue
} //undefined do {
try find(text: "any", on: nil)
}
catch Mismatched.invalid {
Mismatched.invalid.rawValue
} //invalid
Now for those two input we will get the corresponding error.
enum
makes it much easier. 😊
For a far more details talk on Swift’s Error handling, we can always visit another swift enum advance talk, Error handling through try variance.
Enum mutation, code smelling!!!
The concept is very clear. enum
is a value-based instance and it is been chosen by us for its immutability property. Thats mean always there will be only a single entity of that enum
. If we need another instance of different value for that enum
it has to be created. The previous was will not be modified.That's it, full-stop.
If we saw something other that, an enum
is being modified, then we can be sure there is a code smelling.
value types, such as enum
, struct
or tuple
, are not used for mutation. If we need mutation then class
is the way to go.
Now, how will we find an enum
is being modified/mutated. Simple the instance method which mutate the enum
will be prefixed with mutate
keyword. Example coming through:
enum State{
case notConnected
case connecting
case disconnected
case connected(String)
indirect case currentState(State)
} extension State{
mutating func updateCurrentState(state: State) {
self = .currentState(state)
}
} var state = State.connecting state.updateCurrentState(state: .connected("Oh ya"))
We will talk about the indirect
just on the next section. As we can see the updateCurrentState(state: State)
modified the self
or the enum
instance.
We already talked about mutating of value types, have a look.
Recursive enum
The name suggest it all. Yes this type of enum
is recursive, so they can call themselves. We express the recursive enum
with the indirect
keyword, placed either in front of case
declaration or in front of enum
declaration.
indirect enum Buffer{
case data(String)
case append(Buffer, Buffer, Buffer) }
On the above we can see the append
case will take three of its sibling Buffer
case. Now we can write:
let mobi = Buffer.data("Mobile")
let dev = Buffer.data("development")
let talk = Buffer.data("talk")
let mobiDevTalk = Buffer.append(mobi, dev, talk)
The mobiDevTak
takes three Buffer
instance, mobi
, dev
and talk
. Hmm ok no deal. But what to do with that. Lets convert the Buffer
to String
.
extension Buffer{
func convert() -> String {
switch self {
case .data(let text):
return text
case .append(let appendedBuffer):
return appendedBuffer.0.convert() + " " + appendedBuffer.1.convert() + " " + appendedBuffer.2.convert()
}
}
}
As we can see for .data(let text)
case we are just returning the String
. But on the .append(let appendedBuffer)
we are recursively calling convert()
to get the full string. Just a note: Swift is smart enough to define appendedBuffer
as Tuple
😉.
Now we can get the full string for append(Buffer, Buffer, Buffer)
.
mobiDevTalk.convert() //"Mobile development talk"
On the above we saw how to use recursive enum. Now the most important question should we use recursive enum? Well it may add some complexity to our code, but gives us the ability encapsulate the similar concerns. So it depends on the situation.
allCases
of CaseIterable
We will found this feature of enum
very useful when we need to do some common operation over all the case
of that enum
and definitely on the TDD, Test Driven Development, time. This feature was added on Swift 4.2 with Xcode 10.0+. Prior to allCases
there was some weird ways to list all the cases
of an enum
.
As we can already understand, this feature will list all the cases
of an enum
. So an example please:
enum Direction{
case north
case south
case east
case west
} extension Direction: CaseIterable{} Direction.allCases //[north, south, east, west]
Pretty simple. Now why do we write the CaseIterable
on the extension of Direction
. It is kind of a good practice to have separated implementation for different protocol. So what will happen if there is another protocol? Probably another extension. "Separation of concerns" or "Single responsibility" fits well with this type of extension based implementation of different concern aka protocol
.
At the end
We talked about swift enum advance topic. But still a lot of things to talk about, they are not cover on this blog post, something like patterns matching needs separated blog series. And there are more. But we have to move on. On the next blog post we will go with the case study of Tic-tac-toe using enum
. See you around.
The code used for this blog post can be found on Github
Originally published at https://mobidevtalk.com on December 26, 2018.