Blog

  • Swift, Data Structure – Linked List

    Swift, Data Structure – Linked List

    ✍️ Note

    Some codes are sourced from Loony Corn’s Udemy Course (https://www.udemy.com/user/janani-ravi-2/). This post is for personal notes where I summarize the original contents to grasp the key concepts

    What is Linked List

    It is a list data structure where each element in the list holds some information and points to where the next element lies.

    Linked List vs Arrays

    • Linked List
      • List sizes are unlimited, you don’t need to know up front how many elements will be added to the linked list, the size of the linked list can grow dynamically
      • Inserting a new element in a linked list is a very easy operation, the logical next pointers have to be reassigned but not much else needs to be done
      • Similarly deleting an element in a list is very easy and efficient
      • Random access to an element at a specific index in the linked list is not possible. The entire list up-to that element has to be traversed
      • Each element requires additional space to store a pointer to the next element
      • Cannot take advantage of spatial locality (for caching) when accessing the elements
      • Each element can live pretty much anywhere in memory and be pointed to
    • Arrays
      • Arrays have to be declared upfront with the number of elements it will hold, the size of array cannot be increased dynamically <- In Swift, You can increase size, You can read details at Apple Document – Growing the Size of an Array
      • Arrays are located in contiguous memory locations, in order to insert an element, the elements to it’s right have to move over to make room for it. It is a more heavyweight operation.
      • Array elements have to be moved in the case of deletion as well
      • Arrays provide very quick lookup for elements at specific indices, since they are in contiguous memory locations, we know exactly where in memory the element is
      • No extra space is required other than for the actual elements which make up the array
      • As elements are in contiguous memory locations access to arrays takes significant advantage of spatial locality in caches
      • This can be a significant performance improvement

    Use Linked Lists when

    • You have a large number of insert or delete operations to perform
    • You have no idea how large your list might be

    Use Array when

    • Read operations need to be extremely fast and you have relatively few updates to the array
    • You require random access to array elements

    Source: https://www.udemy.com/course/from-0-to-1-data-structures/learn/lecture/8504232#content

    Last element points to nil

    First element is head

    screenshot 2024 03 17 at 9.54.42e280afpm
    class Node {
        let value: Int
        var next: Node?
        init(value: Int, next: Node? = nil) {
            self.value = value
            self.next = next
        }
    }
    
    let head = Node(value: 1)
    let second = Node(value: 2)
    let third = Node(value: 3)
    let fourth = Node(value: 4)
    
    //Make linked list
    head.next = second
    second.next = third
    third.next = fourth
    fourth.next = nil
    
    var start: Node? = head
    
    while start != nil {
        print("\(start!.value)")
        var next = start?.next
        start = next
    }
    
    //Prints
    1
    2
    3
    4

    Exercise 1. Get Length of a linked list

    func length(of linkedList: Node?) -> Int {
        var length = 0
        var head = linkedList
        while head != nil {
            length += 1
            head = head?.next
        }
        return length
    }
    
    length(of: head)

    Exercise 2. Get the N-th element in a Linked List

    func element(of linkedList: Node?, at index: Int) -> Node? {
        if index < 0 {
            return nil
        }
        var length = 0
        var head = linkedList
        while head != nil {
            if index == length {
                return head
            }
            length += 1
            head = head?.next
        }
        return nil
    }
    
    // 1 -> 2 -> 3 -> 4, Index 3 is 4
    element(of: head, at: 3)?.value //Prints 4, It's correct
    

    Exercise 3. Append an element to the end of a linked list

    There is 2 case you need to handle

    • head is null -> create a head node
    • there is a linked list, create a new node and append it as a last node. Don’t forget to handle last node’s next is nil
    var head: Node? = Node(value: 1)
    let second = Node(value: 2)
    let third = Node(value: 3)
    let fourth = Node(value: 4)
    
    //Make linked list
    head?.next = second
    second.next = third
    third.next = fourth
    fourth.next = nil
    
    func print(linkedList: Node?) {
        var head = linkedList
        while head != nil {
            print("\(head!.value)")
            head = head?.next
        }
    }
    
    //To handle if linkedList is nil, and You don't want to return anything then use inout parameter.
    func append(data: Int, at linkedList: inout Node?) {
        if linkedList == nil {
            linkedList = Node(value: data)
            return
        }
        var head = linkedList
        var lastItem: Node?
        while linkedList?.next != nil {
            linkedList = linkedList?.next
        }
        lastItem = linkedList
        lastItem?.next = Node(value: data)
        //Important, Reset head node
        linkedList = head
    }
    
    var emptyHead: Node?
    //Case 1. Empty Head
    append(data: 4, at: &emptyHead)
    print(emptyHead?.value)
    
    //Case 2. 1 -> 2 -> 3 -> 4, append 5
    append(data: 5, at: &head)
    print(linkedList: head)
    screenshot 2024 03 17 at 10.27.32e280afpm

    Exercise 4. Remove the first element

    screenshot 2024 03 21 at 2.22.42e280afpm
    func removeFirst(linkedList: inout Node?) {
        guard linkedList != nil else {
            return
        }
        var head: Node? = linkedList
        head = head?.next
        linkedList = head
    }
    print("LinkedList")
    print(linkedList: head)
    
    removeFirst(linkedList: &head)
    print("Removed:")
    print(linkedList: head)
    
    //Prints
    LinkedList
    1
    2
    3
    4
    Removed:
    2
    3
    4

    Exercise 5. Delete all the elements (All nodes should be removed from Memory)

    screenshot 2024 03 21 at 2.40.15e280afpm
    class Node {
        let value: Int
        var next: Node?
        init(value: Int, next: Node? = nil) {
            self.value = value
            self.next = next
        }
        
        deinit {
            print("Node \(value) is deinit")
        }
    }
    
    func removeAll(linkedList: inout Node?) {
        guard linkedList != nil else {
            return
        }
        while linkedList != nil {
            print("Remove : \(linkedList!.value) Refcount: \(CFGetRetainCount(linkedList))")
            var next = linkedList?.next
            linkedList?.next = nil
            linkedList = next
        }
    }
    
    //Prints
    Remove : 1 Refcount: 2
    Node 1 is deinit
    Remove : 2 Refcount: 3
    Remove : 3 Refcount: 3
    Remove : 4 Refcount: 3

    What’s wrong? Only Node 1 deinit. The rest of nodes are still remaining in Memory.

    To remove all nodes from Memory, We need to fix previous example of linked list.

    screenshot 2024 03 21 at 5.16.52e280afpm

    Let’s make a linkedList function.

    func linkedList(_ input: [Int]) -> Node? {
        guard input.count > 0 else {
            return nil
        }
        var head: Node? = Node(value: input.first!)
        var linkedList = head
        for i in 1..<input.count {
            let value = input[i]
            linkedList?.next = Node(value: value)
            linkedList = linkedList?.next
        }
        return head
    }
    
    var newList = linkedList([1, 2, 3, 4])
    removeAll(linkedList: &newList)
    
    //Prints
    Remove : 1 Refcount: 2
    Node 1 is deinit
    Remove : 2 Refcount: 2
    Node 2 is deinit
    Remove : 3 Refcount: 2
    Node 3 is deinit
    Remove : 4 Refcount: 2
    Node 4 is deinit

    I used a same removeAll function. I only change the linkedList by using linkedList function. This function returns linkedList and all nodes has a same reference counts.

    Now You can see all nodes are completely removed from Memory.

    screenshot 2024 03 21 at 5.30.18e280afpm

    Next example, I’ll continue use linkedList function for creating a linked list.

    Exercise 6. Insert an elements at index

    screenshot 2024 03 21 at 5.49.24e280afpm
    func print(linkedList: Node?) {
        var head = linkedList
        while head != nil {
            print("\(head!.value)", terminator: head?.next == nil ? "" : " -> ")
            head = head?.next
        }
    }
    
    func insert(value: Int, at index: Int, in linkedList: inout Node?) {
        guard linkedList != nil else {
            return
        }
        var head = linkedList
        //Edge case
        if index == 0 {
            var newHead = Node(value: value, next: linkedList)
            linkedList = newHead
            return
        }
        for i in 0..<index - 1 {
            var next = linkedList?.next
            linkedList = next
        }
        var prevItem = linkedList
        var prevNextItem = prevItem?.next
        prevItem?.next = Node(value: value)
        prevItem?.next?.next = prevNextItem
        
        //Pointing to head node
        linkedList = head
    }
    var newList = linkedList([1, 2, 3, 4])
    
    //MARK: Print All nodes
    print("Current List")
    print(linkedList: newList)
    print("\n")
    
    insert(value: 9, at: 2, in: &newList)
    print("After insert 9 at index 2")
    print(linkedList: newList)
    
    
    print("\n")
    insert(value: 10, at: 0, in: &newList)
    print("After insert 10 at index 0")
    print(linkedList: newList)
    
    //Prints
    Current List
    1 -> 2 -> 3 -> 4
    
    After insert 9 at index 2
    1 -> 2 -> 9 -> 3 -> 4
    
    After insert 10 at index 0
    10 -> 1 -> 2 -> 9 -> 3 -> 4
    
    screenshot 2024 03 21 at 6.18.34e280afpm

    Example 7. Insert an element at the right position in Sorted Linked List

    screenshot 2024 03 21 at 6.24.01e280afpm
    func insert(value: Int, in sortedLinkedList: inout Node?) {
        guard sortedLinkedList != nil else {
            return
        }
        var head = sortedLinkedList
        while sortedLinkedList != nil, sortedLinkedList!.value < value  {
            var next = sortedLinkedList?.next
            if let nextValue = next?.value, nextValue > value {
                break
            }
            else {
                sortedLinkedList = sortedLinkedList?.next
            }
        }
        var prevItem = sortedLinkedList
        var prevNextItem = prevItem?.next
        prevItem?.next = Node(value: value)
        prevItem?.next?.next = prevNextItem
        
        //Pointing to head node
        sortedLinkedList = head
    }
    
    var newList = linkedList([1, 2, 3, 5])
    
    //MARK: Print All nodes
    print("Current List")
    print(linkedList: newList)
    print("\n")
    
    insert(value: 4, in: &newList)
    print("After insert 4 in sorted LinkedList")
    print(linkedList: newList)
    
    //Prints
    Current List
    1 -> 2 -> 3 -> 5
    
    After insert 4 in sorted LinkedList
    1 -> 2 -> 3 -> 4 -> 5

    Example 8. Append one linked list to the end of another

    screenshot 2024 03 21 at 8.07.19e280afpm
    func append(a: inout Node?, b: inout Node?) {
        var mergedLinkedListHeader = a
        
        while a?.next != nil {
            a = a?.next
        }
        a?.next = b
        a = mergedLinkedListHeader
        b = mergedLinkedListHeader
    }
    
    var a = linkedList([1, 2, 3, 4])
    var b = linkedList([5, 6, 7, 8])
    
    //MARK: Print All nodes
    print("A: Linked List")
    print(linkedList: a)
    print("\n")
    
    print("B: Linked List")
    print(linkedList: b)
    print("\n")
    
    print("A+B")
    append(a: &a, b: &b)
    print(linkedList: a)
    print("\n")
    
    //Prints
    A: Linked List
    1 -> 2 -> 3 -> 4
    
    B: Linked List
    5 -> 6 -> 7 -> 8
    
    A+B
    1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8
    

    Example 9. Split one Linked List to two Linked List, find a mid point and split it!

    🤔 Wait, why use 2 pointer?

    • Without 2 pointer, You need to get a length of linkedList first and then iterate linkedList again to split it.
    screenshot 2024 03 21 at 8.30.00e280afpm
    func split(list: inout Node?) -> (first: Node?, seconds: Node?) {
        var firstNode: Node? = list
        var secondsNode: Node?
        
        //for 1 step
        var slowPointer = list
        //for 2 steps
        var fastPointer = list?.next
        
        while fastPointer?.next != nil {
            slowPointer = slowPointer?.next
            fastPointer = fastPointer?.next?.next
        }
        //Split first list
        secondsNode = slowPointer?.next
        slowPointer?.next = nil
        return (firstNode, secondsNode)
    }

    Test case 1. Input = [1, 2, 3, 4, 5, 6, 7, 8]

    var list = linkedList([1, 2, 3, 4, 5, 6, 7, 8])
    //MARK: Print All nodes
    print("Linked List")
    print(linkedList: list)
    print("\n")
    
    var (firstSubList, secondsSublist) = split(list: &list)
    
    print("First Sub Linked List")
    print(linkedList: firstSubList)
    print("\n")
    
    print("Seconds Sub Linked List")
    print(linkedList: secondsSublist)
    print("\n")
    
    /Prints
    Linked List
    1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8
    
    First Sub Linked List
    1 -> 2 -> 3 -> 4
    
    Seconds Sub Linked List
    5 -> 6 -> 7 -> 8

    Test case 2. Input = [1, 2, 3, 4, 5, 6, 7]

    var list = linkedList([1, 2, 3, 4, 5, 6, 7])
    //MARK: Print All nodes
    print("Linked List")
    print(linkedList: list)
    print("\n")
    
    var (firstSubList, secondsSublist) = split(list: &list)
    
    print("First Sub Linked List")
    print(linkedList: firstSubList)
    print("\n")
    
    print("Seconds Sub Linked List")
    print(linkedList: secondsSublist)
    print("\n")
    
    //Prints
    Linked List
    1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7
    
    First Sub Linked List
    1 -> 2 -> 3 -> 4
    
    Seconds Sub Linked List
    5 -> 6 -> 7
    
    screenshot 2024 03 21 at 9.26.42e280afpm

    Example 10. Remove duplicate nodes in a sorted linked list

    screenshot 2024 03 21 at 9.34.34e280afpm
    func removeDuplicates(linkedList: inout Node?) {
        var head: Node? = linkedList
        while linkedList?.next != nil {
            var nextNode = linkedList?.next
            //Check duplicates
            if linkedList?.value == nextNode?.value {
                while nextNode?.value == nextNode?.next?.value {
                    var tempNext = nextNode?.next
                    nextNode = tempNext
                }
                linkedList?.next = nextNode?.next
            }
            linkedList = linkedList?.next
        }
        linkedList = head
    }
    
    var list = linkedList([1, 2, 3, 3, 3, 4, 4, 5])
    //MARK: Print All nodes
    print("Linked List")
    print(linkedList: list)
    print("\n")
    
    
    removeDuplicates(linkedList: &list)
    print("Removed duplicates in Linked List")
    print(linkedList: list)
    print("\n")
    screenshot 2024 03 21 at 9.49.39e280afpm

    Example 11. Move node from the head of one list and add to the front of another

    screenshot 2024 03 21 at 9.56.04e280afpm
    func moveHeadToAnother(source: inout Node?, dest: inout Node?) {
        var sourceHead = source
        source = source?.next
        
        var destHead = dest
        sourceHead?.next = destHead
        dest = sourceHead
    }
    
    var source = linkedList([1, 2, 3])
    var dest = linkedList([7, 8, 9])
    //MARK: Print All nodes
    print("Source Linked List")
    print(linkedList: source)
    print("\n")
    
    print("Destination Linked List")
    print(linkedList: dest)
    print("\n")
    
    moveHeadToAnother(source: &source, dest: &dest)
    print("Result: Source Linked List")
    print(linkedList: source)
    print("\n")
    
    print("Result: Destination Linked List")
    print(linkedList: dest)
    print("\n")
    
    //Prints
    Source Linked List
    1 -> 2 -> 3
    
    Destination Linked List
    7 -> 8 -> 9
    
    Result: Source Linked List
    2 -> 3
    
    Result: Destination Linked List
    1 -> 7 -> 8 -> 9

    🤯 Example 12. Merge two sorted linked list and then return a new sorted linked list

    screenshot 2024 03 21 at 10.26.58e280afpm
    func merge(a: Node?, b: Node?) -> Node? {
        //Edge cases
        if a == nil, b == nil {
            return nil
        }
        if a == nil, b != nil {
            return b
        }
        if a != nil, b == nil {
            return a
        }
        
        var result: Node?
        var resultHead: Node?
        var headA = a
        var headB = b
        
        if headA!.value < headB!.value {
            resultHead = headA
            headA = headA?.next
        }
        else {
            resultHead = headB
            headB = headB?.next
        }
        resultHead?.next = nil
        result = resultHead
        
        while headA != nil && headB != nil {
            if headA!.value < headB!.value {
                result?.next = headA
                headA = headA?.next
            }
            else {
                result?.next = headB
                headB = headB?.next
            }
            result = result?.next
        }
        //Add rest of nodes
        var restNodes = headA ?? headB
        //When one list runs out make sure the remaining elements of the other are appended to the end of the result
        result?.next = restNodes
        //Pointing to head
        result = resultHead
        return result
    }
    
    var listA = linkedList([1, 2, 5])
    var listB = linkedList([1, 4, 6])
    //MARK: Print All nodes
    print("listA Linked List")
    print(linkedList: listA)
    print("\n")
    
    print("listB Linked List")
    print(linkedList: listB)
    print("\n")
    
    let mergedList = merge(a: listA, b: listB)
    print("Merged Linked List")
    print(linkedList: mergedList)
    print("\n")
    
    //Prints
    listA Linked List
    1 -> 2 -> 5
    
    listB Linked List
    1 -> 4 -> 6
    
    Merged Linked List
    1 -> 1 -> 2 -> 4 -> 5 -> 6
    

    Example 13. Reverse a Linked List

    screenshot 2024 03 22 at 12.48.24e280afpm

    Code

    func reverseLinkedList(input: Node?) -> Node? {
        //Prev will become a head of reversed linked list
        var prev = input
        var current = input?.next
        
        //At this time, prev is tail node, but after while loop, It become a head node
        prev?.next = nil
        while current != nil {
            //next node is input's next step
            var next = current?.next
            //It makes reversed direction
            current?.next = prev
            
            //next step for reversed linkedList
            prev = current
            current = next
        }
        return prev
    }
    
    var input = linkedList([1, 2, 5])
    //MARK: Print All nodes
    print("Input Linked List")
    print(linkedList: input)
    print("\n")
    
    var reversedLinkedList = reverseLinkedList(input: input)
    
    print("Reversed Linked List")
    print(linkedList: reversedLinkedList)
    print("\n")
    
    //Prints
    Input Linked List
    1 -> 2 -> 5
    
    Reversed Linked List
    5 -> 2 -> 1
    
    screenshot 2024 03 22 at 8.18.42e280afpm
    screenshot 2024 03 22 at 8.19.27e280afpm

    Doubly Linked List

    A doubly linked list has pointers to the next element as well as pointers to the previous element.

    This requires additional memory in every node to store the previous pointer.

    https://www.udemy.com/course/break-away-coding-interviews-1/learn/lecture/8462452#content

    screenshot 2024 03 28 at 10.33.46e280afpm
    class Node {
        let value: Int
        var prev: Node?
        var next: Node?
        
        init(value: Int, prev: Node? = nil, next: Node? = nil) {
            self.value = value
            self.prev = prev
            self.next = next
        }
    }
    
    func createDoublyLinkedList(input: [Int]) -> Node? {
        guard !input.isEmpty else {
            return nil
        }
        var queue = input
        var head: Node? = Node(value: queue.removeFirst())
        var temp = head
        
        while !queue.isEmpty {
            var currentNode = Node(value: queue.removeFirst())
            temp?.next = currentNode
            currentNode.prev = temp
            //Pointing to the next
            temp = currentNode
        }
        return head
    }
    
    func printAll(head: Node?) {
        guard let head else {
            return
        }
        var node: Node? = head
        while node != nil {
            
            print("Node: \(node!.value), prev: \(node?.prev?.value) next: \(node?.next?.value)", terminator: node?.next == nil ? "" : " <-> ")
            node = node?.next
        }
    }
    
    
    var head = createDoublyLinkedList(input: [1, 2, 3])
    printAll(head: head)
    
    //Prints
    current: 1, prev: nil next: Optional(2) <-> current: 2, prev: Optional(1) next: Optional(3) <-> current: 3, prev: Optional(2) next: nil

    Deleting a node in a doubly linked list

    screenshot 2024 03 28 at 11.16.30e280afpm

    Notes. Consider edge cases

    • Delete head
    • Delete tail
    • Delete node
    class Node {
        let value: Int
        var prev: Node?
        var next: Node?
        
        init(value: Int, prev: Node? = nil, next: Node? = nil) {
            self.value = value
            self.prev = prev
            self.next = next
        }
        
        deinit {
            print("🔥 deinit: \(value)")
        }
    }
    
    func createDoublyLinkedList(input: [Int]) -> Node? {
        guard !input.isEmpty else {
            return nil
        }
        var queue = input
        var head: Node? = Node(value: queue.removeFirst())
        var temp = head
        
        while !queue.isEmpty {
            var currentNode = Node(value: queue.removeFirst())
            temp?.next = currentNode
            currentNode.prev = temp
            //Pointing to the next
            temp = currentNode
        }
        return head
    }
    
    func printAll(head: Node?) {
        guard let head else {
            return
        }
        var node: Node? = head
        while node != nil {
            print("Node: \(node!.value), prev: \(node?.prev?.value) next: \(node?.next?.value)", terminator: node?.next == nil ? "" : " <-> ")
            node = node?.next
        }
        print("\n")
    }
    
    func delete(value: Int, in linkedList: inout Node?) {
        guard linkedList != nil else {
            return
        }
        print("Delete : \(value)")
        var head: Node? = linkedList
        var temp: Node? = head
        
        //Step find node to be deleted
        while temp != nil && temp?.value != value {
            temp = temp?.next
        }
    
        //Edge case, Not found value
        if temp == nil {
            return
        }
        
        //Unlink
        if temp?.prev != nil {
            temp?.prev?.next = temp?.next
        }
        else {
            //Edge case, It deleting node is head, set new head
            linkedList = temp?.next
            //Note, after removed the node, new head's prev became nil because It removed from memory
        }
        
        if temp?.next != nil {
            temp?.next?.prev = temp?.prev
        }
    }
    
    var head = createDoublyLinkedList(input: [1, 2, 3])
    printAll(head: head)
    delete(value: 1, in: &head)
    
    print("\nAfter delete\n")
    printAll(head: head)
    
    //Prints
    Node: 1, prev: nil next: Optional(2) <-> Node: 2, prev: Optional(1) next: Optional(3) <-> Node: 3, prev: Optional(2) next: nil
    
    Delete : 1
    🔥 deinit: 1
    
    After delete
    
    Node: 2, prev: nil next: Optional(3) <-> Node: 3, prev: Optional(2) next: nil
    
  • Swift, Data Structure – Tree Structure

    Swift, Data Structure – Tree Structure

    ✍️ Note

    Some codes are sourced from Loony Corn’s Udemy Course (https://www.udemy.com/user/janani-ravi-2/). This post is for personal notes where I summarize the original contents to grasp the key concepts

    screenshot 2024 03 17 at 11.04.13e280afam

    Tree Data Structure

    class Node {
        var left: Node?
        var right: Node?
        var value: Int
        
        init(value: Int) {
            self.value = value
        }
    }
    
    //Create a Tree
    let root = Node(value: 5)
    
    root.left = Node(value: 3)
    root.right = Node(value: 7)
    
    root.right?.left = Node(value: 6)
    root.right?.right = Node(value: 9)

    BFS: Breadth First Search

    func bfs(root: Node) {
        var root = root
        var queue = [Node]()
        queue.append(root)
        while !queue.isEmpty {
            let dequeue = queue.removeFirst()
            print("\(dequeue.value)", terminator: " ")
            if let leftChild = dequeue.left {
                queue.append(leftChild)
            }
            if let rightChild = dequeue.right {
                queue.append(rightChild)
            }
        }
    }
    
    bfs(root: root)
    screenshot 2024 03 17 at 11.12.59e280afam

    DFS: Depth First Search

    Use Recursion!

    • Define base case -> when the root is null
    • Recursion technique -> Processing can be performed
      • Before -> Pre-Order
      • In-Between or -> In-Order
      • After -> Post-Order

    Pre-Order

    Each node is processed first (PRE) before It’s right and left subtrees

    screenshot 2024 03 17 at 11.45.19e280afam
    screenshot 2024 03 17 at 11.47.18e280afam

    In-Order

    screenshot 2024 03 17 at 11.49.31e280afam
    screenshot 2024 03 17 at 11.49.13e280afam

    Post-Order

    screenshot 2024 03 17 at 11.58.11e280afam
    screenshot 2024 03 17 at 11.58.25e280afam
    func dfs(root: Node?) {
        //base: node is not null
        guard root != nil else {
            return
        }
        dfs(root: root?.left)
        dfs(root: root?.right)
        //Post-Order - print value after recursing to the left and right subtrees
        print("\(root!.value)", terminator: "  ")
    }
    dfs(root: root)
    
    //Prints
    3  6  9  7  5  
    

    Binary Search Tree

    It is called an Ordered Binary Tree.

    • Each node in the left sub-tree of that node has a value less than or equal to the value of the node
    • Each node in the right sub-tree of that node has a value greater than the value of the node

    Key Feature

    • Typically used for Fast Insertion and Fast Lookup
    • Fast Insertion
      • In a tree when a new node is added there is exactly one place that it can be
      • The structure of a tree depends on the order in which the nodes are added
    • Fast Lookup
      • While searching for a node in the tree there is only one place where that node can be found
      • You can simply follow the right or left sub-trees based on the value you want to find

    Insertion

    🙏 Below Images are wrong, Level 4 -> Level 3.

    screenshot 2024 03 17 at 3.43.01e280afpm

    Let’s see step by step.

    Step 1

    screenshot 2024 03 17 at 3.59.24e280afpm

    Step 2

    screenshot 2024 03 17 at 3.59.46e280afpm

    Step 3

    screenshot 2024 03 17 at 4.00.09e280afpm

    Step 4

    screenshot 2024 03 17 at 4.00.28e280afpm

    Step 5

    screenshot 2024 03 17 at 4.00.45e280afpm

    Step 6

    screenshot 2024 03 17 at 4.00.59e280afpm

    Step 7

    screenshot 2024 03 17 at 4.01.14e280afpm

    Step 8 -> Finish

    screenshot 2024 03 17 at 4.01.29e280afpm
    class Node {
        var left: Node?
        var right: Node?
        var value: Int
        
        init(value: Int) {
            self.value = value
        }
    }
    
    
    let root = Node(value: 8)
    
    //Level 1
    root.left = Node(value: 6)
    root.right = Node(value: 14)
    
    //Level 2
    root.left?.left = Node(value: 4)
    root.left?.right = Node(value: 7)
    root.right?.right = Node(value: 16)
    
    //Level 3
    root.right?.right?.right = Node(value: 18)
    
    func insertion(root: Node?, newNode: Node) -> Node? {
        if root == nil {
            return newNode
        }
        if newNode.value < root!.value {
            root?.left = insertion(root: root?.left, newNode: newNode)
        }
        else {
            root?.right = insertion(root: root?.right, newNode: newNode)
        }
        return root
    }
    
    insertion(root: root, newNode: Node(value: 15))
    
    func bfs(root: Node) {
        var root = root
        var queue = [Node]()
        queue.append(root)
        while !queue.isEmpty {
            let dequeue = queue.removeFirst()
            print("\(dequeue.value)", terminator: " ")
            if let leftChild = dequeue.left {
                queue.append(leftChild)
            }
            if let rightChild = dequeue.right {
                queue.append(rightChild)
            }
        }
    }
    bfs(root: root)
    
    //Prints
    8 6 14 4 7 16 15 18
    

    Look Up

    You can consider 2 case. Found or Not Found

    Found case

    🙏 Below Images are wrong, Level 4 -> Level 3.

    screenshot 2024 03 17 at 4.06.51e280afpm

    Not Found case

    screenshot 2024 03 17 at 4.07.16e280afpm

    Let’s implement Look Up function

    func lookUp(node: Node, from root: Node?) -> Node? {
        if root == nil {
            return nil
        }
        if node.value == root?.value {
            return root
        }
        if node.value < root!.value {
            return lookUp(node: node, from: root?.left)
        }
        else {
            return lookUp(node: node, from: root?.right)
        }
    }
    lookUp(node: Node(value: 7), from: root)?.value == 7 //found, true
    lookUp(node: Node(value: 20), from: root)?.value == 20 //Not found, false
    Insertion Look UP
    Average complexity O(logN) Average complexity O(logN)
    The actual complexity depends on the shape of the tree – Skewed tree (I.E with all left or right children only) can have complexity of O(n) For both insertion and lookup you halve the tree you have to traverse at every step. This gives us the O(logN) complexity
    Source: Udemy

    Let’s practice. Binary Tree Problems

    Exercise 1. Minimum Value

    screenshot 2024 03 17 at 4.31.08e280afpm
    screenshot 2024 03 17 at 4.30.47e280afpm
    func minimumValue(root: Node?) -> Int {
        if root == nil {
            return 0 //or return Int.min
        }
        //Left child has a minimum value
        if root?.left == nil {
            return root!.value
        }
        return minimumValue(root: root?.left)
    }
    
    minimumValue(root: root)

    Exercise 2. Maximum Depth

    🙏 Below Images are wrong, Level 4 -> Level 3.

    screenshot 2024 03 17 at 4.39.20e280afpm
    func maximumDepth(root: Node?) -> Int {
        if root == nil {
            return 0
        }
        //Leaf node
        if root?.left == nil, root?.right == nil {
            return 0
        }
        //Count up when look up next level
        let leftDepth = 1 + maximumDepth(root: root?.left)
        let rightDepth = 1 + maximumDepth(root: root?.right)
        return max(leftDepth, rightDepth)
    }
    
    maximumDepth(root: root) //Prints 3

    Exercise 3. Mirror Tree

    screenshot 2024 03 17 at 4.52.06e280afpm
    func mirror(root: Node?) {
        if root == nil {
            return
        }
        mirror(root: root?.left)
        mirror(root: root?.right)
        //Switch left and right child
        let temp = root?.left
        root?.left = root?.right
        root?.right = temp
    }
    mirror(root: root)
    screenshot 2024 03 17 at 5.02.01e280afpm

    🤯 Exercise 4. Create the number of structurally unique Binary Trees possible

    Problem definition

    • Let say If you have 3 nodes, How many various binary trees you can create?
    screenshot 2024 03 17 at 5.15.00e280afpm
    func countTrees(nodeCount: Int) -> Int {
        //Base case: node 1 -> possible tree is 1, If it is zero then root node will be null
        if nodeCount <= 1 {
            return 1
        }
        var sum = 0
        //Every node can be the root
        for i in 1...nodeCount {
            //Left and right form their own sub trees
            //The nodes before it will be on the left
            let leftTrees = countTrees(nodeCount: i - 1)
            //THe nodes after it on the right
            let rightTrees = countTrees(nodeCount: nodeCount - i)
            
            //leftTree * rightTrees = the number of possible trees with this root
            sum = sum + leftTrees * rightTrees
        }
        return sum
    }
    
    countTrees(nodeCount: 3) //Prints 5

    Exercise 5. Print all nodes within a range in a Binary Search Tree

    screenshot 2024 03 17 at 5.39.10e280afpm
    func printAllNodeWithin(root: Node?, range: ClosedRange<Int>) {
        if root == nil {
            return
        }
    //If the range low value is less than the current, run the operation on the left subtree
        if root!.value >= range.lowerBound {
            printAllNodeWithin(root: root?.left, range: range)
        }
        //In-Order
        if range ~= root!.value {
            print("\(root!.value)", terminator: "  ")
        }
    //If the range high value is greater than the current node, run the operation on the right subtree
        if root!.value < range.upperBound {
            printAllNodeWithin(root: root?.right, range: range)
        }
    }
    print("\nExercise: Print All Nodes\n")
    printAllNodeWithin(root: root, range: 6...14)
    
    //Prints
    Exercise: Print All Nodes
    6  7  8  14 

    Exercise 6. Is a Binary Tree is a Binary Search Tree?

    Remind what is Binary Search Tree?

    • For every node in a binary search tree – The nodes with values <= node are in the left subtree and nodes with values > node are in the right sub tree
    screenshot 2024 03 17 at 6.06.26e280afpm
    screenshot 2024 03 17 at 6.05.34e280afpm
    let nonBinarySearchTreeRoot = Node(value: 8)
    
    //Level 1
    nonBinarySearchTreeRoot.left = Node(value: 14)
    nonBinarySearchTreeRoot.right = Node(value: 6)
    
    //Level 2
    nonBinarySearchTreeRoot.left?.left = Node(value: 4)
    nonBinarySearchTreeRoot.left?.right = Node(value: 7)
    nonBinarySearchTreeRoot.right?.right = Node(value: 16)
    
    //Level 3
    nonBinarySearchTreeRoot.right?.right?.right = Node(value: 18)
    
    func isBinarySearchTree(root: Node?) -> Bool {
        //Null node consider valid binary search tree
        if root == nil {
            return true
        }
        if let left = root?.left?.value, root!.value < left {
            return false
        }
        if let right = root?.right?.value, root!.value > right {
            return false
        }
        return isBinarySearchTree(root: root?.left) && isBinarySearchTree(root: root?.right)
    }
    isBinarySearchTree(root: nonBinarySearchTreeRoot) //Prints false
    

    Exercise 7. Has Path Sum?

    screenshot 2024 03 17 at 6.24.49e280afpm
    func hasPathSum(root: Node?, sum: Int) -> Bool {
        //Check hasPathSum or not
        if root?.left == nil, root?.right == nil {
            return root?.value == sum
        }
        if root == nil {
            return false
        }
        let subSum = sum - root!.value
        if let left = root?.left {
            var hasPathSum = hasPathSum(root: root?.left, sum: subSum)
            if hasPathSum {
                return true
            }
        }
        if let right = root?.right {
            var hasPassSum = hasPathSum(root: root?.right, sum: subSum)
            if hasPassSum {
                return true
            }
        }
        return false
    }
    
    hasPathSum(root: root, sum: 21) //True
    

    Exercise 8. Print Paths from Root -> Leaf node

    screenshot 2024 03 17 at 6.47.36e280afpm
    func printAllPathes(root: Node?, array: inout [Int]) {
        if root == nil {
            return
        }
        array.append(root!.value)
        printAllPathes(root: root?.left, array: &array)
        printAllPathes(root: root?.right, array: &array)
        
        if root?.left == nil, root?.right == nil {
            print("Path: \(array)")
        }
        array.removeLast()
    }
    
    //Prints
    Path: [8, 6, 4]
    Path: [8, 6, 7]
    Path: [8, 14, 16, 18]

    Exercise 9. Find the Least Common Ancestor for 2 nodes

    screenshot 2024 03 17 at 6.53.21e280afpm

    Let’s check step by step.

    screenshot 2024 03 17 at 7.34.34e280afpm
    screenshot 2024 03 17 at 7.34.55e280afpm
    screenshot 2024 03 17 at 7.35.07e280afpm

    Remember, This step is important!

    screenshot 2024 03 17 at 7.35.27e280afpm
    screenshot 2024 03 17 at 7.36.08e280afpm
    screenshot 2024 03 17 at 7.36.18e280afpm
    screenshot 2024 03 17 at 7.36.27e280afpm
    screenshot 2024 03 17 at 7.36.43e280afpm
    screenshot 2024 03 17 at 7.36.54e280afpm
    screenshot 2024 03 17 at 7.37.03e280afpm
    screenshot 2024 03 17 at 7.37.14e280afpm
    screenshot 2024 03 17 at 7.37.38e280afpm
    screenshot 2024 03 17 at 7.37.55e280afpm

    Found! The lease common ancestor is 3

    screenshot 2024 03 17 at 7.38.22e280afpm
    //It's not a Binary Search Tree
    let findAncestorRoot = Node(value: 1)
    
    //Level 1
    findAncestorRoot.left = Node(value: 2)
    findAncestorRoot.right = Node(value: 3)
    
    //Level 2
    findAncestorRoot.right?.left = Node(value: 7)
    findAncestorRoot.right?.right = Node(value: 6)
    
    //Level 3
    findAncestorRoot.right?.left?.left = Node(value: 8)
    findAncestorRoot.right?.left?.right = Node(value: 5)
    findAncestorRoot.right?.right?.right = Node(value: 4)
    
    
    func findLeastCommonAncestor(root: Node?, targetNodeValue1: Int, targetNodeValue2: Int) -> Int? {
        if root == nil {
            return nil
        }
        //If the current root is either of the two node then return it itself. (It will be use later)
        if root!.value == targetNodeValue1 || root!.value == targetNodeValue2 {
            return root!.value
        }
        let leftAncestor = findLeastCommonAncestor(root: root?.left, targetNodeValue1: targetNodeValue1, targetNodeValue2: targetNodeValue2)
        let rightAncestor = findLeastCommonAncestor(root: root?.right, targetNodeValue1: targetNodeValue1, targetNodeValue2: targetNodeValue2)
        
        //If both exist it means - either the node or it's ancestor exist in the left and right subtree. So current node is the least common ancestor
        if let leftAncestor, let rightAncestor {
            return root!.value
        }
        if let leftAncestor {
            return leftAncestor
        }
        return rightAncestor
    }
    
    findLeastCommonAncestor(root: findAncestorRoot, targetNodeValue1: 8, targetNodeValue2: 6) //It is 3
    screenshot 2024 03 17 at 7.43.01e280afpm

    Exercise 10. iOS, Find the Least Common Ancestor for 2 subview

    UIView is not a binary tree. It has more 2 child views.

    screenshot 2024 03 17 at 9.11.45e280afpm
    func createNodeView(tag: Int) -> UIView {
        let view = UIView()
        view.tag = tag
        return view
    }
    
    //Level 0
    let rootView = createNodeView(tag: 1)
    
    //Level 1
    let tag3View = createNodeView(tag: 3)
    rootView.addSubview(createNodeView(tag: 2))
    rootView.addSubview(tag3View)
    
    
    //Level 2
    let tag7View = createNodeView(tag: 7)
    let tag6View = createNodeView(tag: 6)
    let tag9View = createNodeView(tag: 9)
    tag3View.addSubview(tag7View)
    tag3View.addSubview(tag6View)
    tag3View.addSubview(tag9View)
    
    //Level 3
    tag7View.addSubview(createNodeView(tag: 11))
    tag6View.addSubview(createNodeView(tag: 8))
    tag6View.addSubview(createNodeView(tag: 5))
    
    tag9View.addSubview(createNodeView(tag: 4))
    func findLeastCommonAncestor(root: UIView?, targetNodeValue1: Int, targetNodeValue2: Int) -> UIView? {
        print("visited: \(root?.tag)")
        if root == nil {
            return nil
        }
        //If the current root is either of the two node then return it itself. (It will be use later)
        if root!.tag == targetNodeValue1 || root!.tag == targetNodeValue2 {
            return root
        }
        var ancestors = [UIView]()
        for subView in root!.subviews {
            if let ancestor =
                findLeastCommonAncestor(root: subView, targetNodeValue1: targetNodeValue1, targetNodeValue2: targetNodeValue2) {
                ancestors.append(ancestor)
            }
        }
        print(ancestors.compactMap { $0.tag })
        let tags = ancestors.compactMap { $0.tag }
        //If rootView has both nodes then return it
        if tags.contains(targetNodeValue1), tags.contains(targetNodeValue2) {
            return root
        }
        if ancestors.isEmpty {
            return nil
        }
    //Need to check whether It's okay to return last item or not..but most of cases It works correctly so far.
        return ancestors.last
    }
    screenshot 2024 03 17 at 9.13.50e280afpm
  • iOS, NSCoding

    iOS, NSCoding

    ✍️ Note

    Some 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

    Reference

    Archives and serializations are two ways in which you can create architecture-independent byte streams of hierarchical data. Byte streams can then be written to a file or transmitted to another process, perhaps over a network. When the byte stream is decoded, the hierarchy is regenerated. Archives provide a detailed record of a collection of interrelated objects and values. Serializations record only the simple hierarchy of property-list values

    Apple

    Example 1, Conforms NSCoding

    class Person: NSObject, NSCoding {
        var name: String
        var age: Int
        var friend: Person?
        
        init(name: String, age: Int, friend: Person? = nil) {
            self.name = name
            self.age = age
            self.friend = friend
        }
        
        required convenience init?(coder: NSCoder) {
            guard let name = coder.decodeObject(forKey: "name") as? String
            else {
                return nil
            }
            let age = coder.decodeInteger(forKey: "age")
            let friend = coder.decodeObject(forKey: "friend") as? Person
            
            self.init(name: name, age: age, friend: friend)
        }
        
        func encode(with coder: NSCoder) {
            coder.encode(name, forKey: "name")
            coder.encode(age, forKey: "age")
            coder.encode(friend, forKey: "friend")
        }
    }
    
    func savePerson(_ person: Person, to path: String) {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: person, requiringSecureCoding: false)
            try data.write(to: URL(fileURLWithPath: path))
            print("Saved Person")
        } catch {
            print("Failed: \(error)")
        }
    }
    
    func loadPerson(from path: String) -> Person? {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path))
    
    //Instead unarchiveTopLevelObjectWithData You can use unarchivedObject(ofClass:from:) when you conforms NSSecureCoding protocol
    
            if let loadedPerson = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Person {
                return loadedPerson
            }
        } catch {
            print("Failed: \(error)")
        }
        return nil
    }
    
    let friend = Person(name: "Bob", age: 35)
    let person = Person(name: "Alice", age: 30, friend: friend)
    let filePath = NSTemporaryDirectory() + "person_with_friend.archive"
    savePerson(person, to: filePath)
    
    if let loadedPerson = loadPerson(from: filePath) {
        if let loadedFriend = loadedPerson.friend {
            print("Person: \(loadedPerson.name), \(loadedPerson.age)")
            print("Friend: \(loadedFriend.name), \(loadedFriend.age)")
        } else {
            print("No friends")
        }
    }
    screenshot 2024 03 16 at 10.21.56e280afpm

    Example 2. Conforms NSSecureCoding

    screenshot 2024 03 17 at 10.53.19e280afam

    In example 1, You may see this warning. unarchiveTopLevelObjectWithData was deprecated. To resolve it, let’s conforms NSSecureCoding.

    screenshot 2024 03 17 at 10.55.43e280afam
    screenshot 2024 03 17 at 10.54.56e280afam
    class Person: NSObject, NSSecureCoding {
        static var supportsSecureCoding = true
        var name: String
        var age: Int
        var friend: Person?
        
        init(name: String, age: Int, friend: Person? = nil) {
            self.name = name
            self.age = age
            self.friend = friend
        }
        
        required convenience init?(coder: NSCoder) {
            guard let name = coder.decodeObject(forKey: "name") as? String
            else {
                return nil
            }
            let age = coder.decodeInteger(forKey: "age")
            let friend = coder.decodeObject(forKey: "friend") as? Person
            
            self.init(name: name, age: age, friend: friend)
        }
        
        func encode(with coder: NSCoder) {
            coder.encode(name, forKey: "name")
            coder.encode(age, forKey: "age")
            coder.encode(friend, forKey: "friend")
        }
    }
    func savePerson(_ person: Person, to path: String) {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: person, requiringSecureCoding: true)
            try data.write(to: URL(fileURLWithPath: path))
            print("Saved Person")
        } catch {
            print("Failed: \(error)")
        }
    }
    
    func loadPerson(from path: String) -> Person? {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: path))
            if let loadedPerson = try NSKeyedUnarchiver.unarchivedObject(ofClass: Person.self, from: data) {
                return loadedPerson
            }
        } catch {
            print("Failed: \(error)")
        }
        
        return nil
    }
    
    let friend = Person(name: "Bob", age: 35)
    let person = Person(name: "Alice", age: 30, friend: friend)
    let filePath = NSTemporaryDirectory() + "person_with_friend.archive"
    savePerson(person, to: filePath)
    
    if let loadedPerson = loadPerson(from: filePath) {
        if let loadedFriend = loadedPerson.friend {
            print("Person: \(loadedPerson.name), \(loadedPerson.age)")
            print("Friend: \(loadedFriend.name), \(loadedFriend.age)")
        } else {
            print("No friends")
        }
    }
    

  • UIKit: UIView, UIImageView

    UIKit: UIView, UIImageView

    ✍️ Note

    Some 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

    When layoutSubViews has called?

    screenshot 2024 03 16 at 9.56.24e280afpm

    Subclasses can override this method as needed to perform more precise layout of their subviews. You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.

    You should not call this method directly. If you want to force a layout update, call the setNeedsLayout() method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded() method.

    https://developer.apple.com/documentation/uikit/uiview/1622482-layoutsubviews

    class CustomView: UIView {
        override func layoutSubviews() {
            super.layoutSubviews()
            print("layoutSubviews called")
        }
    }
    
    //In ViewController
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        print("viewDidAppear")
            
        print("✅ setNeedsLayout called")
        customView.setNeedsLayout()
    }
    
    //Prints
    ✅ setNeedsLayout called
    layoutSubviews called

    Test it your self. When you call a setNeedsLayout a layoutSubviews in CustomView will be called.

    viewWillLayoutSubviews and viewDidLayoutSubviews

    When a view’s bounds change, the view adjusts the position of its subviews. Your view controller can override this method to make changes before the view lays out its subviews. The default implementation of this method does nothing.

    https://developer.apple.com/documentation/uikit/uiviewcontroller/1621437-viewwilllayoutsubviews

    override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            print("viewDidAppear")
            
            print("✅ setNeedsLayout called")
            customView.setNeedsLayout()
            
            //Change subview's bounds!
            var customBounds = customView.bounds
            customView.bounds = CGRect(x: 10, y: 10, width: customBounds.width, height: customBounds.height)
        }
    screenshot 2024 03 16 at 9.40.23e280afpm

    When you changed a subview’s bounds, viewWillLayoutSubviews and viewDidLayoutSubviews are called. And then subview’s layoutSubviews is also called

    screenshot 2024 03 16 at 9.57.39e280afpm

    When use Autolayout and changes NSLayoutConstraints, It also calls viewWillLayoutSubviews and viewDidLayoutSubviews

    What is intrinsicContentSize?

    The natural size for the receiving view, considering only properties of the view itself.

    Custom views typically have content that they display of which the layout system is unaware. Setting this property allows a custom view to communicate to the layout system what size it would like to be based on its content. This intrinsic size must be independent of the content frame, because there’s no way to dynamically communicate a changed width to the layout system based on a changed height, for example.

    If a custom view has no intrinsic size for a given dimension, it can use noIntrinsicMetric for that dimension.

    https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize

    screenshot 2024 03 20 at 9.47.15e280afpm

    Let’s test. UILabel has 2 constraints which are leading and top. I set font and text. As you can see It’s intrinsicContentSize is based on it’s content.

    screenshot 2024 03 20 at 9.52.37e280afpm

    I added trailingAnchor. As you can see frame width is bigger than first one. But intrinsicSize doesn’t changed. Because It’s based on contents. Now we know that It is independent from frame

    UIImageView

    An image view uses its contentMode property and the configuration of the image itself to determine how to display the image. It’s best to specify images whose dimensions match the dimensions of the image view exactly, but image views can scale your images to fit all or some of the available space. If the size of the image view itself changes, it automatically scales the image as needed.

    You can create a resizable image that stretches using the resizableImage(withCapInsets:resizingMode:) method of UIImage. When using an image of this type, you typically set the image view’s content mode to UIView.ContentMode.scaleToFill so that the image stretches in the appropriate places and fills the image view’s bounds.

    Image scaling and alpha blending are two relatively expensive operations that can impact your app’s performance. To maximize performance of your image view code, consider the following tips:

    • Cache scaled versions of frequently used images. If you expect certain large images to be displayed frequently in a scaled-down thumbnail view, consider creating the scaled-down images in advance and storing them in a thumbnail cache. Doing so alleviates the need for each image view to scale them separately.
    • Use images whose size is close to the size of the image view. Rather than assigning a large image to an image view, created a scaled version that matches the current size of the image view. You can also create a resizable image object using the UIImage.ResizingMode.tile option, which tiles the image instead of scaling it.
    • Make your image view opaque whenever possible. Unless you’re intentionally working with images that contain transparency (drawing UI elements, for example), make sure the isOpaque property of your image view is set to true. For more information about how transparency is determined, see Determine the final transparency of the image.

    https://developer.apple.com/documentation/uikit/uiimageview

  • Swift, Dispatch Semaphore

    Swift, Dispatch Semaphore

    ✍️ Note

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

    References

    • Apple official documents
    • Raywenderlich.com

    An object that controls access to a resource across multiple execution contexts through use of a traditional counting semaphore.

    A dispatch semaphore is an efficient implementation of a traditional counting semaphore. Dispatch semaphores call down to the kernel only when the calling thread needs to be blocked. If the calling semaphore does not need to block, no kernel call is made.

    You increment a semaphore count by calling the signal() method, and decrement a semaphore count by calling wait() or one of its variants that specifies a timeout.

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

    screenshot 2024 03 16 at 4.33.56e280afpm

    It’s useful when you want to limit the number of task at a time.

    Example. Set value 4

    let semaphore = DispatchSemaphore(value: 4)
    
    for i in 0...10 {
        DispatchQueue.global().async {
            semaphore.wait()
            print("✅ Start download: \(i)")
            URLSession.shared.downloadTask(with: URL(string: "https://picsum.photos/200")!) { location, resopnse, error in
                let tempFileLocation = location
                print("✅ download completed: \(i)")
                semaphore.signal()
            }.resume()
        }
    }
    
    screenshot 2024 03 16 at 4.53.56e280afpm
  • iOS, Memory Layout, static library, dynamic library

    iOS, Memory Layout, static library, dynamic library

    ✍️ Note

    Some 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 about Dynamic and Static library was written for OSX Application.

    Two important factors that determine the performance of apps are their launch times and their memory footprints. Reducing the size of an app’s executable file and minimizing its use of memory once it’s launched make the app launch faster and use less memory once it’s launched. Using dynamic libraries instead of static libraries reduces the executable file size of an app. They also allow apps to delay loading libraries with special functionality only when they’re needed instead of at launch time. This feature contributes further to reduced launch times and efficient memory use.

    This article introduces dynamic libraries and shows how using dynamic libraries instead of static libraries reduces both the file size and initial memory footprint of the apps that use them. This article also provides an overview of the dynamic loader compatibility functions apps use to work with dynamic libraries at runtime.

    https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/OverviewOfDynamicLibraries.html#//apple_ref/doc/uid/TP40001873-SW1

    screenshot 2024 03 16 at 3.56.30e280afpm

    Xcode -> File -> New -> Target

    You can create a Framework or Static Library

    Static Library

    Most of an app’s functionality is implemented in libraries of executable code. When an app is linked with a library using a static linker, the code that the app uses is copied to the generated executable file. A static linker collects compiled source code, known as object code, and library code into one executable file that is loaded into memory in its entirety at runtime. The kind of library that becomes part of an app’s executable file is known as a static libraryStatic libraries are collections or archives of object files.

    Note: Static libraries are also known as static archive libraries and static linked shared libraries.

    When an app is launched, the app’s code—which includes the code of the static libraries it was linked with—is loaded into the app’s address space. Linking many static libraries into an app produces large app executable files. Figure 1 shows the memory usage of an app that uses functionality implemented in static libraries. Applications with large executables suffer from slow launch times and large memory footprints. Also, when a static library is updated, its client apps don’t benefit from the improvements made to it. To gain access to the improved functionality, the app’s developer must link the app’s object files with the new version of the library. And the apps users would have to replace their copy of the app with the latest version. Therefore, keeping an app up to date with the latest functionality provided by static libraries requires disruptive work by both developers and end users.

    Apple

    screenshot 2024 03 16 at 3.29.37e280afpm

    When you create a module using SPM, It is a static library.

    Dynamic Library

    A better approach is for an app to load code into its address space when it’s actually needed, either at launch time or at runtime. The type of library that provides this flexibility is called dynamic library. Dynamic libraries are not statically linked into client apps; they don’t become part of the executable file. Instead, dynamic libraries can be loaded (and linked) into an app either when the app is launched or as it runs.

    Using dynamic libraries, programs can benefit from improvements to the libraries they use automatically because their link to the libraries is dynamic, not static. That is, the functionality of the client apps can be improved and extended without requiring app developers to recompile the apps. Apps written for OS X benefit from this feature because all system libraries in OS X are dynamic libraries. This is how apps that use Carbon or Cocoa technologies benefit from improvements to OS X.

    Another benefit dynamic libraries offer is that they can be initialized when they are loaded and can perform clean-up tasks when the client app terminates normally. Static libraries don’t have this feature. For details, see Module Initializers and Finalizers.

    Apple

    screenshot 2024 03 16 at 3.34.29e280afpm

    Reduce dependencies on external frameworks and dynamic libraries

    Before any of your code runs, the system must find and load your app’s executable and any libraries on which it depends.

    The dynamic loader (dyld) loads the app’s executable file, and examines the Mach load commands in the executable to find frameworks and dynamic libraries that the app needs. It then loads each of the frameworks into memory, and resolves dynamic symbols in the executable to point to the appropriate addresses in the dynamic libraries.

    Each additional third-party framework that your app loads adds to the launch time. Although dyld caches a lot of this work in a launch closure when the user installs the app, the size of the launch closure and the amount of work done after loading it still depend on the number and sizes of the libraries loaded. You can reduce your app’s launch time by limiting the number of 3rd party frameworks you embed. Frameworks that you import or add to your app’s Linked Frameworks and Libraries setting in the Target editor in Xcode count toward this number. Built-in frameworks, like CoreFoundation, have a much lower impact on launch, because they use shared memory with other processes that use the same framework.

    https://developer.apple.com/documentation/xcode/reducing-your-app-s-launch-time

    Memory Layout

    Types of Variable

    https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW1

    screenshot 2024 03 16 at 4.09.14e280afpm

  • Swift, Property Wrappers

    Swift, Property Wrappers

    ✍️ Note

    Some 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

    Property Wrappers

    A property wrapper adds a layer of separation between code that manages how a property is stored and the code that defines a property. For example, if you have properties that provide thread-safety checks or store their underlying data in a database, you have to write that code on every property. When you use a property wrapper, you write the management code once when you define the wrapper, and then reuse that management code by applying it to multiple properties.

    When you apply a wrapper to a property, the compiler synthesizes code that provides storage for the wrapper and code that provides access to the property through the wrapper. (The property wrapper is responsible for storing the wrapped value, so there’s no synthesized code for that.) 

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

    screenshot 2024 03 16 at 2.43.04e280afpm

    Wrapped Value

    struct SmallRectangle {
        private var _height = TwelveOrLess()
        private var _width = TwelveOrLess()
        var height: Int {
            get { return _height.wrappedValue }
            set { _height.wrappedValue = newValue }
        }
        var width: Int {
            get { return _width.wrappedValue }
            set { _width.wrappedValue = newValue }
        }
    }

    projected Value

    The name of the projected value is the same as the wrapped value, except it begins with a dollar sign ($)

    Apple

    @propertyWrapper
    struct TwelveOrLess {
        private var number = 0
        private(set) var projectedValue = true
        var wrappedValue: Int {
            get { return number }
            set {
                number = min(newValue, 12)
                projectedValue = number == 0
            }
        }
        
        init() {
            self.number = 0
            self.projectedValue = true
        }
    }
    
    struct Number {
        @TwelveOrLess var lessNumber
    }
    
    var number = Number()
    number.lessNumber = 10
    number.$lessNumber
    

    Projected Value, property name should be projectedValue

    And Compiler doesn’t allow to mutating the projectedValue from the others. So Access control for it, setting the private(set) var is make sense.

  • Swift, Tips for solving data structure and algorithms

    Swift, Tips for solving data structure and algorithms

    Handling String

    How to access character?

    screenshot 2024 03 15 at 11.42.59e280afpm

    You can’t access character by integer.

    screenshot 2024 03 15 at 11.43.50e280afpm

    You should use str.index, It’s not good.

    Tip, Create a character array.

    screenshot 2024 03 15 at 11.45.59e280afpm

  • Swift, Modern Concurrency

    Swift, Modern Concurrency

    ✍️ Note

    Some 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

    Swift Concurrency Model

    screenshot 2024 03 15 at 12.34.44e280afpm

    The possible suspension points in your code marked with await indicate that the current piece of code might pause execution while waiting for the asynchronous function or method to return. This is also called yielding the thread because, behind the scenes, Swift suspends the execution of your code on the current thread and runs some other code on that thread instead. Because code with await needs to be able to suspend execution, only certain places in your program can call asynchronous functions or methods:

    • Code in the body of an asynchronous function, method, or property.
    • Code in the static main() method of a structure, class, or enumeration that’s marked with @main.
    • Code in an unstructured child task, as shown in Unstructured Concurrency below.

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

    Explicitly insert a suspension point

    Call Task.yield() method for long running operation that doesn’t contain any suspension point.

    func listPhotos(inGallery name: String) async throws -> [String] {
        // ... some asynchronous networking code ...
        // use Task.sleep for simulating networking logic
        try await Task.sleep(for: .seconds(2))
        let result = ["photo1.jpg", "photo2.jpg", "photo3.jpg"]
        return result
    }
    
    func generateSlideshow(forGallery gallery: String) async throws {
        let photos = try await listPhotos(inGallery: gallery)
        for photo in photos {
            // ... render a few seconds of video for this photo ...
            print("photo file: \(photo)")
            // Explicitly insert a suspending point
            // It allows other tasks to execute
            await Task.yield()
        }
    }
    
    Task {
        let _ = try? await generateSlideshow(forGallery: "Summer Vacation")
    }

    Wait, there is no function to resuming it explicitly?

    No.

    Suspends the current task and allows other tasks to execute.

    A task can voluntarily suspend itself in the middle of a long-running operation that doesn’t contain any suspension points, to let other tasks run for a while before execution returns to this task.

    If this task is the highest-priority task in the system, the executor immediately resumes execution of the same task. As such, this method isn’t necessarily a way to avoid resource starvation.

    Structuring long-running code this way (explicitly insert a suspension point) lets Swift balance between making progress on this task, and letting other tasks in your program make progress on their work.

    Task.yield(), Apple

    Wrap a throwing function

    When you define an asynchronous or throwing function, you mark it with async or throws, and you mark calls to that function with await or try. An asynchronous function can call another asynchronous function, just like a throwing function can call another throwing function.

    However, there’s a very important difference. You can wrap throwing code in a docatch block to handle errors, or use Result to store the error for code elsewhere to handle it. These approaches let you call throwing functions from nonthrowing code.

    Apple

    func photoList(inGallery: String) throws -> [String] {
        return ["photo1.jpg", "photo2.jpg"]
    }
    
    func photoListResult(inGallery name: String) -> Result<[String], Error> {
        return Result {
            try photoList(inGallery: name)
        }
    }

    Normal function can wrap a throwing function returning Result. But there’s no safe way to wrap asynchronous code so you can call it from synchronous code and wait for the result.

    The Swift standard library intentionally omits this unsafe functionality — trying to implement it yourself can lead to problems like subtle races, threading issues, and deadlocks. When adding concurrent code to an existing project, work from the top down. Specifically, start by converting the top-most layer of code to use concurrency, and then start converting the functions and methods that it calls, working through the project’s architecture one layer at a time. There’s no way to take a bottom-up approach, because synchronous code can’t ever call asynchronous code.

    Apple

    screenshot 2024 03 15 at 1.27.12e280afpm

    Above the example is working fine. But If you try to wrap a async throws function using Result, It can’t. See below example.

    screenshot 2024 03 15 at 1.27.58e280afpm

    Asynchronous Sequences

    let handle = FileHandle.standardInput
    for try await line in handle.bytes.lines {
        print(line)
    }

    In the same way that you can use your own types in a forin loop by adding conformance to the Sequence protocol, you can use your own types in a forawaitin loop by adding conformance to the AsyncSequence protocol.

    Apple

    Calling Asynchronous Functions in Parallel

    screenshot 2024 03 15 at 1.39.56e280afpm
    • Call asynchronous functions with asynclet when you don’t need the result until later in your code. This creates work that can be carried out in parallel.
    • Both await and asynclet allow other code to run while they’re suspended.
    • In both cases, you mark the possible suspension point with await to indicate that execution will pause, if needed, until an asynchronous function has returned.

    Apple

    🤯 Tasks and Task Groups

    func downloadPhoto(from url: String) async throws -> Data {
        //Download image
        try await URLSession.shared.data(from: URL(string: url)!).0
    }
    
    Task {
        let photoUrls = [
            "https://picsum.photos/200/300?grayscale",
            "https://picsum.photos/200",
            "https://picsum.photos/300"
        ]
        //async let, It implicitly create a new child task
        async let firstPhoto = downloadPhoto(from: photoUrls[0])
        async let secondPhoto = downloadPhoto(from: photoUrls[1])
        async let thirdPhoto = downloadPhoto(from: photoUrls[2])
        
        //3 child tasks are created
        let photos = try await [firstPhoto, secondPhoto, thirdPhoto]
    }
    screenshot 2024 03 15 at 2.01.07e280afpm

    Tasks are arranged in a hierarchy. Each task in a given task group has the same parent task, and each task can have child tasks. Because of the explicit relationship between tasks and task groups, this approach is called structured concurrency. The explicit parent-child relationships between tasks has several advantages:

    • In a parent task, you can’t forget to wait for its child tasks to complete.
    • When setting a higher priority on a child task, the parent task’s priority is automatically escalated.
    • When a parent task is canceled, each of its child tasks is also automatically canceled.
    • Task-local values propagate to child tasks efficiently and automatically.

    Apple

    Task Group

    Swift runs as many of these tasks concurrently as conditions allow.

    Apple

    Task {
        let photos = await withTaskGroup(of: Data.self) { group in
            let photoUrls = [
                "https://picsum.photos/200/300?grayscale",
                "https://picsum.photos/200",
                "https://picsum.photos/300"
            ]
            
            for photoUrl in photoUrls {
                group.addTask {
                    return try await downloadPhoto(from: photoUrl)
                }
            }
            
            var results: [Data] = []
            for await photo in group {
                results.append(photo)
            }
            return results
        }
    }
    screenshot 2024 03 15 at 2.11.39e280afpm

    Ops, There is an error. Because withTaskGroup doesn’t support error handling.

    withTaskGroup

    func withTaskGroup<ChildTaskResult, GroupResult>(
        of childTaskResultType: ChildTaskResult.Type,
        returning returnType: GroupResult.Type = GroupResult.self,
        body: (inout TaskGroup<ChildTaskResult>) async -> GroupResult
    ) async -> GroupResult where ChildTaskResult : Sendable

    withThrowingTaskGroup

    Task {
        let photos = try await withThrowingTaskGroup(of: Data.self) { group in
            let photoUrls = [
                "https://picsum.photos/200/300?grayscale",
                "https://picsum.photos/200",
                "https://picsum.photos/300"
            ]
            
            for photoUrl in photoUrls {
                //creates child tasks
                group.addTask {
                    return try await downloadPhoto(from: photoUrl)
                }
            }
            
            var results: [Data] = []
            for try await photo in group {
                results.append(photo)
            }
            return results
        }
    }
    
    screenshot 2024 03 15 at 2.16.00e280afpm

    forawaitin loop waits for the next child task to finish, appends the result of that task to the array of results, and then continues waiting until all child tasks have finished. Finally, the task group returns the array of downloaded photos as its overall result.

    Apple

    I fixed it by using withThrowingTaskGroup

    screenshot 2024 03 15 at 2.19.01e280afpm

    Task Cancellation

    Swift concurrency uses a cooperative cancellation model. Each task checks whether it has been canceled at the appropriate points in its execution, and responds to cancellation appropriately. Depending on what work the task is doing, responding to cancellation usually means one of the following:

    • Throwing an error like CancellationError
    • Returning nil or an empty collection
    • Returning the partially completed work

    Downloading pictures could take a long time if the pictures are large or the network is slow. To let the user stop this work, without waiting for all of the tasks to complete, the tasks need check for cancellation and stop running if they are canceled. There are two ways a task can do this: by calling the Task.checkCancellation() method, or by reading the Task.isCancelled property.

    Calling checkCancellation() throws an error if the task is canceled; a throwing task can propagate the error out of the task, stopping all of the task’s work. This has the advantage of being simple to implement and understand. For more flexibility, use the isCancelled property, which lets you perform clean-up work as part of stopping the task, like closing network connections and deleting temporary files.

    Apple

    Task {
        let photos = try await withThrowingTaskGroup(of: Optional<Data>.self) { group in
            let photoUrls = [
                "https://picsum.photos/200/300?grayscale",
                "https://picsum.photos/200",
                "https://picsum.photos/300"
            ]
            
            for photoUrl in photoUrls {
                group.addTaskUnlessCancelled {
                    return try await downloadPhoto(from: photoUrl)
                }
            }
            
            var results: [Data] = []
            for try await photo in group {
                if let photo {
                    results.append(photo)
                }
                print("🟢 downloaded")
            }
            return results
        }
    }
    • Each task is added using the TaskGroup.addTaskUnlessCancelled(priority:operation:) method, to avoid starting new work after cancellation.
    • Each task checks for cancellation before starting to download the photo. If it has been canceled, the task returns nil. <- 🤔 Need to check…
    • At the end, the task group skips nil values when collecting the results. Handling cancellation by returning nil means the task group can return a partial result — the photos that were already downloaded at the time of cancellation — instead of discarding that completed work.
    screenshot 2024 03 15 at 5.13.04e280afpm
    screenshot 2024 03 15 at 5.44.03e280afpm

    If parent task has cancelled, all the child tasks are also cancelled.

    What if I cancel a child task?

    screenshot 2024 03 15 at 5.56.42e280afpm 1

    It seems cancelling a child task doesn’t cancel the parent task.

    Let’s remind Task cancellation.

    There are two ways a task can do this: by calling the Task.checkCancellation() method, or by reading the Task.isCancelled property. Calling checkCancellation() throws an error if the task is canceled;

    a throwing task can propagate the error out of the task, stopping all of the task’s work. This has the advantage of being simple to implement and understand. For more flexibility, use the isCancelled property, which lets you perform clean-up work as part of stopping the task, like closing network connections and deleting temporary files.

    Apple

    Unstructured Concurrency

    Unlike tasks that are part of a task group, an unstructured task doesn’t have a parent task. You have complete flexibility to manage unstructured tasks in whatever way your program needs, but you’re also completely responsible for their correctness. To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:)initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method. Both of these operations return a task that you can interact with — for example, to wait for its result or to cancel it.

    Apple

    Apple document’s example

    let newPhoto = // ... some photo data ...
    let handle = Task {
        return await add(newPhoto, toGalleryNamed: "Spring Adventures")
    }
    let result = await handle.value

    My example

    let unstructuredTask = Task { () -> Data in
        return try! await URLSession.shared.data(from: URL(string: "https://picsum.photos/100")!).0
    }
    let firstPhoto = await unstructuredTask.value

    Task Closure life cycle

    Tasks are initialized by passing a closure containing the code that will be executed by a given task.

    After this code has run to completion, the task has completed, resulting in either a failure or result value, this closure is eagerly released.

    Retaining a task object doesn’t indefinitely retain the closure, because any references that a task holds are released after the task completes. Consequently, tasks rarely need to capture weak references to values.

    For example, in the following snippet of code it is not necessary to capture the actor as weak, because as the task completes it’ll let go of the actor reference, breaking the reference cycle between the Task and the actor holding it.

    Note that there is nothing, other than the Task’s use of self retaining the actor, And that the start method immediately returns, without waiting for the unstructured Taskto finish. So once the task completes and its the closure is destroyed, the strong reference to the “self” of the actor is also released allowing the actor to deinitialize as expected.

    Apple

    struct Work: Sendable {}
    
    
    actor Worker {
        var work: Task<Void, Never>?
        var result: Work?
    
    
        deinit {
            assert(work != nil)
            // even though the task is still retained,
            // once it completes it no longer causes a reference cycle with the actor
    
    
            print("deinit actor")
        }
    
    
        func start() {
            //unstructured Task
            work = Task {
                print("start task work")
                try? await Task.sleep(for: .seconds(3))
                self.result = Work() // we captured self
                print("completed task work")
                // but as the task completes, this reference is released
            }
            // we keep a strong reference to the task
        }
    }
    
    await Actor().start()
    
    //Prints
    start task work
    completed task work
    deinit actor

    Actors

    sometimes you need to share some information between tasks. Actors let you safely share information between concurrent code.

    Like classes, actors are reference types, so the comparison of value types and reference types in Classes Are Reference Types applies to actors as well as classes. Unlike classes, actors allow only one task to access their mutable state at a time, which makes it safe for code in multiple tasks to interact with the same instance of an actor. For example, here’s an actor that records temperatures:

    You introduce an actor with the actor keyword, followed by its definition in a pair of braces. The TemperatureLogger actor has properties that other code outside the actor can access, and restricts the max property so only code inside the actor can update the maximum value.

    Apple

    actor TemperatureLogger {
        let label: String
        var measurements: [Int]
        private(set) var max: Int
    
    
        init(label: String, measurement: Int) {
            self.label = label
            self.measurements = [measurement]
            self.max = measurement
        }
    }
    
    let logger = TemperatureLogger(label: "Outdoors", measurement: 25)
    
    print(await logger.max)
    //Prints "25"
    
    print(logger.max)  // Error, Should use await
    

    You create an instance of an actor using the same initializer syntax as structures and classes. When you access a property or method of an actor, you use await to mark the potential suspension point. For example:

    In this example, accessing logger.max is a possible suspension point. Because the actor allows only one task at a time to access its mutable state, if code from another task is already interacting with the logger, this code suspends while it waits to access the property.

    Apple

    extension TemperatureLogger {
        func update(with measurement: Int) {
        //part of the actor doesn’t write await when accessing the actor’s properties
            measurements.append(measurement)
            //At here, temporary inconsistent state.
            if measurement > max {
                max = measurement
            }
        }
    }
    
    
    1. Your code calls the update(with:) method. It updates the measurements array first.
    2. Before your code can update max, code elsewhere reads the maximum value and the array of temperatures.
    3. Your code finishes its update by changing max.

    In this case, the code running elsewhere would read incorrect information because its access to the actor was interleaved in the middle of the call to update(with:) while the data was temporarily invalid. You can prevent this problem when using Swift actors because they only allow one operation on their state at a time, and because that code can be interrupted only in places where await marks a suspension point. Because update(with:) doesn’t contain any suspension points, no other code can access the data in the middle of an update.

    Apple

    actor isolation

     Swift guarantees that only code running on an actor can access that actor’s local state. This guarantee is known as actor isolation.

    The following aspects of the Swift concurrency model work together to make it easier to reason about shared mutable state:

    • Code in between possible suspension points runs sequentially, without the possibility of interruption from other concurrent code.
    • Code that interacts with an actor’s local state runs only on that actor.
    • An actor runs only one piece of code at a time.

    Because of these guarantees, code that doesn’t include await and that’s inside an actor can make the updates without a risk of other places in your program observing the temporarily invalid state. For example, the code below converts measured temperatures from Fahrenheit to Celsius:

    Apple

    extension TemperatureLogger {
        func convertFahrenheitToCelsius() {
            measurements = measurements.map { measurement in
                (measurement - 32) * 5 / 9
            }
        }
    }

    writing code in an actor that protects temporary invalid state by omitting potential suspension points, you can move that code into a synchronous method. The convertFahrenheitToCelsius() method above is a synchronous method, so it’s guaranteed to never contain potential suspension points

    Apple

    Sendable Types

    Inside of a task or an instance of an actor, the part of a program that contains mutable state, like variables and properties, is called a concurrency domain.

    You mark a type as being sendable by declaring conformance to the Sendableprotocol. That protocol doesn’t have any code requirements, but it does have semantic requirements that Swift enforces. In general, there are three ways for a type to be sendable:

    • The type is a value type, and its mutable state is made up of other sendable data — for example, a structure with stored properties that are sendable or an enumeration with associated values that are sendable.
    • The type doesn’t have any mutable state, and its immutable state is made up of other sendable data — for example, a structure or class that has only read-only properties.
    • The type has code that ensures the safety of its mutable state, like a class that’s marked @MainActor or a class that serializes access to its properties on a particular thread or queue.
    • Value types
    • Reference types with no mutable storage
    • Reference types that internally manage access to their state
    • Functions and closures (by marking them with @Sendable)

    Apple

    struct TemperatureReading: Sendable {
        var measurement: Int
    }
    
    
    extension TemperatureLogger {
        func addReading(from reading: TemperatureReading) {
            measurements.append(reading.measurement)
        }
    }
    
    
    let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
    let reading = TemperatureReading(measurement: 45)
    await logger.addReading(from: reading)

    Sendable Classes

    • Be marked final
    • Contain only stored properties that are immutable and sendable
    • Have no superclass or have NSObject as the superclass

    Classes marked with @MainActor are implicitly sendable, because the main actor coordinates all access to its state. These classes can have stored properties that are mutable and nonsendable.

    Apple

    Sendable Functions and Closures

    Instead of conforming to the Sendable protocol, you mark sendable functions and closures with the @Sendable attribute. Any values that the function or closure captures must be sendable. In addition, sendable closures must use only by-value captures, and the captured values must be of a sendable type.

    In a context that expects a sendable closure, a closure that satisfies the requirements implicitly conforms to Sendable — for example, in a call to Task.detached(priority:operation:).

    You can explicitly mark a closure as sendable by writing @Sendable as part of a type annotation, or by writing @Sendable before the closure’s parameters — for example:

    Apple

    let sendableClosure = { @Sendable (number: Int) -> String in
        if number > 12 {
            return "More than a dozen."
        } else {
            return "Less than a dozen"
        }
    }

    Sendable Tuples

    To satisfy the requirements of the Sendable protocol, all of the elements of the tuple must be sendable. Tuples that satisfy the requirements implicitly conform to Sendable. by Apple

    Sendable Metatypes

    Metatypes such as Int.Type implicitly conform to the Sendable protocol.

  • Swift, OperationQueue

    Swift, OperationQueue

    ✍️ Note

    Some 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

    OperationQueue

    An operation queue invokes its queued Operation objects based on their priority and readiness. After you add an operation to a queue, it remains in the queue until the operation finishes its task. You can’t directly remove an operation from a queue after you add it.

    Operation queues retain operations until the operations finish, and queues themselves are retained until all operations are finished. Suspending an operation queue with operations that aren’t finished can result in a memory leak.

    Apple

    Operation

    Operation objects are synchronous by default. In a synchronous operation, the operation object does not create a separate thread on which to run its task

    Apple

    screenshot 2024 03 14 at 11.45.56e280afpm
    class DownloadImageOperation: Operation {
        var dataTask: URLSessionDataTask!
        init(url: URL) {
            super.init()
            dataTask = URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in
                print("\(self.name!) downloaded")
            }
        }
        
        override func start() {
            super.start()
            print("\(self.name!) started")
            dataTask.resume()
        }
        
        override func cancel() {
            super.cancel()
        }
    }
    let operationQueue = OperationQueue()
    
    let firstOperation = DownloadImageOperation(url: URL(string: "https://picsum.photos/200/300?grayscale")!)
    firstOperation.name = "first"
    
    let secondOperation = DownloadImageOperation(url: URL(string: "https://picsum.photos/200/300?grayscale")!)
    secondOperation.name = "second"
    
    //first operation will not start until second operation start.
    firstOperation.addDependency(secondOperation)
    operationQueue.addOperations([secondOperation, firstOperation], waitUntilFinished: false)
    
    
    //Prints
    second started
    first started
    second downloaded
    first downloaded

    The first operation depends on the second operation. This means it can’t be started if the second operation hasn’t started yet.

    operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)

    If you add firstOperation first, the results are the same. second started and then first started.

    Cancel Operation

    screenshot 2024 03 15 at 11.46.48e280afam
    class DownloadImageOperation: Operation {
        var dataTask: URLSessionDataTask!
        init(url: URL) {
            super.init()
            dataTask = URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in
                print("🟢 \(self.name!) downloaded")
            }
        }
        
        override func start() {
            super.start()
            print("🟢 \(self.name!) started")
            dataTask.resume()
        }
        
        override func cancel() {
            super.cancel()
            print("🔴 \(self.name!) canceled")
        }
    }
    
    let operationQueue = OperationQueue()
    let firstOperation = DownloadImageOperation(url: URL(string: "https://picsum.photos/200/300?grayscale")!)
    firstOperation.name = "first"
    
    let secondOperation = DownloadImageOperation(url: URL(string: "https://picsum.photos/200/300?grayscale")!)
    secondOperation.name = "second"
    
    firstOperation.addDependency(secondOperation)
    
    operationQueue.addOperations([firstOperation, secondOperation], waitUntilFinished: false)
    operationQueue.cancelAllOperations()
    
    screenshot 2024 03 15 at 11.49.13e280afam

    When you cancel operations, operations will start and finish the task. And then It removed from operationQueue.