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

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

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. Bug-Free by Design
    Marina Vatmakhter


    @hybridcattt
    Crafting Swift Code That Doesn't Sting

    View full-size slide

  2. 2nd time at NSSpain 👋


    Professionally enjoying iOS development since 2011


    Worked at: Citrix, Storytel, Novo Nordisk
    Marina Vatmakhter

    @hybridcattt

    View full-size slide

  3. What is "good code"?
    Readable


    Maintainable


    Testable


    Nice to look at


    ...?
    3
    Correct


    Robust


    Performant


    Serves better UX

    View full-size slide

  4. Testing is nice


    but wouldn't it be nicer

    if bugs just didn't compile?

    View full-size slide

  5. Swift to the rescue

    View full-size slide

  6. Our building blocks
    Optional let
    enum
    Late let
    defer guard
    Strong typing
    Access control

    View full-size slide

  7. 💪 Strong typing
    let string = "hello NSSpain"


    var myNumber = 2023


    myNumber = string // error: Cannot assign value of type 'String' to
    type 'Int'


    View full-size slide

  8. ❓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()


    }

    View full-size slide

  9. 💂 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)


    }


    View full-size slide

  10. 💂 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)


    }


    View full-size slide

  11. 👌 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

    View full-size slide

  12. 🛂 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


    }

    View full-size slide

  13. 👯 Enum
    final class ModelWithState {


    private var isLoading: Bool = false


    private var data: [Int] = []


    private var lastError: Error? = nil


    }
    23 permutations

    View full-size slide

  14. 👯 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)")


    }

    View full-size slide

  15. 👋 Defer
    func f(x: Int) {


    defer { print("1st defer") }


    print("End of function")


    }
    f(x: 5)

    // Prints "End of function"


    // Prints "1st defer"

    View full-size slide

  16. 👋 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"


    View full-size slide

  17. 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


    }

    View full-size slide

  18. func writeLog() {


    let file = openNewFile()


    defer {


    closeFile(file)


    }


    print("writing all the logs here...")


    ...


    ...


    }

    View full-size slide

  19. func loadData() async throws -> UserData {


    isLoading = true


    defer {


    isLoading = false


    }




    let user = try await Loader().getUser()


    return user


    }

    View full-size slide

  20. 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


    }


    }

    View full-size slide

  21. Consistent UI updates

    View full-size slide

  22. 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


    }

    View full-size slide

  23. 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"


    }


    }


    }


    🤯

    View full-size slide

  24. 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


    }

    View full-size slide

  25. 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

    View full-size slide

  26. Completion handlers

    View full-size slide

  27. func load(url: URL, completion: @escaping (Result) -> ()) {




    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

    View full-size slide

  28. func load(url: URL, completion: @escaping (Result) -> ()) {


    URLSession.shared.dataTask(with: url) { data, response, error in


    let result: Result


    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()


    }

    View full-size slide

  29. func load(url: URL, completion: @escaping (Result) -> ()) {


    URLSession.shared.dataTask(with: url) { data, response, error in


    let result: Result


    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()


    }

    View full-size slide

  30. Safer Identi
    f
    iers

    View full-size slide

  31. let userId = "42"


    let orderId = "42"


    func findUser(id: String) {}


    func findOrder(id: String) {}


    findUser(id: orderId)
    Compiles

    View full-size slide

  32. typealias UserID = String


    typealias OrderID = String


    let userId = "42"


    let orderId = "42"


    func findUser(id: UserID) {}


    func findOrder(id: OrderID) {}


    findUser(id: orderId)
    Compiles

    View full-size slide

  33. 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

    View full-size slide

  34. 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")

    View full-size slide

  35. 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)


    }


    }

    View full-size slide

  36. 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)


    }


    }

    View full-size slide

  37. Bene
    f
    its of typesafe identi
    f
    iers
    Compile-time guarantees


    Self-documenting


    Intuitive API design


    Avoid confusion


    Refactoring safety


    Better code completion

    View full-size slide

  38. Safer Assets

    View full-size slide

  39. 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) }


    }

    View full-size slide

  40. func test() {


    ImageAsset.allCases.forEach {


    XCTAssertNotNil($0.image, "Image \($0) is missing")


    }




    SFSymbol.allCases.forEach {


    XCTAssertNotNil($0.image, "Image \($0) is missing")


    }


    }

    View full-size slide

  41. Third-party libraries
    SwiftGen R.Swift
    let icon = R.image.settingsIcon()


    let color = R.color.indicatorHighlight()

    View full-size slide

  42. Xcode 15 - images and colors
    Image source:

    @sarunw

    View full-size slide

  43. Refactoring with the compiler

    View full-size slide

  44. 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

    View full-size slide

  45. 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

    View full-size slide

  46. 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)

    View full-size slide

  47. 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

    View full-size slide

  48. 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()

    View full-size slide

  49. Compile-time checks 🫶 🫶 🫶
    • Refactoring messy codebases


    • Operating with IDs safely


    • No missing assets


    • Declarative UI updates


    • No memory leaks


    • No hanging spinners

    View full-size slide

  50. ioscodereview.com
    Thank you 👋
    come get a sticker or a t-shirt!

    View full-size slide