AndiOSDev Foundation

Language and Architecture

Siamak (Ash) Ashrafi
30 min readMay 20, 2024

Article 1: Foundation

  • Languages & Features: Kotlin & Swift, including key features like functions, extensions, concurrency (coroutines/Swift structured concurrency).
  • Architecture Paradigms: Object-Oriented Programming (OOP) and Protocol-Oriented Programming (POP).
  • Package Management: Learn how to manage dependencies for each platform.

This is the first part of a three part series:

Languages & Features

A Shared Language Ideology: Kotlin and Swift

The worlds of Android and iOS development have traditionally been known for their distinct programming languages: Java for Android and Objective-C[rap]* for iOS. However, the emergence of Kotlin and Swift has ushered in a new era of shared ideology when it comes to mobile development. Let’s delve into the characteristics that make these languages feel more like cousins than rivals:

Modern Design Principles

Both Kotlin and Swift are relatively young languages compared to their predecessors (Java, Objective-C[rap]). This means they were designed with modern programming principles in mind, emphasizing features like:

  • Null Safety: Both languages enforce null safety, preventing the dreaded NullPointerException crashes that plagued older languages. This leads to more robust and predictable code.
  • Functional Programming Concepts: Both Kotlin and Swift incorporate functional programming concepts like immutability and lambda expressions. This promotes cleaner code with fewer side effects and improved testability.
  • Code Readability: Both languages prioritize code readability with features like concise syntax and type inference. This makes code easier to understand for both the developer writing it and others who need to maintain it.
  • Interoperability: While not directly interoperable at the bytecode level, Kotlin and Swift share a similar design philosophy that makes it easier for developers familiar with one language to pick up the other. The syntax for common operations often feels intuitive, reducing the learning curve when switching between platforms.
  • Focus on Developer Experience: Both Kotlin and Swift are designed with developer experience in mind. Features like built-in type safety, powerful standard libraries, and excellent integration with development tools (Android Studio, Xcode) contribute to a smoother and more productive development experience.
Kotlin features — looks like Swift similarity

It’s important to remember that Kotlin and Swift are still distinct languages with their own unique features and quirks. However, the shared emphasis on modern design principles, readability, and developer experience creates a sense of familiarity for programmers working across both Android and iOS ecosystems. This can be a significant advantage in today’s mobile development landscape, where the ability to work across platforms is becoming increasingly valuable.

*All C based languages (C/C++ and Objective-C[rap]) should be replaced. For Objective-C[rap] use Swift and for C/C++ is Rust is the way forward.

With Rust the “Good” Languages of the world

Table Kotlin & Swift

Table of Language Features

Language Features

Here we will review the language features and how they lead to:

  • Kotlin — Object Oriented Programming
  • Swift — Protocol Oriented Programming

In both Kotlin / Swift have classes and Swift has Structs (stored on Stack/Value Types)

Kotlin & Swift Classes
- Classes are reference types.
- They support inheritance and can be subclassed.
- They allow for more complex memory management, as they are allocated on the heap.
- They support features like data encapsulation, inheritance, and polymorphism.
- They can have both mutable and immutable properties.

Swift Structs
- Kotlin doesn’t have a direct equivalent of Swift’s structs.
- Structs in Swift are value types.
- Structs are allocated on the Stack, making them more efficient in terms of memory management compared to classes.
- They are copied when passed around, making it safer in terms of concurrency.

Program Memory:

  • Stack - Smaller/Faster access, used for short-term data (variables, function & parameters). Removed when function is done. Last-In-First-Out (LIFO) data structure.
  • Heap - Larger/Slower access, used for long-term data (objects & data structures). Manually deallocated. Memory has no predefined data structure. Memory management for the heap is more complex and involves mechanisms like reference counting and garbage collection.

*SwiftUI views are value types (structs) and are typically allocated on the stack, which makes them efficient for short-lived, localized operations. Swift is Protocol-Oriented Programming (see below)

Code Examples

Kotlin Class / Data:

In Kotlin, classes and data classes are reference types allocated on the heap, meaning that instances point to the same memory location unless explicitly copied. When a data class is copied, a new instance is created, and changes to the copy do not affect the original instance.

In contrast, Swift structs are value types allocated on the stack. Assigning one struct to another creates a complete copy, so modifications to the copied struct do not impact the original. This difference underscores Kotlin’s preference for reference semantics in its classes and Swift’s use of value semantics in its structs.

Class is a reference. (Heap)

data class Person(var name: String, var age: Int)
val person1 = Person("Alex", 20)
var person2 = person1
person2.age = 30
println("Person1 age: ${person1.age.toString()}") // Person1 age: 30
println("Person2 age: ${person2.age.toString()}") // Person2 age: 30

Data Class is a reference but here is copied. (Heap)

data class Person(var name: String, var age: Int)
val person1 = Person("Alex", 20)
var person2 = person1.copy() // create a new person
person2.age = 30
println("Person1 age: ${person1.age.toString()}") // Person1 age: 20
println("Person2 age: ${person2.age.toString()}") // Person2 age: 30

~~~

Swift Struct is a value type (Stack):

struct Person {
let name: String
var age: Int
}
let person1 = Person (name: "Alex", age: 20)
var person2 = person1 // created a whole new person
person2.age = 30
print(person1.age) // Prints 20
print(person2.age) // Prints 30

