Concepts and Features of Swift Properties – All You Need to Know

0
644
Swift Properties
Swift Properties

Properties are at the heart of any piece of software we write and encounter everyday as developers. Programming is all about processing and manipulating data to achieve different goals. This data is encapsulated in properties. It can represent a single piece of data or a complex data structure. In object oriented programming, properties also act as a pointer to the location where the data is stored.

Though the basic concept of properties and how they are used is mostly similar in all programming languages, there are differences in the features supported by each language. They are similar to variables, but with some added features that make them more powerful. Swift being a modern object oriented programming language, offers many property features which enhances the code structure and ease of programming. In this article we will discuss about various features supported by properties in Swift.

Types of Properties

Properties in Swift can be classified in two broad categories, stored properties and computed properties. As the name suggests, stored properties are used to hold some values and provide a way to access and set that value. Values can be instances of classes (objects) or structs. Stored properties can be further classified into variables and constants.

Variable properties can be reassigned values anytime after initialisation. However, properties defined as constant cannot be assigned new values after initialisation. Variables are declared with var keyword and constants are declared with let keyword. Default values can be assigned to the properties at the point of declaration.

class MyClass {
    var myStoredString: String = "Hello, World!"
}

let myInstance = MyClass()
print(myInstance.myStoredString) 
// prints "Hello, World!"

Lazy properties: Stored properties can be declared as lazy. This means the variable will not be initialised until this is used for the first time. Variables are declared as lazy when construction of the property is complex and time consuming and the probability of the property being used is less. Deferring initialisation of a property to a later stage can improve loading performance of an app to some extent. Please note if the chances of a property being used is very high then it is better to not declare the variable as lazy because a bit extra app loading time is better than subsequent slow operation of the app when the property is accessed.

class MyClass {
    lazy var myString: String = {
        // Some expensive computation
        return "Hello, World!"
    }()
}

let myInstance = MyClass()
print(myInstance.myString) 
// prints "Hello, World!"


Computed properties are used to return calculated or derived values based on other properties defined in the class/struct or some other mathematical formula. Computed properties can be handy for getting outputs for standard formula like perimeter of a circle based on its radius or momentum of an object based on its mass and velocity.

Computed properties provide a getter (through keyword get) to compute and return the value when the property is used in an expression. They can also provide an optional setter (through keyword set) which can be used to set value of other stored properties used to derive the computed property based on new value of the computed property passed in the argument to the set.

class MyClass {
    var myString: String = "Hello, World!"
    var myComputedString: String {
        get {
            return myString.uppercased()
        }
        set {
            myString = newValue.lowercased()
        }
    }
}

let myInstance = MyClass()
print(myInstance.myComputedString) 
// prints "HELLO, WORLD!"
myInstance.myComputedString = "goodbye"
print(myInstance.myString) 
// prints "goodbye"


Property Scope, Accessibility and Lifetime

Scope of a property determines the visibility of the property within a file, code block/section or from other files in the module and other modules. Scope and accessibility of a property is determined by the location where the property is defined along with the access modifier associated with the property. Access modifiers allow us to further control and restrict access of properties. Following access modifiers can be associated with a property:

  • open : Can be accessed from anywhere. ‘Open’ classes can be subclassed from outside the module.
  • private properties can only be accessed within the same class or struct that they are defined in.
  • fileprivate properties can only be accessed within the same file that they are defined in.
  • public properties can be accessed from anywhere, including other modules and files; but cannot be subclassed from outside the module.

By default, properties are internal, which means they can be accessed within the same module, but not from other modules.

class MyClass {
    private var myPrivateString = "I am private"
    fileprivate var myFilePrivateString = "I am fileprivate"
    public var myPublicString = "I am public"
    open var myOpenString = "I am open"
    internal var myInternalString = "I am internal"
}

class MySubclass: MyClass {
    override internal var myInternalString: String {
        get {
            return super.myInternalString + " but I can't override it"
        }
        set {
            super.myInternalString = newValue
        }
    }
}

