Category: iOS

  • Hướng dẫn sử dụng SwiftGen cho iOS

    Hướng dẫn sử dụng SwiftGen cho iOS

    Tìm hiểu cách mà SwiftGen giúp dễ dàng loại bỏ các chuỗi ma thuật(magic strings) trong các dự án iOS của bạn.

    Là một nhà phát triển ứng dụng trên thiết bị di động, bạn có thể cảm thấy như bạn gặp phép thuật hàng ngày. Các dòng mã bạn viết sẽ được chuyển đổi thành các ứng dụng mà mọi người trên khắp thế giới có thể sử dụng. Các công cụ mà Apple cung cấp giúp biến điều kỳ diệu đó thành hiện thực và làm cho cuộc sống của bạn dễ dàng hơn. Khi tiến xa hơn vào lĩnh vực phát triển phần mềm, bạn có thể nhận ra có một thứ ma thuật mà bạn không thích: Chuỗi thần kỳ(magic strings).

    Loại an toàn, khái niệm rằng các biến chỉ có thể thuộc một loại cụ thể, cung cấp cho các nhà phát triển các rào chắn(guardrails) để giữ cho các chương trình của họ an toàn. Tuy nhiên, chuỗi ma thuật đưa mã không an toàn vào các ứng dụng đó. Chuỗi ma thuật là gì? Trong quá trình phát triển iOS, bạn đã gặp phải những điều này nhiều lần. Một ví dụ trông giống như sau:

    let color = UIColor(named: "green-apple")
    self.title = "Welcome!"

    Ví dụ này hiển thị “green-apple” và “Welcome!” được viết dưới dạng chuỗi trực tiếp trong mã của bạn. Không quá khi nói rằng tất cả các nhà phát triển đôi khi nhận thấy mình có lỗi với hành vi này.

    Trên thực tế, trong quá trình phát triển iOS, bạn không có nhiều lựa chọn. Ngoài ra, Xcode không cung cấp cách nào để tránh việc này.

    Những người đã làm việc trong Android có thể thấy mình run sợ trước những đoạn mã như thế này. Môi trường phát triển Android có cơ chế chuyển đổi tài nguyên ứng dụng, chẳng hạn như chuỗi, màu sắc, hình ảnh và phông chữ, thành các kiểu biến an toàn. Có rất nhiều lợi ích từ việc này, và đó là:

    • Giảm lỗi sai chính tả
    • Ngăn chặn sự trùng lặp tài nguyên không cần thiết
    • Cung cấp kiểm tra tài nguyên tại thời điểm biên dịch
    • Giúp dọn dẹp tài nguyên cũ
    • Và nhiều hơn thế nữa

    Như đã nêu, các nhà phát triển iOS và macOS không có quyền truy cập vào hệ thống cung cấp sự an toàn cho loại tài nguyên này.

    May mắn thay, có SwiftGen, một trình tạo mã để loại bỏ các chuỗi ma thuật trong ứng dụng của bạn. Có sẵn dưới dạng thư viện mã nguồn mở trên GitHub, bạn có thể thêm tính năng này vào các dự án iOS và macOS của mình để mang lại sự an toàn về loại và kiểm tra thời gian biên dịch của tất cả các Assets của bạn.

    Trong hướng dẫn này bạn sẽ học được cách:

    • Thiết lập dự án của bạn với SwiftGen
    • Xác định các nội dung mà bạn muốn chuyển đổi
    • Xác định nơi mà code được generated
    • Tạo các templates cho phép SwiftGen generate khi dùng SwiftUI cho Fonts và màu sắc.

    Getting Started

    Để bắt đầu, hãy nhấp vào nút Tải xuống tài liệu dưới đây:

    Có một số cách bạn có thể cài đặt SwiftGen để hoạt động với môi trường của mình như sau:

    • CocoaPods
    • Homebrew
    • Mint
    • Directly download a zipped release(Nên dùng cách này)

    Link hướng dẫn cài ở đây: https://github.com/SwiftGen/SwiftGen

    Trong hướng dẫn này, bạn sẽ sử dụng CocoaPods để quản lý SwiftGen.

    Lưu ý: Nếu bạn không có CocoaPods, đừng lo lắng – dự án khởi động và dự án cuối cùng đã được tải xuống phần phụ thuộc. :]

    Mở workspace, có tên là DrinksUp! .Xcworkspace. Vì dự án này sử dụng CocoaPods nên bạn sẽ không thể làm việc trực tiếp với DrinksUp! .Xcodeproj.

    Hãy dành một chút thời gian để xem xét xung quanh trong Xcode. Dự án đã ở trạng thái hoàn thành nhưng sử dụng chuỗi để tham chiếu phông chữ, màu sắc, hình ảnh và chuỗi. Bạn sẽ chuyển đổi tất cả những thứ này vào cuối hướng dẫn.

    Xây dựng và chạy và làm quen với ứng dụng.

    Ứng dụng, DrinksUp !, là một cách để theo dõi các loại đồ uống thú vị mà bạn và gia đình đã thử khi đến nhà hàng hoặc ở nhà.

    Thiết lập SwiftGen

    Bắt đầu bằng cách mở Terminal và điều hướng đến thư mục gốc của dự án khởi động của bạn. Tiếp theo, nhập lệnh sau vào Terminal:

    ./Pods/SwiftGen/bin/swiftgen config init

    Thao tác này sẽ tạo một tệp cấu hình, có tên là swiftgen.yml, tại thư mục gốc dự án của bạn. Nếu tệp này tự động mở trong Xcode, hãy tiếp tục và tắt nó đi.

    Tiếp theo, trong workspace dự án của bạn, đi tới Files ▸ Add new file vào “DrinksUp!”…. Tìm swiftgen.yml. Đảm bảo bỏ chọn Sao chép các mục nếu cần và chọn Tạo tham chiếu thư mục.

    Add SwiftGen.yml without copying file

    Nhấp vào nút Thêm. Khi hoàn tất, bạn sẽ thấy swiftgen.yml ở đầu trình điều hướng Dự án, như bên dưới:

    SwiftGen yml file in Project navigator

    Lưu ý: Bạn có thể di chuyển tệp này đến vị trí giống như hình minh họa, nếu Xcode không thêm nó theo cách tương tự.

    Tệp này là nơi bạn sẽ đặt các hướng dẫn cho SwiftGen biết tệp nào bạn muốn chuyển đổi thành mã được tạo. Loại tệp, YML , cho biết nó đang sử dụng YAML cho cú pháp của nó. Nếu bạn chưa sử dụng YAML trước đây, đây chỉ đơn giản là một cách dễ đọc hơn để xem dữ liệu được tuần tự hóa. Bạn có thể nghĩ về nó như là JSON , được đơn giản hóa.

    Bây giờ, thay thế toàn bộ nội dung của swiftgen.yml bằng nội dung sau:

    # 1 
    input_dir:  DrinksUp! / 
    # 2 
    output_dir:  DrinksUp! / Generated /
    

    Đây là những gì bạn đã thêm:

    1. Bạn đã khai báo một biến input_dir thư mục đầu vào. Điều này cho SwiftGen biết thư mục gốc để điều hướng đến tất cả các đường dẫn tệp mà bạn sẽ sớm thêm vào.
    2. Một biến khác xác định thư mục đầu ra của các tệp Swift được tạo. Bằng cách này, bạn sẽ dễ dàng theo dõi tất cả các tệp SwiftGen hơn.

    Thêm Build Phase

    Để chạy SwiftGen, bạn sẽ cần thêm một giai đoạn xây dựng mới vào dự án của mình. Để thực hiện việc này, hãy chọn dự án của bạn trong trình điều hướng Dự án, chọn Build Phases . Chọn + và chọn New Run Script Phase.

    Add new run script to project

    Đổi tên script thành SwiftGen bằng cách nhấp đúp vào tên hiện tại, Run Script . Tiếp theo, thêm phần sau vào trường văn bản của tập lệnh:

    if [[ -f "${PODS_ROOT}/SwiftGen/bin/swiftgen" ]]; then
      "${PODS_ROOT}/SwiftGen/bin/swiftgen"
    else
      echo "warning: SwiftGen is not installed. Run 'pod install --repo-update' to install it."
    fi 
    

    Cuối cùng, sắp xếp lại thứ tự tập lệnh để ngồi ngay sau tên tập lệnh [CP] Check Pods Manifest.lock . Các Build Phases của bạn bây giờ sẽ giống như sau:

    SwiftGen script added and ordered

    Build và Run. Nếu mọi thứ được thiết lập đúng cách, bạn sẽ không có bất kỳ lỗi nào. Bạn sẽ không có bất kỳ thứ gì trong thư mục Đã tạo của mình. Được rồi chúng ta đến bước tiếp theo.

    Chuyển đổi XCAssets

    Bây giờ, bạn đã sẵn sàng để bắt đầu xóa các chuỗi khỏi dự án của mình! Bước đầu tiên sẽ là SwiftGen tạo mã cho các tệp XCAsset trong dự án. Mở swiftgen.yml và thêm phần sau vào cuối tệp:

    ## XCAssets
    # 1
    xcassets:
      # 2
      inputs:
        - Assets.xcassets
        - Colors.xcassets
      # 3
      outputs:
        # 4
        templateName: swift5
        # 5
        output: XCAssets+Generated.swift
    

    Đây là ý nghĩa của mỗi dòng trong số những dòng này:

    1. Mỗi loại tệp hoặc mẫu, bạn muốn chuyển đổi bằng SwiftGen yêu cầu một mục nhập ở cấp gốc là swiftgen.yml . Ở đây, điều này cho biết bạn muốn SwiftGen chuyển đổi các tệp là XCAsset .
    2. Danh sách này cho biết những tệp SwiftGen nên giới hạn chuyển đổi của nó.
    3. Bạn cần cho SwiftGen biết cách tạo đầu ra.
    4. Bạn phải cung cấp tên mẫu. Ở đây, swift5 là một mẫu mặc định được cung cấp bởi nhóm SwiftGen. Bạn sẽ học cách sử dụng các mẫu của riêng mình sau này.
    5. Cuối cùng, bạn cung cấp tên tệp mà bạn muốn mã Swift mới của mình tạo ra. Hãy nhớ rằng, bạn đã xác định output_dir ở đầu tệp, có nghĩa là nó sẽ xuất thành Generated / XCAssets + Generated.swift .

    Build và Run. Nếu bạn không gặp bất kỳ lỗi nào, thì quá trình tạo mã của bạn đã hoạt động!

    Thêm tệp

    Mở rộng folder Generated trong trình Project navigator. Hiện tại, bạn vẫn sẽ không tìm thấy tệp mới của mình. Để thêm nó, nhấp chuột phải vào Generated và chọn Add Files to “DrinksUp!”… .

    Add generated file to project

    Chọn XCAssets + Generated.swift . Đảm bảo Copy items if needed không được chọn, sau đó nhấp vào Add . Bây giờ, hãy mở XCAssets + Generate.swift và quan sát. Bạn sẽ thấy enum Asset. Trong enum, bạn sẽ tìm thấy các bảng liệt kê khác được xác định phù hợp với các danh mục XCAsset mà bạn đã xác định. Ví dụ:

    • Assets: Mỗi hình ảnh trong dự án bây giờ có một thuộc tính tĩnh được xác định.
    • Colors: Tất cả các màu của ứng dụng cũng có các thuộc tính tĩnh để tham khảo.

    Mở Assets.xcassets . Lưu ý rằng có một nhóm hình ảnh có tên là nội dung khởi chạy . Nhưng Assets đã khai báo tất cả các thuộc tính ảnh tĩnh ở cùng một mức. SwiftGen có thể duy trì tổ chức này cho bạn nhưng không làm như vậy theo mặc định. Mở swiftgen.yml và thay thế toàn bộ mục nhập xcassets bằng mục sau:

    ## XCAssets
    xcassets:
      inputs:
        - Assets.xcassets
        - Colors.xcassets
      outputs:
        templateName: swift5
        # 1
        params:
           # 2
           forceProvidesNamespaces: true
           # 3
           forceFileNameEnum: true
        output: XCAssets+Generated.swift

    Tại đây, bạn tận dụng khả năng của SwiftGen để tùy chỉnh đầu ra mã của bạn bằng cách thực hiện như sau:

    1. Xác định params trên outputs của bạn.
    2. forceProvidesNamespaces: Điều này sẽ duy trì không gian tên của bạn được tìm thấy trong danh mục nội dung.
    3. Tham số bổ sung này đảm bảo cho dù bạn đã cung cấp bao nhiêu tên tệp inputs, SwiftGen sẽ duy trì các bảng liệt kê riêng biệt để đại diện cho từng danh mục nội dung.

    Xây dựng dự án, sau đó quay lại XCAssets + Generated.swift . Bây giờ bạn sẽ thấy Assets có một enum tên mới LaunchAssets để đại diện cho cấu trúc thư mục của bạn.

    Bây giờ, đã đến lúc sử dụng mã mới được tạo này để xóa bất kỳ tham chiếu chuỗi nào đến hình ảnh. Mở DrinksListView.swift . Bạn sẽ thấy Image("milkshake")bên trong các mục trên thanh công cụ được thêm vào chế độ xem. Thay thế dòng bằng dòng sau:

    Image(Asset.Assets.milkshake.name)

    Ở đây, bạn đã tham chiếu tên hình ảnh cho milkshake. Hiện tại, SwiftGen không hỗ trợ làm việc trực tiếp với SwiftUI. Bạn sẽ học cách tự thêm cái này sau. Hiện tại, bạn vẫn có thể sử dụng những gì có sẵn để tải nội dung hình ảnh mà không cần tham chiếu trực tiếp đến chuỗi.

    Sử dụng các mẫu cơ bản bổ sung

    Có một số mẫu bổ sung mà bạn có thể tận dụng mà không cần tùy chỉnh.

    Làm việc với trình tạo giao diện

    Ứng dụng bạn đang làm việc đang sử dụng SwiftUI. Tuy nhiên, để giới thiệu khả năng của SwiftGen để làm việc với Trình tạo giao diện, dự án mẫu bao gồm một bảng phân cảnh và một số bộ điều khiển chế độ xem. Bắt đầu bằng cách tạo mã để hỗ trợ Trình tạo giao diện hoặc Bảng phân cảnh bằng cách thêm phần sau vào swiftgen.yml :

    ## Interface Builder
    ib:
      inputs:
        # 1
        - .
      outputs:
        # 2
        - templateName: scenes-swift5
          output: IB-Scenes+Generated.swift
        # 3
        - templateName: segues-swift5
          output: IB-Segues+Generated.swift
    

    Tại đây, bạn đã làm như sau:

    1. Cho SwiftGen biết rằng bạn muốn nó tìm kiếm bất kỳ tệp nào được hỗ trợ trình tạo giao diện trong thư mục gốc của dự án của bạn.
    2. Một điều tuyệt vời về SwiftGen là nó tách biệt khái niệm Cảnh khỏi Segues . Điều này cho biết tệp mà cảnh của bạn sẽ xuất ra.
    3. Cuối cùng, điều này cho biết tất cả thông tin giả mạo sẽ được xuất ra ở đâu.

    Xây dựng dự án của bạn. Thêm IB-Scenes + Generate.swift và IB-Segues + Generated.swift vào nhóm Generated , giống như bạn đã làm đối với XCAssets + Generated.swift .

    Giờ đây, bạn có thể thay thế thông tin cảnh hoặc thông tin xác thực trong ứng dụng.

    Bắt đầu bằng cách mở InformationViewController.swift . Trong InformationViewController, thay thế việc triển khai showAbout()bằng những điều sau:

    performSegue(
      withIdentifier: StoryboardSegue.Main.showAbout.rawValue, 
      sender: self)

    Tất cả các segues sẽ tạo ra dưới dạng các enum trường hợp thực tế, điều này giúp mọi thứ dễ dàng hơn khi bạn kiểm tra xem segue nào được kích hoạt. Vì đơn giản, mã bạn đã thêm chỉ đang kích hoạt segue.

    Tiếp theo, bên trong InformationView, thay thế makeUIViewController(context:)bằng những thứ sau:

    func makeUIViewController(context: Context) -> some UIViewController {
      StoryboardScene.Main.initialScene.instantiate()
    }
    

    Ở đây, bạn đã đơn giản hóa mã cần thiết để khởi tạo bộ điều khiển chế độ xem ban đầu của bảng phân cảnh. SwiftGen cung cấp một phương thức trợ giúp để nhanh chóng truy cập cảnh ban đầu của bạn.

    Xây dựng và chạy. Nhấn vào Tìm hiểu thêm ở trên cùng bên phải. Bạn sẽ thấy một phương thức xuất hiện, hiển thị cho bạn một số thông tin về ứng dụng. Điều này được gọi vào makeUIViewController(context:)để biết chế độ xem nào cần tải.

    Hiển thị màn hình Tìm hiểu thêm

    Bây giờ, hãy nhấn vào nút About . Bạn sẽ kích hoạt segue mà bạn đã sửa đổi.

    Đồ uốngUp!  Giới thiệu về màn hình

    Làm việc với JSON

    Cuối cùng chúng ta sẽ thêm phần hỗ trợ cho JSON. Thêm phần sau vào cuối swiftgen.yml :

    ## JSON
    json:
      inputs:
        - Resources/
      outputs:
        templateName: runtime-swift5
        output: JSON+Generated.swift
    

    Điều này hiện cung cấp một cách để tham chiếu đến bất kỳ tệp JSON nào bạn có trong tài nguyên của dự án. Bạn sẽ sử dụng điều này để chuyển đổi dữ liệu giả đi kèm với ứng dụng.

    Xây dựng dự án, sau đó thêm JSON + Generated.swift vào nhóm Đã tạo của bạn .

    Bây giờ, hãy mở Drink.swift . Loại bỏ hoàn toàn các struct tên được đặt tên MockData. Sau đó, thay thế phần mở rộng cho DrinkStore, được tìm thấy ở dưới cùng, bằng phần mở rộng sau:

    extension DrinkStore {
      static var mockData: [Drink] {
        do {
          let data = try JSONSerialization.data(
            withJSONObject: JSONFiles.starterDrinks,
            options: [])
          let mockDrinks = try JSONDecoder().decode([Drink].self, from: data)
          return mockDrinks
        } catch {
          print(error.localizedDescription)
          return []
        }
      }
    }
    

    Tại đây, bạn sẽ thấy các tham chiếu mã của mình JSONFiles.starterDrinks. Mở MockData.json và nhận thấy khóa đầu tiên trong tệp, có tên là starterDrinks . SwiftGen đã lấy đối tượng cấp cao nhất này và cung cấp nó dưới dạng thuộc tính tĩnh trên JSONFiles để bạn tham khảo khi cần.

    Xây dựng và chạy. Bạn sẽ không nhận thấy bất cứ điều gì khác so với trước đây – chỉ là đồ uống hiển thị trong danh sách.

    Thức uống

    Làm việc với chuỗi

    Có lẽ một trong những tiện ích lớn nhất mà SwiftGen cung cấp là khả năng sử dụng các biến để tham chiếu các chuỗi được bản địa hóa(Localized Strings). Đó là một thực tiễn tuyệt vời để đặt bất kỳ văn bản nào bạn sẽ trình bày cho người dùng ứng dụng của mình bên trong các tệp strings hoặc tệp stringsdict được bản địa hóa . Nhưng nếu bạn đã làm điều này, bạn biết rằng một khi bạn vượt quá một số chuỗi, sẽ trở nên khó khăn để nhớ những chuỗi nào có sẵn. Cũng có cảm giác thừa rằng bạn có một chuỗi trong tệp chuỗi và… một chuỗi trong mã của bạn.

    Dự án này chứa các tệp chuỗi sau:

    • Localizable.strings : Tệp chuỗi run of the-mill của bạn, được tạo bằng các khóa và giá trị.
    • Localizable.stringsdict : Bạn nên sử dụng các tệp stringdict bất cứ khi nào bạn cần lo lắng về các chuỗi đa nguyên. Loại tệp này không chỉ hỗ trợ dịch các chuỗi mà còn hỗ trợ cách đa hóa các từ cho bất kỳ biến thể nào mà một ngôn ngữ yêu cầu.

    Để chuyển đổi tất cả các tệp chuỗi của bạn, hãy thêm phần sau vào swiftgen.yml :

    ## Strings
    strings:
      inputs:
        # 1
        - en.lproj
      outputs:
        - templateName: structured-swift5
          # 2
          params:
            publicAccess: true
          output: Strings+Generated.swift
    

    Bạn nên biết một số điều quan trọng về những gì bạn đã thêm ở đây:

    1. Khi bạn chuyển đổi các tệp chuỗi của mình, bạn chỉ nên sử dụng một trong các thư mục được bản địa hóa. Đối với mỗi ngôn ngữ được thêm vào, một thư mục bản địa hóa mới sẽ được tạo. Trong dự án này, bản địa hóa duy nhất là tiếng Anh. Nếu bạn thêm nhiều ngôn ngữ hơn, không cần phải sửa đổi mục nhập này để nhận các bản dịch bổ sung đó. Bởi vì tệp chuỗi phải có một tập hợp các khóa và giá trị phù hợp, bạn sẽ tham chiếu các bản dịch giống như bạn sẽ làm nếu bạn không sử dụng SwiftGen.
    2. Bạn đã thêm một tham số mới, được đặt tên publicAccess. Nếu bạn nhìn xung quanh bất kỳ tệp nào đã tạo mà bạn đã thêm, bạn sẽ thấy tất cả các loại đều có công cụ sửa đổi quyền truy cập internal. Bằng cách sử dụng tham số này, bạn có thể thay đổi công cụ sửa đổi quyền truy cập của các bảng kê đã tạo thành công khai.

    Build dự án, sau đó thêm Strings + Generated.swift vào nhóm Generated . Trước khi bạn chuyển đổi tất cả các chuỗi trong ứng dụng, điều quan trọng là phải hiểu tệp này hơi khác một chút như thế nào.

    Hiểu tệp

    Mở Strings + Generated.swift . Bạn sẽ thấy kiểu cha được tạo, được đặt tên L10n. Trong phần này enum, bạn sẽ thấy một số kiểu con được tạo bên trong nó DrinkDetailNavigationvà DrinkList. Chúng tương ứng với các chuỗi được khai báo trong Localizable.strings .

    Mở Localizable.strings và xem cách nó khai báo mục nhập đầu tiên:

    "DrinkList.Navigation.Title" = "Drinks";

    Lưu ý cách khai báo khóa bằng ký hiệu không gian tên bằng dấu chấm:

    • DrinkList : Điều này cho biết chuỗi này thuộc về màn hình Danh sách đồ uống.
    • Navigation : Cho biết chuỗi này sẽ được sử dụng trong thanh điều hướng.
    • Title : Cuối cùng, điều này cho biết đó là tiêu đề trong thanh điều hướng.

    Bây giờ, bạn có thể chọn tổ chức và đặt tên cho các chuỗi của mình theo cách khác. Không có gì sai với điều đó – kiểu đặt tên này được sử dụng để hiển thị cách SwiftGen sẽ chuyển đổi và tổ chức mã của bạn. Đối với mỗi khoảng thời gian bạn đặt trong chuỗi của mình, SwiftGen sẽ tạo thêm một kiểu con.

    Quay lại Strings + Generated.swift , bạn sẽ thấy hàm tĩnh drinksCount. SwiftGen giúp bạn dễ dàng làm việc với các chuỗi đa nguyên. Thay vì phải tạo tham chiếu đến các chuỗi được bản địa hóa và sử dụng trình định dạng chuỗi, các hàm được tạo này giúp bạn dễ dàng sử dụng một hàm lấy các giá trị của chuỗi đa nguyên của bạn.

    Bây giờ, chuyển đổi tất cả các chuỗi bản địa hóa được sử dụng trong ứng dụng để trỏ đến các loại được tạo. Bắt đầu bằng cách mở DrinksListView.swift . Tiếp theo, tìm dòng mã:

    Text("DrinkList.Navigation.Title")

    Đổi nó thành

    Text(L10n.DrinkList.Navigation.title)

    Chờ một chút… L10n là gì? Đây là một phím tắt cho “bản địa hóa”. Bạn cũng có thể thấy “quốc tế hóa” được viết tắt là i18n . Nếu bạn đếm các chữ cái giữa chữ cái đầu tiên và chữ “n” cuối cùng trong một trong hai từ, bạn sẽ tìm thấy 10 hoặc 18 chữ cái tương ứng. Mặc dù điều này có ý nghĩa, sẽ không tốt nếu sử dụng một tên khác cho loại chuỗi cấp cao nhất của bạn phải không?

    Mở swiftgen.yml và thêm một thuộc tính vào mục nhập chuỗi của bạn, ngay sau publicAccessđó, nó trông giống như sau:

    strings:
      inputs:
        - en.lproj
      outputs:
        - templateName: structured-swift5
          params:
            publicAccess: true
            enumName: Strings
          output: Strings+Generated.swift

    Ở đây, bạn đã thêm tham số enumName. Điều này cho phép bạn thay đổi loại từ “L10n” thành “Strings”.

    Xây dựng và chạy. Lần này, bạn sẽ có một lỗi biên dịch. Điều này là do loại L10n không còn nữa. Truy cập DrinksListView.swift và tìm:

    Text(L10n.DrinkList.Navigation.title)

    Thay thế nó bằng

    Text(Strings.DrinkList.Navigation.title)

    Bây giờ, ứng dụng của bạn đang sử dụng tên loại mới mà bạn đã cung cấp ở bước trước.

    Build ứng dụng của bạn. Bạn sẽ không còn gặp bất kỳ lỗi biên dịch nào nữa.

    Lưu ý : Nếu bạn vẫn gặp lỗi, bạn có thể cần phải làm sạch dự án của mình. Chọn Product ▸ Clean and build folders , sau đó tạo lại.

    Tiếp theo tìm tài sản drinkCountString. Đây là mã sử dụng tệp stringdict để xử lý cách hiển thị số lượng đồ uống trong danh sách. Thay thế nó bằng những thứ sau:

    private  var drinkCountString: String {
       Strings .drinksCount (drinkStore.drinks.count)
    }

    Nếu bạn so sánh nó với mã ở đó trước đây, bạn có thể thấy đây là cách nhanh hơn nhiều để tham chiếu các chuỗi đa nguyên.

    Bạn nên chuyển đổi tất cả các chuỗi trong dự án khỏi sử dụng chuỗi. Mở Localizable.strings và xem tất cả các khóa chuỗi. Bạn nên tìm từng cách sử dụng các khóa này trong một tệp Swift và hoán đổi nó cho các biến được tạo bởi SwiftGen.

    Khi bạn hoán đổi văn bản xếp hạng trong DrinkDetailView.swift , nó sẽ sử dụng một hàm để cung cấp chuỗi, giống như cách bạn xử lý số lượng đồ uống.

    Tạo mẫu tùy chỉnh

    Cho đến thời điểm này, tệp swiftgen.yml của bạn đã sử dụng các mẫu mặc định do SwiftGen cung cấp. Tất cả những thứ này được lưu trữ trong SwiftGen pod. Nếu các mẫu này không cung cấp đầy đủ chức năng bạn muốn, bạn có thể tạo các mẫu của riêng mình để tạo mã theo cách bạn muốn. Các mẫu được xây dựng bằng Stencil , một dự án mã nguồn mở cung cấp ngôn ngữ tạo mẫu cho Swift. Trong phần này, bạn sẽ học cách sửa đổi các mẫu hiện có và sử dụng chúng để tạo mã của bạn.

    Nếu bạn nhìn trong trình điều hướng Dự án, bạn sẽ thấy có một thư mục có tên là Mẫu . Trong đó, có hai thư mục con: Fonts và xcassets . Với những điều này, bạn sẽ được SwiftGen cung cấp hỗ trợ để sử dụng màu sắc và phông chữ trực tiếp trong SwiftUI.

    Hỗ trợ màu SwiftUI

    Để thêm Colorhỗ trợ SwiftUI, hãy mở asset_swift5_swiftui.stencil . Lúc đầu, mọi thứ có thể hơi choáng ngợp vì tệp không có bất kỳ hỗ trợ cú pháp mã nào.

    Trên dòng 13, thêm dòng mã sau:

    import SwiftUI

    Tiếp theo, quay lại swiftgen.yml . Trong mục nhập đầu tiên của bạn, đối với xcassets , hãy tìm dòng nơi bạn xác định tên mẫu:

    templateName:  swift5

    Bây giờ, hãy thay thế nó như sau:

    templatePath:  Templates / xcassets / asset_swift5_swiftui.stencil

    Tại đây, bạn đã thay đổi từ sử dụng templateNamesang templatePath. Điều này yêu cầu SwiftGen sử dụng mẫu tùy chỉnh của bạn thay vì mẫu tích hợp sẵn.

    Xây dựng dự án, sau đó truy cập XCAssets + Generated.swift . Ở trên cùng, bây giờ bạn sẽ thấy:

    import SwiftUI

    Vì bạn đã thêm nhập vào tệp Stencil nên khi mã được tạo, mã sẽ chọn thay đổi này và thêm nhập mới. Khá tuyệt, phải không?

    Mở asset_swift5_swiftui.stencil và thay thế:

    // Add Support For SwiftUI Here

    Như dưới đây:

    {{accessModifier}} private(set) lazy var color: Color = {
      Color(systemColor)
    }()

    Trong đoạn mã này, bạn có thể xem cách sử dụng Stencil nhiều hơn một chút. Đây là những gì bạn đã thêm:

    • {{accessModifier}}: Đây là cách bạn nói với Stencil cách thay thế bằng thứ gì đó được cung cấp trong quá trình tạo mã. Nếu bạn nhìn vào dòng 11 của tệp này, bạn sẽ thấy accessModifier được định nghĩa là một biến. Theo mặc định, công cụ sửa đổi quyền truy cập là internal. Nếu bạn nhớ từ trước, bạn đã thấy cách bạn có thể thay đổi điều này thànhpublic
    • Phần còn lại của điều này thực sự chỉ là mã tiêu chuẩn. Nó tạo ra màu SwiftUI từ màu UIKit.

    Lưu ý : Có một sửa đổi khác được thực hiện đối với mẫu như một phần của vật liệu khởi động. Nó thay đổi loại Colorthành SystemColor.

    Build lại dự án, sau đó quay lại XCAssets + Generated.swift . Trên dòng 62, bạn sẽ thấy mã từ mẫu của mình, hiện được tạo dưới dạng mã thực.

    Bây giờ, bạn cần hoán đổi bất kỳ tham chiếu được mã hóa cứng nào thành một màu để sử dụng chức năng mới của mình. Mở DrinksListView.swift và tìm vị trí đặt màu nền trước trên văn bản tiêu đề điều hướng. Thay thế nó như sau:

    Text(Strings.DrinkList.Navigation.title)
      .font(Font.custom("NotoSans-Bold", size: 17, relativeTo: .body))
      .foregroundColor(Asset.Colors.textColor.color)

    Tại đây, bạn đã chuyển từ sử dụng chuỗi mã hóa cứng, với sự hỗ trợ SwiftUI thực sự để tham chiếu màu trực tiếp.

    Bạn cũng có thể sử dụng màu trực tiếp trong UIKit. Mở AppMain.swift . Thay đổi dòng mã sau:

    appearance.backgroundColor = UIColor(named: "header")

    Thành đoạn code dưới đây:

    appearance.backgroundColor = Asset.Colors.header.systemColor

    Đây systemColor là tham chiếu đến loại màu cụ thể của nền tảng. Sẽ như UIColor vậy nếu bạn đang sử dụng iOS và NSColor nếu bạn đang sử dụng macOS.

    Hai tệp có màu được khai báo bằng cách sử dụng chuỗi:

    • AppMain.swift
    • DrinkListView.swift

    Hoàn tất chuyển đổi các màu còn lại từ việc sử dụng các chuỗi trong mỗi tệp này theo ví dụ trên.

    Hỗ trợ Phông chữ SwiftUI

    Cũng như SwiftUI Color hiện không được hỗ trợ trong SwiftGen, Font cũng không được hỗ trợ. Bắt đầu bằng cách mở font_swift5_swiftui.stencil và thêm nhập sau vào đầu tệp:

    import SwiftUI

    Tiếp theo, thay thế khối mã này được tìm thấy gần cuối tệp:

    // Add Support For SwiftUI here
    fileprivate extension Font {
    }

    Bằng đoạn code dưới đây:

    fileprivate extension Font {
      // 1
      static func mappedFont(_ name: String, textStyle: TextStyle) -> Font {
        let fontStyle = mapToUIFontTextStyle(textStyle)
        let fontSize = UIFont.preferredFont(forTextStyle: fontStyle).pointSize
        return Font.custom(name, size: fontSize, relativeTo: textStyle)
      }
    
      // 2
      static func mapToUIFontTextStyle(
        _ textStyle: SwiftUI.Font.TextStyle
      ) -> UIFont.TextStyle {
        switch textStyle {
        case .largeTitle:
          return .largeTitle
        case .title:
          return .title1
        case .title2:
          return .title2
        case .title3:
          return .title3
        case .headline:
          return .headline
        case .subheadline:
          return .subheadline
        case .callout:
          return .callout
        case .body:
          return .body
        case .caption:
          return .caption1
        case .caption2:
          return .caption2
        case .footnote:
          return .footnote
        @unknown default:
          fatalError("Missing a TextStyle mapping")
        }
      }
    }

    Đây là những gì bạn đã thêm:

    1. mappedFont(_:textStyle:)tạo một phông chữ tùy chỉnh từ tên và TextStyle. Kiểu được sử dụng để lấy kích thước phông chữ tiêu chuẩn, mặc định cho mỗi phông chữ.
    2. mapToUIFontTextStyle(_:)chỉ đơn giản là cung cấp ánh xạ 1: 1 của SwiftUI TextStyletới UIKitTextStyle

    Tiếp theo, tìm comment này ở giữa tệp:

    // Add Support For SwiftUI Here

    Thay thế nó bằng đoạn code dưới đây:

    {{accessModifier}} func textStyle(_ textStyle: Font.TextStyle) -> Font {
      Font.mappedFont(name, textStyle: textStyle)
    }

    Khối mã này tương tự như những gì bạn đã thêm để cung cấp Color hỗ trợ. Sự khác biệt duy nhất ở đây là nó dành riêng cho việc cung cấp phông chữ trực tiếp trong SwiftUI.

    Bây giờ, mở swiftgen.yml và thêm mục sau:

    ## Fonts
    fonts:
      inputs:
        - Resources/Noto_Sans
      outputs:
        templatePath: Templates/fonts/fonts_swift5_swiftui.stencil
        output: Fonts+Generated.swift
    

    Ứng dụng này sử dụng hai phông chữ, cả hai đều nằm trong nhóm Tài nguyên :

    1. NotoSans
    2. NotoSans-Bold

    Mục nhập mới này chỉ cần biết thư mục mẹ của phông chữ bạn muốn hỗ trợ ở đâu. Mọi thứ khác tương tự như tất cả các mục nhập khác mà bạn đã thêm trước đó.

    Xây dựng ứng dụng của bạn và thêm Fonts+Generated.swift vào Generated . Sau khi thực hiện, hãy mở Fonts+Generated.swift . Tại đây, bạn có thể thấy cách tổ chức họ phông chữ. Bạn sẽ thấy rằng NotoSans có sẵn các biến thể sau:

    • Thường
    • In đậm
    • In đậm nghiêng
    • In nghiêng

    Giống như tất cả các mã được tạo khác trong ứng dụng, nó khá dễ sử dụng. Mở AppMain.swift và thay thế dòng đầu tiên application(_:didFinishLaunchingWithOptions:)bằng dòng sau:

    let buttonFont = FontFamily.NotoSans.bold.font(size: 16)

    Tại đây, bạn đặt trực tiếp kích thước phông chữ thành phông chữ NotoSans Bold(in đậm) .

    Tiếp theo, truy cập DrinksListView.swift , tìm tham chiếu đầu tiên đến một phông chữ, trong NavigationLink và thay thế nó bằng như sau:

    Text(drinkStore.drinks[index].name)
      .font(FontFamily.NotoSans.bold.textStyle(.body))

    Tại đây, bạn tận dụng mã của mẫu tùy chỉnh của mình để có thể tạo phông chữ SwiftUI tùy chỉnh, kích thước phù hợp với mặc định TextStyle: trong trường hợp này body.

    Cuối cùng, hoàn tất việc chuyển đổi tất cả các cách sử dụng của phông chữ trong toàn bộ ứng dụng. Trong cả DrinksListView.swift và DrinkDetailView.swift , bạn sẽ tìm thấy một số nơi đặt phông chữ. Theo ví dụ trên, bạn có thể chuyển đổi mã từ việc sử dụng một chuỗi sang trọng số thích hợp của NotoSans . Mỗi vị trí trong số này đã cho biết TextStylechúng nên có vị trí nào.

    Build và Run. Ứng dụng của bạn trông vẫn giống như cách nó hoạt động khi bạn bắt đầu. Nhưng bây giờ bạn sẽ có tất cả các tài nguyên được tham chiếu theo cách an toàn về kiểu loại!

    Thức uống

    Tổng kết

    Bây giờ bạn có thể sử dụng SwiftGen để:

    • Loại bỏ nhu cầu sử dụng chuỗi để tham chiếu tài nguyên trong ứng dụng của bạn, cho dù bạn sử dụng SwiftUI hay UIKit.
    • Tùy chỉnh các tệp đầu ra bằng cách sử dụng các thông số cài sẵn.
    • Sử dụng các mẫu của riêng bạn để tạo mã.

    Để tìm hiểu thêm về nó, hãy xem SwiftGen trên GitHub . Bạn cũng có thể tìm hiểu thêm về Stencil trên GitHub .

    Chúng tôi hy vọng bạn thích hướng dẫn này. Nếu bạn có bất kỳ câu hỏi hoặc ý kiến ​​nào, hãy tham gia thảo luận của diễn đàn bên dưới!

    Nội dung bài viết được dịch từ link: https://www.raywenderlich.com/23709326-swiftgen-tutorial-for-ios

  • HƯỚNG DẪN THỰC HIỆN ỨNG DỤNG MOBILE HỖ TRỢ ĐA NGÔN NGỮ

    HƯỚNG DẪN THỰC HIỆN ỨNG DỤNG MOBILE HỖ TRỢ ĐA NGÔN NGỮ

    Trong thời đại công nghệ 4.0, các công ty đua nhau chuyển đổi số, vì vậy có rất nhiều những ứng dụng di động được phát triển để giúp tiếp cận người dùng một cách dễ dàng hơn. Để những ứng dụng có thể vươn xa ra tầm thế giới, tiếp cận được với những người dùng nước ngoài, thì ứng dụng đó cần phải hỗ trợ đa ngôn ngữ. Vì vậy hôm nay mình sẽ hướng dẫn các bạn một số cách thực hiện một ứng dụng iOS hỗ trợ đa ngôn ngữ.

    Cài đặt dự án hỗ trợ đa ngôn ngữ

    Bước 1: Thực hiện thêm ngôn ngữ hỗ trợ bằng cách Chọn Project -> Info(thông tin) -> Bấm nút +

    Thêm ngôn ngữ
    Bỏ tích ở các file storyboard để xCode không gen ra các file String cho các files storyboard

    Bước 2: Tạo file String để chứa nội dung theo các ngôn ngữ New File… -> tìm string và chọn Strings File -> Next

    Bước 3: Localize file string vừa mới tạo Chọn File vừa tạo -> Bấm vào nút Localize… -> Popup hiển thị lên thì chọn Localize

    Bước 4: Tích vào ngôn ngữ bạn hỗ trợ, Chọn File String localize vừa được tạo -> Ở menu bên phải mục Localization tích vào những ngôn ngữ mà ứng dụng của bạn hỗ trợ.

    Chọn ngôn ngữ hỗ trợ

    Vậy là việc cài đặt đa ngôn ngữ cho ứng dụng của bạn đã hoàn thành.

    Thực hiện đa ngôn ngữ với các chuỗi (Localize String)

    1. Để tránh các lỗi sai chính tả và việc thực hiện đa ngôn ngữ trở nên dễ dàng hơn thì chúng ta sẽ tạo ra một enum Localization để liệt kê các item/string dưới dạng keyword để sử dụng như sau:
    
    enum Localization {
        static let helloWorld: String = "Hello"
        static let buttonChangeLanguageTitle: String = "btn.changeLanguage"
    }

    Với mỗi 1 key của string chúng ta cần tạo ra value tương ứng với nó ở trong file Localizable mà chúng ta đã tạo.

    Thêm key/value cho các file ngôn ngữ tương ứng:

    Thêm key/value cho ngôn ngữ Tiếng Anh
    Thêm key/value cho ngôn ngữ Tiếng Việt

    NOTE:
    – Kết thúc của dòng code phải là dấu chấm phẩy “;” nếu 1 dòng code bị thiếu nó sẽ khiến ứng dụng của bạn hiển thị không đúng ngôn ngữ.
    – Key phải trùng với giá trị của enum Localization

    2. Tạo một file chung để quản lí ngôn ngữ như đoạn code dưới đây

    // for manage language
    class LanguageManager {
        static let shared = LanguageManager()
        private init(){}
        
        // save language to UserDefault
        func changeLanguage(_ language: Language) {
            UserDefaults.standard.set(language.rawValue, forKey: "APP_LANGUAGE")
        }
        
        /// Get language of set on app, if nil use device language
        /// - Returns: current language of application
        func getAppLanguage() -> Language {
            if let language = UserDefaults.standard.value(forKey: "APP_LANGUAGE") as? String {
                return Language(rawValue: language) ?? .english
            } else {
                // default lan is english
                let currentLanguage: String = Locale.current.languageCode ?? Language.english.rawValue
                return Language(rawValue: currentLanguage) ?? .english
            }
        }
        
        // define enum language
        enum Language: String {
            case vietnamese = "vi"
            case english = "en"
        }
    }

    Ở đây mình tạo ra một file quản lí ngôn ngữ nhằm mục đích tập trung tất cả những tính năng liên quan tới ngôn ngữ: thay đổi, lấy ra ngôn ngữ …

    Chúng ta cần lưu ngôn ngữ của ứng dụng vào Local Storage để khi người dùng tắt ứng dụng vào lại thay đổi vẫn được áp dụng. Trong trường hợp này mình chọn cách dùng UserDefault vì những lí do sau:
    – Dễ sử dụng
    – Dữ liệu cần lưu không yêu cầu bảo mật
    – Dữ liệu lưu có dung lượng nhỏ

    3. Vậy là chúng ta đã tạo xong file quản lí ngôn ngữ. Tiếp đến để việc sử dụng localization dễ dàng chúng ta sẽ tạo ra một var localized trong extension String như sau:

    
    extension String {
        var localized: String {
            let currentLanguage = LanguageManager.shared.getAppLanguage().rawValue
            guard let bundlePath = Bundle.main.path(forResource: currentLanguage, ofType: "lproj"), let bundle = Bundle(path: bundlePath) else {
                return self
            }
            return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "")
        }
    }
    

    Sau khi tạo xong extension thì việc thực hiện code localized trở nên rất dễ dàng. Khi cần sử dụng chúng ta chỉ cần .localized là xong.

    4. Demo và cách sử dụng:

    Trong ví dụ này mình sẽ tạo ra 1 label hiển thị text và một nút để thay đổi ngôn ngữ của ứng dụng. Sau đó mình tạo ra một func setDataForUI() nhằm mục đích set tất cả các data liên quan tới localization ở đây. Nếu sau đó đổi ngôn ngữ ta chỉ cần gọi lại hàm này để thực hiện set lại.

        private func setDataForUI() {
            lbHello.text = Localization.helloWorld.localized
            btnChangeLanguage.setTitle(Localization.buttonChangeLanguageTitle.localized, for: .normal)
        }
    
        @IBAction func changeLanguage(_ sender: Any) {
            if LanguageManager.shared.getAppLanguage() == .english {
                LanguageManager.shared.changeLanguage(.vietnamese)
            } else {
                LanguageManager.shared.changeLanguage(.english)
            }
            setDataForUI()
        }

    Kết quả

    Vậy là chúng ta đã hoàn thành việc thực hiện localize String cho ứng dụng. Mình hi vọng bài viết này có thể giúp được chút gì đó cho các bạn. Cảm ơn các bạn đã đọc bài viết này, chúc các bạn thành công!

  • Adobe xD to SWIFT

    Adobe xD to SWIFT

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

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

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

    Drop shadow

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

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

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

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

    Border

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

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

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

    Line spacing

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

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

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

    Line spacing

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

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

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

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

  • WWDC21 – ARC in Swift: Basics and beyond

    WWDC21 – ARC in Swift: Basics and beyond

    ARC in Swift: Basics and beyond

    Trong Swift thì chúng ta nên sử dụng value type như là struct hay là enum.
    Tuy nhiên, có một số trường hợp bắt buộc phải dùng reference type như là class thì chúng ta phải hết sức cẩn thận khi sử dụng để tránh reference cycle.

    Reference type trong Swift được quản lý bộ nhớ thông qua ARC (Autoatic Reference Counting).
    Trong bài viết này, chúng ta sẽ cùng tìm hiểu ARC làm việc như nào trong các version sắp tới, và một vài điểm chú ý khi sử dụng reference type.

    Object’s lifetimes

    An object’s lifetime in Swift begins at initialization and ends at last use.
    ARC automatically manages memory, by deallocating an object after its lifetime ends.
    It determines an object’s lifetime by keeping track of its reference counts.
    ARC is mainly driven by the Swift compiler which inserts retain and release operations.
    At runtime, retain increments the reference count and release decrements it.
    When the reference count drops to zero, the object will be deallocated.
    

    Từ trước đến giờ chúng ta vẫn luôn hiểu rằng ARC sẽ release một vùng nhớ khi không còn con trỏ nào trỏ vào nó. Ví dụ những vùng nhớ được khai báo trong block code, như function, sẽ được release sau khi content của function này kết thúc.

    Tuy nhiên, trên thực tế, mọi thứ còn hơn thế. Trong một function thì Object’s life time được tính từ khi nó được khởi tạo cho đến lần cuối cùng nó được dùng. Sau đó, lifetimes của nó sẽ kết thúc, và nó sẽ được xóa đi.

    Ví dụ, với những function giả sử dài 100 LOC, và chúng ta có object myObject nào đó được sử dụng trong khoảng 30 LOC đầu, thì sau khi lần cuối cùng nó được dùng, nó sẽ bị release đi, tức là trong khoảng thời gian 70 LOC còn lại được thực thi, thì vùng nhớ của myObject đã không còn tồn tại.

    Tham khảo đoạn source code bên dưới:

    class Traveller {
        var name: String
        var destination: String?
        
        init(name: String) {
            self.name = name
        }
    }
    
    func test() {
        let traveller1 = Traveller(name: "NhatHM") // traveller1 Object lifetime begins
        let traveller2 = traveller1
        traveller2.destination = "WWDC21-10216" // traveller1 Object lifetime ends
        print("Done travelling \(traveller2.destination)")
    }
    

    Ở ví dụ này, ngay sau khi object traveller1 được gán cho traveller2 thì nó đã không còn được sử dụng nữa, cho nên, đó cũng chính là thời điểm kết thúc lifetime của object traveller. Và lúc này, traveller1 đã sẵn sàng để bị release.

    Việc release object traveller1 là do Swift compiler tự xử lý, và thời điểm release là do ARC thực hiện.

    Như hiện tại, với XCode13 thì đã có option để optimize object life time. Tức là, có thể những bug trước đây chưa xảy ra vì coding logic observer đến object life time, thì bây giờ hoàn toàn có thể xảy ra. Bản thân Apple cũng khuyến cáo là khi coding, tránh các logic mà phụ thuộc vào object life time quá nhiều, vì với mỗi version Swift mới thì Swift compiler và ARC optimization có thể thay đổi, dẫn đến những bug tiềm ẩn có thể xảy ra.

    Note: ở đa số các ngôn ngữ lập trình khác, thì object chỉ bị release khi kết thúc body của function.

    Obserable object lifetimes và vấn đề của nó

    Thông qua:

    • weakunowned reference
    • Deinitializer (deinit)

    Dùng weak và unowned không sai, tuy nhiên trong một số trường hợp, nó rất có thể gây ra bug tiềm ẩn (phụ thuộc vào việc ARC được optimize như nào trong các phiên bản tiếp theo).

    Because relying on observed object lifetimes may work today, but it is only a coincidence.
    
    Observed object lifetimes are an emergent property of the Swift compiler and can change as implementation details change.
    

    Xem ví dụ dưới (captured from Apple WWDC21 video)

    Ở ví dụ trên, ngay sau đoạn code traveler.account = account kết thúc thì lifetimes của traveler đã kết thúc, và với các version Swift compiler sau này thì có thể xảy ra bug, vì lúc này, traveler bị release, do đó reference count đến vùng nhớ của Traveler đã bị release, dẫn đến đoạn code trong func printSummary sẽ bị crash, nếu dùng if let để unwrap value ra thì bản thân function này cũng sẽ bị sai logic, vì lúc này logic mong muốn print ra thông tin của traveler đã không còn được thực hiện.

    Đương nhiên, hiện tại code như trên sẽ chưa thể bug ngay được, vì Swift compiler đang chưa optimize đến mức là vừa kết thúc lifetimes của object thì object sẽ bị release đi luôn. Tuy nhiên, với sections này thì Apple đang nhắc nhở chúng ta vì việc tương lai, chắc chắn là việc xử lý memory của object lifetimes sẽ được thực hiện mạnh tay hơn, có thể ngay sau dòng code cuối cùng mà object được gọi thì nó sẽ bị release, và như vậy, source code của chúng ta chưa bug ở thời điểm này, nhưng tương lai có thể sẽ bị bug.

    How to fix

    Trong video, Apple gợi ý 3 cách fix:

    • Sử dụng withExtendedLifetime
    • Thay đổi solution code, sử dụng strong reference.
    • Thay đổi cách code, tránh sử dụng weak/unowned

    Dùng withExtendedLifetime (không khuyến khích)

    Sử dụng strong references

    Thay đổi cách code tránh các object reference lẫn nhau (khuyến khích)

    Kết luận:

    • Object lifetimes không tồn tại suốt vòng đời của function, mà chỉ tồn tại từ khi khởi tạo object -> lần cuối cùng object được sử dụng.
    • ARC optimization có thể thay đổi sau mỗi version của Swift (và Swift compiler), do đó, việc implement source code phụ thuộc quá nhiều vào object lifetimes tiềm ẩn bug.

    Nguồn:

  • Swift: weak and unowned

    Swift: weak and unowned

       

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

       

    1-ARC

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

    Theo như doc của Apple:

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

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

    Ví dụ : 

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

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

       

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

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

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

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

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

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

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

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

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

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

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

     

    Weak và Unowned

     

    1. Weak

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

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

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

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

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

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

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

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

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

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

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

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

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

      2. unowned

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

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

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

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

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

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

    Triển khai trong code:

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

       

    3-Debugging

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

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

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

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

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

       

    4-Tổng kết

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

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

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

    HAPPY CODING!

    REFERENCE

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

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

  • Bắt đầu với RxSwift ( Phần 1)

    Bắt đầu với RxSwift ( Phần 1)

    I. Giới thiệu

    Một trong những điều quan trọng của lập trình hướng đối tượng (OOP) và hướng thủ tục đó là imperative (lập trình mệnh lệnh). Chúng ta cần sử dụng những câu lệnh để thay đổi trạng thái của chương trình.

    Vậy câu hỏi đặt ra làm sao để trạng thái của chương trình có thể thay đổi một cách tự động, liệu ngôn ngữ, khái niệm lập trình nào làm được việc đó không ?

    → Câu trả lời đó là reactive programming.

    Khi mà ứng dụng của bạn phản ứng lại với những thay đổi của data, reactive programming sẽ giúp chúng ta thực hiện điều đó. Nó giúp chúng ta tâp trung vào việc xử lí logic và không cần quan tâm tới việc thay đổi giữa các trạng thái (state) với nhau.

    Chúng ta có thấy trong swift sử dụng KVO và didSet để thiết lập cho lập trình phản ứng (reactive), nhưng việc thiết lập cho dữ liệu lớn, hoặc bài toán phức tạp hơn khá rắc rối. Vậy nên chung ta sẽ dùng tới thư viện thứ 3 đó là RxSwift

    Note*: “KVO là một khái niệm chúng ta sẽ gặp nhiều khi sử dụng SwiftUI, các bạn có thể search thêm về khái niệm này nhé.”

    Thư viện RxSwift sẽ giúp chúng ta giải quyết vấn đề trên qua lập trình bất đồng bộ (asynchronous programming).

    Để không tốn thời gian của các bạn mình sẽ đi nhanh qua các thành phần mà RxSwift cung cấp

    II. Các thành thần (components) chính của RxSwift:

    • Observable và Observer
    • Subject
    • DisposeBag
    • Operators
    • Schedules

    Observable và Observer

    Có rất nhiều thuật ngữ để mô tả cho lập trình bất đồng bộ, ở đâu mình sẽ sử dụng thuật ngữ observebal và Observer luôn nhé.

    • Observer lắng nghe Observable.
    • Observable phát ra các items hoặc gửi các notifications đến các Observer bằng cách gọi các Observer methods.
    Khái niệm Observable đến từ observer design pattern là một đối tượng thông báo cho các đối tượng theo dõi về một điều gì đó đang diễn ra

    • Một Observer đăng ký lắng nghe một Observable, sau đó nó sẽ xử lý một item hoặc nhiều các item mà Observable phát ra.

    Chúng ta có thể đăng kí tới một Observable sequence thông qua subscribe(on:(Event<T>)->()).

    Trong RxSwift một sự kiện sẽ chỉ là một trong Enumeration Type với 3 trạng thái có thể xảy ra:

    .onNext(value: T) -> Observable  gọi hàm onNext  có tham số là item, item này là một trong các tập items của Observable

    .onError(error: Error) -> Được gọi khi Observable  kết thúc với một lỗi xảy ra trong quá trình chuyển đổi, xử lý dữ liệu.

    .onCompleted -> Observable  gọi hàm này sau khi hàm onNext  cuối cùng được gọi, nếu không có bất kì lỗi nào xảy ra.

    Ví dụ với code Swift: 

    let obj = Observable.from(["A", "B", "C", "D"]) // Khởi tạo một Observable
    obj.subscribe( // Thực hiện subscribe Observable
      onNext: { data in
        print(data) // Nơi nhận dữ liệu của Observer được gửi đi từ Observable
      }, 
      onError: { error in
        print(error) // Nơi nhận error và Observable được giải phóng
      }, 
      onCompleted: {
        print("Completed") // Nhận được sự kiện khi Observable hoàn thành và Observable được giải phóng
      })
       .disposed()
    

    Kết quả trả về:

    A

    B

    C

    D

    Competed

    Subject

    Một đối tượng vừa có thể là Observable vừa có thể là Observer được gọi là Subject.

    Trong RxSwift cung cấp cho chúng ta 4 subject khác nhau với cách thứ hoạt động khác nhau đó là:

    • PublishSubject: Khởi đầu “empty” và chỉ emit các element mới cho subscriber của nó.
    • BehaviorSubject: Khởi đầu với một giá trí khởi tạo và sẽ relay lại element cuối cùng của chuỗi cho Subscriber mới.
    • ReplaySubject: Khởi tạo với một kích thước bộ đệm cố định, sau đó sẽ lưu trữ các element gần nhất vào bộ đệm này và relay lại các element chứa trong bộ đệm cho một Subscriber mới.
    • BehaviourReplay (which was Variable): Lưu trữ một giá trị như một state và sẽ relay duy nhất giá trị cuối cùng cho Subscriber mới.

    Để đi sâu vào từng loại subject mà RxSwift cung cấp khá là dài, nên mình chỉ lướt qua. Các bạn có thể tìm hiểu thêm, hoặc chờ một bài viết viết của mình đi sâu phân tích các subject trên nhé!

    Ở phần 1 mình đã giới thiệu qua khái niệm Reactive Programing, và một số thành phần chính của nó là Observabel và Observer, Subject.

    Các phần tiếp theo mình sẽ gửi tới các bạn ở phần 2 nhé.

    Cảm ơn các bạn đã đọc bài viết của mình.

    CongPQ

  • TaskGroup Swift Part 2

    TaskGroup Swift Part 2

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

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

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

       

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

       

    1-Tổng quát

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

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

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

    – Throw error

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

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

       

    2-Throw error

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

       

    3-Trả về result

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

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

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

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

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

       

    4-Tổng kết

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

    Happy coding!!

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

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

    The most powerful tool we have as developers is automation.

    Scott Hanselman

    Overview

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Sam

    Overview

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

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

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

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

    Distribute IPA sử dụng Command Line

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

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

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

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

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

    ipa/cicd-test.ipa

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

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

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

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

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

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

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

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

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

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

    Copy API Key này nhé bạn

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Overview

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

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

    Distribute IPA sử dụng Command Line

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

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

    Setting Automatically trong Xcode

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

    xcodebuild build -scheme cicd-test

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

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

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

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

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

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

    Output của quá trình archive app

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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