Kotlin Data Class / Swift Structs

In Swift, structs are value types that exhibit copy-on-write semantics and are often preferred for their immutability and thread safety. Additionally, Swift heavily emphasizes the use of protocols to define blueprints for functionalities that types must adhere to. These protocols enable Swift to achieve a high degree of code modularity, flexibility, and polymorphism.

Conversely, Kotlin, while offering interfaces similar to Swift’s protocols, does not share the same emphasis on structs or immutability by default. Kotlin interfaces are primarily used for defining contracts that classes must adhere to, but they lack the same level of flexibility and extension capabilities as Swift’s protocols.

Therefore, due to Swift’s strong emphasis on structs, immutability, and protocols, it is often regarded as a protocol-oriented language, whereas Kotlin’s design choices and lack of emphasis on structs lead to a different categorization.

Data Classes vs Struct

Data classes are a powerful tool for modeling data structures in Kotlin. They offer a concise way to define classes specifically designed to hold data and provide essential functionality out of the box.

Kotlin Data Class (Heap)

data class User(val id: Int, val name: String, val email: String) {
// Functions are automatically generated by the data class keyword
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as User
if (id != other.id) return false
return true
}
override fun hashCode(): Int {
return id
}
override fun toString(): String {
return "User(id=$id, name='$name', email='$email')"
}
// You can also define custom methods specific to your User class
fun getInitials(): String {
return name.substring(0, 1) + "." + name.substring(name.indexOf(' ') + 1, name.indexOf(' ') + 2)
}
}

Swift Struct (Stack)

struct User {
let id: Int
var name: String
let email: String
// Functions for a struct need to be manually implemented
func equals(other: User) -> Bool {
return self.id == other.id
}
func getInitials() -> String {
return name.first!.uppercased() + "." + name.split(separator: " ").last!.first!.uppercased()
}
}

Swift Structs

Swift structs are truly immutable. Once a struct is created, its properties cannot be changed. Any attempt to modify a property results in a new struct instance with the updated value.

Kotlin Data Classes

While data classes provide features like automatic generation of equals, hashCode, and toString, they don't enforce immutability by default. Properties of a data class can be modified after creation.

We use data class to create immutable objects.

But there is a downside of using data class. Instantiating a data class is expensive. Primitive values can be written to the stack which is fast and efficient. Instances of data classes are written to the heap.

Functions

In Kotlin, the filterNumbers function filters a list of integers based on a provided predicate function, returning a new list of integers that satisfy the condition. In Swift, a similar function named filterNumbers uses Swift's built-in filter method to achieve the same result, demonstrating that Swift allows the omission of argument labels when they match parameter names. Both examples show how each language handles higher-order functions and list filtering, with Kotlin using an explicit loop and mutable list, while Swift leverages its standard library's functional capabilities.

Kotlin:

fun filterNumbers(numbers: List<Int>, filter: (Int) -> Boolean): List<Int> {
val filteredList = mutableListOf<Int>()
for (number in numbers) {
if (filter(number)) {
filteredList.add(number)
}
}
return filteredList
}

// Usage
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evenNumbers = filterNumbers(numbers) { number -> number % 2 == 0 }
println(evenNumbers) // Output: [2, 4, 6]

Swift:

func greet(person name: String) -> String {
return "Hello, \(name)!"
}

let greeting = greet(person: "Alice")
print(greeting) // Output: Hello, Alice!
  • Swift allows omitting argument labels when they match parameter names
  • “numbers” label is needed to compile
func filterNumbers(numbers: [Int], filter: (Int) -> Bool) -> [Int] {
return numbers.filter(filter) // lable matches parameter name.
}

// Usage
let numbers = [1, 2, 3, 4, 5, 6]
let evenNumbers = filterNumbers(numbers: numbers) { number in number % 2 == 0 }
print(evenNumbers) // Output: [2, 4, 6]

Extension Functions

In Kotlin, a function `wordCount` is added to the `String` class as an extension function, which splits the string by spaces and returns the number of words. For instance, calling `wordCount` on the string “Hello, world! How are you?” returns 5.

In Swift, an extension is added to the `String` class to introduce an `isPalindrome` function, which checks if the string is the same forwards and backwards, ignoring case.

Kotlin:

// Extend the String class to add a "wordCount" function
fun String.wordCount(): Int {
return this.split(" ").size
}

// Usage
val message = "Hello, world! How are you?"
val wordCount = message.wordCount()
println(wordCount) // Output: 5

Swift:

// Extend the String class to add a "isPalindrome" function
extension String {
func isPalindrome() -> Bool {
let lowercaseString = self.lowercased()
return lowercaseString == String(lowercaseString.reversed())
}
}

// Usage
let text = "Racecar"
if text.isPalindrome() {
print("The text is a palindrome!")
}

Null Safety

Kotlin and Swift both provide robust mechanisms for handling nullability and ensuring null safety in their respective languages. In Kotlin, non-nullable variables cannot be assigned null values, while nullable variables can be. Kotlin uses explicit null checks, the safe call operator (?.), and the Elvis operator (?:) for default values to handle nullability.

Swift, on the other hand, uses optional types to represent variables that can be nil. It employs “if let” syntax for safely unwrapping optionals, optional chaining (?.) for accessing properties or methods on optionals, and the nil coalescing operator (??) to provide default values when an optional is nil.