let myInstance = MySubclass()
print(myInstance.myPrivateString) // Error: Property 'myPrivateString' is inaccessible due to 'private' protection level
print(myInstance.myFilePrivateString) // Error: Property 'myFilePrivateString' is inaccessible due to 'fileprivate' protection level
print(myInstance.myPublicString) // "I am public"
print(myInstance.myOpenString) // "I am open"
print(myInstance.myInternalString) // "I am internal but I can't override it"


In these examples, you can see that the private and fileprivate properties can only be accessed within the same class or file that they are defined in, respectively. The public property can be accessed from anywhere, and open, internal, fileprivate, and private properties have different accessibility level. Also, you can see that stored properties are created when an instance of a class or struct is created and lazy properties are created the first time they are accessed.

Scope of properties can be further categorised based on where they are declared and whether they are declared as ‘class’ property:

Global Properties

Scope: Global properties are declared outside any function, type or closure i.e. at the top level of the file. As the name suggests global variables can be accessed from anywhere in the same file where this is declared. Global variables can also be accessed from any other file within the same module.

Lifetime: Global properties live for the whole duration of the program execution.

Usage: Global properties are typically used for storing values that need to be accessed from multiple places within the program.

It’s worth noting that global properties are not recommended in most cases, as they can cause naming collisions and make it harder to understand the relationships between different parts of the program. Instead, it is usually better to use local properties within a type or function or to pass values between different parts of the program as arguments or return values.

Class/static Properties

Scope: Class level properties are associated to the class/type itself i.e. they are not linked to any instance of the class. Class properties are declared with static keyword. These variables can be accessed directly using the type without creating any instance of the type.

Lifetime: Static properties live as long as the class/type is alive. If the class/type is declared at the top level of the file then it lives as long as the app runs.

Usage: Generally constants are defined as static properties. Resources shared between the instances of a type can also be defined as static properties.

Here is an example of a static property in Swift:

class Counter {
    static var count = 0

    func increment() {
        Counter.count += 1
    }
}

let counter1 = Counter()
let counter2 = Counter()

counter1.increment()
print(Counter.count) // 1

counter2.increment()
print(Counter.count) // 2


In this example, the Counter class has a static property named count with an initial value of 0. When an instance of the Counter class calls the increment method, it increments the value of the count property, and the value of count is the same for all instances of the Counter class. The final output shows that the value of count is 2, which is the result of both counter1 and counter2 calling the increment method.

Static properties are useful for shared data that needs to be accessed by all instances of a type, without having to pass the data between instances. They can also be used to store information that is global to the type, rather than being associated with a specific instance.

Instance Properties

Scope: Instance properties are declared within a type but outside any function or closure declaration. As the name suggests, every instance of the class has its own copy of the property unlike class/static properties. Instance properties are primarily used to hold values that are used in the application processing.

Lifetime: Instance properties live as long as the instance exists in the application runtime. When an instance of the type/class is assigned to a property, it comes into existence. As long as the property is being referred within the application, it will live.

Local/function Properties

Scope: Local properties are declared within a function or closure declaration. These properties are only accessible within the function or closure declaration i.e. within the curly braces within which this is declared.

Lifetime: Local properties live as long as the program control is within the curly braces within which contains the property.

Property Association

Stored properties can be associated with objects and structs. Computed properties can be associated with objects, structs and enumerations.

How Properties are Assigned Values?

There are different ways of assigning values to a property. If the property has a default value then it is better to assign that default value in the property declaration. If there is no default value then the value can be assigned in the initialiser declaration. If the property is not optional then a value must be assigned either during declaration or in the initialiser.

If the property is non optional then value can be assigned in later part of the code when the property is being used for the first time. Values of constant properties cannot be reassigned.

When a property is declared, you can either explicitly provide the type of the property or the compiler can determine this automatically based on the assigned value to the property in the declaration. However, it is a good practice to explicitly specify the type as this makes the code more readable.

