Category: iOS

  • 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:

  • Swift—Design patterns: Multicast Delegate

    Swift—Design patterns: Multicast Delegate

    Multicast delegate

    Khái niệm

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

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

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

    So sánh với observer và notification

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

    Implementation

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

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

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

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

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

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

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

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

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

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

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

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

    OK, bây giờ test thử:

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

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

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

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

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

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

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

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

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

  • Swift—KeyPaths (1)

    Swift—KeyPaths (1)

    Swift KeyPaths (1)

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

    Table of contents

    KeyPaths basic

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

    KeyPaths store uninvoked reference to property

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

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

    Log

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

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

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

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

    KeyPaths as parameter

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

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

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

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

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

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

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

    Sorted, filter…using KeyPaths

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

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

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

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

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

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

    Danh sách contacts:

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

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

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

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

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

    Define enum thứ tự sort:

    enum Order {
        case ascending
        case descending
    }
    

    Override logic sort

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

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

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

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

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

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

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

    Sample playground: Swift_KeyPaths.playground

  • Swift—Codable

    Swift—Codable

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

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

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

    Table of contents

    Swift Codable basic

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

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

    Cách làm:

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

    Implementation:

    Define Swift struct:

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

    Convert string to instance:

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

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

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

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

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

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

    Swift Codable manual encode decode

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

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

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

    Implementation

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

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

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

    Manual encode decode

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

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

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

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

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

    Implementation

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

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

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

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

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

    Swift Codable coding key

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

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

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

    Implementation

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

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

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

    hay

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

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

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

    Swift Codable key decoding strategy

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

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

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

    Custom key decoding strategy

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

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

    Swift Codable date decoding strategy

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

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

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

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

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

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

    Swift Codable nested unkeyed container

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

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

    Implementation

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

    Sample playground: Swift_Codable.playground

  • iOS — Play RTSP Streaming

    iOS — Play RTSP Streaming

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

    Table of Contents

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

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

    Build lib IJKPlayer

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

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

        Thành

        FF_ALL_ARCHS_IOS8_SDK="arm64 i386 x86_64"

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

    Tích hợp IJKPlayer vào project

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

    Sample

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

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

    Khởi tạo (Initialization) trong Swift

    Việc khởi tạo trong bất cứ ngôn ngữ lập trình nào đều rất rất quan trọng, trong một project thì bạn sẽ liên tục phải thực hiện khởi tạo các instance của các struct, class hoặc enum. Việc hiểu rõ và sử dụng thành thạo quá trình khởi tạo trong swift sẽ giúp bạn tăng performance cũng như chất lượng của source code, dưới đây sẽ là bài viết đào sâu hơn vào quá trình khởi tạo trong swift nhé.

    I. Khởi tạo trong Structure

    1. Khởi tạo mặc định

    Việc khởi tạo này đơn gỉản và giống với việc tạo 1 instance của method không có parameter nào, ví dụ

    let phoneX = Phone()

    Syntax

    init() {
       // New Instance sẽ được khởi tạo ở đây
    }

    Example

    struct Rectangle {
        var length: Double
        var breadth: Double
        init() {
            length = 12.0
            breadth = 5.0
        }
    }
    
    var rec = Rectangle()
    print("area of rectangle is \(rec.length * rec.breadth)")

    Ở đoạn code bên trên 1 instance là rec được khởi tạo với các thuộc tính là chiều rộng và chiều dài lần lượt là length = 12, và breadth = 5, sau khi chạy đoạn code trên sẽ thu được kết quả:

    area of rectangle is 60.0

    Chuyện gì sẽ xảy ra nếu xoá phần init() trong đoạn code trên:

    struct Rectangle {
        var length: Double
        var breadth: Double
    }
    
    var rec = Rectangle()
    print("area of rectangle is \(rec.length * rec.breadth)")

    Khi chạy đoạn code trên sẽ có thông báo lỗi là

    error: missing argument for parameter 'length' in call
    let rec = Rectangle()

    Ở đây thì trình biên dịch đang thông báo 1 error và bắt chúng ta phải khởi tạo với đối số của length, taị sao lại như vậy???
    Bởi vì bạn đang cố khởi tạo cho struct Rectangle mà các stored property chưa được gán giá trị mặc định
    Đoạn code sửa lỗi này được sửa như sau:

    struct Rectangle {
        var length: Double = 12.0
        var breadth: Double = 5.0
    }
    
    var rec = Rectangle()
    print("area of rectangle is \(rec.length * rec.breadth)")

    Chúng ta gán giá trị mặc định cho tất cả các stored property, ở đây thì chúng ta đã tạo ra instance rec với các stored property được gán giá trị ban đầu lần lượt là 12.0 và 5.0,
    vậy là vấn đề được giải quyết.

    Như trên thì chúng ta đã được tiếp cận với việc khởi tạo 1 struct với kiểu mặc định, nhìn lại 2 cách viết ở trên nhé:

    // 1
    struct Rectangle {
        var length: Double
        var breadth: Double
        init() {
            length = 12.0
            breadth = 5.0
        }
    }
    
    // 2
    struct Rectangle {
        var length: Double = 12.0
        var breadth: Double = 5.0
    }

    2 cách viết nói trên đều đưa đến 1 kết quả như nhau là đều tạo ra 1 instance có các giá trị khởi tạo mặc định là 12.0 và 5.0,
    tuy nhiên thì cách viết thứ 2 sẽ giảm bớt line code và tường minh hơn, vì vậy trong trường hợp cần khởi tạo với giá trị mặc định thì chúng ta nên chọn cách viết thứ 2 nhé.

    2. Khởi tạo với các Parameter

    Khi cần khởi tạo 1 instance với các giá trị của property được truyền vào như dưới đây:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    }
    
    let rect = Rectangle(length: 6, breadth: 12)
    print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Kết quả sau khi chạy:

    length is: 6.0
    breadth is: 12.0

    Giờ bạn thử xoá phần init trong đọan code trên xem có gì xảy ra nhé.

    struct Rectangle {
        var length: Double
        var breadth: Double
    }
    
    let rect = Rectangle(length: 6, breadth: 12)
    print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Oh, Code trên vẫn chạy bình thường. Do với struct thì Swift mặc định đã tạo sẵn cho bạn 1 hàm khởi tạo rồi, tuy nhiên nếu sử dụng hàm khởi tạo mặc định này thì chúng ta sẽ gặp không ít phiền phức như:

    • Khi đổi thứ tự các stored property thì khi gọi hàm khởi tạo mặc định thứ tự của Parameter cũng phải đổi tương ứng
    struct Rectangle {
        var breadth: Double
        var length: Double
    }

    Khi gọi phải đảo lại parameter:

    let rect = Rectangle(breadth: 6, length: 12)
    • Khi thêm init với name các parameter khác với mặc định trong struct:
    struct Rectangle {
        var length: Double
        var breadth: Double
    
        init(length: Double) {
            self.length = length
            self.breadth = 5.0
        }
    }
    
    let rect = Rectangle(length: 6, breadth: 12) // Lỗi
    print(" length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Đoạn code trên sẽ lỗi vì chúng ta đã viết 1 hàm init với parameter là length ở trong struct, việc này sẽ khiến cho swift không tự động viết hàm khởi tạo cho bạn nữa.
    Vậy làm thế nào để vừa có hàm khởi tạo tự viết và cả hàm khởi tạo tự động mà swift sẽ tạo cho chúng ta, đơn giản chỉ cần chuyển hàm init của chúng ta qua extension:

    struct Rectangle {
       var length: Double
       var breadth: Double
    }
    extension Rectangle {
       init(length: Double) {
           self.length = length
           self.breadth = length + 5.0
       }
    }

    Okey, vấn đề được giải quyết.

    3. Khởi tạo với các Parameter mặc định

    Đôi khi trong các hàm khởi tạo, bạn muốn gán giá trị mặc định cho các tham số như dưới đây:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        init(length: Double = 12.0, breadth: Double = 5.0) {
            self.length = length
            self.breadth = breadth
        }
    }
    
    let rectA = Rectangle()
    print(" rectA:\n length is: \(rectA.length)\n breadth is: \(rectA.breadth) ")
    
    let rectB = Rectangle(length: 50)
    print(" rectB:\n length is: \(rectB.length)\n breadth is: \(rectB.breadth) ")
    
    let rectC = Rectangle(length: 20, breadth: 6)
    print(" rectB:\n length is: \(rectC.length)\n breadth is: \(rectC.breadth) ")

    Kết quả là:

    rectA:
    length is: 12.0
    breadth is: 5.0
    rectB:
    length is: 50.0
    breadth is: 5.0
    rectB:
    length is: 20.0
    breadth is: 6.0

    Việc khởi tạo với các parameter mặc định khiến cho việc gọi hàm được đa dạng hơn, với tuỳ trường hợp thì sẽ có hay không có parameter.

    4. Initializer Delegation

    Initializer Delegation thực chất chính là việc gọi hàm khởi tạo từ các hàm khởi tạo khác, như ví dụ dưới đây:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        // 1
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    
        // 2
        init(length: Double) {
            let breadth = length + 2.0
            self.init(length: length, breadth: breadth)
        }
    
    }
    
    let rect = Rectangle(length: 50)
    print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Kết quả là:

    rect:
    length is: 50.0
    breadth is: 52.0

    Ở đoạn code trên thì chúng ta có tới 2 hàm init trong struct, ở hàm init thứ 2 thì chúng ta thực hiện tính toán bề rộng trước khi gọi đến hàm init 1.
    Đây chính là Initializer Delegation nhé, viết ra 1, 2 lần là quen tay ngay.

    Điều gì sẽ xảy ra khi chúng ta gọi self trước khi call hàm init số 1, sửa đoạn code như sau:

    struct Rectangle {
        var length: Double
        var breadth: Double
    
        // 1
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    
        // 2
        init(length: Double) {
            self.length = 5.0 // Báo lỗi: 'self' used before 'self.init' call or assignment to 'self'
    
            let breadth = length + 2.0
            self.init(length: length, breadth: breadth)
        }
    
    }
    
    let rect = Rectangle(length: 50)
    print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Lúc này thì compiler báo lỗi: ‘self’ used before ‘self.init’ call or assignment to ‘self’
    Lỗi này thông báo rằng chúng ta không được dùng self trước khi gọi Initializer Delegation. Việc này đảm bảo code sẽ không bị thay đổi không mong muốn. Giả sử như đoạn code trên pass qua compiler thì lúc này length = 50.0 chứ không phải bằng 5.0 như mong muốn.

    Chú ý trong phần I:

    - Khi khởi tạo 1 instance, chúng ta cần gán giá trị ban đầu cho tất cả các non-optional stored property.
    - Không gọi self trước khi call Initializer Delegation

    II. Khởi tạo trong Class

    Ở phần trên giới thiệu việc khởi tạo cũng như các vấn đề thường gặp khi khởi tạo 1 stucture. Đối với class thì việc khởi tạo sẽ tương đối khác và biến hoá hơn, biến hoá dư nào thì các bạn đọc ở bên dưới nhé:

    1. Hai loại khởi tạo trong class

    Riêng với class thì khởi tạo chia thành 2 loại là designated initializers and convenience initializers

    1.1. Designated Initializer

    Được coi là việc khởi tạo chính của class, đây là cách khởi tạo thông thường nhất. Trong hàm khởi tạo chúng ta cần gán giá trị ban đầu cho tất cả các non-optional stored property.

    class Rectangle {
        var length: Double
        var breadth: Double
    
        // Designated Initializer
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    }

    Khi class của bạn có stored property là non-opional thì Việc khởi tạo với kiểu Designated Initializer là bắt buộc.

    1.1. Convenience Initializers

    Được coi là hàm hỗ trợ việc khởi tạo của class, trong hàm khởi tạo này chúng ta sẽ gọi đến các hàm khởi tạo khác ( Khá giống với Initializer Delegation trong struct)

    class Rectangle {
        var length: Double
        var breadth: Double
    
        // Designated Initializer
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    
        // Convenience Initializer
        convenience init(length: Double, area: Double) {
            let breadth = area / length
            self.init(length: length, breadth: breadth)
    
        }
    }
    
    let rect = Rectangle(length: 5, area: 20)
    print(" rect:\n length is: \(rect.length)\n breadth is: \(rect.breadth) ")

    Kết quả sau khi chạy

    rect:
    length is: 5.0
    breadth is: 4.0

    Nhìn vào ví dụ trên thì chúng ta cũng thấy được vai trò của hàm Convenience Initializer rồi, khi bạn cần xử lý abc-xyz ở trong hàm khởi tạo, để việc khởi tạo gọi là tiện lợi nhất đối với bài toán của bạn,
    thì hãy nghĩ ngay đến Convenience Initializer nhé.

    2. Khởi tạo với subclass

    Để hiểu rõ phần này chúng ta đến với phần ví dụ luôn nhé:

    class Rectangle {
        var length: Double
        var breadth: Double
    
        // Designated Initializer
        init(length: Double, breadth: Double) {
            self.length = length
            self.breadth = breadth
        }
    }
    
    class Square: Rectangle {
        var area: Double
    }

    Khi run đoạn code trên chúng ta sẽ nhận được lỗi là

    stored property 'area' without initial value prevents synthesized initializers
        var area: Double

    Như đã được phân tích ở phần I, khi khởi tạo 1 instance bất kỳ thì luôn phải đảm bảo các giá trị mặc định cho tất cả các stored property non-optional
    Ở đây do đã khởi tạo thêm biến area ở subclass là Square nên swift yêu cầu phải gán giá trị.

    Như dưới đây là 2 cách để pass qua compiler

    // option 1
    class Square: Rectangle {
        var color: String = "red"
    }
    
    // option 2
    class Square: Rectangle {
        var color: String
        init(length: Double, breadth: Double, color: String) {
            self.color = color
            super.init(length: length, breadth: breadth)
        }
    }

    Ở option 1, sẽ gán giá trị mặc định cho property color là “red”
    Ở option 2, thêm một hàm khởi tạo ở subclass Square để thực hiện Designated Initializer

    Vậy sẽ thế nào nếu chúng ta viết như sau:

    class Square: Rectangle {
        var color: String
        init(length: Double, breadth: Double, color: String) {
            self.color = color
            self.length = length
            self.breadth = breadth
        }
    }

    Lúc này ta nhận được error với nội dung:

    error: 'self' used in property access 'length' before 'super.init' call
            self.length = length

    Compiler đang báo chúng ta đang sử dụng self để truy cập vào property length trước khi gọi super.init.
    super ở đây chính là class cha Rectangle, việc sử dụng super.init là bắt buộc để gán giá trị cho lengt và breadth.

    Tóm lại thì việc khởi tạo của subclass cũng chung quy tắc là phải gán giá trị ban đầu cho tất cả các non-optional stored property, thứ tự thì sẽ là gán property ở subclass trước, sau đó là gọi super.init để gán cho các property ở lớp cha.

    3. Thừa kế hàm khởi tạo

    Ở subclass, nếu chúng ta muốn khởi tạo bằng cách gọi hàm khởi tạo của class cha thì bắt buộc subclass đó phải có giá trị mặc định của tất cả các non-optional stored property và subclass không có hàm init. Việc này khiến khá khó chịu, vì vậy để sử dụng hàm khởi tạo của class cha chúng ta có thể sử dụng thừa kế hàm khởi tạo như sau:

    class Square: Rectangle {
        var color: String
    
        init(length: Double, breadth: Double, color: String) {
            self.color = color
            super.init(length: length, breadth: breadth)
        }
    
        override init(length: Double, breadth: Double) {
            self.color = "red"
            super.init(length: length, breadth: breadth)
        }
    }

    Đơn giản chỉ cần override lại hàm init từ class cha là Rectangle, chúng ta có thể sài hàm khởi tạo của class cha một cách ngon lành rồi.

    III. Tổng kết

    Bài viết này mình đã giới thiệu về khởi tạo ở structure cũng như class, hy vọng sẽ giúp các bạn hiểu sâu hơn về khởi tạo trong swift.
    Phần sau mình sẽ giới thiệu về các cách khởi tạo của 1 custom class thuộc UI element nhé (ví dụ như UIView)
    Mong các bạn đón đọc.

  • Fresher Training—iOS Basic Day 2

    Fresher Training—iOS Basic Day 2

    Today topic:

    • App Life cycle
    • View Controller Life cycle
    • UIView

    Exerices:

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

    4 điểm

    Exercise 02: View Life Cycle

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

    2 điểm

    Exercise 03: View

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

    2 điểm

  • Fresher Training—iOS Swift Day 4

    Fresher Training—iOS Swift Day 4

    Today topic:

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

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

    Exercises:

    Exercise 01: ENCODING & DECODING

    Make this source code Codeable

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

    Điểm: 1

    Exercise 02: ENCODING & DECODING

                Decoding this JSON

    [{

             “country”: {

                  “country_capital”: {

                      “capital_name”: “Ha Noi”,

                      “population”: 5000000

                  },

                  “country_name”: “Viet Nam”

             }

         },

         {

             “country”: {

                  “country_capital”: {

                      “capital_name”: “Tokyo”,

                      “population”: 4000000

                  },

                  “country_name”: “Japan”

             }

         }

    ]

    Điểm: 2

    Exercise 03: MEMORY MANAGEMENT

        What wrong with below code? Fix it

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

    Điểm: 2

    Exercise 04: PROTOCOL

    Making this source code runable

    var color = UIColor.aliceBlue
    color = UIColor.oceanBlue
    

    Điểm: 2

    Exercise 05: generics

    What is generics? Show me an example

    Điểm: 1

  • Fresher Training—iOS Swift Day 3

    Fresher Training—iOS Swift Day 3

    Today topic:

    • Access Control & Code Organization
    • Custom Operators, Subscripts & Keypaths
    • Pattern Matching
    • Error Handling

    Thao khảo: Swift—Advanced control flow

    Exercises:

    Exercise 01: SINGLETON
    • A singleton is a design pattern that restricts the instantiation of a class to one object.
    • Use access modifiers to create a singleton class Logger. This Logger should:
    • Provide shared, public, global access to the single Logger object.
    • Not be able to be instantiated by consuming code.
    • Have a method log() that will print a string to the console.

    Điểm: 2

    Exercise 02: STACK
    • Declare a generic type Stack. A stack is a LIFO (last-in-first-out) data structure that supports the following operations:
    • peek: returns the top element on the stack without removing it. Returns nil if the stack is empty.
    • push: adds an element on top of the stack.
    • pop: returns and removes the top element on the stack. Returns nil if the stack is empty.
    • count: returns the size of the stack.
    • Ensure that these operations are the only exposed interface. In other words, additional properties or methods needed to implement the type should not be visible.

    Điểm: 2.5

    Exercise 03: SUBSCRIPT
    extension Array {
        subscript(index: Int) -> (String, String)? {
            guard let value = self[index] as? Int else {
                return nil
            }
            
            switch (value >= 0, abs(value) % 2) {
            case (true, 0):
                return ("positive", "even")
            case (true, 1):
                return ("positive", "odd")
            case (false, 0):
                return ("negative", "even")
            case (false, 1):
                return ("negative", "odd")
            default:
                return nil
            }
        }
    }

    What wrong with this code? How to fix?

    Điểm: 1.5

    Exercise 04: ERROR HANDLING

    Write a throwing function that converts a String to an even number, rounding down if necessary.

    Điểm: 2

  • Swift—Advanced control flow

    Swift—Advanced control flow

    Bài viết này giới thiếu cái khái niệm và các dùng về control flow trong Swift:

    For loop

    Countable ranges

    • countable closed range: 0…5
    • countable half-open range: let halfOpenRange = 0..<5

    For in với where condition

    • Swift hỗ trợ for in where để lọc ra các điều kiện phù hợp trong tập cho trước:
    var sum = 0
    
    for i in 1...10 where i % 2 == 1 {
        sum += i
    }

    Continue and labeled statements

    • Trong nhiều trường hợp, khi điều kiện trong vòng loops trở nên phức tạp hơn, thì ta có thể sử dụng “labeled statements” để tiếp tục vòng loop tương ứng, ví dụ như dưới:
    var sum = 0
    rowLoop: for row in 0..<8 {
        columnLoop: for column in 0..<8 {
            if row == column {
                continue rowLoop
            }
            sum += row * column
            print(sum)
        }
    }
    • Ở đây, rowLoop và columnLoop được gọi là “labeled statements”, ta có thể tiếp tục vòng loop tương ứng bằng câu lệnh “continue”

    Switch statements

    • Không như Objective C là switch điều kiện switch (expression) chỉ có thể sử dụng các loại data dạng như int, character…, thì trong Swift, điều kiện switch đã trở nên phong phú và thuận tiện hơn rất nhiều.
    • Các điều kiện switch mà Swift support:
      • Điều kiện switch là một hoặc nhiều giá trị cùng loại data với nhau
      • Điều kiện switch có thể là range
    • Các điểm chính của Switch Statement:
      • No implicit Fallthrough (không tự động chuyển sang case tiếp theo): đối với các case điều kiện của Switch statement thì không có chuyện điều kiện switch được tự động chuyển sang case tiếp theo, nghĩa là mỗi điều kiện trong case của switch đều phải có body. Đoạn code như dưới sẽ báo lỗi khi compile. (Trong trường hợp muốn kết hợp nhiều điều kiện switch case thì sử dụng “compound case” switch.)
    switch age {
    case 19: // Error
    case 20:
        print("adult")
    default:
        print("default case")
    }
    • Interval matching: switch trong Swift hỗ trợ switch case theo từng khoảng giá trị. Ví dụ:
    let age = 18
    
    switch age {
    case 0..<18:
        print("child")
    case 18..<60:
        print("adult")
    default:
        print("old")
    }
    • Tuples: Swift switch statement hỗ trợ điều kiện trong switch case là tuple, cách sử dụng cũng rất linh hoạt. Ví dụ (lấy từ swift.org):
    let somePoint = (1, 1)
    switch somePoint {
    case (0, 0):
        print("\(somePoint) is at the origin")
    case (_, 0):
        print("\(somePoint) is on the x-axis")
    case (0, _):
        print("\(somePoint) is on the y-axis")
    case (-2...2, -2...2):
        print("\(somePoint) is inside the box")
    default:
        print("\(somePoint) is outside of the box")
    }
    • Đối với trường hợp sử dụng tuple thì điều kiện trong case có thể là mapping cả tuple, hoặc chỉ 1 giá trị trong tuple mà thôi, đối với giá trị không cần so sánh thì dùng ký tự “_” (wildcard pattern) để định nghĩa.
    • Value Bindings: trong trường hợp muốn lấy giá trị của giá trị trong tuple khi đã thoả mãn điều kiện thì dùng câu lệnh như sau:
    let anotherPoint = (2, 0)
    switch anotherPoint {
    case (let x, 0):
        print("on the x-axis with an x value of \(x)")
    case (0, let y):
        print("on the y-axis with a y value of \(y)")
    case let (x, y):
        print("somewhere else at (\(x), \(y))")
    }
    • -> như trong trường hợp này thì chỉ cần giá trị point thoả mãn y = 0 thì sẽ lấy được giá trị của x ra bằng câu lệnh “let x”
    • Where: where statement được dùng trong switch case để xử lý những câu switch với điều kiện phức tạp, ví dụ:
    let person = ("Gaby", "Female", 18)
    switch person {
    case (_, _, let age) where (age > 50):
        print("This person is not in age range")
    case (_, let gender, _) where (gender == "Male"):
        print("This is male employee")
    case (_, let gender, _) where (gender == "Female"):
        print("This is female employee")
    default:
        print("Default")
    }
    • Compound case: trong trường hợp nhiều điều kiện case xử lý chung một logic thì ta có thể kết hợp các điều kiện đó vào chung 1 case switch, ví dụ:
    let language = "Swift">switch language {
    case "Swift", "Objective-C":
        print("Company: Apple")
    case "Dart", "Go lang":
        print("Company: Google")
    default:
        print("Some other companies")
    }
    • Switch statement cho phép sau khi xử lý logic ở case trên, sẽ tiếp tục xử lý logic ở case dưới với keyword “fallthrough”

    Control Transfer Statements

    • continue:
      • Câu lệnh continue hỗ trợ việc break ra khỏi vòng lặp hiện tại của for-loop mà không break hoàn toàn khỏi for-loop
    • break
      • Câu lệnh break trong loop statement sẽ ngắt hoàn toàn vòng lặp.
      • Câu lệnh break trong switch statement sẽ kết thúc switch statement. Bởi vì các case của switch statement không được phép empty nên thường add break vào để tránh compile error. Ví dụ add break vào default case.
    • fallthrough
      • Fallthrough cho phép switch statement sau khi thực hiện xong logic của case bất kỳ, sẽ được chuyển tiếp xuống dưới để thực hiện case tiếp theo. Thường ứng dụng trong trường hợp muốn append thêm logic vào kết quả đã được xử lý ở các case của switch statement (sẽ viết ở default case)
    • return
    • throw
      • https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html#ID510