Blog

  • 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.
  • How to write clean code (P1)

    How to write clean code (P1)

    Mặc dù lâu không viết gì vì đang bận làm đồ án, nhưng sau khi khi hoàn thành 1 task về refactor vài nghìn dòng code đã là động lực để em phải ngoi lên viết bài viết này, để chia sẻ về cách viết code mà bản thân đang áp dụng.
    Phần 1 của bài viết sẽ giới thiệu về cách cải thiện code đơn giản và dễ dàng nhất: Đặt tên

    Nội dung bài viết

    • Tầm quan trọng của clean code
    • Meaningful names

    Tầm quan trọng của clean code

    Tại sao clean code lại quan trọng?

    • Những dòng code cũng chính là 1 bản design document, vì vậy nếu code được viết 1 cách gọn gàng, dễ hiểu thì khi 1 người mới đọc code thì sẽ dễ dàng nắm được logic, flow của code – Sếp HoaND1 said.
    • Việc viết code 1 cách clean sẽ giảm thiểu thời gian dể người khác, hoặc chính bạn sau 1 thời gian đọc lại, có thể nhanh chóng hiểu được; tránh gây ra những hiểu lầm về mặt logic.
    Clean code cũng giúp tránh việc đồng nghiệp của bạn phải thốt lên “WTF” nữa

    Meaningful names

    Việc chọn 1 cái tên sao cho truyền tải đủ ý định của biến, hàm, class, … đôi khi sẽ mất nhiều thời gian, những nó sẽ tiết kiệm được nhiều thời gian hơn so với thời gian bị mất.
    Một cái tên tốt nên thể hiện tại sao biến này, hàm này, … tồn tại, nó có tác dụng thế nào, được sử dụng thế nào mà không cần tốn thêm quá nhiều thời gian để đi tìm hiểu chúng làm gì.

    let temp: Int = 0
    func check() -> Bool {}

    Ví dụ với những cái tên kiểu này, đồng nghiệp của bạn sẽ phải lặn lội mọi ngóc ngách nơi biến, hàm được gọi để có thể hình dung 1 cái nhìn mơ hồ xem chúng có tác dụng gì… Hãy biết thương đồng nghiệp của bạn.
    Ok, giờ thì đi tìm hiểu 1 vài cách đặt tên cho tốt

    Coding Conventions

    Trong swift, có 1 vài coding conventions cơ bản trong việc đặt tên như:

    • Tên biến, tên class nên là danh từ, tên hàm nên bắt đầu bằng 1 động từ
    • Sử dụng camel case (tránh dùng snakeCase)
    • Viết hoa chữ cái đầu cho các kiểu dữ liệu type, protocol, viết thường cho các thứ khác. …

    Hãy chọn những cái tên cụ thể với hành vi

    func sortListObject() {}

    Như tên hàm này không được cụ thể lắm, vì qua cái tên không thể hiện được là object sẽ sort theo kiểu gì?
    Nếu sort theo tên thì nên sửa lại thành sortListObjectById() chẳng hạn, hoặc sortListObjectByName() nếu sort theo tên.

    Nếu có điều gì quan trọng về 1 biến, 1 hàm mà người đọc nên biết, thì nên thêm thông tin đó vào tên.

    var id: String // "af84ef845cd8"
    -> var hexID: String

    Tránh những cái tên chung chung

    Những cái tên mơ hồ như temp, tmp, i, j, … trong đa số trường hợp thường không thể hiện được quá nhiều thông tin. Vì vậy, hãy chọn 1 cái tên truyền tải nhiều ý nghĩa hơn thay vì chúng.

    Cũng có 1 vài trường hợp những cái tên chung chung có thể chấp nhận. Ví dụ như:

    if left < right {
       let temp = left
       left = right
       right = left
    }

    Trong những trường hợp như thế này, việc sử dụng tên “temp” cũng ổn. Bởi mục đích của nó là lưu trữ tạm thời,với thời gian tồn tại chỉ vài dòng, nó cũng không có nhiệm vụ nào khác. Nó không được chuyển sang chức năng khác hoặc được đặt lại, hoặc sử dụng nhiều lần. Và quan trọng hơn là người khác vẫn có thể dễ dàng hiểu.

    Hãy chọn những cái tên ý nghĩa. Nếu bạn định sử dụng những cái tên chung chung như “temp”, “tmp”, … hãy chắc chắn rằng có 1 lí do hợp lí cho việc đó.

    Tránh những tên quá dài

    • Khi chọn tên, nên tránh những cái tên quá dài, bởi vì chúng rất khó đọc và khó để nhớ
    newNavigationControllerWrappingViewControllerForDataSourceOfClass
    • Đôi khi những cái tên cũng chứa những thông tin bị thừa, mà nếu bỏ đi thì cũng không ảnh hưởng gì tới ý nghĩa.
    func convertToString() 
    func toString() // Vẫn truyền tải đủ ý nghĩa

    Tránh những cái tên hiểu lầm

    • Tránh đặt những tên kiểu getXYZ(), sizeXYZ(), … nếu bên trong thân hàm xử lý những logic có độ phức tạp tính toán lớn, bởi những cái tên này thường dễ khiến người đọc hiểu lầm là hàm này "lightweight" nên dùng 1 cách thường xuyên.
    // Tên hàm gây hiểu lầm
    func getNumberOfSelectedObjects() -> Int {
       Loop hundreds time to calculate ...
    }
    • 1 vài kiểu gây hiểu lầm khác như đặt tên biến là objectIndexs nhưng lại trả về kiểu Object, …
    • Những cái tên trả về kiểu boolean thì tên biến, hàm nên bắt đầu bằng các từ như is, has, can, should, … để làm cho rõ nghĩa.
    • 1 số cái tên thông dụng khác như max, min cho giới hạn; first, last cho thứ tự,…

    Kết luận

    Hãy cố gắng chọn những cái tên truyền đạt đầy đủ ý nghĩa và dễ hiểu đối với mọi người.

    Tham khảo

    Sách "The Art of Readable Code" – O’Reilly
    Sách "Clean Code" – Uncle Bob

  • Tạo HTTP Request với URLSession

    Tạo HTTP Request với URLSession

    Alamofire là thư viện về HTTP Networking được biết đến nhiều nhất trong lập trình iOS sử dụng Swift. Vậy nếu không sử dụng Alamofire thì chúng ta thực hiện các HTTP request như thế nào? Dưới đây là một trong những cách để thực hiện các request với URL Loading System được cung cấp ở ngay thư viện cơ bản nhất Foundation của Apple.

    URL Loading System bao gồm các cấu trúc, giao thức để làm việc với URL và giao tiếp với server. Ở bài viết này chúng ta sẽ làm việc chủ yếu với lớp URLSession.

    1. Thực hiện Request đơn giản với URLSession

    Lấy ví dụ thực hiện search key word “Son Tung MTP” với iTunes Store API, chúng ta cần thực hiện một request với thông tin sau đây:

     let iTunesHostURL = "https://itunes.apple.com/search?term=Son+Tung+MTP"
     guard let url = URL(string: iTunesHostURL) else {
         print("URL Not valid")
         return
     } 

    Để thực thi request trên, ta sử dụng URLSession như sau:

     let task1 = session.dataTask(with: url, completionHandler: { data, response, error in
         if let error = error {
             print(error)
         }
         if let response = response {
             print(response)
         }
         if let data = data, let dataString = String(data: data, encoding: .utf8) {
             print(dataString)
         }
     })
     task1.resume() 

    Nhân vật chính là phương thức dataTask(with:completionHandler:) của URLSession, ở đây ta chỉ cần khởi tạo một URL với đường dẫn sẵn có, sau đó xử lý dữ liệu bên trong completionHandler block của phương thức trên. Với TH trên, thông tin in ra của response (HTTP status code, headers) hiển thị trên output log như sau:

    { Status Code: 200, Headers { "Cache-Control" = ( "max-age=86400" ); "Content-Disposition" = ( "attachment; filename=1.txt" ); "Content-Encoding" = ( gzip ); "Content-Length" = ( 7849 ); "Content-Type" = ( "text/javascript; charset=utf-8" ); Date = ( "Tue, 08 Dec 2020 08:55:43 GMT" ); "Strict-Transport-Security" = ( "max-age=31536000" ); Vary = ( "Accept-Encoding" ); "apple-originating-system" = ( MZStoreServices ); "apple-seq" = ( 0 ); "apple-timing-app" = ( "277 ms" ); "apple-tk" = ( false ); b3 = ( "ab8b7390acfee63d8fddd6db794d7776-27de63d447f98e57" ); "x-apple-application-instance" = ( 2007320 ); "x-apple-application-site" = ( ST11 ); "x-apple-jingle-correlation-key" = ( VOFXHEFM73TD3D6523NXSTLXOY ); "x-apple-orig-url" = ( "https://itunes.apple.com/search?term=Son+Tung+MTP" ); "x-apple-partner" = ( "origin.0" ); "x-apple-request-uuid" = ( "ab8b7390-acfe-e63d-8fdd-d6db794d7776" ); "x-apple-translated-wo-url" = ( "/WebObjects/MZStoreServices.woa/ws/wsSearch?term=Son+Tung+MTP&urlDesc=" ); "x-b3-spanid" = ( 27de63d447f98e57 ); "x-b3-traceid" = ( ab8b7390acfee63d8fddd6db794d7776 ); "x-cache" = ( "TCP_MISS from a113-171-230-176.deploy.akamaitechnologies.com (AkamaiGHost/10.2.2.1-31386017) (-)" ); "x-cache-remote" = ( "TCP_MISS from a23-67-57-164.deploy.akamaitechnologies.com (AkamaiGHost/10.2.2.1-31386017) (-)" ); "x-content-type-options" = ( nosniff ); "x-true-cache-key" = ( "/L/itunes.apple.com/search vcd=2897 ci2=term=Son+Tung+MTP///" ); "x-webobjects-loadaverage" = ( 0 ); } }

    Thông tin data trả về sau khi được convert thành String hiển thị lên output log như sau:

    { "resultCount":48, "results": [ {"wrapperType":"track", "kind":"song", "artistId":705007874, "collectionId":1380326325, "trackId":1380326334, "artistName":"Sơn Tùng M-TP", "collectionName":"Lạc Trôi - Single", "trackName":"Lạc Trôi", "collectionCensoredName":"Lạc Trôi - Single", "trackCensoredName":"Lạc Trôi", "artistViewUrl":"https://music.apple.com/us/artist/s%C6%A1n-t%C3%B9ng-m-tp/705007874?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/l%E1%BA%A1c-tr%C3%B4i/1380326325?i=1380326334&uo=4", "trackViewUrl":"https://music.apple.com/us/album/l%E1%BA%A1c-tr%C3%B4i/1380326325?i=1380326334&uo=4", "previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview118/v4/d3/ee/c7/d3eec748-a929-3ed5-4850-f79a13c34dbb/mzaf_4185904769253643682.plus.aac.p.m4a", "artworkUrl30":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/30x30bb.jpg", "artworkUrl60":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/60x60bb.jpg", "artworkUrl100":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/100x100bb.jpg", "collectionPrice":1.29, "trackPrice":1.29, "releaseDate":"2016-12-31T12:00:00Z", "collectionExplicitness":"notExplicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":1, "trackNumber":1, "trackTimeMillis":232889, "country":"USA", "currency":"USD", "primaryGenreName":"Alternative", "isStreamable":true}...]}

    2. Thực hiện Request tuỳ chỉnh HTTP Request Methods

    Có tổng cộng 9 loại HTTP Request Methods, hai loại phổ biến nhất là GET và POST. Đối với các API được định nghĩa Method, chúng ta sẽ sử dụng thêm URLRequest để tuỳ chỉnh các methods trên. Tiếp tục với ví dụ ở trên, ta sẽ định nghĩa HTTP method là GET như sau:

     var request = URLRequest(url: url)
     request.httpMethod = "GET"

    Đối với các HTTP Methods khác, ta chỉ cần thay đổi giá trị httpMethod dưới dạng string như “POST”, “PUT”, …
    Để gửi request với URLRequest, ta sử dụng phương thức dataTask(with:completionHandler:), kết quả vẫn không đổi so với ví dụ request ở trên:

     let task = session.dataTask(with: request, completionHandler: { data, response, error in
         if let error = error {
             print(error)
         }
         if let response = response {
             print(response)
         }
         if let data = data, let dataString = String(data: data, encoding: .utf8) {
             print(dataString)
         }
     })
     task.resume() 

    3. Thực hiện Request tuỳ chỉnh HTTP Header

    Đối với nhiều hệ thống, việc yêu cầu thêm các thông tin của Client khi gửi request lên là bắt buộc, và sẽ được truyền lên trên HTTP Headers của Request. Ví dụ với một request yêu cầu client gửi lên các thông tin Authorization, Content-Type, Accept-Language, ta sẽ tuỳ chỉnh URLRequest như sau:

     var request = URLRequest(url: url)
     request.httpMethod = "GET"
     request.setValue("Basic bGluaG5iMTpsaW5obmIx", forHTTPHeaderField: "Authorization")
     request.setValue("application/json; charse=UTF-8", forHTTPHeaderField: "Content-Type")
     request.setValue("en-US", forHTTPHeaderField: "Accept-Language") 

    4. Thực hiện Request tuỳ chỉnh HTTP Body

    HTTP Body có rất nhiều kiểu, ví dụ như raw data (string, json), x-www-form-urlencoded, form-data, … Bài viết sẽ tập trung vào một số kiểu body phổ biến:

    4.1 HTTP Body với x-www-form-urlencoded

    x-www-form-urlencoded có dạng key-value và sẽ được mã hoá theo dạng URL Encoding (Percent Encoding) khi gửi request. Để gửi một request với body là x-www-form-urlencoded ta cần cài đặt HTTP Header như sau:

     request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

    Đối với phần body, lấy ví dụ dữ liệu truyền lên như sau:

     let requestDictionary = ["term":"Son Tung MTP", "country":"VN", "lang":"vi_vn"]

    Chúng ta sẽ sử dụng URLComponent để chứa các thành phần key-value, sau đó encode thành URL

     var requestBodyComponent = URLComponents()
     requestBodyComponent.queryItems = [URLQueryItem(name: "term", value: "Son Tung MTP"),
                                                URLQueryItem(name: "country", value: "VN"),
                                                URLQueryItem(name: "lang", value: "vi_vn")] 
    print(requestBodyComponent.string) // print -> "?term=Son%20Tung%20MTP&country=VN&lang=vi_vn" 

    Sau đó, ta chỉ cần convert URL Encoded String thành Data và truyền vào body của request:

     request.httpBody = requestBodyComponent.query?.data(using: .utf8) 

    4.2 HTTP Body với JSON

    JSON là một kiểu dữ liệu rất phổ biến và được hỗ trợ rất tốt. Do đó, việc sử dụng request với JSON cũng rất dễ dàng. Vẫn với ví dụ dictionary ở trên, chúng ta muốn truyền đi ở dạng Json như sau “{\”country\”:\”VN\”,\”lang\”:\”vi_vn\”,\”term\”:\”Son Tung MTP\”}”:

     request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    let requestDictionary = ["term":"Son Tung MTP", "country":"VN", "lang":"vi_vn"]
     guard let jsonData = try? JSONEncoder().encode(requestDictionary) else {
         print("Error: Failed to JSON data")
         return
     } 
     request.httpBody = jsonData

    Chúng ta có thể sử dụng nhiều kiểu dữ liệu đầu vào để encode thành json Data với phương thức JSONEncoder().encode(_ value: T), chỉ cần kiểu dữ liệu đó có thuộc tính Encodable. Vì vậy, các lớp để chứa dữ liệu cho request body dạng JSon thường được kế thừa Encodable hoặc rộng hơn là Codable.

    4.3 HTTP Body với form data

    Kiểu dữ liệu multipart/form-data thường được sử dụng khi request có chứa các file đính kèm cần upload (Ví dụ: JPG, PNG, …). Lấy ví dụ với trường hợp cần upload một UIImage dạng PNG:

     let uploadImage = UIImage(named: "sample")

    Trong trường hợp này, HTTP Header và Body sẽ có dạng như sau:

    Content-Type:multipart/form-data; boundary=--26142EB6-EDB0-4F36-A4EE-079B11F200C3
    --26142EB6-EDB0-4F36-A4EE-079B11F200C3
    
    Content-Disposition: form-data; name="image_field"; filename="sample.jpg"
    Content-Type: image/jpg
    
    ￘¢\u{10}䩆䥆\u{01}\0£Œ䕸楦\0䵍*\0\u{08}\u{05}Ē\u{03}\0\u{01}\u{01}\0Ě\u{05}\0\u{01}\0Jě\u{05}\0\u{01 // Image Data
    --26142EB6-EDB0-4F36-A4EE-079B11F200C3--

    Đầu tiên, với header sẽ có format như sau:

    Content-Type:multipart/form-data; boundary=\(boundary)

    Giá trị boundary là free defined bởi người dùng và tuân theo một số ràng buộc.

    Đổi với phần body, mỗi một part sẽ có format như sau:

    --\(boundary)  // Start part 1
     Content-Disposition: form-data; name="\(partName)"
    
    \(partData)
    --\(boundary) // Start part 2
     Content-Disposition: form-data; name="\(partName)"
     Content-Type: \(contentType) // application/json
    
    \(partData)
    --\(boundary) // Start part 3
     Content-Disposition: form-data; name="\(partName)"; filename="\(fileNameOnlyForFile)"
     Content-Type: \(contentType) // image/jpg
    
    \(partData)

    Trong đó, trường filename là optional, chỉ sử dụng khi upload file, đối với dữ liệu plain thì không cần thiết. Trường contentType có một số loại như mimeType của image, application/json hoặc không cần thiết đối với plain text. partData chính là value của tham part cần truyền lên dưới dạng Data.

    Ở cuối cùng của HTTP Body, ta sẽ thêm một block nữa để đánh dấu kết thúc của phần body:

    --\(boundary)--

    Sample code sẽ trông như sau:

     let uploadImage = UIImage(named: "sample")
     guard let imageData = uploadImage?.jpegData(compressionQuality: 1) else {
         return
     }
    
     let boundary = "Boundary-\(UUID().uuidString)"
     request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
     
     let httpBody = NSMutableData()
    // Loop following 5 block for each form part
     httpBody.appendString("--\(boundary)\r\n")
     httpBody.appendString("Content-Disposition: form-data; name=\"image_field\"; filename=\"sample.jpg\"\r\n")
     httpBody.appendString("Content-Type: image/jpg\r\n\r\n")
     httpBody.append(imageData)
     httpBody.appendString("\r\n")
    
    // Add the ending block for body
     httpBody.appendString("--\(boundary)--")
     
     request.httpBody = httpBody as Data 
     extension NSMutableData {
       func appendString(_ string: String) {
         if let data = string.data(using: .utf8) {
           self.append(data)
         }
       }
     } 

    5. Thực hiện Request download file

    Để thực hiện download một file, ta có thể sử dụng downloadTask(with:completionHandler:), dưới đây là một ví dụ download file jpg và hiển thị lên UI:

     let iTunesHostURL = "https://phongvu.vn/cong-nghe/wp-content/uploads/2018/07/dota_2_traxe_drow_ranger_art_milimalism_97328_1920x1080.jpg"
     guard let url = URL(string: iTunesHostURL) else {
         print("URL Not valid")
         return
     }
     
     let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in
         guard let localURL = localURL,
               let data = try? Data.init(contentsOf: localURL),
               let image = UIImage.init(data: data) else {
             print("Failed to read downloaded file to image")
             return
         }
         DispatchQueue.main.async {
             let imageview = UIImageView.init(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
             imageview.image = image
             imageview.contentMode = .scaleToFill
             self.view.addSubview(imageview)
         }
     }
     task.resume() 

    6. Một số chú ý khi sử dụng URLSession

    1. Completion Handler được thực thi ở delegate queue, do đó các cập nhật liên quan đến các thành phần thuộc UIKit (chỉ thực thi được trên main thread) cần đặc biệt chú ý khi sử dụng bên trong completionHandler.
    2. Kể từ iOS 9, mặc định các Request HTTP sẽ bị block theo chính sách AppTransportSecurity của Apple. Do đó nếu request không phải HTTPS mà là HTTP, cần cài đặt lại thông tin như sau trong info.plist
     <key>NSAppTransportSecurity</key>
     <dict>
         <key>NSAllowsArbitraryLoads</key>
         <true/>
     </dict> 

  • Ứng dụng đa ngôn ngữ cho APNs

    Ứng dụng đa ngôn ngữ cho APNs

    Để làm đa ngôn ngữ cho APNs rất đơn giản, đầu tiên chúng ta cần những thứ sau đây:
    1. File Localizable.strings define các localization
    2. Đăng ký Push Notification cho ứng dụng
    Ở đây mình mặc định hai công việc trên đã được hoàn thành, và tập trung vào việc xử lý đa ngôn ngữ cho APNs.

    Thông thường, một Notification đơn giản sẽ có nội dung như sau:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title":"Đây là tiêu đề thông báo",
              "body":"Còn đây chắc là nội dung thông báo"
           }
        }
    }
    Nội dung notification hiển thị trên device

    Như ví dụ trên, giá trị tương ứng với key titlebody sẽ được hiển thị trực tiếp vào tiêu đề và nội dung của notification như ảnh. Để áp dụng đa ngôn ngữ, ta sẽ thay thế titlebody bằng hai key khác là title-loc-keyloc-key, tiếp đó ta truyền giá trị của hai trường trên bằng key của localization đã define trong file Localizable.strings:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title-loc-key":"notification_title",
              "loc-key":"notification_content"
           }
        }
    }
    // Localizable.strings content
     "notification_title" = "Thông báo ";
     "notification_content" = "Nội dung thông báo"; 
    Nội dung notification hiển thị trên device

    Như vậy là ta xong với các Notification có nội dung cố định. Đối với các Notification có sử dụng thêm tham số ví dụ như “Bạn có 10 thông báo mới” hay “NhatHM đã ném tiền vào mặt bạn”, chúng ta sử dụng thêm hai key title-loc-argsloc-args, và setting như sau:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title-loc-key":"notification_title_args",
              "title-loc-args":["10"],
              "loc-key":"notification_content_agrs",
              "loc-args":["tinh thần"]
           }
        }
     }
    // Localizable.strings content
    "notification_title_args" = "Bạn có thêm %@ thông báo mới"; "notification_content_agrs" = "Nội dung thông báo với giá trị %@";
    Nội dung notification hiển thị trên device

    Để đề phòng trường hợp ứng dụng chưa định nghĩa các key trong file Localizable.strings, ta chỉ cần cung cấp thêm hai key titlebody và truyền vào giá trị mặc định cho Notification. Hai giá trị này sẽ được hiển thị khi ứng dụng client không tìm thấy strings được trả về trong Notification payload. Nên để đẹp trai thì Notification sẽ như thế này:

    // Notification payload
    {
        "aps":{
           "alert":{
              "title":"Đây là tiêu đề thông báo",
              "body":"Còn đây chắc là nội dung thông báo",
              "title-loc-key":"notification_title_args",
              "title-loc-args":["10"],
              "loc-key":"notification_content_agrs",
              "loc-args":["tinh thần"]
           }
        }
     }

    Như vậy là đủ để quẩy đa ngôn ngữ cho Notification rồi đấy 😀

  • Triển khai CI/CD với GitLab-CI (P1 – Cài đặt và khởi tạo)

    Triển khai CI/CD với GitLab-CI (P1 – Cài đặt và khởi tạo)

    Chú ý 1: Phạm vi của bài viết bao gồm việc triển khai CI/CD với GitLab-CI trực tiếp trên thiết bị MacOS, không sử dụng container

    Chú ý 2: Do nội dung của phần này hơi nhiều nên mình xin phép tách thành nhiều bài nhỏ để mọi người đọc đỡ bị mệt mỏi và dễ focus vào các phần muốn đọc hơn

    Bỏ qua các triết lý hay định nghĩa về CI/CD, mình xin phép đề cập luôn đến việc triển khai CI/CD . Để triển trai CI/CD với GitLab-CI, chúng ta cần làm các bước sau:

    1. Cài đặt gitlab-runner trên thiết bị node (MacOS, Window)
    2. Khởi tạo runner liên kết với GitLab Repository
    3. Cấu hình GitLab Runner với config.toml (optional)
    4. Cấu hình các jobs CI/CD với .gitlab-ci.yml

    1. Cài đặt gitlab-runner trên thiết bị node

    Để cài đặt gitlab-runner trên máy, chạy command sau trên terminal:

    > brew install gitlab-runner

    Ngoài ra, các bạn có thể tham khảo quá trình cài đặt trên doc của GitLab (https://docs.gitlab.com/runner/install/)

    Sau khi cài xong, bạn có để sử dụng command sau để kiểm tra phiên bản hiện tại trên máy:

    > gitlab-runner verify

    2. Khởi tạo runner liên kết với GitLab Repository

    Chú ý: Để truy cập các chức năng liên quan đến cài đặt GitLab-CI, hãy đảm bảo tài khoản được cấp quyền Maintainer hoặc cao hơn.

    Đầu tiên, chúng ta truy cập GitLab Repo cần chaỵ CI/CD, sau đó chọn Settings -> CI/CD -> Expand section Runners.

    Thông tin để đăng ký Runner sẽ hiển thị ở đây

    Ở đây, hãy chú ý đến hai giá trị là gitlab-ci coordinatorgitlab-ci token

    Sau đó. ta dùng command sau để bắt đầu khởi tạo Runner:

    > gitlab-runner register

    Tiếp đó, ta nhập các thông tin của Runner theo hướng dẫn

    Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
    > https://gitlab.techover.io/ (gitlab-ci coordinator ở trên kia)
    Please enter the gitlab-ci token for this runner:
    > oH5aKzZsqibPr******** (gitlab-ci token ở trên kia)
    Please enter the gitlab-ci description for this runner:
    > Techover IO Runner (Mô tả cho runner)
    Please enter the gitlab-ci tags for this runner (comma separated):
    > develop (gắn tag cho runner)
    Please enter the executor: ssh, virtualbox, docker+machine, kubernetes, parallels, shell, docker-ssh, docker-ssh+machine, custom, docker:
    > shell (tuỳ chọn executor, ở đây mình sẽ dùng shell)
     Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!  

    Như vậy là đã đăng ký xong runner với GitLab Repo, giờ chúng ta sẽ start runner bằng command sau:

    > gitlab-runner start 

    Reload lại page GitLab, ta sẽ thấy Runner xuất hiện trong danh sách Runners của Repo. Như vậy là đã hoàn thành việc khởi tạo runner cho Repo.

    3. Cấu hình GitLab Runner với config.toml (Optional)

    Bạn có thể skip qua phần 3 nếu không hứng thú, vì nếu không làm thì GitLab-Ci vẫn chạy bình thường, mà ở đây mình cũng chỉ giới thiệu qua thôi 😀 chi tiết mình sẽ cố gắng làm trong một phần khác trong tương lai . File config.toml bao gồm các cài đặt, tuỳ chỉnh cho runner chạy trên thiết bị. Ta có thể truy cập file config.toml với đường dẫn sau:

    > ~/.gitlab-runner/config.toml 

    Tại đây bạn có thể tuỳ chỉnh rất nhiều các Các chi tiết về việc cài đặt có thể tham khảo trên doc của GitLab: https://docs.gitlab.com/runner/configuration/advanced-configuration.html

    Ví dụ ở đây mình thêm cài đặt giới hạn size của file log lên 40MB (Mặc định là 4MB)

    4. Cấu hình các jobs CI/CD với .gitlab-ci.yml

    GitLab-CI không có cấu hình mặc định cho CI/CD, để cấu hình ta cần thêm file .gitlab-ci.yml vào thư mục gốc của Repo. File sử dụng YAML, có thể tham khảo cú pháp ở link sau: https://docs.gitlab.com/ce/ci/yaml/

    Dưới đây là ví dụ template file .gitlab-ci.yml của GitLab cho Repository sử dụng Swift

    # This file is a template, and might need editing before it works on your project.
    # Lifted from: https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/
    # This file assumes an own GitLab CI runner, setup on a macOS system.
     stages:
       - build
       - test
       - archive
       - deploy
     
    
     build_project:
       stage: build
       script:
         - xcodebuild clean -project ProjectName.xcodeproj -scheme SchemeName | xcpretty
         - xcodebuild test -project ProjectName.xcodeproj -scheme SchemeName -destination 'platform=iOS Simulator,name=iPhone 8,OS=11.3' | xcpretty -s
       tags:
         - ios_11-3
         - xcode_9-3
         - macos_10-13
     
    
     archive_project:
       stage: archive
       script:
         - xcodebuild clean archive -archivePath build/ProjectName -scheme SchemeName
         - xcodebuild -exportArchive -exportFormat ipa -archivePath "build/ProjectName.xcarchive" -exportPath "build/ProjectName.ipa" -exportProvisioningProfile "ProvisioningProfileName"
       only:
         - master
       artifacts:
         paths:
           - build/ProjectName.ipa
       tags:
         - ios_11-3
         - xcode_9-3
         - macos_10-13 

    Để apply các cài đặt trong file .gitlab-ci.yml, ta chỉ cần đẩy file lên origin. Sau đó Runner sẽ tự động nhận và chạy jobs theo những gì đã định nghĩa trong file.

    Để kiểm tra lịch sử chạy các Pipeline, chúng ta truy cập GitLab Repo chaỵ CI/CD, sau đó chọn CI/CD .

    Vậy là chúng ta đã hoàn tất quá trình cài đặt và khởi tạo Gitlab runner cho một Repository. Ở các bài viết tiếp theo, mình sẽ nói về việc cài cắm pipeline để chạy CI/CD cho các dự án thực tế. Xin cảm ơn các bạn đã theo dõi 😀

  • CoreData with MultiThreading

    CoreData with MultiThreading

    Nội dung

    • Tóm tắt CoreData
    • Sử dụng CoreData với MultiThreading
    • Debug CoreData MultiThreading
    • Kết luận

    Tóm tắt về CoreData:

    Các thành phần chính của CoreData:

    • Managed Object Model
    • Managed Object Context
    • Persistent store coordinator

    MultiThreading with CoreData:

    Khi khởi tạo managedObjectContext(MOC) thì sẽ có thể lựa chọn 1 trong 2 loại queue để khởi tạo MOC, đó là:

    • NSMainQueueConcurrencyType (main Thread)
    • NSPrivateQueueConcurrencyType (background Thread)

    NSMainQueueConcurrencyType chỉ có thể được sử dụng trên main queue.
    NSPrivateQueueConcurrencyType tạo ra 1 queue riêng để sử dụng. Vì queue này là private, nên chỉ có thể access queue thông qua hàm perform(_:)performAndWait(_:) của MOC .

    Nếu ứng dụng sử dụng nhiều thao tác data processing (parse JSON to data, …) thì việc sử dụng trên main queue sẽ gây ra block main. Khi đó, có thể khởi tạo 1 context dùng private queue và thực hiện xử lí data trên đó.

    Trước khi sử dụng CoreData với MultiThread, chú í đến điều Apple recommend:

    Hãy chắc chắn rằng MOC được sử dụng trên thread(queue) mà chúng được liên kết khi khởi tạo.

    Nếu MOC không được sử dụng trên thread(queue) mà chúng được liên kết, trong trường hợp MOC liên kết với mainQueue nhưng được sử dụng trên background thread, hoặc ngược lại, sẽ khiến app đôi lúc sẽ gặp những lỗi crash lạ.

    Vì vậy để chắc chắn MOC luôn được sử dụng trên thread mà MOC được liên kết, thì có thể sử dụng perform( _:) và performAndWait( _:) như sau:

    • perform( _:) và performAndWait( _:) sẽ tự động đưa đoạn code bên trong nó thực hiện trên queue mà context đó được khởi tạo -> Điều đó sẽ chắc chắn rằng context được sử dụng trên đúng queue.
    • perform(_:) sẽ thực hiện async hàm bên trong nó.
    • performAndWait(_:) sẽ thực hiện sync hàm bên trong nó -> Nó sẽ block thread hiện tại gọi đến hàm đó cho đến khi hàm bên trong thực hiện xong -> Không nên gọi trên main.

    Debug Concurrency:

    Để đảm bảo Context được chạy trên đúng luồng nó được liên kết khi khởi tạo, có thể bật debug CoreData Concurrency như sau:

    • Chọn Edit Scheme -> Run -> Thêm "-com.apple.CoreData.ConcurrencyDebug 1".
    • Khi bật debug này lên, nó sẽ dừng app của bạn lại tại nơi context bị dùng sai Thread.

    Ví dụ:

    • Khởi tạo 1 context bằng private queue:
    private(set) lazy var managedObjectContext: NSManagedObjectContext = {
       let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
       managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
       return managedObjectContext
    }()
    • Sử dụng 1 context trên main:
    func createNewEntity() {
       DispatchQueue.main.async {
           let user = User(context: self.manager.managedObjectContext)
           user.name = "Hoang Anh Tuan"
                
           let account = Account(context: self.manager.managedObjectContext)
           account.username = "sunlight"
           account.password = "123"
                
           user.account = account
           account.user = user
                
           do {
               try self.manager.managedObjectContext.save()
           } catch let error {
               print("Save error: \(error.localizedDescription)")
           }
        }
    }
    • Kết quả khi bật debug:
    Có thể sử dụng perform( _:) / performAndWait( _:) để giải quyết tình huống này.

    Kết luận:

    • Nên dùng background thread cho context để tránh block main thread.
    • Nên dùng các hàm perform( _:) và performAndWait( _:) để đảm bảo context được chạy trên đúng luồng.
    • Ngoài cách ở trên thì còn 1 vài cách như sử dụng child/parent context, nhưng sẽ không được đề cập ở bài viết này.
  • 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

  • Working with EAAcessory & NSStream

    Working with EAAcessory & NSStream

    Nội dung:

    • EAAcessory là gì?
    • Cấu hình project để làm việc với EAAcessory.
    • Lấy thông tin accessory
    • Tạo stream để gửi & nhận data
    • Gửi data đến accessory
    • Nhận data từ accessory

    EAAcessory là gì?

    • 1 đối tượng kiểu EAAcessory đại diện cho 1 thiết bị ngoại vi đang kết nối với app thông qua Lightning connector hoặc Bluetooth.
    • EAAcessory gồm các thuộc tính chứa các thông tin quan trọng về thiết bị ngoại vi: isConnected, name, manufacturer, serialNumber, protocols mà thiết bị ngoại vi dùng, firmware version, …
    • Ngoài ra, cũng có thể lấy macAddress của thiết bị ngoại vi:
     let macAddress = accessory.value(forKey: PrinterConstant.MACAddress) as? String
    • Từ những thông tin đó, bạn có thể mở 1 session tới thiết bị ngoại vi để trao đổi dữ liệu.

    Config Project:

    • Không phải cứ kết nối Bluetooth là app sẽ scan thấy hoặc lấy được thông tin của thiết bị ngoại vi đó.
    • Để app có thể giao tiếp với thiết bị ngoại vi, thì app cần khai báo các protocols mà thiết bị ngoại vi đó hỗ trợ vào Info.plist.

    Đọc thông tin accessory:

    • Sau khi config project và kết nốt với thiết bị ngoại vi thông qua Bluetooth, có thể bắt đầu đọc thông tin của thiết bị ngọai vi:

    Đầu tiên, import ExternalAccessory:

    import ExternalAccessory
    • Đọc thông tin:
    let listAccessoryAvailable = EAAccessoryManager.shared().connectedAccessories
    accessory = listAccessoryAvailable.first
    print(accessory?.name)
    print(accessory?.serialNumber)
    print(accessory?.protocolStrings)
    ...

    Tạo socket để gửi/nhận data:

    • Để gửi/nhận data đến thiết bị ngoại vi thì cần mở 1 socket đến nó.
    func openSession() {
        guard let accessory = self.accessory else {
            return
        }
            
        guard let protocolString = accessory.protocolStrings.first else {
            return
        }
            
        session = EASession(accessory: accessory, forProtocol: protocolString)
        if session != nil {
            session?.inputStream?.delegate = self
            session?.inputStream?.open()
            session?.inputStream?.schedule(in: .current, forMode: .default)
                
            session?.outputStream?.delegate = self
            session?.outputStream?.open()
            session?.outputStream?.schedule(in: .current, forMode: .default)
        }
    }

    conform to StreamDelegate để handle event của socket:

    extension ViewController: StreamDelegate {
        func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
            switch eventCode {
            case .openCompleted:
                print("Open session complete")
            case .hasBytesAvailable:
                print("Has bytes available")
            case .hasSpaceAvailable:
                print("Has space available")
            case .errorOccurred:
                let error = aStream.streamError
                print("Error occur")
            case .endEncountered:
                print("End stream")
                aStream.close()
                aStream.remove(from: .current, forMode: .default)
            default:
                return
            }
        }
    }

    Note:

    • Gửi data đến thiết bị ngoại vi thông qua outputStream, nhận data từ thiết bị ngoại vi thông qua inputStream.

    Gửi data:

    • Sau khi đã open được session, bắt đầu gửi data sang thiết bị ngoại vi thông qua outputStream:
    guard let outputStream = session?.outputStream else {
        return
    }
    
    outputStream.write(data, maxLength: 128)

    There is no firm guideline on how many bytes to write at one time. Although it may be possible to write all the data to the stream in one event, this depends on external factors, such as the behavior of the kernel and device and socket characteristics. The best approach is to use some reasonable buffer size, such as 512 bytes, one kilobyte, or a page size (four kilobytes) – Apple

    Nhận data:

    • Khi thiết bị ngoại vi gửi dữ liệu đến app của bạn, StreamDelegate sẽ trigger event hasSpaceAvailable, khi đó app sẽ đọc data thông qua inputStream:
    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
        switch eventCode {
    ...
            case .hasBytesAvailable:
                print("Has bytes available")
                
                let dataBuffer = [UInt8](repeating: 0, count: 128)
                guard let inputStream = session?.inputStream else {
                    return
                }
                while inputStream.hasBytesAvailable {
                    inputStream.read(UnsafeMutablePointer<UInt8>(mutating: dataBuffer), maxLength: 128)
                    print("Read Data: \(dataBuffer)")
    ...
         }
    }

    Xử lí notification khi connect/disconnect với thiết bị ngoại vi:

    • Để nhận được notification, app cần phải đăng kí nhận thông báo trước:
    EAAccessoryManager.shared().registerForLocalNotifications()
    • Tiếp theo là add Observer khi 1 accessory connect/disconnect.
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.accessoryDidConnect(notification:)),
                                           name: NSNotification.Name.EAAccessoryDidConnect,
                                            object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(self.accessoryDidDisconnect(notification:)),
                                           name: NSNotification.Name.EAAccessoryDidDisconnect,
                                           object: nil)
    • Lấy thông tin của thiết bị ngoại vi thay đổi trạng thái:
    guard let connectedAcessory = notification.userInfo?[EAAccessoryKey] as? EAAccessory else {
        return
    }

    Scan Accessory Available:

    • App có thể scan các accessory available ở gần và kết nối Bluetooth đến chúng từ trong app mà không cần phải mở System.
    • Đầu tiên, show view scan Accessory Available:
    EAAccessoryManager.shared().showBluetoothAccessoryPicker(withNameFilter: nil) { (error) in
        
    }
    • Bạn có thể tạo predicate để filter các accessory available.
    Nguồn: internet
    • Chọn 1 accessory để thực hiện kết nối, hàm sẽ trả về error nếu xảy ra lỗi; nếu không có lỗi tức là bạn đã connect thành công.
  • Tips to ask a good question or make a better request

    Tips to ask a good question or make a better request

    Gần đây, mình thấy rất nhiều các bạn trẻ gặp phải các lỗi cơ bản về kỹ năng giao tiếp. Cụ thể là cách đặt câu hỏi/đưa ra yêu cầu trợ giúp. Dẫn đến việc có khá nhiều questions/requests không nhận được câu trả lời, hoặc câu trả lời không đúng như mong muốn của người hỏi.

    Để tránh rơi vào những tình huống như vậy, mình sẽ chỉ cho các bạn một số tips dựa trên kinh nghiệm làm việc với nhiều đối tác, khách hàng cũng như đồng nghiệp trong công ty của mình. Hy vọng sẽ giúp được các bạn phần nào.

    Đầu tiên, bạn cần nắm rõ vấn đề mà mình cần hỏi đã. Hãy tự trả lời những câu hỏi sau trước nhé:

    1. Context/Background (bối cảnh) là gì? Ví dụ:
      • Ai là người phát hiện (detect) ra?
      • Xảy ra ở giai đoạn (phase) nào của dự án?
    2. Problem/Issue (vấn đề) cụ thể là gì?
      • Liên quan đến tính năng (feature/module) nào trong requirements?
      • Tần xuất (frequency) xảy ra là bao nhiêu?
      • Đang hoặc có thể gây ra những ảnh hưởng (impact) gì?
      • Bạn đánh giá mức độ nghiêm trọng (severity) này ra sao (critical, high)?
    3. What have you done/Investigated Actions của bạn là gì rồi?
      • Bạn đã thực hiện tìm kiếm (search/research) thông tin trên Internet chưa?
      • Bạn đã thảo luận với các thành viên có kinh nghiệm hơn trong chính dự án (your team) chưa?
      • Bạn đã thử bao nhiêu phương án (try how many solutions) để giải quyết vấn đề rồi? Cụ thể là gì, kết quả (result) của từng phương án ra sao, đang bị tắc chỗ nào?

    Sau khi đã nắm khá rõ vấn đề và thử một số cách rồi nhưng vẫn chưa ra kết quả, thì mới đi tìm kiếm thêm sự giúp đỡ bạn nhé. Bây giờ, bạn cần xác định thêm:

    1. Right People, người/nhóm cụ thể bạn cần hỏi/tìm kiếm sự trợ giúp là ai? Đừng quăng vấn đề của mình vào 1 group quá lớn hoặc send email tới quá nhiều người mà mong có sự phản hồi nha.
    2. TODO List, những công việc cần làm tiếp theo là gì? Người bạn đang hỏi có thể giúp được gì trong đó?
      • TODO list để giải quyết vấn đề trên có thể là cả tá items, nên bạn cần xác định rõ những items nào bạn và team có thể xoay sở tiếp được, những items nào thực sự vượt quá năng lực/khả năng thì mới nhờ nhé.
    3. Bạn mong muốn nhận được phản hồi hay sự trợ giúp vào lúc nào? Đừng đưa ra một cái expected due time quá gấp, trong trường hợp vấn đề của bạn thực sự urgent thì hãy hỏi xem khi nào người trợ giúp bố trí được thời gian để bạn trình bày trực tiếp với họ nhé.

    Tóm lại, trước khi hỏi hay tìm kiếm sự giúp đỡ, hãy chắc chắn rằng bạn đã hiểu rõ (understand clearly) vấn đề của mình. Và bóc tách (break) vấn đề ra càng nhỏ càng tốt (as small as possible). Khi đó, bạn sẽ rất ngạc nhiên là sao vấn đề to tát của mình nó lại trở nên simple như thế và từ đó bạn có thể ask the Right People the Right Questions.

  • Debug Conflict Constraint iOS

    Debug Conflict Constraint iOS

    Có thể bạn đã từng gặp lỗi constraint bị breaking với kiểu log như thế này:

    Thường thì Xcode sẽ tự loại bỏ 1 constraint để view không bị conflict nữa. Điều này sẽ dẫn đến UI hiển thị trên màn hình đúng hoặc không, tùy thuộc vào việc Xcode loại bỏ constraint nào.

    Tuy nhiên, kể cả trong trường hợp UI hiển thị đúng, thì chúng ta vẫn nên đi fix cái lỗi này.

    Đối với những màn hình phức tạp, nhiều subview, việc biết view nào đang bị lỗi constraint là khó có thể phán đoán được.

    Vì vậy ở bài viết này, hãy cùng nhau đi đọc đống log kia để biết view nào đang bị conflict constraint 🙂

    Tìm xem view nào bị lỗi bằng cách đọc địa chỉ

    Để í đến dòng log sau:

    Đoạn log này hướng dẫn rằng code sẽ recover bằng cách loại bỏ constraint 0x60000336d7c0 của view có địa chỉ 0x7fccafe06a10.
    Giờ thì bắt đầu đi tìm view có địa chỉ 0x7fccafe06a10:

    • Chọn Show Debug Navigator (Command + 7).
    • Chọn View Memory Graph Hierachy
    • Nhập địa chỉ của View cần tìm vào phần Filter.
    • Chọn vào View có địa chỉ cần tìm, Xcode sẽ hiển thị kết quả như sau:
    • Click chuột phải vào View, chọn Quick Look để xem đó là View nào:
    • Với Quick Look, Xcode sẽ hiển thị View đang bị conflict constraint:

    Đổi màu background của View bằng lldb:

    Nếu trong trường hợp có quá nhiều View giống nhau, và bạn vẫn chưa thể xác định đó là view nào?
    -> Dùng lldb để đổi màu background view đó để có thể xác định dễ hơn.

    Đầu tiên, pause chương trình lại và thực hiện lệnh sau ở cửa sổ lldb:

    expression [(UIView*)0x7fccafe06a10 setBackgroundColor:[UIColor redColor]]
    • Với lệnh này, chọn Quick Look với View vừa rồi thì View đã chuyển sang màu đỏ rồi, nhưng vẫn chưa hiển thị trên màn hình simulator/device.
    • Để View đổi màu ngay lập tức trên simulator/device, thực hiện tiếp 1 câu lệnh sau:
    expression (void)[CATransaction flush]

    OK, khi đó thì View bị lỗi constraint đã ngay lập tức đổi màu rồi.

    Xem tất cả constraint của View:

    • Mở Debug View Hỉeachy.
    • Tìm và chọn view bị lỗi constraint.
    • Chọn Show the size inspector (Option + Command + 5)
    • Khi đó, ở mục Constraint thì sẽ hiển thị tất cả constraint đang gắn với view đó. Cuối cùng thì chỉ cần bỏ đi constraint nào không cần thiết là được.

    Vừa leading, vừa centerX, vừa set width gây ra bị conflic constraint. Bỏ width constraint hoặc leading constraint tùy vào mục đích.

    Tham khảo: https://medium.com/ios-os-x-development/dynamically-modify-ui-via-lldb-expression-1b354254e1dd