Author: NhatHM

  • OWASP Top 10 for Mobile

    OWASP Top 10 for Mobile

    OWASP (Open Web Application Security Project) là dự án xây dựng một nền tảng hướng đến việc tăng cường bảo mật cho phần mềm. Bắt nguồn là cho các dự án Web app, Back end. Tuy nhiên hiện nay thì bảo mật cho các dự án Mobile cũng đã trở nên cấp thiết hơn rất nhiều, và series bài viết này sẽ hướng đến tìm hiểu về các vấn đề về security, phương thức tấn công, các phương pháp phòng tránh, các checklist để tạo ra một project mobile an toàn hơn.

    OWASP Top 10 for Mobile

    1. Improper Platform Usage

    Improper Platform Usage tức là sử dụng các chức năng (features) của nền tảng mobile (iOS/Android) một cách không phù hợp.

    Cụ thể, từng OS system sẽ cung cấp cho developers các tính năng (capabilities, features) có thể sử dụng để phát triển ứng dụng. Nếu developers không sử dụng các tính năng này để phát triển, hoặc sử dụng chúng một cách không chính xác, thì sẽ được gọi là improper use (sử dụng không đúng cách).

    Ví dụ: với iOS thì lưu các thông tin quan trọng của user như passworld hay token thì không được sủ dựng UserDefault để lưu mà cần lưu bằng Keychain, tuy nhiên, việc sử dụng sai config của Keychain cũng tiềm ẩn rủi ro bị tấn công lộ mật khẩu. Hoặc với những quyền truy cập vào thông tin user như Location, HealthKit, Photos cũng không được sử dụng một cách thiếu cân nhắc.

    2. Insecure Data Storage

    Insecure data storage, lưu trữ dữ liệu một cách không an toàn, không đơn thuần chỉ là cách lưu trữ data (ảnh, text, sql data…) một cách không an toàn. Mà còn có thể là log file, cookie file, cached file (URL, browser, third party data…). Một ví dụ điển hình là các developer rất hay sử dụng log trong quá trình phát triển phần mềm, rất dễ tiềm ẩn nguy cơ "lỡ tay" log ra các thông tin nhạy cảm mà quên không loại bỏ, dẫn đến việc bị lọt ra môi trường product.

    Những item cần cân nhắc khi nghĩ đến nguy cơ lộ data:

    • Cách OS cache data, cache image, có thể là cả logging, buffer, thậm chí key-press (đặc biệt là OS mở như Android)
    • Cách các framework được sử dụng trong dự án cache data
    • Cách các thư viện open source cache data, gửi / nhận data

    3. Insecure Communication

    Các ứng dụng mobile thì luôn cần phải transfer data, có thể là giữa client – server, giữa các devices với nhau. Và trong quá trình gửi nhận data, nếu không tuân thủ các quy tắc bảo mật thì có thể bị kẻ xấu tấn công ăn cắp các thông tin nhạy cảm. Đó có thể là password, thông tin account, hoặc các thông tin private của user.

    Việc tấn công ăn cắp thông tin có thể thông qua wifi mà devices đang truy cập, các router nhà mạng hoặc các thiết bị ngoại vi BLE, NFC…

    4. Insecure Authentication

    Insecure Authentication mô tả việc thực hiện xác thực user kém bảo mật dẫn đến người khác có thể lợi dụng để tấn công vào backend nhằm ăn cắp thông tin hoặc thực hiện các request tổn hại đến hệ thống. Vấn đề này có thể do việc thiết kế backend thiếu security như việc request không có access token, hoặc từ bản thân mobile app đã thực hiện việc authenticate offline sơ sài (ví dụ như set passcode ngắn để truy cập vào chức năng quan trọng, hoặc lưu password của user) dẫn đến việc kẻ tấn công có thể ăn cắp thông tin và truy cập vào server.

    5. Insufficient Cryptography

    Việc bảo mật dữ liệu trong mobile app thường áp dụng mã hóa. Tuy nhiên, trong một vài trường hợp thì mã hõa vẫn sẽ tiềm ân rủi ro bị tấn công ăn cắp dữ liệu. Một vài khả năng có thể kể đến như:

    • Quá ỷ lại vào mã hóa của OS (Built-In Code Encryption Processes), ví dụ iOS bản thân nó cũng sẽ mã hóa ứng dụng, tuy nhiên nếu devices bị jail break thì cũng có khả năng decode ứng dụng để lấy dữ liệu.
    • Sử dụng một cơ chế mã hóa đã lỗi thời hoặc tự viết lại thuật toán mã hóa.
    • Lưu trữ encrytion key một cách thiếu bảo mật.

    6. Insecure Authorization

    • Authentication: xác thực user
    • Authorization: xác thực quyền của user

    Việc thiếu sót trong việc xác thực quyền hạn của user trong hệ thống có thể dẫn đến sai sót trong việc cho phép user được truy cập vào những tài nguyên không được cho phép, hoặc có tính bảo mật cao. Dẫn đến việc mất mát các thông tin nhạy cảm của server hoặc bị thực thi những quyền có tính chất ảnh hưởng lớn đến hệ thống. Các vấn đề có thể xảy ra trong trường hợp này như:

    • Server không check quyền của user khi request lên, mà hoàn toàn dựa vào request của client, ví dụ người tấn công có thể lợi dụng bằng cách thêm các param vào GET/POST request để lừa server rằng "tao là admin" và có thể truy cập vào các thông tin nhạy cảm.
    • Một số trường hợp có thể server cung cấp các API dành riêng cho "admin" và nghĩ rằng các user bình thường sẽ không biết các API này nên không cần phải check quyền, tuy nhiên việc này không hề đảm bảo đến việc sẽ bảo mật được các API này mà không lộ ra ngoài cho các user không có quyền khác.

    7. Poor Code Quality

    Poor code quality là các lỗi bảo mật liên quan đến bản thân ngôn ngữ lập trình, có thể kể đến như lỗi:

    • Buffer overflows: các lỗi buffer overflows thì hay xảy ra với các thư viện viết bởi C, C++, và iOS cùng Android đề sử dụng các thư viện C, C++ trong hệ thống, do đó kẻ xấu có thể tấn công vào các thư viện này và qua đó thực thi các đoạn lệnh hoặc thậm chí cài mã độc vào ứng dụng.
    • Format string vulnerabilities: những lỗi dạng này thường là kẻ tấn công cố tình input các đoạn string mã hóa hoặc format đặc biệt vào ứng dụng, hoặc các câu lệnh để tấn công vào ứng dụng. Qua đó có thể gây ra các lỗi như crash ứng dụng, view data trong stack, view thông tin trong memory, thậm chí thực thi source code
    • Tấn công vào third party hoặc tấn công thông qua webview của OS: sử dụng các thư viện kém bảo mật cũng tiềm ẩn nguy cơ bị tấn công. Ngoài ra, trong các version OS cũ, cả iOS và Android đều bộc lộ rất nhiều lỗi liên quan đến webview như thực thi các đoạn javascript nhằm ăn cắp thông tin user.

    8: Code Tampering

    Các ứng dụng mobile, về cơ bản là sẽ nằm trên máy của user, do đó nên ứng dụng mobile rất dễ bị tấn công thay đổi nội dung source code để thực hiện các mục đích xấu. Có thể kể đến các kịch bản tấn công như: kẻ xấu sẽ cài đặt ứng dụng của bạn lên device đã bị jail, qua đó có thể xem được nội dung bên trong của ứng dụng như assets, resource. Thậm chí thay đổi source code hoặc các API được call. Mục đích của việc tấn công này có thể là unlock các chức năng ẩn, hoặc trả phí, hoặc ăn cắp thông tin. Một vài trường hợp có thể là cài mã độc vào, thay đổi asset, resource, sau đó lại distribute ứng dụng lên các kho ứng dụng lậu nhằm ăn cắp thông tin của người tải về. Trước đây thì mình hay jail device để cheat game, hoặc các ứng dụng ngân hàng cũng có thể là mục tiêu yêu thích để bị tấn công dạng này. -> Hãy check Jail/Root khi khởi động ứng dụng.

    9. Reverse Engineering

    Kẻ tấn công sẽ down ứng dụng và sử dụng các tool để tấn con giải mã source code nhằm các mục đích như ăn cắp thông tin trong string table/plist, đọc source code, tìm hiểu thuật toán hoặc các thư viện mà app sử dụng. Với kiểu tấn công này thì mục tiêu tấn công thường là

    • Ăn cắp thông tin về backend server, như url, path, cert nếu có
    • Ăn cắp các key của thuật toán mã hóa
    • Ăn cắp các thông tin liên quan đến sở hữu trí tuệ

    10. Extraneous Functionality

    Các chức năng không liên quan đến ứng dụng?

    Khi ứng dụng được phát triển, trên thực tế nó có rất nhiều các chức năng không thuộc functional requirement và non-functional requirement, nhưng là không thể thiếu trong quá trình phát triển ứng dụng như:

    • Log file, log console để debug những thông tin API, thông tin user hoặc thông tin của ứng dụng để debugging trong quá trình phát triển
    • Các switch flag để on off các chức năng nào đó trong ứng dụng, có thể là chức năng của admin hoặc chức năng liên quan đến A-B testing…
    • Các API path nhằm debug nhưng quên không loại bỏ khi lên môi trường production.

    Các chức năng này nếu bị khai thác có thể làm lộ thông tin về ứng dụng cũng như thông tin về user.

  • WWDC21 – ARC in Swift: Basics and beyond

    WWDC21 – ARC in Swift: Basics and beyond

    ARC in Swift: Basics and beyond

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

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

    Object’s lifetimes

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

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

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

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

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

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

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

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

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

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

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

    Thông qua:

    • weakunowned reference
    • Deinitializer (deinit)

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

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

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

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

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

    How to fix

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

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

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

    Sử dụng strong references

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

    Kết luận:

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

    Nguồn:

  • GIT – Những lưu ý khi config user name/email cho repo

    GIT – Những lưu ý khi config user name/email cho repo

    Git và các dịch vụ Git như Github, Gitlab đang ngày càng trở nên phổ biến đối với các developer hiện này, hay có thể nói đó là phần không thể thiếu rồi.

    Và đương nhiên, một developer có thể contribute đến nhiều project/repository, và mỗi project có thể dùng một định danh khác nhau. Ví dụ như dùng account email công ty với những project của công ty, hoặc email cá nhân với những project làm thêm, học thêm. Thậm chí có những người được khách hàng cấp account riêng để truy cập vào git riêng của họ.

    Và nếu, developer không quản lý tốt git config cho từng project thì có thể sẽ bị nhầm lẫn email lúc commit lên. Đây là điều rất hay xảy ra nhưng tiếc là ít người để ý.

    Nếu bạn làm trong FSoft, hoặc các công ty đề cao việc security đối với việc dùng email công ty cho các mục đích ngoài công việc, thì có thể nhận được những email warning dạng: bạn đang dùng email công ty trong project git này….hãy cho biết lý do tại sao, và một loạt các câu hỏi khác.

    Tại sao IT lại biết được việc bạn push lên github, gần như ngay lập tức?

    Github có API public các event xảy ra trên dịch vụ này gần như realtime tại: https://api.github.com/events. Và nếu tôi viết 1 con Bot scan liên tục API này và phân tích data thì tôi dễ dàng tìm được thông tin người thực hiện commit đấy lên như email hay user name (đương nhiên là email và user name config thôi)

    Ví dụ như:

    Github public events

    Với sample phía trên, ta dễ dàng scan ra được user name và email của người vừa push lên. Và đương nhiên, con BOT của IT cũng sẽ tóm được ta nếu ta sử dụng email công ty để push lên.

    Tuy nhiên, nhiều người sẽ thắc mắc tại sao với project này, tôi sử dụng email cá nhân để đăng ký, nhưng thông tin push lên lại là email công ty?

    Câu trả lời rất đơn giản, nằm ở chỗ config của git mà thôi.

    Git có 2 loại config chính cho các repo là Global config và Local config. Với những thông tin mà được sử dụng ở tất cả các repo thì thường sẽ config trong Global config. Còn những thông tin riêng của từng project sẽ được config trong Local config của từng dự án.

    Tuy nhiên, rất nhiều developer quên, hoặc không biết đến điều này.

    Cùng xem ví dụ bên dưới.

    Ví dụ về config Git Global trong máy: (open terminal và gõ lệnh git config --list)

    Git global config sample

    Sau khi gõ lệnh thì ta sẽ lấy được tất cả thông tin mà git global config đang setting. (ấn q để thoát ra)

    Ví dụ về 1 project không có config user name và email (dùng lệnh git config --list --local)

    Với project/repo này thì khi commit lên, do thông tin user name và email bị thiếu, nêu git sẽ lấy config default trong Global config để thêm vào. Do đó, nếu bạn đã từng setting email global là email công ty, thì với commit này của bạn, email cũng sẽ là mail công ty, mặc dù repo này bạn không hề đăng ký bằng email công ty. Và IT sờ gáy.

    Vậy, làm thế nào để config thông tin cho từng repo? Ví dụ như dưới

    Cd vào thư mục source, dùng config:

    git config user.name nhathm
    git config user.email [email protected]

    Và như vậy, config đã được áp dụng cho project của bạn, check lại bằng command git config --list --local, nếu 2 dòng cuối cùng hiển thị thông tin đúng như bạn đã config là thành công

    user.name=nhathm
    [email protected]

    Hãy hết sức cẩn thận khi commit nếu như bạn đang tham gia nhiều project khác nhau, sẽ không tốt chút nào nếu repo cá nhân lại bị gắn email công ty, và ngược lại.

    Thanks.

  • Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Hướng dẫn tạo plugin cho dự án Cordova/Ionic

    Table of contents

    • Tại sao cần tạo plugin cho Cordova
    • Tạo plugin bằng plugman
    • Hoàn thiện plugin

    Tại sao cần tạo plugin cho Cordova

    Về cơ bản, thì Cordova là framework phát triển các app iOS/Android (là chính) sử dụng html/js/css làm UI, và các bộ plugin làm cầu nối để call xuống source native của platform (iOS/Android)

    Cordova bao gồm:

    • Bộ html/js/css làm UI.
    • Native webview engine làm bộ render hiển thị UI
    • Cordova framework chịu trách nhiệm cầu nối giữa function call js và funtion native.
    • Source code native làm plugin cùng các config và các pulic js method.
    Cordova project struct

    Bình thường đối với người làm Cordova thì chủ yếu họ sẽ focus vào tầng UI bằng html/js/css. Việc sử dụng các chức năng native của platform thì sẽ sử dụng các plugin được cung cấp sẵn. Vậy nên về cơ bản, một lập trình viên làm Cordova chỉ cần làm được html/js/css là đủ.

    Tuy nhiên trong một số trường hợp, các plugin có sẵn không đảm bảo giải quyết được vấn đề bài toán, lúc này việc phát triển riêng một plugin thực hiện được logic của project và support được các platform là điều cần phải làm.

    Trong một vài trường hợp khác, có thể dự án đã có sẵn source native, tuy nhiên cần chuyển sang Cordova để support multi platform và tận dụng source code native có sẵn.

    -> Đo đó, hiểu biết về cách tạo một plugin để giải quyết nhu câu bài toán sẽ nảy sinh. Bài viết này sẽ tập trung vào việc

    • Làm thế nào để tạo plugin
    • Luồng xử lý từ js xuống native source của plugin như nào
    • Install plugin vào Cordova project
    • Build và test plugin trên iOS và Androd

    Tạo plugin bằng plugman

    Tạo Cordova project

    Để tạo Cordova plugin sample thì trước tiên cần có một Cordova project để test việc add plugin và kiểm tra hoạt động của plugin trên từng platform.

    • Install Cordova CLI: sudo npm install -g cordova
    • Create Cordova project: cordova create SamplePlugin com.nhathm.samplePlugin SamplePlugin

    Install plugman và tạo plugin template

    plugman là command line tool để tạo Apache Cordova plugin. Install bằng command: npm install -g plugman

    Create plugin:

    • Command: plugman create –name pluginName –plugin_id pluginID –plugin_version version
    • Ví dụ: plugman create –name GSTPlugin –plugin_id cordova-plugin-gstplugin –plugin_version 0.0.1

    Thêm platform mà plugin sẽ hỗ trợ:

    • plugman platform add –platform_name android
    • plugman platform add –platform_name ios

    Sau khi cài đặt xong thì thư mục plugin sẽ có struct như dưới.

    .
    └── GSTPlugin
    ├── plugin.xml
    ├── src
    │ ├── android
    │ │ └── GSTPlugin.java
    │ └── ios
    │ └── GSTPlugin.m
    └── www
    └── GSTPlugin.js

    Ở đây, plugin.xml là file config cho plugin, bao gồm các thông tin như tên của plugin, các file assets, resources. Define js-module như file js của plugin, define namespace của plugin, define các plugin phụ thuộc của plugin đang phát triển…

    Hoàn thiện plugin

    Cùng view file plugin.xml của plugin mới tạo:

    <?xml version='1.0' encoding='utf-8'?>
    <plugin id="cordova-plugin-gstplugin" version="0.0.1"
    xmlns="http://apache.org/cordova/ns/plugins/1.0"
    xmlns:android="http://schemas.android.com/apk/res/android">
    <name>GSTPlugin</name>
    <js-module name="GSTPlugin" src="www/GSTPlugin.js">
    <clobbers target="cordova.plugins.GSTPlugin" />
    </js-module>
    <platform name="android">
    <config-file parent="/*" target="res/xml/config.xml">
    <feature name="GSTPlugin">
    <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" />
    </feature>
    </config-file>
    <config-file parent="/*" target="AndroidManifest.xml" />
    <source-file src="src/android/GSTPlugin.java" target-dir="src/cordova-plugin-gstplugin/GSTPlugin" />
    </platform>
    <platform name="ios">
    <config-file parent="/*" target="config.xml">
    <feature name="GSTPlugin">
    <param name="ios-package" value="GSTPlugin" />
    </feature>
    </config-file>
    <source-file src="src/ios/GSTPlugin.m" />
    </platform>
    </plugin>

    Trong file này có một vài điểm cần hiểu như dưới:

    • <clobbers target="cordova.plugins.GSTPlugin" /> đây là namespace phần js của plugin. Từ file js, call xuống method native của plugin thì sẽ sử dụng cordova.plugins.GSTPlugin.sampleMethod
    • <param name="android-package" value="cordova-plugin-gstplugin.GSTPlugin" /> đây là config package name của Android, cần đổi sang tên đúng => <param name="android-package" value="com.gst.gstplugin.GSTPlugin" />. Như vậy Cordova sẽ tạo ra file GSTPlugin.java trong thư mục com/gst/gstplugin.

    Trong sample này, chúng ta sẽ sử dụng Swift làm ngôn ngữ code Native logic cho iOS platform chứ không dùng Objective-C, do đó phần platform iOS cần update.

    • Trong thẻ <platform name="ios> thêm tag <dependency id="cordova-plugin-add-swift-support" version="2.0.2"/>. Đây là plugin support việc import các file source Swift vào source Objective-C. Mà bản chất Cordova sẽ generate ra source Objective-C cho platform iOS.
    • Vì sử dụng Swift nên ta thay thế <source-file src="src/ios/GSTPlugin.m" /> bằng <source-file src="src/ios/GSTPlugin.swift" />. Và đổi tên file GSTPlugin.m sang GSTPlugin.swift

    Tiếp theo, chỉnh sửa các file js và native tương ứng cho plugin.

    File GSTPlugin.js

    • File này export các public method của plugin. Update file như dưới
    var exec = require('cordova/exec');
    
    exports.helloNative = function (arg0, success, error) {
        exec(success, error, 'GSTPlugin', 'helloNative', [arg0]);
    };
    • File này sẽ export method helloNative ra js và call method helloNative của native platform tương ứng.

    File GSTPlugin.swift

    • File này chứa logic và implementation cho iOS platform. Chỉnh sửa file như dưới
    @objc(GSTPlugin) class GSTPlugin : CDVPlugin {
        @objc(helloNative:)
        func helloNative(command: CDVInvokedUrlCommand) {
            // If plugin result nil, then we should let app crash
            var pluginResult: CDVPluginResult!
    
            if let message = command.arguments.first as? String {
                let returnMessage = "GSTPlugin hello \(message) from iOS"
                pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: returnMessage)
            } else {
                pluginResult = CDVPluginResult (status: CDVCommandStatus_ERROR, messageAs: "Expected one non-empty string argument.")
            }
    
            commandDelegate.send(pluginResult, callbackId: command.callbackId)
        }
    }

    File GSTPlugin.java

    package com.gst.gstplugin;
    
    import org.apache.cordova.CordovaPlugin;
    import org.apache.cordova.CallbackContext;
    
    import org.json.JSONArray;
    import org.json.JSONException;
    import org.json.JSONObject;
    
    import android.util.Log;
    
    public class GSTPlugin extends CordovaPlugin {
    
        @Override
        public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
            if (action.equals("helloNative")) {
                String message = args.getString(0);
                this.helloNative(message, callbackContext);
                return true;
            }
            return false;
        }
    
        private void helloNative(String message, CallbackContext callbackContext) {
            if (message != null && message.length() > 0) {
                callbackContext.success("GSTPlugin hello " + message + " from Android");
            } else {
                callbackContext.error("Expected one non-empty string argument.");
            }
        }
    }

    Tại thư mục của plugin, chạy command plugman createpackagejson . và điền các câu trả lời, phần nào không có thì enter để bỏ qua

    Switch sang thư mục chứa project Cordova SamplePlugin đã tạo, run command cordova plugin add --path-to-plugin.

    -> Ví dụ cordova plugin add /Users/nhathm/Desktop/Cordova-plugin_sample/GSTPlugin/GSTPlugin

    Bây giờ, plugin đã được add vào project Cordova. Tiếp theo sẽ chỉnh sửa source của index.html và index.js để test hoạt động của project.

    File index.html

    <div class="app">
          <h1>Apache Cordova</h1>
          <div id="deviceready" class="blink">
              <p class="event listening">Connecting to Device</p>
              <p class="event received">Device is Ready</p>
          </div>
    
          <button id="testPlugin">Test Plugin</button><br/>
    </div>

    File index.js

    • Add vào onDeviceReady()
    document.getElementById("testPlugin").addEventListener("click", gstPlugin_test);

    Thêm function:

    function gstPlugin_test() {
        cordova.plugins.GSTPlugin.helloNative("NhatHM",
            function (result) {
                alert(result);
            },
            function (error) {
                alert("Error " + error);
            }
        )
    }

    Sau khi chỉnh sửa hoàn chỉnh, build source cho platform iOS và Android

    • cordova platform add ios
    • cordova platform add android

    Build project native đã được generate ra và kiểm tra kết quả

    cordova plugin sample

    Đối với việc tạo UI cho project Cordova, chúng ta nên dùng các framework support như Ionic: https://ionicframework.com/

    Note: các dự án hybrid rất hạn chế về mặt performance, do đó nên cân nhắc khi bắt đầu dự án mới bằng hybrid

  • Swift—Design patterns: Multicast Delegate

    Swift—Design patterns: Multicast Delegate

    Multicast delegate

    Khái niệm

    • Chúng ta đều biết rằng delegate là mối quan hệ 1 – 1 giữa 2 objects, trong đó 1 ọbject sẽ gửi data/event và object còn lại sẽ nhận data hoặc thực thi event
    • Multicast delegate, về cơ bản chỉ là mối quan hệ 1 – n, trong đó, 1 object sẽ gửi dataevent đi, và có n class đón nhận data, event đó.

    Ứng dụng của multicase delegate, lúc nào thì dùng?

    • Dùng multicast delegate khi mà bạn muốn xây dựng một mô hình delegate có mối quan hệ 1 – nhiều.
    • Ví dụ: bạn có 1 class chuyên lấy thông tin data và các logic liên quan, và bạn muốn mỗi khi data của class được update và muốn implement các logic tương ứng của class này trên nhiều view/view controller khác nhau. Thì multicast delegate có thể dùng để xử lý bài toán này.

    So sánh với observer và notification

    • Tại sao không dùng observer: chủ yếu chúng ta dùng obverver để theo dõi data hoặc event nào đó xảy ra, và thực hiện logic sau khi nó đã xảy ra. Còn nếu dùng delegate, chúng ta còn có thể xử lý data hoặc event, hay nói cách khác, chúng ta có thể quyết định xem có cho phép event đó xảy ra hay không, ví dụ như các delegate của Table view như tableView:willSelectRowAtIndexPath:
    • Tại sao không dùng Notification thay vì multicast delegate? Về cơ bản, notification là thông tin một chiều từ 1 object gửi sang nhiều object nhận, chứ không thể có tương tác ngược lại. Ngoài ra, việc gửi data từ object gửi sang các object nhận thông qua userInfo thực sự là một điểm trừ rất lớn của Notifiction. Hơn nữa, việc quản lý Notification sẽ tốn công sức hơn là delegate, và chúng ta khá là khó khăn để nhìn ra mối quan hệ giữa object gửi và nhận khi dùng Notification.

    Implementation

    • Vì Swift không có sẵn phương pháp tạo multicast delegate nên chúng ta cần phải tạo ra 1 class helper, nhằm quản lý các object muốn nhận delegate cũng như gọi các method delegate muốn gửi.

    Đầu tiên, chúng ta tạo class helper như dưới:

    class MulticastDelegate<ProtocolType> {
        private let delegates: NSHashTable<AnyObject> = NSHashTable.weakObjects()
        
        func add(_ delegate: ProtocolType) {
            delegates.add(delegate as AnyObject)
        }
        
        func remove(_ delegateToRemove: ProtocolType) {
            for delegate in delegates.allObjects.reversed() {
                if delegate === delegateToRemove as AnyObject {
                    delegates.remove(delegate)
                }
            }
        }
        
        func invokeDelegates (_ invocation: (ProtocolType) -> Void) {
            for delegate in delegates.allObjects.reversed() {
                invocation(delegate as! ProtocolType)
            }
        }
    }
    

    Class này là helper class có các method là add:(:) dùng để add và remove các object muốn nhận delegate. Ngoài ra nó có method invokeDelegates(:)->Void để gửi method delegate sang toàn bộ các object muốn nhận delegate.

    Tiếp theo, define protocol (các delegate method) muốn implement:

    protocol SampleDelegate: class {
        func sendSampleDelegateWithoutData()
        func sendSampleDelegate(with string: String)
    }
    
    extension SampleDelegate {
        func sendSampleDelegateWithoutData() {        
        }
    }
    

    Ở đây, protocol SampleDelegate có 2 method là để ví dụ thêm rõ ràng rằng multicast delegate có thể thoải mái gửi các delegate tuỳ ý. Phần extention của SampleDelegate chỉ là để khiến cho method sendSampleDelegateWithoutData trở thành optional, không cần phải "conform" đến SampleDelegate. Đây là cách khá Swift, thay vì dùng cách sử dụng @objC và keywork optional

    Tiếp theo, define ra class sẽ gửi các method của delegate

    class SampleClass {
        var delegate = MulticastDelegate<SampleDelegate>()
        
        func didGetData() {
            delegate.invokeDelegates {
                $0.sendSampleDelegate(with: "Sample Data")
            }
        }
    }
    

    Ở đây, có thể thấy rằng delegate của class "SampleClass" thực chất là object của helpper "MulticastDelegate", và nó chỉ chấp nhận delegate là objects của các class mà conform đến protocol "SampleDelegate"

    Khai báo vài class conform đến protocol "SampleDelegate"

    class ReceivedDelegate1: SampleDelegate {
        func sendSampleDelegate(with string: String) {
            print("ReceivedDelegate === 1 === \(string)")
        }
        
        deinit {
            print("deinit ReceivedDelegate1")
        }
    }
    
    class ReceivedDelegate2: SampleDelegate {
        func sendSampleDelegate(with string: String) {
            print("ReceivedDelegate === 2 === \(string)")
        }
        
        deinit {
            print("deinit ReceivedDelegate2")
        }
    }
    

    OK, bây giờ test thử:

    let sendDelegate = SampleClass()
    let received1 = ReceivedDelegate1()
    sendDelegate.delegate.add(received1)
    
    do {
        let received2 = ReceivedDelegate2()
        sendDelegate.delegate.add(received2)
        sendDelegate.didGetData()
    }
    print("Đợi cho object received2 trong block do được release")
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        sendDelegate.didGetData()
    }
    
    

    Sau khi run đoạn source code này, ta được log như dưới:

    ReceivedDelegate === 2 === Sample Data
    ReceivedDelegate === 1 === Sample Data
    Đợi cho object received2 trong block do được release
    deinit ReceivedDelegate2
    ReceivedDelegate === 1 === Sample Data
    

    Giờ cùng phân tích:

    Trong block do thì object "sendDelegate" đã append được 2 delegate objects vào biến delegate của nó, tiến hành send delegate thì ta thấy rằng cả object của cả 2 class ReceivedDelegate1ReceivedDelegate2 đều nhận được.

    Sau block do thì object received2 sẽ được release, vì việc release sẽ tốn một chút thời gian cho nên chúng ta sẽ thử thực hiện việc send delegate sau khoảng thời gian 1s, sau khi received2 đã được release (bằng cách check log của method deinit)

    Lúc này, ta thấy rằng chỉ có object của class ReceivedDelegate1 là còn nhận được delegate, object của class ReceivedDelegate2 đã bị release nên không còn nhận được object nữa. Như vậy, cách làm này vẫn đảm bảo các delegate vẫn là weak reference, không gây ra leak memory.

    Đề làm được điều này thì ta đã sử dụng NSHashTable.weakObjects() để lưu weak reference đến các delegate được gán vào biến delegates của helper MulticastDelegate. Do đó đảm bảo được việc keep weak reference của class helper, nhằm tránh memory leak.

    Ví dụ xem file: https://github.com/nhathm/swift.sample/tree/master/DesignPatterns/MulticastDelegate.playground

  • Swift—KeyPaths (1)

    Swift—KeyPaths (1)

    Swift KeyPaths (1)

    KeyPaths là khái niệm được đưa vào Swift từ version Swift 4 (source: SE-0161)

    Table of contents

    KeyPaths basic

    • KeyPaths là cách để truy cập đến property chứ không phải truy cập đến giá trị của property.
    • Khi định nghĩa KeyPaths thì ta hoàn toàn có thể xác định/định nghĩa được kiểu biến của KeyPaths
    • Ở Objective C cũng đã có khái niệm KeyPaths, tuy nhiên KeyPaths của Objective C chỉ là string, còn KetPaths của Swift thì được định nghĩa rõ ràng kiểu dữ liệu

    KeyPaths store uninvoked reference to property

    Cùng check đoạn code phía dưới

    class Person {
        var firstName: String
        var lastName: String
        var age: Int
    
        init(_ firstName: String, _ lastName: String, _ age: Int) {
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
        }
    }
    
    var firstPerson: Person? = Person("Nhat", "Hoang", 10)
    
    var nameKeyPaths = \Person.firstName
    var refVar = firstPerson?.firstName
    
    print("refVar value = \(refVar ?? "refVar nil")")
    print("KeyPaths value = \(firstPerson?[keyPath: nameKeyPaths] ?? "nil")")
    
    firstPerson = nil
    print("refVar value = \(refVar ?? "refVar nil")")
    print("KeyPaths value = \(firstPerson?[keyPath: nameKeyPaths] ?? "nil")")
    

    Log

    refVar value = Nhat
    KeyPaths value = Nhat
    refVar value = Nhat
    KeyPaths value = nil
    

    Với đoạn code trên, chúng ta dễ dàng thấy được sự khác nhau lớn giữa việc tham chiếu đến property của class khác nhau giữa việc sử dụng KeyPaths và bằng việc gán variable đến property của class.

    • Với việc tham chiếu giá trị dùng cách gán variable đến property thì ta có thể thấy rằng, mặc dù firstPerson đã được gán bằng nil, tuy nhiên do khi gán refVar đến firstName cho nên sau khi firstPerson bị gán là nil thì refVar vẫn có giá trị vì phép gán này đã ảnh hưởng đến reference count của property.
    • Đối với cách dùng KeyPaths, chúng ta vẫn có thể lấy được giá trị của property mặc cách bình thường, tuy nhiên KeyPaths không hề ảnh hưởng đến reference của property, do đó khi firstPerson được gán bằng nil, KeyPaths sẽ có value là nil.

    Với sự khác biệt trên, chúng ta hết sức lưu ý khi sử dụng phương pháp tham chiếu (reference) đến giá trị của property nếu không rất dễ nẩy sinh bug.

    KeyPaths as parameter

    Khi khai báo KeyPaths đến property nào đó của struct/class thì biến KeyPaths đó thể hiện rất rõ type của KeyPaths. Như ví dụ dưới, chúng ta có thể thấy rằng nameKeyPath là KeyPaths mô tả những property là kiểu String của type Person:

    Nếu đơn thuần chỉ gán variable đến property của instance thì ta sẽ được kiểu biến là String (hoặc loại type tương ứng với property của instance) như ảnh dưới:

    Vậy ứng dụng của việc này là gì?

    Nếu ta có một logic nào đó chỉ chấp nhận đầu vào là property của một class/struct cho trước, thì chúng ta nên sử dụng KeyPaths để giới hạn kiểu parameters cho logic đó. Ví dụ:

    func printPersonName(_ person: Person, _ path: KeyPath<Person, String>) {
        print("Person name = \(person[keyPath: path])")
    }
    
    class Person {
        var firstName: String
        var lastName: String
        var age: Int
    
        init(_ firstName: String, _ lastName: String, _ age: Int) {
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
        }
    }
    class Student: Person {
        var className: String
    
        init(_ firstName: String, _ lastName: String, _ age: Int, _ className: String) {
            self.className = className
            super.init(firstName, lastName, age)
        }
    }
    
    var firstPerson = Person("Nhat", "Hoang", 10)
    var firstStudent = Student("Rio", "Vincente", 20, "Mẫu giáo lớn")
    var nameKeyPath = \Person.lastName
    
    printPersonName(firstPerson, nameKeyPath)
    

    Với ví dụ trên thì func printPersonName chỉ chấp nhận đầu vào là String và thuộc class Person, nếu chúng ta sử dụng các KeyPaths cùng class Person nhưng khác kiểu data như var agekeyPath = \Person.age hoặc dùng KeyPaths cùng kiểu data nhưng khác class như var studentNameKeyPath = \Student.lastName (Student là class kế thừa Person) thì đều bị báo lỗi.

    => Điều này giúp chúng ta hạn chế sai sót ngay từ lúc coding.

    Sorted, filter…using KeyPaths

    Một trong những ứng dụng rất hay của KeyPaths đó là làm cho các closure như sorted, map, filter trở nên linh hoạt hơn, và tính ứng dụng cao hơn.

    Đi vào bài toán thực tế như sau:

    Bạn được yêu cầu làm một ứng dụng dạng như app Contact, và ứng dụng này có 1 vài chức năng như là:

    • Sắp xếp tên người dùng theo thứ tự
    • Lọc ra những người dùng đủ 18 tuổi

    Thì ta có thể triển khai như sau: Khai báo class Person tương ứng với từng Contact:

    class Person {
        var firstName: String
        var lastName: String
        var age: Int
        var workingYear: Int
    
        init(_ firstName: String, _ lastName: String, _ age: Int) {
            self.firstName = firstName
            self.lastName = lastName
            self.age = age
            self.workingYear = 0
        }
    }
    

    Danh sách contacts:

    let listPersons: [Person] = [Person("Alex", "X", 1),
                                 Person("Bosh", "Bucus", 12),
                                 Person("David", "Lipis", 20),
                                 Person("Why", "Always Me", 69),
                                 Person("Granado", "Espada", 45),
                                 Person("Granado", "Espada", 46)]
    

    Bây giờ, nếu muốn sắp xếp danh sách người dùng theo thứ từ A->Z đối với first name, thì ta có thể làm đơn giản như sau:

    let sortedPersons = listPersons.sorted {
        $0.firstName < $1.firstName
    }
    

    Đây là cách làm không sai, tuy nhiên nếu như sau này có thêm các yêu cầu như: sắp xếp theo tứ tự first name Z->A, last name A->Z, last name Z->A, hoặc là sắp xếp theo quê quán, đất nước… thì có lẽ phải clone đoạn source code sort ra dài dài.

    Trong trường hợp này, nếu sử dụng KeyPaths để implement logic sort, chúng ta có thể rút ngắn source code đi rất nhiều, và điều kiện sort cũng có thể tuỳ biến nhiều hơn.

    Define enum thứ tự sort:

    enum Order {
        case ascending
        case descending
    }
    

    Override logic sort

    extension Sequence {
        func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, order: Order) -> [Element] {
            return sorted { a, b in
                switch order {
                case .ascending:
                    return a[keyPath: keyPath] < b[keyPath: keyPath]
                case .descending:
                    fallthrough
                default:
                    return a[keyPath: keyPath] > b[keyPath: keyPath]
                }
            }
        }
    }
    

    Với cách làm sử dụng KeyPaths, thì ta có thể thoải mái sort listPersons dựa trên các điều kiện sort khác nhau như sort theo tên, theo họ, ascending hoặc descending… Ví dụ: var sortedPersons = listPersons.sorted(by: \.firstName, order: .descending)

    Note: ở đây dùng extension của protocol Sequencesortedfilter thực chất là được define ở Sequence protocol

    Ví dụ về cách filter các điều kiện của list persons: Xem trong file playground cuối bài.

    Tài liệu có tham khảo:

    • Swift’s document
    • nshipster.com
    • hackingwithswift.com
    • swiftbysundell.com

    Trong phần tiếp theo, chúng ta sẽ đi vào một vài ứng dụng thực tế hơn sử dụng KeyPaths.

    Sample playground: Swift_KeyPaths.playground

  • Swift—Codable

    Swift—Codable

    Codable được giới thiệu cùng với phiên bản 4.0 của Swift, đem lại sự thuận tiện cho người dùng mỗi khi cần encode/decode giữa JSON và Swift object.

    Codable là alias của 2 protocols: Decodable & Encodable

    • Decodable: chuyển data dạng string, bytes…sang instance (decoding/deserialization)
    • Encodable: chuyển instace sang string, bytes… (encoding/serialization)

    Table of contents

    Swift Codable basic

    Chúng ta sẽ đi vào ví dụ đầu tiên của Swift Codable, mục tiêu sẽ là convert đoạn JSON sau sang Swift object (struct)

    {
        "name": "NhatHM",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
    

    Cách làm:

    Đối với các JSON có dạng đơn giản thế này, công việc của chúng ta chỉ là define Swift struct conform to Codable protocol cho chính xác, sau đó dùng JSONDecoder() để decode về instance là được. Note: Nếu không cần phải chuyển ngược lại thành string/bytes (không cần encode) thì chỉ cần conform protocol Decodable là đủ.

    Implementation:

    Define Swift struct:

    struct Person: Codable {
        let name: String
        let age: Int
        let page: URL
        let bio: String?
    }
    

    Convert string to instance:

    // Convert json string to data
    var data = Data(json.utf8)
    let decoder = JSONDecoder()
    // Decode json with dictionary
    let personEntity = try? decoder.decode(Person.self, from: data)
    if let personEntity = personEntity {
        print(personEntity)
    }
    

    Chú ý: đối với dạng json trả về là array như dưới:

    [{
        "name": "NhatHM",
        "age": 29,
        "page": "https://magz.techover.io/"
    },
    {
        "name": "RioV",
        "age": 19,
        "page": "https://nhathm.com/"
    }]
    

    thì chỉ cần define loại data sẽ decode cho chính xác là được:

    let personEntity = try? decoder.decode([Person].self, from: data)
    

    Ở đây ta đã định nghĩa được data decode ra sẽ là array của struct Person.

    Swift Codable manual encode decode

    Trong một vài trường hợp, data trả về mà chúng ta cần có thể nằm trong một key khác như dưới:

    {
        "person": {
            "name": "NhatHM",
            "age": 29,
            "page": "https://magz.techover.io/"
        }
    }
    

    Trong trường hợp này, nếu define Swift struct đơn giản như phần 1 chắc chắn sẽ không thể decode được. Do đó cách làm sẽ là define struct sao cho nó tương đồng nhất có thể với format của JSON. Ví dụ như đối với JSON ở trên, chúng ta có thể define struct như dưới:

    Implementation

    struct PersonData: Codable {
        struct Person: Codable {
            let name: String
            let age: Int
            let page: URL
            let bio: String?
        }
    
        let person: Person
    }
    

    Đối với trường hợp này, chúng ta vẫn sử dụng JSONDecoder() để decode string về instance như thường, tuy nhiên lúc sử dụng value của struct thì sẽ hơi bất tiện:

    let data = Data(json.utf8)
    let decoder = JSONDecoder()
    let personEntity = try? decoder.decode(PersonData.self, from: data)
    if let personEntity = personEntity {
        print(personEntity)
        print(personEntity.person.name)
    }
    

    Manual encode decode

    Đối với dạng JSON data như này, chúng ta còn có một cách khác để xử lý data cho phù hợp, dễ dùng hơn như dưới:

    Define struct (chú ý, lúc này không thể hiện struct conform to Codable nữa, mà sẽ conform to Encodable và Decodable một cách riêng biệt):

    struct Person {
        var name: String
        var age: Int
        var page: URL
        var bio: String?
    
        enum PersonKeys: String, CodingKey {
            case person
        }
    
        enum PersonDetailKeys: String, CodingKey {
            case name
            case age
            case page
            case bio
        }
    }
    

    Ở đây có một khái niệm mới là CodingKey. Về cơ bản, CodingKey chính là enum define các "key" mà chúng ta muốn Swift sử dụng để decode các value tương ứng. Ở đây key PersonKeys.person sẽ tương ứng với key "person" trong JSON string, các enum khác cũng tương tự (đọc thêm về CodingKey ở phần sau)

    Với trường hợp này, ta sử dụng nestedContainer để đọc các value ở phía sâu của JSON, sau đó gán giá trị tương ứng cho properties của Struct.

    Implementation

    extension Person: Decodable {
        init(from decoder: Decoder) throws {
            let personContainer = try decoder.container(keyedBy: PersonKeys.self)
    
            let personDetailContainer = try personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)
            name = try personDetailContainer.decode(String.self, forKey: .name)
            age = try personDetailContainer.decode(Int.self, forKey: .age)
            page = try personDetailContainer.decode(URL.self, forKey: .page)
            bio = try personDetailContainer.decodeIfPresent(String.self, forKey: .bio)
        }
    }
    

    Đây chính là phần implement để đọc ra các value ở tầng sâu của JSON, sau đó gán lại vào các properties tương ứng của struct. Các đoạn code trên có ý nghĩa như sau:

    • personContainer là container tương ứng với toàn bộ JSON string
    • personDetailContainer là container tương ứng với value của key person
    • Nếu có các level sâu hơn thì ta lại tiếp tục sử dụng nestedContainer để đọc sau vào trong
    • Nếu một property nào đó (key value nào đó của json) mà có thể không trả về, thì sử dụng decodeIfPresent để decode (nếu không có value thì gán bằng nil)

    Note: Đối với việc Encode thì cũng làm tương tự, tham khảo source code đi kèm (link cuối bài)

    Với cách làm này, thì khi gọi đến properties của struct, đơn giản ta chỉ cần personEntity.name là đủ.

    Swift Codable coding key

    Trong đa số các trường hợp thì client sẽ sử dụng json format mà server đã định sẵn, do đó có thể gặp các kiểu json có format như sau:

    {
        "person_detail": {
            "first_name": "Nhat",
            "last_name": "Hoang",
            "age": 29,
            "page": "https://magz.techover.io/"
        }
    }
    

    Đối với kiểu json như này, để Struct có thể codable được thì cần phải define properties dạng person_detail, first_name. Điều này vi phạm vào coding convention của Swift. Trong trường hợp này chúng ta sử dụng Coding key để mapping giữa properties của Struct và key của JSON.

    Implementation

    struct Person {
        var firstName: String
        var lastName: String
        var age: Int
        var page: URL
        var bio: String?
    
        enum PersonKeys: String, CodingKey {
            case person = "person_detail"
        }
    
        enum PersonDetailKeys: String, CodingKey {
            case firstName = "first_name"
            case lastName = "last_name"
            case age
            case page
            case bio
        }
    }
    

    Với trường hợp này, khi sử dụng đoạn code decode như

    var personDetailContainer = personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)

    hay

    try personDetailContainer.encode(firstName, forKey: .firstName)

    thì khi đó, Swift sẽ sử dụng các key json tương ứng là person_detail hoặc first_name.

    Cách implement cụ thể tham khảo file playground cuối bài

    Swift Codable key decoding strategy

    Nếu json format từ server trả về là snake case (example_about_snake_case) thì chúng ta không cần phải define Coding key, mà chỉ cần dùng keyDecodingStrategy của JSONDecoder là đủ. Ví dụ:

    let json = """
    {
        "first_name": "Nhat",
        "last_name": "Hoang",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
    """
    
    struct Person: Codable {
        var firstName: String
        var lastName: String
        var age: Int
        var page: URL
        var bio: String?
    }
    
    let data = Data(json.utf8)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    
    let personEntity = try? decoder.decode(Person.self, from: data)
    if let personEntity = personEntity {
        print(personEntity)
    } else {
        print("Decode entity failed")
    }
    

    Với trường hợp này, Swift decoder sẽ tự hiểu để decode key first_name thành property firstName. Điều kiện duy nhất là sử dụng keyDecodingStrategyconvertFromSnakeCase và server trả về format JSON đúng theo format của snake case.

    Custom key decoding strategy

    Ngoài ra cũng có thể define custom keyDecodingStategy bằng cách sử dụng:

    jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in
       let key = /* logic for custom key here */
       return CodingKey(stringValue: String(key))!
    }
    

    Swift Codable date decoding strategy

    Trong rất nhiều trường hợp thì JSON trả về từ server sẽ bao gồm cả date time string. Và JSONDecoder cũng cung cấp phương pháp để decode date time từ string một cách nhanh gọn bằng dateDecodingStrategy. Ví dụ, với date time string đúng chuẩn 8601 thì chỉ cần define:

    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    

    là có thể convert date time dạng 1990-01-01T14:12:41+0700 sang Swift date time 1990-01-01 07:12:41 +0000 một cách đơn giản.

    Trong trường hợp muốn decode một vài string date time có format khác đi, thì có thể làm bằng cách:

    func dateFormatter() -> DateFormatter {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
        return formatter
    }
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(dateFormatter())
    

    Với ví dụ này, thì chúng ta có thể tự define date formatter và sử dụng cho dateDecodingStrategy của JSONDecoder

    Swift Codable nested unkeyed container

    {
        "persons": [
            {
                "name": "NhatHM",
                "age": 29,
                "page": "https://magz.techover.io/"
            },
            {
                "name": "RioV",
                "age": 19,
                "page": "https://nhathm.com/"
            }
        ]
    }
    

    Với dạng JSON như trên thì values của persons là một array chứa các thông tin của person. Và các item trong array thì không có key tương ứng. Do đó để Decode được trường hợp này thì ta dùng nestedUnkeyedContainer.

    Implementation

    extension ListPerson: Decodable {
        init(from decoder: Decoder) throws {
            let personContainer = try decoder.container(keyedBy: PersonKeys.self)
            listPerson = [Person]()
    
            var personDetailContainer = try personContainer.nestedUnkeyedContainer(forKey: .persons)
            while (!personDetailContainer.isAtEnd) {
                if let person = try? personDetailContainer.decode(Person.self) {
                    listPerson.append(person)
                }
            }
        }
    }
    

    Sample playground: Swift_Codable.playground

  • iOS — Play RTSP Streaming

    iOS — Play RTSP Streaming

    Hướng dẫn build IJK Player để play RTSP streaming

    Table of Contents

    Chuẩn bị môi trường

    • Cài đặt Homebrew ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    • Cài git brew install git
    • Cài yasm brew install yasm
    • Clone IJKPlayer từ github:
      • git clone https://github.com/RioV/ijkplayer.git ijkplayer-ios
        • Note: ở đây dùng /RioV/ijkplayer bởi vì đang tìm hiểu thấy IJKPlayer có issue, vậy nên folk sang một bản khác để tiện fix bug
        • Note: chú ý checkout source code về folder mà tên không có space, ví dụ: IJK Player => NG, IJK-Player => OK. Việc này sẽ ảnh hưởng đến tiến trình build lib, nếu như có space thì build sẽ bị lỗi.
      • cd ijkplayer-ios
      • git checkout -B latest k0.8.8 version lấy theo release tag của IJKPlayer Release Nếu sau này sửa lỗi lib ở branch develop thì sẽ là git checkout -B develop

    Build lib IJKPlayer

    • cd config
    • rm module.sh
    • ln -s module-lite.sh module.sh -> việc này sẽ bỏ module.sh default và thay vào đó sử dụng moule-lite.sh nhằm giảm binary size
    • Để build lib support RTSP thì cần chỉnh sửa file module-lite.sh như sau:
      • Xoá: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --disable-protocol=rtp"
      • Add: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=rtp"
      • Add: export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtsp" (nên để trong section của demuxer)
    • cd ..
    • ./init-ios.sh
    • cd ios
    • ./compile-ffmpeg.sh clean
    • Sửa file ijkplayer-ios/ios/compile-ffmpeg.sh
      • Chuyển:

        FF_ALL_ARCHS_IOS6_SDK="armv7 armv7s i386" FF_ALL_ARCHS_IOS7_SDK="armv7 armv7s arm64 i386 x86_64" FF_ALL_ARCHS_IOS8_SDK="armv7 arm64 i386 x86_64"

        Thành

        FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"

    • ./compile-ffmpeg.sh all
      • Note: Với câu lệnh ./compile-ffmpeg.sh all thì rất dễ xảy ra lỗi nếu như source code đang ở trong directoy có chứa space. Ví dụ: working directory là /Documents/JLK Player thì sẽ lỗi, để fix thì chuyển thành /Documents/IJKPlayer

    Tích hợp IJKPlayer vào project

    • Add IJKPlayer vào project: File -> add File to "Project" -> chọn ijkplayer-ios/ios/IJKMediaPlayer/IJKMediaPlayer.xcodeproj
    • Chọn: Application’s target.
      • Vào: Build Phases -> Target Dependencies -> Chọn IJKMediaFramework
      • Chọn IJKMediaPlayer.xcodeproj, chọn target IJKMediaFramework và build.
      • Vào: Build Phases -> Link Binary with Libraries -> Thêm:
        • libc++.tbd
        • libz.tbd
        • libbz2.tbd
        • AudioToolbox.framework
        • UIKit.framework
        • CoreGraphics.framework
        • AVFoundation.framework
        • CoreMedia.framework
        • CoreVideo.framework
        • MediaPlayer.framework
        • MobileCoreServices.framework
        • OpenGLES.framework
        • QuartzCore.framework
        • VideoToolbox.framework

    Sample

    Sử dụng đoạn source code sau để play thử RTSP stream bằng IJKPlayer

    import UIKit
    import IJKMediaFramework
    
    class IJKPlayerViewController: UIViewController {
        var player: IJKFFMoviePlayerController!
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let options = IJKFFOptions.byDefault()
            let url = URL(string: "rtsp://170.93.143.139/rtplive/470011e600ef003a004ee33696235daa")
            guard let player = IJKFFMoviePlayerController(contentURL: url, with: options) else {
                print("Create RTSP Player failed")
                return
            }
    
            let autoresize = UIView.AutoresizingMask.flexibleWidth.rawValue |
                UIView.AutoresizingMask.flexibleHeight.rawValue
            player.view.autoresizingMask = UIView.AutoresizingMask(rawValue: autoresize)
    
            player.view.frame = self.view.bounds
            player.scalingMode = IJKMPMovieScalingMode.aspectFit
            player.shouldAutoplay = true
            self.view.autoresizesSubviews = true
            self.view.addSubview(player.view)
            self.player = player        
            self.player.prepareToPlay()
        }
    }
  • Fresher Training—iOS Basic Day 2

    Fresher Training—iOS Basic Day 2

    Today topic:

    • App Life cycle
    • View Controller Life cycle
    • UIView

    Exerices:

    Exercise 01: App Life Cycle
    • Hãy phân tích những delegate sẽ được gọi trong những trường hợp sau:
      • Khi user quit app từ fast app switcher (multi task)
      • Khi app bị crash do source code
      • Khi app bị suspended
      • Khi user mở app khác (bằng cách tap vào notification của app khác hoặc open app khác từ app hiện tại)

    4 điểm

    Exercise 02: View Life Cycle

    • Hãy liệt kê những methods (delegate) được gọi khi
      • Push screen B từ screen A
      • Back lại screen A từ screen B
      • User tap button Home của iPhone để cho app xuống background rồi mở lại app.

    2 điểm

    Exercise 03: View

    • Hãy phân tích điểm khác nhau và giống nhau giữa frame và bounds. Nhất là trong trường hợp như ảnh.
      • Biết toạ độ của A(x: 10, y: 55)
      • Biết toạ độ của B(x: 60, y: 5)
      • Biết toạ độ của C(x: 110, y: 55)
      • Biết toạ độ của D(x: 60, y: 105)
      • AC vuông góc BD

    2 điểm

  • Fresher Training—iOS Swift Day 4

    Fresher Training—iOS Swift Day 4

    Today topic:

    • Encoding & Decoding Types
    • Asynchronous Closures & Memory Management
    • Value Types & Value Semantics
    • Protocol-Oriented Programming

    Tham khảo: https://nhathm.com/swift-closure-escaping-autoclosure-b6cc22729e7

    Exercises:

    Exercise 01: ENCODING & DECODING

    Make this source code Codeable

    struct Student {
        var name: String
        var age: Int
        var study: [StudyClass]
    }
    
    struct StudyClass {
        var className: String
        var classCode: String
    }

    Điểm: 1

    Exercise 02: ENCODING & DECODING

                Decoding this JSON

    [{

             “country”: {

                  “country_capital”: {

                      “capital_name”: “Ha Noi”,

                      “population”: 5000000

                  },

                  “country_name”: “Viet Nam”

             }

         },

         {

             “country”: {

                  “country_capital”: {

                      “capital_name”: “Tokyo”,

                      “population”: 4000000

                  },

                  “country_name”: “Japan”

             }

         }

    ]

    Điểm: 2

    Exercise 03: MEMORY MANAGEMENT

        What wrong with below code? Fix it

    class People {
        let name: String
        let email: String
        var bike: Bike?
        
        init(name: String, email: String) {
            self.name = name
            self.email = email
        }
        
        deinit {
            print("People deinit \(name)!")
        }
    }
    
    class Bike {
        let id: Int
        let type: String
        var owner: People?
        
        init(id: Int, type: String) {
            self.id = id
            self.type = type
        }
        
        deinit {
            print("Bike deinit \(type)!")
        }
    }
    
    var owner: People? = People(name: "NhatHM", email: "[email protected]")
    var bike: Bike? = Bike(id: 1, type: "Honda")
    
    owner?.bike = bike
    bike?.owner = owner
    
    owner = nil
    bike = nil

    Điểm: 2

    Exercise 04: PROTOCOL

    Making this source code runable

    var color = UIColor.aliceBlue
    color = UIColor.oceanBlue
    

    Điểm: 2

    Exercise 05: generics

    What is generics? Show me an example

    Điểm: 1