Of course, Swift protocol extensions and default parameter values are great features. And they are always safe, aren’t they? Well, not really.

Overview

In this article, I’ll show you how to get hurt using protocol extensions and default parameter values together. If you don’t know well these two features, no worries, I’ll explain them, briefly, in the first two paragraphs—feel free to skip them if you know well the subjects.

After the explanation of these two features, I’ll introduce the threat step by step with some examples. In the end, I’ll provide some suggestions to remove it. Happy Reading.

Protocols can be extended to provide method and property implementations to conforming types. This allows you to define behavior on protocols themselves, rather than in each type’s individual conformance or in a global function.

We have a protocol APIRequestProtocol, which contains a method request and the members baseUrl and query. Then, we create two classes, UsersAPIRequest and GroupsAPIRequest, to get the users and groups data from an API request:

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

protocolAPIRequestProtocol{

varbaseUrl: String{get}

varquery: String{get}

funcrequest()->Any

}

classUsersAPIRequest: APIRequestProtocol{

varbaseUrl: String{

return"my_baseUrl"

}

varquery: String{

return"?get=users"

}

funcrequest()->Any{

leturl=baseUrl+query

// send api request to url

}

}

classGroupsAPIRequest: APIRequestProtocol{

varbaseUrl: String{

return"my_baseUrl"

}

varquery: String{

return"?get=groups"

}

funcrequest()->Any{

leturl=baseUrl+query

// send api request to url

}

}

You can notice that both classes have the same value for the member baseUrl and the same implementation for the method request. To get rid of this duplication of code, we can use Protocol Extensions:

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

protocolAPIRequestProtocol{

varbaseUrl: String{get}

varquery: String{get}

funcrequest()->Any

}

// Protocol Extensions

extensionAPIRequestProtocol{

varbaseUrl: String{

return"my_baseUrl"

}

funcrequest()->Any{

leturl=baseUrl+query

// send api request to url

}

}

classUsersAPIRequest: APIRequestProtocol{

varquery: String{

return"?get=users"

}

}

classGroupsAPIRequest: APIRequestProtocol{

varquery: String{

return"?get=groups"

}

}

After the refactor, both classes use the default implementation inside extension APIRequestProtocol.

In this way, if the compiler doesn’t find an APIRequestProtocol implementation inside UsersAPIRequest/GroupsAPIRequest, it will be able to use the implementation inside the protocol extension.

You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

We have a class View, which has a constructor, init, to set its background color. Then, we initialize 4 View objects using its constructor:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

classView{

private letbackgroundColor:UIColor

init(backgroundColor:UIColor){

self.backgroundColor=backgroundColor

}

}

letview1=View(backgroundColor:.clear)

letview2=View(backgroundColor:.clear)

letview3=View(backgroundColor:.yellow)

letview4=View(backgroundColor:.clear)

You can notice that, most of the time, we set a background color .clear. Instead of using every time .clear as argument, we can assign a default parameter to backgroundColor. In this way, we can omit it and leave its value implicit:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

classView{

private letbackgroundColor:UIColor

init(backgroundColor:UIColor=.clear){

self.backgroundColor=backgroundColor

}

}

letview1=View()

letview2=View()

letview3=View(backgroundColor:.yellow)

letview4=View()

Notes:

We can use the default parameter also in methods with several parameters:

Once created our APIRequestProtocol, and extended with the default implementation, we create a new class UsersAPIRequest which conforms to APIRequestProtocol:

1

2

3

4

classUsersAPIRequest: APIRequestProtocol{

}

This class doesn’t implement request, but uses the default implementation.

Now, we can call the method request:

1

2

3

letusersRequest=UsersAPIRequest()

usersRequest.request(query:"?get=users")

request uses the default parameter values for baseUrl and entriesLimit.

So far so good. Let’s introduce the threat:

Your boss introduces a new business logic. To achieve it, UsersAPIRequest cannot use the default implementation of the protocol extensions anymore, therefore we add a custom request implementation inside the class:

1

2

3

4

5

6

classUsersAPIRequest: APIRequestProtocol{

funcrequest(baseUrl:String,query:String,entriesLimit:Int?)->Any{

// custom fetch the data

}

}

This code works and is fine.

But, if we call this new method, we’ll have an unexpected behaviour:

1

2

3

letusersRequest=UsersAPIRequest()

usersRequest.request(query:"?get=users")

⚠️ We expect the compiler to call the method request inside UsersAPIRequest, instead, it calls the method inside the protocol extension.⚠️

This is the reason:

We have two methods request in our hierarchy: Rpe (Request of protocol extension) and Ruar (Request of user api request).

When we write usersRequest.request(query: "?get=users"), we ask the compiler to call Ruar with just a parameter query. It goes inside UsersAPIRequest to read the implementation, but, unfortunately, it doesn’t find a method request with just an explicit parameter query, since Ruar has 3 explicit parameter baseUrl, query, entriesLimit:

func request(baseUrl: String, query: String, entriesLimit: Int?)

Usually, when Swift doesn’t find the right parameters of a method, it shows a compile error error: missing argument. In this case, it doesn’t throw an error because we still have Rpe, which has just an explicit query parameter, and this is exactly what the compiler is looking for.

When we declare Ruar, we don’t override the protocol extension implementation. To do it, Ruar and Rpe should have a default value in the same parameters—the values can be different, doesn’t matter.

Notes:

You may have noticed that I added the default parameter values in the extension instead of in the protocol. Swift doesn’t allow default values in the protocol declaration, but just in its extension—like in our example–or in the classes which conform to the protocol:

Adding the parameters explicitly when we call the method

Refactoring the method

We can create a new struct, which contains the parameters of the method, and move the default values inside 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

26

structApiRequestConfig{

letbaseUrl="my_baseUrl"

letquery:String

letentriesLimit:Int?=nil

}

protocolAPIRequestProtocol{

funcrequest(config:ApiRequestConfig)->Any

}

extensionAPIRequestProtocol{

funcrequest(config:ApiRequestConfig)->Any{

// fetch the data

}

}

classUsersAPIRequest: APIRequestProtocol{

funcrequest(config:ApiRequestConfig)->Any{

// custom fetch the data

}

}

letconfig=ApiRequestConfig(query:"?get=users")

letusersRequest=UsersAPIRequest()

usersRequest.request(config:config)

This kind of refactoring was introduced by Martin Fowler in his book Refactoring.

Conclusion

I agree, it may be a silly threat, and it occurs because of the developer’s distraction. Nevertheless, it can happen, and you would waste a lot of time understanding what’s going on. Sometimes, the issues because of distraction are the most difficult to solve.

Share this:

Related

Marco Santarossa

Hi there, I'm Marco and I'm an Italian developer. I moved to London in 2016 to work at Sky as iOS developer.
I've been an iOS Developer since 2011 and I sometimes write embarrassing PHP/JS code.
I'm keen to learn new things and I spend most of my spare time learning as self-taught.
When I don't develop, I like watching MMA fights and cooking Italian food.