Category: Apple

  • Adobe xD to SWIFT

    Adobe xD to SWIFT

    Xin chào! Bài viết này mình muốn chia sẻ cho mọi người về một số cách để code SWIFT giống với Adobe xD và hạn chế phần nào việc bị bắt bug UI không đáng.

    Trong nỗi trăn trở ở mỗi dự án mobile có hàng trăm, hàng ngàn các bug UI được log. Tự nhiên mình lại nghĩ phải làm việc gì đó để giúp cho anh em code UI ngon hơn, đỡ tạo ra bug UI hơn. Vì vậy mình đã viết bài viết này hi vọng sẽ giúp anh em được phần nào trong việc tránh dính phải những bug UI.

    Khi làm việc với xD các anh em thường bỏ qua các chỉ số của xD mà hay tự thực hiện code để nhìn sao cho giống UI nhất có thể, nếu không hiểu rõ bản chất nó khiến cho anh em mất khá nhiều thời gian để có thể làm giống được với file thiết kế cụ thể ở đây là file Adobe xD.

    Drop shadow

    Để giúp mọi người làm việc dễ dàng hơn nên mình đã tạo ra một hàm trong CALayer để giúp anh em đổ bóng bao chuẩn, bao giống xD :v Việc của anh em là lấy chỉ số ở xD và truyền vào func để setup là xong.

    extension CALayer {
        
        /// make shadow like Adobe xD, all prameters using same value with Adobe xD
        /// - Parameters:
        ///   - color: shadow color
        ///   - opacity: alpha of shadow color (0-100)
        ///   - x: x
        ///   - y: y
        ///   - b: shadow radius
        func dropShadowLikeXD(color: UIColor = .black,
                              opacity: Int = 50,
                              x: CGFloat = 0,
                              y: CGFloat = 3,
                              b: CGFloat = 6) {
            masksToBounds = false
            shadowColor = color.cgColor
            shadowOpacity = Float(opacity) / 100.0
            shadowOffset = CGSize(width: x, height: y)
            shadowRadius = b / 2.0
            shadowPath = nil
        }
    }

    Để anh em dễ hiểu hơn thì mình xin giải thích như sau:
    – color: đây là shadow color, nó là màu shadow trên xD, cái này khá đơn giản mọi người chỉ cần lấy màu trên xD và fill vào là xong
    – opacity: đây là độ trong suốt của shadow, trên Adobe xD không có thuộc tính này mà giá trị này sẽ là Opacity của shadow color
    – x: độ lệch của shadow so với view, tính từ trái qua phải. x > 0 thì shadow lệch qua phải và ngược lại
    – y: độ lệch của shadow so với view, tính từ trên xuống dưới, x > thì shadow lệch xuống dưới và ngược lại.
    – b: là thuộc tính blur trên xD, nhưng trong Swift không có thuộc tính này, mà chỉ có shadowRadius nó là bán kính của shadow, và nó bằng 1/2 blur trên xD.
    – masksToBounds: thuộc tính này bằng true sẽ không thể tạo được shadow vì nó sẽ cắt mất view shadow đi. vì trong hàm mình đã set lại giá trị này bằng false.

    Lưu ý: Để vừa đổ bóng được kết hợp với bo góc chúng ta cần thực hiện bo góc trước khi gọi hàm dropShadowLikeXD()

    Border

    Border trong Adobe xD cho phép custom khá nhiều thuộc tính, tuy nhiên mấy ông Dev tạo ra CALayer của Apple lại chỉ cho set mỗi 2 thuộc tính là borderWidth và borderColor. Vì vậy để làm giống Adobe xD chúng ta sẽ mất công hơn 1 chút. Cụ thể chúng ta sẽ cần thêm 1 enum và một func trong extension của UIView như sau:

    extension UIView {
        enum BorderStrokeStyle {
            case inner 
            case outer
            case center
        }
        
        func borderLikeXD(size: CGFloat = 1,
                          color: UIColor = .black,
                          dash: CGFloat = 0,
                          cap: CAShapeLayerLineCap = .butt,
                          join: CAShapeLayerLineJoin = .miter,
                          stroke: BorderStrokeStyle = .inner) {
            if dash <= 0 {
                layer.borderColor = color.cgColor
                layer.borderWidth = size
            } else {
                let newShapeLayer  = CAShapeLayer()
                newShapeLayer.strokeColor = color.cgColor
                newShapeLayer.lineWidth = size
                newShapeLayer.lineDashPattern = [NSNumber(value: dash), NSNumber(value: dash)]
                newShapeLayer.frame = self.bounds
                newShapeLayer.fillColor = nil
                newShapeLayer.lineCap = cap
                newShapeLayer.lineJoin = join
                switch stroke {
                case .inner:
                    let innerBounds = CGRect(x: bounds.origin.x + size / 2, y: bounds.origin.y + size / 2, width: bounds.size.width - size, height: bounds.size.height - size)
                    newShapeLayer.path = UIBezierPath(roundedRect: innerBounds, cornerRadius: layer.cornerRadius).cgPath
                case .outer:
                    let outerBounds = CGRect(x: bounds.origin.x - size / 2, y: bounds.origin.y - size / 2, width: bounds.size.width + size, height: bounds.size.height + size)
                    newShapeLayer.path = UIBezierPath(roundedRect: outerBounds, cornerRadius: layer.cornerRadius).cgPath
                case .center:
                    newShapeLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
                }
                
                self.layer.addSublayer(newShapeLayer)
                
            }
        }
    }

    Lưu ý: Do hàm này thực hiện thêm mới sublayer nên mọi người không nên gọi nó thực hiện ở func có thể gọi nhiều lần như: viewWillAppear(), viewDidAppear() …

    Line spacing

    Do định nghĩa về line spacing của Apple và Adobe xD khác nhau nên chúng ta không thể sử dụng cùng chỉ số được, vì vậy chúng ta cần tạo ra một phương thức để sửa lại công thức sao cho khớp với Adobe xD.

    Adobe xD định nghĩa line spacing: là khoảng cách từ Top của dòng trên so với Top của dòng dưới liền kề.

    Apple định nghĩa line spacing: là khoảng cách giữa Bot của dòng trên so với Top của dòng dưới liền kề.

    Line spacing

    Chúng ta có thể nhận ra sự chênh lệch giá trị line spacing của Apple so với Adobe chính là chiều cao của 1 dòng. Vậy nên mình có tạo ra một func giúp mọi người set lại giá trị line spacing giống xD mà không phải đau đầu tính toán nữa.

    extension UILabel {
        /// Set line spacing for label
        ///
        /// - Parameter lineSpacing: Line spacing
        func setLineSpacing(_ lineSpacing: CGFloat) {
            // Check label text empty
            guard let labelText: String = self.text,
                  let font = self.font else {
                return
            }
            let constraintRect: CGSize = CGSize(width: self.bounds.width, height: .greatestFiniteMagnitude)
            let boundingBox: CGRect = "Ok".boundingRect(with: constraintRect,
                                                        options: .usesLineFragmentOrigin,
                                                        attributes: [NSAttributedString.Key.font: font],
                                                        context: nil)
            let heightLabel: CGFloat = ceil(boundingBox.height)
            let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle()
            // line spacing on xD - height of one line
            paragraphStyle.lineSpacing = lineSpacing - heightLabel
            
            let attributedString: NSMutableAttributedString
            if let labelattributedText: NSAttributedString = self.attributedText {
                attributedString = NSMutableAttributedString(attributedString: labelattributedText)
            } else {
                attributedString = NSMutableAttributedString(string: labelText)
            }
            
            // Line spacing attribute
            attributedString.addAttribute(NSAttributedString.Key.paragraphStyle,
                                          value: paragraphStyle,
                                          range: NSRange(location: 0,
                                                         length: attributedString.length))
            
            self.attributedText = attributedString
        }
    }

    Trên đây là những gì mình muốn chia sẻ lại cho mọi người, hi vọng nó sẽ giúp được mọi người phần nào trong công việc. Nếu mọi người có câu hỏi hay thắc mắc gì có thể đặt câu hỏi ở dưới comment mình sẽ cố gắng giải đáp những thắc mắc của mọi người.

    Xin cảm ơn mọi người đã đọc bài viết của mình!

  • Swift: weak and unowned

    Swift: weak and unowned

       

    Đầu mục bài viết

       

    1-ARC

    Automatic Reference Counting aka ARC, là 1 tính năng của Swift dùng để đếm số lượng strong reference, và cho phép quản lý việc giải phóng memory khi 1 instance không còn được reference(tham chiếu) đến.

    Theo như doc của Apple:

    Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.

    Chúng ta cũng cần nhớ rằng trong swift một reference của được định nghĩa mặc định là kiểu strong

    Ví dụ : 

    class FirstClass: UIViewController {
    
        let secondClass = SecondClass()
    
    }
    
    class SecondClass {
    
    }
    

    Trong ví dụ này, class FirstViewController của mình đang strong reference đến instance secondClass

       

    2-Vấn đề của strong reference cycle

    Như mình đã đề cập bên trên, ARC sẽ giúp chúng ta quản lý, phân bổ memory từ những instance không còn được tham chiếu đến.

    Tuy nhiên, câu hỏi đặt ra là điều gì sẽ xảy ra khi có 2 object/instance strong reference đến nhau?

    Từ strong việt hóa ra là mạnh, bền, … nghe thôi cũng cảm giác khó phá hỏng, phá hủy nó rồi đúng không? :)))

    Thì strong reference cũng thế, strong reference trỏ đến 1 instance/object và sở hữu nó, quyết định đến sự tồn tại của instance/object đó. 

    ARC không thể có cách nào giải phóng được memory từ những kiểu instace/object này, và điều này sẽ dẫn đến memory leak.

    Trong thực tế những project đã làm, thì mình rất hay thường gặp những case strong reference, và mình cũng rất hay vô tình tạo ra strong reference :v 

    Case mình hay gặp nhất là case khi khởi tạo delegate:

    protocol DemoDelegate {
        func demoFunc()
    }
    
    class FirstClass {
        var delegate: DemoDelegate?
    }
    

    Trông ví dụ này có vẻ quen đúng không? :))) ở đây mình có 1 protocol DemoDelegate, và khởi tạo delegate này trong FirstClass. Và FirstClass và delegate DemoDelegate đang strong reference đến nhau.

    Để giải quyết vấn đề này, chúng ta có weak và unowned.

     

    Weak và Unowned

     

    1. Weak

    Trái ngược với strong pointer, weak pointer trỏ đến một instance/object và không quyết định đến sự tồn tại của instance/object đó.

    Vì thế nên ARC có thể dễ dàng giải phóng bộ nhớ từ instance/object này kể cả chúng ta vẫn còn đang tham chiếu đến nó 

    => Weak reference rất hữu ich, giúp chúng ra tránh được việc vô tình hay cố ý tạo ra 1 strong reference cycle.

    Note: Weak reference không thể sử dụng với việc khởi tạo instance/object là let bởi vì instance/object ở một vài case vẫn phải truyền nil.

    Việc sử dụng weak reference thực sự quan trọng, ví dụ nếu chúng ta để ý hơn và xem source code bên trong definition của UITableView, ta có thể nhận thấy rằng tất cả delegate(bao gồm 2 var quen thuộc là dataSource và delegate) đều được sử dụng với weak khi khởi tạo.

    Và mình nghĩ là đến apple còn để ý và chú trọng đến việc sử dụng weak, tại sao chúng ta lại không làm như thế ? :v

    protocol DemoDelegate {
        func demoFunc()
    }
    
    class FirstClass {
        weak var delegate: DemoDelegate?
    }
    

    Ok, thêm weak là xong, đơn giản phải không nào.

    Tuy nhiên, khi thêm weak, lại nảy sinh vấn đề không thể compile đoạn code:

    Để giải quyết, theo doc Protocols của apple:

    Use a class-only protocol when the behavior defined by that protocol’s requirements assumes or requires that a conforming type has reference semantics rather than value semantics.

    Để fix, chỉ cần thêm keyword class là được

    protocol DemoDelegate: class {
        func demoFunc()
    }
    
    class FirstClass {
        weak var delegate: DemoDelegate?
    }
    

      2. unowned

    Một variable kiểu unownevề cơ bản giống với variable kiểu weak, nhưng khác nhau ở chỗ: compiler sẽ make sure rằng variable này khi được gọi đến sẽ không có giá trị nil.

    Vậy thực sự trong trường hợp nào nên sử dụng unowned thay vì sử dụng weak ? Vẫn theo doc của Apple: 

    Like a weak reference, an unowned reference doesn’t keep a strong hold on the instance it refers to. Unlike a weak reference, however, an unowned reference is used when the other instance has the same lifetime or a longer lifetime.

    Để hiểu rõ hơn, mình có 2 ví dụ như này

    Ví dụ của weak reference: mình sở hữu 1 chiếc xe đạp, nhưng 1 hôm đẹp trời nào đó, mình đổi ý không muốn đạp xe nữa, thì ở đây chiếc xe đạp vẫn còn đó, có chăng chỉ là đổi chủ, trong khi mình đang ngồi 1 chiếc 4 bánh nào đó :v 

    Ví dụ của unowned reference: mình chơi 1 yasuo chẳng hạn :))), thằng nhân vật của mình tăng skill theo cấp độ. Tuy nhiên, khi xám màn hình, những skill đó cũng sẽ xám theo. Có nghĩa rằng là những skill này có tuổi thọ = tuổi thọ của nhân vật mình đang chơi.

    Triển khai trong code:

    class Yasuo {
    
        let name: String
        let skill: Skill?
    
        init(name: String) {
            self.name = name
        }
    
    }
    
    class Skill {
    
        let damage: Int
        unowned let champ: Yasuo
    
        init(damage: Int, champ: Yasuo) {
            self.damage= power
            self.champ = champ
        }
    
    }
    

       

    3-Debugging

    Vậy là chúng ta đã hiểu phần nào về 2 keyword weak và unowned. Chúng dùng để tránh tình trạng vô tình tạo ra memory leak. 

    Nhưng sao để biết được có thực sự tránh được hay không?

    Cách đơn giản và xưa như quả đất, là sử dụng print trong 2 method init và deinit

    class FirstClass {
    
        init() {
            print("FirstClass is being initialized")
        }
    
        deinit { 
            print("FirstClass is being deinitialized") 
        }
    
    }
    

    Và để ý memory trong XCode nữa :v

       

    4-Tổng kết

    Strong, weak, unowned là những thuật ngữ cơ bản trong swift, tuy nhiên chúng ta thường – do vô ý hoặc chủ quan – bỏ qua chúng.

    Điều này không thực sự ảnh hưởng quá lớn với những project nhỏ. 

    Tuy nhiên với những app lớn, việc quản lý bộ nhớ ra sao cho hiệu quả là 1 việc quan trọng và đáng lưu ý, bởi vì khi memory leak trở thành 1 vấn đề nghiêm trọng, thì rất khó và tốn effort để fix. 😀

    HAPPY CODING!

    REFERENCE

    https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

    https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

  • TaskGroup Swift Part 2

    TaskGroup Swift Part 2

    Nếu đã đọc bài viết trước của mình về task group, mọi người sẽ hiểu bản chất task group, cách khởi tạo, cách add 1 task con, và cách nhận result của tất cả các task con đó.

    Tuy nhiên, trong bài viết đó mình không đi sâu vào 1 phần quan trọng của task group, đó là việc handle error.

    Nay mình rảnh rảnh nên lên gõ 1 bài về chủ đề này ?

       

    Đầu mục bài viết

       

    1-Tổng quát

    Như chúng ta đã biết, TaskGroup là 1 tập hợp những task con thực thi đồng thời.

    Tuy nhiên, sẽ có vấn đề được đặt ra là khi có bất kỳ 1 trong những task con gặp lỗi, task group handle việc đó như nào? Điều gì sẽ xảy ra cho những task con khác đang thực thi?

    Ở bài viết này, mình sẽ đưa ra 2 cách cơ bản để handle error trong một task group, đó là:

    – Throw error

    – Trả về result của tất cả các task con

    Ta sẽ đi sâu vào từng cách ở những phần bên dưới.

       

    2-Throw error

    Cách đầu tiên là throw error thông qua withThrowingTaskGroup.

    Nhưng trước tiên, chúng ta cần phải hiểu một task group sẽ hoạt động thế nào khi một task con của nó gặp lỗi:

    1. Một task group sẽ chỉ throw ra error đầu tiên mà task con của nó gặp phải, task group sau đó sẽ bỏ qua tất cả những error sau. Tức là khi bất kỳ task con nào trong quá trình thực thi mà gặp phải lỗi, task group sẽ throw lỗi đó, và sẽ không quan tâm đến bất kỳ error nào task con của nó gặp phải sau đó nữa.

    2. Khi một task con throw 1 error, tất cả các task con còn lại (kể cả những task đang thực thi) sẽ được đánh dấu.

    3. Những task con còn lại mà đã được đánh dấu đó sẽ tiếp tục thực thi cho đến khi chúng ta can thiệp và dừng chúng lại.

    4. Khi chúng ta đã can thiệp và dừng việc thực thi của những task con còn lại đó, những task con đó sẽ không trigger việc add vào result tổng của 1 task group nữa (kể cả với những task con đã hoàn thành việc thực thi).

    Để hiểu rõ hơn, mình có tạo ra 1 struct DemoChildTask, mà trong đó xử lý việc chia 2 số cho nhau, và sẽ throw ra error khi có bất kỳ phép chia nào cho 0

    enum ErrorCase: Error {
        case divideToZero
    }
    
    struct DemoChildTask {
        let name: String
        let a: Double
        let b: Double
        let sleepDuration: UInt64
        
        func execute() async throws -> Double {
            // Sleep for x seconds
            await Task.sleep(sleepDuration * 1_000_000_000)
            
            // Throw error when divisor is zero
            guard b != 0 else {
                print("\(name) throw error")
                throw ErrorCase.divideToZero
            }
            
            let result = a/b
            print("\(name) has been completed: \(result)")
            return result
        }
    }
    

    Tiếp theo, mình tạo ra 1 task group và add các task con vào trong đó. Các task con đó sẽ thực thi DivideOperation và trả ra 2 giá trị name và result.

    Khi tất cả các task con hoàn thành, task group sẽ add các result con đó vào một dictionary result tổng mà trong đó chứa tất cả giá trị name và result của task con.

    Bất kể khi nào một trong những task con gặp error, nó sẽ throw và truyền ra cho task group, và task group sẽ throw error đó.

    let demoOperation: [DemoChildTask] = [DemoChildTask(name: "operation-0", a: 4, b: 1, sleepDuration: 3),
                                            DemoChildTask(name: "operation-1", a: 5, b: 2, sleepDuration: 1),
                                            DemoChildTask(name: "operation-2", a: 10, b: 5, sleepDuration: 2),
                                            DemoChildTask(name: "operation-3", a: 5, b: 0, sleepDuration: 2),
                                            DemoChildTask(name: "operation-4", a: 4, b: 2, sleepDuration: 5)]
    
    Task {
        do {
            let allResult = try await withThrowingTaskGroup(of: (String, Double).self,
                                                            returning: [String: Double].self,
                                                            body: { taskGroup in
                // Loop through operations array
                for operation in demoOperation {
                    
                    // Add child task to task group
                    taskGroup.addTask {
                        
                        // Execute slow operation
                        let value = try await operation.execute()
                        
                        // Return child task result
                        return (operation.name, value)
                    }
                }
                
                // Collect results of all child task in a dictionary
                var childTaskResults = [String: Double]()
                
                for try await result in taskGroup {
                    // Set operation name as key and operation result as value
                    childTaskResults[result.0] = result.1
                }
                // All child tasks finish running, thus return task group result
                return childTaskResults
            })
            
            print("Task group completed: \(allResult)")
        } catch {
            print("Task group error: \(error)")
        }
    }
    

    withThrowingTaskGroup về cơ bản giống với withTaskGroup, tuy nhiên chúng ta cần sử dụng keyword try để có thể throw error. Đồng thời cũng cần dùng try khi call func execute() của task con, điều này cho phép error throw từ execute() có thể truyền ra cho task group. Bên cạnh đó, try cũng phải được dùng khi tổng hợp result từ các task con.

    Khi chạy đoạn code trên, ta sẽ nhận được đoạn log như này:

    Đoạn output trên cho chúng ta thấy “operation-3” sẽ throw ra lỗi vì đã chia cho 0.

    Mặc dù đoạn code trên đã khá hoàn chỉnh cho việc handle error với throw, tuy nhiên để tối ưu hơn, ta sẽ không muốn các task con khác thực thi tiếp khi một task con đã throw ra error. Đây là lúc chúng ta cần phải can thiệp vào việc thực thi của chúng như mình đã note bên trên.

    Để làm được việc này, đơn giản có thể sử dụng Task.checkCancellation(). Func này dùng để check những task nào còn đang thực thi.

    func execute() async throws -> Double {
            // Sleep for x seconds
            await Task.sleep(sleepDuration * 1_000_000_000)
            
            // Check for cancellation.
            try Task.checkCancellation()
            
            // Throw error when divisor is zero
    //        guard b != 0 else {
    //            print("\(name) throw error")
    //            throw ErrorCase.divideToZero
    //        }
    //
    //        let result = a/b
    //        print("\(name) has been completed: \(result )")
    //        return result
        }
    

    Về cơ bản, khi func checkCancellation() gặp bất kỳ task con nào đã được đánh dấu, nó sẽ dừng task con đó và throw ra CancellationError . Tuy nhiên, như mình đã đề cập, việc throw này không mang quá nhiều ý nghĩa vì task group sẽ reject toàn bộ những error này.

    Khi chạy lại đoạn code trên với checkCancellation(), ta sẽ nhận được log như này:

       

    3-Trả về result

    Cách thứ 2 này thực ra hoàn toàn ngược lại với cách đầu tiên. Cách này cơ bản task group sẽ ignore toàn bộ task con mà throw error, mà sẽ chỉ trả về tất cả result của task con thực thi thành công.

    Cách 2 này code khá giống với cách 1, chỉ khác là bây giờ sẽ sử dụng non-throwing task group thay vì throw task group, và sẽ ignore toàn bộ error với try?:

    Task {
        let allResult = await withTaskGroup(of: (String, Double)?.self,
                                            returning: [String: Double].self,
                                            body: { taskGroup in
            // Loop through operations array
            for operation in demoOperation {
                
                // Add child task to task group
                taskGroup.addTask {
                    
                    // Execute slow operation
                    guard let value = try? await operation.execute() else {
                        return nil
                    }
                    
                    // Return child task result
                    return (operation.name, value)
                }
            }
            
            // Collect results of all child task in a dictionary
            var childTaskResults = [String: Double]()
            
            for await result in taskGroup.compactMap({ $0 }) {
                // Set operation name as key and operation result as value
                childTaskResults[result.0] = result.1
            }
            // All child tasks finish running, thus return task group result
            return childTaskResults
        })
        
        print("Task group completed: \(allResult)")
    }
    

    Để trả về result tổng của tất cả task con, mình sử dụng compactMap để loại bỏ toàn bộ giá trị nil được trả về từ task con.

    Khi chạy đoạn code trên, đây sẽ là output:

       

    4-Tổng kết

    Như vậy, với 2 bài viết của mình, mọi người đã có những cái nhìn và hiểu biết rõ ràng hơn về task group, một trong những update mới và quan trọng của swift 5.5 ?

    Happy coding!!

  • TaskGroup Swift

    TaskGroup Swift

    Trong sự kiện WWDC21, apple đã giới thiệu với công chúng swift 5.5 với nhiều cải tiến, trong đó có async/await. Đây là một cập nhật lớn về cách chúng ta làm việc với bất đồng bộ, chúng dùng để viết những đoạn code async dễ hiểu và dễ đọc.
    Tuy nhiên, async/await bản thân nó không cho phép chúng ta chạy tất cả mọi thứ một cách đồng thời, ngay cả khi với nhiều CPU cores cùng hoạt động, async/ await code vẫn sẽ thực thi tuần tự.
    Để giải quyết vấn đề này, vẫn ở swift 5.5, apple giới thiệu với chúng ta Task và TaskGroups. Chúng là 1 trong những phần quan trọng trong concurrency framework của Swift.

       

    1-Bản chất taskgroup

    Đúng như tên gọi, TaskGroup là 1 tập hợp những task con thực thi đồng thời, nói cách khác, TaskGroup sẽ giúp chúng ta chia 1 công việc thành nhiều concurrent operations.
    Taskgroup hoạt động tốt nhất khi các task con của nó trả về cùng 1 kiểu data, tuy nhiên, ta cũng có thể ép chúng hỗ trợ kiểu data khác nhau.
    Để dể hiểu hơn, mình có 5 gạch đầu dòng về Taskgroup:

    – Một taskgroup là 1 tập hợp các task async mà trong tập hợp đó, các task này hoạt động độc lập với nhau.

    – Tất cả các task con sẽ thực thi 1 cách đồng thời và gần như ngay lập tức sau khi được add vào Taskgroup.

    – Chúng ta không thể kiểm soát khi nào các task con hoàn tất việc thực thi của chúng. Vì thế, chúng ta không nên sử dụng taskgroup nếu muốn các task con hoàn thành theo một thứ tự nào đó.

    – Một taskgroup chỉ return khi và chỉ khi tất các task con của nó hoàn tất. Nói cách khác, tất cả các task của Taskgroup chỉ tồn tại trong chính TaskGroup đó còn đang hoạt động.

    – Có thể dừng một taskgroup bằng cách return 1 giá trị, hoặc return void, hoặc throw error.

       

    2-Mức độ ưu tiên

    TaskGroup có thể được tạo với 1 trong 4 độ ưu tiên: High là độ ưu tiên cao nhất, tiếp đó là Medium, Low, và Background.
    Mức độ ưu tiên task cho phép hệ thống sắp xếp task nào thực thi trước.
    Nếu so sánh với các mức độ của DispatchQueue, userInitiated và utility sẽ là High và low. Taskgroup không có mức độ tương đương với userInteractive, vì với mức độ đó, nó dành riêng cho user interface.

       

    3-Khởi tạo taskGroup

    Để tạo một taskgroup, ta có thể sử dụng withTaskGroup(of:returning:body:) hoặc withThrowingTaskGroup(of:returning:body:). Ở bài viết này, mình không sử dụng taskgroup có throw error, nên nếu muốn tìm hiểu thêm về withThrowingTaskGroup(of:returning:body:), mọi người có thể xem thêm ở document của apple.

       

    4-Làm việc với taskgroup

    Mình có tạo ra 1 struct demoChildTask việc nhân 2 số với nhau, và trong đó có một khoảng nghỉ để tiện cho việc control taskgroup. Ở đây mình có tạo ra một mảng demoChildTask:

    let demoOperations = [
        demoChildTask(name: "operation-0", a: 5, b: 1, sleepDuration: 5),
        demoChildTask(name: "operation-1", a: 14, b: 7, sleepDuration: 1),
        demoChildTask(name: "operation-2", a: 8, b: 2, sleepDuration: 3),
    ]
    

    Sau đó add các task con vào taskgroup bằng cách chạy vòng lặp array demoChildTask

    let demoResult = await withTaskGroup(of: (String, Double).self,
                                             returning: [String: Double].self,
                                             body: { taskGroup in
            
            // Loop through demoOperations array
            for operation in demoOperations {
                
                // Add child task to task group
                taskGroup.addTask {
                    
                    // Execute slow operation
                    let value = await operation.slowMulti()
                    
                    // Return child task result
                    print("Quang Huy -: \(operation.name)")
                    return (operation.name, value)
                }
                
            }
            
            // Collect child task result...
        })
    

    Lưu ý, kiểu dữ liệu task con trả về phải đúng là kiểu dữ liệu của task con mà ta đã khai báo khi khởi tạo TaskGroup

    Như đã đề cập, tất cả các task con đều thực thi đồng thời với nhau, nên ta không thể control việc khi nào chúng hoàn tất. Vì thế để nhận result của từng task con, ta phải loop qua taskgroup:

    // Collect results of all child task in a dictionary
    var demoChildTaskResults = [String: Double]()
    for await result in taskGroup {
        print("Quang Huy 1 - \(result.0)")
        // Set operation name as key and operation result as value
        demoChildTaskResults[result.0] = result.1
    }
            
    // All child tasks finish running, return task group result
    return demoChildTaskResults
    

    Ở đoạn code trên, mình sử dụng await keyword, keyword này có ý nghĩa là vòng lặp có thể dừng lại để đợi task con thực thi xong. Mỗi khi task con xong, vòng lặp lại tiếp tục và update giá trị cho childTaskResults.
    Sau khi xử lý result của tất cả các task con hoàn tất, ta return về result của taskgroup. Giống như task con, return type của taskgroup cũng cần phải trùng với kiểu return khi khởi tạo nó:

    Khi chạy đoạn code trên, ta nhận được đoạn log như này:

    Như có thể thấy, task group cần ~ 5s để hoàn thành, 5s cũng là khoảng nghỉ dài nhất mà mình khởi tạo.

       

    5-Tổng kết

    Với sự ra mắt của TaskGroup, việc quản lý và sử dụng các concurrent task chưa bao giờ đơn giản đến thế. Đồng thời cũng là sự kết hợp hoàn hảo với async/await, thứ cũng là 1 cập nhật lớn ở WWDC21.


    REFERENCE

    https://www.hackingwithswift.com/quick-start/concurrency/what-are-tasks-and-task-groups

    https://developer.apple.com/documentation/swift/taskgroup

  • Triển khai CI/CD cho iOS – Appstore Distribution

    Triển khai CI/CD cho iOS – Appstore Distribution

    The most powerful tool we have as developers is automation.

    Scott Hanselman

    Overview

    Tiếp tục với series triển khai CD cho iOS, hôm nay chúng ta sẽ tìm hiểu cách chạy CD để đẩy một ứng dụng lên App Store Connect một cách tự động.

    Có rất nhiều doanh nghiệp, dự án chọn App Store Connect & TestFlight làm nền tảng để Test sản phẩm, đăc biệt với các sản phẩm gần thời điểm lên App Store. Thêm một sự tiện lợi nữa đến từ TestFlight, khi có phiên bản mới được upload lên sẽ có thông báo về cho các thiết bị cài đặt TestFlight. Vậy nếu triển khai CD để tự động đưa ứng dụng lên App Store Connect, ta cũng sẽ có một hệ thống thông báo tự động khi quá trình hoàn tất mà không cần triển khai thêm gì.

    Quan trọng: Để sử dụng được phương thức này ta cần chú ý một số điểm sau:

    • Tạo sẵn App trên App Store Connect với đúng bundle id
    • Bản build sau bắt buộc phải có version/build number lớn hơn version/build number của bản trước đó

    Distribute và Upload IPA sử dụng Command Line

    Sẽ không có nhiều khác biệt giữa phương thức Archive và Export của phiên bản App Store với các phiên bản khác, ta vẫn sẽ sử dụng các câu lệnh cũ, tuy nhiên phần export sẽ không cần tham số -exportPath:

    xcodebuild archive -archivePath "cicd-test" -scheme cicd-test
    xcodebuild -exportArchive -archivePath "cicd-test.xcarchive" -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates

    Với chế độ App Store, chúng ta sẽ sử dụng file ExportOptions.plist với nội dung như sau:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>method</key>
    	<string>app-store</string>
    	<key>destination</key>
    	<string>upload</string>
    </dict>
    </plist>

    Cần thêm có key destination với value upload để cấu hình việc tự động upload ipa thay vì export file.

    Cấu hình .gitlab-ci.yml cho GitlabCI

    Với phương thức này, ta chỉ cần đơn giản cấu hình file .gitlab-ci.yml như sau:

    stages:
      - deploy
    
    deploy_appstore:
      stage: deploy
      script:
        - echo 'Hello bitches, welcome to lazy boys world!'
        - echo 'Just commit code, serve yourself a cup of coffee. Let gitlab build your app!'
        - xcodebuild archive -archivePath "cicd-test" -scheme cicd-test
        - xcodebuild -exportArchive  -archivePath "cicd-test.xcarchive" -exportPath "ipa" -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates
      tags:
        - main

    Kết quả log của Job khi upload thành công trên Gitlab sẽ trông như sau

    Log của quá trình upload App lên App Store Connect

    Như vậy là đã hoàn thành việc cấu hình CD upload ứng dụng lên App Store Connect. Ở bài viết tiếp theo, chúng ta sẽ cùng tìm hiểu cách triển khai CD cho ứng dụng iOS theo phương thức OTA với các Website đang Deploy trên AWS S3 😀

  • Triển khai CI/CD cho iOS – In house Distribution với DeployGate

    Triển khai CI/CD cho iOS – In house Distribution với DeployGate

    Một ngày đẹp trời, anh ấy nhận được một cái mail giới thiệu anh là chuyên gia về tài khoản Apple… đó chính ngày định mệnh mở ra con đường Enterprise. Many thanks anh Vũ Béo, người nhanh tay mang ánh sáng về và đặt những bước chân đầu tiên.

    Sam

    Overview

    DeployGate hỗ trợ rất nhiều chế độ Distribution, tuy nhiên để tạo ra sự đa dạng cho series, ở bài viết này chúng ta sẽ sử dụng Enterprise (In house) kết hợp với DeployGate để tạo thành một quy trình CD.

    Nói thêm một chút về DeployGate, đây là một nền tảng hỗ trợ phân phối ứng dụng di động (iOS & Android). Người dùng có thể upload app package (ipa & apk) lên và chia sẻ link cài đặt cho người khác. Sử dụng DeployGate, Tester sẽ không cần phải cài đặt ứng dụng trực tiếp với ipa hay apk file, và cũng không cần sử dụng Window hoặc MacOS để cài đặt. DeployGate cũng giảm bớt sự phụ thuộc của người dùng trong quá trình test vào Test Flight khi thời gian process một số ứng dụng có thể lên tới hàng chục giờ đồng hồ.

    Bài viết sẽ gồm có 3 phẩn:

    1. Distribute IPA sử dụng Command Line
    2. Cấu hình DeployGate để upload IPA bằng Command Line
    3. Cấu hình file .gitlab-ci.yml cho GitlabCI

    Distribute IPA sử dụng Command Line

    Bước đầu tiên của quá trình vẫn là tạo ra được file IPA để upload lên Deploy Gate. Như ở bài trước đã hướng dẫn, ta sẽ sử dụng các câu lệnh sau để thực hiện tạo ra file IPA.

    xcodebuild archive -archivePath "cicd-test" -scheme cicd-test
    xcodebuild -exportArchive  -archivePath "cicd-test.xcarchive" -exportPath "ipa" -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates

    Với chế độ In House, chúng ta sẽ sử dụng file ExportOptions.plist với nội dung như sau:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>method</key>
    	<string>enterprise</string>
    </dict>
    </plist>

    Sau khi export xong, chúng ta sẽ có một file IPA với đường dẫn tương đối so với vị trí của runner như sau:

    ipa/cicd-test.ipa

    Việc tiếp theo sẽ là upload file IPA lên DeployGate.

    Cấu hình DeployGate để upload IPA bằng Command Line

    Đầu tiên chúng ta cần upload ít nhất một bản IPA lên Deploy Gate, sau đó bật Distribution lên bằng cách ấn vào nút Add a link for sharing trong ảnh sau:

    Ấn vào button [+ Add a link for sharing] phía bên phải

    Sau đó, trang web sẽ điều hướng đến Distribution Page, đừng hoảng, ta chỉ cần copy đường dẫn của trang đó là được. Đường dẫn sẽ có dạng như sau:

    https://deploygate.com/distributions/818c15431b824aac1763f074931ecacaed2a03d5
    DISTRIBUTION_KEY = 818c15431b824aac1763f074931ecacaed2a03d5

    Ta sẽ lưu key phía sau của đường dẫn lại, tạm gọi là DISTRIBUTION_KEY như trên.

    Tiếp đó ta quay lại Dashboard của app như trong ảnh trên và lấy thông tin user từ đường dẫn như sau:

    https://deploygate.com/users/gstdn_test/platforms/ios/apps/com.magz.techover.ios
    USER = gstdn_test
    USER_API_HOST = https://deploygate.com/api/users/gstdn_test/apps

    Cuối cùng của việc cấu hình và thu thập thông tin lên Deploy Gate, ta vào mục Account Setting -> Profile và copy API Key ở dưới cùng của page.

    Copy API Key này nhé bạn

    Ta sẽ có một bộ các thông tin sau:

    USER_API_HOST = https://deploygate.com/api/users/gstdn_test/apps
    DISTRIBUTION_KEY = 818c15431b824aac1763f074931ecacaed2a03d5
    API_KEY = 246147bb-3ab7-4cc8-8fbe-ahihidongoc

    Tiếp đó, ta chỉ cần thực hiện upload IPA file lên Deploy Gate sử dụng các thông tin trên với Command sau:

    Curl -H "Authorization: token $API_KEY" -F "file=@ipa/cicd-test.ipa" -F "message=New distribution" -F "distribution_key=$DISTRIBUTION_KEY" -F "release_note=Release Note" "$USER_API_HOST"

    Response trả về sẽ có dạng Json như sau, nếu error = false thì tức là quá trình upload thành công.

    Response trả về của Curl upload IPA lên Deploy Gate

    Refresh lại Dashboard của app ta sẽ thấy có một bản build mới được upload lên với message “New distribution” như ảnh sau:

    Bản build đã lên với message đi kèm

    Tiếp tục qua trang Update Distribution, ta sẽ thấy bản build mới upload sẽ tự động được cập nhật thành phiên bản được phân phối với Release Notes là “Release Note”.

    Bản build vừa upload được tự động phân phối với Release Note mới

    Như vậy là ta đã hoàn thành việc upload và cập nhật bản build trên Deploy Gate một cách tự động. Tiếp đến là cấu hình các câu lệnh trên vào file .gitlab-ci.yml.

    Cấu hình .gitlab-ci.yml cho GitlabCI

    Rồi, lại tới công chuyện tiếp! Đầu tiên ta cần ném các thông tin Key của Deploy Gate vào mục Variables thay vì setting cứng trong file.

    Luôn đặt các thông tin key, môi trường vào Variables

    Tiếp đến, nội dung của file .gitlab-ci.yml cơ bản sẽ như sau:

    stages:
      - build
    
    build_project:
      stage: build
      script:
        - echo 'Hello bitches, welcome to lazy boys world!'
        - echo 'Just commit code, serve yourself a cup of coffee. Let gitlab build your app!'
        - xcodebuild archive -archivePath "cicd-test" -scheme cicd-test
        - xcodebuild -exportArchive  -archivePath "cicd-test.xcarchive" -exportPath "ipa" -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates
        - Curl -H "Authorization: token ${API_KEY}" -F "file=@ipa/cicd-test.ipa" -F "message=New distribution" -F "distribution_key=${DISTRIBUTION_KEY}" -F "release_note=Release Note" "${USER_API_HOST}"
      tags:
        - main

    Vậy là xong, sau khi code được commit/merge vào main, GitlabCI sẽ tự động chạy Jobs build IPA và đẩy lên DeployGate. Hẹn gặp lại các bạn ở bài viết tiếp theo!

    P/S: Sẽ rất tiện lợi nếu có thể cấu hình để tuỳ chỉnh Upload Message, Release Note để ghi các thay đổi cho Tester hay thông báo khi có bản build mới lên Deploy Gate. Nên mình sẽ dành riêng một bài trong series để hướng dẫn cách cấu hình các tuỳ chỉnh này.

  • Triển khai CI/CD cho iOS – GitlabCI Artifact

    Triển khai CI/CD cho iOS – GitlabCI Artifact

    Ngày đông giá rét, thay vì ngồi dài cổ đợi ae commit code lên để build cho Tester. Bạn có thể đơn giản add Tester vào Gitlab, cài một con CD Artifact đơn giản và ném vào mặt nó phần việc nhàm chán kia. Còn bạn có thể nhàn nhã mò ra Highlands, nhâm nhi cốc cafe nóng và xem cơm chó của ngày Noel buồn…

    Sam, gửi Cương Good, Anh Mẹc và Very Good Team

    Overview

    Tiếp tục với series CI/CD cho iOS, hôm nay chúng ta sẽ chuyển sang chuyên mục CD với phương thức Deploy đơn giản nhất – GitlabCI Artifact.

    Vì đây là bài viết đầu tiên liên quan đến CD, chúng ta sẽ cần đề cập đến phương thức Archive và Distribute ứng dụng sử dụng command line.

    Distribute IPA sử dụng Command Line

    Để triển khai CD, mọi công việc đều phải thực hiện thông qua Shell Command, do đó từ các công việc Build, Test, Archive hay Distribute IPA đều phải thực hiện bằng các câu lệnh. Rất may, Apple cung cấp sẵn một bộ Command Line Tools được tích hợp kèm với Xcode, ta có thể sử dụng ngay khi máy đã cài đặt Xcode. Các bạn có thể tham khảo hướng dẫn các command tại đây.

    Đầu tiên, chúng ta sẽ cài đặt setting project sang Automatic Signing, đảm bảo thiết bị chạy runner đã đăng nhập tài khoản hợp lệ và build thành công App bằng Xcode.

    Setting Automatically trong Xcode

    Sau khi thành công, ta sẽ thử build project bằng câu lệnh với cú pháp:

    xcodebuild build -scheme cicd-test

    Với tham số -scheme là chỉ định scheme/target để build.

    Build thành công Project trên Terminal

    Sau khi thành công, chứng tỏ là project đã được setting đúng cách, chúng ta sẽ thực hiện archive App bằng câu lệnh sau:

    xcodebuild archive -archivePath "cicd-test" -scheme cicd-test

    Với tham số -archivePath chỉ định tên và đường dẫn export ra .xcarchive file.

    Sau khi chạy xong, chúng ta sẽ thấy có một file được export ra với tên cicd-test.xcarchive trông như ảnh sau:

    Output của quá trình archive app

    Sau khi đã có .xcarchive file, chúng ta cần thực hiện export ra IPA file. Để export được IPA file, chúng ta cần một file ExportOptions.plist, ở đây chúng ta sẽ export bằng chế độ adhoc, file sẽ có nội dung như sau:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    	<key>method</key>
    	<string>ad-hoc</string>
    </dict>
    </plist>

    Trong trường hợp IPA cần export ở các chế độ khác, ta sẽ thay ad-hoc bằng các keyword tương ứng sau:

    • Development: development
    • Adhoc: ad-hoc
    • AppStore: app-store
    • Enterprise: enterprise

    Sau đó, thực hiện export IPA bằng câu lệnh sau:

    xcodebuild -exportArchive  -archivePath "cicd-test.xcarchive" -exportPath "ipa" -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates

    Với các tham số sau đây:

    • -exportPath: Chỉ định thư mục sẽ export ra file IPA
    • exportOptionsPlist: Chỉ định file chưa các config export
    • -allowProvisioningUpdates: Cần phải có nếu Project setting chế độ Automatic Signing. Không cần sử dụng nếu setting manual signing.

    Sau khi export xong, chúng ta sẽ nhận được file IPA ở thư mục như hình sau:

    Vị trí file IPA được export ra

    Như vậy là ta đã tạo xong file IPA, tiếp đến sẽ là setting với GitlabCI

    Cấu hình .gitlab-ci.yml cho GitlabCI

    Rồi tới công chuyện, giờ chúng ta chỉ cần chuyển toàn bộ các câu lệnh ở trên vào phần script của file .gitlab-ci.yml và cấu hình để GitlabCI nhận file IPA làm Artifact là xong.

    File .gitlab-ci.yml cơ bản sẽ có nội dung như sau:

    stages:
      - build
    
    build_project:
      stage: build
      script:
        - echo 'Hello bitches, welcome to lazy boys world!'
        - echo 'Just commit code, serve yourself a cup of coffee. Let gitlab build your app!'
        - xcodebuild archive -archivePath "cicd-test" -scheme cicd-test
        - xcodebuild -exportArchive  -archivePath "cicd-test.xcarchive" -exportPath "ipa" -exportOptionsPlist ExportOptions.plist -allowProvisioningUpdates
      tags:
        - main
      artifacts:
        paths:
          - ipa/cicd-test.ipa

    Sau đó, hãy commit các thay đổi lên và chờ đợi thành quả!

    Thành quả của quá trình là đây

    Như bạn thấy trong ảnh trên, phía bên phải của Jobs Detail sẽ có một section để tải về Job Artifacts chứa file IPA có thể cài được. Ở đây, ta chỉ cần ấn nút Download IPA thực hiện cài đặt ứng dụng vào điện thoại.

    Như vậy là ta đã hoàn thành cấu hình CD đơn giản cho iOS với GitlabCI Artifact. Hẹn gặp các bạn ở bài viết tiếp theo.

  • Một số tips Debug ứng dụng với Xcode

    Một số tips Debug ứng dụng với Xcode

    Overview

    Debug là một kỹ năng không thể thiếu của một lập trình viên, kỹ năng debug giỏi giúp ích rất nhiều trong việc tìm hiểu và giải quyết vấn đề phát sinh. Thêm vào đó, Xcode cung cấp rất nhiều công cụ hỗ trợ quá trình debug, nếu tận dụng được hết thì bạn có thể dễ dàng xử lý nhiều vấn đề khó. Bài viết tổng hợp lại một số kinh nghiệm và kiến thức của mình trong quá trình debug ứng dụng với Xcode.

    Debugging Process

    Việc có một process cụ thể sẽ giúp ta biết được sẽ phải làm gì tiếp theo trong từng giai đoạn, giúp việc debug diễn ra suôn sẻ. Theo như những gì mình học và thực hành được từ thực tế, một flow debug sẽ có các bước như sau:

    1. Discover: Xác định lỗi, đơn giản là chúng ta cần phải biết được lỗi đang gặp phải là gì, là crash app, freeze app, memory leak, business chạy không đúng, data không
    2. Locate: Xác định vị trí đoạn code gây ra lỗi
    3. Inspect: Kiểm tra flow, dữ liệu, logic để tìm ra nguyên nhân khiến đoạn code chạy sai
    4. Fix: Tất nhiên là sửa lại đoạn code cho đúng
    5. Confirm: Đảm bảo là lỗi đã được giải quyết và không có ảnh hưởng, hậu quả gì để lại

    Debugger Tools

    Tiếp theo chúng ta sẽ đi qua một lượt những gì Xcode cung cấp trong Debugger Tools. Đây là màn hình capture các thành phần Debugger Tools Xcode cung cấp cho chúng ta:

    Debug Navigator

    Debug Navigator là khu vực bên trái, gồm có 2 thành phần chính:

    1. Debug Gauge: Là thước đo các chỉ số của ứng dụng như CPU, Memory, Disk I/O, GPU, … Theo dõi các chỉ số trên Debug Gauge có thể giúp ích trong quá trình Discover các bug liên quan đến Memory Leak, CPU Usage, Read/Write Data, FPS, …
    2. Process View: Hiển thị các Threads đang chạy và Call Stack Trace đến breakpoint tại thời điểm ứng dụng dừng để debug. Sử dụng Process View giúp ích trong việc Locate các đoạn code gây nên lỗi, các bạn có thể dễ dàng tìm được đoạn một đoạn code/hàm được gọi từ đâu chỉ bằng việc nhìn vào Call Stack thay vì đọc và search từng dòng code.
    Nhìn vào CallStack ta có thể thấy, Breakpoint được gọi có nguồn gốc từ viewDidLoad() của ViewController

    Breakpoint

    Breakpoint là khái niệm rất quen thuộc ở mọi Debugger Tools, chính là đánh dấu cho vị trí được đánh dấu để debug, khi ứng dụng chạy đến vị trí code này, Debugger Tools sẽ tự động dừng ứng dụng và thực hiện quá trình debug, hiển thị các thông tin tại thời điểm và vị trí đặt Breakpoint.

    Chúng ta có thể đặt nhiều Breakpoint, ứng dụng sẽ dừng ở Breakpoint đầu tiên chạy tới.

    Source Editor

    Source Editor là khu vực để thay đổi source code, tuy nhiên trong khi ứng dụng đang ở chế độ debug chúng ta có thể di chuyển pointer tới các instance cần kiểm tra, và inspect trực tiếp các thông tin của instance đó. Đối với một số data type đặc biệt như UIImage, chúng ta có thể view trực tiếp ảnh từ Source Editor.

    Inspect UIImage
    Inspect các thông tin của instance

    Debug Bar

    Debug Bar bao gồm bộ các chức năng hỗ trợ trong quá trình debug, dưới đây là một số chức năng mình hay sử dụng:

    1. Hide/Show: Hiển thị hoặc Ẩn khu vực debug bên dưới Debug Bar
    2. Breakpoint Activation: Enable/Disable các Breakpoint, khi disable thì ứng dụng sẽ không dừng lại khi xử lý qua các vị trí đặt Breakpoint
    3. Continue/Pause: Tạm dừng/Tiếp tục việc thực thi code của ứng dụng
    4. Step controls: Thực thi dòng code tại vị trí hiện tại; Nếu vị trí hiện tại là function, jump vào function đó; Jump ra vị trí đang gọi đến function chứ vị trí hiện tại
    5. Debug View Hierarchy: Hiển thị cấu trúc các Controllers, Views dưới dạng 3D. Hỗ trợ việc theo dõi vị trí và mối quan hệ của các UIComponents tại một thời điểm

    Variables View

    Variables View là khu vực bên trái, phía dưới Debug Bar, nơi hiển thị các instance của ứng dụng tại thời điểm debug. Tuy nhiên sẽ chỉ hiển thị các instance được sử gọi đến trong block/function chứa vị trí Breakpoint.

    Ngoài việc hiển thị, chúng ta còn có thể inspect/edit thông tin các instance

    Console & LLBD

    Console View là nơi hiển thị các thông tin chúng ta in ra console. Ngoài ra, Console còn được tích hợp thêm LLDB Command, giúp cho việc Debug đã dễ lại càng trở nên dễ dàng hơn. Mình sẽ liệt kê một số LLDB Command thông dụng thường đươc sử dụng trong khi Debug.

    • po variable: In ra giá trị của một biến
    • p variable: In ra toàn bộ thông tin của một biến
    • fr v: In ra toàn bộ thông tin của các biến
    • e expression: Thực thi một expression, ví dụ: e i=10 sẽ thay đổi giá trị của i bằng 10; e var $x=10 sẽ khai báo thêm một biến có giá trị 10.
    Ví dụ sử dụng LLDB Command

    Ngoài ra còn rất nhiều command hữu ích khác, các bạn có thể tham khảo thêm ở đây.

    Trên đây là một số kiến thức và kinh nghiệm của mình, nếu có gì thiếu xót mong được các bạn góp ý. Cảm ơn đã ủng hộ!

    Authors

    MinhNN44

  • iOS Home Screen Action

    iOS Home Screen Action

    Overview

    Khi sử dụng một số ứng dụng phổ biến như Zalo, Telegram hay YouTube, … trên các thiết bị iOS 13 trở lên, hẳn chúng ta đã từng nhìn thấy một số option mới khi long press ở Home Screen như hình dưới. Ở bài viết này, chúng ta sẽ cùng tìm hiểu cách để tạo ra những option đó.

    Các option hay shortcut này được gọi là Home Screen Actions (hay Home Screen Quick Actions), được hỗ trợ từ iOS 13 trở lên. Đúng như cái tên của nó, đây là những shortcut được thiết kế để thực hiện trực tiếp lựa chọn các action của ứng dụng tại màn hình Home thay vì phải mở và thao tác nhiều lần bên trong ứng dụng.

    Hãy lấy ví dụ như chúng ta có ứng dụng Safari, khi muốn mở Techover trên Safari ta có một số lựa chọn như sau:

    1. Mở Safari -> Tap vào thanh địa chỉ -> Nhập magz.techover.io -> ấn Go
    2. Mở Safari -> Tap New Tab -> Chọn BookMark -> Chọn magz.techover.io

    Có thể thấy, để thực hiện công việc trên chúng ta đều cần ít nhất 4 bước, lựa chọn đầu tiên còn yêu cầu người dùng nhập địa chỉ thủ công. Thay vì thế, ta có thể đơn giản hoá bằng cách đưa magz.techover.io vào một shortcut của Safari, chỉ với hai lần chạm, ta đã có thể mở trang web trên Safari một cách nhanh chóng.

    Home Screen Action có hai loại, Static và Dynamic:

    1. Static: Luôn cố định, không thể thay đổi tuỳ chỉnh được
    2. Dynamic: Có thể tuỳ chỉnh, thay đổi tuỳ theo hoạt động của ứng dụng

    Giờ chúng ta sẽ đi tìm hiểu, làm sao để cấu hình cho từng loại Home Screen Action.

    Home Screen Static Action

    Đối với Static Actions, chúng ta sẽ cài đặt ở trong Info.plist với cấu trúc sau:

    <key>UIApplicationShortcutItems</key>
    <array>
        <dict>
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeHome</string>
            <key>UIApplicationShortcutItemTitle</key>
            <string>Techover</string>
            <key>UIApplicationShortcutItemSubtitle</key>
            <string>Open Techover</string>
            <key>UIApplicationShortcutItemType</key>
            <string>Techover</string>
        </dict>
        <dict>
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeCompose</string>
            <key>UIApplicationShortcutItemTitle</key>
            <string>New</string>
            <key>UIApplicationShortcutItemSubtitle</key>
            <string>Create New Post</string>
            <key>UIApplicationShortcutItemType</key>
            <string>Create</string>
        </dict>
    </array>

    Trong đó:

    • Key UIApplicationShortcutItemIconType là define Icon cho Action, các bạn có thể tham khảo list value ở đây
    • Key UIApplicationShortcutItemTitle là define Title cho Action
    • Key UIApplicationShortcutItemSubtitle là define SubTitle, dòng chữ nhỏ phía dưới Title
    • Key UIApplicationShortcutItemType là một String dùng để định danh, phải là duy nhất, dùng để handle xử lý Action ở trong code.

    Sau khi setting xong, chúng ta sẽ có kết quả như sau:

    Home Screen Dynamic Action

    Đối với Dynamic Actions, chúng ta có thể thêm bất cứ chỗ nào bên trong xử lý của ứng dụng bằng đoạn code sau:

    let application = UIApplication.shared
    var shortcuts = application.shortcutItems
    shortcuts?.append(UIApplicationShortcutItem(type: "FavoriteAction",
                                                localizedTitle: "Home Screen Action",
                                                localizedSubtitle: "My first Technopedia post",
                                                icon: UIApplicationShortcutIcon(systemImageName: "star.fill"),
                                                userInfo: ["Marked Post": "Minhnn44-1" as NSSecureCoding]))
    shortcuts?.append(UIApplicationShortcutItem(type: "FavoriteAction",
                                                localizedTitle: "iOS Distribution Methods",
                                                localizedSubtitle: "My seconds Technopedia post",
                                                icon: UIApplicationShortcutIcon(systemImageName: "star.fill"),
                                                userInfo: ["Marked Post": "Minhnn44-2" as NSSecureCoding]))
    application.shortcutItems = shortcuts

    Trong đó, UIApplication.shared.shortcutItems là một [UIApplicationShortcutItem], mỗi một UIApplicationShortcutItem tượng trưng cho một Action ở Home Screen với các tham số khởi tạo:

    • type: String định danh của Action
    • localizedTitle: Title
    • localizedSubtitle: SubTitle
    • icon: icon
    • userInfo: Dictionary để lưu các thông tin cần truyền thêm

    Action Handle

    Việc handle khi người dùng ấn các action sẽ được thực hiện bên trong SceneDelegate như sau.
    Đối với trường hợp ứng dụng chưa được mở (not running), chúng ta sẽ handle bên trong hàm scene(_:willConnectTo:options:) như sau:

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        /** Process the quick action if the user selected one to launch the app.
            Grab a reference to the shortcutItem to use in the scene.
        */
        if let shortcutItem = connectionOptions.shortcutItem {
            // Save it off for later when we become active.
            savedShortCutItem = shortcutItem
        }
    }

    Trong trường hợp ứng dụng đang ở background, chúng ta sẽ handle bên trong hàm windowScene(_:performActionFor:completionHandler:) như sau:

    func windowScene(_ windowScene: UIWindowScene,
                     performActionFor shortcutItem: UIApplicationShortcutItem,
                     completionHandler: @escaping (Bool) -> Void) {
        let handled = handleShortCutItem(shortcutItem: shortcutItem)
        completionHandler(handled)
    }
    
    func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
        /** In this sample an alert is being shown to indicate that the action has been triggered,
            but in real code the functionality for the quick action would be triggered.
        */
        switch shortcutItem.type {
        case "Techover":
            print("Techover Selected")
        case "Create":
            print("Create New Post Selected")
        case "FavoriteAction":
            if let markedPost = shortcutItem.userInfo?["Marked Post"] as? String {
                print("Open \(markedPost)")
            }
            break
        default:
            break
        }
        return true
    }

    Vậy là xong, nếu có thắc mắc hoặc góp ý thì các bạn comment phía dưới để mình giải đáp và cái thiện nhé!

    Authors

    MinhNN44

  • Hiểu và phân biệt các Distribution Methods

    Hiểu và phân biệt các Distribution Methods

    Overview

    Đối với lập trình viên iOS, việc archive và distribute app là một kỹ năng quan trọng cần phải có. Xcode cung cấp cho người dùng khá nhiều phương pháp để distribute một ứng dụng, việc hiểu và lựa chọn phương pháp phù hợp với từng yêu cầu sẽ giúp ta rất nhiều.

    Xcode cung cấp một số phương pháp Distribution như sau:

    1. App Store Connect
    2. Ad Hoc
    3. Enterprise
    4. Development
    Distribution Methods

    1. App Store Connect

    Đây là phương pháp Distribution được sử dụng nhiều nhất trong những dự án làm Product, khi phát triển các ứng dụng đã, đang và sẵn sàng đưa lên App Store. Phương pháp này sẽ tạo ra một bản Distribution nhằm mục đích upload trực tiếp ứng dụng lên App Store Connect. Xcode cho phép lựa chọn hỗ trợ upload tự động hoặc xuất ra .ipa file để người dùng upload thủ công. Sau khi upload ứng dụng lên App Store Connect, ta có thể test lại ứng dụng thông qua TestFlight và Submit For Review để chờ Apple review qua lần cuối trước khi chính thức đưa ứng dụng lên App Store.

    Lưu ý: Phương pháp này chỉ hỗ trợ các ứng dụng được build & distribute với certificate của tài khoản Apple Developer thông thường (99$).

    Có 2 lựa chọn: Upload tự động hoặc xuất .ipa

    2. Ad Hoc

    Phương pháp Ad Hoc có cùng một cơ chế và cấu hình như App Store Connect, mục đích là để kiểm thử ứng dụng mà không cần đến App Store Connect/TestFlight. Các cấu hình có phân chia Sandbox/Production như APNs sẽ được nhận Production khi sử dụng Ad Hoc. Ad hoc hỗ trợ xuất ra file .ipa có thể cài đặt trực tiếp vào iPhone thông qua Xcode hoặc iTunes, trình duyệt Safari (OTA). Có một số điểm mạnh của Ad Hoc có thể liệt kê:

    1. Tạo ra phiên bản hoàn toàn giống với App Store, thuận lợi cho việc test trước khi go live
    2. Không phụ thuộc vào App Store Connect: Không mất thời gian chờ Processing Binary, không cần phải invite user vào TestFlight
    3. Linh động và đa dạng trong việc cài đặt ứng dụng, hỗ trợ cả OTA

    Tuy vậy, Ad Hoc cũng có những hạn chế:

    1. Thiết bị iPhone cần đăng ký liên kết với tài khoản Apple Developer để có thể cài đặt được ứng dụng
    2. Mỗi tài khoản Apple Developer có giới hạn 100 thiết bị, Apple chỉ cho phép xoá mỗi năm một lần vào thời điểm Renew Payment
    3. Mỗi lần đăng ký thêm một thiết bị, cần build và distribute lại ứng dụng mới có thể cài được trên thiết bị mới
    4. Ứng dụng sẽ không thể sử dụng khi certificate/provisioning profile dùng để distribute hết hạn

    3. Enterprise

    Enterprise là phương pháp khác biệt nhất trong 4 phương pháp, việc đầu tiên cần phải kể đến là yêu cầu cần phải có tài khoản Apple Developer phải tham gia vào chương trình Enterprise (299$). Việc đăng ký tham gia chương trình Enterprise khá nhiều thủ tục và yêu cầu những thông tin khó tiếp cận hay sở hữu (DUNS). Bù lại, những lợi ích phương pháp này mang lại rất lớn.

    Đầu tiên, đó là việc Distribute ứng dụng đến người dùng một cách không giới hạn mà không cần sử dụng đến App Store. Mục đích của phương pháp Enterprise là tạo ra một bản ứng dụng với cấu hình Production với mục đích phát hành nội bộ bên trong một tổ chức, doanh nghiệp thay vì công khai trên App Store. Việc này sẽ đảm bảo tính bảo mật của doanh nghiệp, sự nhanh chóng và linh hoạt trong việc cập nhật vì không bị phụ thuộc vào App Store. Theo đó, sẽ có nhiều mục đích khác được sử dụng như phát hành game lậu, ứng dụng lậu, các ứng dụng vi phạm bản quyền hoặc bất cứ chính sách nào đó của App Store, hoặc đơn giản là vì không muốn phụ thuộc vào App Store.

    Ngoài việc phá bỏ các rườm rà liên quan đến thiết bị của Ad Hoc, Enterprise vẫn giữ được các điểm mạnh như hỗ trợ nhiều phương pháp cài đặt (Xcode, OTA, …). Tuy vậy, ứng dụng vẫn có thể không thể sử dụng nếu certificate/provisioning profile bị hết hạn.

    4. Development

    Development là phương pháp tạo một bản ứng dụng với môi trường Sandbox, có thể cài đặt với những tài khoản được đăng ký liên kết với Apple Developer Account dưới role Developer hoặc Admin. Đây là phương pháp thường sử dụng để phân phối bản build cho Tester trong giai đoạn IT khi không muốn tác động đến môi trường Production. Phương pháp này cũng có điểm hạn chế về việc phải build lại ứng dụng khi thêm một tài khoản mới. Phương pháp cũng hỗ trợ các hình thức cài đặt giống như Ad Hoc (Xcode, iTunes, OTA).

    Authors

    MinhNN44