Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Bug-Free by Design: Crafting Swift Code That Do...

Bug-Free by Design: Crafting Swift Code That Doesn't Sting

This talk is from NSSpain 2023. Discover pragmatic techniques to elevate your Swift code, diminish the risk of bugs, and amplify user satisfaction. Dive into actionable code improvement tips that bridge the gap between code quality and tangible app improvement, ensuring a smoother, faster, and more delightful user experience.
Find more tips on https://ioscodereview.com

Marina Vatmakhter

September 26, 2023
Tweet

More Decks by Marina Vatmakhter

Other Decks in Programming

Transcript

  1. 2nd time at NSSpain 👋 Professionally enjoying iOS development since

    2011 Worked at: Citrix, Storytel, Novo Nordisk Marina Vatmakhter 
 @hybridcattt
  2. What is "good code"? Readable Maintainable Testable Nice to look

    at ...? 3 Correct Robust Performant Serves better UX
  3. 💪 Strong typing let string = "hello NSSpain" var myNumber

    = 2023 myNumber = string // error: Cannot assign value of type 'String' to type 'Int'
  4. ❓Optional var myNumber: Int? = nil print(myNumber + 5) //

    error: Value of optional type 'Int?' must be unwrapped print(myNumber ?? 0) if let myNumber { doA() } else { doB() }
  5. 💂 Guard func printNonZeroNumber() { var myNumber = 5 guard

    myNumber != 0 else { print("not good") // error: 'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope } print(myNumber) } func printNonZeroNumber() { var myNumber = 5 if myNumber == 0 { print("not good") // mistake: falls through } print(myNumber) }
  6. 💂 Guard func printNonZeroNumber() { var myNumber = 5 guard

    myNumber != 0 else { print("not good") return } print(myNumber) } func printNonZeroNumber() { var myNumber = 5 if myNumber == 0 { print("not good") // mistake: falls through } print(myNumber) }
  7. 👌 Let let myNumber = 0 myNumber = 2 //

    error: Cannot assign to value: 'myNumber' is a 'let' constant let myNumber: Int myNumber = 1 myNumber = 2 // error: Immutable value 'myNumber' may only be initialized once 🫡 Late Let
  8. 🛂 Access control final class AccessControlExample { private(set) var myNumber:

    Int = 0 func insideFunction() { print(myNumber) myNumber = 5 } } func outsideFunction() { let x = AccessControlExample() print(myNumber) x.myNumber = 5 // error: Cannot assign to property: 'myNumber' setter is inaccessible }
  9. 👯 Enum final class ModelWithState { private var isLoading: Bool

    = false private var data: [Int] = [] private var lastError: Error? = nil } 23 permutations
  10. 👯 Enum enum State { case idle case loading case

    data([Int]) case error(Error) } final class ModelWithState { private var state = State.idle } switch state { // error: Switch must be exhaustive case .loading: print("loading") case .data(let data): print("data \(data)") case .error(let error): print("error \(error)") }
  11. 👋 Defer func f(x: Int) { defer { print("1st defer")

    } print("End of function") } f(x: 5) 
 // Prints "End of function" // Prints "1st defer" 

  12. 👋 Defer func f(x: Int) { defer { print("1st defer")

    } if x < 10 { defer { print("2nd defer") } print("End of if") } print("End of function") } f(x: 5) // Prints "End of if" // Prints "2nd defer" // Prints "End of function" // Prints "1st defer"
  13. func renderImage() -> UIImage? { UIGraphicsBeginImageContextWithOptions( 
 CGSize(width: 100, height:

    100), true, 0.0 
 ) defer { UIGraphicsEndImageContext() } let context = UIGraphicsGetCurrentContext() context?.setFillColor(UIColor.red.cgColor) ... let image = UIGraphicsGetImageFromCurrentImageContext() return image }
  14. func writeLog() { let file = openNewFile() defer { closeFile(file)

    } print("writing all the logs here...") ... ... }
  15. func loadData() async throws -> UserData { isLoading = true

    defer { isLoading = false } let user = try await Loader().getUser() return user }
  16. func write(data: Data) throws { // ensuring the app isn't

    suspended while writing to disk let bgId = UIApplication.shared.beginBackgroundTask() defer { UIApplication.shared.endBackgroundTask(bgId) } do { try data.write(to: URL(string: "filepath")!, options: .atomic) } catch { print("\(#function) - \(error.localizedDescription)") throw error } }
  17. final class MyView: UIView { enum State { case idle

    case loading case data(String) } private let spinner: UIActivityIndicatorView = .init(style: .large) private let dataLabel: UILabel = .init() private let errorLabel: UILabel = .init() private var state: State = .idle }
  18. private func update() { switch state { case .idle: spinner.isHidden

    = true dataLabel.isHidden = true errorLabel.isHidden = true case .loading: spinner.isHidden = false dataLabel.isHidden = true errorLabel.isHidden = true case .data(let str): dataLabel.text = str if str.isEmpty { spinner.isHidden = true dataLabel.isHidden = false errorLabel.isHidden = true } else { spinner.isHidden = true dataLabel.isHidden = true errorLabel.text = "No data" } } } private func update() { switch state { case .idle: spinner.isHidden = true dataLabel.isHidden = true errorLabel.isHidden = true case .loading: spinner.isHidden = false dataLabel.isHidden = true errorLabel.isHidden = true case .data(let str): dataLabel.text = str if !str.isEmpty { spinner.isHidden = true dataLabel.isHidden = false errorLabel.isHidden = true } else { spinner.isHidden = true dataLabel.isHidden = true errorLabel.isHidden = false errorLabel.text = "No data" } } } 🤯
  19. private func update2() { let showSpinner: Bool let dataText: String?

    let errorText: String? switch state { case .idle: showSpinner = false dataText = nil errorText = nil case .loading: showSpinner = true dataText = nil errorText = nil case .data(let str): if str.isEmpty { showSpinner = false errorText = "No data" dataText = nil } else { showSpinner = false dataText = str errorText = nil } } spinner.isHidden = !showSpinner errorLabel.text = errorText errorLabel.isHidden = errorText == nil dataLabel.text = dataText dataLabel.isHidden = dataText == nil }
  20. private func update() { let showSpinner: Bool 
 let dataText:

    String? 
 let errorText: String? switch state { 
 case .idle: ... 
 case .loading: ... case .data(let data): if str.isEmpty { 
 errorText = "No data" 
 dataText = nil 
 } else { 
 showSpinner = false 
 dataText = str 
 errorText = nil 
 } 
 } spinner.isHidden = !showSpinner // error: Constant 'showSpinner' used before being initialized dataText = "oops" // error: Immutable value 'dataText' may only be initialized once 
 ... 
 } 
 showSpinner is not set
  21. func load(url: URL, completion: @escaping (Result<Data, RequestError>) -> ()) {

    URLSession.shared.dataTask(with: url) { data, response, error in if let error { completion(.failure(.responseError(error))) return } guard let data = data else { DispatchQueue.main.async { completion(.failure(.noData)) } return } guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else { return } completion(.success(data)) }.resume() } Missing completion No switching to main No switching to main
  22. func load(url: URL, completion: @escaping (Result<Data, RequestError>) -> ()) {

    URLSession.shared.dataTask(with: url) { data, response, error in let result: Result<Data, RequestError> defer { DispatchQueue.main.async { completion(result) } } if let error { result = .failure(.responseError(error)) return } guard let data = data else { result = .failure(.noData) return } guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else { result = .failure(.httpError) return } result = .success(data) }.resume() }
  23. func load(url: URL, completion: @escaping (Result<Data, RequestError>) -> ()) {

    URLSession.shared.dataTask(with: url) { data, response, error in let result: Result<Data, RequestError> defer { // error: Constant 'result' used before being initialized DispatchQueue.main.async { completion(result) } } if let error { result = .failure(.responseError(error)) return } guard let data = data else { // result = .failure(.noData) return } guard let response = response as? HTTPURLResponse, (200 ..< 300) ~= response.statusCode else { result = .failure(.httpError) return } result = .success(data) }.resume() }
  24. let userId = "42" let orderId = "42" func findUser(id:

    String) {} func findOrder(id: String) {} findUser(id: orderId) Compiles
  25. typealias UserID = String typealias OrderID = String let userId

    = "42" let orderId = "42" func findUser(id: UserID) {} func findOrder(id: OrderID) {} findUser(id: orderId) Compiles
  26. func findUser(id: UserID) {} func findOrder(id: OrderID) {} let userId

    = UserID(rawValue: "42") let orderId = OrderID(rawValue: "42") findUser(id: orderId) // error: Cannot convert value of type 'OrderID' to expected argument type 'UserID' Compilation 
 error
  27. protocol ID: RawRepresentable, Decodable { var rawValue: String { get

    } init(rawValue: String) } extension ID { init(_ rawValue: String) { self.init(rawValue: rawValue) } } struct UserID: ID { 
 var rawValue: String 
 } struct OrderID: ID { 
 var rawValue: String 
 } 
 let id = UserID("123")
  28. struct User: Decodable { let id: String let orders: [String]

    let name: String } extension ID { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let id = try container.decode(String.self) self.init(id) } }
  29. struct User: Decodable { let id: UserID let orders: [OrderID]

    let name: String } extension ID { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let id = try container.decode(String.self) self.init(id) } }
  30. Bene f its of typesafe identi f iers Compile-time guarantees

    Self-documenting Intuitive API design Avoid confusion Refactoring safety Better code completion
  31. enum ImageAsset: String, CaseIterable { case headIcon = "head_icon" case

    worldIcon = "world_icon" var image: UIImage? { UIImage(named: rawValue) } } enum SFSymbol: String, CaseIterable { case heart = "heart" case arrowFilled = "arrowFilled" var image: UIImage? { UIImage(systemName: rawValue) } }
  32. func test() { ImageAsset.allCases.forEach { XCTAssertNotNil($0.image, "Image \($0) is missing")

    } SFSymbol.allCases.forEach { XCTAssertNotNil($0.image, "Image \($0) is missing") } }
  33. class BigScaryClass { // ... 100 lines of code static

    var userID: Int = -1 // ... 10000 more lines of code } // elsewhere loadUser(BigScaryClass.userID) Loading user with id -1
  34. class BigScaryClass { // ... 100 lines of code static

    var userID: Int? = nil // ... 10000 more lines of code } // elsewhere loadUser(BigScaryClass.userID) // error: 
 Value of optional type 'Int?' must be unwrapped
  35. class BigScaryClass { // ... 100 lines of code static

    var userID: Int? = nil // ... 10000 more lines of code } // elsewhere guard let userID = BigScaryClass.userID else { print("no user - no loading") return } loadUser(userID)
  36. class BigScaryClass { // ... 100 lines of code static

    private(set) var userID: Int? = nil // ... 10000 more lines of code } // elsewhere BigScaryClass.userID = nil // error: Cannot assign 
 to property: 'userID' setter is inaccessible
  37. class BigScaryClass { // ... 100 lines of code static

    private(set) var userID: Int? = nil static func resetUserID() { userID = nil } // ... 10000 more lines of code } // elsewhere BigScaryClass.resetUserID()
  38. Compile-time checks 🫶 🫶 🫶 • Refactoring messy codebases •

    Operating with IDs safely • No missing assets • Declarative UI updates • No memory leaks • No hanging spinners