Both languages provide powerful tools to manage nullability, ensuring safer and more predictable code.

Kotlin:

// Non-nullable variable (will crash if assigned null)
var name: String = "Alice"

// Nullable variable (can be assigned null)
var nickname: String? = null

// Checking for null before using -- Kotlin does not have if let.
if (nickname != null) {
println("Her nickname is ${nickname}")
} else {
println("She doesn't have a nickname yet.")
}

// Using the Elvis operator (?:) for default values
val greeting = nickname ?: "Hi there!" // Assigns "Hi there!" if nickname is null

// Safe call operator (?.) for accessing properties/methods on nullable objects
val length = nickname?.length // Returns null if nickname is null, otherwise its length

Swift: has “if let” and “guard” but no “Elvis operator” it uses “Nil Coalescing” …

// Non-optional variable (implicitly unwrapped optional)
var name: String = "Alice"

// Optional variable (can be nil)
var nickname: String? = nil

// Checking for nil before using
if let unwrappedNickname = nickname {
print("Her nickname is \(unwrappedNickname)")
} else {
print("She doesn't have a nickname yet.")
}

// Forced unwrapping (use with caution, can crash if nil)
let forcedNickname = nickname! // Can crash if nickname is nil

// Optional chaining (?.) for accessing properties/methods on optionals
let length = nickname?.count // Returns nil if nickname is nil, otherwise its count

nickname = nil

// if let
if let unwrappedNickname = nickname {
print("Her nickname is \(unwrappedNickname)")
} else {
print("She doesn't have a nickname yet.")
}

// Nil Coalescing
nickname = nil
let greeting = nickname ?? "Hi there!" // Assigns "Hi there!" if nickname is nil

print(greeting) // Output: Hi there!

nickname = "Ash"

// not using if let but guard allows for the happy path.
func printNickname() {
guard let name = nickname else {
return
}

print("\(name) ... This is the happy path")
}
printNickname()

// Output
// She doesn't have a nickname yet.
// She doesn't have a nickname yet.
// Hi there!
// Ash ... This is the happy path

Closures

In both Kotlin and Swift, closures are powerful constructs that encapsulate blocks of code and can be passed around as variables or parameters to functions. Closures, similar to functions, can capture and access variables from their surrounding environment, making them particularly useful for creating flexible and reusable code.

Kotlin:

  • Functions are self-contained units of code with limited access.
  • Closures are like functions with an extra ability to “remember” and access variables from their creation environment.
fun createMultiplier(factor: Int): (Int) -> Int {
return { number -> number * factor } // Closure captures 'factor'
}

val doubler = createMultiplier(2)
val tripler = createMultiplier(3)

val doubledValue = doubler(5) // doubledValue will be 10
val tripledValue = tripler(5) // tripledValue will be 15
// Function taking an integer and a closure as arguments
fun operate(number: Int, operation: (Int) -> Int): Int {
return operation(number)
}

// Defining a closure
val doubler = { value: Int -> value * 2 }

// Using the closure with the function
val result = operate(5, doubler)
println(result) // Output: 10

val results = operate(5) {value: Int -> value * 2 }
println(results) // Output: 10

// Closure accessing a variable from its surrounding context
val name = "Alice"
val greeting = { println("Hello, $name!") }

// Calling the closure
greeting() // Output: Hello, Alice!

Swift:

func createMultiplier(factor: Int) -> (Int) -> Int {
var internalTotal = 0 // we can even add some storage here if needed
return { number in
return number * factor
}
}

let doubler = createMultiplier(factor: 2)
let tripler = createMultiplier(factor: 3)

let doubledValue = doubler(5) // doubledValue will be 10
let tripledValue = tripler(5) // tripledValue will be 15
// Function taking an integer and a closure as arguments
func operate(number: Int, operation: (Int) -> Int) -> Int {
return operation(number)
}

// Defining a closure
let doubler = { value in value * 2 }

// Using the closure with the function
let result = operate(number: 5, operation: doubler)
print(result) // Output: 10

// Using the closure with the function
let result2 = operate(number: 5) {value in value * 2}
print(result2) // Output: 10

// Closure accessing a variable from its surrounding context
var name = "Alice"
let greeting = { print("Hello, \(name)!") }

// Calling the closure
greeting() // Output: Hello, Alice!

Error Handeling

The Kotlin and Swift examples showcase different approaches to error handling. In Kotlin we utilizes a `try-catch` block to handle potential exceptions that may occur during something like a file reading, such as file not found or permission denied. It catches exceptions of type `Exception`, prints an error message, and returns an empty string in case of an error.

In Swift, error handling is achieved through the use of `throws` and `do-catch` blocks. Again a file reading example, the `readFile` function throws custom `FileError` enum cases to indicate file-related errors like file not found or permission denied. The caller handles these errors using `do-catch` blocks, catching specific `FileError` cases or a general `catch` block for any other unexpected errors.

Both Kotlin and Swift do not “force” exception handling.

// does not force exception handling.
fun main() {
val userInput = "hello"

// This line attempts to convert a String to an Int, which will throw an exception
val number = userInput.toInt()

println("Converted number: $number")
}

Both approaches ensure robust error handling, with Kotlin using exceptions and Swift employing a more structured approach with custom error types.

Kotlin:

