Category: iOS

  • How to make a super app?

    How to make a super app?

    Ngày nay, các ứng dụng Super app ngày càng phổ biến. Đứng ở phía người dùng, super app là 1 app mà "thứ gì cũng có". Sự xuất hiện của Super app hướng đến sự tiện lợi dành cho người dùng khi mà họ có thể làm mọi thứ trên cùng 1 ứng dụng, từ việc thanh toán tiền điện, nước, đặt xe, đặt vé máy bay, đặt phòng, shopping, …

    Còn đứng ở trên góc nhìn của 1 developer thì sao? Làm thế nào để có thể đưa 1 app vào thành 1 ứng dụng nhỏ bên trong 1 app khác? Nếu đưa nhiều ứng dụng vào trong 1 super app như vậy, size của super app chắc chắn sẽ phải tăng lên rất nhiều. 1 app thông thường sẽ có size từ khoảng 50-150MB. Vậy tại sao 1 super app như shopee, grab, … có rất nhiều mini app lại chỉ có size 300MB?… Đó là 2 trong những câu hỏi mình đã từng băn khoăn về super app và chưa có câu trả lời. Sau 1 khoảng thời gian trực tiếp tham gia phát triển 1 super app, mình đã hiểu hơn về cách phát triển 1 super app.

    Nội dung

    • Super app app, mini app là gì?
    • Nguồn gốc của Super app?
    • Lợi ích của super app?
    • Tạo ra 1 super app?
    • Tổng kết
    • Reference

    Super app, mini app là gì?

    2 thuật ngữ super app và mini app đi song hành cùng với nhau.

    Super app là những app cung cấp nhiều services khác nhau, và những service này có thể được làm từ những team khác nhau.

    Mỗi service mà super app cung cấp được coi là 1 mini app (hay mini programs).

    Lợi ích của super app

    Đối với người dùng

    • 1 Ứng dụng cung cấp nhiều services cho người dùng => Người dùng có thể trải nghiệm nhiều loại dịch vụ, tiện ích ngay trên cùng 1 ứng dụng mà không cần phải chuyển app, hay download 1 app mới. Ví dụ: Zalo là 1 super app, gồm nhiều mini app như: Thanh toán tiền điện/nước, mua vé máy bay, nạp thẻ điện thoại, đặt xe, shoping, … => Người dùng không cần phải down thêm nhiều app mà chỉ cần 1 app là đủ.
    • Tiết kiệm dung lượng điện thoại vì không cần tải các app khác.

    Đối với công ty

    • Chia sẻ được lượng người dùng giữa các super app và mini app với nhau. Ví dụ: Zalo là 1 super app, chứa Lazada là 1 mini app nhỏ => Lazada có thể hưởng được lượng user hiện có của Zalo.
    • super app trở nên sinh động, nhiều tính năng hơn để cạnh tranh với các app khác.
    Công ty xây dựng 1 hệ sinh thái lớn mạnh, thu hút người dùng

    Super App và Mini App kết nối với nhau thế nào?

    Hãy thử suy nghĩ 1 vài câu hỏi sau đây trước khi đọc câu trả lời.

    Câu hỏi 1: Làm thế nào để SuperApp có thể mở được 1 MiniApp?
    Câu trả lời đó là source code của super app phải chứa code của mini app. Nhưng source code của mini app này sẽ được đóng gói dưới dạng framework.
    Team MiniApp sẽ build service mà họ muốn được tích hợp vào super app thành framework, và gửi cho team super app để team super app embed vào source code của họ. Các framework này có những yêu cầu sau:

    • Phải cung cấp 1 public function trả ra 1 View – view này là đầu vào của mini app.
    • Team MiniApp sẽ chỉ build phần service mà họ muốn đưa vào super app thành framework, chứ không build toàn bộ application của họ thành framework. Bởi nếu build toàn bộ app thành framework -> size của framework sẽ tăng lên đáng kể -> size của super app cũng sẽ tăng lên 1 tương đương. Vậy thì mỗi super app sẽ có size lên đến hàng nghìn MB chứ không còn là 300MB nữa.
      Ví dụ: Zalo muốn đưa Service Zalo Pay vào 1 super app khác, thì họ sẽ chỉ build service này thành framework, và đưa cho bên super app tích hợp, chứ ko phải build cả app Zalo thành 1 framework.

    Câu hỏi 2: Nếu mỗi mini app cung cấp 1 kiểu function có tên khác nhau, thì super app phải xử lí thế nào?
    Team SuperApp sẽ tạo ra 1 Interface / Protocol. Interface này sẽ là chuẩn chung cho toàn bộ MiniApp. Sau đó, team SuperApp sẽ đóng gói interface này thành 1 framework, và đưa cho phía mini app implement.

    Triển khai

    Step 1: Xây dựng 1 interface làm chuẩn chung cho các mini app, và dóng gói nó lại thành 1 framework và đưa cho mini app sử dụng.

    Step 2: Team MiniApp implement interface đó, và sau đó build source code thành framework và gửi cho team SuperApp.

    Step 3: SuperApp lấy ra input view mà team MiniApp đã cung cấp để hiển thị.

    Kết quả:

    Tổng kết:

    • Bài viết này chỉ xây dựng một demo đơn giản về xây dựng 1 super app để đem lại 1 cái nhìn tổng quan về cách thức super app và mini app kết nối với nhau.
    • Trên thực tế, để xây dựng 1 super app còn gặp nhiều khó khăn khác như: Authen giữa super app và mini app, thanh toán giữa super app và mini app, … Đó cũng là những bài toán hay cần phải giải quyết.

    Reference

    https://hoangatuan.medium.com/create-a-xcframework-for-ios-986c4fc1421e: Cách tạo framework
    https://www.brandsvietnam.com/congdong/topic/27240-WeChat-da-khoi-nguon-thuat-ngu-Super-App-nhu-the-nao: Lịch sử của super app

    Source code:

  • Hướng dẫn tạo Swift Package Manager

    Hướng dẫn tạo Swift Package Manager

    Tạo thư viện bằng Swift Package Manager trong Xcode

    Bạn có tò mò một bàn phím gợi ý suggest ra số tiền khi bạn nhập vào được code như thế nào không? Hôm nay mình sẽ hướng dẫn các bạn custom bàn phím gợi ý và public nó lên Swift Package nhé.

    I .Swift Package Manager là gì ?

    Swift Packager Manager là một công cụ giúp quản lý việc phân phối mã nguồn, giúp cho việc chia sẻ và dùng lại code được dễ dàng. Được Apple tích hợp từ Xcode 11 giúp cho việc chúng ta quản lí dependency một cách đơn giản hơn. Mình chỉ nói sơ qua còn các bạn tìm đọc thêm ở https://swift.org/package-manager nhé.

    II .Hướng dẫn tạo Swift Package Manager

    1. Tạo package suggest tiền Việt Nam Đầu tiên chúng ta mở Xcode ➞ File ➞ New ➞ Swift Package như hình: Image of article đặt tên của Package như trên là “VNDTextField” ở đây chúng ta đã tạo được Swift Package. Image of article Sau khi tạo xong, chúng ta vào file package và sẽ tiến hành config trong như sau:
    import PackageDescription
    
    let package = Package(
        name: "VNDTextField",
        platforms: [
                .iOS(.v10)            
            ],
        products: [
            // Products define the executables and libraries a package produces, and make them visible to other packages.
            .library(
                name: "VNDTextField",
                targets: ["VNDTextField"]),
        ],
        dependencies: [
            // Dependencies declare other packages that this package depends on.
            // .package(url: /* package url */, from: "1.0.0"),
        ],
        targets: [
            // Targets are the basic building blocks of a package. A target can define a module or a test suite.
            // Targets can depend on other targets in this package, and on products in packages this package depends on.
            .target(
                name: "VNDTextField",
                dependencies: []),
            .testTarget(
                name: "VNDTextFieldTests",
                dependencies: ["VNDTextField"]),
        ]
    )
    

    Ở đây chúng ta chú ý: 1.products đây là phần định nghĩa chúng ta có thể thấy các file của package khi dùng vào project khác

    products: [
            // Products define the executables and libraries a package produces, and make them visible to other packages.
            .library(
                name: "VNDTextField",
                targets: ["VNDTextField"]),
        ],
    

    2.platforms chúng ta thêm tham số vào cho hệ điều hành mình muốn build

    platforms: [
                .iOS(.v10)            
            ],
    

    ở đây mình chọn cho hệ điều hành từ iOS 10 Sau khi chỉnh sửa và setup file xong chúng ta tiến hành push code lên GitHub, và sau đó import lại cho project mà bạn muốn sử dụng. Đơn giản phải không nào !

    III. Sử dụng lại package vào project của bạn

    1. Đầu tiên chúng ta nhấn chuột vào fiel .xcodeproj -> chọn Swift Packager, chúng ta sẽ có như hình, ở đây mình đã thêm package custom ở trên vào project demo. Image of article

    2. Sau đó chúng ta nhấm vào dấu cộng(+) ở góc trái phía dưới để thêm package bạn muốn vào project, còn nếu muốn xóa package mà bạn đang sử dụng thì nhấn vào dấu trừ(-) nhé. Đây là lúc chúng ta bấm dấu + Image of article

    3. Ở đây bạn paste vào link package của bạn đã tạo trên github, chờ đợi trong vài giây chúng ta sẽ có kết quả như mình đây: Image of article

    4. Mình đã thêm package VNDTextField vào class của mình, và sử dụng lại nó như sau: Image of article

    5. Đây là class mình demo sử dụng lại VNDTextField. Chúng ta cần import lại package ở class mình muốn sử dụng, sau đó bạn có thể sử dụng thoải những gì package cung cấp. Sau khi sử dụng VNDTextField đây là kết quả mà mình đạt được. Image of article

    IV. Tổng kết

    Thật đơn giản, với Swift Packager bạn có thể tự custom cho mình những class common, tái sử dụng lại nhiều lần, giúp giảm chí phí build lại từ đầu, dynamic hơn. Đồng thời cũng dễ dàng cho bạn thoải sức custom và mong muốn nhiều người sử dụng lại dễ dàng. Từ nay việc sử dụng 1 third party hay class common đã trở nên dễ dàng hơn rất nhiều.

    Qua bài viết, hi vọng các bạn có cái nhìn cơ bản về cách tạo Swift Packager và sử dụng lại nó. Đồng thời cũng thêm vào bộ kĩ năng dev iOS của càng bạn trên hành trang fix bug. Hi vọng các bạn yêu thích bài viết này

    Mình có tạo 1 Package demo tại đây: https://github.com/ios-lib/VNDTextField

    Authors

    [email protected]

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

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

    Overview

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

    Debugging Process

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

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

    Debugger Tools

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

    Debug Navigator

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

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

    Breakpoint

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

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

    Source Editor

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

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

    Debug Bar

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

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

    Variables View

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

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

    Console & LLBD

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

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

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

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

    Authors

    MinhNN44

  • iOS Home Screen Action

    iOS Home Screen Action

    Overview

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

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

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

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

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

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

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

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

    Home Screen Static Action

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

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

    Trong đó:

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

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

    Home Screen Dynamic Action

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

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

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

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

    Action Handle

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

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

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

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

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

    Authors

    MinhNN44

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

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

    Overview

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

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

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

    1. App Store Connect

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

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

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

    2. Ad Hoc

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

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

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

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

    3. Enterprise

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

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

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

    4. Development

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

    Authors

    MinhNN44

  • 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