In this article, I am going to share my experience of learning Swift programming language as a developer coming from Typescript knowledge.
Display to Standard Output
console.log("Hello World!");
print("Hello World!")
Variable Declaration
const PI = 3.14; // Constants
let age = 23; // Variable, Recommended.
var age = 23; // Variable, Not Recommended.
let PI = 3.14 // Constants
var age = 23 // Variable
Declaration of constants can confuse, since let
is used to create variables in Typescript and let
is used to create constants in Swift.
Variable Type Declaration
The type of the variable will be inferred based on the initial value of the variable. In Typescript, constant PI
and variable age
will infer number
type. In Swift, constant PI
will infer Double
and variable age
will infer Int
type.
In Typescript, Variables can be assigned multiple types using the union |
operator can assign different types of values to it.
let customVariable: string | boolean | undefined;
In Swift, Variables can be assigned only a single type, Once the type is declared or inferred, it cannot change.
var customVariable: String?
No implicit Type Conversion
Typescript will implicity modify the type of variable based on the operator or situation.
In Typescript, console.log("10" + 1)
statement will result in "101"
, where 1
is converted to string
and is concerned with "10"
.
In Swift, print("10" + 1)
will result in an error with the below message.
note: overloads for '+' exist with these partially matching parameter lists: (Int, Int), (String, String)
If we need the same behaviour as Typescript, then we must explicitly convert 1
to String
.
The statement will be print("10" + String(1))
.
String
In Typescript, we can create string variables using
Single line: single quotation mark
''
, double quotation mark""
or back-ticks``
Multiline: back-ticks
``
In Swift, we can create string variables using
Single line: double quotes
""
Multiline: three double quotation mark
"""
Only double quotation mark is used in Swift.
String Interpolation
In Typescript, we use ${}
within string wrapped with back-ticks ``
.
console.log(`The width is ${100}`);
In Swift, we use \()
within string wrapped with double quotes ""
. Expression within \()
will be evaluated to create an actual string and is replaced with that string.
print("The width is \(100)")
Arrays
The creation of Arrays is the same in both Typescript and Swift. The only difference is the way the array type is represented and unlike Typescript, Swift will throw an error when we try to access an index outside the array.
const fruits: string[] = ['Orange'];
let fruits: [String] = ["Apple"]
Dictionary
A dictionary stores associations between keys of the same type and values of the same type in a collection with no defined ordering. I think of them as arrays with custom indexes.
var responseMessages: [Int: String] = [
200: "OK",
403: "Access forbidden",
404: "File not found",
500: "Internal server error"
]
print(responseMessages[200]) // Prints "OK"
From a Typescript perspective, they are similar objects where the keys and values are limited to a single type.
const responseMessage: {[key: number]: string} = {
200: "OK",
403: "Access forbidden",
404: "File not found",
500: "Internal server error"
};
console.log(responseMessage["200"]); // Logs "OK"
Optionals
An optional represents two possibilities: Either there is a value, and you can unwrap the optional to access that value, or there isn’t a value at all.
var optionalMessage: String?
optionalMessage = "Hello!"
The question mark indicates that the value it contains is optional. When optionalMessages
is defined as an optional variable without providing a default value, the variable is automatically set to nil
.
In Typescript, Optionals variables can be defined by adding either null
or undefined
.
let optionalMessage: String | undefined;
optionalMessage = "Hello!";
Optional Checking
If Statements and Forced Unwrapping
Use an if
statement to find out whether an optional contains a value by comparing the optional against nil
. If an optional has a value, it’s considered to be “not equal to” nil
.
if optionalMessage != nil {
print("optionalMessage contains a message.")
}
Optional Binding
Optional binding is used to find out whether an optional contains a value, and if so, to make that value available as a temporary constant or variable.
If
optionalMessage
!=nil
,message
is assigned withoptionalMessage
value andmessage
value is printed.If
optionalMessage
==nil
, else block is executed.
if let message = optionalMessage {
print(message)
} else {
print("No message")
}
Nil-Coalescing Operator
The nil-coalescing operator (a ?? b
) unwraps an optional a
if it contains a value, or returns a default value b
if a
is nil
. The expression a
is always of an optional type. The expression b
must match the type that’s stored inside a
.
let message = optionalMessage ?? "No Message"
Switch
A switch
statement considers a value and compares it against several possible matching patterns.
let vegetable = "red pepper"
switch vegetable {
case "celery":
print("Celery")
case "cucumber", "watercress":
print("Sandwich ingredients")
case let x where x.hasSuffix("pepper"):
print("Spicy \(x)")
default:
print("Unknown ingredient")
}
The difference between Typescript switch
cases are:
No Implicit Fallthrough:
break
statements are not required at the end of a case block.Compound Cases: The body of each case must contain at least one executable statement. Multiple switch cases that share the same body can be combined by writing several patterns after
case
, with a comma,
between each of the patterns.var letter: Character = "a" switch(letter) { case "a", "e", "i", "o", "u": print("\(letter) is vowel") default: print("\(letter) is consanant")
Swift case supports other types of case values like intervals, tuples, and where.
Functions
Defining and Calling Functions
func addWithNamedParameters(a: Int, b: Int) -> Int {
return a + b
}
func addWithoutNamedParameters(_ a: Int, _ b: Int) -> Int {
return a + b
}
func
is the keyword to define functionsaddWithNamedParameters
is the name of the function.a
andb
are named parameters of the function.The return type of the function is the type defined after
->
.If an argument label for a parameter is not needed then, we can write an underscore (
_
) instead of an explicit argument label for that parameter.print(addWithNamedParameters(a: 10, b: 20)) // Prints 30 print(addWithoutNamedParameters(10, 20)) // Prints 30
We can use a tuple type as the return type for a function to return multiple values as part of one compound return value.
func calculate(a: Int, b: Int) -> (sum: Int, avg: Double) { let sum = a + b return (sum, Double(sum) / 2) } print(calculate(a: 10, b: 20))
Default Parameters
We 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.
func customPrint(a: Int = 3, b: Int = 5) {
print(a, b)
}
customPrint(a: 10, b: 20) // Prints 10 20
customPrint(b: 20) // Prints 3 2
customPrint() // Prints 3 5
Variadic Parameters
Variadic parameters are similar to Typescript's Rest parameters.
A variadic parameter accepts zero or more values of a specified type. Write variadic parameters by inserting three-period characters (...
) after the parameter’s type name.
func add(_ numbers: Int...) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
print(add(1,2,3,4)) // Prints 10
In-Out Parameters
Function parameters are constants by default, updating them within the function body will result in a compile-time error which prevents modifying the parameter value by mistake. If we want a function to modify a parameter’s value, and we want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.
func swap(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
var a = 3, b = 4
print(a, b) // Prints 3 4
swap(&a, &b)
print(a, b) // Prints 4 3
Closure Expressions
Closure Expressions are similar to Typescript's arrow function syntax.
Closure expressions are a way to write inline closures in a brief, focused syntax. Closure expressions provide several syntax optimizations for writing closures in a shortened form without loss of clarity or intent.
The Sorted Method
The sorted(by:)
method accepts a closure that takes two arguments of the same type as the array’s contents, and returns a Bool
value to say whether the first value should appear before or after the second value once the values are sorted. The sorting closure needs to return true
if the first value should appear before the second value, and false
otherwise.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
Implicit Returns from Single-Expression Closures
To reduce the syntax, We can omit the return type in the single expression closure, where the return type is implicity inferred.
var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in s1 > s2 })
Inferring Type From Context
Swift can infer the types of its parameters and the type of value it returns. The sorted(by:)
method is being called on an array of strings, so its argument must be a function of type (String, String) -> Bool
.
var reversedNames = names.sorted(by: { (s1, s2) in s1 > s2 })
Shorthand Argument Name
Swift automatically provides shorthand argument names to inline closures, which can be used to refer to the values of the closure’s arguments by the names $0
, $1
, $2
, and so on.
var reversedNames = names.sorted(by: { $0 > $1 })
Operator Methods
Swift’s String
type defines its string-specific implementation of the greater-than operator (>
) as a method that has two parameters of type String
, and returns a value of type Bool
. This exactly matches the method type needed by the sorted(by:)
method. Therefore, you can simply pass in the greater-than operator, and Swift will infer that you want to use its string-specific implementation.
var reversedNames = names.sorted(by: >)
Classes
Classes are reference types that are not copied when they’re assigned to a variable or constant, or when they’re passed to a function. Rather than a copy, a reference to the same existing instance is used.
Definition Syntax
class NamedShape {
var numberOfSides: Int = 0
var name: String
init(_ name: String, numberOfSides: Int) {
self.name = name
self.numberOfSides = numberOfSides
}
func getDescription() -> String {
return "A \(name) has \(numberOfSides) sides."
}
}
var shape = NamedShape("Triangle", numberOfSides: 3)
A class definition starts with
class
keyword followed by class name, which can be followed by a list of classes or protocols to support inheritance or comply with protocol structure. From the exampleNamedShape
is the class name.A class can contain a set of properties, which can have a default value. From the example
numberOfSides
andname
are instance properties.init
will be called when the instance of the class is created. If a property defined for a class does not have a default value or it is an optional value. It is mandatory to set that property in theinit
.
Insideinit
the names of the parameters and properties are the same, to differentiate the two, we useself
to refer to the properties while the parameters can be used directly.A class can contain a set of methods. They support the functionality of those instances, either by providing ways to access and modify instance properties or by providing functionality related to the instance’s purpose. From the example
getDescription()
is an instance method.
Inheritance
A class can inherit methods, properties, and other characteristics from another class. When one class inherits from another, the inheriting class is known as a subclass, and the class it inherits from is known as its superclass.
class Square: NamedShape {
var sideLength: Double
var perimeter: Double {
get {
return sideLength * 4
}
set {
sideLength = newValue / 4
}
}
init(sideLength: Double, name: String) {
super.init(name, numberOfSides: 4)
self.sideLength = sideLength
}
func area() -> Double {
return sideLength * sideLength
}
override func description() -> String {
return "A \(name) with sides of length \(sideLength)"
}
}
var square = Square(sideLength: 10.0, name: "Square")
Square
subclass is derived fromNamedShape
base class.Square
class contains its own set of properties likesideLength
,perimeter
as well as the properties inherited fromNamedShape
class.The
init()
initializer forSquare
starts by callingsuper.init()
, which calls the default initializer for theSquare
class’s superclass,NamedShape
. This ensures that the inherited property is initialized byNamedShape
beforeSquare
has the opportunity to modify the property. ThenSquare
class properties are initialized.Square
class contains its own set of methods likearea()
andSquare
class has a choice either to use the methods provided by its superclassNamedShape
or to override the method to provide an alternative implementation of the method.
Computed Properties
We can provide a getter and an optional setter to retrieve and set other properties and values indirectly. For example perimeter
property present in the Square
definition.
shape.perimeter // 40.0
When we retrieve perimeter
value, perimeter
value will be computed based on the sideLength
value and returned.
shape.perimeter = 80.0 // sideLength will be set as 20.0
Similarly, When we set perimeter
value, the value set will be available using newValue
variable by default. We can use this value to update sideLength
value.
Protocols
Protocols are similar to Typescript's Interfaces. It defines a blueprint of methods, properties, and other requirements that can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements.
protocol ExampleProtocol {
var description: String { get }
// In class, functions are able to modify the structure by default. mutating keyword is not needed.
// In struct, functions are not able to modify the structure by default. mutating keyword is needed.
mutating func adjust()
}
class ExampleProtocolClass: ExampleProtocol {
var description: String = "A simple class."
func adjust() {
description += " Adjusted"
}
}
struct ExampleProtocolStructure: ExampleProtocol {
var description: String = "A simple class."
mutating func adjust() {
description += " Adjusted"
}
}
Extensions
Extensions add new functionality to an existing class, structure, enumeration, or protocol type.
extension Int: ExampleProtocol {
var description: String { "The number is \(self)"}
mutating func increment() {
self += 1
}
}
var num = 10
print(num.description) // The number is 10
num.increment()
print(num.description) // The number is 11
We are extending the Int
type to have a property description
which has a read-only getter and a method increment()
to increment its value by 1.
Enumeration
CompassPoint
enum uses multiple protocols Int
and CaseIterable
.
Int
type is used to implicitly fill the without values with default values. It also provides access to the raw value of the cases by exposing rawValue
property.
CaseIterable
type is used to expose allCases
which can be used to iterate over all the cases in the enum.
Unlike Typescript where an enum
only supports constant values, an enum
in Swift can store any type of data and also provides a feature to write methods inside them.
enum CompassPoint: Int, CaseIterable {
case north
case south
case east
case west
}
var directionToHead = CompassPoint.north
switch directionToHead {
case .north:
print("North")
case .south:
print("South")
case .east:
print("East")
case .west:
print("West")
}
for direction in CompassPoint.allCases {
print(direction.rawValue)
}
When using switch
and enum
together, if all the cases are not addressed then a compile-time error will be thrown. When it isn’t appropriate to provide a case
for every enumeration case, you can provide a default
case to cover any cases that aren’t addressed explicitly.
Concurrency
Swift has built-in support for writing asynchronous and parallel code in a structured way.
There are no promises here.
Asynchronous code is written by defining asynchronous functions. To indicate that a function or method is asynchronous, we write the async
keyword in its declaration after its parameters.
func fetchUserId(from server: String) async -> Int {
if server == "primary" {
return 97
}
return 501
}
func fetchUsername(from server: String) async -> String {
let userId = await fetchUserId(from: server)
if userId == 97 {
return "John"
}
return "Guest"
}
// Use Task to call asynchronous functions from synchronous code, without waiting for them to return.
Task {
let username = await fetchUsername(from: "primary")
// print(username)
}
Calling Asynchronous Functions in Sequence
let firstPhoto = await downloadPhoto(named: photoNames[0])
let secondPhoto = await downloadPhoto(named: photoNames[1])
let thirdPhoto = await downloadPhoto(named: photoNames[2])
let photos = [firstPhoto, secondPhoto, thirdPhoto]
In this case, await
is added to every downloadPhoto()
function, the code will wait and execute each downloadPhoto()
function one after the other and finally stores all the results into photos
variable.
Calling Asynchronous Functions in Parallel
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
In this case, async
is added to every downloadPhoto()
function, the code will not wait and execute each downloadPhoto()
function one after the other and finally will wait for all the results and store the results into photos
variable.