Blog

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

  • Custom Lint Rules

    Custom Lint Rules

    Bài viết này tôi sẽ giới thiệu về phương pháp mở rộng lint và tạo custom rule.

    Lint và cách custom rule của lint như thế nào?

    Lint là một bộ phân tích tĩnh, có mục tiêu tìm lỗi trên mã nguồn của bạn mà không cần phải biên dịch hoặc chạy nó.
    Để tạo custom rule của lint thì cần phải tạo Detector, Issue, Registry.

    Creating your rules module

    Chúng ta bắt đầu bằng cách định nghĩa một module Java / Kotlin riêng biệt. Sau đó chúng ta sẽ thêm vào build.gradle như sau:

    apply plugin: 'java-library'
    apply plugin: 'kotlin'
    
    dependencies {
        compileOnly "com.android.tools.lint:lint-api:26.5.3"
        compileOnly "com.android.tools.lint:lint-checks:26.5.3"
    }
    
    jar {
        manifest {
            attributes("Lint-Registry-v2": "techover.rules.IssueRegistry")
        }
    }

    Creating Detector

    Detector là một lớp có thể tìm thấy một hay nhiều vấn đề cụ thể hơn. Tùy thuộc vào vấn đề, chúng ta có thể sử dụng các loại phát hiện khác nhau:
    SourceCodeScanner – một trình phát hiện chuyên về các tệp nguồn Java / Kotlin.
    XmlScanner – một trình phát hiện chuyên về các tệp XML.
    GradleScanner – một trình phát hiện chuyên về các tệp Gradle.
    ResourceFolderScanner – một trình phát hiện chuyên về các thư mục tài nguyên (không phải các tệp mà nó chứa).
    Bạn có thể tham khảo tạo Detector dưới đây:

    package techover.rules
    
    import com.android.tools.lint.detector.api.*
    import com.intellij.psi.PsiMethod
    import org.jetbrains.uast.UCallExpression
    
    /**
     * This detector will report any usages of the android.util.Log.
     */
    class AndroidLogDetector : Detector(), SourceCodeScanner {
    
        override fun getApplicableMethodNames(): List<String> =
            listOf("tag", "format", "v", "d", "i", "w", "e", "wtf")
    
        override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
            super.visitMethodCall(context, node, method)
            val evaluator = context.evaluator
            if (evaluator.isMemberInClass(method, "android.util.Log")) {
                reportUsage(context, node)
            }
        }
    
        private fun reportUsage(context: JavaContext, node: UCallExpression) {
            context.report(
                issue = ISSUE,
                scope = node,
                location = context.getCallLocation(
                    call = node,
                    includeReceiver = true,
                    includeArguments = true
                ),
                message = "android.util.Log usage is forbidden."
            )
        }
    
        companion object {
            private val IMPLEMENTATION = Implementation(
                AndroidLogDetector::class.java,
                Scope.JAVA_FILE_SCOPE
            )
    
            val ISSUE: Issue = Issue
                .create(
                    id = "AndroidLogDetector",
                    briefDescription = "The android Log should not be used",
                    explanation = """
                    For amazing showcasing purposes we should not use the Android Log. We should the
                    AmazingLog instead.
                """.trimIndent(),
                    category = Category.CORRECTNESS,
                    priority = 9,
                    severity = Severity.ERROR,
                    androidSpecific = true,
                    implementation = IMPLEMENTATION
                )
        }
    }
    • Nó extends Detector để Android Lint có thể sử dụng để phát hiện sự cố.
    • Nó extends SourceCodeScanner vì chúng ta cần kiểm tra cả hai tệp Kotlin và Java.
    • getApplossibleMethodNames – chỉ lọc các chữ ký phương thức tồn tại trong android.util.Log
    • visitMethodCall – sử dụng trình đánh giá để đảm bảo rằng phương thức này được gọi bởi android.util.Log chứ không phải bởi bất kỳ lớp nào khác. Ví dụ: AmazingLog có cùng phương thức và không nên gắn cờ.
    • reportUsage – được sử dụng để báo cáo sự cố khi tìm thấy.

    Creating Issue

    Issue là một lỗi tiềm ẩn trong ứng dụng Android. Đây là cách bạn khai báo lỗi mà quy tắc của bạn sẽ giải quyết.
    id – để xác định duy nhất vấn đề này.
    briefDescription– mô tả tóm tắt về vấn đề.
    explanation – nên là một mô tả sâu hơn về vấn đề và lý tưởng về cách giải quyết vấn đề.
    category – xác định loại vấn đề. Có rất nhiều danh mục có thể có như CORRECTNESS, USABILITY, I18N, COMPLIANCE, PERFORMANCE, …
    priority – một số từ 1 đến 10, trong đó số càng lớn thì vấn đề càng nghiêm trọng.
    severity – nó có thể là một trong những giá trị sau: FATAL, ERROR, WARNING, INFORMATIONAL and IGNORE. Lưu ý: Nếu mức độ nghiêm trọng là FATAL hoặc ERROR thì việc chạy lint sẽ thất bại và bạn sẽ phải giải quyết vấn đề.
    implementation – lớp chịu trách nhiệm phân tích tệp và phát hiện vấn đề.
    Bạn có thể tham khảo tạo Issue dưới đây:

    val ISSUE: Issue = Issue
                .create(
                    id = "AndroidLogDetector",
                    briefDescription = "The android Log should not be used",
                    explanation = """
                    For amazing showcasing purposes we should not use the Android Log. We should the
                    AmazingLog instead.
                """.trimIndent(),
                    category = Category.CORRECTNESS,
                    priority = 9,
                    severity = Severity.ERROR,
                    androidSpecific = true,
                    implementation = IMPLEMENTATION
                )

    Creating Registry

    Bạn có thể tham khảo Registry dưới đây:

    package techover.rules
    
    import com.android.tools.lint.client.api.IssueRegistry
    import com.android.tools.lint.detector.api.CURRENT_API
    import com.android.tools.lint.detector.api.Issue
    
    class IssueRegistry : IssueRegistry() {
    
        override val api: Int = CURRENT_API
    
        override val issues: List<Issue>
            get() = listOf(AndroidLogDetector.ISSUE)
    }

    Run Lint

    Chúng ta sẽ thêm vào app/build.gradle như sau:

    dependencies {
        lintChecks project(path: ':rules')
    }

    Sau đó chúng ta sẽ chạy command dưới đây:

    ./gradlew app:lintDebug

    Nếu source không có Issue mà chúng ta đã Registry thì sẽ in ra nội dung như sau:

    > Task :app:lintDebug
    Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
    Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
    
    BUILD SUCCESSFUL 

    Ví dụ, trong class MainActivity bạn có log như sau:

    Log.d("MainActivity", "https://magz.techover.io/")

    Thì chúng ta thấy source có Issue mà chúng ta đã Registry nê sẽ in ra nội dung như sau:

    > Task :app:lintDebug FAILED
    Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
    Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
    
    FAILURE: Build failed with an exception.
    
    * What went wrong:
    Execution failed for task ':app:lintDebug'.
    > Lint found errors in the project; aborting build.
    
      Fix the issues identified by lint, or add the following to your build script to proceed with errors:
      ...
      android {
          lintOptions {
              abortOnError false
          }
      }
      ...
    
      Errors found:
    
      ~/Techover/Lint/app/src/main/java/techover/lint/MainActivity.kt:13: Error: android.util.Log usage is forbidden. [AndroidLogDetector]
              Log.d("MainActivity", "https://magz.techover.io/")
              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    
    
    * Try:
    Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
    
    * Get more help at https://help.gradle.org
    
    BUILD FAILED 

    Link tham khảo:

    • https://github.com/fabiocarballo/lint-sample
  • 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

  • Detekt

    Detekt

    Xin chào các bạn, lại là tôi đây, bài viết lần này tôi sẽ chỉ cho các bạn cách cải thiện mã nguồn của bạn với Detekt.

    Detekt là gì?
    Detekt là một công cụ để phân tích code cho lập trình kotlin. Nó hoạt động dựa trên các cú phúp trừu tượng được cung cấp bởi trình biên dịch kotlin.

    Detekt có những đặc trưng nào?

    • Phân tích code smell cho các dự án kotlin của bạn.
    • Báo cáo độ phức tạp dựa trên các dòng code. Độ phức tạp của McCabe và số lượng code smells.
    • Cấu hình cao (rule set or rule level)
    • Loại bỏ các phát hiện với chú thích của kotlin là @Suppress và java là @SuppressWarnings
    • Xác định ngưỡng code smell sẽ phá vỡ bản build của bạn và in ra cảnh báo.
    • Code smell baseline và bỏ qua danh sách legacy của dự án.
    • Gradle Plugin để phân tích code qua Gradle Build.
    • Grade task sử dụng IntelliJ để định dạng và kiểm tra mã kotlin.
    • Tuỳ chọn cấu hình của detekt cho mỗi module bằng cách sử dụng profiles (gradle-plugin).
    • Tích hợp SonarQube.
    • Có thể mở rộng bằng quy tắc riêng và FileProcessListener’s.
    • Tích hợp IntelliJ.

    Sử dụng detekt như nào?
    Trước tiên, bạn hãy cấu hình trong gradle build file.

    configurations {
        detekt
    }
    
    task detekt(type: JavaExec) {
        main = "io.gitlab.arturbosch.detekt.cli.Main"
        classpath = configurations.detekt
        def input = "$projectDir/src/"
        def config = "$rootDir/detekt/detekt-config.yml"
        def filters = ".*/techover.detekt/.*"
        def output = "$rootDir/detekt/reports"
        def params = ['-i', input, '-c', config, '-f', filters, '-o', output]
        args(params)
    }
    
    dependencies {
        detekt "io.gitlab.arturbosch.detekt:detekt-cli:$detekt_version"
    }

    Chúng ta cùng đi tìm hiểu trong task detekt bên trên có những gì?
    def input = "$projectDir/src/" là phần nào trong dự án mà bạn muốn được phân tích.
    def config = "$rootDir/detekt/detekt-config.yml" là phần bạn cài đặt sẽ sử dụng những quy tắc nào để phân tích.
    def filters = ".*/techover.detekt/.*" là phần loại bỏ không cần phân tích, với nhiều đường dẫn khác nhau thì bạn sử dụng dấu phẩy để phân tách.
    def output = "$rootDir/detekt/reports" là phần mà khi chạy phân tích sẽ đưa ra tài liệu báo cáo.

    Bạn có thể sử dụng detekt-config.yml tham khảo ở đây.

    Vậy là bạn đã có thể chạy detekt cho dự án của bạn rồi, bằng cách chạy câu lệnh:

    ./gradlew detekt

    Sau khi chạy câu lệnh gradlew bên trên thì sẽ xuất hiện output như dưới đây:

    Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details
    
    > Task :app:detekt
    
    Successfully generated XmlOutputReport.
    Successfully generated PlainOutputReport.
    
    detekt run within 1019 ms
    
    BUILD SUCCESSFUL in 14s
    1 actionable task: 1 executed

    Như vậy là source code của dự án bạn đang không có lỗi nào.
    Nếu như bạn chạy mà source code có lỗi thì sẽ hiển thị như sau:

    > Task :app:detekt
    Ruleset: code-smell
    Ruleset: comments
    Ruleset: complexity
    Ruleset: empty-blocks
    Ruleset: exceptions
    Ruleset: performance
    Ruleset: potential-bugs
    Ruleset: style
            WildcardImport - [ExampleInstrumentedTest.kt] at androidTest/java/techover/detekt/ExampleInstrumentedTest.kt:9:1
            WildcardImport - [ExampleUnitTest.kt] at test/java/techover/detekt/ExampleUnitTest.kt:5:1
            FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
            FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
            MagicNumber - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:15:22
    
    Successfully generated XmlOutputReport.
    Successfully generated PlainOutputReport.
    
    detekt run within 1203 ms
    
    BUILD SUCCESSFUL in 3s
    1 actionable task: 1 executed

    Để bạn có thể hiểu hơn về tập tin cấu hình mà bạn tham khảo ở link bên trên detekt-config.yml thì dưới đây tôi sẽ nói qua về phần này cho bạn hiểu hơn.

    Tập tin cấu hình detekt có những gì?
    Detekt sử dụng tệp cấu hình kiểu yaml cho nhiều thứ khác nhau:

    • Bộ quy tắc và thuộc tính quy tắc.
    • Build thất bại.
    • Bộ xử lý tệp kotlin.
    • Console và định dạng đầu ra.

    Bộ quy tắc và quy tắc:
    Detekt cho phép dễ dàng chỉ cần chọn các quy tắc bạn muốn và cấu hình chúng theo cách bạn muốn. Ví dụ, nếu bạn muốn cho phép tối đa 20 chức năng trong tệp Kotlin thay vì ngưỡng mặc định là 10.

    complexity:
      TooManyFunctions:
        threshold: 20

    Bộ lọc đường dẫn / Không bao gồm / Bao gồm:
    Bắt đầu với phiên bản RC15 bộ lọc đường dẫn có thể được xác định cho từng quy tắc hoặc bộ quy tắc:

    complexity:
      TooManyFunctions:
        ...
        excludes: "**/internal/**"
        includes: "**/internal/util/NeedsToBeChecked.kt"

    Bảng điều khiển và báo cáo đầu ra:

    console-reports:
      active: true
      exclude:
      #  - 'ProjectStatisticsReport'
      #  - 'ComplexityReport'
      #  - 'NotificationReport'
      #  - 'FindingsReport'
      #  - 'FileBasedFindingsReport'
      #  - 'BuildFailureReport'
    
    output-reports:
      active: true
      exclude:
      #  - 'HtmlOutputReport'
      #  - 'TxtOutputReport'
      #  - 'XmlOutputReport'

    Bộ vi xử lý (Bộ xử lý thường được sử dụng để nâng cao số liệu dự án):

    processors:
      active: true
      exclude:
      # - 'FunctionCountProcessor'
      # - 'PropertyCountProcessor'
      # - 'ClassCountProcessor'
      # - 'PackageCountProcessor'
      # - 'KtFileCountProcessor'

    Link tham khảo:

    • https://arturbosch.github.io/detekt/index.html
    • https://github.com/arturbosch/detekt

  • 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