Tag: ios

  • 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

  • Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder Pattern trong iOS

    Design Pattern: Builder pattern trong iOS

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

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

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

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

    Nội dung

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

    Builder pattern là gì?

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

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

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

    Vấn đề

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

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

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

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

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

    Class diagram

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

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

    Demo

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

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

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

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

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

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

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

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

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

    Áp dụng trong iOS

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

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

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

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

    Tạo một builder và protocol

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

    Sử dụng

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

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

    Tổng kết

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

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

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

    Table of contents

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

    Đặt tên biến

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Đặt tên hàm

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

    Not Preferred

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

    Preferred

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

    Preferred

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

    Preferred

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

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

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

    Spacing

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Comment

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

    Access Control

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Self & Closure

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

    Not Preferred

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

    Preferred

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

    To be continue…

  • How To Create A Framework In Swift

    How To Create A Framework In Swift

    Updated for Swift 5

    Vì sao nên sử dụng framework

    • Việc sử dụng các tính chất hướng đối tượng trong lập trình rất phổ biến trong đó có tính chất kế thừa
    • Nhưng với việc swiftUI được ra đời và với SwiftUI thì View nó là struct, nên không thể kế thừa
    • Vậy có cách nào để thay thế được kế thừa trong swift?
    • Đối với riêng SwiftUI nói riêng hay việc sử dụng các framework iOS nói chung: Việc sử dụng tính chất kế thừa sẽ có những issue như swiftUI đang sử dụng View là struct ( struct k có các tính chất hướng đối tượng như class). Vâyviệc Customize 1 class để tái sử dụng như Class MainView: CustomizeView hay các phương thức như override hay class cha có các property gì thì class con cũng sẽ có các property đấy

    Tạo framework như thế nào?

    • Từ Xcode 11, Apple đã công bố tạo một framework được gọi là XCFramework
      • Mở Xcode, File > New > Project và chọn Framework

    • Triển khai mã code cho framwork tại đây
    • Tiếp theo sử dụng xcodebuild archive để tạo kho lưu trữ

      xcodebuild archive -scheme PROJECTNAME_HERE -destination=”iOS” -archivePath /tmp/xcf/ios.xcarchive -derivedDataPath /tmp/iphoneos -sdk iphoneos SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES

      xcodebuild archive -scheme PROJECTNAME_HERE -destination=”iOS Simulator” -archivePath /tmp/xcf/iossimulator.xcarchive -derivedDataPath /tmp/iphoneos -sdk iphonesimulator SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES destination: tùy chọn xác định nền tảng mục tiêu và các thiết bị.archivePath: đường dẫn tới thư mục cần tạo framework

    • Sau khi chạy 2 lệnh ở trên thành công
    • Tiếp theo sử dung câu lệnh này để đóng gói framwork

      xcodebuild -create-xcframework -framework /tmp/xcf/ios.xcarchive/Products/Library/Frameworks/PROJECTNAME_HERE.framework -framework /tmp/xcf/iossimulator.xcarchive/Products/Library/Frameworks/PROJECTNAME_HERE.framework -output FRAMEWORK_NAME.xcframework

      File đã được gen ra thành công:

    • Add framework đã tạo vào trong project cần dùng và để sang Embed & Sign 
    • Import framework vào class cần sử dụng

    (more…)

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

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

    Table of contents

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

    Magic number & Duplicate code

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

    Not Preferred

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

    Preferred

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

    Code Organization

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

    Not Preferred

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

    Preferred

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

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

    Scene Delegate

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

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

    Computed Property

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

    Preferred

    var diameter: Double {
      return radius * 2
    }
    

    Not Preferred

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

    Optional

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

    Preferred

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

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Multi-line String

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

    Preferred

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

    Not Preferred

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

    Bonus

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

    Preferred

    let swift = "not a scripting language"
    

    Not Preferred

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

    Preferred

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

    Not Preferred

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

    Preferred

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

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

    Preferred

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

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

    Preferred

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

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

    Not Preferred

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

    Reference

  • Method Swizzling in Swift

    Method Swizzling in Swift

    Table of Contents

    • Problems?
    • What is method swizziling?
    • Swizzling CocoaTouch class
    • Swizzling custom Swift class
    • Note
    • References

    Problems?

    If you meet one of these situations, how will you handle?

    • Firebase SDK only provides you a function named "showLoginView()" to present a LoginViewController. The problem is all of view controllers in your app use a custom background color? So how can we set background color for LoginViewController?
    • Firebase SDK saves value to UserDataDefault, but you expect that all keys must have a prefix, for example is "FPT". How can you do this?

    Method swizzling comes to rescue

    Defination

    So what is method swizzling?

    Method swizzling is the process of changing the implementation of an existing selector at runtime.

    Speak in a easy-to-understand way, method swizzling acts like swap(a, b) function. It will takes implementation of function 1 and function 2 and swap.

    Swizzling

    Use swizzling to solve the problem:

    So with method swizzling, we can change the implementation of viewDidLoad in LoginViewController to our custom implementation that calls change backgroundColor.

    Swizzling CocoaTouch class

    To swizzle, you just need to follow some steps:

    1. Create a new method with your custom implementation.
    2. Get default method selector reference.
    3. Get new method selector reference.
    4. Use objective-C runtime to switch the selectors.

    Let’s swizzle:

    First, create a demo view controller and create new method with your custom implementation.

    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            debugPrint("Call default view did load")
        }
    }
    
    extension UIViewController {
        // 1 
        @objc func viewDidLoadSwizzlingMethod() {
            // 2 
            self.viewDidLoadSwizzlingMethod()
            
            // 3 
            debugPrint("Swizzleeee. Call NEW view did load ")
            view.backgroundColor = .yellow
        }
    }
    
    
    1. Create new method with custom implementation
    2. If you add this line, it will call new implementation first, then call default implementation. If. you don’t add this line, it will call new implentation only.
    3. Your custom implementation

    Next, create a function where the swizzle takes place.

    extension UIViewController {
    ...
         static func startSwizzlingViewDidLoad() {
            // 1
            let defaultSelector = #selector(viewDidLoad)
            let newSelector = #selector(viewDidLoadSwizzlingMethod)
    
            // 2
            let defaultInstace = class_getInstanceMethod(UIViewController.self, defaultSelector)
            let newInstance = class_getInstanceMethod(UIViewController.self, newSelector)
            
            // 3
            if let instance1 = defaultInstace, let instance2 = newInstance {
                debugPrint("Swizzlle for all view controller success")
                method_exchangeImplementations(instance1, instance2)
            }
        }
    }
    1. Create 2 selectors of default method and new method.
    2. Create 2 references of 2 selectors by using class_getInstanceMethod.
    3. Use Objective-C runtime to “swaps implementation” of 2 selectors.

    The final step is call the function startSwizzlingViewDidLoad. We must swizzle before the viewController call it’s default viewDidLoad.
    Here, I will swizzle at AppDelegate to make all ViewControllers in apps change backgroundColor to yellow.

    class AppDelegate {
        func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            UIViewController.startSwizzlingViewDidLoad()
            return true
        }
    
        ...
    }

    As you can see, all of our view controllers will be set backgroundColor to yellow color.

    Swizzling custom Swift class

    To use method swizzling with your Swift classes, you just need to do:

    • The methods you want to swizzle must have the dynamic attribute.
    • Flow the steps like swizzle CocoaTouch class
    class GST {
        
        @objc dynamic func workFromHome() {
            print("Working...")
        }
        
        @objc dynamic func swizzleWorkFromHome() {
            print("Playing from home...")
        }
        
        static func startSwizzling() {
            let defaultInstance = class_getInstanceMethod(GST.self, #selector(GST.workFromHome))
            let newInstance = class_getInstanceMethod(GST.self, #selector(GST.swizzleWorkFromHome))
            
            if let instance1 = defaultInstance, let instance2 = newInstance {
                method_exchangeImplementations(instance1, instance2)
            }
        }
    }

    And the results:

    Note

    1. If you swizzle multiple times default method, that default method will have the implementation of the lastest swizzle method.

    Example:

    • You swizzle viewDidLoad with your custom method in your AppDelegate.
    • Firebase swizzle viewDidLoad with its custom method when FirebaseSDK init in your app => after AppDelegate.
    • When a ViewController init => It will takes the implementation of Firebase’s custom method instead of your, because Firebase swizzle after you swizzle.
    1. If you are shipping a framework which is used by hundreds of apps, better not to use swizzling in this case. If you must use swizzling, you should added it to the framework’s document.

    References:

  • Swift – Basic to advanced Closure

    Swift – Basic to advanced Closure

    Với nhiều bài toán, không phải lúc nào cũng đơn giản. Ví dụ chỉ với 2 số nguyên, thực tế có rất nhiều công thức áp dụng được với 2 số này, từ đơn giản như cộng, trừ, nhân, chia … đến phức tạp như hàm mũ, khai căn,… Nếu chỉ sử dụng cách định nghĩa sẵn các function ta không giải quyết tất các các case của bài toán. Giải pháp ở đây là chúng ta sử dụng closure.


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

    Closure cơ bản

    1. Tạo closure cơ bản
    2. Tạo tham số cho closure
    3. Trả về giá trị từ closure
    4. Truyền closure như 1 tham số vào Function

    Closure nâng cao

    1. Truyền closure với nhiều tham số vào Function
    2. Trả về closure từ functiion
    3. Lưu trữ dữ liệu với closure

    Các kiểu gọi hàm với closure

    Tổng kết


    Basic closure

    1. Tạo closure cơ bản

    Swift cho phép chúng ta sử dụng function như bất kỳ kiểu dữ liệu nào,ví dụ như string, integers,… Điều này có nghĩa rằng chúng ta có thể tạo ra 1 function và gán nó cho một biến, gọi function đó bằng cách sử dụng biến đó và thậm chí có thể gán function đó vào các function khác dưới dạng tham số.

    Function mà ta sử dụng theo cách này được gọi là closure, mặc dù cách hoạt động của nó giống function tuy nhiên cách viết khác nhau 1 chút.

    Ví dụ đơn giản để print 1 đoạn text :

    Ở đây mình tạo ra một function tuy nhiên không có tên, và gán nó cho biến driving. Ta có thể gọi driving() như thể nó là 1 hàm thông thường :

    2. Tạo tham số cho closure

    Khi khởi tạo closure, ta sẽ nhận ra nó không có tên nên sẽ không có bất kỳ vị trí nào để thêm tham số như function bình thường. Tuy nhiên không có nghĩa là closure không nhận tham số input, chỉ là nó có cách làm khác so với function: các tham số được liệt kê bên trong dấu {}.

    Cách tạo ra các tham số để closure có thể “chứa chấp” rất đơn giản, chỉ cần liệt kê chúng bên trong dấu ngoặc đơn ngay sau dấu ngoặc nhọn mở, sau đó thêm keyword in để Swift biết phần nào là phần bắt đầu closure.

    Ví dụ mình sửa driving() bên trên thành closure có chứa tham số

    Và một trong những điểm khác biệt nữa giữa closure và function là bạn không cần sử dụng tên tham số khi gọi closure.

    3. Trả về giá trị từ closure

    Closure có thể trả về giá trị, và cách viết tương tự như khai báo tham số: viết nó trong closure, ngay trước keyword in.

    Với closure driving() bên trên, mình sẽ trả về đoạn string kia thay vì print thẳng ra, để làm thế ta sử dụng → String trước keyword in, sau đó sử dụng return như function bình thường

    Bây giờ có thể gọi closure này và in ra giá trị String nó trả về

    4. Truyền closure như một tham số vào function

    Vì closure có thể được sử dụng như string, integer,…, bạn có thể truyền nó vào 1 function. Syntax của nó khá là rắc rối với newbie tuy nhiên nếu bạn đã hiểu về nó thì sẽ thấy không rắc rối lắm :))

    Đây là closure driving() gốc của chúng ta

    Nếu bạn muốn truyền closure này vào trong 1 function để nó có thể thực thi bên trong function đó, bạn phải chỉ định kiểu tham số là () -> Void.

    Ví dụ mình viết 1 function travel() mà nó nhận tham số là các kiểu action khác nhau

    Ta có thể gọi hàm travel() mà sử dụng closure driving

    Bên trên là 1 ví dụ về truyền closure như 1 tham số, tuy nhiên ta đang sử dụng () → Void, nói cách khác là không truyền vào tham số và cũng không nhận giá trị trả về.

    Tuy nhiên,1 closure vẫn có thể nhận tham số của chính nó khi chính closure đó đang là 1 tham số của function khác.

    Ví dụ, mình viết lại function travel() mà nó chỉ có 1 closure là tham số duy nhất, và closure đó nhận tham số là 1 String

    Và để thực thi function travel(), ta gọi nó với 1 tham số closure


    Advanced closure

    1. Truyền closure với nhiều tham số vào function

    Mình lại xin phép viết lại hàm travel() bên trên, tuy nhiên lần này hàm travel() của chúng ta sẽ cần 1 closure mà nó chứa một vài thông tin khác thay vì 1 string như bên trên, cụ thể nó sẽ chứa thông tin về địa điểm mà một người sẽ đến và tốc độ họ đi. Lúc này chúng ta cần sử dụng (String, Int) → String cho kiểu tham số của closure

    Để thực thi function travel(), ta gọi function này với tham số closure được truyền vào

    ( Bạn có thể thấy lạ với các keyword $0 và $1, hiện tại bạn không hiểu cũng không sao cả vì mình sẽ giải thích rõ hơn ở dưới =]] )

    Closure giống với function là nó có thể nhận bao nhiêu tham số cũng được, tuy nhiên bạn có thể thấy rằng một function mà nhận quá nhiều tham số thì sẽ rất khó hiểu dẫn đến confuse, điều này còn kinh khủng hơn với closure khi bản chất closure cũng đã rất phức tạp rồi. Vì thế để mọi thứ clear bạn nên chỉ sử dụng từ 1 đến 3 tham số  mà thôi.

    2. Trả về closure từ function

    Tương tự như việc bạn truyền tham số closure vào function, bạn cũng có thể nhận về 1 closure mà được trả về từ function.

    Syntax để return closure từ function có hơi rắc rối 1 chút, bởi vì nó dùng → 2 lần: một để chỉ định giá trị trả về của function và một để chỉ định giá trị trả về từ closure.

    Mình lại viết lại hàm travel() mà không nhận tham số, tuy nhiên lại có trả về 1 closure mà closure nhận tham số là String và trả về Void

    Chúng ta gọi travel() để nhận về closure đó, sau đó gọi nó như 1 function

    Còn 1 cách gọi nữa để gọi trực tiếp giá trị trả về từ travel() – cách này không được khuyến khích sử dụng:

    3. Lưu trữ dữ liệu với closure

    Nếu bạn sử dụng bất kỳ giá trị bên ngoài nào trong closure, Swift sẽ lưu và giữ chúng cùng closure, vì thế giá trị này có thể bị thay đổi kể cả nó không còn tồn tại.

    Ví dụ, việc lưu trữ giá trị trong closure xảy ra khi ta tạo 1 giá trị trong hàm travel() mà giá trị đó được sử dụng trong closure, ở đây ví dụ như ta muốn kiểm tra xem closure được gọi trả về bao nhiêu lần

    Mặc dù biến counter được tạo bên trong travel(), nó sẽ được lưu trữ bởi closure vì thế nến nó sẽ vẫn luôn tồn tại cho closure đó.

    Vì thế nếu ta gọi result(“London”) nhiều lần, biến đếm sẽ luôn tăng lên :


    Các kiểu gọi hàm với closure

    Ví dụ, ở đây định nghĩa 1 functiontypecalculationresultcallback với tham số đầu vào là 1 kiểu int và không có giá trị trả về, chúng ta cũng có thêm 1 hàm là multiplyNumber với 2 tham số nhận vào là kiểu int, không có giá trị trả về và kèm theo 1 tham số có tên là callback có kiểu là CalculationResultCallback. Trong ngôn ngữ lập trình nói chung, callback có nghĩa là lời gọi hàm sau, tức là sau quá trình sử lý logic và ra được 1 kết quả nào đó mà chúng ta cần xử lý thêm với kết quả đó.

    Hàm multiplyNumbers có ý nghĩa là, với 2 giá trị nguyên bất kỳ, nó sẽ tính phép nhân 2 số, sau đó sẽ trả về kết quả bằng 1 hàm mà ở đó, ta có thể tùy ý sử dụng kết quả theo ý ta muốn.

    Ta có các cách gọi hàm như sau:

    • Ta có thể thấy, 2 tham số đầu tiên có kiểu int được gọi hoàn toàn bình thường, nhưng với tham số thứ 3 với tên là callback, thay vì truyền vào 1 số, hoặc 1 tên hàm như trước, thì ta truyền vào hẳn 1 hàm,mà ở đó ta in ra dòng “Tích của 2 số là …”
    • Trong closure này,chúng ta có từ khóa in phân cách 2 nửa,bên trái là khai báo tham số nằm trong () và khai báo kiểu trả về là void,bên phải là lệnh được thực thi khi gọi đến closure này,toàn bộ closure được bao trong cặp dấu {}.
    • Giống như function bình thường,ta có thể bỏ void nếu hàm không trả về kết quả,và có thể bỏ đi luôn () bao tham số.
    • Đây chính là dạng gọi cơ bản nhất của closure
    • Swift quy định,với những hàm khai báo closure là tham số nhưng ở vị trí cuối cùng trong danh sách tham số: ví dụ như trên,ta có thể gọi hàm bằng cách gọi hàm với 2 tham số int bình thường,kéo theo 1 tham số closure được bao bằng dấu {},trường hợp này được gọi là Trailing closure
    • Để closure gọn hơn nữa, có 1 tính năng gọi là Shorthands, bản chất là thay vì khai báo tham số với tên biến cụ thể thì chúng ta sử dụng cú pháp $0,$1,$2,… để thay thế cho các tham số ở vị trí 0,1,2,.. Tất nhiên,nếu hàm chỉ có 1 tham số mà ta gọi đến $1 thì hàm sẽ báo lỗi.
    • Đối với shorthands, ta không thể khai báo tham số như những trường hợp trên nữa,đổi lại sẽ ngắn gọn hết sức có thể..
    • Tuy nhiên,với hàm có nhiều tham số,ta khó xác định $0,$1,$2,… là gì.Vì thế nên chỉ sử dụng shorthands với hàm đơn giản chỉ có từ 1-3 tham số mà thôi.

    Tổng kết

    Về bản chất

    • Closure là 1 function.
    • Nhưng là function không đầy đủ tên function và thân function , mà chỉ có mỗi thân function .
    • Mục đích của nó không phải gọi function bằng tên, mà là được chèn vào tham số của 1 function khác.