Blog

  • F*** the people, who do not add Newline at End of File

    F*** the people, who do not add Newline at End of File

    Nghe có vẻ cứt bò, tuy nhiên sự thật là việc không add một trong trống ở cuối file thực sự là một tội ác

    No Newline at End of File

    Bạn đã bao giờ nhìn thấy dòng chữ ở git

    alt text

    hay ở vim

    alt text

    Ờ thế tại sao git lại báo thế, nó đến từ một quyết định trong quá khứ của Unix

    Một tệp nguồn không trống sẽ kết thúc bằng một ký tự dòng mới, không được đặt ngay trước ký tự dấu gạch chéo ngược (backslash).

    Vì đây là mệnh đề, nên chúng ta sẽ kiểm tra việc vi phạm quy tắc

    Vì vậy, nó chỉ ra rằng, theo POSIX, mọi tệp văn bản (bao gồm các tệp nguồn Ruby và JavaScript) phải kết thúc bằng một ký tự \n, hoặc mới newline (không phải là một dòng mới). Điều này đóng vai trò là eol, hoặc kết thúc của dòng nhân vật. Nó là một dòng Terminator.

    Bây giờ thì gần như mọi chương trình text editor đều hỗ trợ việc tự động thêm 1 dòng trống vào cuối file, ví dụ:

    • vim: mặc định rồi, nên là cứ để nó mặc định thôi
    • VSCode: dùng setting Files: Insert Final Newline
    • Sublime: dùng setting ensure_newline_at_eof_on_save

    Tóm lại là anh em nên thêm 1 dòng vào cuối file code nếu chưa có thì hãy sửa như sau:

    git ls-files -z | while IFS= read -rd '' f; do tail -c1 < "$f" | read -r _ || echo >> "$f"; done
    

    ref: https://unix.stackexchange.com/questions/31947/how-to-add-a-newline-to-the-end-of-a-file

  • How to recover corrupted GIT repository

    How to recover corrupted GIT repository

    GIT corrupted, really?

    Thực sự vấn đề này mình cũng không sure lắm là do đâu, và cũng không chắc vấn đề này có phải là corrupted hay không nhưng sẽ note lại một chút về case này.

    Nạn nhân

    Ngô Hải Đoàn, 1995, Android Dev. Anh ta đang làm việc trên PC, đột nhiên tòa nhà bị mất điện, khi bật máy lại anh ta phát hiện ra anh ta không thể thực hiện các câu lệnh git trên local-repository của mình nữa và mình là thằng cave được gọi support.

    Phán đoán

    Trước khi vào phần phán đoán thì mình có hỏi về source code đã commit chưa, nếu commit rồi thì khả năng recover được rất cao. Câu hỏi thứ 2, source code đó có quan trọng không, nếu có thể re-work trong dưới 30m thì chắc sẽ không có node này.

    Mình sẽ kết thúc phần phán đoán ở đây vì chả có gì để phán đoán cả

    Kiểm tra một chút.

    Trong khi voọc máy của Đoàn thì mình có cố re-init repository nhưng có vẻ không khả quan, kiểm tra một lúc thì có vẻ như các object và ref của git vẫn còn khá ổn. Bạn có thể tự kiểm tra thư mục .git trong local repository của mình.

    Như vậy, nhìn chung khả năng cao branch/commit quan trọng vẫn ổn.

    Thử recover.

    Mình có sử dụng một số câu lệnh git cơ bản như git log hay git init để thử lấy lại commit-id nhưng không thành công.

    Đến đây nhìn chung bạn không thể làm gì với repo này nữa, nên mình sẽ chuyển toàn bộ object của repo này sang một thư mục khác mà vừa clone về. (copy thư mục object trong .git của thư mục cũ sang chỗ tương ứng của thư mục mới)

    Đến đây bạn có một repository giống với remote (chỗ mà bạn clone về) và cộng thêm các commit mà bạn đã commit hoặc đã pull về từ remote.

    Tại sao lại copy objects

    Nhìn chung nếu bạn đã commit thì không sợ mất content. Bởi khi commit git sẽ sinh ra các file object trong thư mục objects, công với log trong logs và một số ref hay info.

    Object name sẽ dài 40 ký tự (SHA1)

    Thư mục object của bạn sẽ trông như này

    alt text

    Tên thư mục sẽ là 2 ký tự đầu trong 40 ký tự. Trong thư mục sẽ có các file với tên là 38 ký tự còn lại.

    Objects sẽ chứa thông tin về file (changed content) để xem được thì cần dùng câu lệnh git(do git mã hóa nên chỉ git xem đc :D): git cat-file

    alt text

    Đến đấy các bạn sẽ đặt câu hỏi thế git quản lý các file thành commit như thế nào

    Từ các objects thì git sẽ dựng lên tree là các phiên bản của thư mục bao gồm các objects.

    alt text

    alt text

    Cứ thế, bằng cách duyệt tree, ta sẽ có 1 tree hoàn chỉnh gồm content của cả một phiên bản (commit).

    Cuối cùng commit sẽ bao gồm 1 tree

    alt text

    Tóm lại cứ còn objects là bạn còn tất cả, việc còn lại là đi tìm commit-id để recover.

    Tìm commit

    Có một vài chỗ để bạn có thể tìm GIT commit-id, thứ nhất đó là thư mục refs

    Refs

    Refs chỉ đơn giản là references của commit. Refs dùng để bạn nhớ lastest commit bằng cách lưu commit id vào thư mục refs.

    alt text

    Ở đây lastest commit của nhánh master đang là b94514784b0b86e84bdd2d66903de5bf7f9722c2.

    Vậy nhớ được tên nhánh là có thể recover lại được. Lúc này bạn sẽ recover toàn bộ tree của nhánh đó và mặc nhiên các commit base trên commit này sẽ có thể trace được.

    logs

    Một chỗ nữa, toàn năng hơn là logs. Log lưu lại lịch sử của từng nhánh.

    alt text

    Bạn có thể xem mình đã tunglamgi trên từng nhánh.

    Đến đây bạn có commit-id rồi việc còn lại là recover bằng câu lệnh git checkout commit-id

    Btw

    Nhìn chung xử lý mấy việc lặt vặt này khá hay và tạo cho mình nhiều cơ hội xử lý các thủ thuật về git.

    Anw: Dù mất kì chuyện gì xảy ra đừng bao giờ xóa thư mục .git 😀

  • Git from noob to master – Chapter 1

    Git from noob to master – Chapter 1

    Trong một vài lần chém gió về Git, thấy mọi người có vẻ chưa chú ý nhiều đến Git và sử dụng nó một cách hiệu quả. Nhân đây xin mạn phép chém gió sơ qua để mọi người hoàn thiện.

    Git là gì

    Câu hỏi cơ bản và đơn giản nhưng thay vì trích dẫn bất kì định nghĩa nào mình sẽ quay sang từ khóa và hinh ảnh

    • SCM – Source code management
    • Linus Torvarlds (Linux)
    • Thay thế BitKeeper trong dự án Linux Kernel

    Cài đặt

    Git cung cấp bộ cài cho tất cả các OS hệ POSIX: Linux, Windows, macOS trên git-scm.

    Sơ lược lịch sử

    alt text.

    Torvalds thổ lộ rằng: ông chưa bao giờ muốn thực hiện quản lý source code, đó là công việc nhàm chán nhất trong thế giới máy tính (trừ database ra). Torvalds cùng các đồng đội sử dụng rất nhiều SCM khác nhau cho đến khi họ thấy phù hợp với BitKeeper(BK), thậm chí BK là nhân tố quan trong trong việc phát triển nhanh chóng của Linux Kernel. Vấn đề duy nhất của BK là một phần mềm độc quyền, tất nhiên có bản miễn phí với các hạn chế cho các open-source project. Larry McVoy(CEO BitMover công ty phát triển BK) vừa muốn hỗ trợ các dự án phần mềm tự do vừa muốn bảo vệ lợi ích của BK cũng như BitMover tất nhiên chừng nào hội phần mềm tự do chưa đe dọa đến lợi ích.

    Năm 2005, Andrew "Tridge" Tridgell đã dịch ngược BK, gần như ngay lập tức BK bỏ việc hỗ trợ Linux, ngược lại Linus cũng ngừng sử dụng BK để quản lý mã nguồn, tuy nhiên Linus chỉ có 3 tháng để chuyển toàn bộ source code sang công cụ mới khi BK chính thức ngừng hoàn toàn việc hỗ trợ. Điều đó nảy sỉnh việc phát triển Git.

    Quá trình phát triển bắt đầu từ ngày 3 tháng 4 nằm 2005, Torvalds công bố dự án vào ngày 6, Git có thể cài đặt trên PC vào ngày 7 và tính năng merge branch được release vào ngày 18. Đến ngày 16/6 Git thực hiện quản lý việc release Linux Kernel v2.6.12

    Có lẽ việc ôn lại lịch sử sẽ kết thúc ở đây, phần tiếp theo sẽ quay lại thực tế sử dụng Git

    Một số khái niệm trong Git

    Repository

    • Repository là khái niệm cơ bản của Git tương đương với project trong GitLab, Gerrit
    • Git có thể tạo bằng câu lệnh git init, thư mục trở thành local repository trong khi đó các repository online như trên GitLab được gọi là remote repository.
    • Lịch sử làm việc đều được lưu trong thư mục khởi tạo git

    Remote

    • Repository hosted in server. Remote repository chính thường được gọi là Origin
    • Các local clone đều liên kết đến remote.
    • 1 local clone can have 0, 1 or many remote.

    Branch – nhánh

    • Branch cũng là một khái niệm mới trong Git mà cần phải làm quen.
    • Branch là các version song song với nhau của Repo.
    • Người dùng làm việc trên các branch mà không làm ảnh hưởng đến live version.

    Một vài branch đặc biệt

    • master – branch mặc định
    • HEAD – branch hiện tại mà người dùng đang làm việc
    • origin/master, origin/xxx, origin2/xxx là các nhánh ở remote repository

    Checkout

    • Checkout là cách người ta chuyển nhánh đề làm việc
    • Ví dụ: Bạn lấy project trên GitLab về và cần chuyển qua nhánh master 2 để làm việc bạn cần checkout sang nhánh master 2: git checkout master2, hoặc cũng có thể bạn cần tạo ra một nhánh mới hãy thêm flag -b trong câu lệnh: git checkout -b dev_another_function.

    Commit

    • Nếu như Branch là nhánh cây, cành cây thì có thể coi commit chính là các nút (node) hoặc cũng có thể là một điểm nào đó trên nhánh.
    • Người ta lưu lịch sử làm việc trong các commit, nói cách khác nếu bạn commit dữ liệu của bạn không thể mất trừ khi bạn xóa hoàn toàn project git.
    • Cách tạo commit cũng đơn giản: git commit -m "Commit Message" hoặc cũng có thể đơn giản là git commit sau đó làm theo hướng dẫn với giao diện text editor(thường là vi, vim, nano, etc, ..)
    • Mỗi commit đều có message và một ID đặc biệt (SHA –hash), từ đó có thể trace được commit

    Nói đến đây mình đột nhiên nhiên phát hiện ra trình bay theo hướng này sẽ khiến mọi người khó hiểu một chút, vì vậy ngay sau đây mình sẽ trình bày về commit một cách rõ hơn, theo một cách nhin khác

    Git Concept

    alt text

    alt text

    2 hình trên đây đã trình bày cách sử dụng git ở local một cách cơ bản

    Nhìn chung một file sẽ có 4 trạng thái như hình thứ nhất

    • Untracked: Trong working directory nhưng git không quản lý, đó là nhưng file tạo mới, được copy vào hay đơn giản là bạn vừa chạy git rm fileName.
    • Unmodified: Rất dễ hiểu, đó là file git quản lý, git ghi nhận rằng file đó chưa có sự thay đổi so với commit hiện tại (HEAD). Có 2 cách để chuyển file vào trạng thái này đó là thêm các file untracked git add hay git commit tức là lưu sự thay đổi đó lại (file sẽ được ghi nhận là ko thay đổi so với commit mới :D, đôi chút confuse).
    • Modified: Là file từ trạng thái unmodified và đã bị edit, tất nhiên khi Crtl + Z toàn bộ step, file sẽ trở lại unmodified.
    • Staged: là trạng thái chuẩn bị được commit, thông thường người dùng không để ý đến trạng thái này, trạng thái này được trình bày khá rõ ràng ở hình thứ 2. Một trạng thái kiểu như git đã ghi nhận sự thay đổi nhưng chưa tạo commit. Để chuyển sang trạng thái staged sử dụng câu lệnh git stage.

    Một tips nhỏ với stage, khi bạn cần commit lên nhánh A mà lại làm việc trên nhánh B thì bạn có thể stage toàn bộ file cần thay đổi và checkout sang nhánh A và commit. Một cách khác đa số mọi người sẽ áp dụng đó là commit trên nhánh B, cherry-pick(sẽ nói ở dưới đây) commit sang A, ngoài ra cũng có cách chuyên nghiệp hơn đó là tạo pack diff.

    Merge và Rebase

    Từ thực tế làm việc trong nhóm sẽ nảy sinh các trường hợp phải kết hợp code của nhiều developer để tạo nên một bản hoàn chỉnh thay vì tiếp tục phát triển song song mãi điều đó nảy sinh ra 2 khái niệm trong git là merge và rebase.

  • iOS — Device orientations

    iOS — Device orientations

    Mình xin chia sẻ một vài lưu ý khi develope application support full orientations :

    • UIInterfaceOrientation.portraitUpsideDown không support cho iPhone X Family (có tai thỏ) 

    -> Link refer : https://developer.apple.com/documentation/uikit/uiviewcontroller/1621435-supportedinterfaceorientations

    • Default orientation support cho iPad là UIInterfaceOrientationMask.all (tất cả các hướng), còn cho iPhone là UIInterfaceOrientationMask.allButUpsideDown (không hỗ trợ upside down) 
    • Nếu trong app sử dụng navigation view controller hoặc tabbar view controller, muốn enable tất cả các orientation thì mình phải override lại trong Base UINavigationViewController của app (tương tự với Base UITabbarViewController của app) với 2 method delegate là shouldAutorotate và supportedInterfaceOrientations
      Example: 
        override var shouldAutorotate: Bool {
            return true
        }
        
        override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
            return .all
        }
    • Nhớ check trong setting app, tab General, section Deployment Info xem mình đã select đủ các orientation mình cần support hay chưa.
      Kĩ hơn thì check trong Info.plist với key “Supported interface orientations” và “Supported interface orientations (iPad)” xem đã đủ orientation chưa. 

    Hi vọng bài viết này sẽ giúp cho các bạn tiết kiệm được 1 ngày :v
    Have a nice day!

  • Add placeholder for UITextView (phần 2)

    Add placeholder for UITextView (phần 2)

    Hello everybody 🙂

    Như chia sẻ ở phần trước thì tại phần này mình sẽ hướng dẫn các bạn một cách để có thể add placeholder cho UITextView. Hì hì, nói hướng dẫn thì hơi quá, mình chỉ muốn chia sẻ cho các bạn cách làm của mình khi gặp phải vấn đề như này mà thôi! 🙂

    OK không dài dòng nữa chúng ta cùng bắt đầu nào!

    Ứng dụng mà chúng ta sẽ tạo ra rất đơn giản và nó có tên là PlaceholderTextView (chả có ý nghĩa gì nhỉ 🙂 ). Sơ qua thì app này chỉ có một cái màn hình, bên trong nó có một UITextView để người dùng nhập message, một UIButton để gửi đi message (send message đi đâu thì mình sẽ không implement 🙂 ). Và mục đích chính của chúng ta đó là thêm placeholder cho thanh nhập message, chính là UITextView đó. OK, bước đầu tiên đó là hãy tạo ra UI giống như thế này:

    Tạo UI đơn giản như này thì chắc ai cũng làm được nhỉ? Nhưng nếu bạn là người mới thì có thể xem code mẫu của mình ở đây nhé! Mình làm bằng storyboard chứ không code chay tốn nhiều time lắm hix hix.

    Đã done phần UI, bây giờ mình sẽ giải thích chi tiết về cách mình add placeholder cho UITextView nhé! Mình sử dụng cách tạo extension của UITextView để thêm thuộc tính placeholder vào. Bạn có thể xem file extension UITextView của mình ở đây luôn! Ý tưởng của mình cũng rất là đơn giản thôi, khi đã thêm một thuộc tính placeholder cho UITextView rồi thì lúc đó chỉ cần set thuộc tính placeholder != nil và != empty là ta sẽ có được cái placeholder mà mình mong muốn, và khi text của UITextView là != empty thì placeholder phải mất đi còn khi text của UITextView = empty thì placeholder lại phải xuất hiện.

    Mình sẽ giải thích từng thành phần một ở dưới đây:

    Đầu tiên đây là thuộc tính placeholder mình thêm vào trong extension của UITextView, vì trong extension không thể thêm một stored property nên mới phải làm phức tạp ra như này đấy 🙁

    fileprivate var placeholderKey = "placeholderKey"
    
    var placeholder: String? {
        get {
            if let value = objc_getAssociatedObject(self, &placeholderKey) as? String {
                return value
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &placeholderKey, newValue, .OBJC_ASSOCIATION_COPY)
            if newValue?.isEmpty ?? true {
                removePlaceholder()
                removeObserverText()
            }
            else {
                addPlaceholder(newValue!)
                addObserverText()
            }
        }
    }

    Với đoạn code trên thì khi ta set thuộc tính placeholder là không nil và không empty thì sẽ gọi hàm addPlaceholder(), đây là hàm sẽ làm công việc tạo ra một UILabel với text là argument được truyền vào sau đó sẽ được addSubview vào UITextView, chính UILabel đó sẽ là placeholder của UITextView. Cùng với việc gọi hàm addPlaceholder() thì cũng gọi hàm addObserverText(), đây là một hàm sẽ thêm một đối tượng observer sự kiện thay đổi text của UITextView và khi người dùng edit text trên UITextView, mục đích của việc này chính là để mình control việc add placeholder và remove placehodler cho UITextView. Ngược lại khi ta set thuộc tính placeholder là nil hoặc là empty thì sẽ gọi hàm removePlaceholder() và hàm removeObserverText() đây là những hàm dùng để remove đi UILabel mà mình đã thêm vào UITextView tại hàm addPlaceholder() và remove đối tượng observer mà mình đã thêm tại hàm addObserverText()

    Dưới đây là hàm addPlaceholder() và hàm removePlaceholder()

    func addPlaceholder(_ text: String) {
        placeholderLabel?.removeFromSuperview()
        placeholderLabel = UILabel()
        placeholderLabel?.text = text
        if let selfFont = self.font {
            placeholderLabel?.font = UIFont.systemFont(ofSize: selfFont.pointSize, weight: .thin)
        }
        else {
            placeholderLabel?.font = UIFont.systemFont(ofSize: 12.0, weight: .thin)
        }
        placeholderLabel?.textColor = .lightGray
        placeholderLabel?.numberOfLines = 0
        let padding = self.textContainer.lineFragmentPadding
        let estimateSize = placeholderLabel!.sizeThatFits(CGSize(width: self.frame.size.width - 2*padding, height: self.frame.size.height))
        placeholderLabel?.frame = CGRect(x: padding, y: self.textContainerInset.top, width: estimateSize.width, height: estimateSize.height)
        self.addSubview(placeholderLabel!)
    }
    
    func removePlaceholder() {
        placeholderLabel?.removeFromSuperview()
    }

    Ở 2 func trên thì mình có sử dụng thuộc tính placeholderLabel, đây là thuộc tính mà mình cũng đã thêm vào phần extension của UITextView. Lý do mình thêm thuộc tính này là bởi vì để việc remove placeholder trở nên dễ dàng hơn thay vì việc phải duyệt hết các subView của UITextView, kiểm tra subView nào đang là placeholder rồi mới remove đi.

    Đây là phần implement cho thuộc tính placeholderLabel của mình

    fileprivate var placeholderLabelKey = "placeholderLabelKey"
    
    fileprivate var placeholderLabel: UILabel? {
        get {
            if let label = objc_getAssociatedObject(self, &placeholderLabelKey) as? UILabel {
                return label
            }
            return nil
        }
        set {
            objc_setAssociatedObject(self, &placeholderLabelKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    Dưới đây là hàm addObserverText() và hàm removeObserverText()

     fileprivate let observerKeyPath = "text"
    
     func addObserverText() {
         observerText = ObserverText()
         self.addObserver(observerText!, forKeyPath: observerKeyPath, options: .new, context: nil)
     }
        
     func removeObserverText() {
         if let observerText = observerText {
            self.removeObserver(observerText, forKeyPath: observerKeyPath)
         }
         observerText = nil
     }

    Ở 2 func trên thì có thể thấy khi mình add observer thì cũng đã add observer sự kiện thay đổi thuộc tính text của UITextView (self ở trên chính là UITextView, những hàm này đều đặt trong extension của UITextView). À còn với thuộc tính observerText mình sử dụng ở trên thì cũng là một thuộc tính mình thêm vào extension của UITextView, implement của nó đây 🙂

    fileprivate var observerTextKey = "observerTextKey"
    
    fileprivate var observerText: ObserverText? {
        get {
             if let observer = objc_getAssociatedObject(self, &observerTextKey) as? ObserverText {
                 return observer
             }
             return nil
         }
         set {
              objc_setAssociatedObject(self, &observerTextKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
         }
    }

    Class ObserverText mình sử dụng ở trên là một class mà mình tự tạo ra, nhiệm vụ của nó là xử lý hành động khi thuộc tính text của UITextView thay đổi và xử lý hành động khi người dùng edit text trên UITextView. Implement của nó ở đây:

    fileprivate class ObserverText: NSObject {
        
        override init() {
            super.init()
            NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChange(notification:)), name: UITextView.textDidChangeNotification, object: nil)
        }
        
        deinit {
            NotificationCenter.default.removeObserver(self)
        }
        
        @objc func textViewDidChange(notification: Notification) {
            if let textView = notification.object as? UITextView {
                if let text = textView.placeholder, textView.text.isEmpty {
                    textView.addPlaceholder(text)
                }
                else {
                    textView.removePlaceholder()
                }
            }
        }
        
        override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
            if let keyPath = keyPath, keyPath == observerKeyPath, let textView = object as? UITextView {
                if let newText = change?[NSKeyValueChangeKey.newKey] as? String, !newText.isEmpty {
                    textView.removePlaceholder()
                }
                else {
                    if let text = textView.placeholder {
                        textView.addPlaceholder(text)
                    }
                }
            }
        }
        
    }

    Class này cũng khá là dễ hiểu phải không nào, chỉ là implement để bắt các sự kiện thôi mà 🙂.

    OK 🙂 vậy là ở trên mình đã trình bày toàn bộ cách mà mình coding để có thể thêm placeholder cho UITextView. Bây giờ, khi sử dụng UITextView, bạn chỉ cần set thuộc tính placeholder là khác nil và khác empty là bạn sẽ có placeholder của UITextView rồi, và mỗi khi người dùng edit text trên UITextView thì nó cũng sẽ tự động thêm placeholder khi mà người dùng xoá tất cả các kí tự trên UITextView và cũng tự động remove placeholder khi người dùng nhập một kí tự nào đó lên UITextView. Giống như trong file gif này nè 🙂

    Cảm ơn bạn đã dành thời gian để đọc bài viết này. Happy reading

    Source code tham khảo

  • Add placeholder for UITextView (phần 1)

    Add placeholder for UITextView (phần 1)

    Hello everybody 🙂

    Chắc hẳn mọi người điều biết trong iOS khi muốn nhập giá trị đầu vào thì sẽ sử dụng UITextFieldUITextView. Với UITextFiled thì ta chỉ có thể nhập giá trị trên một dòng còn với UITextView thì có thể nhập giá trị trên nhiều dòng, có thể scroll vì UITextView là extends từ UIScrollView mà 🙂. Tuy nhiên khi sử dụng hai View này thì ta còn thấy một điểm khác nhau nữa, đó là với UITextView Apple đang không hỗ trợ thuộc tính placeholder còn với UITextField thì có luôn placeholder ngon như này 🙁.

    Tại sao lại có sự bất công này???

    Có lẽ do Apple thích thế thôi nhỉ? 🙂. Thực ra cũng có một quan điểm cho rằng lý do mà UITextView không có placeholder là bởi vì khi lập trình các dev thường sẽ setup một header ở bên trên UITextView để giải thích rõ tại UITextView này sẽ cần ghi nội dung gì rồi nên không cần phải thêm placeholder để làm gì nữa cả, việc này cũng khá dễ hiểu tại vì khi xét tới bản chất của UITextView đó là nơi sẽ cho phép người dùng nhập một content gì đó sẽ mất nhiều dòng và ngốn nhiều khá thời gian. Thật tai hại khi người dùng đang nhập content của họ và bỗng dưng họ quên mất rằng nội dung chính phải nhập là gì 🙂 khi đó chỉ còn cách xoá hết đi và xem lại placeholder (nếu có), với những người đãng trí chắc điều này sẽ là một cơn ác mộng 🙂

    Có nên sử dụng placeholder cho UITextView?

    Một số trường hợp cũng nên dùng placeholder cho UITextView khi mà không có đủ không gian để thêm một header cho UITextView và nếu thêm vào cũng không được đẹp mắt. Chẳng hạn trong trường hợp này:

    Như hình bên trên thanh iMessage trong ứng dụng tin nhắn kia là một UITextView và có placeholder = "iMessage"

    Kết luận!

    Qua bài này, có thể thấy rằng việc sử dụng UITextView thì sẽ không được Apple hỗ trợ thuộc tính placeholder. Điều này có thể sẽ gây khó khăn cho những bạn mới lập trình về iOS 🙂. Tuy nhiên, với một lập trình viên thì không gì là không thể làm được phải không nào? Khi Apple không hỗ trợ thuộc tính placeholder cho UITextView thì ta cũng có thể tự làm điều đó hộ Apple được mà 🙂. Ở phần sau, mình sẽ hướng dẫn các bạn một cách để có thể thêm placeholder cho UITextView thông qua việc tạo ra một app đơn giản như thế này:

  • Hướng dẫn viết bài

    Hướng dẫn viết bài

    Hướng dẫn viết bài:

    • Mọi người có thể viết bài draft bằng .markdown format rồi import vào bài viết.
    • Bài viết nên có Featured image, nếu không có hình ảnh phù hợp có thể lên trang chia sẻ ảnh free như https://unsplash.com/ để lấy ảnh (nên ghi nguồn ảnh). Như vậy dàn trang sẽ đẹp hơn.
    • Nên bổ sung category, tag… cho bài viết, như vậy dễ quản lý bài và bot google có thể đọc được.
    • Đối với các bài viết về technical, nên có source code đi kèm. Format source bằng cách để source vào block code như dưới (define language cho source code thì có thể highligh syntax phù hợp), hoặc dùng gist.github.com rồi embed link vào bài viết.
    • Bài viết nên có mở bài, thân bài, kết bài, nếu có table of contents thì càng tốt. Bài viết về công nghệ nên hướng về tính ứng dụng, kinh nghiệm sử dụng hoặc khó khăn khi triển khai.
    • Bài viết không nên quá dài, chỉ khoảng 5 phút đọc là tốt nhất, nếu bài dài quá nên tách thành nhiều bài nhỏ.
    • Bài viết không nên dùng keyword liên quan đến dự án, luôn phải suy nghĩ đến tình tiết bảo mật khi viết bài.

    Hướng dẫn chèn markdown vào bài viết:

    Chọn Add block
    Search markdown

    Hướng dẫn highlight source code:

    Search Prismatic
    Trong phần setting cho block thì chọn language tương ứng

    Kết quả:

    func greeting(_ something: String) {
        print("Hello \(something)")
    }
    
    greeting("World")

    techover.io

  • 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