Swift - Protocol extensions - Property default values

IosSwiftProtocol Oriented

Ios Problem Overview


Let's say that I have the following protocol:

protocol Identifiable {
    var id: Int {get}
    var name: String {get}
}

And that I have the following structs:

struct A: Identifiable {
    var id: Int
    var name: String
}

struct B: Identifiable {
    var id: Int
    var name: String
}

As you can see, I had to 'conform' to the Identifiable protocol in struct A and struct B. But imagine if I had N more structs that needs to conform to this protocol... I don't want to 'copy/paste' the conformance (var id: Int, var name: String)

So I create a protocol extension:

extension Identifiable {
    var id: Int {
        return 0
    }
    
    var name: String {
        return "default"
    }
}

With this extension now I can create a struct that conforms to the Identifiable protocol without having to implement both properties:

struct C: Identifiable {

}

Now the problem is that I can't set a value to the id property or the name property:

var c: C = C()
c.id = 12 // Cannot assign to property: 'id' is a get-only property

This happens because in the Identifiable protocol, id and name are only gettable. Now if I change the id and name properties to {get set} I get the following error:

Type 'C' does not conform to protocol 'Identifiable'

This error happens because I haven't implemented a setter in the protocol extension... So I change the protocol extension:

extension Identifiable {
    var id: Int {
        get {
            return 0
        }
        
        set {
            
        }
    }
    
    var name: String {
        get {
            return "default"
        }
        
        set {
            
        }
    }
}

Now the error goes away but if I set a new value to id or name, it gets the default value (getter). Of course, the setter is empty.

My question is: What piece of code do I have to put inside the setter? Because if I add self.id = newValue it crashes (recursive).

Thanks in advance.

Ios Solutions


Solution 1 - Ios

It seems you want to add a stored property to a type via protocol extension. However this is not possible because with extensions you cannot add a stored property.

I can show you a couple of alternatives.

Subclassing (Object Oriented Programming)

The easiest way (as probably you already imagine) is using classes instead of structs.

class IdentifiableBase {
    var id = 0
    var name = "default"
}

class A: IdentifiableBase { }

let a = A()
a.name  = "test"
print(a.name) // test

>Cons: In this case your A class needs to inherit from IdentifiableBase and since in Swift theres is not multiple inheritance this will be the only class A will be able to inherit from.

Components (Protocol Oriented Programming)

This technique is pretty popular in game development

struct IdentifiableComponent {
    var id = 0
    var name = "default"
}

protocol HasIdentifiableComponent {
    var identifiableComponent: IdentifiableComponent { get set }
}

protocol Identifiable: HasIdentifiableComponent { }

extension Identifiable {
    var id: Int {
        get { return identifiableComponent.id }
        set { identifiableComponent.id = newValue }
    }
    var name: String {
        get { return identifiableComponent.name }
        set { identifiableComponent.name = newValue }
    }
}

Now you can make your type conform to Identifiable simply writing

struct A: Identifiable {
    var identifiableComponent = IdentifiableComponent()
}

Test

var a = A()
a.identifiableComponent.name = "test"
print(a.identifiableComponent.name) // test

Solution 2 - Ios

Objective-C Associated Objects

You can use Objective-C associated objects to basically add a stored property to a class or protocol. Note that associated objects only work for class objects.

import ObjectiveC.runtime

protocol Identifiable: class {
    var id: Int { get set }
    var name: String { get set }
}

var IdentifiableIdKey   = "kIdentifiableIdKey"
var IdentifiableNameKey = "kIdentifiableNameKey"

extension Identifiable {
    var id: Int {
        get { 
            return (objc_getAssociatedObject(self, &IdentifiableIdKey) as? Int) ?? 0
        }
        set {
            objc_setAssociatedObject(self, &IdentifiableIdKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }   
    }

    var name: String {
        get { 
            return (objc_getAssociatedObject(self, &IdentifiableNameKey) as? String) ?? "default"
        }
        set {
            objc_setAssociatedObject(self, &IdentifiableNameKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }   
    }
}

Now you can make your class conform to Identifiable by simply writing

class A: Identifiable {

}

Test

var a = A()
print(a.id)    // 0
print(a.name)  // default
a.id = 5
a.name = "changed"
print(a.id)    // 5
print(a.name)  // changed

Solution 3 - Ios

Protocols and protocol extensions are very powerful, but they tend to be most useful for read-only properties and functions.

for what you're trying to accomplish (stored properties with a default value), classes and inheritance might actually be the more elegant solution

something like:

class Identifiable {
    var id: Int = 0
    var name: String = "default"
}

class A:Identifiable {
}

class B:Identifiable {
}

let a = A()

print("\(a.id) \(a.name)")

a.id = 42
a.name = "foo"

print("\(a.id) \(a.name)")

Solution 4 - Ios

This is why you were not able to set the properties.

The property becomes a computed property which means it does not have a backing variable such as _x as it would in ObjC. In the solution code below you can see the xTimesTwo does not store anything, but simply computes the result from x.

See Official docs on computed properties.

The functionality you want might also be Property Observers.

Setters/getters are different than they were in Objective-C.

What you need is:

var x:Int

var xTimesTwo:Int {
    set {
       x = newValue / 2
    }
    get {
        return x * 2
    }
}

You can modify other properties within the setter/getters, which is what they are meant for

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAxortView Question on Stackoverflow
Solution 1 - IosLuca AngelettiView Answer on Stackoverflow
Solution 2 - IosDominicMDevView Answer on Stackoverflow
Solution 3 - IosNickView Answer on Stackoverflow
Solution 4 - IosHarshView Answer on Stackoverflow