My journey through learning swift as a flutter developer
About Me
Click to expand!
If you are already familiar with one programming language, learning a new programming language becomes a little easy. Since most of the concepts are common between programming languages i.e (variable, functions, loops, conditional statements etc). So the base concepts are same. All you need to learn is syntax. Same in my case. By the time I started learning swift I already had 1.5 years of experience with dart. For me it was very easy and interesting to learn and compare swift with dart. Following is my journey of learning swift as a flutter developer. I hope my journey will ease your learning of swift.
Swift is type-safe language. var
keyword assigns the type. Unlike Javascript, types are not interchangeable.
var str = "greetings"
str = 7 // error
Swift lets you use underscores as thousands separators _ they don’t change the number, but they do make it easier to read. For example:
var population = 8_000_000 // 8000000
It makes the numbers easier to read. (sugar syntax)
To create String use double quotes (””).
var str = "Hello world"
To create multiline string use double quotes thrice(“””). quotes should always be in new line.
var multiline = """
this is cute
multiline
string
"""
If you only want multi-line strings to format your code neatly, and you don’t want those line breaks to actually be in your string, end each line with a \
, like this:
var str2 = """
This goes \
over multiple \
lines
"""
Tip: Sometimes you will want to have long strings of text in your code without using multiple lines, but this is quite rare. Specifically, this is most commonly important if you plan to share your code with others – if they see an error message in your program they might want to search your code for it, and if you’ve split it across multiple lines their search might fail.
Want to add variable inside string? Simply add backslash and put variable inside round brackets.
var score = 85
var str = "Your score was \(score)" // Your score was 85
struct Person {
var type: String
var action: String
}
extension String.StringInterpolation {
mutating func appendInterpolation(_ person: Person) {
appendLiteral("I'm a \(person.type) and I'm gonna \(person.action).")
}
}
let hater = Person(type: "hater", action: "hate")
print("Status check: \(hater)")
For more: super powered string interpolation in swift 5.0
Save floating points
var floating = 2.099
var myInteger = 6 // Note that this is interger type not double
var myDouble = 6.0 // this is type double
As before, data types in swift are not interchangeable. Similarly we cannot perform operations
var total = myInteger + myDouble // error
Saves either true
or false
.
var awesome = true
var isLoaded = 1 // this is not type boolean
var
is a mutable variable. Value in variable with var
can be changed over time.
let
is and immutable variable. Value in variable with let
cannot be changed once assigned. in dart or c++ it is refered as const
.
If you try to change value in let
, Xcode will refuse to run your code. It’s a form of safety: when you use constants you can no longer change something by accident.
When you write your own Swift code, you should always use let
unless you specifically want to change a value. In fact, Xcode will warn you if you use var
then don’t change the variable.
let
, var
also accepts late assignment. Means, declare first and assign later. make sure to assign before using it. btw compiler is intelligent enough to point out.
var awesome = true
awesome = false
let awesome = true
awesome = false // error
Type inference refers to the automatic detection of the type of an expression in a formal language. But some time it is better to define types
let album: String = "Reputation"
let year: Int = 1989
let height: Double = 1.78
let taylorRocks: Bool = true
let customType: CustomType = CustomType()
The answer to the first question is primarily one of three reasons:
- Swift can’t figure out what type should be used.
- You want Swift to use a different type from its default.
- You don’t want to assign a value just yet.
Sequence of same type values. It is a value type data structure.
// 1d
let beatles : [String] = ["john", "paul", "george", "ringo"]
beatles[0] // "john"
// 2d
let beatles : [[String]] = [["john"], ["paul"], ["george"], ["ringo"]]
beatles[0][0] // "john"
beatles[0][1] // Error index out of range
Like array but with 2 conditions
- no order
- no duplicates
let colors2 = Set(["red", "green", "blue", "red", "blue"]) // {"green", "blue", "red"}
Tuples allow you to store several values together in a single value. That might sound like arrays, but tuples are different:
- You can’t add or remove items from a tuple; they are fixed in size.
- You can’t change the type of items in a tuple; they always have the same types they were created with.
- You can access items in a tuple using numerical positions or by naming them, but Swift won’t let you read numbers or names that don’t exist.
Tuples are created by placing multiple items into parentheses, like this:
var name = (first: "Taylor", last: "Swift")
print(name.0) // Taylor
For more: Why is it any different from class and structure?
Key value pair of data collection. i.e (json like sructure), (refered as maps in dart).
let heights = [
"Taylor Swift": 1.78,
"Ed Sheeran": 1.73
]
let heights : [String: Double] = [
"Taylor Swift": 1.78,
"Ed Sheeran": 1.73
]
heights["Taylor Swift", default: 1.78]
// ARRAY
var results = [Int]()
var results = Array<Int>()
// SET
var words = Set<String>()
// DICTIONARY
var teams = [String: String]()
var scores = Dictionary<String, Int>()
Enumerations – usually called just enums – are a way of defining groups of related values in a way that makes them easier to use.
enum Result {
case success
case failure
}
enum Planet: Int {
case mercury = 1
case venus
case earth
case mars
}
// Now Swift will assign 1 to mercury and count upwards from there, meaning that earth is now the third planet.
Enums are very powerful in swift. Read more: https://www.avanderlee.com/swift/enumerations/
Name | operator |
---|---|
Add | + |
Subtract | - |
Multiply | * |
Divide | / |
Remainder / Modulo | % |
nil
is like null
in dart. It is a special value that represents an absence of a value.
var count : Int? = nil
// usuall way
if count == nil {
print("count is nil")
}else {
print("count has \(count)")
}
// swift's way
// this let us un wrap the value from optionals
if let nonNullCount = count {
// nonNullCount is only available in this scope
print("count has \(nonNullCount)")
} else {
print("count is nil")
}
// similarly
func greet(_ name: String?) {
// guard let you make the variable non null
// and can be used later down in the scope.
guard let unwrapped = name else {
print("You didn't provide a name!")
return
}
print("Hello, \(unwrapped)!")
}
force unwrapping: force non null using !
Implicitly unwrapped optionals
Implicitly unwrapped optionals can contain a value or be nil, and regular optionals can also contain a value or be nil, but there’s a subtle difference: implicitly unwrapped optionals don’t need to be unwrapped to be used. This means if you attempt to use an implicitly unwrapped optional and it’s actually nil, your code will just crash – Swift won’t make you use if let
or similar like it would with regular optionals. Althought try to avoid this as much as possible but this is a very useful feature, because it means you can use an implicitly unwrapped optional without having to worry about whether it’s nil or not.
let age: Int! = nil
print(age) // nil
let age2: Int = age // error because I implicitly make the age non nil, but in actual, value in age is nil and age2 demands non nilable type
Nil coalescing
let user = username(for: 15) ?? "Anonymous"
Optional chaining
let beatle = names.first?.uppercased()
try catch
// if function returns nil instead of throwing error with this syntax
if let result = try? checkPassword("password") {
print("Result was \(result)")
} else {
print("D'oh.")
}
// if null then code crash
try! checkPassword("sekrit")
print("OK!")
fail-able constructor
struct Person {
var id: String
init?(id: String) {
if id.count == 9 {
self.id = id
} else {
return nil
}
}
}
type-casting
if let dog = pet as? Dog {
dog.makeNoise()
}
if firstCard + secondCard == 2 {
print("Aces – lucky!")
} else if firstCard + secondCard == 21 {
print("Blackjack!")
} else {
print("Regular cards")
}
if age1 > 18 && age2 > 18 {
print("Both are over 18")
}
if age1 > 18 || age2 > 18 {
print("At least one is over 18")
}
if (isOwner == true && isEditingEnabled) || isAdmin == true {
print("You can delete this post")
}
print(firstCard == secondCard ? "Cards are the same" : "Cards are different")
Swift will only run the code inside each case. If you want execution to continue on to the next case, use the fallthrough
keyword like this:
switch weather {
case "rain":
print("Bring an umbrella")
case "snow":
print("Wrap up warm")
case "sunny":
print("Wear sunscreen")
fallthrough
default:
print("Enjoy your day!")
}
Swift gives us two ways of making ranges: the ..<
and ...
operators. The half-open range operator, ..<
, creates ranges up to but excluding the final value, and the closed range operator, ...
, creates ranges up to and including the final value.
For example, the range 1..<5
contains the numbers 1, 2, 3, and 4, whereas the range 1...5
contains the numbers 1, 2, 3, 4, and 5.
Ranges are helpful with switch
blocks, because you can use them for each of your cases. For example, if someone sat an exam we could print different messages depending on their score:
let score = 85
switch score {
case 0..<50:
print("You failed badly.")
case 50..<85:
print("You did OK.")
default:
print("You did great!")
}
let count: ClosedRange<Int> = 1...10
let count: Range<Int> = 1..<10
More on https://www.avanderlee.com/swift/ranges-explained/
for number in count {
print("Number is \(number)")
}
// use _ if variable is unused
for _ in 1...5 {
print("play")
}
while number <= 20 {
print(number)
number += 1
break
}
repeat like a ⇒ do while
repeat {
print(number)
number += 1
} while number <= 20
exiting multiple loops
outerLoop: for i in 1...10 {
for j in 1...10 {
let product = i * j
print ("\(i) * \(j) is \(product)")
if product == 50 {
print("It's a bullseye!")
break outerLoop
}
}
}
skipping items ⇒ use continue
for i in 1...10 {
if i % 2 == 1 {
continue
}
print(i)
}
func printHelp() {
print(message)
}
func printHelp() -> Void {
print(message)
}
parameters / arguments
func square(number: Int) {
print(number * number)
}
square(number: 8)
parameter labels
Swift lets us provide two names for each parameter: one to be used externally when calling the function, and one to be used internally inside the function.
func sayHello(to name: String) {
print("Hello, \(name)!")
}
Note: swift does not support positional parameters, it strictly uses named parameters.
to make positional use hyphen _
func print(_ name: String, _ age : Int) {
}
print("hello" , 23)
default parameters
func print(_ name: String, _ age : Int = 23) {
}
print("hello")
return type
func square(number: Int) -> Int {
return number * number
}
let result = square(number: 8)
print(result)
one line code does not require to write return
func greet(name: String) -> String {
"Oh wow!"
}
Variadic functions
Swift’s variadic functions let us accept any number of parameters of the same type, separated by a comma.
func square(numbers: Int...) {
for number in numbers {
print("\(number) squared is \(number * number)")
}
}
square(numbers: 1, 2, 3, 4, 5)
// 1 squared is 1
// 2 squared is 4
// 3 squared is 9
// 4 squared is 16
// 5 squared is 25
function to throw error
enum PasswordError: Error {
case obvious
}
func checkPassword(_ password: String) throws -> Bool {
if password == "password" {
throw PasswordError.obvious
}
return true
}
Note: Function can only throw
errors of type Error
that is why we extended PasswordError
from Error
. Error in other words Exception.
More on: https://www.donnywals.com/working-with-throwing-functions-in-swift/
Handle throwing errors
do {
try checkPassword("password")
print("That password is good!")
} catch {
print("You can't use that password.")
}
mutable and immutable parameters
by default parameters are constant type. but if you want mutable types then use inout
func doubleInPlace(number:inout Int) {
number *= 2
}
var myNum = 10
doubleInPlace(number: &myNum)
Create a function and assign to a variable.
let driving = {
print("I'm driving in my car")
}
driving()
// type annotation
let typedVariable : () -> () = driving
accepting parameters
Note: closures does not have named parameters
let driving = { (place: String) in
print("I'm going to \(place) in my car")
}
driving("London")
// type annotation
let typedVariable : (String) -> () = driving
returning value
let drivingWithReturn = { (place: String) -> String in
return "I'm going to \(place) in my car"
}
let message = drivingWithReturn("London")
print(message)
// type annotation
let typedVariable : (String) -> String = drivingWithReturn
Closures as parameters
// let driving = { () -> Void in
let driving = { () -> () in
print("I'm going to in my car")
}
// func travel(action: () -> Void) {
func travel(action: () -> ()) {
print("I'm getting ready to go.")
action()
print("I arrived!")
}
travel(action: driving)
Trailing closure syntax
If the last parameter to a function is a closure, Swift lets you use special syntax called trailing closure syntax. Rather than pass in your closure as a parameter, you pass it directly after the function inside braces.
func travel(action: () -> Void) {
print("I'm getting ready to go.")
action()
print("I arrived!")
}
travel() {
print("I'm driving in my car")
}
travel {
print("I'm driving in my car")
}
A little complex example
func travel(action: (String) -> Void, action2: (String) -> Void) {
print("I'm getting ready to go.")
action("Car")
print("I arrived!")
}
func message(type : String) {
print("I'm driving \(type)")
}
// without Trailing closure syntax
travel(
action: { (str: String) -> Void in
print("I'm driving \(str)")
},
action2: message
)
// with Trailing closure syntax
travel { (str: String) -> Void in
print("I'm driving \(str)")
} action2: { (str: String) -> Void in
print("I'm driving \(str)")
}
example and use
func reduce(values : [Int], callback: (Int, Int)-> Int) -> Int {
// start with a total equal to the first value
var current = values[0]
// loop over all the values in the array, counting from index 1 onwards
for value in values[1...] {
// call our closure with the current value and the array element, assigning its result to our current value
current = callback(current, value)
}
// send back the final current value
return current
}
func add(a: Int, b: Int) -> Int {
return a + b
}
reduce(values: [1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 , 9, 10], callback: add)
More about function returning callback https://www.hackingwithswift.com/quick-start/understanding-swift/why-do-swifts-closures-capture-values
A structure is a way to group data together to form a single compound value.
struct Sport {
// store property
var name: String
var isOlympicSport: Bool
// computed property
var olympicStatus: String {
if isOlympicSport {
return "\(name) is an Olympic sport"
} else {
return "\(name) is not an Olympic sport"
}
}
}
property observers
struct Progress {
var task: String
var amount: Int {
didSet {
print("\(task) is now \(amount)% complete")
}
}
}
Function in struct is called method
struct City {
var population: Int
func collectTaxes() -> Int {
return population * 1000
}
}
preventing const properties to be able to mutate.
If a struct has a variable property but the instance of the struct was created as a constant, that property can’t be changed – the struct is constant, so all its properties are also constant regardless of how they were created.
The problem is that when you create the struct Swift has no idea whether you will use it with constants or variables, so by default it takes the safe approach: Swift won’t let you write methods that change properties unless you specifically request it.
When you want to change a property inside a method, you need to mark it using the mutating
keyword, like this:
struct Person {
var name: String
mutating func makeAnonymous() {
name = "Anonymous"
}
}
init() {
username = "Anonymous"
print("Creating a new user!")
}
Unlike others Strings in swift are structs not arrays
More on: https://www.hackingwithswift.com/articles/181/why-using-isempty-is-faster-than-checking-count-0
Instead of this
swift has self
.
class Dog {
var name: String
var breed: String
init(name: String, breed: String) {
self.name = name
self.breed = breed
}
}
- Classes do not come with synthesized memberwise initializers.
- One class can be built upon (“inherit from”) another class, gaining its properties and methods.
- Copies of structs are always unique, whereas copies of classes actually point to the same shared data.
- Classes have deinitializers, which are methods that are called when an instance of the class is destroyed, but structs do not.
- Variable properties in constant classes can be modified freely, but variable properties in constant structs cannot.
class Poodle: Dog {
init(name: String) {
super.init(name: name, breed: "Poodle")
}
}
Note: final
keyword does not allow you to change or override or extend the behaviour of the class.
final class Dog {
var name: String
var breed: String
init(name: String, breed: String) {
self.name = name
self.breed = breed
}
}
class Person {
var name = "John Doe"
// constructor
init() {
print("\(name) is alive!")
}
func printGreeting() {
print("Hello, I'm \(name)")
}
// destructor
deinit {
print("\(name) is no more!")
}
}
The final difference between classes and structs is the way they deal with constants. If you have a constant struct with a variable property, that property can’t be changed because the struct itself is constant.
However, if you have a constant class with a variable property, that property can be changed. Because of this, classes don’t need the mutating
keyword with methods that change properties; that’s only needed with structs.
This difference means you can change any variable property on a class even when the class is created as a constant – this is perfectly valid code:
class Singer {
var name = "Taylor Swift"}
let taylor = Singer()
taylor.name = "Ed Sheeran"print(taylor.name)
Protocols are like interfaces.
So, protocols let us create blueprints of how our types share functionality, then use those blueprints in our functions to let them work on a wider variety of data.
protocol Identifiable {
var id: String { get set }
}
struct User: Identifiable {
var id: String
}
func displayID(thing: Identifiable) {
print("My ID is \(thing.id)")
}
One protocol can inherit from another in a process known as protocol inheritance. Unlike with classes, you can inherit from multiple protocols at the same time before you add your own customisations on top.
protocol Payable {
func calculateWages() -> Int
}
protocol NeedsTraining {
func study()
}
protocol HasVacation {
func takeVacation(days: Int)
}
protocol Employee: Payable, NeedsTraining, HasVacation { }
Extensions allow you to add methods to existing types (class, structs, protocols etc), to make them do things they weren’t originally designed to do. But you cannot add properties. Only methods allowed
extension Int {
var isEven: Bool {
return self % 2 == 0
}
}
Sadly there is not such thing in swift. But here is an alternative. using protocol and extensions. protocol
creates a blueprint and extension
gives the default implementation. just like abstract classes.
protocol Identifiable {
var id: String { get set }
func identify()
}
extension Identifiable {
func identify() {
print("My ID is \(id).")
}
}
More on: https://developer.apple.com/videos/play/wwdc2015/408/
classes are reference types
structs are value type
note: Always make value types equatable.
MVC stands for Model View Controller. But in iOS it is also know as Massive View Controller due to the massive size of ViewController class over time.
communication happen between controller via delegates.
Delegate pattern is like Observer pattern. Difference is that In Observer there is One to Many (1-to-m) communication. And in delegates there is One to One (1-to-1) communication.
// Suppose this is a view
struct View {
var label : String {
didSet {
print("UI Updated: \(label)")
}
}
}
// Model that holds data
struct Model {
var data : String
}
// API that will end data
class Api {
var delegate : Delegate
init(delegate: Delegate){
self.delegate = delegate
}
func getData() {
delegate.onData(data: Model(data: "Awesome Data from API"))
}
}
// Delegate to observe onData arrived.
protocol Delegate {
func onData(data: Model)
}
class ViewController {
var view = View(label: "")
// loades the screen
func screendidLoad(){
print("Screen Loaded")
view.label = "NO DATA"
print("waiting for 5 sec")
let api = Api(delegate: self)
api.getData()
}
}
// Conforms delegate, wirte the implementaion on data arrival.
extension ViewController : Delegate {
func onData(data: Model) {
view.label = "HAS DATA: \(data.data)"
}
}
// main
var inctance = ViewController()
inctance.screendidLoad()
Output:
Screen Loaded
UI Updated: NO DATA
waiting for 5 sec
UI Updated: HAS DATA: Awesome Data from API
Box technique: https://holyswift.app/reactive-swift-the-boxing-technique
mvvm: https://github.com/borderfree/imdb-mvvm
Imperative is “How you do something” Declarative is “What you do”
https://www.youtube.com/watch?v=E7Fbf7R3x6I
Declarative way is generally a better approach for write easy to understand code.
Many bugs come from the changing state, and assignment (mutation) statements that perform changes “=” are often the root cause of all evil in universe.
the bigger takeaway that writing reliable software is all about properly managing complexity.
What we can do is we can take our imperative code and abstract it behind a declarative api. In fact many (if not all) declarative apis have some sort of underlying imperative implementation.
Note: good declarative approach should conforms with the mental model of the developer(human) rather than the operational model of the machine.
CREDITS