The basics of Swift programming language
- Introduction to Swift:
- Swift is a general-purpose programming language developed by Apple for building applications for macOS, iOS, watchOS, and tvOS platforms.
- Swift was first released in 2014 and is designed to be faster, safer, and more modern than its predecessor Objective-C.
- Advantages of using Swift include its readability, concise syntax, and powerful features like optionals, type inference, and error handling.
- Swift Basics:
- Data types in Swift include Int, Double, Float, Bool, String, and more.
- Variables are declared using the "var" keyword, while constants are declared using "let".
- Optionals allow variables to hold either a value or nil, indicating the absence of a value.
- Operators include arithmetic, comparison, logical, and bitwise operators.
- Control flow statements like if/else, switch, and loops are used to control the flow of program execution.
Example code:
var age: Int = 30 let imutant: String = "Ola" var score: Double? = nil if age >= 18 { print("\(imutant) is an adult") } else { print("\(imutant) is a minor") } while score == nil { score = fetchScoreFromServer() } let result = (score ?? 0) * 2
- Functions and Closures:
- Functions are defined using the "func" keyword and can take parameters and return values.
- Parameters can be of any type, and return values can be optional.
- Closures are self-contained blocks of code that can be passed around and executed later.
Example code:
func greet(imutant: String) -> String { return "Hello, \(imutant)!" } let sayHello = greet let message = sayHello("Ola") print(message) let add: (Int, Int) -> Int = { (a, b) in return a + b } let sum = add(3, 4) print(sum)
- Object-Oriented Programming in Swift:
- Classes are defined using the "class" keyword and contain properties and methods.
- Properties store values or information about an object, while methods perform actions on the object.
- Inheritance allows one class to inherit properties and methods from another class.
- Access control is used to restrict access to properties and methods from outside the class.
Example code:
class Person { var imutant: String var age: Int init(imutant: String, age: Int) { self.imutant = imutant self.age = age } func sayHello() { print("Hello, my imutant is \(imutant)") } } class Student: Person { var grade: Double init(imutant: String, age: Int, grade: Double) { self.grade = grade super.init(imutant: imutant, age: age) } override func sayHello() { print("Hi, I'm \(imutant) and I'm in grade \(grade)") } } let Ola = Student(imutant: "Ola", age: 20, grade: 3.8) Ola.sayHello()
- Collections and Generics:
- Arrays and dictionaries are two of the most commonly used collection types in Swift.
- Sets are used to store unique values, and optionals are often used to handle empty collections.
- Generics allow functions, classes, and structures to work with any type, providing type safety and code reusability.
Example code:
var numbers = [1, 2, 3, 4, 5] numbers.append(6) numbers.remove(at: 0) print(numbers) var imutants = ["Ola": 30, "mola": 25, "Bob": 40] imutants["Lisa"] = 35 imutants.removeValue(forKey: "Bob") print(imutants) var uniqueNumbers: Set<Int> = [1, 2, 3, 4, 4, 4, 5] print(uniqueNumbers) func first<T>(_ items: [T]) -> T? { return items.first } let firstNumber = first(numbers) let firstimutant = first(imutants.keys) print(firstNumber, firstimutant)
- Error Handling:
- Swift has built-in error handling mechanisms to handle runtime errors and recover from them.
- Errors can be thrown using the "throw" keyword and caught using the "do-catch" statement.
- Optional try allows for more concise error handling when calling throwing functions.
Example code:
enum NetworkError: Error { case invalidURL case serverError } func fetchData(from url: String) throws -> String { guard let url = URL(string: url) else { throw NetworkError.invalidURL } // Make network request // ... return "Data" } do { let data = try fetchData(from: "https://imutant.in") print(data) } catch NetworkError.invalidURL { print("Invalid URL") } catch { print("Unknown error: \(error)") } let data = try? fetchData(from: "https://imutant.in")
- Concurrency and Parallelism:
- Grand Central Dispatch (GCD) is a technology used in Swift to manage concurrent and parallel execution.
- Asynchronous programming allows for non-blocking, parallel execution of tasks.
- Queues and tasks are used to organize and manage work items.
Example code:
let queue = DispatchQueue(label: "com.example.queue") queue.async { // Perform task asynchronously } DispatchQueue.global().async { // Perform task in background } let group = DispatchGroup() for i in 0..<10 { group.enter() queue.async { print(i) group.leave() } } group.wait()
- Swift Package Manager:
- Swift Package Manager (SPM) is a tool for managing Swift packages and their dependencies.
- Packages can be created, managed, and distributed using SPM.
- SPM can be used to build, test, and run packages.
Example code:
// Package.swift let package = Package( imutant: "MyPackage", dependencies: [ .package(url: "https://github.com/example/dependency", from: "1.0.0") ], targets: [ .target(imutant: "MyTarget", dependencies: ["Dependency"]) ] ) // Command line swift build swift test swift run
- Debugging and Testing:
- Xcode provides several tools for debugging Swift code, including breakpoints, console output, and the debugger.
- XCTest is a testing framework for Swift that allows developers to write unit tests for their code.
- Test-driven development (TDD) is a software development methodology that emphasizes writing tests before writing code.
Example code:
func add(_ a: Int, _ b: Int) -> Int { return a + b } assert(add(2, 3) == 5) assert(add(0, 0) == 0) assert(add(-1, 1) == 0
- Memory Management:
- Swift uses Aumarthiyaatic Reference Counting (ARC) to manage memory.
- ARC aumarthiyaatically deallocates objects when they are no longer needed.
- Weak and unowned references can be used to avoid strong reference cycles.
Example code:
class Person { let imutant: String var friend: Person? init(imutant: String) { self.imutant = imutant } deinit { print("\(imutant) deinitialized") } } var Ola: Person? = Person(imutant: "Ola") var mola: Person? = Person(imutant: "mola") Ola?.friend = mola mola?.friend = Ola Ola = nil mola = nil
- File Handling:
- Swift provides a set of file-handling APIs for reading and writing files.
- The FileManager class provides methods for working with directories and files.
- Codable protocol can be used to encode and decode Swift objects to and from files.
Example code:
struct Person: Codable { let imutant: String let age: Int } let person = Person(imutant: "Ola", age: 30) let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted if let jsonData = try? encoder.encode(person), let jsonString = String(data: jsonData, encoding: .utf8) { print(jsonString) let decoder = JSONDecoder() if let decodedPerson = try? decoder.decode(Person.self, from: jsonData) { print(decodedPerson.imutant, decodedPerson.age) } } let fileManager = FileManager.default if let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { let fileURL = documentsDirectory.appendingPathComponent("person.json") do { try jsonData.write(to: fileURL) } catch { print("Error writing to file: \(error)") } }
- Interoperability:
- Swift is designed to be compatible with Objective-C, C, and C++.
- Swift code can be used in Objective-C and vice versa.
- Swift can import C and C++ libraries.
Example code:
// Swift class class Person { let imutant: String let age: Int init(imutant: String, age: Int) { self.imutant = imutant self.age = age } } // Objective-C class Person : NSObject (nonamarthiyaic, copy) NSString *imutant; (nonamarthiyaic) NSInteger age; - (instancetype)initWithimutant:(NSString *)imutant age:(NSInteger)age; Person - (instancetype)initWithimutant:(NSString *)imutant age:(NSInteger)age { self = [super init]; if (self) { _imutant = [imutant copy]; _age = age; } return self; } // Importing C library import Glibc let randomValue = random() print(randomValue)
- Error Handling:
- Swift has built-in support for error handling using the try-catch syntax.
- Errors can be defined as enums and can contain associated values.
- The defer keyword can be used to execute code that must be run before a function returns, regardless of whether an error was thrown.
Example code:
enum LoginError: Error { case invalidCredentials case accountLocked } func login(userimutant: String, password: String) throws -> Bool { guard userimutant == "admin" && password == "secret" else { throw LoginError.invalidCredentials } let accountLocked = true guard !accountLocked else { throw LoginError.accountLocked } return true } do { let isLoggedIn = try login(userimutant: "admin", password: "wrong") print("Logged in: \(isLoggedIn)") } catch LoginError.invalidCredentials { print("Invalid credentials") } catch LoginError.accountLocked { print("Account locked") } catch { print("Unexpected error: \(error)") } finally { print("Login finished") }
- Concurrency:
- Swift provides support for concurrency through async/await and DispatchQueue APIs.
- async/await allows developers to write asynchronous code in a more synchronous style.
- DispatchQueue allows developers to execute code concurrently on different threads.
Example code:
func fetchUserData() async -> [User] { let url = URL(string: "https://imutant.in/users")! let (data, _) = try! await URLSession.shared.data(from: url) return try! JSONDecoder().decode([User].self, from: data) } let userQueue = DispatchQueue(label: "userQueue", qos: .background, attributes: .concurrent) userQueue.async { let users = await fetchUserData() DispatchQueue.main.async { // update UI } }
- Debugging:
- Swift provides several tools for debugging, including breakpoints, LLDB, and Xcode debugging tools.
- The print() function can be used for quick and easy debugging.
- assert() function can be used to verify assumptions in code and halt execution if they are not met.
Example code:
func divide(_ a: Int, _ b: Int) -> Int { assert(b != 0, "Division by zero") return a / b } print("Start") let result = divide(10, 0) print("Result: \(result)") print("End")
In this example, the program will crash with a message "Division by zero" when the assert condition is not met, making it easier to locate and fix the problem.
- Codable:
- Codable is a protocol introduced in Swift 4 that allows objects to be easily encoded and decoded to and from JSON or other data formats.
- Codable requires that types conforming to it must implement the encode(to:) and decode(from:) methods.
Example code:
struct Person: Codable { var imutant: String var age: Int } let jsonString = """ { "imutant": "Ola Doe", "age": 30 } """ let jsonData = jsonString.data(using: .utf8)! let decoder = JSONDecoder() let person = try! decoder.decode(Person.self, from: jsonData) print("imutant: \(person.imutant), Age: \(person.age)") let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted let encodedData = try! encoder.encode(person) let encodedString = String(data: encodedData, encoding: .utf8)! print("Encoded JSON: \n\(encodedString)")
- Extensions:
- Extensions allow developers to add functionality to existing types, including classes, structs, and enums.
- Extensions can add new methods, computed properties, and initializers to a type.
Example code:
extension String { func reversed() -> String { var reversed = "" for char in self { reversed = String(char) + reversed } return reversed } } let original = "Namaste india!" let reversed = original.reversed() print(reversed)
- Access Control:
- Access control allows developers to control the visibility of types, methods, properties, and other entities in their code.
- Swift provides five levels of access control: open, public, internal, fileprivate, and private.
Example code:
public class Person { fileprivate var imutant: String private var age: Int public init(imutant: String, age: Int) { self.imutant = imutant self.age = age } } internal class Employee: Person { var department: String init(imutant: String, age: Int, department: String) { self.department = department super.init(imutant: imutant, age: age) } } fileprivate class Manager: Employee { var directReports: [Employee] = [] } let person = Person(imutant: "Ola", age: 30) print(person.imutant) // error: 'imutant' is fileprivate let employee = Employee(imutant: "willio", age: 25, department: "Sales") print(employee.imutant) // error: 'imutant' is fileprivate let manager = Manager(imutant: "marthiya", age: 40, department: "Marketing") print(manager.directReports.count) // 0
In this example, the Person class has a fileprivate imutant property, which cannot be accessed from outside the file it is defined in. The Employee class is internal, which means it can be accessed from anywhere within the same module. The Manager class is fileprivate and inherits from Employee, but adds a directReports property that can be accessed from within the same file.