Category: Swift

  • TaskGroup Swift

    TaskGroup Swift

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

       

    1-Bản chất taskgroup

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

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

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

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

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

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

       

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

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

       

    3-Khởi tạo taskGroup

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

       

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

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

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

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

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

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

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

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

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

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

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

       

    5-Tổng kết

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


    REFERENCE

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

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

  • IOS/Swift: Cách sử dụng Lazy Sequences để tối ưu hoá performance app ios

    IOS/Swift: Cách sử dụng Lazy Sequences để tối ưu hoá performance app ios

    Article overview

    Nói về Lazy property chúng ta đã quá quen thuộc với lazy var, nhưng bạn đã bao giờ sử dụng lazy property vào sequences ? Bài viết này sẽ giúp các bạn cách sử dụng lazy vào các phép xử lý map, filter, reduce một cách hiệu quả và tối ưu hoá performance app ios.

    Table of contents

    Lazy Sequences là gì ?

    Chúng ta cùng xem ví dụ sau, cho một collection mảng số nguyên từ 1 tới 1000, lọc mảng số nguyên chỉ lấy những giá trị là số chẵn, sau đó nhân đôi từng giá trị lên, rồi trả về kết quả giá trị đầu tiên value lớn hơn 10. Nếu không sử dụng lazy chúng ta có kết quả như sau:

    Image of article

    Nếu sử dụng lazy chúng ta sẽ có kết quả như sau:

    Image of article

    Chúng ta có thể thấy kết quả 2 bên đều = 12, nhưng có sự khác biệt lớn ở trường hợp không sử dụng lazy, phép xử lý filter chạy duyệt hết 1000 phần tử rồi tới phép xử lý map chạy tiếp và tương tự chạy 500 phần tử rồi mới tới phép xử lý tiếp theo. Nhưng nếu sử dụng lazy chúng ta thấy filter chỉ cần chạy 6 lần, map chỉ cần chạy 3 lần.

    Ở trường hợp không sử dụng lazy chúng ta thấy phép xử lý phải chạy hết 1000 phần tử nhưng với điều kiện bài toán của chúng ta thì không cần thiết phải duyệt hết 1000 phần tử, lazy sẽ giúp chúng ta chạy song song các phép xử lý tức là khi filter chạy duyệt 1 phần tử chúng ta có được 1 kết quả của phép filter sau đó kết quả này sẽ dùng chạy tiếp cho phép xử lý map rồi đến first và cứ lặp lại như vậy khi có được kết quả thoả mãn điều kiện bài toán sẽ dừng lại không chạy tiếp. Ở ví dụ này thì chúng ta thấy con số tương đối nhỏ nhưng hãy tưởng tượng nếu chúng ta có một bài toán tương tự nhưng phạm vi mảng tới hơn 1 triệu thì lazy sẽ giúp chúng ta tối ưu rất nhiều số lần duyệt mảng từ đó hiệu suất ứng dụng app sẽ tốt hơn.

    Nếu chúng ta có một collection ít phần tử thì việc sử dụng lazy cũng không mang lại tối ưu nhiều chính vì vậy chúng ta tránh lạm dụng lazy, chỉ nên sử dụng nếu chúng ta có một collection với nhiều phần tử.

    Lazy collections không được cache lại, với lưu ý này chúng ta cùng xem ví dụ sau:

    let modifiedLazyNumbers = (1...4)
        .filter { number in
            print("Even number filter")
            return number % 2 == 0
        }.map { number -> Int in
            print("Doubling the number")
            return number * 2
        }
        
    print("Ket qua ne print lan 1: \(modifiedLazyNumbers.first!)")
    print("Ket qua ne print lan 2: \(modifiedLazyNumbers.first!)")
    
    // Even number filter
    // Even number filter
    // Even number filter
    // Even number filter
    // Doubling the number
    // Doubling the number
    // Ket qua ne print lan 1: 4
    // Ket qua ne print lan 2: 4
    
    let modifiedLazyNumbers = (1...4)
        .lazy
        .filter { number in
            print("Even number filter")
            return number % 2 == 0
        }.map { number -> Int in
            print("Doubling the number")
            return number * 2
        }
        
    print("Ket qua ne print lan 1: \(modifiedLazyNumbers.first!)")
    print("Ket qua ne print lan 2: \(modifiedLazyNumbers.first!)")
    
    // Even number filter
    // Even number filter
    // Doubling the number
    // Ket qua ne print lan 1: 4
    // Even number filter
    // Even number filter
    // Doubling the number
    // Ket qua ne print lan 2: 4
    

    Các bạn có thể thấy sự khác nhau chính là vì lazy collection chỉ chạy các phép xử lý khi có request tới, chính vì vậy giá trị kết quả cuối cùng sẽ không được lưu ở output array, mỗi khi có request tới phép xử lý phải chạy lại.

    • Kết luận Chúng ta có thể thấy lazy collection mang lại hiệu quả tốt với performance nhưng chỉ nên áp dụng nếu chúng ta xử lý bài toán với phạm vi mảng lớn.

    Authors

    [email protected]

  • Triển khai CI/CD cho iOS – SonarQube & Blackduck

    Overview

    Tiếp tục với series CI/CD cho iOS, hôm nay chúng ta sẽ triển khai CI với hai nền tảng kiểm tra source code rất nổi tiếng là SonarQube và Blackduck.

    Triển khai CI với Blackduck

    Khác với SonarQube, Blackduck không đánh giá chất lượng mà giúp chúng ta quản trị open source code và source code từ các thư viện được thêm vào. Giúp chúng ta đánh giá và quản lý được các rủi ro về bản quyền, bảo mật khi sử dụng source code có sẵn trên mạng cũng như lưu hành trong cộng đồng.

    Do Blackduck không có Server public như Sonar, nên mình sẽ giả định chúng ta có một Server Blackduck được đặt tại địa chỉ sau:

    https://blackduck.techover.io

    Sau khi truy cập vào, chúng ta sẽ thấy danh sách các project đã có ở Dashboard. Ở đây mình đã tạo sẵn một Project W95.CICD, nếu muốn tạo mới chúng ta sẽ ấn nút Create Project ở góc trên bên phải.

    Dashboard của Blackduck

    Cũng giống như Sonar, để có thể đồng bộ các dữ liệu Scan chúng ta cần có một Token. Để tạo Blackduck Token, chúng ta sẽ vào màn hình quản lý Access Tokens như sau:

    Ấn vào Profile ở góc trên bên phải, chọn My Access Tokens

    Sau khi chuyển đến màn hình quản lý Access Token, chúng ta ấn Create New Access Token và điền thông tin.

    Màn hình quản lý Access Tokens
    Nhập thông tin và ấn Create

    Sau khi có token, chúng ta sẽ lưu lại để sử dụng sau này. Lưu ý, token sẽ không hiển thị lại lần thứ 2 nên hãy copy và lưu lại ngay và luôn nhé. Ví dụ mình sẽ lưu lại token lại dưới đây:

    YWJmYzc5MDMS05N2VjLWFkNGE4ZMS05N2VjLWFkNGE4Z--=/

    Đối với Blackduck, chúng ta cũng sử dụng gần giống như Sonar nhưng thay vì CLI, chúng ta sẽ sử dụng Java Archive, vì vậy hãy đảm bảo bạn đã cài Java trong thiết bị Runner nhé.

    Tải về phiên bản .jar mới nhất của blackduck ở đây

    Sau khi tải xong, chúng ta sẽ giải nén và lưu và một thư mục trong thiết bị Runner, ở đây mình sẽ lưu ở địa chỉ sau:

    /Users/jena/CICD/synopsys-detect/synopsys-detect-6.4.1.jar

    Rồi xong, tới công chuyện luôn!!

    Tiếp đó chúng ta chỉ cần cấu hình file .gitlab-ci.yml như sau:

    stages:
      - Lint
    blackduck-detect:
      stage: Lint
      only:
          - cicd
      script:
        - java -jar /Users/jena/CICD/synopsys-detect/synopsys-detect-6.4.1.jar \
          --blackduck.url=https://blackduck.techover.io \
          --blackduck.api.token=YWJmYzc5MDMS05N2VjLWFkNGE4ZMS05N2VjLWFkNGE4Z--=/ \
          --detect.project.name=W95.CICD
      tags:
        - w95

    Vậy là xong, mỗi khi có commit/merge lên nhánh cicd (hãy thay bằng master/main/develop) thì hệ thống sẽ tự động scan source code và gửi kết quả lên Server.

    Quên mất, còn một bước cuối cùng nữa là bạn có thể che đi các thông tin nhạy cảm trong file cấu hình .gitlab-ci.yml, đề phòng trong trường hợp file cấu hình bị rò rỉ, các thông tin về Server Blackduck cũng như Token cũng sẽ không bị ảnh hưởng. Để làm việc này chúng ta sẽ cấu hình một số thông tin sau vào biến môi trường của Gitlab-CI

    BLACKDUCK_SERVER = https://blackduck.techover.io
    BLACKDUCK_TOKEN = YWJmYzc5MDMS05N2VjLWFkNGE4ZMS05N2VjLWFkNGE4Z--=/

    Và kết quả, file .gitlab-ci.yml sẽ trông như sau:

    stages:
      - Lint
    blackduck-detect:
      stage: Lint
      only:
          - cicd
      script:
        - java -jar /Users/jena/CICD/synopsys-detect/synopsys-detect-6.4.1.jar \
          --blackduck.url=${BLACKDUCK_SERVER} \
          --blackduck.api.token=${BLACKDUCK_TOKEN} \
          --detect.project.name=W95.CICD
      tags:
        - w95

    Rồi xong, tới công chuyện luôn!! Như vậy là chúng ta đã hoàn thành cấu hình CI với Blackduck để scan các lỗi hổng bảo mật, bản quyền. Mặc dù liên quan đến CI còn rất nhiều section như Coverity, Build & Compile nhưng mình xin phép tạm dừng hạng mục CI và chuyển sang CD. Rất mong được các bạn ủng hộ.

  • Triển khai CI/CD cho iOS – SonarQube & Blackduck

    Triển khai CI/CD cho iOS – SonarQube & Blackduck

    Overview

    Tiếp tục với series CI/CD cho iOS, hôm nay chúng ta sẽ triển khai CI với hai nền tảng kiểm tra source code rất nổi tiếng là SonarQube và Blackduck.

    Triển khai CI với SonarQube

    Đôi chút về SonarQube, đây là một nền tảng mã nguồn mở sử dụng để kiểm tra chất lượng của source code, đánh giá các lỗi ở nhiều mức độ và tiêu chí khác nhau. Mục đích cuối cùng là để thống kê và cải thiện chất lượng của source code theo mọi mặt cũng như giúp lập trình viên đánh giá chất lượng của chính mình. Vì vậy, việc sử dụng SonarQube để hỗ trợ quá trình phát triển, nâng cao chất lượng source code luôn được các doanh nghiệp lớn áp dụng.

    Chúng ta sẽ thực hiện CI với SonarQube server ở địa chỉ sau: https://sonarcloud.io, bạn có thể sử dụng tài khoản GitLab của mình để đăng nhập và tạo project ở đây. Sau một vài bước đăng nhập, tạo organization và chọn plan thì chúng ta sẽ đến với step đầu tiên.

    Đối với một số trường hợp có Server riêng để host SonarQube, các bạn hãy truy cập host và sử dụng tài khoản/mật khẩu được cung cấp bởi admin.

    Cấu hình SonarQube

    Việc đầu tiên, chúng ta sẽ cần tạo một project, ở đây mình sẽ tạo một project như hình sau:

    Điền Project Key và Display name sau đó ấn Set Up

    Trong một số trường hợp, bước config tạo Project sẽ do admin tạo, các bạn chỉ cần đăng nhập là xem được các project mình được phân quyền.

    Sau khi ấn Set Up, chúng ta sẽ được suggest 3 lựa chọn, ở đây mình sẽ chọn Manually để có thể sử dụng ở nhiều nền tảng khác nhau (Không chỉ riêng GitLab-CI).

    Lựa chọn Manually

    Tiếp đó chúng ta sẽ chọn các lựa chọn như hình sau:

    Chọn Other(…) -> macOS

    Tiếp đó chúng ta ấn Download để tải CLI của SonarQube về, giải nén và lưu vào một thư mục trong thiết bị runner. Giả sử mình sẽ lưu ở địa chỉ sau:

    /Users/jena/Projects/sonar-scanner/bin

    Mình sẽ thêm thư mục bin vào trong PATH của macOS bằng câu lệnh sau

    export PATH=$PATH:/Users/jena/Projects/sonar-scanner/bin

    Sau đó, bạn có thể chạy lệnh sau để kiểm tra xem cli đã được nhận vào PATH chưa. Sẽ có một số lỗi yêu cầu cấp quyền để chạy CLI, bạn hãy vào System Preference -> Security & Privacy -> Tab General -> Allow Anyway tất cả

    sonar-scanner -v
    Sau khi cli được add vào PATH, bạn có thể chạy bằng lệnh sonar-scanner

    Tiếp đó, chúng ta sẽ vào GitLab và cài đặt biến môi trường SONAR_TOKEN như hình sau

    Tiếp đó, chúng ta sẽ cấu hình CI ở file .gitlab-ci.yml như sau, phần script sẽ được gen cùng với SONAR_TOKEN, các bạn chỉ cần copy và paster vào là được:

    stages:
      - Lint
    sonar-scanner:
      stage: Lint
      only:
          - cicd
      script:
        - sonar-scanner -Dsonar.organization=w95 -Dsonar.projectKey=CICD.iOS -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.branch=master
      tags:
        - w95

    Sau khi commit file .gitlab-ci.yml lên branch cicd, chúng ta sẽ có kết quả như sau:

    Job Sonar Scanner chạy thành công với log như trên
    Màn hình thống kê trên SonarCloud.io cũng sẽ hiển thị các thông số của source code

    Theo như ảnh trên, Quality Gate đang đánh giá Passed tức là source code đạt chất lượng, nhưng thật ra mình scan Starter Project của iOS nên mới không có lỗi, còn code của mình thì lắm lỗi lắm :p

    Như vậy là chúng ta đã hoàn thành bước cấu hình CI sử dụng SonarQube cho một project iOS. Mỗi khi có commit, merge hay sự thay đổi trên branch cicd (bạn sẽ đổi thành master/develop/main) thì hệ thống sẽ tự động chạy CI và đẩy thống kê lên Sonar server. Chúng ta chỉ cần lên đó, tracking các thông số và sửa các lỗi bị cảnh báo là được.

    Do bài viết hơi dài, nên mình sẽ để phần Blackduck sang bài viết sau. Cảm ơn các bạn đã đọc!

    Authors

    LinhNB1

  • Triển khai CI/CD cho iOS – SwiftLint

    Triển khai CI/CD cho iOS – SwiftLint

    Hưởng ứng theo tinh thần của Editor team, mình đóng góp Series này để hưởng ứng Technopedia, không nhằm mục đích dự thi. Mong rằng các kinh nghiệm của mình sẽ giúp ích được cho cộng đồng trong lĩnh vực liên quan.

    Sam

    Để triển khai CI/CD cho một sản phẩm iOS có rất nhiều lựa chọn, chúng ta có thể sử dụng GitLab-CI, Xcode Server, Fastlane, Jenkins, Microsoft App Center, Circle CI, …
    Ở phạm vi bài viết này, chúng ta sẽ đề cập đến một nền tảng được tích hợp với GitLab: GitLab-CI

    Việc triển khai CI/CD cho một dự án iOS Swift bao gồm một số phần sau:

    1. Triển khai CI với SwiftLint
    2. Triển khai CI với SonarQube, Blackduck
    3. Triển khai CD đơn giản với Gitlab-CI Artifact
    4. Triển khai CD In-house với DeployGate.
    5. Triển khai CD OTA Inhouse trên Website với AWS S3 (Static Website Hosting)
    6. Triển khai CD với Appstore Connect

    Cài đặt và khởi tạo Runner

    Đầu tiên, các chúng ta cần cài đặt và khởi tạo Runner cho Gitlab Repo. Mình có viết một bải hướng dẫn ở đây
    Chú ý:
    Sau khi đăng ký runner, nhưng các job vẫn chạy trên container mặc định của GitLab-CI thì hãy chạy các lệnh sau:

    gitlab-runner install
    gitlab-runner start
    gitlab-runner status

    1. Triển khai CI với SwiftLint

    Đầu tiên, chúng ta sẽ đi đến việc cấu hình CI sử dụng SwiftLint để phân tích chất lượng source code.

    Tốt nhất chúng ta sẽ cài đặt và sử dụng Swiftlint tách biệt với source code của dự án như sau

    brew install swiftlint

    Sau khi cài đặt Swiftlint, ta có thể test bằng cách di chuyển vào thư mục root của source code và chạy lệnh

    swiftlint

    Kết quả lint source code sẽ hiển thị như sau, ví dụ ở đây ta có 16 lỗi.

    Sau đó, chúng ta cấu hình file .gitlab-ci.yml để chạy lint trên branch cicd như sau:

    stages:
      - Lint
    lint-source:
      stage: Lint
      only:
          - cicd
      script:
        - swiftlint

    Kết quả khi commit code lên branch cicd, chúng ta sẽ có kết quả log như sau:

    Mặc định Swiftlint job sẽ trả về kết quả thành công exit 0


    Ở đây chúng ta thấy, hệ thống đã phát hiện được 16 lỗi ở 3 files code. Tuy nhiên, job vẫn success và các merge request vẫn được phép tiếp tục vì Swiftlint vẫn trả về success thay vì error. Đây là một rủi ro, và để ép chặt các thành viên phải fix hết các lỗi Swiftlint trước khi được merge code, ta sẽ thêm tham số vào script như sau:

    script:
      - swiftlint --strict

    Kết quả thu được, hệ thống scan code và trả về lỗi nếu swiftlint chưa được fix hết.

    Swiftlint job sẽ trả về lỗi exit 1 khi sử dụng tham số –strict


    Trong trường hợp chúng ta muốn “dễ dãi”, cho phép merge code trong trường hợp các lỗi swiftlint vẫn còn hoặc muốn job trả về thành công để tiếp tục các job tiếp theo, ta sẽ cấu hình thêm một chút như sau:

    stages:
      - Lint
    lint-source:
      stage: Lint
      allow_failure: true
      only:
        - cicd
      script:
        - swiftlint --strict
    Các job fail khi cấu hình allow_failure = true


    Khi ấy, các job phía sau vẫn sẽ được thực hiện, merge request vẫn sẽ được approve nhưng với thông báo Warning đáng chú ý hơn.

    Như vậy là đã hoàn thành công đoạn triển khai CI với Swiftlint, ở bài viết tiếp theo, mình sẽ hướng dẫn các bạn triển khai CI với SonarQube, Blackduck

    Authors

    LinhNB1

  • Đừng lạm dụng Enum

    Đừng lạm dụng Enum

    Nhà tâm lý học người Mỹ Abraham Maslow có một câu nói rất nổi tiếng

    If you only have a hammer, you tend to see every problem as a nail. (Nếu dụng cụ duy nhất bạn có chỉ là một chiếc búa, thì mọi vấn đề đều trông giống cái đinh)

    Câu nói này rất phù hợp với lập trình. Mỗi vấn đề đều có nhiều cách tiếp cận với ưu nhược điểm riêng tuỳ theo ngữ cảnh và ràng buộc. Không có giải pháp nào luôn hợp lý hoặc luôn tệ trong tất cả các trường hợp, kể cả Singleton ?. Enum cũng vậy. Nó là một tính năng ngôn ngữ linh hoạt và mạnh mẽ, tuy nhiên việc lạm dụng enum không chỉ làm giảm chất lượng code mà còn khiến codebase khó mở rộng hơn.


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


    1-Bản chất enum

    Trong tài liệu của mình, Apple chỉ ra rằng enum được tạo ra để định nghĩa một type với mục đích chứa các giá trị liên quan tới nhau. Nói cách khác, hãy dùng enum để nhóm một tập giá trị hữu hạn, cố định, và có quan hệ với nhau. Ví dụ như enum định nghĩa phương hướng

    //Tạo enum Direction ở đây là hợp lý bởi
    //các case liên quan tới nhau và số lượng case là hữu hạn
    enum Direction {
        case north
        case south
        case east
        case west
    }
    

    2-Vấn đề của enum

    Một khi được định nghĩa, chúng ta sẽ không thể thêm case để mở rộng enum mà không làm ảnh hướng tới những chỗ nó được sử dụng. Điều này có thể mang lại lợi ích nếu ta không dùng default case bởi Xcode sẽ giúp tránh việc bỏ lọt code. Tuy nhiên, đây cũng là một nhược điểm lớn trong trường hợp code hiện giờ không quan tâm tới case mới đó.

    Dùng enum để model các Error phức tạp

    Chắc hẳn chúng ta đã từng dùng enum để nhóm các loại Error cho response API như dưới đây

    enum APIError: Error {
        case invalidData(Data)
        case parsingError(Error)
        case noConnection
        case unknown(code: Int, description: String)
        //... các case khác ...
    }
    

    Thoạt nhìn, việc tạo APIError là hợp lý bởi chúng đều là lỗi liên quan đến response API và giờ đây ta có thể gõ .parsingError hay .invalidData cực kì tiện lợi. Mặc dù vậy, hướng tiếp cận này có 2 nhược điểm lớn:

    • Ta không bao giờ muốn switch toàn bộ case của nó
    • Nó không phải là cách tối ưu bởi struct là công cụ tốt hơn để giải quyết bài toán này

    Trong quá trình sử dụng, ngoại trừ các case cần thiết của APIError, việc switch toàn bộ case là hơi thừa thãi. Có thể hiện tại ta chỉ quan tâm đến lỗi .noConnection để hiện alert riêng và các lỗi khác sẽ dùng chung một kiểu alert. Cũng có thể ta chỉ quan tâm đến một vài lỗi nhất định để xử lý logic code nhưng chắc chắn không bao giờ là tất cả case cùng lúc. Lý do là bởi ngoài việc cùng miêu tả các error response, các error trên không có quan hệ gì với nhau.


    Hơn nữa, về mặt logic, việc dùng enum ở đây là sai bởi số lỗi có thể xảy ra khi xử lý API là vô hạn. Điều này trái ngược trực tiếp với bản chất của enum được nhắc đến ở trên. Trong trường hợp này, model APIError bằng struct phù hợp hơn rất nhiều

    struct InvalidData: Error {
        let data: Data
    }
    
    struct ParsingError: Error {
        let underlyingError: Error
    }
    
    struct NoConnection: Error { }
    
    struct Unknown: Error {
        let code: Int
        let description: String
    }
    

    Nếu thực sự muốn nhóm các lỗi vào một kiểu Error, chỉ cần tạo riêng một protocol và conform chúng với protocol đó

    protocol APIError: Error { }
    
    extension InvalidData: APIError { }
    extension ParsingError: APIError { }
    extension NoConnection: APIError { }
    extension Unknown: APIError { }
    

    Việc model APIError bằng structprotocol giúp code linh hoạt hơn khi giờ đây việc tạo ra các Error mới không làm ảnh hưởng đến codebase. Ta cũng có thể cung cấp hàm khởi tạo custom cho chúng, hay conform từng lỗi với các protocol khác nhau một cách dễ dàng thay vì những switch statement cồng kềnh trong enum. Cuối cùng, việc thêm và truy cập biến trong struct đơn giản hơn so với associated value trong enum rất nhiều.


    Sử dụng enum để model các Error đơn giản và hữu hạn là điều nên làm. Tuy nhiên, nếu tập Error đó lớn, hoặc chứa nhiều data đính kèm như các lỗi liên quan đến API thì struct là một lựa chọn tốt hơn hẳn. Trong thực tế, Apple cũng chọn cách này khi tạo URLError xử lý cho Networking của Foundation.

    Dùng enum để config code

    Một sai lầm phổ biến nữa là dùng enum để config UIView, UIViewController, hoặc các object nói chung

    enum MessageCellType {
        case text
        case image
        case join
        case leave
    }
    
    extension MessageCellType {
        var backgroundColor: UIColor {
            switch self {
            case .text: return .blue
            case .image: return .red
            case .join: return .yellow
            case .leave: return .green
            }
        }
        
        var font: UIFont {
            switch self {
            case .text: return .systemFont(ofSize: 16)
            case .image: return .systemFont(ofSize: 14)
            case .join: return .systemFont(ofSize: 12, weight: .bold)
            case .leave: return .systemFont(ofSize: 12, weight: .light)
            }
        }
        
        //...
    }
    
    class TextCell: UITableViewCell {
        func style(with type: MessageCellType) {
            contentView.backgroundColor = type.backgroundColor
            textLabel?.font = type.font
        }
    }
    

    MessageCellType định nghĩa các style cho giao diện của cell ứng với từng loại message để tái sử dụng ở nhiều màn khác nhau. Các thuộc tính chung có thể kể đến như backgroundColor hay UIFont.


    Giống với APIError, vấn đề đầu tiên của MessageCellType là ta không muốn switch toàn bộ case của nó. Với mỗi loại cell, ta chỉ muốn dùng một type nhất định để config cell đó. Việc switch tất cả các case ở hàm cellForRow(at:) là không hợp lý bởi luôn phải trả ra fatalError hoặc một UITableViewCell bù nhìn để thoả mãn Xcode vì số lượng subclass của UITableViewCell là vô hạn ?‍♂️.


    Một vấn đề khác với MessageCellType là việc khó mở rộng. Bản chất của enum là tính hoàn thiện và hữu hạn. Khi thêm bất kì case mới nào, ta đều phải update tất cả các switch statement sử dụng nó. Điều này đặc biệt tệ trong trường hợp đang viết framework vì giờ đây thay đổi sẽ phá hỏng code từ phía client.
    Giải pháp cho MessageCellType là biến nó thành struct và tạo ra các biến static thuộc type này

    struct MessageCellType {
        let backgroundColor: UIColor
        let font: UIFont
    }
    
    extension MessageCellType {
        static let text = MessageCellType(backgroundColor: .blue, font: .systemFont(ofSize: 16))
        static let image = MessageCellType(backgroundColor: .red, font: .systemFont(ofSize: 14))
        static let join = MessageCellType(backgroundColor: .yellow, font: .systemFont(ofSize: 12, weight: .bold))
        static let leave = MessageCellType(backgroundColor: .green, font: .systemFont(ofSize: 12, weight: .light))
    }
    

    Refactor từ enum thành struct giúp việc thêm config mới không còn là vấn đề bởi nó không hề ảnh hưởng tới codebase. Một lợi ích nhỏ nữa là ta vẫn được gõ .join hoặc .leave khi truyền chúng vào trong function

    let cell: TextCell = TextCell()
    cell.style(with: .join)
    

    3-Tổng kết

    Trước khi tạo enum, hãy luôn nhớ rằng

    Enum dùng để switch. Nếu không chắc rằng mình muốn switch nó thì hãy sử dụng struct và protocol


  • Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder pattern trong iOS

    Nghe đến Design Pattern, chắc hẳn mỗi lập trình viên đều biết đến kỹ thuật quan trọng này và đã từng áp dụng nó ít nhất một lần. Design pattern giúp bạn giải quyết vấn đề một cách tối ưu nhất, cung cấp cho bạn các giải pháp trong lập trình hướng đối tượng (OOP). Phần lớn các ngôn ngữ lập trình đều có thể áp dụng Design pattern và Swift cũng không ngoại lệ!

    Design pattern có 3 nhóm chính:

    • Creational Pattern bao gồm: Abstract Factory, Factory Method, Singleton, Builder, Prototype.
    • Structural Pattern bao gồm: Adapter, Bridge, Composite, Decorator, Facade, Proxy và Flyweight.
    • Behavioral Pattern bao gồm: Interpreter, Template Method, Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy và Visitor.

    Hôm nay, chúng ta sẽ cùng tìm hiểu một design pattern trong nhóm Creational Pattern (Nhóm khởi tạo) là Builder pattern.

    Nội dung

    • Builder pattern là gì?
    • Vấn đề
    • Sử dụng nó như thế nào
    • Áp dụng trong iOS
    • Tổng kết

    Builder pattern là gì?

    Trong OOP, Builder pattern là một loại pattern thuộc nhóm Creational pattern (Tạo dựng), vì vậy nó sẽ giúp chúng ta giải quyết các vấn đề liên quan tới tạo dựng một object bằng cách:

    • Chia nhỏ các hàm khởi tạo của một object.
    • Đặt cấu hình mặc định và logic xử lý liên quan tới việc khởi tạo một object từ trong class ra bên ngoài và đẩy vào Builder class.

    Và việc chia nhỏ và đẩy vào Builder class như thế nào thì chúng ta hãy cùng tìm hiểu tiếp về vấn đề và áp dụng Builder Pattern trong một bài toán.

    Vấn đề

    Giả sử, chúng ta có một chương trình đặt bánh pizza, và mục tiêu của chúng ta là tạo ra chương trình đặt bánh để lấy yêu cầu từ khách hàng.

    Chúng ta có đối tượng Pizza, vả khởi tạo các đối tượng Pizza:

    
    class Pizza {
        
        var smell: String
        var base: String
        var size: String
        var isChiliSauce: Bool
        var isKetchup: Bool
        
        init(smell: String, base: String, size: String, isChiliSauce: Bool, isKetchup: Bool) {
            self.smell = smell
            self.base = base
            self.size = size
            self.isChiliSauce = isChiliSauce
            self.isKetchup = isKetchup
        }
    }
    
    
    
    let sizeM = Pizza(smell: "Bò", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeMChicken = Pizza(smell: "Gà", base: "Mỏng", size: "M", isChiliSauce: true, isKetchup: true)
    
    let sizeL = Pizza(smell: "Gà", base: "Dày", size: "L", isChiliSauce: true, isKetchup: true)
    
    

    Chúng ta có thể thấy hàm tạo khá nhiều thuộc tính, và có những thuộc tính mặc định ít thay đổi. Vì những thuộc tính đó ít thay đổi nên sẽ không tránh khỏi việc duplicate code. Do đó, người ta sử dụng Builder Pattern.

    Sử dụng nó như thế nào

    Class diagram

    Cùng phân tích một chút nhé:

    • Director: Đại diện cho module cần kết quả từ việc khởi tạo Pizza.
    • PizzaBuilder: Một class implement InterfaceBuilder, có nhiệm vụ khởi tạo ra Pizza, thông qua hàm build().
    • Pizza: Là object cần khởi tạo.

    Demo

    Một protocol nhằm khai báo các phương thức

    
    protocol PizzaBuilderProtocol {
        
        func build() -> Pizza
        
        func setSmell(_ smell: String) -> PizzaBuilder
    
        func setBase(_ base: String) -> PizzaBuilder
    
        func setSize(_ size: String) -> PizzaBuilder
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder
    }
    
    

    PizzaBuilder triển khai các phương thức đã khai báo ở PizzaBuilderProtocol

    
    class PizzaBuilder: PizzaBuilderProtocol {
        
        private var pizza = Pizza()
        
        func build() -> Pizza {
            return self.pizza
        }
        
        func setSmell(_ smell: String) -> PizzaBuilder {
            pizza.smell = smell
            return self
        }
        
        func setBase(_ base: String) -> PizzaBuilder {
            pizza.base = base
            return self
        }
        
        func setSize(_ size: String) -> PizzaBuilder {
            pizza.size = size
            return self
        }
        
        func withChiliSauce(_ isChiliSauce: Bool) -> PizzaBuilder {
            pizza.isChiliSauce = isChiliSauce
            return self
        }
        
        func withKetchup(_ isKetchup: Bool) -> PizzaBuilder {
            pizza.isKetchup = isKetchup
            return self
        }
        
    }
    
    

    Cart ở đây giữ vai trò tương đương Director trong hình bên trên.

    
    class Cart {
        
        let orderId: String
        let products: [Pizza]
        
        init(orderId: String, products: [Pizza]) {
            self.orderId = orderId
            self.products = products
        }
    }
    
    let pizza1 = PizzaBuilder()
                .setSize("s")
                .setSmell("Gà")
                .setBase("Dày")
                .build()
            
    let cart1 = Cart(orderId: "OrderId1", products: [pizza1])
    
    

    Cách thực hiện thật đơn giản phải không? Bản chất của nó là chia nhỏ các properties trong Object ra thành các hàm getter, setter trong Builder và sau đó thực hiện set giá trị cho chúng và khởi tạo Object qua hàm build(). Và đó chính là cách viết theo Builder Pattern.

    Chắc hẳn khi các bạn đọc đến đây sẽ có một suy nghĩ rằng: Vậy tại sao không dùng default value? Đúng vậy, khi Builder Pattern được giới thiệu, ngôn ngữ họ sử dụng khi đó là C++. Nhưng, Swift của chúng ta đã cung cấp tính năng default value, và chúng ta có thể hoàn toàn loại bỏ Builder. Nói như vậy thì Builder Pattern sẽ trở nên vô dụng trong Swift?

    Chắc chắn là không. Những tài liệu trên mạng về Builder Pattern khá nhiều, và nó đang hướng người đọc vào vấn đề default value trong các ngôn ngữ như: C++, Java…Chúng ta hãy cùng nhìn lại về những thứ mà Builder làm, đó là giảm thiệu sự phức tạp khi khởi tạo thông qua hàm builder(). Bản chất chính là nó đang đóng gói việc khởi tạo một Object. Khi đó, chúng ta sẽ có thể xử lý nhiều hiện object, do something… trước khi trả về một Object hoàn chỉnh.

    Áp dụng trong iOS

    Chúng ta có thể áp dụng Builder Pattern ở rất nhiều trường hợp trong lập trình iOS. Mình, mình áp dụng trong việc customize một style cho Label, Button…

    Thường khi, chúng ta customize một Label theo một style nào đó, chúng ta sẽ thường khởi tạo như thế này, nhìn có vẻ khá rối và không được thú vị cho lắm.

    
    let attrString = NSMutableAttributedString(string: title)
            
    attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: UIColor.red],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttributes([NSMutableAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 13)],
                                      range: attrString.mutableString.range(of: title))
            
    attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, attrString.length))
            
    titleLabel.attributedText = attrString
    
    

    Chúng ta có thể áp dụng Builder pattern trong trường hợp này để nhìn code trở lên dễ đọc hơn và cũng dễ dàng phát triển trong tương lai.

    Tạo một builder và protocol

    
    protocol AttributedStringBuilderProtocol {
        
        func build() -> NSMutableAttributedString
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder
        func withStrikeThrough() -> AttributedStringBuilder
        
    }
    
    class AttributedStringBuilder: AttributedStringBuilderProtocol {
        
        private var attrString: NSMutableAttributedString
        
        init(string: String) {
            self.attrString = NSMutableAttributedString(string: string)
        }
        
        func build() -> NSMutableAttributedString {
            return self.attrString
        }
        
        func setFont(_ font: UIFont, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.font: font],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func setColor(_ color: UIColor, forSubString subString: String) -> AttributedStringBuilder {
            self.attrString.addAttributes([NSMutableAttributedString.Key.foregroundColor: color],
                                          range: self.attrString.mutableString.range(of: subString))
            return self
        }
        
        func withStrikeThrough() -> AttributedStringBuilder {
            self.attrString.addAttribute(NSAttributedString.Key.strikethroughStyle, value: 2,
                                         range: NSMakeRange(0, self.attrString.length))
            return self
        }
    }
    
    

    Sử dụng

    
    let atrString = AttributedStringBuilder(string: title)
                .setFont(.boldSystemFont(ofSize: 13), forSubString: title)
                .setColor(.red, forSubString: title)
                .withStrikeThrough()
                .build()
    
    titleLabel.attributedText = attrString
    
    

    Như vậy, với trường hợp trên, việc sử dụng Builder Pattern sẽ giúp những dòng code của chúng ta trở nên dễ nhìn hơn và thuận lợi trong việc phát triển tiếp những properties của nó.

    Tổng kết

    Tóm lại, Design pattern: Builder Pattern là một trong những pattern dễ thực hiện. Nhưng khi sử dụng pattern này chúng ta cần phải mất thời gian để xác định được trường hợp nào cần nó, nó có thể trở nên rườm rà hơn khi sử dụng trong Swift. Nhưng chúng ta hãy đọc, tìm hiểu để áp dụng những tư duy và ý tưởng của một pattern vào một bài toán phù hợp. Đầu tư thời gian vào việc chuẩn bị xây dựng một sản phẩm sẽ luôn sẽ giúp việc bảo trì code trở nên dễ dàng hơn.

  • Vision:  Person Segmentation

    Vision: Person Segmentation

    Overview

    Vision framework được giới thiệu lần đầu vào WWDC 2017. Vision giúp thực hiện phát hiện khuôn mặt, phát hiện văn bản, nhận dạng mã vạch, đăng ký hình ảnh… Vision cũng cho phép sử dụng Core ML để tùy chỉnh cho các tác vụ như phân loại hoặc phát hiện đối tượng.

    Trong sample này chúng ta sẽ cùng tìm hiểu Person Segmentation API, giúp ứng dụng của bạn tách mọi người trong hình ảnh khỏi môi trường xung quanh.

    alt text

    Notes: Sample yêu cầu chạy Physical device với ios 15+ hoặc chạy Simulator ios 15+ trên macOS chip Intel

    Cách tạo một Vision requests

    Muốn sử dụng bất kỳ thuật toán nào của Vision, bạn sẽ cần ba bước:

    1. Request: Yêu cầu xác định loại thứ bạn muốn phát hiện và một trình xử lý hoàn thành sẽ xử lý kết quả. Đây là subclass của VNRequest.

    2. Request handler: Sử dụng VNImageRequestHandler để thực hiện Request.

    3. Results: Kết quả sẽ được đính kèm với Request ban đầu và được chuyển đến trình xử lý hoàn thành được xác định khi tạo Request. Chúng là các subclass của VNObservation.

    Prepare the Requests

    // 1. Request
    let request = VNGeneratePersonSegmentationRequest { (request, error) in
        // 3. Results
        self.personSegmentation(request: request, error: error)
    }
            
    request.qualityLevel = .accurate
    request.outputPixelFormat = kCVPixelFormatType_OneComponent8
            
    // 2. Request Handler
    let imageRequestHandle = VNImageRequestHandler(cgImage: cgImage, options: [:])
    

    Chúng ta sẽ tạo 1 request với VNGeneratePersonSegmentationRequest. VNGeneratePersonSegmentationRequest giúp tạo mask hình ảnh cho người mà nó phát hiện trong hình ảnh. Việc set thuộc tính qualityLevel cho request thành .fast, .balanced, hoặc .accurate giúp xác định chất lượng của mask được tạo như trong hình minh họa bên dưới.

    alt text

    Việc tăng độ chính xác của mask cũng đồng hời làm giảm suất làm việc của app nên chúng ta cần lựa chọn qualityLevel phù hợp cho từng tác vụ mà nó xử lý

    alt text

    Thuộc tính tiếp theo là format đầu ra của mask: outputPixelFormat. outputPixelFormat định dạng mà mặt nạ kết quả sẽ được trả về, có 3 định dạng có thể chọn đó là:

    alt text

    Trong sample chúng ta set outputPixelFormat với kCVPixelFormatType_OneComponent8 với range từ 0 đến 255.

    Bước cuối cùng là thực hiện request với VNImageRequestHandler đã tạo từ trước. result trả về của request handle sẽ là 1 instance của VNPixelBufferObservation

    DispatchQueue.global(qos: .userInitiated).async {
        do {
            try imageRequestHandle.perform([request])
        } catch let error {
            print(error)
        }
    }
    

    Xử lý Results

    alt text

    Chúng ta đã tìm hiểu qua về request và các thuộc tính của nó, ta sẽ đến với phần tiếp theo là xử lý result trả về. Những gì ta cần làm ở đây là thay thế background của ảnh gốc nằm ngoài mask trả về từ result.

    guard let result = request.results?.first as? VNPixelBufferObservation else {
        loadingView.stopAnimating()
        return
    }
    // 1. Processing
    let buffer: CVPixelBuffer = result.pixelBuffer
    let maskImage: CIImage = CIImage(cvImageBuffer: buffer)
    let bgImage = UIImage(named: "background")!
    let background = CIImage(cgImage: bgImage.cgImage!)
    let input = UIImage(named: "humanFace")!
    let inputImage = CIImage(cgImage: input.cgImage!)
            
    // 2. Scale mask, and background to size of original image
    let maskScaleX = inputImage.extent.width / maskImage.extent.width
    let maskScaleY = inputImage.extent.height / maskImage.extent.height
    let maskScaled =  maskImage.transformed(by: __CGAffineTransformMake(maskScaleX, 0, 0, maskScaleY, 0, 0))
            
    let backgroundScaleX = inputImage.extent.width / background.extent.width
    let backgroundScaleY = inputImage.extent.height / background.extent.height
    let backgroundScaled =  background.transformed(by: __CGAffineTransformMake(backgroundScaleX, 0, 0, backgroundScaleY, 0, 0))
            
    // 3. Blending Image
    let blendFilter = CIFilter.blendWithMask()
    blendFilter.inputImage = inputImage
    blendFilter.maskImage = maskScaled
    blendFilter.backgroundImage = backgroundScaled
    
    // 4. Handle Result
    if let blendedImage = blendFilter.outputImage {
        let context = CIContext(options: nil)
        let maskDisplayRef = context.createCGImage(maskScaled, from: maskScaled.extent)
        let filteredImageRef = context.createCGImage(blendedImage, from: blendedImage.extent)
        DispatchQueue.main.async {
            self.imageDisplay.image = UIImage(cgImage: filteredImageRef!)
            self.maskImage.image = UIImage(cgImage: maskDisplayRef!)
        }
    }
    

    Đây là những gì ta làm trong đoạn code trên:

    1. Import ảnh gốc, hình nền cần thay thế và mask từ result trả về.

    2. Scale kích thước của mask và hình nền về size của ảnh gốc.

    3. Tạo ra 1 CoreImage blend filter, ta dùng blendWithRedMask() vì khi tạo ra CIImage của mask từ CVPixelBuffer của nó, nó sẽ tạo ra 1 object mặc định ở red chanel.

    4. Xử lý result filter, update UI.

    Đây là kết quả sau khi đã xử lý xong:

    alt text

    Kết luận

    Qua bài viết này mình muốn chia sẻ tới mọi người trình tự tạo một Vision requests, hiểu hơn về Person Segmentation API và cách sử dụng của nó. Mong rằng bài viết tới mình có thể chia sẻ tới các bạn cách sử dụng của Person Segmentation API với video.

    Refer

  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 1)

    Table of contents

    • Đặt tên biến
    • Đặt tên hàm
    • Đặt tên class, struct, enum, protocol
    • Spacing
    • Comment
    • Access Control
    • Self & Closure

    Đặt tên biến

    • Hai quy tắc cơ bản nhất khi đặt tên biến đó là: sử dụng tiếng Anh thay vì tiếng Việt, sử dụng lowerCamelCase (kiểu lạc đà) thay vì snake_case

    Not Preferred

     private let height_normal_avatar: CGFloat = 60.0
     private let chieuRongNormalAvatar: CGFloat = 120.0
    

    Preferred

     private let heightNormalAvatar: CGFloat = 60.0
     private let widthNormalAvatar: CGFloat = 120.0
    
    • Khi đặt tên biến, hãy chú trọng đến sự rõ ràng, rành mạch hơn là sự ngắn gọn. Cố gắng làm sao khi đọc tên biến lên, ta có thể tưởng tượng được ngay biến đó có nhiệm vụ gì hoặc đang ám chỉ đến đối tượng nào. Vì vậy khi đặt tên biến không nên viết tắt và cũng không nên đặt tên giống với các đối tượng của hệ thống

    Not Preferred

    @IBOutlet private weak var tableView: UITableView!
    @IBOutlet private weak var imgAvatar: UIImageView!
    @IBOutlet private weak var lblbName: UILabel!
    
    private var bool: Bool = false
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var avatarImageView: UIImageView!
    @IBOutlet private weak var nameLabel: UILabel!
    
    private var isLoadingState: Bool = false
    
    • Tên biến nên được bắt đầu bằng 1 danh từ và khi khai báo biến, nên khai báo luôn kiểu dữ liệu của biến đó (điều này có thể làm giảm được phần nào thời gian compile của app)

    Not Preferred

    private var dataSalaryArray = [Salary]()
    private var isLoadingState = false
    

    Preferred

    private var dataSalaryArray: [Salary] = [Salary]()
    private var isLoadingState: Bool = false
    
    • Với những biến cùng kiểu, nên đặt tên có sự thống nhất từ trên xuống dưới, tránh tình trạng mỗi biến 1 style đặt tên khác nhau. Ví dụ trong trường hợp với height và top constraint của đối tượng avatarImageView

    Not Preferred

    @IBOutlet private weak var heightOfAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintForAvatarImageView: NSLayoutConstraint!
    

    Preferred

    @IBOutlet private weak var heightConstraintAvatarImageView: NSLayoutConstraint!
    @IBOutlet private weak var topConstraintAvatarImageView: NSLayoutConstraint!
    

    Đặt tên hàm

    • Cũng giống như việc đặt tên biến, việc đặt tên hàm cũng có các quy tắc tương tự: dùng tiếng Anh, dùng kiểu lowerCamelCase (kiểu lạc đà)
    • Tên hàm thường được bắt đầu bằng động từ, tên hàm phải rõ ràng, rành mạch. Cố gắng làm sao khi đọc tên hàm lên, ta có thể tưởng tượng được ngay hàm đó làm nhiệm vụ gì.
    • Đối với những function có nhiều param, nên đặt mỗi param trên 1 dòng và căn lề cho chúng. Ngoài ra, với những param có giá trị mặc định, nên đặt chúng ở cuối list parameter. Còn những param không có giá trị mặc định thì nên đặt lên đầu

    Not Preferred

    func setAttributedString(string: String, font: UIFont, lineSpacing: CGFloat, alignment: NSTextAlignment = .left, icon: UIImage? = nil, íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    

    Preferred

    func setAttributedString(string: String, 
                            font: UIFont, 
                            lineSpacing: CGFloat, 
                            lignment: NSTextAlignment = .left, 
                            icon: UIImage? = nil, 
                            íconRect: CGRect? = nil) -> NSAttributedString {
        // Do something
    }
    
    • Bên cạnh việc sử dụng parameter 1 cách truyền thống, ta có thể sử dụng thêm specifying argument labels (thêm 1 label vào trước tên của param) hoặc omitting argument labels (thêm dấu gạch dưới _ vào trước tên của param). Điều này làm cho việc gọi tên hàm sẽ trở nên gần gũi hơn với ngôn ngữ tự nhiên. (Tham khảo thêm tại Function)

    Preferred

    func greet(person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(greet(person: "Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Preferred

    func welcome(_ person: String, from hometown: String) -> String {
        return "Hello \(person)!  Glad you could visit from \(hometown)."
    }
    
    print(welcome("Bill", from: "Cupertino"))
    // Prints "Hello Bill!  Glad you could visit from Cupertino."
    

    Đặt tên class, struct, enum, protocol

    • Đặt tên class, struct, enum và protocol ta cũng sử dụng tiếng Anh nhưng sẽ dùng kiểu UpperCamelCase – đây là điểm khác biệt so với function và property
    • Tên class, struct, enum, protocol thường được bắt đầu bằng danh từ và khi đặt tên cũng cần ưu tiên sự rõ ràng, rành mạch.

    Spacing

    • Các dấu ngoặc nhọn mở đầu cho các function và các dấu ngoặc sau các biểu thức if/else/switch/while… đều phải được mở trên cùng 1 dòng với câu lệnh, có thêm 1 khoảng trắng phía bên trái và đóng trên 1 dòng khác

    Not Preferred

    if user.isHappy
    {
      // Do something
    }
    else {
      // Do something else
    }
    

    Preferred

    if user.isHappy {
      // Do something
    } else {
      // Do something else
    }
    
    • Nên có 1 dòng trắng giữa các function, giữa các block code và giữa khu vực khai báo các properties với khu vực ánh xạ các outlet. Trong 1 function cũng cần có những dòng trắng để phân tách các chức năng nhỏ trong function đó.

    Not Preferred

    import UIKit
    class SettingScreen: UIViewController {
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
        var snapView: UIView?
        var snapTabbarView: UIView?
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    extension SettingScreen {
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    

    Preferred

    import UIKit
    
    class SettingScreen: UIViewController {
    
        @IBOutlet private weak var settingTitleLabel: UILabel!
        @IBOutlet private weak var stateSettingSwitch: UISwitch!
    
        var snapView: UIView?
        var snapTabbarView: UIView?
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
        }
    }
    
    extension SettingScreen {
    
        func loadData(state: String) {
            DispatchQueue.main.async {
                self.settingTitleLabel.text = state
            }
        }
    }
    
    • Dấu 2 chấm luôn không có khoảng trắng ở phía bên trái và có 1 khoảng trắng ở phía bên phải. Ngoại trừ 3 trường hợp: toán tử 3 ngôi A ? B : C, empty dictionary [:] và #selector syntax addTarget(_:action:)

    Not Preferred

     class ViewController : UIViewController {
         private var data :[String:CGFloat] = ["A" : 1.2, "B":3.2]
     }
    

    Preferred

     class ViewController: UIViewController {
         private var data: [String: CGFloat] = ["A": 1.2, "B": 3.2]
     }
    
    • Không nên có khoảng trắng ở cuối mỗi dòng code nhưng nên có thêm 1 dòng trắng ở cuối mỗi file
    • Không có giới hạn nhất định cho số ký tự trên mỗi dòng code. Tuy nhiên mỗi dòng code của bạn không nên có quá 100 ký tự. Có 1 vài cách để làm giảm số lượng ký tự trên 1 dòng code:
      • Đối với function có nhiều param và các param có tên quá dài, bạn có thể xuống dòng và căn lề cho chúng (đã để cập ở trên)
      • Đối với các biểu thức tính toán, bạn có thể đặt ra các biến phụ thay vì gộp chung lại vào 1 biểu thức

    Not Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        return sqrt(((firstEdge + secondEdge + thirdEdge) / 2) * ((firstEdge + secondEdge + thirdEdge) / 2 - firstEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - secondEdge) * ((firstEdge + secondEdge + thirdEdge) / 2 - thirdEdge))
    }
    

    Preferred

    // Tính diện tích tam giác bất kỳ khi biết độ dài 3 cạnh (hệ thức Heron)
    func calculateSquareTriangleUsingHeron(firstEdge: CGFloat,
                                           secondEdge: CGFloat,
                                           thirdEdge: CGFloat) -> CGFloat {
        let halfPerimeter: CGFloat = (firstEdge + secondEdge + thirdEdge) / 2
        let halfPerimeterMinusA: CGFloat = halfPerimeter - firstEdge
        let halfPerimeterMinusB: CGFloat = halfPerimeter - secondEdge
        let halfPerimeterMinusC: CGFloat = halfPerimeter - thirdEdge
        let doubleSquare: CGFloat = halfPerimeter * halfPerimeterMinusA * halfPerimeterMinusB * halfPerimeterMinusC
        let square: CGFloat = sqrt(doubleSquare)
        return square
    }
    

    Comment

    • Đôi khi ta cần phải add comment để chú thích cho các đoạn code, phục vụ cho quá trình maintenance sau này. Tất nhiên là khi code được thay đổi thì comment cũng cần được update theo.
    • Khi comment, không nên dùng C-style /*…*/ mà nên dùng double-slash // hoặc triple-slash ///. Cũng không nên để code và comment xuất hiện trên cùng 1 dòng

    Access Control

    • Các function và property nên mặc định để là private hoặc fileprivate để đảm bảo tính đóng gói trong lập trình. Nên hạn chế việc sử dụng open, public hoặc internal. Tham khảo thêm tại Access Control

    Not Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet weak var monthLabel: UILabel!
        @IBOutlet weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.monthLabel.text = dataSalaryArray[indexPath.row].month
            cell.incomeLabel.text = dataSalaryArray[indexPath.row].incomeLabel
            return cell
        }
    }
    

    Preferred

    class SalaryCell: UITableViewCell {
        @IBOutlet private weak var monthLabel: UILabel!
        @IBOutlet private weak var incomeLabel: UILabel!
        
        override func awakeFromNib() {
            super.awakeFromNib()
        }
        
        func setupData(data: Salary) {
            monthLabel.text = data.month
            incomeLabel.text = "\(data.income)"
        }
    }
    
    extension ViewController: UITableViewDataSource {
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
    }
    
    • Khi khai báo property, các từ khoá liên quan đến access control nên được đặt lên đầu. Chỉ có 1 số từ khoá được đứng trước chúng đó là: static, @IBAction, @IBOutlet, @discardableResult.

    Not Preferred

    @IBOutlet weak private var salaryTableView: UITableView!
    @IBOutlet weak private var fullNameLabel: UILabel!
    

    Preferred

    @IBOutlet private weak var salaryTableView: UITableView!
    @IBOutlet private weak var fullNameLabel: UILabel!
    

    Self & Closure

    • Không nên sử dụng từ khoá self một cách tuỳ ý. Chỉ dùng self trong 2 trường hợp sau:
      • Khi trình biên dịch yêu cầu, thường là khi đang trong biểu thức closure
      func loadData() {
          DispatchQueue.main.async {
              self.salaryTableView.reloadData()
          }
      }
      
      • Khi đang ở trong hàm init, ta cần phân biệt giữa property của object và param của hàm init
      class Salary {
          var income: Int
          var month: String
      
          init(income: Int, month: String) {
              self.income = income
              self.month = month
          }
      }
      
    • Đối với closure, ta có thể dùng trailling closure syntax trong trường hợp chỉ có duy nhất 1 biều thức closure trong list parameter. Còn nếu có nhiều hơn 1, ta phải giữ lại tên cho các closure đó.

    Not Preferred

    // Trong trường hợp này, chỉ có duy nhất 1 biểu thức closure nên không cần thiết phải để lại label "animations"
    UIView.animate(withDuration: 1, animations: {
        self.avatarImageView.alpha = 0.0
    })
            
    // Trường hợp này có 2 biểu thức closure, vì vậy nên để lại cả 2 label "animations" và "completion" để phân biệt chúng với nhau
    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    } completion: { (_) in
        self.avatarImageView.removeFromSuperview()
    }
    

    Preferred

    UIView.animate(withDuration: 1) {
        self.avatarImageView.alpha = 0.0
    }
            
    UIView.animate(withDuration: 1,
                    animations: {
                        self.avatarImageView.alpha = 0.0
                    }, completion: { _ in
                        self.avatarImageView.removeFromSuperview()
                    })
    

    To be continue…

  • Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

    Coding convention – Những điều cần biết trước khi bắt tay vào code (Part 2)

    Table of contents

    • Magic number & Duplicate code
    • Code Organization
    • Scene Delegate
    • Computed Property
    • Optional
    • Multi-line String
    • Bonus

    Magic number & Duplicate code

    • Khi code ta không nên dùng những con số vô định, hay còn gọi là magic number, gây khó hiểu cho người khác. Điều này sẽ ảnh hưởng đến quá trình maintain sau này. Ta có thể thay thế những con số magic này bằng cách tạo ra các constant với tên gọi clear nhất có thể, làm sao để khi người khác đọc code của bạn, họ cũng có thể hiểu được vì sao bạn lại dùng đến con số đó. Hoặc ít nhất trước khi dùng magic number, ta phải thêm comment để giải thích lý do tại sao sử dụng chúng.
    • Ta cũng không nên để những đoạn code giống nhau được lặp đi lặp lại trong source code của mình. Nếu nhận thấy có những đoạn code cùng thực hiện một chức năng nhất định, hoặc cùng được apply cho 1 đối tượng nhất định, ta có thể nghĩ đến việc grouping chúng lại thành các function để tiện cho việc implement cũng như maintain sau này.

    Not Preferred

    class SignUpViewController: UIViewController {
    
        @IBOutlet private weak var firstNameTextField: UITextField!
        @IBOutlet private weak var lastNameTextField: UITextField!
        @IBOutlet private weak var accountTextField: UITextField!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Có thể thấy đoạn code này đang set borderWidth, borderColor, cornerRadius cho
            // lần lượt 3 TextField khác nhau. Tưởng tượng nếu sau này màn hình được update
            // thêm nhiều textfield khác nữa, hoặc trong app cũng có nhiều textfield cần phải 
            // setup các thuộc tính tương tự như trên. Mỗi textfield cần 3 dòng code x số lượng
            // textfield cả app = ...
            // -> Ta nghĩ đến việc tạo ra các function common để dùng chung cho các đối tượng 
            // UITextField, sẽ thuận tiện hơn cho việc implement và maintain sau này, sửa 1 hàm 
            // có thể apply được toàn bộ
            firstNameTextField.layer.borderColor = UIColor.orange.cgColor
            firstNameTextField.layer.borderWidth = 1.0
            firstNameTextField.layer.cornerRadius = 25
            
            lastNameTextField.layer.borderColor = UIColor.orange.cgColor
            lastNameTextField.layer.borderWidth = 1.0
            lastNameTextField.layer.cornerRadius = 25
            
            accountTextField.layer.borderColor = UIColor.orange.cgColor
            accountTextField.layer.borderWidth = 1.0
            accountTextField.layer.cornerRadius = 25
        }
    }
    

    Preferred

    class SignUpViewController: UIViewController {
    
       @IBOutlet private weak var firstNameTextField: UITextField!
       @IBOutlet private weak var lastNameTextField: UITextField!
       @IBOutlet  weak var accountTextField: UITextField!
       
       override func viewDidLoad() {
           super.viewDidLoad()
           
           firstNameTextField.setupLayer()
           lastNameTextField.setupLayer()
           accountTextField.setupLayer()
       }
    }
    // Phần extension cho các compoment như UILabel, UITextField, UIButton, ... thường 
    // được tách ra thành các file riêng. Xem thêm phần Code Organization
    extension UITextField {
       func setupLayer(borderWidth: CGFloat = 1.0,
                       borderColor: CGColor = UIColor.orange.cgColor,
                       cornerRadius: CGFloat = 25) {
           layer.borderWidth = borderWidth
           layer.borderColor = borderColor
           layer.cornerRadius = cornerRadius
       }
    }

    Code Organization

    • Ta không nên gộp tất cả các property và các function vào trong một block code ở trong một file. Vì đối với những file có số lượng dòng code lớn, việc làm này sẽ khiến ta khó hình dung được cấu trúc tổ chức của file và mục đích sử dụng của các function trong file đó. Ta nên chia nhỏ file thành nhiều block code, mỗi block code giải quyết một nhiệm vụ khác nhau và chứa các function liên quan tới nhiệm vụ đó. Ta có thể thêm từ khoá // MARK: – dosomething vào trên mỗi block code

    Not Preferred

    // 1 block code không nên adopt quá nhiều protocol, delegate như vậy
    class HomeViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    
        @IBOutlet private weak var salaryTableView: UITableView!
        @IBOutlet private weak var avatarImageView: UIImageView!
        @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
        
        private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                                Salary(income: 654321, month: "2/2021")]
        
        override func viewDidLoad() {
            super.viewDidLoad()
            salaryTableView.delegate = self
            salaryTableView.dataSource = self
            salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
        }
        
        func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return dataSalaryArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                             for: indexPath) as? SalaryCell else {
                return UITableViewCell()
            }
            cell.setupData(data: dataSalaryArray[indexPath.row])
            return cell
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            
        }
    
        func loadData() {
            // Do something
        }
    }
    

    Preferred

    class HomeViewController: UIViewController {
    
       // MARK: - Outlet
       @IBOutlet private weak var salaryTableView: UITableView!
       @IBOutlet private weak var avatarImageView: UIImageView!
       @IBOutlet private weak var widthConstraintAvatarImageView: NSLayoutConstraint!
       
       // MARK: - Property
       private var dataSalaryArray: [Salary] = [Salary(income: 123456, month: "1/2021"),
                                               Salary(income: 654321, month: "2/2021")]
       
       override func viewDidLoad() {
           super.viewDidLoad()
           salaryTableView.delegate = self
           salaryTableView.dataSource = self
           salaryTableView.register(UINib(nibName: "SalaryCell", bundle: nil), forCellReuseIdentifier: "SalaryCell")
       }
    }
    
    // MARK: - TableView DataSource
    extension HomeViewController: UITableViewDataSource {
       func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
           return dataSalaryArray.count
       }
       
       func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
           guard let cell: SalaryCell = salaryTableView.dequeueReusableCell(withIdentifier: "SalaryCell",
                                                                            for: indexPath) as? SalaryCell else {
               return UITableViewCell()
           }
           cell.setupData(data: dataSalaryArray[indexPath.row])
           return cell
       }
    }
    
    // MARK: - TableView Delegate
    extension HomeViewController: UITableViewDelegate {
       func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
           
       }
    }
    
    // MARK: - Do something
    extension HomeViewController {
       func loadData() {
           // Do something
       }
    }
    
    • Khi thêm từ khoá // MARK: – dosomething vào trước các block code, Xcode sẽ tự gen cho ta đường line phân cách giữa các block đó. Thêm vào đó, khi tap vào thanh công cụ như trên ảnh, ta cũng có thể overview được trong file đang có những block code nào, các block đảm nhận nhiệm vụ gì, các function liên quan đến nhiệm vụ đó. Việc này rất có ích khi file của bạn có số lượng dòng code lớn và có nhiều người cùng phát triển

    • Tương tự, ta cũng nên chia project thành nhiều các folder, mỗi folder có chức năng riêng và chứa các file liên quan đến chức năng đó.
    Preferred Not Preferred

    Scene Delegate

    • Khi tạo một project mới trong Xcode, hệ thống sẽ mặc định cho rằng app của bạn sẽ chỉ support cho version iOS cao nhất mà bản Xcode đó support trở lên (ví dụ: Xcode 11.3 sẽ support cho version iOS 13.6 trở lên, Xcode 12.0 sẽ support cho version iOS 14.0 trở lên, Xcode 12.5 là iOS 14.5 trở lên, …).
    • Từ Xcode 11.0 trở về trước, khi tạo mới 1 project, sẽ có 1 vài file mặc định được tạo như: ViewController.swift, Main.storyboard, AppDelegate.swift, Info.plist… Từ Xcode 11.0 trở lên, để phục vụ cho iOS 13, ngoài những file mặc định vừa liệt kê ở trên, còn có thêm 1 file nữa là SceneDelegate.swift (Tìm hiểu thêm tại đây).

    • Vì vậy, nếu muốn project có thể được build trên device (hoặc simulator) chạy version < iOS 13, ta sẽ phải tiến hành xoá file SceneDelegate.swift và thực hiện 1 vài config như sau.
    • Ngoại trừ trường hợp là bạn muốn làm việc với SwiftUI, việc xoá file SceneDelegate.swift và giảm target version build của app dường như là 1 việc bắt buộc bởi 1 vài lý do chính sau:
      • Khi phát triển 1 ứng dụng, chắc chắn chúng ta đều muốn đông đảo người dùng có thể tiếp cận được với ứng dụng đó. Nhưng không phải người dùng nào cũng sẵn sàng update version iOS mới nhất cho device của họ. Vì vậy việc để target vesion build của app ở mức “phổ thông” như iOS 11.0, 12.0, … sẽ làm tăng tính thương mại cho app của bạn.
      • Về mặt technical, khi build app trên những device “thấp” hoặc trên những iOS version thấp, ta sẽ có cơ hội để test nhiều hơn. Vì thực tế sẽ có rất nhiều bug rất “dị”, chúng chỉ xảy ra trên những “môi trường” thấp mà không xảy ra ở trên “môi trường” cao, hoặc cũng có nhiều trường hợp ngược lại. Đối với 1 developer, ta cần phải đảm bảo những dòng code của ta phải chạy ngon trên nhiều môi trường khác nhau.
      • Cũng về mặt technical, khi bạn muốn chia sẻ source code của mình và người khác muốn clone về. Trong trường hợp bạn dùng bản Xcode mới nhất còn họ dùng bản Xcode thấp hơn, nếu bạn không giảm target version build thì người khác sẽ không thể build source của bạn được.

    Computed Property

    • Để cho ngắn gọn, nếu 1 computed property thuộc kiểu read-only, ta có thể bỏ qua mệnh đề get. Mệnh đề này chỉ cần thêm vào khi có thêm cả mệnh đề set

    Preferred

    var diameter: Double {
      return radius * 2
    }
    

    Not Preferred

    var diameter: Double {
      get {
        return radius * 2
      }
    }
    

    Optional

    • Khi truy cập giá trị optional, nếu giá trị đó chỉ được truy cập 1 lần hoặc có nhiều optional trong chuỗi, ta có thể dùng 1 chuỗi optional liên tiếp
    textContainer?.textLabel?.setNeedsDisplay()
    
    • Trong trường hợp giá trị optional được truy cập nhiều lần, nên sử dụng if…let để mở ra 1 block code rồi thao tác trong đó
    if let textContainer = textContainer {
      // do many things with textContainer
    }
    
    • Khi đặt tên cho các biến và các property optinal, không cần thiết phải đặt kiểu như optionalNameLabel hay couldAvatarImageView vì trạng thái optional (?) đã có trong khi khai báo biến rồi.
    • Khi unwrapp biến optional, cũng không cần thiết phải đặt các tên như unwrappedView hay realLabel mà hãy dùng chính tên gốc của biến đó

    Preferred

    var subview: UIView?
    var volume: Double?
    
    // later on...
    if let subview = subview, let volume = volume {
      // do something with unwrapped subview and volume
    }
    
    // another example
    resource.request().onComplete { [weak self] response in
      guard let self = self else { return }
      let model = self.updateModel(response)
      self.updateUI(model)
    }
    

    Not Preferred

    var optionalSubview: UIView?
    var volume: Double?
    
    if let unwrappedSubview = optionalSubview {
      if let realVolume = volume {
        // do something with unwrappedSubview and realVolume
      }
    }
    
    // another example
    UIView.animate(withDuration: 2.0) { [weak self] in
      guard let strongSelf = self else { return }
      strongSelf.alpha = 1.0
    }
    
    • Nếu có nhiều biến optional được unwrapp với guard let hoặc if…let, hãy ghép chúng lại với nhau thành 1 câu lệnh để giảm thiếu việc lồng điều kiện. Khi ghép, hãy đặt guard trên 1 dòng riêng, đặt các điều kiện trên từng dòng riêng và thụt lề cho chúng, cuối cùng mệnh đề else được căn lề thẳng với guard

    Preferred

    guard 
      let number1 = number1,
      let number2 = number2,
      let number3 = number3 
    else {
      fatalError("impossible")
    }
    // do something with numbers
    

    Not Preferred

    if let number1 = number1 {
      if let number2 = number2 {
        if let number3 = number3 {
          // do something with numbers
        } else {
          fatalError("impossible")
        }
      } else {
        fatalError("impossible")
      }
    } else {
      fatalError("impossible")
    }
    
    
    

    Multi-line String

    • Khi muốn viết 1 văn bản dài nhiều dòng, nên sử dụng cú pháp Multi-line String Literal. Đó là mở văn trên 1 dòng rồi bắt đầu văn bản từ dòng thứ 2 trở đi, kết hợp với việc thụt lè và căn lề thằng các dòng tiêp theo

    Preferred

    let message: String = """
      You cannot charge the flux \
      capacitor with a 9V battery.
      You must use a super-charger \
      which costs 10 credits. You currently \
      have \(credits) credits available.
      """
    

    Not Preferred

    let message: String = """You cannot charge the flux \
      capacitor with a 9V battery.
      You must use a super-charger \
      which costs 10 credits. You currently \
      have \(credits) credits available.
      """
    

    Bonus

    • Không giống như 1 số ngôn ngữ khác (C/C++, JavaScript, …), Swift không yêu cầu phải có dấu chấm phảy ở cuối mỗi dòng code

    Preferred

    let swift = "not a scripting language"
    

    Not Preferred

    let swift = "not a scripting language";
    
    • Dấu ngoặc đơn bao quanh các điều kiện là không bắt buộc và nên được bỏ qua

    Preferred

    if name == "Hello" {
      print("World")
    }
    

    Not Preferred

    if (name == "Hello") {
      print("World")
    }
    
    • Tuy nhiên với những biểu thức phức tạp, có dấu ngoặc đơn bao quanh sẽ làm code trở nên clear hơn

    Preferred

    let playerMark: String = (player == current) ? "X" : "O"

    • Ngoài ra, còn 1 lỗi nữa mà người viết thấy đa phần các bạn newbie rất hay mắc phải. Tuy rằng lỗi này liên quan đến logic nhưng cũng xin liệt kê vào đây để anh em newbie tiện theo dõi. Đó là khi ta muốn truy xuất các phần tử trong 1 collection data (set, dictionary, array, … và ở đây xin lấy ví dụ là array), việc đầu tiên ta cần làm là phải check xem array đó có phần tử hay không rồi mới tiến hành việc truy xuất. Dưới đây là 1 ví dụ:

    Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    if PlistManager().getPlist(withName: name)?.count == 0 {
    return nil
    }
    let data = PlistManager().getPlist(withName: name)?[0]
    return data
    }

    Chúng ta có thể viết clear hơn như sau (dĩ nhiên đây là cách được recommend):

    Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    if let data = PlistManager().getPlist(withName: name)?.first {
    return data
    }
    }

    Tuyệt đối không được truy xuất trực tiếp vào các phần tử của array khi chưa check điều kiện, vì ta không thể chắc chắn được là array đó luôn có phần tử hay là không. Điều này sẽ tiềm ẩn nguy cơ bug rất cao.

    Not Preferred

    private func getDataFromPlistFile(_ name: String) -> Dictionary<String, AnyObject>? {
    let data = PlistManager().getPlist(withName: name)?[0]
    return data
    }

    Reference