Category: Swift

  • Operation (P2)

    Operation (P2)

    Ở phần 1 của bài viết, mình đã giới thiệu về Operation là gì. Ở phần 2 của loạt bài về Operation, mình sẽ nói về Dependency trong Operation.

    Nội dung bài viết:

    • Operation Dependencies
    • Passing Data using Dependencies

    Operation Dependencies:

    Operation cho phép bạn thiết lập các sự phụ thuộc lẫn nhau. Điều này mang lại 2 lợi ích:

    • Giả sử operation 2 phụ thuộc vào operation 1. Khi đó operation 2 chỉ được thực hiện sau khi operation 1 đã hoàn thành.
    • Cung cấp 1 cách để bạn truyền data giữa các operation.
    class DownloadImage: Operation {
        var index: Int
        
        init(ind: Int) {
            self.index = ind
        }
    
        override func main() {
            // Download image task
            print("Start downloading task \(index) at time: \(Date().timeIntervalSince1970)")
            sleep(5)
            print("Finish downloading task \(index) at time: \(Date().timeIntervalSince1970)")
        }
    }
    
    let firstOperation = DownloadImage(ind: 1)
    let secondOperation = DownloadImage(ind: 2)
    //firstOperation.addDependency(secondOperation)
    
    let operationQueue = OperationQueue()
    operationQueue.addOperation(firstOperation)
    operationQueue.addOperation(secondOperation)

    Ở trên là đoạn code khởi tạo 2 operation bình thường, giữa chúng chưa có dependency. Chạy đoạn code trên và đây là kết quả thu được:

    2 task chạy song song

    Giờ thì bỏ comment dòng code firstOperation.addDependency(secondOperation) và chạy thử:

    Task 1 phụ thuộc vào task 2. Vì vậy, task 1 chỉ chạy khi task 2 đã hoàn thành.

    Note: Bạn có thể tạo dependency cho 2 opeartion đang chạy ở 2 operation queue khác nhau.

    Đây là 1 cách khá ngắn trong khi ở GCD bạn phải khai báo 1 dispatchGroup, sau đó gọi hàm enter(), leave(), notify(), … Tuy nhiên, cách làm này rất dễ gây ra deadlock.

    Để remove 1 dependency, bạn chỉ cần gọi:

    firstOperation.removeDependency(op: secondOperation)
    Task 5 chỉ được chạy khi task 2 hoàn thành, task 2 chỉ chạy khi task 3 hoàn thành, task 3 chỉ chạy khi task 5 hoàn thành.

    Ở đoạn code mẫu ở trên, nếu bạn sửa đoạn code thành như dưới đây thì code của bạn sẽ bị deadlock và không chạy.

    firstOperation.addDependency(secondOperation)
    secondOperation.addDependency(firstOperation)

    Note: Hãy vẽ sơ đồ ra 1 tờ giấy để luôn clear về flow của bạn, tránh bị deadlock.

    Truyền data giữa các operation thông qua dependency:

    class Calculate: Operation {
        let firstNum: Int
        let secondNum: Int
        var sum: Int?
        
        init(first: Int, second: Int) {
            self.firstNum = first
            self.secondNum = second
        }
        
        override func main() {
            sum = firstNum + secondNum
        }
    }
    
    class Display: Operation {
        
        override func main() {
            // 2
            let sum = dependencies.compactMap{ ($0 as? Calculate)?.sum }.first
            
            guard let unwrappedSum = sum else {
                return
            }
            // 3
            print("Sum = \(unwrappedSum)")
        }
    }
    
    let calculateOperation = Calculate(first: 5, second: 10)
    let displayOperation = Display()
    // 1
    displayOperation.addDependency(calculateOperation)
    
    
    let operationQueue = OperationQueue()
    // 4
    operationQueue.addOperation(calculateOperation)
    operationQueue.addOperation(displayOperation)
    1. Khởi tạo 2 operation, và set dependency để task Display chỉ chạy sau khi task Calculate hoàn thành.
    2. Hàm main() là hãm sẽ chạy khi 1 operation được chạy.
      Ở đây, ta sẽ lấy ra list operations có dependency với DisplayOperation, chọn ra operation nào là Calculate và lấy ra sum.
    3. Nếu sum khác nil thì hiển thị ra.
    4. Add các operation vào queue để chạy.

    Kết quả hiển thị trên màn hình Console:

    Dependency trong Operation là 1 trong những thứ giúp Operation vượt trội hơn so với GCD.

    Ở phần tiếp, mình sẽ nói về Async Operation và xử lí cancel Opeartion.

  • iOS/Swift: UIPanGestureReconizer

    iOS/Swift: UIPanGestureReconizer

    Lời mở đầu

    Xin chào mọi người, hôm nay mình sẽ giới thiệu với các bạn về UIPanGestureReconizer. Và mình sẽ hướng dẫn mọi người cách sử dụng và ứng dụng nó vào thực tế. Mình hi vọng sau khi xem hết bài viết này, mọi người có thể áp dụng nó vào các ứng dụng sau này nếu cần đến 😀

    Ý tưởng

    Chắc hẳn ai sử dụng iOS đều sử dụng qua tính năng “Control Center” của iOS. Để mở Control center lên ta phải vuốt từ dưới màn hình lên và ẩn đi thì ngược lại, nó còn cho phép người dùng kéo thả một cách mượt mà.

    UIGestureRecognizer là gì?

    UIGestureRecognizer là class định nghĩa một tập hợp các hành vi phổ biến có thể được cấu hình cho tất cả các cử chỉ cụ thể. Dạng như chạm, kéo thả …

    UIGestureRecognizer được sử dụng để nhận dạng các loại hành vi của người dùng khi tương tác lên màn hình và thực hiện hành động được cấu hình.

    Dưới đây là một số subclasses của nó:

    UIPanGestureRecognizer là gì?

    UIPangestureRecognizer là subclass của UIGestureRecognizer dùng để nhận biết hành vi kéo thả.

    Ví dụ về UIPanGestureRecognizer

    Để bắt đầu chúng ta cần mở XCode và tạo mới một Single View App. Sau khi tạo project xong chúng ta mở file Main.storyboard và kéo vào một UIView -> đổi màu nền(Background color) của view sang một màu khác cho dễ nhìn.

    Khi này chúng ta cần gán constraint cho view đó theo Hình 1
    Cần chú ý ở đây chúng ta sẽ constraint bottom của view vào bottom của superview thay vì Safe Area để khi build trên các dòng điện thoại tai thỏ (iphone x, xs ….) sẽ không bị trắng ở dưới của màn hình.

    Hình 1

    Tiếp đến chúng ta sẽ add 1 UIView nhỏ vào trong UIView vừa mới tạo để thể hiện view này có thể kéo thả được.
    Chúng ta cũng constraint UIView nhỏ để nó nằm trên top và giữa của superview kết quả chúng ta thu được như Hình 2

    Hình 2

    Vậy là chúng ta đã xong phần UI, tiếp đến chúng ta cần kéo IBOutlet top constraint cho cái view to và đặt tên nó là topViewContainer kết quả sẽ được như hình 3

    Chúng ta kéo IBOutlet cho top constraint của view để làm gì? Mục đích ở đây là để sau này chúng ta sẽ thay đổi giá trị của topConstraint.constant -> thay đổi vị trí và để tạo hiệu ứng chuyển động.

    Tiếp đến chúng ta kéo outlet cho thằng View to đặt tên là viewContainer

    OK, giờ chúng ta sẽ đi vào code chi tiết.
    Giờ chúng ta mở file ViewController.swift ra và tạo enum cho các trạng thái mở rộng

    // 1: Tạo 3 trạng thái mở rộng cho viewContainer
    enum ExpansionState {
        case compressed
        case haft
        case expanded
    }

    Tạo pangesture và gán nó cho viewContainer, trong code mình đã comment rõ từng dòng, từng hàm để các bạn có thể hiểu được nó làm gì.

       // thêm UIGestureRecognizerDelegate để sử dụng được PanGesture
    class ViewController: UIViewController, UIGestureRecognizerDelegate {
    
        @IBOutlet weak var viewContainer: UIView!
        @IBOutlet weak var topViewContainer: NSLayoutConstraint!
        // Khởi tạo trạng thái
        var expansionState: ExpansionState = .haft
        // Khởi tạo giá trị cũ của topViewContainer
        var oldTopViewContainer: CGFloat = 0
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            setupGestureRecognizers()
            // Khởi tạo vị trí bắt đầu cho việc animte viewContainer.
            // Ở đây chúng ta đang để nó ở trạng thái compressed có nghĩa là ở trạng thái expansion nhỏ nhất.
            topViewContainer.constant = topViewContainer(forState: .compressed)
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
            // Do mình muốn viewContainer sẽ chuyển động từ dưới lên trên và dừng lại ở trạng thái expansion 1 nửa màn hình
            // Nên mình gọi hàm phía dưới để nó thực hiện chuyển động vì ở viewDidLoad nó đang ở trạng thái compressed
            animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: CGPoint(x: 0, y: 50))
        }
    
        // Cài đặt Pangesture,
        func setupGestureRecognizers() {
            let panGestureRecognizer = UIPanGestureRecognizer(target: self,
                                                              action: #selector(panGestureDidMove(sender:)))
            panGestureRecognizer.delegate = self
            viewContainer.isUserInteractionEnabled = true
    
            viewContainer.addGestureRecognizer(panGestureRecognizer)
        }
    
        @objc func panGestureDidMove(sender: UIPanGestureRecognizer) {
            let translationPoint = sender.translation(in: view.superview)
            // velocity là vận tốc chuyển động của hành động, dạng như bạn vuốt vào màn hình nhanh hay chậm
            let velocity = sender.velocity(in: view.superview)
    
            switch sender.state {
            case .changed:
                panGesture(didChangeTranslationPoint: translationPoint, withVelocity: velocity)
            case .ended:
                panGesture(didEndTranslationPoint: translationPoint, withVelocity: velocity)
            default:
                return
            }
        }
    
        // Phương thức này để tính toán giá trị viewContainer.top theo các trạng thái thái mà nó cần tính
        func topViewContainer(forState state: ExpansionState) -> CGFloat {
            let heightView = self.view.bounds.height
            switch state {
            case .compressed:
                return heightView - 100
            case .haft:
                return heightView / 2
            case .expanded:
                return 100
            }
        }
    }

    Tiếp đến chúng ta cần tạo Extenstion cho phần animation để phục vụ cho việc chuyển động view container

    / MARK: Animation
    extension ViewController {
        /// Animates the top constraint of the drawerViewController by a given constant
        /// using velocity to calculate a spring and damping animation effect.
        func animateTopConstraint(constant: CGFloat, velocity: CGPoint) {
            let previousConstraint = topViewContainer.constant
            let distance = previousConstraint - constant
            let springVelocity = velocity.y != 0 ? max(1 / (abs(velocity.y / distance)), 0.08) : 0.08
            let springDampening = CGFloat(0.6)
    
            UIView.animate(withDuration: 0.5,
                           delay: 0.0,
                           usingSpringWithDamping: springDampening,
                           initialSpringVelocity: springVelocity,
                           options: [.curveLinear],
                           animations: {
                            self.topViewContainer.constant = constant
                            self.oldTopViewContainer = constant
                            self.view.layoutIfNeeded()
            }, completion: nil)
        }
    }

    Tiếp đến chúng ta cần các hàm để xử lí khi mà người dùng kéo viewContainer
    Ở trong code mình đã comment các hàm, các dòng để mọi người dễ hiểu, hãy đọc trong code nhé.

    //MARK: PanGesture handle
    extension ViewController {
    
        // Hàm này được gọi khi PanGestureRecognizer nhận ra được sự thay đổi của view,
        // trong hàm này chúng ta có thể thực hiện một số điều kiện để giới hạn việc kéo của người dùng
        func panGesture(didChangeTranslationPoint translationPoint: CGPoint,
                                  withVelocity velocity: CGPoint) {
            let newConstraintConstant = oldTopViewContainer + translationPoint.y
            // Giới hạn việc người dùng kéo quá xa so với phía trên
            if newConstraintConstant >= 0 {
                topViewContainer.constant = newConstraintConstant
            }
        }
    
        // Phương thức này được gọi khi kết thúc pan
        func panGesture(didEndTranslationPoint translationPoint: CGPoint,
                                  withVelocity velocity: CGPoint) {
            // Velocity là kiểu CGPoint vì vậy nó có 2 giá trị x và y
            // x: đại diện cho vận tốc theo chiều ngang
            // y: đại điện cho vận tốc theo chiều dọc
            // Do mình đang muốn thực hiện kéo thả theo chiều dọc nên ở ví dụ này mình sẽ dùng thuộc tính vận tốc y
            if abs(velocity.y) <= 50.0 {
                // 50: ở đây là vận tốc mà mình coi nó là người dùng đang kéo từ từ. Con số này các bạn có thể tùy chỉnh
                // Giá trị của y < 0 có nghĩa là người dùng đang kéo lên trên và y > 0 là kéo xuống dưới
                // dấu của y sẽ thể hiện chiều di chuyển của nó.
                // Vì vậy mình cần sử dụng hàm abs() trước khi thực hiện phép so sánh
                drag(lowVelocity: velocity)
            } else if velocity.y > 0 {
                dragDown(highVelocity: velocity)
            } else {
                dragUp(highVelocity: velocity)
            }
        }
    
        // Hàm này thực hiện việc xác định xem view đang gần vị trí của trạng thái nào hơn sẽ chuyển sang trạng thái đó.
        func drag(lowVelocity velocity: CGPoint) {
            let compressedTopConstraint = topViewContainer(forState: .compressed)
            let haftTopConstraint = topViewContainer(forState: .haft)
            let expandedTopConstraint = topViewContainer(forState: .expanded)
    
            let expandedDifference = abs(topViewContainer.constant - expandedTopConstraint)
            let haftDifference = abs(topViewContainer.constant - haftTopConstraint)
            let compressedDifference = abs(topViewContainer.constant - compressedTopConstraint)
    
            let heightArray = [expandedDifference, haftDifference, compressedDifference]
            let minHeight = heightArray.min()
    
            if expandedDifference == minHeight {
                expansionState = .expanded
                animateTopConstraint(constant: expandedTopConstraint, velocity: velocity)
            } else if haftDifference == minHeight {
                expansionState = .haft
                animateTopConstraint(constant: haftTopConstraint, velocity: velocity)
            } else {
                expansionState = .compressed
                animateTopConstraint(constant: compressedTopConstraint, velocity: velocity)
            }
        }
    
        // Hàm này để xử lí khi người dùng kéo thả view với tốc độ cao,
        // nó sẽ xác định và chuyển tới trạng thái liền kề theo hướng phía dưới
        func dragDown(highVelocity velocity: CGPoint) {
            // Handle High Velocity Pan Gesture
    
            switch expansionState {
            case .compressed, .haft:
                expansionState = .compressed
                animateTopConstraint(constant: topViewContainer(forState: .compressed), velocity: velocity)
            case .expanded:
                expansionState = .haft
                animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: velocity)
            }
        }
    
        // Hàm này để xử lí khi người dùng kéo thả view với tốc độ cao,
        // nó sẽ xác định và chuyển tới trạng thái liền kề theo hướng phía trên
        func dragUp(highVelocity velocity: CGPoint) {
            // Handle high Velocity Pan Gesture
            switch expansionState {
            case .compressed:
                expansionState = .haft
                animateTopConstraint(constant: topViewContainer(forState: .haft), velocity: velocity)
            case .haft, .expanded:
                expansionState = .expanded
                animateTopConstraint(constant: topViewContainer(forState: .expanded), velocity: velocity)
            }
        }
    }

    Và đây là kết quả khi chúng ta build app lên:

    Vậy là xong!
    Ngoài ra các bạn có thể chỉnh sửa lại view sao cho phù hợp với ứng dụng đang làm 😀


    Cảm ơn mọi người đã theo dõi bài viết!
    Mọi ý kiến đóng góp mọi người hãy comment xuống phía dưới để mình có thể thay đổi cho bài biết được tốt hơn. :v

  • Operation (P1)

    Operation (P1)

    Cũng giống như GCD, Operations giúp chúng ta có thể thực hiện các task đa luồng. Vì Operations là API bậc cao hơn GCD, do đó, Operations cung cấp nhiều tính năng hơn so với GCD như cung cấp các state để quản lí task, dependency giữa các task,… nhưng cũng vì thế mà để khơi tạo và sử dụng operation sẽ khó hơn so với GCD.

    Nếu bạn chưa hiểu về GCD, hãy đọc bài về GCD của mình tại:

    Ở phần 1 của bài viết Operation, mình sẽ giới thiệu về tính chất và các thuộc tính của Operation và Operation queue.

    Nội dung bài viết

    • Operation State
    • Block Operation
    • Subclass Operation
    • Operation Queue

    Operation States

    • Operation cũng có công dụng như là 1 DispatchWorkItem vậy. Chúng đều là 1 task, bạn khai báo thân hàm cho chúng và đưa chúng vào queue để thực hiện.
    • Tuy nhiên, điểm vượt trội của Operation so với DispatchWorkItem là mỗi Operation đều có State của riêng mình, còn workItem thì không.
    • isReady: sẵn sàng để được thực hiện
    • isExecuting: đang được thực hiện
    • isCancelled: Nếu bạn gọi phương thức cancel(), operation sẽ chuyển qua state isCancelled trước khi chuyển sang state isFinished.
    • isFinished: Nếu operation k bị cancel, nó sẽ chuyển từ state isExecuting sang isFinished khi hoàn thành.

    Note:

    • Các state của Operation là các thuộc tính read-only.
    • Bạn có thể gọi cancel() để hủy 1 operation, nhưng tương tự như workItem, chúng chỉ có thể bị cancel trước khi được thực hiện. Tuy nhiên, do Operation có state, nên bạn có thể kiểm tra state trong thân hàm để cancel hàm. Mình sẽ làm rõ hơn ở phần sau.

    Block Operation

    Có thể tạo 1 operation 1 cách nhanh chóng bằng cách sử dụng 1 block operation:

    Gọi hàm start() để thực hiện BlockOperation

    Bạn cũng có thể add thêm nhiều closures vào block operation:

    Block Operation hoạt động như một DispatchGroup. Nếu bạn cung cấp 1 completionBlock closure cho blockOperation, thì nó sẽ được thực hiên khi tất cả các closure được add vào block operation đã được thực hiện hết (tương tự hàm notify của DispatchGroup).

    Kết quả thu được trên màn hình console:

    Note: Từ kết quả thu được ở trên, ta có thể dễ dàng kết luận:

    • Task trong block operation chạy concurrent.
    • Operation Blocks chạy default trên global queue.

    Subclass Operation

    BlockOperation rất dễ sử dụng và thích hợp cho các task đơn giản. Tuy nhiên, đối với những task phức tạp cần kiểm soát state, hoặc những task sử dụng nhiều lần, thì bạn nên tự tạo 1 Operation để sử dụng.

    • Bạn phải override lại hàm main(), đây là hàm được thực hiện khi operation đó bắt đầu thực hiện.
    • Gọi start() để chạy. Tuy nhiên, lưu í rằng, khi gọi start() 1 cách trực tiếp trên 1 operation, Operation đó sẽ chạy theo kiểu sync trên current thread. -> Không nên gọi start trên main thread.

    Note: Không chỉ việc gọi start sẽ block current thread, mà nó còn dễ dẫn tới 1 exception rằng task đó chưa sẵn sàng để thực hiện. Vì vậy bạn không nên gọi start ở 1 operation.

    Vậy làm cách nào để có thể start 1 operation ? Câu trả lời cho việc này là tương tự như DispatchWorkItem, chúng ta sẽ đưa operation vào Operation Queue để chạy.

    Operation Queue:

    Bạn đưa các task vào trong operation queue, operation queue sẽ tự động thực hiện task khi thích hợp mà bạn không cần phải gọi start.

    • Operation Queue chỉ thực hiện task khi task đó đang ở state isReady.
    • Khi bạn add 1 operation vào 1 queue, task đó sẽ chạy cho đến khi hoàn thành hoặc bị hủy (cancel).
    • Không thể add 1 operation vào nhiều operation queues khác nhau.

    Block queue:

    • Operation queue có 1 thuộc tính là waitUntilAllOperationsAreFinished, có tác dụng block queue hiện tại, vì vậy bạn sẽ không nên gọi trên main thread.
    • Trong trường hợp bạn không muốn block cả 1 queue chỉ để đợi 1 task thực hiện xong mà bạn chỉ muốn block 1 vài task, bạn có thể gọi addOperations(_:waitUntilFinished:) method on OperationQueue.

    Tạm dừng queue:

    Bạn có thể tạm dừng operation queue bằng cách set isSuspend = true.
    -> Những task đang được thực hiện vẫn sẽ dc tiếp tục thực hiện, nhưng những task chưa dc thực hiện thì sẽ đợi cho đến khi isSuspend được set lại thành false.

    Set số lượng operations tối đa

    Còn nếu như bạn muốn giới hạn số lượng tối đa operations chạy trong 1 Operation Queue tại 1 thời điểm, chẳng hạn như việc chỉ load những ảnh của những visible cell của collectionView chẳng hạn?
    Khi đó, bạn chỉ cần set thuộc tính maxConcurrentOperationCount của Operation Queue thành 1 số bạn muốn.

    class DownloadImage: Operation {
        override func main() {
            // Download image task
            print("Start downloading... at time: \(Date().timeIntervalSince1970)")
            sleep(5)
            print("Finish downloading... at time: \(Date().timeIntervalSince1970)")
        }
    }
    
    let operation = DownloadImage()
    let operation2 = DownloadImage()
    
    let operationQueue = OperationQueue()
    operationQueue.maxConcurrentOperationCount = 1
    operationQueue.addOperations([operation, operation2], waitUntilFinished: true)

    Nếu set maxConcurrentOperationCount = 1, thì task 1 chạy xong, sau đó task 2 mới chạy. Đây là kết quả trên màn hình console:

    Nếu set maxConcurrentOperationCount = 2, thì 2 task sẽ chạy song song. Đây là kết quả trên màn hình console:

    Nội dung phần tiếp:

    • Dependency Operation

    Nguồn tham khảo: Ray Wenderlich

  • [iOS] – Ai cũng có thể làm Animation (Part 3)

    [iOS] – Ai cũng có thể làm Animation (Part 3)

    Ở part cuối của Animation Basic này, mình xin giới thiệu đến mọi người phần ảo diệu nhất của animation với UIView đó là UIView.transition. Transitions được dùng khi chúng ta muốn tạo hiệu ứng cho việc hidden view hay add hoặc remove một view lên hoặc khỏi một view cha của nó hay thay thế 2 views cho nhau khá tương đồng với các slide chúng ta hay gặp trong power point.

    UIView Transitions và các hiệu ứng của View

    UIView.transition(with: self.view, duration: 2.0, options:[.transitionCrossDissolve, .repeat], animations: {
            self.view.addSubview(self.animatedView)
    }, completion: nil)

    Tương tự keyframe và animation, transition cũng gồm các thuộc tính cơ bản:

    • withView: view thực hiện animation
    • duration: thời gian thực hiện animation
    • options: nơi thêm các hiệu ứng thay đổi view
    • animations: closure chỉ ra animation đối tượng gì
    • completion: khi animation kết thúc, closure này sẽ được thực thi

    Các tùy chọn hiệu ứng options cho Transition

    • transitionFlipFromLeft/ transitionFlipFromRight
    UIView.transition(with: self.animationView, duration: 1, options: [.transitionFlipFromLeft, .repeat], animations: {
                self.animationImage.alpha = 1
            }, completion: { _ in
                self.animationImage.removeFromSuperview()
            })
    • transitionCurlUp/ transitionCurlDown
    UIView.transition(with: self.animationView, duration: 1, options: [.transitionCurlUp], animations: {
                self.animationImage.alpha = 0
                self.animationImage2.alpha = 1
            }, completion: { _ in
                self.animationImage.removeFromSuperview()
            })
    • transitionCrossDissolve
    UIView.transition(with: self.animationView, duration: 4, options: [.transitionCrossDissolve], animations: {
                self.animationImage.alpha = 1
            }, completion: { _ in
            })
    • transitionFlipFromTop/transitionFlipFromBottom
    UIView.transition(with: self.animationView, duration: 2, options: [.transitionFlipFromBottom], animations: {
                self.animationImage.alpha = 1
                self.animationImage2.removeFromSuperview()
    
            }, completion: { _ in
            })

    Transition rất đơn giản và cực kỳ hiệu quả, nó giúp app của chúng ta trở lên thân thiện, gần gũi hơn đối với người dùng. Tuy nhiên, nếu trong một màn hình có quá nhiều chuyển động sẽ dễ gấy rối mắt vì vậy hãy sử dụng các hiệu ứng animation một cách hiệu quả để đạt được kết quả tối ưu nhất.

    Cảm ơn mọi người đã theo dõi loạt bài về Basic Animation của mình!
    Hi vọng sẽ được mọi người ủng hộ ở những bài tiếp theo.

  • [iOS] – Ai cũng có thể làm Animation (Part 2)

    [iOS] – Ai cũng có thể làm Animation (Part 2)

    Như phần trước, mình đã đề cập đến những khái niệm cơ bản về animation trong iOS. Với phần này, mình sẽ đi sâu hơn về các thuộc tính và hiệu ứng của UIView để làm ra một animation đẹp mắt.

    Một số thuộc tính cơ bản của UIView.animation

    UIView.animate(duration: 1, usingSpringWithDamping: 0.2, initSpringVelocity: 0, delay: 0, options: [], animations: { 
    self.imageView.transform = CGAffineTransform(translationX: 0, y: 200)
    }, completion: nil)

    withDuration: giá trị TimeInterval (typealias cho Double) thời gian thực hiện animation tính bằng giây.

    delay: độ trễ tính bằng giây (TimeInterval cũng vậy) trước khi animation bắt đầu. Nếu bạn muốn bắt đầu hoạt hình ngay lập tức, bạn có thể bỏ qua tham số này (chỉ trong một số trường hợp nhất định) hoặc đặt thành 0.

    usingSpringWithDamping: độ nẩy của view khi gần đến điểm kết thúc( thuộc tính này giống với sự dao động của một vật thể khi được treo trên một cái lò xo trong thực tế).

    initSpringVelocity: Tốc độ di chuyển của view bắt đầu ở thời điểm xuất phát.

    Animation with Spring

    options: hiệu ứng hoạt hình của view trên quãng đường. Dưới đây là 1 số thuộc tính cơ bản của options:

    • .repeat: dùng để lặp lại animation vô hạn.
    • .autoreverse: là hiệu ứng view tới điểm kết thúc và trở lại điểm xuất phát.
    //Repeate
    UIView.animate(duration: 1, options: [.repeat], animations: { 
    self.imageView.transform = CGAffineTransform(translationX: 0, y: 200)
    }, completion: nil)
    //Autoreverse
    UIView.animate(duration: 1, options: [.autoreverse], animations: { 
    self.imageView.transform = CGAffineTransform(translationX: 0, y: 200)
    }, completion: nil)
    • .curveLinear: animation không có sự thay đổi về tốc độ từ khi bắt đầu tới điểm kết thúc.
    • .curveEaseIn: Tăng tốc khi bắt đầu animation
    • .curveEaseOut: Giảm tốc khi kết thúc animation
    • .curveEaseInOut: Kết hợp của .CurveEaseIn và .CurveEaseOut, tăng tốc khi bắt đầu animation và giảm tốc khi kết thúc animation.

    Keyframes giúp tạo các chuỗi animation riêng biệt

    Chúng ta có thể sử dụng các UIView.animation chồng lên nhau để tạo thành các chuỗi animation tuy nhiên sẽ gây lộn xộn các dòng code. Mọi việc sẽ trở lên dễ dàng hơn rất nhiều với keyframes hỗ trợ rất tốt xử lý các chuỗi animation kế tiếp nhau.

    Tương tự UIView.animation, câu lệnh của keyframe cũng gồm 1 số thuộc tính:

    UIView.animateKeyframesWithDuration(1.5, delay: 0.0, options: [], animations: {
    //add keyframes
    
    }, completion: nil)
    • duration : tổng thời gian diễn ra animation.
    • options : một danh sách các tuỳ chọn về cách thức animation.
    • animations : closure chỉ ra animation các key frames nào.
    • completion : khi animation kết thúc, closure này sẽ được thực thi.

    Tạo keyframe:

    UIView.addKeyframeWithRelativeStartTime(0.0, relativeDuration: 0.25, animations: {
        self.imageView.transform = CGAffineTransform(translationX: 0, y: 200)
    })
    • relativeStartTime : là thời điểm bắt đầu tính theo % trên tổng thời gian diễn ra animation. RelativeStartTime có giá trị 0 -> 1. Ví dụ: relativeStartTime = 0.2 -> animation sẽ bắt đầu sau 20% * tổng thời gian animation.
    • relativeDuration : là lượng thời gian tương đối tính theo % diễn ra animation cho keyframes với tổng thời gian diễn ra toàn bộ animation. Ví dụ: relativeDuration  = 0.25 tức là animation sẽ diễn ra trong 25% * tổng thời gian animation.
    • animations : closure chỉ ra animation đối tượng gì.

    Một ví dụ về ứng dụng của keyframe.

                UIView.animateKeyframes(withDuration: 2, delay: 0, options: [], animations: {
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25, animations: {
                    let translation = CGAffineTransform(translationX: 0, y: -200)
                    let rotation = CGAffineTransform(rotationAngle: CGFloat.pi)
                    self.imageView.transform = translation.concatenating(rotation)
                })
                UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25, animations: {
                    self.imageView.transform = CGAffineTransform(translationX: -150, y: 200)
                })
                UIView.addKeyframe(withRelativeStartTime: 0.6, relativeDuration: 0.3, animations: {
                    let translation = CGAffineTransform(translationX: 0, y: 0)
                    let rotation = CGAffineTransform(rotationAngle: -CGFloat.pi)
                    self.imageView.transform = translation.concatenating(rotation)
                })
            }, completion: nil)

    Phần tiếp theo, mình sẽ giới thiệu về UIView.transtion.
    Cảm ơn mọi người đã ghé đọc bài viết của mình 🙂

  • [iOS] – Ai cũng có thể làm Animation (Part 1)

    [iOS] – Ai cũng có thể làm Animation (Part 1)

    Xin chào mọi người,

    Hôm nay, mình sẽ chia sẻ đôi chút kiến thức của mình về tạo animation giúp app của chúng ta trở lên thú vị và thu hút hơn đối với người dùng.

    Những điều cơ bản để tạo được animation

    Về cơ bản, animation là tổ hợp các thay đổi về frame, bounds, center, transform, alpla, backgroundColor của đối tượng trên mặt phẳng và không gian chứa đối tượng ấy. Tất cả những animaton phức tạp đều được tạo thành từ những phần tử cơ bản nói trên vì vậy khi gặp 1 yêu cầu về animation phức tạp, bạn đừng vội lo lắng, thay vào đó, hãy phân tích animation ấy và đưa về các khái niệm cơ bản nhất và thực hiện từng bước các thay đổi điều kì diệu sẽ xuất hiện và bạn sẽ rất thích thú với những gì mình làm được. 🙂


    UIView.animation thường được sử dụng với những animation đơn giản:

    UIView.animate(withDuration: 0, animations: {})

    Thay đổi màu sắc và hình dạng của View

    self.animationView.backgroundColor = UIColor.purple
    self.animationView.alpha = 1
    self.animationView.layer.cornerRadius = 50

    Làm thế nào để thay đổi được kích thước và vị trí?

    Hãy cùng tìm hiểu 3 câu lệnh sau đây để thực hiện được điều trên,

    imageView.transform = CGAffineTransform(scaleX: 2, y: 2)
    imageView.transform = CGAffineTransform(translationX: 0, y: 100)
    imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)

    Ngoài ra, chúng ta còn có một số cách sau để thay đổi kích thước và vị trí của đối tượng,.

     imageView.frame.origin.x += 100
     imageView.frame.origin.y += 100
     imageView.center.x = 100
     imageView.frame.origin = CGPoint(x: 100, y: 100)  

    Hãy cùng nhau thử trộn các thay đổi trên vào xem kết quả chúng ta sẽ được gì nhé!

            UIView.animate(withDuration: 2, animations: {
                self.imageView.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
            })
            
            UIView.animate(withDuration: 2, animations: {
                self.imageView.transform = CGAffineTransform(translationX: 0, y: -200)
                let scaleImage = CGAffineTransform(scaleX: 4, y: 4)
                let rotateImage = CGAffineTransform(rotationAngle: CGFloat.pi * 2)
                self.imageView.transform = scaleImage.concatenating(rotateImage)
                
                self.imageView.alpha = 1
            })

    Chúng ta đã cùng nhau hoàn thành 1 trong những animation cơ bản nhất của một app.
    Hi vọng những kiến thức mình chia sẻ có ích với tất cả mọi người!

  • iOS/Swift – View Controller Lifecycle

    iOS/Swift – View Controller Lifecycle

    Lời mở đầu

    Nói về ViewController thì chắc hẳn tất cả iOS Developer đều biết đến và đã sử dụng rất nhiều. Nhưng đối với các bạn mới bắt đầu với iOS, mọi người thường không chú ý nhiều đến vòng đời của ViewController, dẫn đến mắc phải một số lỗi không đáng có.
    Bài viết này mình sẽ giới thiệu cho các bạn mới bắt đầu với iOS về vòng đời của View Controller và cách sử dụng để tránh những lỗi không đáng có.

    View Controller Lifecycle là gì?

    View Controller lifecycle là vòng đời của một view controller được tính từ lúc nó được nạp vào bộ nhớ (RAM) cho tới khi nó bị giải phóng khỏi bộ nhớ.

    Phân tích vòng đời của View Controller

    Dưới đây là sơ đồ về vòng đời của nó:

    Như các bạn đã thấy, trên sơ đồ này có khá nhiều trạng thái mà các bạn chắc dã nhìn thấy rất nhiều nhưng một số thì không phải không 😀
    Và nó cũng là các phương thức tương ứng được gọi tự động trong vòng đời của View Controller

    OK, bây giờ chúng ta sẽ đi vào chi tiết.

    loadView

    Phương thức này được gọi khi View hiện tại đang bằng nil. Cơ bản nó sẽ đưa View mà bạn tạo trong phương thức này vào view của ViewController.

    NOTE: Phương thức này được sử dụng khi View Controller được tạo bằng code. Nếu chúng ta tạo View Controller từ file .xib hoặc storyboard thì tốt nhất không sử dụng hương thức này.

    viewDidload:

    Phương thức này được gọi một lần duy nhất trong vòng đời của ViewController. Nó được gọi khi tất cả các view đã được load vào bộ nhớ(RAM).

    Ứng dụng:
    1. Khi bạn muốn cài đặt giao diện người dùng (User Interface)
    2. Những công việc mà bạn muốn nó chỉ chạy duy nhất một lần trên View Controller này.

    viewWillAppear:

    Phương thức này được gọi mỗi lần trước khi nội dung của View được thêm vào view hierarchy của ứng dụng.

    Ứng dụng:
    Vì phương thức này sẽ được gọi mỗi lần trước khi View được xuất hiện nên nó thường dùng khi bạn muốn 1 công việc nào đó luôn được gọi mỗi khi View Controller đó hiển thị trên màn hình.
    VD: Kiểm tra kết nối mạng, kiểm tra service state, add observer Notification v.v.

    NOTE:
    • Tránh làm các công việc mà bạn chỉ muốn thực hiện nó một lần trong vòng đời của View Controller trong phương thức này.
    • Nếu bạn add observer notification ở hàm này thì cần remove notification ở phương thức viewDidDisappear:. Để tránh trường hợp khi quay trở lại màn hình này hàm add observer notification sẽ được đăng kí một lần nữa -> nó sẽ thực thi hàm trong #selector nhiều lần.
    • Với tương tác tầng Application (VD: Bấm Home, show notification center, show control center… ) rồi trở lại ứng dụng thì sẽ không kích hoạt phương thức này mà nó sẽ kích hoạt các phương thức của UIApplication.

    viewDidAppear:

    Phương thức này được gọi mỗi lần sau khi nội dung của View được thêm vào view hierarchy của ứng dụng.

    Ứng dụng:
    Thường được sử dụng để lưu dữ liệu, bắt đầu animation, bắt đầu chơi Video hoặc âm thanh hoặc thu thập dữ liệu từ network.

    NOTE:
    • Tương tự như viewWillAppear, với tương tác tầng Application (VD: Bấm Home, show notification center, show control center… ) rồi trở lại ứng dụng thì sẽ không kích hoạt phương thức này mà nó sẽ kích hoạt các phương thức của UIApplication.
    • Nếu bạn add observer notification ở hàm này thì cũng phải xóa notification ở viewDidDisappear:

    viewWillDisappear:

    Phương thức này được gọi trước khi view được xóa khỏi view hierarchy. View vẫn còn trên view hierarchy nhưng chưa được xóa.

    Ứng dụng:
    Thường dùng để quản lí các timer, ẩn bàn phím, hủy các network request và lưu lại các trạng thái.

    viewDidDisappear:

    Phương thức này được gọi sau khi view của ViewController được xóa khỏi view hierarchy.

    Ứng dụng:
    Thường sử dụng để hủy việc lắng nghe các thông báo (Notification) hoặc các cảm biến của thiết bị các trên màn hình này.

    deinit:

    Phương thức này được gọi trước khi một view controller bị xóa khỏi bộ nhớ.

    Ứng dụng:
    Thường được sử dụng để xóa tài nguyên mà view controller đã được phân bổ nhưng không được giải phóng bới ARC(Automatic reference counting)

    NOTE:
    Hãy nhớ rằng một view controller không còn hiển thị trên màn hình nữa không có nghĩa là nó đã được giải phóng. Ngay cả khi màn hình bị tắt nếu nó vẫn còn trong bộ nhớ thì nó vẫn hoạt động như thường.

    didReceiveMemoryWarning:

    Phương thức này được gọi khi bộ nhớ (RAM) của máy gần đầy. Và iOS không tự động di chuyển dữ liệu từ bộ nhớ sang không gian ổ cứng hạn chế của nó.

    Ứng dụng:
    Xóa một số đối tượng ra khỏi bộ nhớ.

    NOTE:
    Hãy nhớ rằng nếu bộ nhớ của ứng dụng vượt quá một ngưỡng nhất định, iOS sẽ tắt ứng dụng của bạn. Và nó trông giống như là ứng dụng bị crash


    Cảm ơn mọi người đã theo dõi bài viết!
    Mọi ý kiến đóng góp mọi người hãy comment xuống phía dưới để mình có thể thay đổi cho bài biết được tốt hơn. :v

  • UICollectionView Custom Layout Tutorial

    UICollectionView Custom Layout Tutorial

    Xây dựng UICollection View Custom Layout và làm thế nào để lưu cache và dynamically size cell.

    UICollection view được giới thiệu từ bản iOS 6 và nó đã là UI Control mà các developer hay dùng nhất.

    UICollection view có thể hỗ trợ các loại hiển thị khác nhau và đặc biệt là tính năng dynamically size cell hỗ trợ người dùng hiển thị những phần mô tả ngắn mà đầy đủ nội dung cần truyền tải nhất mà không làm thay đổi thiết kế của mình. Như vậy thì UICollection view được sử dụng nhiều ở các app dạng social network, news,…,.

    Ở bài viết này tôi sẽ hướng dẫn các bạn

    • Custom layout
    • Xử lý dynamically cell
    • Những điểm cần lưu ý

    Tạo custom UICollection View Layout

    Bạn có thể tạo một chế độ hiển thị cho UICollection view theo cách riêng bằng cách tuỳ chỉnh layout của bạn.
    Collection view layout là lớp con của UICollectionViewLayout, nó xác đinh mọi thuộc tính trong chế độ hiển thị trên UICollectionView của bạn.
    Các thuộc tính của UICollectionViewLayout là intances của UICollectionViewLayoutAttributes. Chúng chứa các thuộc tính của từng mục trong chế độ hiển thị của bạn.

    Bắt đầu

    • Trước tiên ta cần chuẩn bị một project sample đã được implement UICollection View. UI cần custom là dạng cột và dynamic height.
    • Tạo custom collection view layout
    • Cách dùng Custom Collection View Layout

    Tạo Custom Collection View Layout

    Tạo file CustomCollectionViewLayout sub class là UICollectionViewLayout

    Tiếp theo config cho collection view sử dụng custom layout.
    Mở file view chứa collection view

    Mở thanh công cụ Attributes inspector. Chọn Custom trên thuộc tính Layout

    Như vậy chúng ta đã config thành công cho collection view sử dụng custom layout.

    Như vậy để hiển thị chúng cần thực hiện những gì

    Như vậy class CustomCollectionViewLayout vừa tạo cần thực hiện những function sau:

    • collectionViewContentSize: function này trả về chiều rộng chiều cao của toàn bộ collection view contents.
    • prepare: UIKit sẽ call function này trước khi có các thay đổi về UI. Đây là nơi mà bạn có thể chuẩn bị các tính toán liên quan đến chế độ xem, kích thước, vị trí ,…,.
    • layoutAttributesForElements(in:): Trong function này bạn trả về các thuộc tính bố cục cho tất cả item dưới dạng array UICollectionViewLayoutAttributes.
    • layoutAttributesForItem(at:): Function này cung cấp thông tin bố trí riêng cho từng item. Bạn cần override lại nó để có thể hiển thị cho từng item mà bạn mong muốn.

    Tính toán bố cục

    Với layout dạng cột và bạn cần height nó được dãn tự động thì bạn cần tính toán lại bố cục của layout

    Mở file CustomCollectionViewLayout và tạo protocol cho nó

    protocol CustomCollectionViewLayoutDelegate: AnyObject {
      func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat
    }

    Trong delegate bạn mới tạo 01 function yêu cầu trả về height, bạn sẽ implement function này trong Controller của chúng.

    Tiếp đến bạn cần thực hiện implement class CustomCollectionViewLayout

    // 1
        weak var delegate: CustomCollectionViewLayoutDelegate?
    
        // 2
        private let numberOfColumns = 2
        private let cellPadding: CGFloat = 6
    
        // 3
        private var cache: [UICollectionViewLayoutAttributes] = []
    
        // 4
        private var contentHeight: CGFloat = 0
    
        private var contentWidth: CGFloat {
          guard let collectionView = collectionView else {
            return 0
          }
          let insets = collectionView.contentInset
          return collectionView.bounds.width - (insets.left + insets.right)
        }
        // 5
        override var collectionViewContentSize: CGSize {
          return CGSize(width: contentWidth, height: contentHeight)
        }

    Bạn có thể thấy ở đây:

    1. delegate
    2. Số columns bạn cần hiển thị
    3. cache để lưu trữ các thuộc tính hiển thị.
    4. 02 Thuộc tính width và height của contents
    5. Trả về kích thước của collection view’s content. Bạn sử dụng cả 2 thuộc tính contentWidth và contentHeight để tính kích thước

    Để tính toán cách hiển thị, mời bạn xem sơ đồ sau:

    Bạn sẽ tính toán dựa trên số cột bạn muốn thiển thị và vị trí của mục trước đó trong cùng một cộ. Để tính toán thì bạn dùng xOffset cho cột và yOffset cho vị trí.

    Tiếp tục bạn cần override lại function prepare

    override func prepare() {
          // 1
          guard
            cache.isEmpty,
            let collectionView = collectionView
            else {
              return
          }
          // 2
          let columnWidth = contentWidth / CGFloat(numberOfColumns)
          var xOffset: [CGFloat] = []
          for column in 0..<numberOfColumns {
            xOffset.append(CGFloat(column) * columnWidth)
          }
          var column = 0
          var yOffset: [CGFloat] = .init(repeating: 0, count: numberOfColumns)
            
          // 3
          for item in 0..<collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: item, section: 0)
              
            // 4
            let photoHeight = delegate?.collectionView(
              collectionView,
              heightForPhotoAtIndexPath: indexPath) ?? 180
            let height = cellPadding * 2 + photoHeight
            let frame = CGRect(x: xOffset[column],
                               y: yOffset[column],
                               width: columnWidth,
                               height: height)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
              
            // 5
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
              
            // 6
            contentHeight = max(contentHeight, frame.maxY)
            yOffset[column] = yOffset[column] + height
              
            column = column < (numberOfColumns - 1) ? (column + 1) : 0
          }
        }

    Ở function này bạn có thể thấy tính width có thể cho số columns mà bạn muốn hiển thị, và tính height cho từng item và được lưu lại các thuộc tính ở cache.

    Tiếp theo bạn override lại function layoutAttributesForElements(in rect: CGRect) function này sẽ được gọi sau khi function prepare() kết thúc

        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
          var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
          
          // Loop through the cache and look for items in the rect
          for attributes in cache {
            if attributes.frame.intersects(rect) {
              visibleLayoutAttributes.append(attributes)
            }
          }
          return visibleLayoutAttributes
        }

    Ở function này, bạn duyệt lại bộ cache xem frame của từng item có giao nhau với collection view không.

    Cuối cùng bạn cần phải override lại function layoutAttributesForItem

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
          return cache[indexPath.item]
        }

    Ở đây sẽ return về các item theo các indexPath.

    Kết nối với ViewController

    Bạn mở View Controller chứa nó và thêm

    extension CustomCollectionViewController: CustomCollectionViewLayoutDelegate {
      func collectionView(
        _ collectionView: UICollectionView,
        heightForPhotoAtIndexPath indexPath:IndexPath) -> CGFloat {
        return photos[indexPath.item].image.size.height
      }
    }

    Ở đây tôi đang dùng dynamic height. Height sẽ tự động theo height của image.

    Tiếp theo trong function viewDidLoad() bạn thêm

    if let layout = collectionView.collectionViewLayout as? CustomCollectionViewLayout {
                layout.delegate = self
            }

    Như vậy bạn đã custom thành công layout trên UICollectionView.

    Những điều cần lưu ý

    Tới đây bạn sẽ gặp bug là bạn reload data cho collection view khi tăng số lượng item hiển thị.
    Bạn debug bạn sẽ thấy mọi quá trình bạn làm đã hoàn tất, từ việc get data và add vào list của data source cũng đã đầy đủ, nhưng chỉ thiếu một điều là collection view chỉ chạy lại với số lượng item lần đầu tiên.

    Vậy vấn đề ở đây là gì?? tại sao reloadData lại không được và không có bất cứ một dòng log error nào ra ??

    Vấn đề ở đây là contents size của collection view không được update, nên collection view không thể hiển thị thêm bất cứ item nào.

    Vậy bug ở function nào?

    Function nào tính toán lại contents size?

    Là func prepare() nó là function được gọi để tính toán size. Ngay ở bước đầu tiên của func prepare()

    // 1
          guard
            cache.isEmpty,
            let collectionView = collectionView
            else {
              return
          }

    Đoạn code trên nó chỉ tính toán lại contents size lần đầu tiên.

    Vậy bước 01 của func prepare() bạn cần làm:

          // 1
          guard
    //        cache.isEmpty,
            let collectionView = collectionView
            else {
              return
          }

    Như vậy bạn đã custom được layout của UICollectionViewLayout và hoàn toàn có thể update data source của UICollection view mà không bị bug.

    Source code tham khảo: https://github.com/TracDV/CustomUICollectionViewLayout

    Tham khảo nguồn: https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest

  • Swift: Map, Flat Map, Filter and Reduce

    Swift: Map, Flat Map, Filter and Reduce

    Xin chào mọi người.
    Trong swift có một số tính năng rất hay đó là Higher Order Function. Nó có một số hàm như là map, CompactMap, Filter and Reduce được sử dụng cho các kiểu dữ liệu dạng collection.

    Khởi tạo giá trị mẫu

    struct Person {
        let name: String
        let age: Int
        let pets: [String]
    }
    struct Pet {
        let name: String
        let age: Int
    }
    
    var peopleArray = [Person(name: "Jack", age: 11, pets: ["Dog", "Cat"]),
                       Person(name: "Queen", age: 12, pets: ["Pig"]),
                       Person(name: "King", age: 13, pets: [])]

    MAP

    Trước khi sử dụng chúng ta cùng tìm hiểu về syntax của hàm này trước nhé:

    let resultCollection = inputCollection.map { (elementOfCollection) -> ResultType in
       return ResultType()
    }

    Nhìn vào đoạn code trên ta có thể hiểu hàm này sẽ trả về cho ta một collection có kiểu dữ liệu là ResultType( Một kiểu dữ liệu bất kì mà bạn mong muốn, nó có thể là Int, Double, String …)

    OK, Giờ chúng ta đi vào code mẫu để dễ hiểu hơn

    Dạng đầy đủ:

    var ages = peopleArray.map { (person) -> Int in
        return person.age
    }
    print(ages)
    // OUTPUT: [11, 12, 13]

    Ngoài dạng thông thường thì hàm này còn có thể viết dưới dạng rút gọn như sau:

    let ages = peopleArray.map({ $0.age })
    print(ages)
    // OUTPUT: [11, 12, 13]

    $0: ở đây được hiểu là argument đầu tiên của function map, trong trường hợp này nó sẽ đại diện cho 1 phần tử trong mảng có kiểu dữ liệu là Person

    Ứng dụng:
    Hàm này nên được sử dụng khi mà bạn muốn tạo một collection mới có kiểu dữ liệu khác từ 1 collection hiện tại

    Flat Map

    let resultCollection = inputCollection.flatMap { (elementOfCollection) -> [ResultType] in
       return [ResultType]
    }

    Cũng giống như hàm map, hàm flatMap cũng trả về 1 collection nhưng flat map sẽ bỏ qua các tầng (nếu có).
    Để dễ hiểu hơn chúng ta sẽ đi vào ví dụ sau:

    Sử dụng hàm Map:

    let pets = peopleArray.map({ $0.pets })
    print(pets)
    //OUTPUT: [["Dog", "Cat"], ["Pig"], []]

    Dạng đầy đủ:

    let flatPets = peopleArray.flatMap { (person) -> [String] in
        return person.pets
    }
    print("Flat pets: \(flatPets)")
    // OUTPUT: ["Dog", "Cat", "Pig"]

    Dạng rút gọn:

    let flatPets = peopleArray.flatMap({ $0.pets })
    print("flatPets: \(flatPets)")
    // OUTPUT: flatPets: ["Dog", "Cat", "Pig"]

    Có thể thấy flat map sẽ loaị bỏ hết các tầng collection bên trong và chuyển về 1 collection chỉ còn 1 tầng duy nhất thay vì collection chứa collection như khi sử dụng hàm Map.
    Ngoài ra hàm flatMap cũng bỏ đi các giá trị collection empty hoặc nil.

    NOTE: Hàm flatMap chỉ trả về giá trị đúng như mong đợi khi kiểu của nó là non-optional.


    Vậy trong trường hợp khai báo kiểu optional thì sao: let pets: [String]?
    Trong trường hợp này hàm Flat Map sẽ trả về giá trị như hàm Map vì vậy chúng ta sẽ call flatMap thêm 1 lần nữa như sau:

    let flatPetsShort = peopleArray.flatMap({ $0.pets }).flatMap({ $0 })
    print("flatPetsShort: \(flatPetsShort)")
    //OUTPUT: flatPetsShort: ["Dog", "Cat", "Pig"]

    Ứng dụng:
    Flat Map thường sử dụng trong trường hợp lọc các giá trị nil ra khỏi collection hoặc chuyển 1 collection nhiều tầng thành collection 1 tầng

    Reduce

    Cấu trúc hàm Reduce

    let result = inputCollection.reduce(initialValue) { (result, nextElement) -> ResultType in
        return a value that can be computed in the next element.
    }

    Hàm reduce sẽ duyệt lần lượt các phần tử trong collection và trả về kết quả dựa trên initialValue(Giá trị khởi tạo) và phép tính ở hàm return.

    Bây giờ chúng ta đi vào ví dụ để dễ hiểu hơn:

    Bài toán cụ thể: Cần tính tổng số tuổi của mọi người trong collection bằng hàm Reduce thì chúng ta sẽ làm như sau:

    Dạng đầy đủ:

    let ageTotal = peopleArray.reduce(0) { (result, personNext) -> Int in
        return result + personNext.age
    }
    print(ageTotal)
    // OUTPUT: 36

    Dạng rút gọn:

    let ageTotal2 = peopleArray.map({ $0.age }).reduce(0, +)
    print(ageTotal2)
    // OUTPUT: 36

    Ở dạng rút gọn, để sử dụng được hàm Reduce trong trường hợp này. Chúng ta cần sử dụng hàm Map để tạo ra một collection mới, chứa các phần tử là tuổi của tất cả mọi người trước, rồi sử dụng hàm Reduce dạng rút gọn để thực hiện.

    Ứng dụng:
    Bạn nên sử dụng hàm Reduce khi bạn muốn kết hợp các phần tử trong collection

    Filter

    Cấu trúc hàm Filter

    let result = inputCollection.filter { (elementOfCollection) -> Bool in 
        return (Conditions)
    }

    Hàm Filter sẽ trả về kết quả là 1 collection chứa tất cả các phần tử thỏa mãn điều kiện (Conditions)

    Bây giờ chúng ta sẽ đi vào ví dụ cho dễ hiểu hơn:
    Bài toán của chúng ta là cần lọc ra được tất cả các Person có số tuổi > 11.

    Chúng ta sẽ sử dụng hàm Filter dạng đầy đủ như sau:

    let result = peopleArray.filter { (person) -> Bool in
        return person.age > 11
    }
    print("result: \(result)")

    Dạng rút gọn:

    let results = peopleArray.filter({ $0.age > 11 })
    print(results)

    Ứng dụng:
    Sử dụng khi bạn cần giải quyết bài toán lọc các phần tử trong collection có điều kiện xác định

    NOTE:

    print("Cảm ơn mọi người đã theo dõi bài viết của mình.\n
    Mọi đóng góp cũng như góp ý, mọi người hãy comment ở phía dưới để mình có thể hoàn thiện bài viết tốt hơn.") 

    THANK YOU!

  • Grand Central Dispatch (Part 3)

    Grand Central Dispatch (Part 3)

    Nội dung bài viết:

    • DispatchBarrier
    • DispatchWorkItem
    • Thread Sanitizer

    Dispatch Barrier

    • 1 trong những vấn đề tiêu biểu của đa luồng như đã đề cập ở phần 1 của bài viết là Race Condition.
    • Có 1 cách đơn giản để tránh Race Condition, đó là dùng serial queue để thực hiện các task, khi đó chỉ được 1 task được thực hiện tại mỗi thời điểm, nhưng sẽ làm app chậm lại vì chạy trên serial queue.
    • Vậy nếu bạn muốn các task được thực hiện concurrent, nhưng khi có 1 task thay đổi tài nguyên chung thì chỉ duy nhất task đó được thực hiện tại thời điểm đó?

    Đừng lo, GCD cung cấp cho bạn DispatchBarrier để xử lí điều đó 1 cách đơn giản.

    Note: Khi 1 task tiến hành việc thay đổi share resources, sẽ có 1 barrier đến, ngăn không cho bất kì 1 task mới nào được thực hiện trong thời gian này. Khi task đó hoàn thành việc thay đổi tài nguyên, barrier mất đi, các tasks tiếp tục chạy concurrent.

    Ví dụ sau đây là cách sử dụng Dispatch Barrier để tránh Race Condition.
    Giả sử bạn có 1 class Person như sau:

    Dưới đây là đoạn code nhiều task khác nhau cùng thay đổi share resources.

    let queue = DispatchQueue(label: "01", attributes: .concurrent)
    let nameChangeGroup = DispatchGroup()
    
    let person = Person(firstName: "Hoang", lastName: "Tuan")
    let nameList = [("A","B"),("C","D"),("E","F"),("G","H"),("I","J")]
    
    for (_, name) in nameList.enumerated() {
        queue.async(group: nameChangeGroup) {
            usleep(UInt32(10000 * idx))
            person.changeName(firstName: name.0, lastName: name.1)
            print("Current name: \(person.name)")
        }
    }
    
    nameChangeGroup.notify(queue: .main) {
        print("Final name: \(person.name)")
    }

    Và đây là kết quả được hiển thị:

    Có thể dễ dàng thấy, các task thực hiện thay đổi tài nguyên cùng thời điểm gây ra sai lệch kết quả. Đồng thời, kết quả đều khác nhau sau mỗi lần Run.

    Đó là lúc chúng ta sẽ sử dụng Dispatch Barrier để xử lí việc này.

    1. Thực hiện việc get name theo sync để không cho các task khác có thể đọc tài nguyên khi có 1 task đang đọc, vì có thể task đang đọc có thể sẽ chỉnh sửa tài nguyên sau đó.
    2. Đưa toàn bộ thân hàm của func changeName vào thực hiện trên luồng isolationQueue1 với flags là 1 barrier. Vì vậy, khi excute changeName, 1 barrier sẽ xuất hiện và không để bất kì task mới nào được thực hiện cho đến khi changeName hoàn thành.

    Và đây là kết quả:

    Kết quả khi đó được thực hiện đúng

    DispatchWorkItem

    DispatchWorkItem là những block of code. Điều khác biệt khi sử dụng DispatchWorkItem là bạn có thể cancel những task đang trong queue.

    Note: Bạn chỉ có thể cancel 1 DispatchWorkItem trước khi nó đi đến đầu queue và bắt đầu execute.

    Khởi tạo 1 DispatchWorkItem:

    let workItem = DispatchWorkItem(qos: .background, flags: .inheritQoS) {
         guard let url = URL(string: self.links[i]) else {
             return
         }
         URLSession.shared.dataTask(with: url) { (data, res, err) in
             guard let imageData = data else {
                 return
             }
             let myImage = UIImage(data: imageData)
             DispatchQueue.main.async {
                 self.listImage[i].image = myImage
             }
         }.resume()
    }

    Đưa DispatchWorkItem vào queue để thực hiện:

    DispatchQueue.global().async(execute: workItem)

    Cancel 1 DispatchWorkItem:

    workItem.cancel()

    Thread Sanitizer

    Sử dụng Thread Sanitizer để phát hiện & debug race condition:

    • Chọn Product>Scheme>Edit Scheme
    • Chọn vào "Thread Sanitizer":

    Khi đó, nếu code của bạn bị race condition, xcode sẽ thông báo cho bạn:

    1 tool khá hay để phát hiện race condition

    Kết luận: GCD là 1 API đỉnh và dễ sử dụng để quản lí multitask, đa luồng, nhưng có 1 nhược điểm là không cung cấp các state của task trong trường hợp bạn muốn quản lí sâu hơn. Khi đó, bạn phải tự custom State cho riêng mình, hoặc dùng Operation.

    -End-