fun main() {
try {
val result = 10 / 0 // This line will cause an ArithmeticException
println(result)
} catch (e: ArithmeticException) {
println("Error: Division by zero!") // Catches the exception and prints a message
}
println("Code continues after the exception...")
}

// Output
// Error: Division by zero!
// Code continues after the exception...

Swift:

enum MyError: Error {
case runtimeError(String)
}

func throwingFunction() throws {
throw MyError.runtimeError("An error occurred")
}

func tryIt() {
do {
try throwingFunction()
print("Success")
} catch MyError.runtimeError(let message) {
print("Caught an error: \(message)")
} catch {
print("An unexpected error occurred: \(error)")
}
print("Done")
}

tryIt()

// Output
// Caught an error: An error occurred
// Done

Concurrency: Maneuvering Through Tasks

Kotlin Coroutines vs. Swift Structured Concurrency

Modern mobile development often involves handling asynchronous operations — network requests, data fetching, etc. — without blocking the main UI thread. Both Android and iOS have introduced powerful tools for managing these tasks concurrently: Kotlin Coroutines and Swift Structured Concurrency. Let’s explore these approaches and see how they tackle asynchronous programming.

Under the Hood: Launching and Suspending

Kotlin Coroutines replace callback

Coroutines replace callback …

The top code passes a callback to handle the async but it is hard to read and understand what is happening. The bottom code is clear to read and understand. We create a token & post passed to processPost in the background.

Coroutines replace callback

Kotlin Coroutines

These lightweight threads of execution offer a powerful mechanism for writing non-blocking code. They leverage a concept called “coroutines” which can be launched and suspended, allowing the coroutine to yield execution and be resumed later. This asynchronous behavior helps maintain a responsive UI while tasks run in the background.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simple(): Flow<Int> = flow { // flow builder
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
emit(i) // emit next value // (3)
}
}

fun main() = runBlocking<Unit> {
// Launch a concurrent coroutine to check if the main thread is blocked
launch {
for (k in 1..3) {
println("I'm not blocked $k") // (2)
delay(100)
}
}
println("Done") //(1)
// Collect the flow
simple().collect { value -> println(value) }
}

/* Output
Done
I'm not blocked 1
1
I'm not blocked 2
2
I'm not blocked 3
3
*/

And with a suspend function

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

fun simple(): Flow<Int> = flow { // flow builder
for (i in 1..3) {
delay(100) // pretend we are doing something useful here
emit(i) // emit next value
}
}

suspend fun performTask() {
simple().collect { value -> println(value) }
}

fun main() = runBlocking<Unit> {
// Launch a concurrent coroutine to check if the main thread is blocked
launch {
for (k in 1..3) {
println("I'm not blocked $k")
delay(100)
}
}

// Launch performTask in a background coroutine
launch {
println("start task")
performTask()
println("end task")
}

// Print "Done" immediately
println("Done")
}

/* Output
Done
I'm not blocked 1
start task
I'm not blocked 2
1
I'm not blocked 3
2
3
end task
*/

We could use async and await …

import kotlinx.coroutines.*

// Function to simulate a long-running task
suspend fun fetchData(): String {
delay(1000) // Simulate a network or database delay
return "Data fetched successfully"
}

fun main() = runBlocking {
// Launch a coroutine to fetch data
val deferredResult = async { fetchData() }

// Other operations can be performed here while waiting for the result
print("Doing some work ....")

// Await the result of the coroutine
val result = deferredResult.await()

// Other operations can be performed here while waiting for the result
print("Doing other work ....")

// Print the result
println(result)
}

/*
Output
Doing some work ....
Doing other work ....
Data fetched successfully
*/

Coroutines are not a new concept, let alone invented by Kotlin. They’ve been around for decades and are popular in some other programming languages such as Go. What is important to note though is that the way they’re implemented in Kotlin, most of the functionality is delegated to libraries. In fact, beyond the suspend keyword, no other keywords are added to the language. This is somewhat different from languages such as C# that have async and await as part of the syntax. With Kotlin, these are just library functions.

import kotlinx.coroutines.*

suspend fun fetchData(): String {
delay(1000) // Simulate a network or database delay
return "Data fetched successfully"
}

fun main() = runBlocking {
val job = launch {
val result = fetchData()
println(result)
}

println("Done")
job.join() // Wait for the coroutine to complete
}

/* OutputDone
Data fetched successfully
*/

Flow

In summary, StateFlow is used for observing changing states, SharedFlow is used for emitting events that multiple consumers can handle, and MutableSharedFlow is used for updating the stream of values dynamically.

We use these in our ViewModel.