Property Observers

As the name suggests, property observers are notified when a value is assigned to the property. Two types of property observers can be defined, one is called just before a property value is set and another just after the value is set. We can define either one of them or both based on our requirement.

Property observer method willSet is called before the property is being set. When willSet method is called, Swift passes the value being set as newValue default parameter name. However, we can also use a separate parameter name.

Property observer method didSet is called after the property has been set. When didSet method is called, Swift passes the previous value of the property as oldValue parameter name and we can specify a different parameter name if needed.

class MyPropertyObserver {
    var myString: String = "Hello, World!" {
        willSet {
            print("About to change myString to \(newValue)")
        }
        didSet {
            print("myString has been changed from \(oldValue) to \(myString)")
        }
    }
}

let myInstance = MyPropertyObserver()
myInstance.myString = "Goodbye"
// prints "About to change myString to Goodbye"
// prints "myString has been changed from Hello, World! to Goodbye"


Property Wrappers

Property wrappers add additional capability and functionality to a type. They allow us to write the common management code for a property type in a single place which can be reused by declaring the property with the property wrapper annotation. We will see the syntax for declaring a property with property wrapper annotation below.

//Property wrapper for human age which restricts age to maximum of 120
@propertyWrapper
struct HumanAge{
    
    var age : Int
    let maxAge = 120
    
    var wrappedValue:Int{
        get{
            return age
        }
        set{
            if newValue>maxAge {
                age = maxAge
            }else{
                age = newValue
            }
        }
    }
    
    init(initAge: Int){
        self.age = initAge
    }
    
    init(){
        self.init(initAge: 1)
    }
    
}

struct HumanBeing{
    
    @HumanAge public var age: Int
    
    init(){
        self.age = 1
    }
}

var newton = HumanBeing()

newton.age = 100
print(newton.age)

//Prints 100

var einstein = HumanBeing()

einstein.age = 150
print(einstein.age)

//Prints 120


As you can see from the example above, @propertyWrapper annotation is used to define a struct called HumanAge which is used in HumanBeing struct to restrict maximum age to 120.

In-out Parameters

In-out parameters allow a function or method to modify a passed-in variable, rather than just reading its value. In-out parameters are denoted by the inout keyword. When a variable is passed as an in-out parameter, the function or method can modify the value of the variable and those changes will persist after the function or method call. Here’s an example:

func increment(number: inout Int) {
    number += 1
}

var myInt = 0
increment(number: &myInt)
print(myInt) // prints "1"


In this example, the increment function takes an inout parameter of type Int. The function increments the value of the parameter by 1. The & symbol is used to pass the address of the variable to the function, allowing it to modify the variable’s value directly.

It’s worth noting that in-out parameters cannot have default values and variadic parameters cannot be marked as inout. Also, in-out parameters cannot be used with immutable types such as constants and literals, only mutable types such as variables.

Here is another example where two variables are passed as in-out parameters and the function swaps their values:

func swapTwoNumbers(a: inout Int, b: inout Int) {
    let temp = a
    a = b
    b = temp
}

var num1 = 10
var num2 = 20
swapTwoNumbers(a: &num1, b: &num2)
print(num1) // prints 20
print(num2) // prints 10


In this example, the swapTwoNumbers function takes two inout parameters a and b of type Int. The function uses a temporary variable to swap the values of a and b, and the changes persist after the function call.

In-out parameters are a powerful feature in Swift and can be used to modify variables in-place, which can be useful in certain situations, such as when working with mutable data structures or when you need to pass a value by reference rather than by value.

Conclusion

In this article you have learned about almost all features and concepts of Swift properties. I hope this article works as a reference which you refer to when you need to refresh your understanding on Swift properties. If you have linked this article and would like me to write more similar articles, please let me know in the comments section.

LEAVE A REPLY

Please enter your comment!
Please enter your name here

This site uses Akismet to reduce spam. Learn how your comment data is processed.