Category: iOS

  • Grand Central Dispatch (Part 3)

    Grand Central Dispatch (Part 3)

    Nội dung bài viết:

    • DispatchBarrier
    • DispatchWorkItem
    • Thread Sanitizer

    Dispatch Barrier

    • 1 trong những vấn đề tiêu biểu của đa luồng như đã đề cập ở phần 1 của bài viết là Race Condition.
    • Có 1 cách đơn giản để tránh Race Condition, đó là dùng serial queue để thực hiện các task, khi đó chỉ được 1 task được thực hiện tại mỗi thời điểm, nhưng sẽ làm app chậm lại vì chạy trên serial queue.
    • Vậy nếu bạn muốn các task được thực hiện concurrent, nhưng khi có 1 task thay đổi tài nguyên chung thì chỉ duy nhất task đó được thực hiện tại thời điểm đó?

    Đừng lo, GCD cung cấp cho bạn DispatchBarrier để xử lí điều đó 1 cách đơn giản.

    Note: Khi 1 task tiến hành việc thay đổi share resources, sẽ có 1 barrier đến, ngăn không cho bất kì 1 task mới nào được thực hiện trong thời gian này. Khi task đó hoàn thành việc thay đổi tài nguyên, barrier mất đi, các tasks tiếp tục chạy concurrent.

    Ví dụ sau đây là cách sử dụng Dispatch Barrier để tránh Race Condition.
    Giả sử bạn có 1 class Person như sau:

    Dưới đây là đoạn code nhiều task khác nhau cùng thay đổi share resources.

    let queue = DispatchQueue(label: "01", attributes: .concurrent)
    let nameChangeGroup = DispatchGroup()
    
    let person = Person(firstName: "Hoang", lastName: "Tuan")
    let nameList = [("A","B"),("C","D"),("E","F"),("G","H"),("I","J")]
    
    for (_, name) in nameList.enumerated() {
        queue.async(group: nameChangeGroup) {
            usleep(UInt32(10000 * idx))
            person.changeName(firstName: name.0, lastName: name.1)
            print("Current name: \(person.name)")
        }
    }
    
    nameChangeGroup.notify(queue: .main) {
        print("Final name: \(person.name)")
    }

    Và đây là kết quả được hiển thị:

    Có thể dễ dàng thấy, các task thực hiện thay đổi tài nguyên cùng thời điểm gây ra sai lệch kết quả. Đồng thời, kết quả đều khác nhau sau mỗi lần Run.

    Đó là lúc chúng ta sẽ sử dụng Dispatch Barrier để xử lí việc này.

    1. Thực hiện việc get name theo sync để không cho các task khác có thể đọc tài nguyên khi có 1 task đang đọc, vì có thể task đang đọc có thể sẽ chỉnh sửa tài nguyên sau đó.
    2. Đưa toàn bộ thân hàm của func changeName vào thực hiện trên luồng isolationQueue1 với flags là 1 barrier. Vì vậy, khi excute changeName, 1 barrier sẽ xuất hiện và không để bất kì task mới nào được thực hiện cho đến khi changeName hoàn thành.

    Và đây là kết quả:

    Kết quả khi đó được thực hiện đúng

    DispatchWorkItem

    DispatchWorkItem là những block of code. Điều khác biệt khi sử dụng DispatchWorkItem là bạn có thể cancel những task đang trong queue.

    Note: Bạn chỉ có thể cancel 1 DispatchWorkItem trước khi nó đi đến đầu queue và bắt đầu execute.

    Khởi tạo 1 DispatchWorkItem:

    let workItem = DispatchWorkItem(qos: .background, flags: .inheritQoS) {
         guard let url = URL(string: self.links[i]) else {
             return
         }
         URLSession.shared.dataTask(with: url) { (data, res, err) in
             guard let imageData = data else {
                 return
             }
             let myImage = UIImage(data: imageData)
             DispatchQueue.main.async {
                 self.listImage[i].image = myImage
             }
         }.resume()
    }

    Đưa DispatchWorkItem vào queue để thực hiện:

    DispatchQueue.global().async(execute: workItem)

    Cancel 1 DispatchWorkItem:

    workItem.cancel()

    Thread Sanitizer

    Sử dụng Thread Sanitizer để phát hiện & debug race condition:

    • Chọn Product>Scheme>Edit Scheme
    • Chọn vào "Thread Sanitizer":

    Khi đó, nếu code của bạn bị race condition, xcode sẽ thông báo cho bạn:

    1 tool khá hay để phát hiện race condition

    Kết luận: GCD là 1 API đỉnh và dễ sử dụng để quản lí multitask, đa luồng, nhưng có 1 nhược điểm là không cung cấp các state của task trong trường hợp bạn muốn quản lí sâu hơn. Khi đó, bạn phải tự custom State cho riêng mình, hoặc dùng Operation.

    -End-

  • Grand Central Dispatch (Part 2)

    Grand Central Dispatch (Part 2)

    Nội dung bài viết:

    • Dispatch Group
    • Semaphores

    Dispatch Group (cont):

    Ở phần trước, bạn đã biết về cơ chế enter-leave-notify của DispatchGroup để nhận biết khi các task trong group đã hoàn thành.
    Vậy nếu bạn muốn block luồng hiện tại cho đến khi các tasks trong group hoàn thành hoặc timeout thì sao? Ngoài cách sử dụng 1 vài logic, thì cách đơn giản hơn là dùng phương thức wait do DispatchGroup cung cấp.

    Phương thức wait của DispatchGroup là 1 sync function nên nó có tác dụng block luồng hiện tại cho đến khi mọi tasks trong group đã được thực hiện xong, hoặc timeout.

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    var formatter: DateFormatter = DateFormatter()
    formatter.dateFormat = "HH:mm:ss"
    
    // 1
    print("Start task 1 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(10))
        print("End task 1 at time: \(formatter.string(from: Date()))")
    }
    
    // 2
    print("Start task 2 at time: \(formatter.string(from: Date()))")
    queue.async(group: group) {
        Thread.sleep(until: Date().addingTimeInterval(2))
        print("End task 2 at time: \(formatter.string(from: Date()))")
    }
    
    // 3
    print("Start call time out at time: \(formatter.string(from: Date()))")
    if group.wait(timeout: .now() + 5) == .timedOut {
        print("Time out at time: \(formatter.string(from: Date()))")
    } else {
        print("All the jobs have completed at time: \(formatter.string(from: Date()))")
    }
    
    // 4
    queue.async(group: group) {
        print("Try to do task 3 at time: \(formatter.string(from: Date()))")
    }
    1. Bạn khởi tạo task đầu tiên, đưa vào queue để thực hiện theo cách async.
    2. Bạn khởi tạo task thứ 2, đưa vào queue để thực hiện theo cách async.
    3. Bạn khai báo hàm wait() với timeOut = 5s. Hàm sẽ block luồng hiện tại cho đến khi các task thực hiện xong hoặc sau 5s.
    4. Khởi tạo 1 task thứ 3, đưa vào queue để thực hiện theo cách async
    Mang đoạn code vào playground, thử sửa thời gian để hoàn thành task 1 thành 1s, xem điều gì sẽ xảy ra 😉
    • Sau 2s kể từ lúc bắt đầu, task 2 hoàn thành
    • Khi gọi wait() thì luồng hiện tại sẽ bị block -> Không thực hiện được tiếp task thứ 3 trong khoảng thời gian đó.
    • Sau 5s kể từ lúc Start time out, các task trong group chưa hoàn thành hết do task 1 cần 10s để thực hiện, vì vậy nên dispatchGroup sẽ trả quyền điều khiển về cho luồng.
    • Ngay sau khi luồng được trả quyền điều khiển, luồng thực hiện ngay lập tức task 3.
    • Sau 10s kể từ lúc bắt đầu, task 1 hoàn thành.

    Note: Vì wait() block luồng gọi nó, nên bạn không bao giờ nên gọi wait ở main queue.

    Semaphores

    • Giả sử bạn có nhiều task cùng truy cập vào 1 tài nguyên chung, và bạn muốn giới hạn số lượng task truy cập vào tài nguyên chung đó tại mỗi thời điểm. -> GCD cung cấp cho bạn Semaphore để giúp bạn xử lí việc này dễ dàng.
    • semaphore cung cấp 2 phương thức là wait()signal(). Chúng tương tự như enter() và leave() của dispatchGroup. wait() sẽ gọi khi bắt đầu 1 task và gọi signal() để thông báo rằng task đã hoàn thành.

    Giả sử bạn có 10 task cùng thực hiện 1 lúc, nhưng bạn muốn chỉ có tối đa 4 task chạy tại 1 thời điểm( ví dụ như load ảnh) -> Semaphore sẽ giúp bạn

    let group = DispatchGroup()
    let queue = DispatchQueue.global(qos: .userInitiated)
    
    // 1
    let semaphore = DispatchSemaphore(value: 4)
    
    // 2
    for i in 1...10 {
        // 3
        queue.async(group: group) {
            // 4
            semaphore.wait()
            // 5
            defer {
                semaphore.signal()
            }
            print(" Start task \(i) at time: \(Date().timeIntervalSince1970)")
    
            Thread.sleep(forTimeInterval: 3)
            print("Finish task \(i) at time: \(Date().timeIntervalSince1970)")
        }
    }
    1. Khởi tạo 1 semaphore, giá trị value biểu thị cho số lượng task tối đa chạy trong 1 thời điểm.
    2. Tạo 1 vòng lặp gồm 10 task chạy.
    3. Mỗi task sẽ chạy theo kiểu async.
    4. gọi hàm wait() để thông báo là task bắt đầu chạy.
    5. gọi signal() để thông báo là task đã hoàn thành.
    Mang đoạn code vào playground, thử không gọi signal() hoặc wait() xem điều gì sẽ xảy ra
    • Task 1, 2, 3, 4 bắt đầu được chạy đầu tiên.
    • Khi task 2, 3, 1 hoàn thành, task 5, 6, 7 ngay lập tức được thực hiện.
    • Task 4 hoàn thành, task 8 ngay lập tức được thực hiện.

      -> Có thể thấy, tại mỗi thời điểm chỉ có tối đa 4 task chạy.

    Note:

    • Luôn nhớ gọi hàm signal() khi task được thực hiện xong, nếu không thì semaphore sẽ không biết task đã hoàn thành để thực hiện task mới.
    • Gọi wait() khi bắt đầu 1 task để thông báo cho semaphore rằng 1 task mới sẽ bắt đầu thực hiện.

    Ở bài viết tiếp theo, mình sẽ giới thiệu về DispatchBarrier, DispatchWorkItem và Thread Sanitizer.

    Nguồn tham khảo: Ray Wenderlich & Lets Build that app.

  • Grand Central Dispatch (Part 1)

    Grand Central Dispatch (Part 1)

    Tổng quan:

    Để app có UI, UX mượt mà và nhanh, ta cần chia các tasks cần nhiều thời gian chạy ra các luồng khác để thực hiện -> GCD là 1 trong các cách giúp các bạn quản lí các luồng đó.

    Nội dung bài viết:

    • 1 số khái niệm cơ bản về luồng, hàng đợi.
    • Cách tạo luồng bằng GCD.
    • Các vấn đề hay gặp phải trong quản lí luồng.
    • Các basic func của GCD.

    Giới thiệu 1 số khái niệm cơ bản về luồng, hàng đợi:

    Thread

    • Luồng là nơi các task được thực hiện.
    • 1 app hoặc 1 chương trình sẽ có 1 hoặc nhiều luồng.
    • Mỗi luồng có thể thực hiện đồng thời, tuy nhiên nó tùy thuộc vào hệ thống để biết khi nào nó được thực hiện và được thực hiện thế nào.
    • GCD được xây dựng dựa trên các luồng. Với GCD, bạn đưa các task vào dispatch queue và GCD sẽ tự quyết định luồng nào được dùng để thực hiện các task.

    Queue

    • Queue là hàng đợi, hoạt động theo cơ chế FIFO.
    • Bạn đưa các task vào queue, và GCD sẽ thực hiện chúng theo cơ chế FIFO.
    • Bản thân Dispatch Queues đã là thread safe nên bạn có thể đưa task vào queue từ bất kì luồng nào.
    • Vì GCD sẽ tự động chọn luồng để thực hiện các task giúp bạn, nên việc của bạn là chỉ cần chọn loại queue phù hợp để đưa task vào.

    Có 2 loại queue là SerialConcurrent.
    Serial: hàng đợi chỉ thực hiện 1 task vào 1 thời điểm, task này xong thì thực hiện task tiếp theo.
    Concurrent: hàng đợi thực hiện nhiều task tại 1 thời điểm, các task vẫn được thực hiện theo thứ tự FIFO, nhưng bạn sẽ không biết số task thực hiện tại 1 thời điểm hay thời điểm các task hoàn thành.

    Serial Queue và Concurrent Queue

    Với GCD, bạn có thể thực hiện các task async hoặc sync.
    sync: 1 task được thực hiện kiểu synchronous sẽ trả quyền điều khiển cho hàm gọi chúng sau khi task được hoàn thành.
    async: 1 task được thực hiện kiểu asynchronus sẽ trả quyền điều khiển ngay lập tức mà không cần đợi phải hoàn thành. Vì vậy, 1 async task sẽ không block luồng hiện tại. Luồng hiện tại sẽ được trả quyền điều khiển để thực hiện task tiếp theo.

    Cách tạo queue với GCD:

    1. Khởi tạo: GCD cung cấp cách khởi tạo luồng với các quality of Service khác nhau như sau:

    Swift gồm 5 loại quality of Service được sử dụng cho các mục đích khác nhau:

    Cách gọi ra Main queue – đây là nơi để bạn thực hiện các task updates UI:

    DispatchQueue.main

    Cách tạo ra 1 private queue: (mặc định là serial queue)

    let serialQueue = DispatchQueue(label: "techOver")

    Để tạo ra 1 queue kiểu concurrent thì bạn set thuộc tính attribute kiểu concurrent:

    let serialQueue = DispatchQueue(label: "techOver", attributes: .concurrent)

    2. Thêm task vào các queue với kiểu thực hiện sync hoặc async:

    DispatchQueue.global().async {
       // do expensive non-UI task
       DispatchQueue.main.async {
          // do updates UI when task is finished
       }
    }

    Các vấn đề hay gặp về quản lí luồng:

    priority inversion: Các task với priority cao sẽ được thực hiện trước các task có priority thấp, tuy nhiên các task có priority thấp lại lock resource, dẫn đến các task với priority cao phải đợi để được thực hiện -> Làm app chậm.
    Race condition: 2 task cùng truy cập để 1 sửa 1 tài nguyên chung trong cùng 1 thời điểm, dẫn đến sai lệch kết quả.

    Deadlock: Xảy ra khi 2 tasks đang đợi lẫn nhau giải phóng tài nguyên để có thể thực hiện tiếp -> Dẫn đến việc cả 2 tasks đều không thể thực hiện -> Treo app.

    Các basic func của GCD:

    1. Dispatch Group: Giả sử bạn có nhiều task được thực hiện theo kiểu async, khi đó bạn sẽ không thể biết khi nào chúng hoàn tất bởi chúng trả về quyền điều khiển ngay lập tức -> Dispatch Group dùng để nhận biết khi 1 nhóm các task được thực hiện xong.

    // 1
    let dispatchGroup = DispatchGroup()
    // 2
    let viewContainer = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
    viewContainer.backgroundColor = .red
    view.addSubview(viewContainer)
    
    // A box move around in the view
    let box = UIView(frame: CGRect(x: 0, y: 0, width: 60, height: 60))
    box.backgroundColor = .yellow
    viewContainer.addSubview(box)
    
    // A label is appear when all the animations finish
    let label = UILabel(frame: CGRect(x: 15, y: 100, width: 360, height: 40))
    label.font = label.font.withSize(50)
    label.text = "All Done!"
    label.textColor = .yellow
    label.textAlignment = .center
    label.isHidden = true
    viewContainer.addSubview(label)
    
    // Animations
    // 3
    dispatchGroup.enter()
    UIView.animate(withDuration: 2, animations: {
        box.center = CGPoint(x: 300, y: 300)
    }, completion: { _ in
        UIView.animate(withDuration: 3, animations: {
            box.transform = CGAffineTransform(rotationAngle: .pi/4)
        }, completion: { _ in
             // 4
             self.dispatchGroup.leave()
        })
    })
    // 5        
    dispatchGroup.notify(queue: .main) {
        UIView.animate(withDuration: 2, animations: {
            viewContainer.backgroundColor = .blue
        }, completion: { _ in
            label.isHidden = false
        })
    }

    Note: Thử đưa đoạn code này vào viewDidLoad và run thử

    1. Khởi tạo 1 DispatchGroup.
    2. Khởi tạo 1 vài view để thực hiện animation.
    3. Gọi hàm enter() để đưa task cần thực hiện vào trong group.
    4. Gọi hàm leave() để báo với group rằng task đã thực hiện xong.
    5. Khai báo xem hàm notify sẽ làm gì khi các task trong group được thực hiện xong. Hàm notify sẽ được tự động gọi đến khi tất cả các task được đưa vào group đã được thực hiện xong.

    Note: Số lần gọi enter() phải bằng số lần gọi hàm leave().

    Kết luận: Thay vì phải đặt delay giữa các animation thì chúng ta có thể dùng DispatchGroup để nhận biết khi 1 animation đã thực hiện xong để thực hiện tiếp animation khác.

    Ở bài viết tiếp theo sẽ tiếp tục là về các basic func và 1 vài advance func của GCD.

  • Binding Objective-C libraries – Xamarin iOS

    Binding Objective-C libraries – Xamarin iOS

    Content

    1. Create project Objective-C lib
    2. Define functions
    3. Config project
    4. Create makefile
    5. Create project binding library in solution Xamarin
    6. Add binding library to project iOS Xamarin
    7. Call Library API trong code Xamarin.

    1. Create project objective-C

    Open Xcode, File -> New project -> Cocoa Touch Static Library> Next

    Tiếp theo, nhập tên project, phần language nhớ chọn Objective-C nhé

    2. Define functions

    Define các functions vào file .m(implement) và file .h (header) phần logic các bạn tự code nhé @@

    3. Config project

    Setting target version sang iOS mà bên Xamarin các bạn sẽ chọn, ở đây project của mình để là 10.0

    4. Run terminal commands

    4.1 Create 1 makefile

    Với content như dưới đây, chú ý thay chỗ in đậm bằng tên của project

    XBUILD=/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild
    PROJECT_ROOT=./TênProject
    PROJECT=$(PROJECT_ROOT)/ TênProject.xcodeproj
    TARGET= TênProject

    all: lib$(TARGET).a

    lib$(TARGET)-i386.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphonesimulator -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphonesimulator/lib$(TARGET).a $@

    lib$(TARGET)-armv7.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphoneos -arch armv7 -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphoneos/lib$(TARGET).a $@

    lib$(TARGET)-arm64.a:
    $(XBUILD) -project $(PROJECT) -target $(TARGET) -sdk iphoneos -arch arm64 -configuration Release clean build
    -mv $(PROJECT_ROOT)/build/Release-iphoneos/lib$(TARGET).a $@

    lib$(TARGET).a: lib$(TARGET)-i386.a lib$(TARGET)-armv7.a lib$(TARGET)-arm64.a
    xcrun -sdk iphoneos lipo -create -output $@ $^

    clean:
    -rm -f *.a *.dll

    4.2 Execute makefile

    Vào terminal đi đến folder chứa Makefile gõ:
    •“make” – để chạy makefile
    •“make clean” – để clean các file thư viện .a được generate ra sau khi đã chạy ”make” ( Hàm này để đỡ phải xoá file .a = tay thôi)

    4.3 The API definition file

    Sau khi đã có file .a, chạy lệnh sau:

    sharpie bind -sdk iphoneos12.4 /Users/apple/Documents/UrlFilterLib2/UrlFilterLib2/*.h -scope /Users/apple/Documents/UrlFilterLib2/UrlFilterLib2/

    Chú ý đường dẫn vào library mỗi máy khác nhau, và cái số ”12.4” là hỗ trợ iOS version mới nhất của Xcode,
    • Ex :  Xcode 10.3 có hỗ trợ iOS mới nhất là 12.4
    • Sau khi chạy lệnh trên, sẽ generate ra file ApiDefinition.cs

    Đến đây là chúng ta đã hoàn thành bước tạo library rồi đó. Tiếp theo là sử dụng nó trong project Xamarin nhé .

    5. Add binding library to project Xamarin

    1. Click chuột phải vào Solution ->  Add ->  Add new project

    2. Chọn iOS -> Library -> Bindings Library.

    3. Next.

    4. Kéo file .a vào binding library project vừa tạo

    5. Alert hiện lên, chọn “Copy the file in the directory”  -> OK
    Copy content trong file ApiDefinition.cs tại step 4  vào file ApiDefinition.cs trong project  Xamarin này.

    6. Sau đó nhấn chọn build Library project.

    6. Add binding library to project Xamarin

    1. Click chuột phải vào “References”, chọn “Edit references”.
    2. Tích chọn Library project..
    3. OK

    7. Call library API

    Sau khi thực hiện các bước ở trên chúng ta có thể gọi chúng như với các lib của Xamarin rồi

    Kết Luận

    Như vậy là chúng ta đã hoàn thành việc binding lib từ objective-c vào Xamarin iOS. Phần tiếp theo mình sẽ hướng dẫn binding với android.
    Tham khảo tại: https://docs.microsoft.com/en-us/xamarin/cross-platform/macios/binding/?context=xamarin/ios

  • Cách upload 1 App iOS lên Deploygate

    Cách upload 1 App iOS lên Deploygate

    Xin chào mọi người, hôm nay mình sẽ hướng dẫn các bạn cách upload 1 app lên Deploygate

    Thông thường việc cài đặt ứng dụng sẽ được thực hiện thông qua App Store, tuy nhiên sẽ có một số trường hợp sẽ ta không cần phải đưa app lên App Store mà thiết bị vẫn có thể cài đặt được. đa số sẽ thuộc vào một trong hai trường hợp sau :

    • Testing: Trước khi release app, ta cần test ứng dụng, vì vậy việc cung cấp bản build để tester có thể test trước khi release là một điều cần thiết
    • In-house Applications: Là những ứng dụng chỉ được sử dụng internal trong một công ty hay tổ chức nào đó ( đối với những ứng dụng In-house application, ta cần có tài khoản Apple Developer Enterprise Program)

    Những điều bắt buộc

    1. Valid Apple developer program account (not the Apple Developer Enterprise Program)
    2. Máy tính chạy Mac OS X
    3. Đã cài Xcode

    Tổng quát

    Bên dưới là danh sách các bước bắt buộc để submit 1 app

    1. Tạo 1 record của app trên iTunes Connect
    2. Cấu hình XCode project cho việc distribution
    3. Export ipa file from xcode
    4. Upload app lên deploygate

    1. Tạo 1 record của app trên iTunes Connect Bạn phải tạo 1 record của app trên iTunes Connect trước khi bạn upload app lên App Store. Nó sẽ chứa tất cả thông tin cần thiết để có thể quản lý để xử lý và hiển thị app trên App Store. Xem thông tin chi tiết tại đây

    2. Cấu hình XCode project cho việc distribution Bạn phải nhập các thông tin để chứng thực app: Identity, Team, Bundle ID, import provisioning file, set version number,… Tạo 1 provisiong profile Xem thông tin chi tiết tại đây

    3. Export ipa file from xcode

    – Achieve app: bước đầu tiên bạn tạo 1 bản lưu trữ của app để build và lưu trữ thông tin app.

    • Chọn scheme hiện tại của app: Ở mục Build only device -> Generic iOS Device
    • Sau đó, trên thanh status bar trên cùng, chọn Product -> Archive Rồi đợi Xcode nó archive app, khi xong thì vô Window -> Organizer để xem cái bản mình vừa mới archive được, bạn cũng có thể xem các bản archive trước đó

    – tiếp theo chúng ta export ra file ipa Xem thông tin chi tiết ở đây

    4. Upload app lên deploygate

    bước 1: đăng ký tài khoản deploygate link đăng ký ở đây

    bước 2: sau khi đăng ký xong ta chọn account -> Organizations -> Create

    • tiếp theo ta điền đầy đủ các thông tin ở trong hình -> Create -> Finish

    bước 3 : Sau khi ấn Finish thành công thì việc còn lại của chúng ta là upload file ipa mà chúng ta export từ xcode lên đây

    • chọn upload App -> tìm tới folder lưu file ipa -> open -> upload
    • việc tiếp theo là chờ đợi tới khi việc upload thành công -> sẽ hiển thị ra một màn hình như bên dưới, tới được bước này thì xin chúc mừng các bạn đã upload thành công app của mình lên deploygate rồi đấy ^^
    • muốn lấy link để tải app của mình cho mọi người, ta chọn vào Add a link for sharing -> hệ thống sẽ tự sinh ra cho mình một link để tải app 😀

    Chúc Các bạn thành công ^^

  • Tổng quan về Mobile App

    Tổng quan về Mobile App

    Ghi chú:  Bài viết này chỉ là một góc nhìn chủ quan của tác giả về mảng mobile app. vì vậy có gì không đúng mọi người có thể đóng góp ở phần comment nhé!! Thank.

    Mở đầu:
    Ở thời điểm hiện tại việc xây dựng ứng dụng native không phải là lựa chọn duy nhất để tạo lên một một ứng dụng mobile app. Ngày nay chúng ta có thể dựa vào yêu cầu của khách hàng, các chức năng của sản phẩm để lựa chọn được hướng đi phù hợp hơn. Ta có thể dựa trên vào công nghệ web (HTML5, CSS3 và JavaScript) đang phát triển mạnh mẽ trên mobile. Hoặc tận hưởng những lợi ích của các công cụ phát triển đa nền tảng như React Native hoặc Flutter. Dưới đây, bạn sẽ tìm thấy chìa khóa để giải quyết vấn đề khó khăn này khi chọn phương pháp phát triển ứng dụng di động.

    Native App

    Native app hay còn được gọi là ứng dụng gốc. Vốn dĩ nó có cái tên này là bởi vì nó được viết bằng chính các ngôn ngữ lập trình gốc thần nhất dành riêng cho từng nền tảng cụ thể. Hai nền tảng di động phổ biến nhất hiện nay là Android và iOS (Windows Phone thì đã bị khai tử vào tháng 10/ 2017 ). Từ đó, các ngôn ngữ lập trình tương ứng được chính các công ty mẹ tạo ra phù hợp với từng nền tảng. Chẳng hạn như Apple đã có Swift, Objecive-C được dành cho lập trình ứng dụng trên nền tảng iOS. Lập trình trên Android thì dùng Java, mặc dù đây không phải ngôn ngữ do Google tạo ra.

    Viết Native App nghĩa là lập trình viên sẽ sử dụng IDE, SDK mà nhà sản xuất cung cấp để lập trình ra một ứng dụng, build ứng dụng đó thành file cài và gửi lên App Store để kiểm duyệt. Người dùng sẽ phải tìm ứng dụng trên App Store, tải về máy và chạy.  

    Với những hệ thống lớn, cần đồng bộ, ta vẫn phải viết phần back-end trên server. Server sẽ đưa ra một số API. Native app lấy dữ liệu về máy, truyền dữ liệu lên server thông qua các API này.

    Ưu điểm

    • Tận dụng được toàn bộ những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
    • Có thể chạy được offline.
    • Performance rất nhanh vì code native sẽ được chạy trực tiếp.
    • UX phù hợp với từng nền tảng
    • Là lựa chọn duy nhất cho các ứng dụng game, xử lý hình ảnh hay video …

    Khuyết điểm

    • Cần cài đặt nặng nề (Android Studio, XCode, Android SDK, …), khó tiếp cận.
    • Với mỗi hệ điều hành, ta phải viết một ứng dụng riêng. Khó đảm bảo sự đồng bộ giữa các ứng dụng (1 button trên Android sẽ khác 1 button trên iOS, pop cũng khác).
    • Cần phải submit app lên App Store, mỗi lần update phải thông báo người dùng.
    • Code mệt và lâu hơn so với Mobile Web dẫn đến một khuyết điểm là chi phí phát triển cao.

    Kĩ năng cần có

    • Ngôn ngữ lập trình: Java / Kotlin cho Android, Objective-C / Swift cho iOS
    • Kiến thức chuyên sâu về ứng dụng: View, Action, Adapter trong Android …
    • Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, …

    __________________________________________________________________________

    Hybrid App

    Hybrid App kết hợp những ưu điểm của Mobile Web và Native App. Ta xây dựng một ứng dụng bằng HTML, CSS, Javascript, chạy trên WebView của mobile. Tuy nhiên, Hybrid App vẫn có thể tận dụng những tính năng của device: chụp hình, GPS, rung, ….

    Hybrid App sẽ được viết dựa trên một cross-platform framework: Cordova, Phonegap, Ionic …. Ta sẽ gọi những chức năng của mobile thông qua API mà framework này cung cấp, dưới dạng Javascript. Bạn chỉ cần viết một lần, những framework này sẽ tự động dịch ứng dụng này ra các file cài đặt cho Android, iOS . Một số ứng dụng không quá nặng về xử lý, cần tận dụng chức năng của device sẽ chọn hướng phát triển này.

    Ưu điểm

    • Chỉ cần biết HTML, CSS, JS .
    • Viết một lần, chạy được trên nhiều hệ điều hành
    • Tận dụng được các chức năng của device.

    Khuyết điểm

    • Không ổn định, khó debug. Framework sẽ dịch code của bạn thành code native, việc sửa lỗi ứng dụng khá khó vì bạn không biết code sẽ được dịch ra như thế nào.
    • Performance chậm.
    • Cần cài đặt nhiều thứ (phải cài đặt SDK này nọ thì mới build ứng dụng được).

    Kiến thức cần biết

    • HTML, CSS, Javscript cơ bản.
    • Cách dùng một số framework CSS, Javascript: jQuery Mobile, Ionic Framework, AngularJS, Bootstrap, …
    • Kiến thức về các cross-platform framework: Cordova, Phonegap
    • Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, … (Hybrid app cũng sẽ kết nối với server thông qua API như Native App).

    __________________________________________________________________________

    Cross-Platform App

    Được sinh ra nhằm mục đích để giải quyết bài toán hiệu năng của Hybrid và bài toán chi phí khi mà phải viết nhiều loại ngôn ngữ native cho từng nền tảng di động. Nhưng chúng ta lại hay nhầm lẫn giữa Hybrid AppCross-Platform App, trên thực tế thì chúng khác hoàn toàn nhau. Có lẽ, đặc điểm chung duy nhất giữa chúng là khả năng chia sẻ source code. Lập trình viên chỉ cần lập trình một lần và biên dịch hoặc phiên dịch ra thành nhiều bản Native App tương ứng với từng nền tảng khác nhau.

    Công cụ quan trọng nhất để thực hiện các dự án ứng dụng đa nền tảng (Cross Platform) chính là Frameworks đa nền tảng. Có rất nhiều Framework đa nền tảng. Mỗi loại sẽ có những điểm mạnh và điểm yếu khác nhau. Tùy vào mục tiêu xây dựng App mà lập trình viên sẽ lựa chọn Framework nào cho phù hợp.

    Nổi tiếng và phổ biến nhất là Framework Xamarin. Ngôn ngữ lập trình chủ đạo trong Xamarin là C#, ngoài ra còn có Objective-C, Swift và Java. Ngoài ra, còn một số cái tên mà khá hot đó là React-Native (thằng này có ông bô là Facebook ), Flutter (thằng này có ông bác là Google)…

    Ưu điểm

    • Tận dụng được những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
    • Hiệu năng tương đối ổn định.
    • Tiết kiệm tiền.
    • Hiệu quả về mặt thời gian khi mà bạn muốn phát triển một ứng dụng nhanh chóng.
    • Trải nghiệm người dùng tốt hơn là hybrid app.

    Nhược điểm

    • Hiệu năng sẽ thấp hơn với app native code.
    • Khó học vẫn đòi hỏi kiến thức native code.
    • Vẫn còn có hạn chế từ framework

    Kĩ năng cần có

    • Kiến thức C# (đối với Xamarin ), JS (đối với React-Native ), Dart(đối với Flutter) Objective-C, Swift và Java cơ bản.
    • Kiến thức về một số framework React-Native, Xamarin …

    __________________________________________________________________________

    Web App

    Hướng Mobile Web thường được áp dụng khi các bạn đã có sẵn một website đang hoạt động. Ta sẽ tạo thêm 1 trang web riêng cho mobile, sử dụng HTML, CSS, một số framework hỗ trợ mobile và responsive (Bootstrap, jQuery Mobile, Materialize). Người dùng sẽ trang web dành cho mobile để dùng ứng dụng.

    Các xử lý khác liên quan đến backend như database sẽ được thực hiện phía trên server. Với một số framework như Angular, VueJS … một trang web có thể giống y hệt một ứng dụng di động thật sự.

    Ưu điểm

    • Chỉ cần có kiến thức về web là viết được
    • Viết một lần, chạy được trên mọi hệ điều hành
    • Người dùng không cần phải cài app, có thể vào thẳng trang web
    • Không cần phải thông qua App Store, tiết kiệm tiền
    • Dễ nâng cấp (Chỉ việc nâng cấp web là xong)

    Nhược điểm

    • Với một số máy đời cũ, Web App sẽ bị bể giao diện, hiển thị sai, hoặc javascript không chạy.
    • Performance chậm
    • Không thể tận dụng được các tính năng của di động: Push notification, chụp hình, nghiêng máy, định vị GPS…

    Kĩ năng cần có

    • Kiến thức HTML, CSS, Javascript cơ bản.
    • Kiến thức về một số framework responsive/mobile như: jQuery Mobile, Bootstrap, …
    • Một số framework javascript để viết Single Page Application: AngularJS, VueJS, …

    Kết Bài

    Sorry các bạn bài viết hơi dài, sau khi nhìn tổng quan về mobile app thì các bạn đã chọn cho mình hướng đi nào chưa? còn mình thì sẽ tiếp tục theo hướng Cross-Platform app.
    Cảm ơn các bạn đã đọc đến đây nhé.

    Tham khảo: https://railsware.com/blog/native-vs-hybrid-vs-cross-platform/

  • Swift Generics (Part 2)

    Swift Generics (Part 2)

    Ở phần 1 của bài viết, bạn đã hiểu được Generic là gì, công dụng và cách sử dụng cơ bản của Generic.
    Ở phần 2 của bài viết, bạn sẽ học được:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.

    Extending a generic type.

    Giả sử bạn có 1 struct Package như sau:

    struct Package<Item> {
        var items = [Item]()
        mutating func push(item: Item) {
            items.append(item)
        }
        
        mutating func popLast() -> Item {
           return items.removeLast()
        }
    }

    Ở đây, Item là 1 generic. Khi bạn extending 1 generic type, bạn không cần khai báo lại <Item>. Item là kiểu dữ liệu generic đã được khai báo trong toàn bộ struct.

    extension Package {
        var firstItem: Item? {
           return items.isEmpty ? nil : items[0]
        }
    }

    extension with a generic where clause

    Giả sử bạn muốn khai báo func isFirstItem(:) như sau:

    Lỗi này tương tự như ở phần 1, khi không phải mọi kiểu dữ liệu trong Swift đều có thể sử dụng toán tử (==) để so sánh. Vậy ở đây, bạn có thể extend generic với 1 mệnh đề where như sau:

    Vì kiểu dữ liệu truyền vào của biến bPerson, mà Person thì không tuân theo Equatable, nên b không có func isFirstItem(:)

    Note: Việc extend 1 generic type với mệnh đề where cho phép bạn thêm 1 điều kiện mới cho extension, vì vậy func isFirstItem(:) chỉ được thêm vào extension khi Item thuộc kiểu Equatable.

    Generic subclass

    Có 2 cách để kế thừa 1 class cha là kiểu generic như sau:

    • Tạo 1 class con kế thừa những thuộc tính và func của class cha nhưng vẫn giữ cho class con là kiểu generic.
    • Tạo 1 class con nhưng là 1 class cụ thể.

    Ở trong ví dụ trên, dễ thấy:

    • Box là một generic subclass của class Package vì bạn muốn chiếc hộp này đựng được bất kì thứ gì, vì vậy Box vẫn là kiểu generic nên khi khởi tạo bạn phải truyền vào một kiểu dữ liệu cụ thể.
    • RosePackage là một subclass cụ thể của class Package, gói RosePackage này sẽ dùng chỉ để đựng hoa hồng.

    Sử dụng generic nâng cao

    Mọi table view cơ bản đều có các func sau:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
    }
    ...

    Trong trường hợp bạn muốn tạo các tableview khác nhau, thì sẽ dẫn đến việc duplicate các đoạn code này.
    -> Sử dụng generic để giảm thiểu duplicate code.
    Example1:

    RoseTableViewController là 1 subclass cụ thể của 1 generic class.

    Ở đây, bạn sử dụng generic để giảm duplicate code như sau:

    • Tạo 1 generic class tên BaseTableViewController chứa các thuộc tính, func cơ bản, và khai báo 1 placeholder T kiểu UITableViewCell để dễ dàng truyền các kiểu cell khác nhau vào.
    • Tạo 1 class RoseCell kiểu UITableViewCell.
    • Tạo class RoseTableViewController extends BaseTableViewController, class này dùng cell kiểu RoseCell nên chúng ta truyền kiểu RoseCell vào.
    • override lại các thuộc tính -> func numberOfRows tự thay đổi theo.

    Example2 : Trong trường hợp bạn muốn mỗi cell có item kiểu dữ liệu bất kì, bạn có thể khai báo 1 class BaseCell kiểu generic, rồi khởi tạo các cell mới extends BaseCell.

    Note: U ở phần khai báo BaseTableViewController và U ở phần khai báo BaseCell là khác nhau, ở đây mình đặt đều là U cho bạn đọc đỡ confuse.

  • Swift Generics (Part 1)

    Swift Generics (Part 1)

    Generic cho phép bạn viết function và type 1 cách linh hoạt, dễ dàng tái sử dụng, có thể hoạt động với bất kì loại nào tùy theo các yêu cầu mà bạn xác định. Với generic, bạn có thể tránh sự trùng lặp của code mà vẫn thể hiện í định của nó 1 cách rõ ràng.

    Ở phần 1 của bài viết, bạn sẽ học được:

    • Generics là gì ?
    • Tại sao chúng hữu dụng ?
    • Cách sử dụng generic đơn giản.

    Getting Started

    Giả sử bạn được yêu cầu viết 1 phương thức tính tổng 2 số với kiểu dữ liệu bất kì. Thông thường, bạn sẽ viết các func riêng cho các kiểu dữ liệu riêng.

    func addInt(num1: Int, num2: Int) -> Int {
        return num1 + num2
    }
    
    func addDouble(num1: Double, num2: Double) -> Double {
        return num1 + num2
    }

    Dễ dàng nhận thấy, func addInt(::) và func addDouble(::) là 2 func khác nhau nhưng có thân hàm giống nhau. Bạn không chỉ có 2 func, mà code của bạn bị lặp lại.
    -> Generic có thể được sử dụng để giảm 2 hàm này thành 1 và loại bỏ sự trùng lặp code.

    Note

    Trong các func ở trên, kiểu dữ liệu của num1num2 là giống nhau. Nếu kiểu dữ liệu của chúng khác nhau thì không thể cộng 2 số.

    Generic Function

    Generic func có thể hoạt động với bất kì kiểu dữ liệu nào. Dưới đây là code generic của addInt(::) ở trên, tên là addTwoNumbers:

    func addTwoNumbers<T>(num1: T, num2: T) -> T {
       return num1 + num2
    }

    Tưởng tượng bạn có 1 UITextfield, UITextfield đó có 1 trường gọi là placeholder. placeholder sẽ được thay thế bằng text khi bạn nhập text vào UITextfield.
    Tương tự như vậy, func addTwoNumbers(::) sử dụng kiểu dữ liệu T như là 1 placeholder. Kiểu dữ liệu T sẽ được thay thế bằng 1 kiểu dữ liệu xác định do bạn quyết định như Int, Double, … mỗi khi func addTwoNumbers(::) dược gọi. Ta có thể thấy trong func addTwoNumbers(::) ở trên, num1 và num2 có cùng kiểu dữ liệu là T.

    Note:

    • Sự khác biệt giữa generic func và non-generic func là: Ở tên của generic func thì có 1 kiểu dữ liệu placeholder (T) được khai báo trong cặp ngoặc nhọn (<T>). Cặp ngoặc nhọn nói với Swift rằng T là kiểu dữ liệu placeholder.
    • Bạn có thể thay T bằng các các tên khác như A, B, C, Element, … tùy bạn muốn. Việc thay đổi tên chỉ có tác dụng thay đổi tên của kiểu dữ liệu placeholder, chứ không thay đổi gì về tác dụng. Nên tránh đặt tên trùng với các kiểu dữ liệu xác định như Int, String, Double, … để tránh gây nhầm lẫn.
    • Và luôn đặt tên bắt đầu bằng chữ cái in hoa để người đọc hiểu rằng đó là 1 placeholder cho 1 kiểu dữ liệu chứ không phải cho 1 giá trị.

    Tuy nhiên, khi khai báo func addTwoNumbers(::) như trên, bạn sẽ gặp lỗi như sau:

    Lỗi này có nghĩa là, bạn không thể áp dụng toán tử + cho kiểu dữ liệu T vì không phải mọi kiểu dữ liệu trong Swift đều có thể cộng với nhau bằng toán tử (+). Để áp dụng được toán tử (+) cho kiểu dữ liệu T, bạn làm như sau:

    protocol Summable {
        static func +(num1: Self, num2: Self) -> Self
    }
    
    func addTwoNumbers<T: Summable>(num1: T, num2: T) -> T {
        return num1 + num2
    }

    Ở đây, bạn khai báo 1 protocol Summable có chứa toán tử (+) và cho T extends Summable. Khi đó có nghĩa là, kiểu dữ liệu xác định mà bạn muốn đưa vào để thay T thì kiểu dữ liệu đó phải extends Summable, và hiển nhiên là chúng sẽ phải khai báo toán tử (+).

    Sử dụng generic:

    Khi bạn gọi hàm addTwoNumbers, bạn sẽ thay T bằng 1 kiểu dữ liệu xác định. Tuy nhiên, vì T là kiểu Summable, nên kiểu dữ liệu xác định mà bạn muốn truyền vào cũng phải extends Summable và khai báo toán tử + cho kiểu dữ liệu đó.

    Example 1: Gọi hàm addTwoNumbers cho 2 số kiểu Int:

    • Trước hết, phải cho Int extend Summable.
    extension Int: Summable {}

    Note: Vì bản thân các kiểu dữ liệu như Int, Double, String, … đã có chứa sẵn toán tử (+), nên không cần khai báo toán tử (+) cho chúng nữa.

    let number1: Int = 5
    let number2: Int = 10
    let total = addTwoNumbers<Int>(num1: number1, num2: number2) // 15

    Example 2: Khởi tạo và khai báo hàm addTwoNumbers(::) cho 2 số kiểu String :v

    Note: Bạn có thể gọi hàm theo cách: addTwoNumbers<String>(num1: string1, num2: string2), hoặc bỏ <String> đi, vì Swift sẽ dựa vào kiểu dữ liệu bạn truyền vào num1, num2 để tự hiểu kiểu dữ liệu xác định bạn muốn thay cho T là String.

    Ở phần tiếp theo, mình sẽ giới thiệu về:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.
  • 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