In this tutorial we cover the following topics
A protocol is a group of related properties and methods that can be implemented by any class. With protocols we can define a one single API and use it (after implementation) in unrelated classes. This way we can represent horizontal relationships on top of an existing tree-like class hierarchy (see graph below). Todo (id=no todo id): image: protocols as horizontal relationship (both in Swift and in Objective-C
In this sense protocols are like Java interfaces.
A syntax of protocol is shown below.
1 2 3 |
protocol ProtocolName { } |
As we can see, there is no method to say that something must be implemented or may be implemented (like @required
and @optional
keyword in Objective-C. Saying the truth, we can do this but optional requirements are available so that we can write code that interoperates with Objective-C. This is not native Swift feature.
Here is the syntax for class ClassName
having a superclass SuperclassName
conforming to protocol ProtocolName1
as well as ProtocolName2
1 2 3 |
class ClassName: SuperclassName, ProtocolName1, ProtocolName2 { } |
A protocol can require any conforming type to provide an instance property or type property with a particular name and type. The protocol doesn’t specify whether the property should be a stored property or a computed property. The protocol also specifies whether each property must be gettable or gettable and settable.
Protocols can require specific instance methods and type methods to be implemented. These methods are written as part of the protocol’s definition in exactly the same way as for normal instance and type methods, but without curly braces or a method body. Variadic parameters are allowed, subject to the same rules as for normal methods. Default values, however, can’t be specified for method parameters within a protocol’s definition.
It’s sometimes necessary for a method to modify (or mutate) the instance it belongs to. In such a case, we place the mutating
keyword before a method’s func
keyword to indicate that the method is allowed to modify the instance it belongs to and any properties of that instance.
Protocols can require specific initializers to be implemented. We write these initializers as part of the protocol’s definition in exactly the same way as for normal initializers, but without curly braces or an initializer body. When implemented, a protocol initializer must be marked with the required
keyword.
1 2 3 4 5 6 7 8 9 10 |
protocol ProtocolName { var mustBeSettable: Int { get set } var mayBeSettable: Int { get } func methodName() -> Int mutating func iAmAllowedToModifyThisInstance(withValue value:Int) init(someParameter value: Int) } |
Let's see how it works in the following example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
protocol ProtocolForClass { var mustBeSettable: Int { get set } var mayBeSettable: Int { get } func methodName() -> Int func iAmAllowedToModifyThisInstance(withValue value:Int) init(someParameter value: Int) } protocol ProtocolForStructure { var mustBeSettable: Int { get set } var mayBeSettable: Int { get } func methodName() -> Int // We need mutating to work with structures and enumerations // We can use it also for classes mutating func iAmAllowedToModifyThisInstance(withValue value:Int) init(someParameter value: Int) } class ClassImplementingProtocol: ProtocolForClass { var mustBeSettable: Int = 0 var mayBeSettable: Int = 0 func methodName() -> Int { return mustBeSettable + mayBeSettable } func iAmAllowedToModifyThisInstance(withValue value: Int) { mustBeSettable = value mayBeSettable = value * 2 } required init(someParameter value: Int) { mustBeSettable = value mayBeSettable = value * 2 } } struct StructureImplementingProtocol: ProtocolForStructure { var mustBeSettable: Int = 0 var mayBeSettable: Int = 0 func methodName() -> Int { return mustBeSettable + mayBeSettable } mutating func iAmAllowedToModifyThisInstance(withValue value: Int) { mustBeSettable = value mayBeSettable = value * 2 } init(someParameter value: Int) { mustBeSettable = value mayBeSettable = value * 2 } } var c = ClassImplementingProtocol(someParameter: 2) print(c.methodName()) c.iAmAllowedToModifyThisInstance(withValue: 3) print(c.methodName()) var s = StructureImplementingProtocol(someParameter: 2) print(s.methodName()) s.iAmAllowedToModifyThisInstance(withValue: 3) print(s.methodName()) |
6
9
6
9
A protocol can inherit one or more other protocols and can add further requirements on top of the requirements it inherits. The syntax for protocol inheritance is similar to the syntax for class inheritance, but with the option to list multiple inherited protocols, separated by commas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
protocol ProtocolCommon { var mustBeSettable: Int { get set } var mayBeSettable: Int { get } func methodName() -> Int init(someParameter value: Int) } protocol ProtocolForClass2: ProtocolCommon { func iAmAllowedToModifyThisInstance(withValue value:Int) } protocol ProtocolForStructure2: ProtocolCommon { mutating func iAmAllowedToModifyThisInstance(withValue value:Int) } class ClassImplementingProtocol2: ProtocolForClass2 { var mustBeSettable: Int = 0 var mayBeSettable: Int = 0 func methodName() -> Int { return mustBeSettable + mayBeSettable } func iAmAllowedToModifyThisInstance(withValue value: Int) { mustBeSettable = value mayBeSettable = value * 2 } required init(someParameter value: Int) { mustBeSettable = value mayBeSettable = value * 2 } } struct StructureImplementingProtocol2: ProtocolForStructure2 { var mustBeSettable: Int = 0 var mayBeSettable: Int = 0 func methodName() -> Int { return mustBeSettable + mayBeSettable } mutating func iAmAllowedToModifyThisInstance(withValue value: Int) { mustBeSettable = value mayBeSettable = value * 2 } init(someParameter value: Int) { mustBeSettable = value mayBeSettable = value * 2 } } var c2 = ClassImplementingProtocol2(someParameter: 2) print(c2.methodName()) c2.iAmAllowedToModifyThisInstance(withValue: 3) print(c2.methodName()) var s2 = StructureImplementingProtocol2(someParameter: 2) print(s2.methodName()) s2.iAmAllowedToModifyThisInstance(withValue: 3) print(s2.methodName()) |
6
9
6
9
Although protocols don’t provide any real functionality themselves they are a fully-fledged type we can use in our code. In consequence, we can use protocols in many places where other types are allowed
- we can use protocols as a parameter type or return type in a function, method, or initializer;
- we can use protocols as the type of a constant, variable, or property;
- we can use protocols as the type of items in an array, dictionary, or other container.
We can use the is
and as
operators to check for protocol conformance, and to cast to a specific protocol. Doing this follows the same, well known, syntax as for type
- The
is
operator returnstrue
if an instance conforms to a protocol and returnsfalse
if it doesn’t. - The
as?
version of the downcast operator returns an optional value of the protocol’s type, and this value isnil
if the instance doesn’t conform to that protocol. - The
as!
version of the downcast operator forces the downcast to the protocol type and triggers a runtime error if the downcast doesn’t succeed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
protocol SimpleProtocol1 { var property1: Int { get } } protocol SimpleProtocol2 { var property2: Int { get } } class TestClass1: SimpleProtocol1 { var property1 = 1 } class TestClass2: SimpleProtocol2 { var property2 = 2 // We can use protocols as the type of a constant, variable, or property var property22: SimpleProtocol1? } class TestClass1_2: SimpleProtocol1, SimpleProtocol2 { var property1 = 1 var property2 = 2 } // We can use protocols as a parameter type or return type in a function. func someFunction(object obj: SimpleProtocol1) -> SimpleProtocol2 { let o = TestClass2(); o.property2 = obj.property1 o.property22 = obj return o } let o1 = TestClass1() let o2 = TestClass1_2() let x: Any = o1 if (x is SimpleProtocol1){ let o = someFunction(object: x as! SimpleProtocol1) print(o.property2) print((o as! TestClass2).property22?.property1 ?? "default") } // We can use protocols as the type of items in an array, dictionary, or other container. var arr = [SimpleProtocol1]() arr.append(o1) arr.append(o2) print(arr) |
1
1
[Classes_05.TestClass1, Classes_05.TestClass1_2]
It can be useful to require a type to conform to multiple protocols at the same time. We can combine multiple protocols into a single requirement with an inheritance defining new protocol types that has the combined requirements of all protocols. In Swift we have a mechanism, named protocol composition, doing this for us. Protocol compositions behave as if we defined a temporary local protocol that has the combined requirements of all protocols in the composition. Protocol compositions don’t define any new protocol types.
Protocol compositions have the form SomeProtocol & AnotherProtocol
where ampersands are used to separate multiple protocols.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
protocol ProtocolForClass3 { func iAmAllowedToModifyThisInstance(withValue value:Int) } protocol ProtocolForStructure3 { mutating func iAmAllowedToModifyThisInstance(withValue value:Int) } class ClassImplementingProtocol3: ProtocolCommon, ProtocolForClass3 { var mustBeSettable: Int = 0 var mayBeSettable: Int = 0 func methodName() -> Int { return mustBeSettable + mayBeSettable } func iAmAllowedToModifyThisInstance(withValue value: Int) { mustBeSettable = value mayBeSettable = value * 2 } required init(someParameter value: Int) { mustBeSettable = value mayBeSettable = value * 2 } } struct StructureImplementingProtocol3: ProtocolCommon, ProtocolForStructure3 { var mustBeSettable: Int = 0 var mayBeSettable: Int = 0 func methodName() -> Int { return mustBeSettable + mayBeSettable } mutating func iAmAllowedToModifyThisInstance(withValue value: Int) { mustBeSettable = value mayBeSettable = value * 2 } init(someParameter value: Int) { mustBeSettable = value mayBeSettable = value * 2 } } var c3 = ClassImplementingProtocol3(someParameter: 2) print(c3.methodName()) c3.iAmAllowedToModifyThisInstance(withValue: 3) print(c3.methodName()) var s3 = StructureImplementingProtocol3(someParameter: 2) print(s3.methodName()) s3.iAmAllowedToModifyThisInstance(withValue: 3) print(s3.methodName()) func acceptOnlyObjectConformingGivenCompositeProtocol(object obj: ProtocolCommon & ProtocolForClass3){ print(obj.methodName()) obj.iAmAllowedToModifyThisInstance(withValue: 3) print(obj.methodName()) } acceptOnlyObjectConformingGivenCompositeProtocol(object: c3) |
6
9
6
9
9
12
This is the essence of protocols. For more crazy details, please refer to The Swift Programming Language (Swift 4): Protocols
With an extensions we can add a new functionality (but we cannot override existing functionality) to an existing code: class, structure, enumeration, or protocol type. This includes the ability to extend types for which we do not have access to the original source code. Extensions are similar to categories in Objective-C, because they allows to extend types without original source code, and are similar to extensions in Objective-C, because they do not have names (in short, categories without name are extensions in Objective-C).
With extensions we can
- Case 1. Add computed instance properties and computed type properties.
- Case 2. Define instance methods and type method. Instance methods added with an extension can also modify (or mutate) the instance itself.
- Case 3. Provide new initializers. This enables us to extend other types to accept our own custom types as initializer parameters, or to provide additional initialization options that were not included as part of the type’s original implementation.
- Case 4. Define subscripts.
- Case 5: Define and use new nested types.
- Case 6: Make an existing type conform to a protocol.
- Case 7: Extend a protocol to provide implementations of its requirements or add additional functionality that conforming types can take advantage of. This allows us to define behavior on protocols themselves, rather than in each type’s individual conformance or in a global function. By creating an extension on the protocol, all conforming types automatically gain this extension implementation without any additional modification.
- Case 8: Add new subscripts to an existing type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
protocol ProtocolUsedToExtend { var referencePoint: Int { get set }; func toggle() } class ClassToExtend { var referencePoint = 0; var x = 0 } extension ClassToExtend: ProtocolUsedToExtend { // Case 6 // Case 1 var incBy1: Int { get { return x + 1 } } // Case 2 func changeValue(value: Int){ x = value } // Case 3 // Designated initializer cannot be declared in an extension of 'ClassToExtend'; // did you mean this to be a convenience initializer? convenience init(value: Int){ self.init() x = value } // Case 4 subscript(repetition: Int) -> String { let v = String(x) var value = "" for _ in 0..<repetition { value += v } return value } // Case 5 enum Sign { case negative, zero, positive } var sign: Sign { switch x { case referencePoint: return .zero case let v where v > referencePoint: return .positive default: return .negative } } // Case 6 func toggle() { x *= -1 } } extension ProtocolUsedToExtend { mutating func setReferencePoint(value: Int) { referencePoint = value } } var cte = ClassToExtend(value: 3) print(cte.x) print(cte.incBy1) cte.changeValue(value: 5) print(cte.x) print(cte[3]) print(cte.sign) cte.toggle() print(cte.x) print(cte.sign) cte.setReferencePoint(value: -6) print(cte.sign) |
3
4
5
555
positive
-5
negative
positive
Generics are a one of my favorite feature. With this we can write very flexible and reusable code without duplicating it only because we want it to work with different type(s).
Generic functions can work with any type. Any time we have a functionality common for different types we can write a universal (generic) code where instead of real type a placeholder type is used. In Swift a placeholder type, known as a type parameter, specify its name, and is written immediately after the function’s name, between a pair of matching angle brackets (such as
<T>
for T
type parameter). The generic version of the function uses a placeholder type name (T
, in our case) instead of an actual type name (such as Int
, or String
). The actual type to use in place of T
is determined every time a function is used.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func arrayReverse<T>(_ array: inout [T]){ let count = array.count var temp: T for i in 0..<count/2 { temp = array[i] array[i] = array[count - 1 - i] array[count - 1 - i] = temp } } var arrayInt = [1, 2, 3, 4, 5] print(arrayInt) arrayReverse(&arrayInt) print(arrayInt) var arrayString = ["one", "two", "three", "four", "five"] print(arrayString) arrayReverse(&arrayString) print(arrayString) |
[1, 2, 3, 4, 5]
[5, 4, 3, 2, 1]
["one", "two", "three", "four", "five"]
["five", "four", "three", "two", "one"]
Although generics functions can work with any type, it’s sometimes useful to enforce certain type constraints on the types that can be used. Type constraints specify that a type parameter must inherit from a specific class, or conform to a particular protocol or protocol composition. Type constraints concerns generic functions as well as generic types (see next subsection).
1 2 3 |
func functionName<T: className, U: protocolName>(argT: T, argU: U) { // function body goes here } |
Generic types are maybe even more often used than generic functions. The most basic examples are quite natural: basic data structures like arrays, dictionaries, stacks or queues.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
struct PriorityQueue<T> { struct Item<T> { var item: T var priority: Int } var items = [Item<T>]() mutating func push(_ item: T, withPriority priority: Int) { items.append(Item(item: item, priority: priority)) } mutating func pop() -> T? { guard items.count > 0 else { return nil } var bestPriorityIndex = 0; var bestPriorityValue = items[0].priority; for index in 1..<items.count { if(items[index].priority < bestPriorityValue){ bestPriorityIndex = index; bestPriorityValue = items[index].priority; } } //let removed = items.remove(at: bestPriorityIndex) //return removed.item return items.remove(at: bestPriorityIndex).item } } var pq = PriorityQueue<String>() pq.push("five", withPriority: 5) pq.push("two", withPriority: 2) pq.push("one", withPriority: 1) pq.push("four", withPriority: 4) pq.push("three", withPriority: 3) print(pq.pop() ?? "undefined") print(pq.pop() ?? "undefined") print(pq.pop() ?? "undefined") print(pq.pop() ?? "undefined") print(pq.pop() ?? "undefined") print(pq.pop() ?? "undefined") |
one
two
three
four
five
undefined
When extending a generic type, we don’t provide a type parameter list as part of the extension’s definition. Instead, the type parameter list from the original type definition is available within the body of the extension, and the original type parameter names are used to refer to the type parameters from the original definition.
Based on the previous code, let's add an extension to it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
extension PriorityQueue { var itemWithBestPriority: T? { guard items.count > 0 else { return nil } var bestPriorityIndex = 0; var bestPriorityValue = items[0].priority; for index in 1..<items.count { if(items[index].priority < bestPriorityValue){ bestPriorityIndex = index; bestPriorityValue = items[index].priority; } } return items[bestPriorityIndex].item } } pq.push("five", withPriority: 5) pq.push("two", withPriority: 2) pq.push("one", withPriority: 1) print(pq.itemWithBestPriority ?? "undefined") |
one
When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. This is a way we make generic protocols. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the
associatedtype
keyword.
1 2 3 4 |
protocol Resetable { associatedtype ItemType mutating func reset(toValue value: ItemType, withPriority priority: Int) } |
Now we can use the extended protocol either in nongeneric way
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
struct PriorityQueueString: Resetable { struct Item { var item: String var priority: Int } var items = [Item]() mutating func push(_ item: String, withPriority priority: Int) { items.append(Item(item: item, priority: priority)) } mutating func pop() -> String? { guard items.count > 0 else { return nil } var bestPriorityIndex = 0; var bestPriorityValue = items[0].priority; for index in 1..<items.count { if(items[index].priority < bestPriorityValue){ bestPriorityIndex = index; bestPriorityValue = items[index].priority; } } return items.remove(at: bestPriorityIndex).item } // BEGIN Protocol conformance part typealias ItemType = String mutating func reset(toValue value: String, withPriority priority: Int) { for index in 0..<items.count { if(items[index].priority == priority){ items[index].item = value } } } // END } var pqs = PriorityQueueString() pqs.push("one", withPriority: 1) pqs.push("two", withPriority: 2) pqs.push("one", withPriority: 1) pqs.reset(toValue: "none", withPriority: 1) print(pqs.pop() ?? "undefined") print(pqs.pop() ?? "undefined") print(pqs.pop() ?? "undefined") |
none
none
two
or in generic way
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
struct PriorityQueueConformingToProtocols<T>: Resetable { struct Item<T> { var item: T var priority: Int } var items = [Item<T>]() mutating func push(_ item: T, withPriority priority: Int) { items.append(Item(item: item, priority: priority)) } mutating func pop() -> T? { guard items.count > 0 else { return nil } var bestPriorityIndex = 0; var bestPriorityValue = items[0].priority; for index in 1..<items.count { if(items[index].priority < bestPriorityValue){ bestPriorityIndex = index; bestPriorityValue = items[index].priority; } } return items.remove(at: bestPriorityIndex).item } mutating func reset(toValue value: T, withPriority priority: Int) { for index in 0..<items.count { if(items[index].priority == priority){ items[index].item = value } } } } var pqp = PriorityQueueConformingToProtocols<String>() pqp.push("one", withPriority: 1) pqp.push("two", withPriority: 2) pqp.push("one", withPriority: 1) pqp.reset(toValue: "none", withPriority: 1) print(pqp.pop() ?? "undefined") print(pqp.pop() ?? "undefined") print(pqp.pop() ?? "undefined") |
none
none
two
Note, that in generic case we don't have to use typealias
keyword.
This is the essence of generics. As for protocols, for more crazy details, please refer to The Swift Programming Language (Swift 4): Generics