// ViewModel
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(Success(data = emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<CatUiState> = _uiState
// Composable
val state by viewModel.uiState.collectAsStateWithLifecycle()
  • — Flow — Emit values sequentially over time. Expose application data using Kotlin Flows. Used with RoomDB for a reactive MVVM architecture. Access local database using suspend functions
  • — Channels — Transfer a stream of values via broadcasters and receivers. Can be used with BLE to communicate with GATT server
Hot Radio / Cold CD

Swift Combine Framework

While Swift does not have direct counterparts to Kotlin Flow/Channels, the Combine framework and Swift’s structured concurrency model provide robust tools for handling similar asynchronous and reactive programming tasks.

The Combine Framework might be add to Swift Language and not a separate framework! Just like Flow / Channel libraries in Kotlin.

Combine Framework Might be going away!

Codelabs:

Video Series:

Swift Structured Concurrency

Again we do not want to use completion handler (callbacks)

Before Swift Structured Concurrency

It’s possible for those functions to call their completion handler more than once, or forget to call it entirely.

The parameter syntax @escaping (String) -> Void can be hard to read.At the call site we end up with a so-called pyramid of doom, with code increasingly indented for each completion handler.

Until Swift 5.0 added the Result type, it was harder to send back errors with completion handlers.

Forget to call the completion handler when an error occurs.

Life after Swift Structured Concurrency is much better.

//OLD
// We call them like a chain
fetchWeatherHistory { records in
calculateAverageTemperature(for: records) { average in
upload(result: average) { response in
print("Server response: \(response)")
}
}
}

// NEW
// Called like normal code
func processWeather() async {
let records = await fetchWeatherHistory()
let average = await calculateAverageTemperature(for: records)
let response = await upload(result: average)
print("Server response: \(response)")
}

Code looks sequential and extremely easy to add error handling !!!

// async throws, we call the function using try await …

func fetchFavorites() async throws -> [Int] {
let url = URL(string: "https://hws.dev/user-favorites.json")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Int].self, from: data)
}

// Parrell
func loadData() async {
async let (userData, _) = URLSession.shared.data(from: URL(string:"https://hws.dev/user-24601.json")!)
async let (messageData, _) = URLSession.shared.data(from: URL(string: "https://hws.dev/user-messages.json")!)
// more code to come ...
}

// Code Example:
do {
let decoder = JSONDecoder()
let user = try await decoder.decode(User.self, from: userData)
let messages = try await decoder.decode([Message].self, from: messageData)
print("User \(user.name) has \(messages.count) message(s).")
} catch {
print("Sorry, there was a network problem.")
}

Introduced in Swift 5.5, structured concurrency provides a more syntactic approach to asynchronous programming. It utilizes keywords like async and await to mark asynchronous functions and wait for their results, respectively. This approach promotes readable code that resembles sequential execution, even though the tasks are happening concurrently.

import Foundation

// Define an asynchronous function that performs a simple task
func performTask(number: Int) async -> String {
let time: UInt64 = 3 - UInt64(number)
do {
try await Task.sleep(nanoseconds: time * 1_000_000_000) // Simulate some work with a 1-second sleep
print("task \(number) Finished")
return "Task \(number) completed"
} catch {

}
return "Ok"
}

// Main function

async let result1 = performTask(number: 1)
async let result2 = performTask(number: 2)
async let result3 = performTask(number: 3)

let results = await [result2, result1, result3]

for result in results {
print(result)
}

print("All tasks completed")

/* Output
task 3 Finished
task 2 Finished
task 1 Finished

// this bound to the array order not done order
Task 2 completed
Task 1 completed
Task 3 completed
All tasks completed
*/

Article:

This article about Swift concurrency discusses the problems with GCD and how Swift concurrency can solve them. Async/await is a new way to write asynchronous code. It replaces completion handlers and makes code easier to read. Other important concepts include tasks, task groups, and actors. Actors are thread-safe and can be used to protect mutable state. Overall, Swift concurrency aims to make it easier to write safe and efficient concurrent code.

Note: Swift Actors

Swift actors offer a powerful tool for managing concurrent programming in a safe and efficient manner. They act like special types of objects that can be accessed concurrently from multiple threads. Unlike regular classes, actors ensure serialized access to their internal state, preventing data races and potential corruption. This makes them ideal for situations where multiple parts of your program need to interact with shared data without causing conflicts. They also provide built-in mechanisms for asynchronous communication between actors, enabling you to structure your code for clear separation of concerns and improved maintainability in concurrent environments.

Only one “like” from the creator of Swift Lang!

Swift actors are a concurrency feature introduced in Swift 5.5 to help manage and synchronize access to mutable state across different parts of your code. They provide a way to safely manage state in a concurrent environment, ensuring that only one task can access the actor’s mutable state at a time.

Simple Explanation

  • Actors: Actors are like classes, but they automatically manage synchronization, ensuring that their state is accessed safely in a concurrent environment.
  • Concurrency Safety: When you define an actor, any access to its state is automatically serialized. This means that even if multiple tasks try to interact with the actor simultaneously, the Swift runtime will ensure these interactions happen one at a time.
  • Isolation: The state within an actor is isolated from the rest of the program, and only the actor itself can mutate its state directly.

Simple Code Example

Here’s a very simple example of using an actor in Swift:

// Define an actor
actor Counter {
private var value = 0
// Method to increment the counter
func increment() {value += 1} // Critical Section with Errors!!!
// Method to get the current value
func getValue() -> Int {return value}
}

// Example usage of the actor
@main
struct MyApp {
static func main() async {
let counter = Counter()
// Create some tasks to increment the counter concurrently
await withTaskGroup(of: Void.self) { group in
for _ in 0..<100 {
group.addTask {
await counter.increment()
}
}
}
// Get the final value of the counter
let finalValue = await counter.getValue()
print("Final counter value: \(finalValue)")
}
}

In this example, even though the increment method is called concurrently from multiple tasks, the actor ensures that these calls are serialized and the internal state is safely managed. This makes actors a powerful tool for managing shared state in concurrent Swift applications.

