Tag: swift

  • Swift, DispatchQueue

    Swift, DispatchQueue

    ✍️ Note

    All the codes and contents are sourced from Apple’s official documentation. This post is for personal notes where I summarize the original contents to grasp the key concepts

    Apple documents

    DispatchQueue

    QoS (Quality of Service)

    • UserInteractive
      • Animations, event handling, or updates to your app’s user interface.
      • User-interactive tasks have the highest priority on the system. Use this class for tasks or queues that interact with the user or actively update your app’s user interface. For example, use this class for animations or for tracking events interactively.
    • UserInitiated
      • Prevent the user from actively using your app
      • User-initiated tasks are second only to user-interactive tasks in their priority on the system. Assign this class to tasks that provide immediate results for something the user is doing, or that would prevent the user from using your app. For example, you might use this quality-of-service class to load the content of an email that you want to display to the user.
    • Default
      • Default tasks have a lower priority than user-initiated and user-interactive tasks, but a higher priority than utility and background tasks. Assign this class to tasks or queues that your app initiates or uses to perform active work on the user’s behalf.
    • Utility
      • Utility tasks have a lower priority than default, user-initiated, and user-interactive tasks, but a higher priority than background tasks. Assign this quality-of-service class to tasks that do not prevent the user from continuing to use your app. For example, you might assign this class to long-running tasks whose progress the user does not follow actively.
    • Background
      • Background tasks have the lowest priority of all tasks. Assign this class to tasks or dispatch queues that you use to perform work while your app is running in the background.

    let concurrentQueue = DispatchQueue(label: "concurrent", qos: .userInitiated, attributes: 
    .concurrent)
    let serialQueue = DispatchQueue(label: "serial", qos: . userInitiated)

    Example 1. Perform async tasks on serialQueue

    for i in 0...3 {
      serialQueue.async {
        print("serial task(\(i)) start")
        sleep(1)
        print("serial task(\(i)) end")
      }
    }
    
    //prints
    serial task(0) start
    serial task(0) end
    
    serial task(1) start
    serial task(1) end
    
    serial task(2) start
    serial task(2) end
    
    serial task(3) start
    serial task(3) end

    It’s make sense because serialQueue can run a one task at a time.

    Example 2. Perform sync tasks on serialQueue

    for i in 0...3 {
      serialQueue.sync {
        print("serial task(\(i)) start")
        sleep(1)
        print("serial task(\(i)) end")
      }
    }
    
    //prints
    serial task(0) start
    serial task(0) end
    
    serial task(1) start
    serial task(1) end
    
    serial task(2) start
    serial task(2) end
    
    serial task(3) start
    serial task(3) end

    Results are the same as example 1

    Submits a work item for execution on the current queue and returns after that block finishes executing.

    https://developer.apple.com/documentation/dispatch/dispatchqueue/2016083-sync

    Example 3. Perform a sync task on the concurrentQueue

    for i in 0...3 {
      concurrentQueue.sync {
        print("concurrent task(\(i)) start")
        sleep(1)
        print("concurrent task(\(i)) end")
      }
    }
    
    //Prints
    concurrent task(0) start
    concurrent task(0) end
    
    concurrent task(1) start
    concurrent task(1) end
    
    concurrent task(2) start
    concurrent task(2) end
    
    concurrent task(3) start
    concurrent task(3) end

    Example 4. Perform a async task on the concurrentQueue

    for i in 0...3 {
      concurrentQueue.async {
        print("concurrent task(\(i)) start")
        sleep(1)
        print("concurrent task(\(i)) end")
      }
    }
    
    //Prints
    concurrent task(0) start
    concurrent task(3) start
    concurrent task(1) start
    concurrent task(2) start
    
    concurrent task(0) end
    concurrent task(1) end
    concurrent task(3) end
    concurrent task(2) end

    Schedules a work item for immediate execution, and returns immediately.

    https://developer.apple.com/documentation/dispatch/dispatchqueue/2016103-async

    It immediate execution and returns immediately.

    Example 5. Perform async task on the concurrentQueue and sync task on the serialQueue

    for i in 0...3 {
      concurrentQueue.async {
        print("concurrent task(\(i)) start")
        sleep(1)
        print("concurrent task(\(i)) end")
      }
                
      serialQueue.sync {
        print("serial task(\(i)) start")
        sleep(1)
        print("serial task(\(i)) end")
      }
    }
    
    //Prints
    concurrent task(0) start
    serial task(0) start
    serial task(0) end
    serial task(1) start
    concurrent task(1) start
    concurrent task(0) end
    concurrent task(1) end
    serial task(1) end
    serial task(2) start
    concurrent task(2) start
    serial task(2) end
    serial task(3) start
    concurrent task(3) start
    concurrent task(2) end
    serial task(3) end
    concurrent task(3) end

    Example 6. Run async tasks on the concurrentQueue and serialQueue

    for i in 0...3 {
       concurrentQueue.async {
         print("concurrent task(\(i)) start")
         sleep(1)
         print("concurrent task(\(i)) end")
       }
    
       serialQueue.async {
         print("serial task(\(i)) start")
         sleep(1)
         print("serial task(\(i)) end")
       }
    }
    
    //Prints
    concurrent task(0) start
    concurrent task(2) start
    concurrent task(3) start
    serial task(0) start
    concurrent task(1) start
    concurrent task(3) end
    concurrent task(0) end
    concurrent task(2) end
    serial task(0) end
    concurrent task(1) end
    serial task(1) start
    serial task(1) end
    serial task(2) start
    serial task(2) end
    serial task(3) start
    serial task(3) end

    As you can see a SerialQueue run a task and returned when task has finished either perform it on sync or async.

    What about dispatchQueue.main.async?

    for i in 0...3 {
      DispatchQueue.main.async {
        print("main task(\(i)) start")
        sleep(1)
        print("main task(\(i)) end")
      }
    }
    
    //Prints
    main task(0) start
    main task(0) end
    
    main task(1) start
    main task(1) end
    
    main task(2) start
    main task(2) end
    
    main task(3) start
    main task(3) end

    The dispatch queue associated with the main thread of the current process.

    The system automatically creates the main queue and associates it with your application’s main thread. Your app uses one (and only one) of the following three approaches to execute blocks submitted to the main queue:

    As with the global concurrent queues, calls to suspend()resume()dispatch_set_context(_:_:), and the like have no effect when used on the queue in this property.

    https://developer.apple.com/documentation/dispatch/dispatchqueue/1781006-main

    As the results, It is not returns immediately like a concurrent queue. Because It’s a serial queue. You can check it on Xcode.

    What about dispatchQueue.global().async?

    for i in 0...3 {
      DispatchQueue.global().async {
        print("global task(\(i)) start")
        sleep(1)
        print("global task(\(i)) end")
      }
    }
    
    //Prints
    global task(3) start
    global task(2) start
    global task(0) start
    global task(1) start
    
    global task(1) end
    global task(0) end
    global task(3) end
    global task(2) end

    This method returns a queue suitable for executing tasks with the specified quality-of-service level. Calls to the suspend()resume(), and dispatch_set_context(_:_:) functions have no effect on the returned queues.

    Tasks submitted to the returned queue are scheduled concurrently with respect to one another

    https://developer.apple.com/documentation/dispatch/dispatchqueue/2300077-global

    🤯 When function is returned?

    It’s a good example for understanding sync vs async.

    Before look at the example, let’s remind

    Dispatch queues are FIFO queues to which your application can submit tasks in the form of block objects. Dispatch queues execute tasks either serially or concurrently. Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app’s main thread, the system makes no guarantees about which thread it uses to execute a task.

    You schedule work items synchronously or asynchronously. When you schedule a work item synchronously, your code waits until that item finishes execution. When you schedule a work item asynchronously, your code continues executing while the work item runs elsewhere.

    https://developer.apple.com/documentation/dispatch/dispatchqueue

    ConcurrentQueue with sync

    runWorkItem function is returned when after finishing the sync task.

    ConcurrentQueue with async

    runWorkItem function returned and then async task starts

    SerialQueue with async

    DispatchQueue.main.async

    Custom Serial DispatchQueue

    SerialQueue with sync but what if a submitted task is delayed?

    Keep in mind

    Work submitted to dispatch queues executes on a pool of threads managed by the system. Except for the dispatch queue representing your app’s main thread, the system makes no guarantees about which thread it uses to execute a task.

    https://developer.apple.com/documentation/dispatch/dispatchqueue

    sync -> block will run and returned when task finished

    async -> block will immediately returned when task started

    syncasyncwhen function returned?
    SerialQeueuewait until task finishedwait until task finishedeither sync or async it returned when after task finished
    ConcurrentQueuewait until task finishedimmediately return when task startedsync: it returned after task finished
    async: it returned before task finished
  • Swift, ARC Automatic Reference Counting

    Swift, ARC Automatic Reference Counting

    ✍️ Note

    All the codes and contents are sourced from Apple’s official documentation. This post is for personal notes where I summarize the original contents to grasp the key concepts

    Apple document

    class Person {
        let name: String
        init(name: String) {
            self.name = name
            print("\(name) is being initialized")
        }
        deinit {
            print("\(name) is being deinitialized")
        }
    }

    Check Strong Reference Count

    Example 1

    var person = Person(name: "Shawn") //1
    CFGetRetainCount(person) //2

    Example 2

    CFGetRetainCount(Person(name: "Shawn") //1

    Example 3

    var person = Person(name: "Shawn") //2
    var person2 = person //3
    var person3 = person //4
    
    CFGetRetainCount(person) //4

    As you can see when you initiate an object, the retain count is 1. And when you assign it, retain count will be counting up.

    Strong Reference Cycle between classes

    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }

    Example 1

    var person = Person(name: "Shawn")
    var apt = Apartment(unit: "3")
    
    CFGetRetainCount(person) //2
    CFGetRetainCount(apt) //2

    Example 2 (🔥 Strong Reference Cycle)

    var person = Person(name: "Shawn")
    var apt = Apartment(unit: "3")
    
    person.apartment = apt
    apt.tenant = person
    
    CFGetRetainCount(person) //3 (apt.tenant -> count up)
    CFGetRetainCount(apt) //3 (person.apartment -> count up)
    

    ✅ Solution -> weak / unowned

    What is weak reference and when to use it?

    Use a weak reference when the other instance has a shorter lifetime — that is, when the other instance can be deallocated first.

    Apple – https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting

    ARC automatically sets a weak reference to nil when the instance that it refers to is deallocated. And, because weak references need to allow their value to be changed to nil at runtime, they’re always declared as variables, rather than constants, of an optional type.

    class Person {
        let name: String
        init(name: String) { self.name = name }
        var apartment: Apartment?
        deinit { print("\(name) is being deinitialized") }
    }
    
    
    class Apartment {
        let unit: String
        init(unit: String) { self.unit = unit }
        weak var tenant: Person?
        deinit { print("Apartment \(unit) is being deinitialized") }
    }

    What is unowned reference and when to use it?

    In contrast, use an unowned reference when the other instance has the same lifetime or a longer lifetime. Unlike a weak reference, an unowned reference is expected to always have a value. As a result, marking a value as unowned doesn’t make it optional, and ARC never sets an unowned reference’s value to nil.

    Important

    Use an unowned reference only when you are sure that the reference always refers to an instance that hasn’t been deallocated.

    If you try to access the value of an unowned reference after that instance has been deallocated, you’ll get a runtime error.

    Apple – https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting

    class Customer {
        let name: String
        var card: CreditCard?
        init(name: String) {
            self.name = name
        }
        deinit { print("\(name) is being deinitialized") }
    }
    
    
    class CreditCard {
        let number: UInt64
        unowned let customer: Customer
        init(number: UInt64, customer: Customer) {
            self.number = number
            self.customer = customer
        }
        deinit { print("Card #\(number) is being deinitialized") }
    }

    Example 1

    var customer: Customer! = Customer(name: "Shawn")
    customer.card = CreditCard(number: 7788_9999_1223_1225, customer: customer)
    
    CFGetRetainCount(customer) //2 (because CreditCard's customer is defined as unowned let)
    
    customer = nil
    
    //Shawn is being deinitialized
    //Card #7788999912231225 is being deinitialized 

    unowned(unsafe)

    Swift also provides unsafe unowned references for cases where you need to disable runtime safety checks — for example, for performance reasons. As with all unsafe operations, you take on the responsibility for checking that code for safety. You indicate an unsafe unowned reference by writing unowned(unsafe). If you try to access an unsafe unowned reference after the instance that it refers to is deallocated, your program will try to access the memory location where the instance used to be, which is an unsafe operation.

    https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting

    unownedself

    When you access the unowned(unsafe), It try to access the memory location where the instance used to be

    unowned

    when use unowned, error type is different.

    Unowned Optional References

    You can mark an optional reference to a class as unowned. In terms of the ARC ownership model, an unowned optional reference and a weak reference can both be used in the same contexts. The difference is that when you use an unowned optional reference, you’re responsible for making sure it always refers to a valid object or is set to nil.

    https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting

    class Department {
        var name: String
        var courses: [Course]
        init(name: String) {
            self.name = name
            self.courses = []
        }
    }
    
    
    class Course {
        var name: String
        unowned var department: Department
        unowned var nextCourse: Course?
        init(name: String, in department: Department) {
            self.name = name
            self.department = department
            self.nextCourse = nil
        }
    }

    Example

    var department: Department! = Department(name: "Horticulture")
    
    let intro = Course(name: "Survey of Plants", in: department)
    var intermediate: Course! = Course(name: "Growing Common Herbs", in: department)
    var advanced: Course = Course(name: "Caring for Tropical Plants", in: department)
    
    intro.nextCourse = intermediate
    intermediate.nextCourse = advanced
    department.courses = [intro, intermediate, advanced]
    
    //Deinit department and intermediate
    department = nil
    intermediate = nil
    
    //Try to access nextCourse
    intro.nextCourse //SIGABRT Error

    Unlike weak, when you try to access the deallocated object, It causes a crash issue. Even you set it as unowned optionals.

    Unowned References and Implicitly Unwrapped Optional Properties

    class Country {
        let name: String
        var capitalCity: City! //Unwrapped Optional Properties
        init(name: String, capitalName: String) {
            self.name = name
            //At this point, Country already initiated. So can pass self to City initializer
            self.capitalCity = City(name: capitalName, country: self)
        }
    }
    
    
    class City {
        let name: String
        unowned let country: Country //Unowned References
        init(name: String, country: Country) {
            self.name = name
            self.country = country
        }
    }

    var capitalCity: City!

    • It’s initial value is nil

    Above the example is Two-Phased initialization

    🤔 Strong Reference Cycles for Closures

    It’s such an important topic.

    A strong reference cycle can also occur if you assign a closure to a property of a class instance, and the body of that closure captures the instance. This capture might occur because the closure’s body accesses a property of the instance, such as self.someProperty, or because the closure calls a method on the instance, such as self.someMethod(). In either case, these accesses cause the closure to “capture” self, creating a strong reference cycle.

    This strong reference cycle occurs because closures, like classes, are reference types. When you assign a closure to a property, you are assigning a reference to that closure. In essence, it’s the same problem as above — two strong references are keeping each other alive. However, rather than two class instances, this time it’s a class instance and a closure that are keeping each other alive.

    Swift provides an elegant solution to this problem, known as a closure capture list

    https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting

    class HTMLElement {
        let name: String
        let text: String?
    
        lazy var asHTML: () -> String = {
            if let text = self.text {
                return "<\(self.name)>\(text)</\(self.name)>"
            } else {
                return "<\(self.name) />"
            }
        }
    
        init(name: String, text: String? = nil) {
            self.name = name
            self.text = text
        }
    
        deinit {
            print("\(name) is being deinitialized")
        }
    }
    var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
    print(paragraph!.asHTML())
    
    //Deallocate HTMLElement
    paragraph = nil // Can't deallocate it because the reference cycle between property (asHTML) and closure's capture list
    screenshot 2024 03 10 at 12.02.07e280afam

    Even though the closure refers to self multiple times (self.text and self.name), it only captures one strong reference to the HTMLElement instance.

    Apple

    Resolving Strong Reference Cycles for Closures

    Use weak or unowned at capture lists

    Defining a capture list

    Each item in a capture list is a pairing of the weak or unowned keyword with a reference to a class instance (such as self) or a variable initialized with some value (such as delegate = self.delegate). These pairings are written within a pair of square braces, separated by commas.

    Place the capture list before a closure’s parameter list and return type if they’re provided:

    Apple

    lazy var someClosure = {
            [unowned self, weak delegate = self.delegate]
            (index: Int, stringToProcess: String) -> String in
        // closure body goes here
    }
    
    
    lazy var someClosure2 = {
            [unowned self, weak delegate = self.delegate] in
        // closure body goes here
    }

    🔥 Think it again, weak vs unowned in Closures

    Define a capture in a closure as an unowned reference when the closure and the instance it captures will always refer to each other, and will always be deallocated at the same time.

    Conversely, define a capture as a weak reference when the captured reference may become nil at some point in the future. Weak references are always of an optional type, and automatically become nil when the instance they reference is deallocated. This enables you to check for their existence within the closure’s body.

    Apple

    class HTMLElement {
        let name: String
        let text: String?
        //Use unowned self to break reference cycle
        lazy var asHTML: () -> String = {
                [unowned self] in
            if let text = self.text {
                return "<\(self.name)>\(text)</\(self.name)>"
            } else {
                return "<\(self.name) />"
            }
        }
    
        init(name: String, text: String? = nil) {
            self.name = name
            self.text = text
        }
    
        deinit {
            print("\(name) is being deinitialized")
        }
    }
    screenshot 2024 03 10 at 12.21.50e280afam

    Capture value from nested Closure

    class Test {}
    DispatchQueue.global().async {
      let test = Test()
       print("closure A: Start retain count is: \(CFGetRetainCount(test))")
       DispatchQueue.global().async {
         print("nested closure B retainCount: \(CFGetRetainCount(test))")
       }
       print("closure A: End")
    }
    
    //Prints
    closure A: Start retain count is: 2
    closure A: End
    nested closure B retainCount: 3

    As you can see nested closure also counting up ARC.

    But… what about this example?

    screenshot 2024 03 14 at 11.00.59e280afpm

    test object was already deallocated. And when you trying to access test, It should not be nil because It’s unowned.

    Conclusion

    Understanding How ARC works is very important to avoid memory leaks and crash issues. It’s also good to know what happens when objects are deallocated and How ARC handles those objects.

  • [Draft] Python cheat sheet for iOS Engineer

    [Draft] Python cheat sheet for iOS Engineer

    I asked my self Is python worth to learn for iOS development? My answer is Yes.

    Because I can use it for

    • Build scripts
    • Core ML + coreml tools
    • BE for Mobile (Django)

    Getting Principles

    • You can read the list of the python principles by importing this
    import this

    Cheatsheet

    Standard LibrarySwiftPython
    variablevar name = "Shawn"name = "Shawn"
    constantslet name = "Shawn"Not support
    But UPPERCASED Naming indicates constants It’s like a implicit rule in Python
    string"Hello World!"

    Multiline String
    """
    Hello
    Swift
    """


    Extended Delimiters
    #"Hello\n Swift"#

    It will print Hello\n Swift
    "Hello World!"
    'Hello World!'


    Swift can’t use single quote but Python can use


    formatted stringlet name = "Shawn"
    "My name is \(name)"
    name = "Shawn"
    f"My name is {name}"


    f means formatted string
    capitalizedvar greeting = “shawn baek”
    print(greeting.capitalized)
    //’Shawn Baek’
    name = “shawn baek”
    print(name.title())

    //’Shawn Baek’
    uppercased / lowercasedvar greeting = “Shawn Baek”

    print(greeting.uppercased())
    //’SHAWN BAEK’

    print(greeting.lowercased())
    //’shawn baek’
    name = “Shawn Baek”
    print(name.upper())
    //’SHAWN BAEK’

    print(name.lower())
    //’shawn baek’
    trimmingCharacters(in: .whitespacesAndNewlines)var greeting = ” Shawn Baek “

    //MARK: Remove leading / trailing spaces

    greeting.trimmingCharacters(in: .whitespacesAndNewlines)

    //’Shawn Baek’
    name = ” Shawn Baek “

    name.rstrip()
    // ‘ Shawn Baek’

    name.lstrip()
    //’Shawn Baek ‘

    name.strip()
    //’Shawn Baek’
    trimPrefixvar blogUrl = “https://shawnbaek.com&#8221;
    blogUrl.trimPrefix(“https://&#8221😉

    //’shawnbaek.com’
    blog_url = ‘https://shawnbaek.com&#8217;

    blog_url.removeprefix(‘https://&#8217😉
    //’shawnbaek.com’
    powvar number = pow(3.0, 2.0)

    print(number)
    //9.0
    number = 3.0 ** 2

    print(number)
    //9.0
    comment//Hello World#Hello World

  • SwiftUI: Syntax Highlighting JSON using UITextView

    SwiftUI: Syntax Highlighting JSON using UITextView

    It’s very simple. I used this Highlight SPM

    Sample Code

    //
    //  ContentView.swift
    //  Button
    //
    //  Created by Sungwook Baek on 2023/09/26.
    //
    
    import SwiftUI
    import Highlight
    
    struct ContentView: View {
        @State var jsonString = NSMutableAttributedString(string: "")
        var body: some View {
            HighlightTextView(text: $jsonString)
        }
    }
    
    #Preview {
        ContentView()
    }
    
    struct HighlightTextView: UIViewRepresentable {
        @Binding var text: NSMutableAttributedString
        
        func makeUIView(context: Context) -> UITextView {
            let textView = UITextView()
            textView.keyboardType = .asciiCapable
            textView.autocapitalizationType = .none
            textView.delegate = context.coordinator
            textView.attributedText = text
            return textView
        }
    
        func updateUIView(_ uiView: UITextView, context: Context) {
            let selectedRange = uiView.selectedRange
            uiView.attributedText = text
            uiView.selectedRange = selectedRange
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
            
        class Coordinator: NSObject, UITextViewDelegate {
            var parent: HighlightTextView
            
            init(_ parent: HighlightTextView) {
                self.parent = parent
            }
            
            func textViewDidChange(_ textView: UITextView) {
                let highligher = JsonSyntaxHighlightProvider.shared.highlight( textView.attributedText.string, as: .json)
                let selectedRange = textView.selectedRange
                textView.attributedText = highligher
                textView.selectedRange = selectedRange
            }
        }
    }
  • Project Setup for Full Stack Swift Developer – Use Workspace

    Project Setup for Full Stack Swift Developer – Use Workspace

    This post is about project setup for full stack swift developer. It is the same as WWDC 2022 – Use Xcode for server-side development

    I like this setup because I don’t want to switching projects between iOS and Vapor.

    Step 1. Copy Vapor Project folder into iOS App Project folder

    
    89e1f 09baf screenshot2023 09 03at1.09.19am
    

    Step 2. Open iOS App Project (xcodeproj) and drag Vapor Project folder at the top level

    9487c ac91f screenshot2023 09 03at1.10.48am

    Step 3. Close iOS App Project folder and open Xcode Workspace

    a726f c08d4 folder

    Step 4. Resolve git subproject -dirt issue

    If you worked on vapor project with git, you may facing git subproject issue. To resolve it you need to update submodule.

    Go to the vapor project you copied in iOS project.

    Check git status

    Update git status by syncing with remote and commit any changes

    Go back to parent folder (iOS Project folder)
    Sync with remote and commit any changes

  • Xcode Cloud – Auto-Generated Test Notes for TestFlight Build (PR Title, Commits, and JIRA Ticket Links)

    Xcode Cloud – Auto-Generated Test Notes for TestFlight Build (PR Title, Commits, and JIRA Ticket Links)

    No more put ‘No test notes’ at Test Flight

    bd086 c016e img 3297

    No test notes.

    It’s not clear what to test using this build

    Setup Xcode Cloud Workflow

    To get a Github PR infos from Xcode Cloud, You should enable settings like below

    860c8 56aac screenshot2023 08 20at4.24.10am

    8df7d d55a3 screenshot2023 08 20at4.24.19am

    Environment

    • Set your Github Token

    • Set Xcode Version – Latest Release

    Start Conditions -> See Apple Document

    • CI_PULL_REQUEST_NUMBER

    • CI_PULL_REQUEST_SOURCE_COMMIT

    • CI_PULL_REQUEST_TARGET_COMMIT

    To get CI_PULL_REQUEST variables You should set Pull Request Changes as Start Condition

    Post Actions

    • Set Test Flight Internal Testing

     

    ci_scripts/ci_post_clone.sh

    #!/bin/sh
    brew install jq

     

    ci_scripts/ci_post_xcodebuild.sh

    #!/bin/zsh
    #  ci_post_xcodebuild.sh
    
    # PR Description
    GITHUB_REST_API_PR_INFO=$(curl -L\
      -H "Accept: vnd.github+json" \
      -H "Authorization: Bearer $GITHUB_TOKEN" \
      -H "X-GitHub-Api-Version: 2022-11-28" \
      "https://api.github.com/repos/$CI_PULL_REQUEST_TARGET_REPO/pulls/$CI_PULL_REQUEST_NUMBER")
    
    # Fixed: parse error: Invalid string: control characters from U+0000 through U+001F must be escaped, https://stackoverflow.com/questions/52399819/invalid-string-control-characters-from-u0000-through-u001f-must-be-escaped-us
    PR_TITLE=$(printf '%s\n' "$GITHUB_REST_API_PR_INFO" | jq -r '.title')
    
    PR_DESCRIPTION=$(printf '%s\n' "GITHUB_REST_API_PR_INFO" | jq -r '.body')
    PR_DESCRIPTION_URLS=$(echo "$PR_DESCRIPTION" | grep -Eo 'https?://[^[:space:]]+')
    PR_DESCRIPTION_JIRA_TICKETS=$(echo "$PR_DESCRIPTION_URLS" | grep 'atlassian')
    
    # GIT COMMITS
    COMMIT_MESSAGES=$(git log $CI_PULL_REQUEST_TARGET_COMMIT^..$CI_PULL_REQUEST_SOURCE_COMMIT --pretty=format:"%s\n%b")
    FORMATTED_COMMIT_MESSAGES=$(echo -e "$COMMIT_MESSAGES" | awk 'NF {print}' ORS='\n')
    
    # TEST FLIGHT - What To Test
    WHAT_TO_TEST="$PR_TITLE\nCommits:\n$FORMATTED_COMMIT_MESSAGES\nJIRA Tickets:\n$PR_DESCRIPTION_JIRA_TICKETS"
    
    if [[ -d "$CI_APP_STORE_SIGNED_APP_PATH" ]]; then
      TESTFLIGHT_DIR_PATH=../TestFlight
      mkdir $TESTFLIGHT_DIR_PATH
      echo "$WHAT_TO_TEST" >! $TESTFLIGHT_DIR_PATH/WhatToTest.en-US.txt
    fi

     

    Auto-Generated Test Notes

    0a253 d913a screenshot2023 08 20at5.30.42am

  • Xcode Cloud – How to check App Version (MARKETING_VERSION) from ci_post_xcodebuild.sh

    Xcode Cloud – How to check App Version (MARKETING_VERSION) from ci_post_xcodebuild.sh

    Check App Version using xcodebuild

    Update ci_scripts/ci_post_xcodebuild.sh

    CURRENT_PROJECT_VERSION=$(xcodebuild -project $CI_PROJECT_FILE_PATH -showBuildSettings | grep "MARKETING_VERSION" | sed 's/[ ]*MARKETING_VERSION = //')
    
    echo "🐥 App Version is $CURRENT_PROJECT_VERSION"

    704cb df295 screenshot2023 08 19at5.14.25pm

  • How to write a unit test for validating regex without using XCUIElement’s typeText function

    How to write a unit test for validating regex without using XCUIElement’s typeText function

    This post might be helpful if you are considering using the typeText function for validating user input using regular expressions, such as for validating passwords or usernames.

    Although XCTest enables you to write unit tests for validating the results of regular expressions, it is not suitable for writing test cases that involve inputting characters one by one.

    Let me show the example.

    d164f 7eaac screenshot2023 07 09at2.27.33pm

    Business requirements

    • Username allows alphabet + numbers only

    • Password allows alphabet + numbers + special characters

    • Address allows alphabet + numbers only

    • Phone number format is 4 digits – 4 digits

    • Postal Code is 5 numbers

    • Birthday format is yyyyMMDD

    • Username, Password, Phone, Address are required fields

    • Error message will be displayed when entering text into the TextField

    • Search button will be enabled once the postal code is validated.

    Let’s write a test cases

    ProfileInputValidatorTests: XCTestCase

    It has a sut for checking unitTests and typeText function (like a XCUIElement’s typeText) to put the characters into the active UITextField.

    4716d dcc87 screenshot2023 07 09at3.06.51pm

    class ProfileInputValidatorTests: XCTestCase {
        var sut: MockClass!
    
        override func setUp() {
            super.setUp()
            sut = MockClass()
        }
    
        override func tearDown() {
            super.tearDown()
            sut.activeTextField = nil
            sut.inputCheckType = nil
            sut.errorMessage = nil
        }
    
        //UnitTests
    }
    
    private extension ProfileInputValidatorTests {
        func typeText(_ input: String) {
            guard let activeTextField = sut.activeTextField else {
                return
            }
            for (location, character) in input.enumerated() {
                let canInputText = activeTextField.delegate?.textField?(
                    activeTextField,
                    shouldChangeCharactersIn: NSRange(location: location, length: 0),
                    replacementString: String(character)
                )
                if canInputText ?? false {
                    let lastPosition = activeTextField.endOfDocument
                    activeTextField.selectedTextRange = activeTextField
                        .textRange(from: lastPosition, to: lastPosition)
                    activeTextField.insertText(String(character))
                }
            }
        }
    }

    Here is the example of unit tests

    func test_textField_should_not_exceed_maxCharacter() {
        ProfileInputType.allCases.forEach {
            sut.setActiveTextField($0)
            let dummyText = """
            Lorem Ipsum is simply dummy text of the printing and typesetting industry.
            """
            typeText(dummyText)
            XCTAssertTrue(
                sut.activeTextField?.text?.count == sut.inputValidator.activeTextFieldType?.maxCharacter,
                "TextField tag: \($0.rawValue) should not exceed maxCharacter"
            )
        }
    }
    
    func test_username_allowance_character() {
        let username = "skboard"
        sut.setActiveTextField(.username)
        typeText(username)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
    }
    
    func test_username_not_allowance_character() {
        let username = "マーン224s한글djfタklsdf"
        sut.setActiveTextField(.username)
        typeText(username)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertEqual(ProfileInputType.username.errorMessage, sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.username)
    }
    
    func test_postal_code_allowance_character() {
        let postalCode = "13456"
        sut.setActiveTextField(.postalCode)
        typeText(postalCode)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.postalCode)
    }
    
    func test_is_enabled_search_button_while_editing() {
        sut.setActiveTextField(.postalCode)
        let postalCode = "89761"
        typeText(postalCode)
        XCTAssertEqual(sut.activeTextField?.text, "89761")
        XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
    }
    
    func test_is_enabled_search_button_after_didEndEditing() {
        sut.setActiveTextField(.postalCode)
        let postalCode = "24028"
        typeText(postalCode)
    
        XCTAssertEqual(sut.activeTextField?.text, "24028")
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
        XCTAssertTrue(sut.searchButton.isEnabled, "searchButton should enabled")
    }
    
    func test_phone_number_format() {
        sut.setActiveTextField(.phone)
        let phoneNumber = "78761241"
        typeText(phoneNumber)
        XCTAssertEqual(sut.activeTextField?.text, "7876-1241")
    }
    
    func test_birthday_format() {
        let birthday = "19980115"
        sut.setActiveTextField(.birthday)
        typeText(birthday)
        sut.activeTextField?.delegate?.textFieldDidEndEditing?(sut.activeTextField!)
    
        XCTAssertNil(sut.errorMessage)
        XCTAssertEqual(sut.inputCheckType, ProfileInputType.birthday)
    }
    
    func test_phone_number_only_allow_one_hyphen_character() {
        sut.setActiveTextField(.phone)
        let phoneNumber = "1467834527891471247"
        typeText(phoneNumber)
        let hyphenCount = numberOfHyphen(in: sut.activeTextField?.text ?? "")
        XCTAssertTrue(
            hyphenCount == 1,
            "phone number only allow one hyphen character"
        )
    }

    InputValidator

    InputValidator checks the inputTypes and validates strings.

    protocol ProfileInputValidatorDelegate: AnyObject {
        var activeTextField: UITextField? { get set }
        func setActiveTextField(_ inputType: ProfileInputType)
        func updateAddressSearchButtonState(isEnabled: Bool)
        func showValidateResult(_ inputType: ProfileInputType?, error message: String?)
    }
    
    final class ProfileInputValidator: NSObject {
        var activeTextFieldType: ProfileInputType?
        var dateFormatter: DateFormatter?
        weak var delegate: ProfileInputValidatorDelegate?
        
        func validate(
            text: String,
            type: ProfileInputType,
            needToSetType: Bool = false,
            placeholder: String? = nil
        ) {
            guard isNotEmpty(text: text) else {
                showEmptyMessage(type: type, needToSetType: needToSetType, placeholder: placeholder)
                return
            }
            let errorMessage: String?
            switch type {
            case .username:
                errorMessage = isValidUsername(text) ? nil : type.errorMessage
            case .password:
                errorMessage = isValidPassword(text) ? nil : type.errorMessage
            case .phone:
                errorMessage = isValidPhoneNumber(text) ? nil : type.errorMessage
            case .address:
                errorMessage = isValidAddress(text) ? nil : type.errorMessage
            case .postalCode:
                errorMessage = isValidPostalCode(text) ? nil : type.errorMessage
            case .birthday:
                errorMessage = isValidBirthday(text) ? nil : type.errorMessage
            }
            delegate?.showValidateResult(needToSetType ? type : activeTextFieldType, error: errorMessage)
        }
        
        private var isRequiredField: Bool {
            if activeTextFieldType == .some(.username) ||
                activeTextFieldType == .some(.password) ||
                activeTextFieldType == .some(.address) ||
                activeTextFieldType == .some(.phone) {
                return true
            }
            else {
                return false
            }
        }
        
        private func showEmptyMessage(type: ProfileInputType, needToSetType: Bool, placeholder: String?) {
            let textFieldTitle = activeTextFieldType?.title ?? ""
            var emptyErrorMessage: String?
            if isRequiredField {
                emptyErrorMessage =
                    "\(placeholder ?? textFieldTitle) is required"
            }
            delegate?.showValidateResult(
                needToSetType ? type : activeTextFieldType,
                error: emptyErrorMessage
            )
        }
    
        private func isNotEmpty(text: String?) -> Bool {
            guard let inputText = text else {
                return false
            }
            let textWithOutWhiteSpace = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
            return !textWithOutWhiteSpace.isEmpty
        }
    
        private func isValidDateFormat(text: String) -> Bool {
            guard convertBirthdayFormat(text)?.count == ProfileInputType.birthday.maxCharacter else {
                return false
            }
            return true
        }
    }

    This class conforms to UITextFieldDelegate to check strings

    //MARK: UITextFieldDelegate
    extension ProfileInputValidator: UITextFieldDelegate {
        func textFieldDidBeginEditing(_ textField: UITextField) {
            guard let fieldType = ProfileInputType(textField.tag) else {
                return
            }
            delegate?.activeTextField = textField
            activeTextFieldType = fieldType
        }
    
        func textFieldDidEndEditing(_ textField: UITextField) {
            guard let inputText = textField.text, let inputType = activeTextFieldType else {
                return
            }
            validate(text: inputText, type: inputType)
        }
    
        func textField(
            _ textField: UITextField,
            shouldChangeCharactersIn range: NSRange,
            replacementString string: String
        ) -> Bool {
            let text = textField.text ?? ""
            guard let stringRange = Range(range, in: text),
                  let activeTextFieldMaxCount = activeTextFieldType?.maxCharacter
            else {
                return false
            }
            if activeTextFieldType == .phone, let phoneNumber = formattingPhoneNumber(text: text, to: string, range: range) {
                textField.text = phoneNumber
                return false
            }
            let updatedText = text.replacingCharacters(in: stringRange, with: string)
            if activeTextFieldType == .postalCode {
                delegate?.updateAddressSearchButtonState(isEnabled: updatedText.count == activeTextFieldMaxCount)
            }
            return updatedText.count <= activeTextFieldMaxCount
        }
    
        func textFieldShouldReturn(_ textField: UITextField) -> Bool {
            guard let activeType = activeTextFieldType else {
                return false
            }
            textField.endEditing(false)
            // Focus on the next field
            switch activeType {
            case .username:
                delegate?.setActiveTextField(.password)
            case .password:
                delegate?.setActiveTextField(.phone)
            case .phone:
                delegate?.setActiveTextField(.address)
            case .address:
                delegate?.setActiveTextField(.postalCode)
            case .postalCode:
                delegate?.setActiveTextField(.birthday)
            default:
                break
            }
            return false
        }
    }

    MockClass

    This class has a InputValidator and conforms to ProfileInputValidatorDelegate. It is use for XCTestCase.

    class MockClass {
        var activeTextField: UITextField?
        var searchButton = UIButton()
        var inputCheckType: ProfileInputType?
        var errorMessage: String?
    
        fileprivate let inputValidator = ProfileInputValidator()
        
        var usernameTextField: UITextField!
        var passwordTextField: UITextField!
        var addressTextField: UITextField!
        var phoneTextField: UITextField!
        var postalCodeTextField: UITextField!
        var birthdayTextField: UITextField!
        
        lazy var dateFormatter: DateFormatter = {
            let formatter = DateFormatter()
            formatter.dateFormat = "yyyy.MM.dd"
            formatter.calendar = Calendar(identifier: .gregorian)
            return formatter
        }()
    
        init() {
            inputValidator.delegate = self
            inputValidator.dateFormatter = dateFormatter
    
            setupTextFields()
            searchButton.isEnabled = false
        }
    
        private func setupTextFields() {
            usernameTextField = UITextField()
            passwordTextField = UITextField()
            addressTextField = UITextField()
            phoneTextField = UITextField()
            postalCodeTextField = UITextField()
            birthdayTextField = UITextField()
        
            let textFields = [
                usernameTextField,
                passwordTextField,
    
                addressTextField,
                phoneTextField,
    
                postalCodeTextField,
                birthdayTextField
            ]
    
            for (index, textField) in textFields.enumerated() {
                textField?.delegate = inputValidator
                textField?.tag = index
            }
        }
    }
    
    extension MockClass: ProfileInputValidatorDelegate {
        func setActiveTextField(_ inputType: ProfileInputType) {
            switch inputType {
            case .username:
                usernameTextField.delegate?.textFieldDidBeginEditing?(usernameTextField)
            case .password:
                passwordTextField.delegate?.textFieldDidBeginEditing?(passwordTextField)
            case .address:
                addressTextField.delegate?.textFieldDidBeginEditing?(addressTextField)
            case .phone:
                phoneTextField.delegate?.textFieldDidBeginEditing?(phoneTextField)
            case .postalCode:
                postalCodeTextField.delegate?.textFieldDidBeginEditing?(postalCodeTextField)
            case .birthday:
                birthdayTextField.delegate?.textFieldDidBeginEditing?(birthdayTextField)
            }
        }
    
        func updateAddressSearchButtonState(isEnabled: Bool) {
            searchButton.isEnabled = isEnabled
        }
    
        func showValidateResult(_ inputType: ProfileInputType?, error message: String?) {
            errorMessage = message
            inputCheckType = inputType
        }
    }

    Conclusion

    XCTest is a powerful framework for testing unit tests, but it can be challenging to test UI-related logic. In mobile development, UI logic is closely intertwined with the user interface itself. The conventional approach for testing UI logic is to use XCUITest. However, XCUITest tends to be slower compared to XCTest. If we have multiple views designed for registering or updating information using input fields, we would need to write multiple UI testing code with XCUITest.

    An alternative approach is to use XCTest and create a function that simulates user actions, such as typing text. This allows us to write a unit test for common input validator logic while considering user actions as well. This way, we can test the UI-related logic using XCTest in a more efficient manner compared to relying solely on XCUITest.

  • How to Find Unused Swift Code with Periphery Static Analyzer

    How to Find Unused Swift Code with Periphery Static Analyzer

    Recently I faced an issue with compile -O for product build. I assumed that It caused by dead function issue. I reported it

    Apple Swift Git Repo

    This post I’ll introduce static analyzer tool for checking the unused swift codes

    Install Periphery

    Link You can install periphery tool using homebrew

    Here is homebrew install command.

    brew install peripheryapp/periphery/periphery

    And then setup peripheryapp on your project

    periphery scan --setup

    34ff6 72338 screenshot2023 06 03at11.33.08pm

    Select your App’s target name. Don’t forget to save configuration to .periphery.yml

    Check unused codes

    You can see the results.

    50103 2554a screenshot2023 06 03at11.38.15pm

    Make a script file to save result as text file

    I recommend make a bash script file to save results as text file to keep track progress of reducing unused codes. Here is the bash script. (I changed ruby code (https://www.youtube.com/watch?v=2OQiCj-BG2o) to bash script)

    #!/bin/bash
    
    echo "🔍 start dead code analysis"
    
    result="$(periphery scan --config .periphery.yml)"
    
    current_dir=$(pwd)
    
    result_stripped_of_absolute_path_prefix=$(echo "$result" | sed "s|$current_dir/||g")
    
    filtered_out_result=$(echo "$result_stripped_of_absolute_path_prefix" | awk '/:[0-9]+:[0-9]+:/{ print }')
    sorted_result=$(echo "$filtered_out_result" | sort)
    
    result_with_removed_code_line_number=$(echo "$sorted_result" | sed "s|:[0-9]+:[0-9]+:|:|")
    output=$(echo "$result_with_removed_code_line_number" | tr '\n' '\n' | sort)
    
    echo "$output" > unused-codes.txt 2>&1
    echo "🙌 done with dead code analysis"

    I saved bash script file as check-unused-codes.sh

    You can run bash script file by running sh check-unused.codes.sh in terminal (You can see the results of the unused codes)

    25550 79731 screenshot2023 06 04at1.24.26am

    fd2e5 cc6e4 screenshot2023 06 04at1.24.38am

    Results will be saved into the unused-codes.txt

    Reference

    Inside iOS Dev

    https://www.parasoft.com/blog/false-positives-in-static-code-analysis/

  • Swift Compile Optimization Levels Explained – O, Onone, Osize Benchmarks

    Swift Compile Optimization Levels Explained – O, Onone, Osize Benchmarks

    Optimization Level

    Swift compiler has 3 optimization options (link)

    • -O

      • for product code

    • -Onone

      • for development and debugging mode.

    • -Osize

      • for size optimization over the optimizing code

     

    How to compile swift with optimization level?

    Here is a simple swift code for checking execution time. (main.swift)

    import Foundation
    func sum(from: Int, to: Int) -> Int {
        var result = 0
        for i in from...to {
            result += i
        }
        return result
    }
    
    let startTime = Date()
    let result = sum(from: 1, to: 10000000)
    let endTime = Date()
    
    print("Result: \(result)")
    print("Calculation Time: \(endTime.timeIntervalSince(startTime)) seconds")

    Compiled it with -Onone

    swiftc -Onone main.swift && ./main
    //Result
    Result: 50000005000000
    Time: 1.4896910190582275 seconds

    Compiled it with -O

    swiftc -O main.swift && ./main
    //Result
    Result: 50000005000000
    Time: 0.006083011627197266 seconds

    Results

    -O option is 24,389% faster than -Onone option

     

    Check SIL file

    swiftc -O main.swift is generating executable file. It is not a human readable file. The optimization process transforms the source code into machine code, which is the binary code that can be executed directly by the computer.

    Do you want to see the intermediate representation of the Swift code generated during the optimization process?

    You can use the -emit-sil flag with the swiftc command. This will output the SIL (Swift Intermediate Language) code generated by the compiler, which is a human-readable representation of the Swift code that has been optimized

    swiftc -O -emit-sil main.swift -o main.sil
    swiftc -Onone -emit-sil main.swift -o main.sil

    908c7 f2128 screenshot2023 04 15at6.09.29pm

    You can see the main.sil file. Let’s open it using TextEdit

    0b60c f50c7 screenshot2023 04 15at6.11.21pm

    Now we can see the details by checking sil file. If you want to learn more about it visit here

     

    Conclusion

    I wanted to test how Swift’s optimization levels work, so I used the -emit-sil option to generate an SIL (Swift Intermediate Language) file. This file serves as a bridge between the Swift source code and the machine code, and allows the Swift compiler to perform optimizations such as dead code elimination, constant folding, inlining, and loop unrolling. By inspecting and analyzing the SIL file, we can better understand how the Swift compiler is optimizing our code. Additionally, SIL can be used for debugging purposes since it provides a more detailed and low-level view of the generated code compared to the original Swift source

    Notes
    This blog post has been edited using ChatGPT to refine some of the sentences