Swift Playgrounds — Code Tutorials:

Video covering everything so far …

~~~

Similarities in Spirit

  • Both Coroutines and Structured Concurrency are designed to address the challenges of asynchronous programming. They offer a way to launch background tasks without blocking the main thread, ensuring a smooth user experience.
  • Both leverage continuations: When a coroutine or async function suspends, its execution context is preserved, allowing it to be resumed later. This enables efficient management of asynchronous operations.
  • Error Handling: Both approaches provide mechanisms for handling errors that occur during asynchronous operations. This ensures your application remains robust even in the face of unexpected issues.

Kotlin:

import kotlinx.coroutines.*

fun main() = runBlocking {
// Launch a new coroutine in the background
val job = launch {
try {
// Simulate a task that might throw an exception
performTask()
} catch (e: Exception) {
// Handle the exception
println("Error: ${e.message}")
}
}

// Wait for the coroutine to complete
job.join()
println("Done")
}

// A suspend function simulating a task that throws an exception
suspend fun performTask() {
delay(1000) // Simulate some work
throw RuntimeException("Something went wrong!")
}

Swift:

// Swift handles the errors like NORMAL!
func updateUsers() async {
do {
let users = try await fetchUsers(count: 3)
let result = try await save(users: users)
print(result)
} catch {
print("Oops!") // These must be caught!!!
}
}

Nuances and Distinctions

  • Kotlin Coroutines offers library utility: Coroutine builders provide flexibility in how coroutines are launched and managed. Developers can choose between launching fire-and-forget coroutines or those that return a result.
  • Swift Structured Concurrency prioritizes readability: The async/await syntax makes code for asynchronous operations look more like sequential code, improving readability and maintainability.
Concurrency Comparison

And Now … Some Differences

Kotlin

Delegation: Kotlin enables the delegation of functionality to other classes or interfaces, promoting code reusability and separation of concerns. The example provided demonstrates how delegation works using interfaces and the by keyword, showcasing how specific functionalities can be delegated to other objects, thereby enhancing code flexibility and maintainability. Steps:

1. Interface Definition:

2. Delegate Implementation:

interface Printer {
fun printMessage(message: String)
}
class ConsolePrinter : Printer {
override fun printMessage(message: String) {
println("Console Printer: $message")
}
}
class FancyPrinter(private val printer: Printer) : Printer by printer {
// Additional methods or properties can be added here
fun printFancyMessage(message: String) {
println("Fancy Printer: $message")
}
}
fun main() {
val consolePrinter = ConsolePrinter()

val fancyPrinter = FancyPrinter(consolePrinter)
fancyPrinter.printMessage("Hello, World!") // Delegated to ConsolePrinter
fancyPrinter.printFancyMessage("Hello, Kotlin!") // Custom method of FancyPrinter
}

3. Delegation with “by” Keyword

By using interfaces and the by keyword, you can delegate specific functionalities to other objects, making your code more flexible and maintainable.

— Swift’s approach requires more boilerplate code compared to Kotlin’s built-in delegation, but it offers flexibility and can be customized to fit various use cases. While not as concise as Kotlin’s by keyword, Swift’s use of protocols, extensions, and composition can effectively replicate the delegation pattern.

Swift

Property Wrappers: Swift introduces property wrappers, which allow the definition of custom wrappers to add additional functionality or validation logic to properties. The provided example illustrates how property wrappers work by defining a NameValidator wrapper to validate the name property of a Person struct, showcasing how property wrappers can enhance property behavior and encapsulate validation logic.

struct NameValidator: PropertyWrapper {
var wrappedValue: String {
willSet {
print("do any setup here")
}
didSet {
if wrappedValue.isEmpty {
print("Error: Name cannot be empty")
}
}
}

init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
struct Person {
@NameValidator var name: String
}
// Usage
let person1 = Person(name: "Alice") // No error
let person2 = Person(name: "") // Prints "Error: Name cannot be empty"

— While Kotlin does not have an exact match for Swift’s property wrappers, its flexibility with custom getters, setters, and property delegates allows developers to implement similar functionality in a clean and reusable manner.

Protocols: In Swift, the protocol keyword is used to define a blueprint for what properties and methods a type (like a class, struct, or enum) must implement. It's like a contract that specifies the functionalities a type needs to provide, without dictating how those functionalities are implemented.

// Define a protocol named Printable
protocol Printable {
func printMessage()
}// Adopt the Printable protocol in a class
class Printer: Printable {
func printMessage() {
print("Printing a message")
}
}// Create an instance of the Printer class
let printer = Printer()// Call the printMessage method defined in the protocol
printer.printMessage() // Output: Printing a message

— Kotlin interfaces do almost the same thing

Code Generation

Kotlin KSP:

KSP is a powerful tool for developers who want to extend the capabilities of the Kotlin compiler by creating lightweight and efficient plugins. Its focus on simplicity and Kotlin-centric design makes it a valuable option for building advanced functionalities on top of the Kotlin language.

Swift Macro:

Macros transform your source code when you compile it, letting you avoid writing repetitive code by hand. During compilation, Swift expands any macros in your code before building your code as usual.

Interfaces (OOP) vs Protocols (POP): Embracing Contracts

One key difference between Swift and Kotlin lies in their approach to defining contracts between objects. While both languages offer mechanisms to achieve similar goals, the underlying philosophies differ.

Swift: Embracing Protocols

Swift is often described as a protocol-oriented language. Protocols define a blueprint for functionalities that a class, struct, or enumeration must implement. They specify the methods, properties, and requirements that conforming types must adhere to. This approach offers several advantages:

  • Improved Code Clarity: Protocols act as contracts, clearly outlining the expected behavior of conforming types. This enhances code readability and maintainability.
  • Loose Coupling: Protocols promote loose coupling between objects. Code written against a protocol can work with any type that conforms to it, regardless of the specific implementation details.
    Think composition over inheritance in OOP
  • Polymorphism: Protocols enable polymorphism, allowing you to treat objects of different types that conform to the same protocol in a similar way. (same as Kotlin inheritance and interfaces)

Example: A Movable Protocol in Swift

protocol Movable {
func move(to destination: Point) // can have a default implementation.
}
struct Car: Movable {
func move(to destination: Point) {
print("Driving car to destination \(destination)")
}
}
struct Person: Movable {
func move(to destination: Point) {
print("Walking to destination \(destination)")
}
}
// Function that can work with any Movable object
func navigate(movable: Movable, to destination: Point) {
movable.move(to: destination)
}

Kotlin: Interfaces (Similar Functionality, Different Philosophy)

Kotlin offers interfaces, which share some similarities with Swift protocols. Interfaces define methods and properties that classes can implement.

Movable “Protocol” in Kotlin

interface Movable {
fun move(destination: Point)
}

data class Point(val x: Int, val y: Int)

class Car : Movable {
override fun move(destination: Point) {
println("Driving car to destination $destination")
}
}

class Person : Movable {
override fun move(destination: Point) {
println("Walking to destination $destination")
}
}

fun navigate(movable: Movable, destination: Point) {
movable.move(destination)
}

fun main() {
val car = Car()
val person = Person()

navigate(car, Point(10, 20))
navigate(person, Point(30, 40))
}

Summery

These are just a few examples — both Kotlin and Swift continue to evolve with new features and improvements. Exploring these features can help you write more efficient, expressive, and maintainable code for your mobile applications.

Protocol-Oriented Programming (POP)

Protocol-Oriented Programming (POP) is a programming paradigm that emphasizes using protocols to design and structure your code, particularly in the Swift programming language. It focuses on defining what functionalities different parts of your code should have rather than how they achieve them. This approach offers several advantages:

  • Flexibility and Modularity: Instead of relying solely on inheritance, POP allows different types of objects to conform to the same protocol, promoting loose coupling and enabling you to swap implementations easily.
  • Clarity: By separating the “what” from the “how,” POP can make code easier to understand and maintain, as the expected behavior is explicitly defined in the protocols.
  • Type Safety: Swift’s type system works well with POP. You can use protocols as constraints to ensure that certain functionalities are available when working with specific types.

We all understand OOP but here is a deeper dive into the core concepts of POP:

Advantages of Protocols

  1. Protocols: These act as blueprints that define the functionalities (methods and properties) that conforming types must implement. They specify the behavior without dictating the specific implementation details.
    Focus on Contracts:
    - Protocols define the “what” (required functionalities) but not the “how” (implementation details) of a type’s behavior.
    - This approach promotes code flexibility and reusability, as different types can conform to the same protocol while providing their own implementations.
  2. Type Constraints: Swift’s powerful type system allows you to specify that a variable or function argument must conform to a particular protocol. This ensures type safety and helps catch errors early in the development process.
  3. Value Types by Default: Swift favors value types (structs) by default, which can benefit from POP as they can’t inherit from other types but can conform to protocols.
  4. Protocol Extensions: These allow you to add default implementations for functionalities defined in a protocol. This can help reduce boilerplate code and provide common behavior for conforming types. It allows you to add default implementations or additional functionality to existing protocols, further enhancing code organization and reusability.

POP vs OOP (Object-Oriented Programming):

While POP and OOP share some similarities, they have key differences in their approach:

Inheritance vs. Composition: OOP relies heavily on class hierarchies where subclasses inherit properties and behaviors from parent classes. POP, on the other hand, prioritizes composition through protocols. Types conform to protocols to gain the required functionalities, promoting loose coupling.

Benefits of POP:

  • Improved Code Maintainability: POP can lead to cleaner and more readable code, as protocols clearly define expected behaviors.
  • Improved Code Reusability: Code that works with a protocol can work with any type that conforms to that protocol, regardless of the specific implementation details.
  • Increased Flexibility: The ability to swap implementations easily between types conforming to the same protocol makes code more adaptable.
  • Enhanced Testability: Focusing on functionalities through protocols makes it easier to write unit tests that isolate specific behaviors.
  • Enhanced Type Safety: Protocols enforce certain functionalities at compile time, helping to prevent errors where types used in code might lack necessary features.
  • Clearer Code Organization: Protocols help group related functionalities, making code more readable and maintainable.
  • Focus on Behavior: POP encourages you to think about the behavior a type should have rather than just its properties and internal structure.

POP is a powerful approach for designing and structuring code in Swift. By leveraging protocols and their features, you can create flexible, modular, and type-safe applications.

How to use Protocols in Swift | Advanced Learning #15

In traditional COP languages like Java or C++, inheritance from classes is a primary mechanism for code reuse. While Swift supports inheritance, Protocol-Oriented Programming (POP) offers a more flexible approach that allows different types (classes, structs, enums) to conform to the same protocol, promoting broader reusability.

Swift: Strongly emphasizes protocols with features like protocol extensions and value types, making it well-suited for POP. Swift supports both POP and traditional object-oriented programming (OOP), allowing for flexible code design.

Kotlin: Provides interfaces with default implementations and extension functions, but relies more on class inheritance compared to Swift’s protocol emphasis. Kotlin supports both interface-based design and traditional OOP for achieving code reusability and type safety.

protocol Greetable {
var name: String { get }
func greet()
}
extension Greetable {
func greet() {
print("Hello, \(name)!")
}
}

struct Person: Greetable {var name: String}
struct Animal: Greetable {var name: String}

let person = Person(name: "Alice")
let animal = Animal(name: "Rex")

person.greet() // Output: Hello, Alice!
animal.greet() // Output: Hello, Rex!

Same in Kotlin

interface Greetable {
val name: String
}
fun Greetable.greet() {
println("Hello, $name!")
}

class Person(override val name: String) : Greetable
class Animal(override val name: String) : Greetable

fun main() {
val person = Person("Alice")
val animal = Animal("Rex")

person.greet() // Output: Hello, Alice!
animal.greet() // Output: Hello, Rex!
}

Package Manager

When building mobile applications, managing dependencies (reusable libraries) is crucial. Both Kotlin and Swift offer robust solutions for this task.

Kotlin DO NOT USE

  • Buck /Bazel— Android team is investing in Gradle.
  • Gradle without version catalog ( TOML file )

Swift DO NOT USE

  • CocoaPods — Not supported by Apple / Might become depreciated.
  • Carthage — Not supported by Apple / Might become depreciated.

Kotlin Gradle with TOML:

  • Gradle: Gradle is the primary build system for Android development using Kotlin. It handles tasks like compiling code, managing dependencies, and packaging the final application.
  • TOML: TOML (Tom’s Obvious, Minimal Language) is a configuration file format gaining popularity for dependency management in Kotlin projects. It provides a human-readable way to define versions and manage libraries. Developers often use TOML files in conjunction with Gradle for centralized version control.

How it works:

  1. Dependency Repositories: Gradle retrieves dependencies from repositories like Maven Central or custom repositories.
  2. Build Script: The build.gradle file specifies the dependencies needed for your project using Gradle syntax. It can also reference a TOML file for versions.
  3. TOML File: the TOML file defines sections like [versions] and [libraries] to manage versions and aliases for dependencies.
  4. Gradle Download and Resolution: Gradle downloads the required dependencies from the repositories and resolves any conflicts based on version compatibility.

Benefits:

  • Centralized Version Control: TOML provides a central location to manage versions, simplifying maintenance and updates.
  • Flexibility: Gradle offers powerful features for managing complex project dependencies and configurations.
  • Large Ecosystem: Android development has a vast ecosystem of libraries readily available through Gradle repositories.

Example (build.gradle with TOML reference):

[versions]
kotlin = "1.7.20"
androidx_core = "1.8.0"
[libraries]
kotlin_stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
[bundles]
ui_dependencies = [
"androidx.appcompat:appcompat:1.4.2",
"com.google.android.material:material:1.6.1",
]
[plugins]
android_gradle = "7.2.1"

Swift Package Manager (SPM):

  • Integrated Tool: SPM is a built-in dependency management system for Swift projects. It seamlessly integrates with Xcode, the primary development environment for iOS and macOS apps.
  • Package Definition: Dependencies are defined as Swift packages, which include source code, resources, and metadata.
  • Package Registries: Packages can be retrieved from public registries like the Swift Package Index (SPI) or private repositories.

How it works:

  • Package.swift: The Package.swift file specifies the dependencies needed for your project using SPM syntax.
  • Package Registry: SPM retrieves the required packages from the specified registries.
  • Integration: Downloaded packages are integrated into your Xcode project for seamless compilation and linking.

Benefits:

  • Simple and Intuitive: SPM is tightly integrated with Xcode, making dependency management straightforward.
  • Modern Approach: SPM aligns with the modern Swift language philosophy and provides a lightweight solution.
  • Version Control Integration: SPM integrates well with version control systems like Git for managing dependency versions.

Example (Package.swift)

// swift-tools-version:5.3
import PackageDescription

let package = Package(
name: "MySwiftProject",
dependencies: [
// Add the ReactiveSwift package from GitHub
.package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "6.9.0"),
// Add the Alamofire package from GitHub
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.4.0")
],
targets: [
.target(
name: "MySwiftProject",
dependencies: ["ReactiveSwift", "Alamofire"])
]
)

Both Kotlin Gradle with TOML and SPM are powerful tools for managing dependencies in their respective ecosystems.

Next Article (2): Building Your App

  • Data Persistence: Dive into the world of databases with RoomDB (Android) and SwiftData (iOS).
  • User Interface: Explore the power of declarative UI with Compose and SwiftUI, including state management and navigation.
  • Development Environment: Set up your development environment with Android Studio or Xcode, and explore additional tooling options.
  • Project Structures: Understand how to organize your project effectively.

Please leave a comment if anything is unclear / wrong / missing …

we will fix it!

--

--