Category: Swift

  • iOS/Swift: Custom UIProgressView

    iOS/Swift: Custom UIProgressView

    Lời mở đầu

    Có thể các bạn đã biết progress view của Apple nó khá là đơn giản và ýt thuộc tính để chúng ta có thể sử dụng cũng như thay đổi để phù hợp với thiết kế. Trong dự án gần đây mình đã có cơ hội để động vào nó. Trong thời gian đầu mình tự mày mò thì progress bar không hỗ trợ việc như vậy. Vì vậy mình viết bài này để chia sẻ về cách mình đã custom lại progress này. Hi vọng nó giúp các bạn gặp khó khăn với việc custom lại progress bar.

    Vấn đề cần giải quyết: Làm thế nào để bo tròn góc của progress bar

    Đây là progress view mình cần đạt được:

    Đây là progress bar mặc đinh của apple:

    Phân tích

    Khi thử đọc tài liệu về UIProgressView. Không có thuộc tính nào giúp mình có thể đạt được điều mình muốn.

    Để bo tròn cả progressview thì ta có thể dùng layer.cornerRadius, nhưng để bo tròn thằng progress chạy bên trong thì sao? Chúng ta cần tìm được cái view đó và bo nó lại.

    Giải quyết bài toán

    Bước 1: Chúng ta cần tạo constraint height cho progressView

    Chúng ta có 2 cách để tăng height cho ProgressView:
    1. Sử dụng Transform Scale: Nó sẽ làm cornerRadius chạy không đúng
    2. Sử dụng constraint height: Nên chúng ta chon cách này

    Tuy rằng chúng ta đã constraint height của nó bằng 16 nhưng thực tế height của progressView vẫn = 2. Vì vậy khi set cornerRadius chúng ta không thể sử dụng frame.height/2 được mặc dù đó là cách tốt nhất đối với các view khác.

    Bước 2: Bo tròn góc cho trackView

    progressBar.layer.cornerRadius = 8.0
    progressBar.clipsToBounds = true

    Bước 3: Bo tròn progressView

            if let sublayers = progressBar.layer.sublayers, sublayers.count > 1 {
                sublayers[1].cornerRadius = 8.0
            }
            progressBar.subviews[1].clipsToBounds = true

    Nếu để ý kĩ các bạn sẽ thấy progress bar này gồm 2 view chồng lên nhau đó là Progress View và track view. Và theo thứ tự trong lập trình thì thằng đầu tiên là thằng nằm dưới có index = 0. Chúng ta nhìn thấy progress view vì nó nằm trên Track View -> nó có index = 1

    Lưu ý: Nó chỉ đúng khi chúng ta không thêm subview, sublayer cho ProgressView. Vậy nên trong trường hợp này chúng ta sẽ ổn.

    If let giúp chúng ta handle trường hợp crash app, khi các layer của progress bị remove. (nó thường k xảy ra, nhưng vì an toàn chúng ta nên thêm dòng này)

    sublayers[1]: là layer của track view
    subviews[1]: là trackView

    Kết quả thu được thật mỹ mãn 😀

    Ngoài ra các bạn có thể sử dụng image cho progress view cũng như trackView để có 1 progress đẹp hơn.

    progressBar.progressImage = UIImage(named: "2697")

    Các bạn có thể tải về file assets ở dưới:

    Tổng kêt

    Mình hi vọng bài viết dưới đây giúp các bạn hiểu rõ hơn về UIProgressView và dễ dàng hơn khi làm việc với nó.

  • SQLite with FMDB in Swift (P2)

    SQLite with FMDB in Swift (P2)

    Nội dung bài viết:

    • Truy vấn insert
    • Truy vấn select
    • Truy vấn delete
    • Truy vấn update

    Truy vấn INSERT

    • Câu truy vấn này dùng để thêm bản ghi vào table
    • Có 2 cách để viết 1 câu truy vấn insert.
      Cách 1:

    Cách 2: Trong trường hợp bạn tạo 1 bản ghi mới đầy đủ các trường của bảng ghi, thì k cần liệt kê cụ thể tên các trường như cách 1:

    • Để thực hiện truy vấn INSERT vào table Book ở bài viết trước, việc đầu tiên là viết 1 câu truy vấn:
    let query = "insert into Book (name, author, price) values ('Kill the mockingbird', 'HarperLee', 85000)"

    Đây là 1 câu truy vấn thực hiện sự thay đổi đến database, nên ta vẫn sử dụng lệnh executeUpdate(…) để thực hiện câu query:
    Ở đây, vì table Book được khởi tạo với thuộc tính id tự động tăng, nên ta có thể bỏ qua việc khai báo id.

    func insertBook() {
        if openDatabaseConnectionAtPath(path: getDatabasePath()) {
            let query = "insert into Book (name, author, price) values ('Kill the mockingbird', 'HarperLee', 85000)"
            do {
               try database.executeUpdate(query, values: nil)
            } catch let err {
               print("Insert failed, error: \(err.localizedDescription)")
            }
        }
        database.close()
    }

    Ở đây, table Book có cả 4 trường đều khác nil, tuy nhiên trường id và price có thuộc tính default nên có thể không cần khai báo trong câu lệnh insert, nhưng nếu bạn insert thêm 1 bản ghi mà không có trường name hay author thì FMDB sẽ trả về lỗi và không thể insert được.

    Note: Bạn có thể thực hiện nhiều câu truy vấn cùng 1 lúc, bằng cách gộp chúng vào 1 câu truy vấn chung, mỗi câu truy vấn nhỏ ngăn cách nhau bằng 1 dấu ;

    • Ngoài ra, còn có 1 cách thực hiện câu lệnh executeUpdate thuận tiện hơn bằng cách sử dụng thuộc tính values như sau:
    • Ở đây, nameauthor không được cụ thể ngay từ câu truy vấn, mà được để 1 dấu ? có tác dụng như 1 placeholder để truyền trường name, author vào sau.
    • Khi thực hiện lệnh executeQuery, điền lần lượt các giá trị muốn truyền tương ứng vào các dấu ? vào thuộc tính values.

    Truy vấn SELECT:

    • Câu truy vấn này dùng để lấy các bản ghi từ table.
    • Cách viết câu truy vấn Select:
      + SELECT column1, column2,… FROM table_name; // Lấy 1 vài trường cụ thể
      + SELECT * FROM table_name; // Lấy tất cả các bản ghi
      + Ngoài ra có thể kết hợp thêm Where, Order by,… vào câu truy vấn để lọc các bản ghi
    • Ví dụ thực hiện câu truy vấn lấy tất cả các sách trong table Book:
    Lấy tất cả các bản ghi và sắp xếp theo giá tăng dần
    Lấy tất cả các tên tác giả mà có trường id = 2
    • Câu truy vấn select k làm thay đổi DB, nên sử dụng lệnh executeQuery(…) để thực hiện.
    • Câu truy vấn này trả về 1 tập dữ liệu FMResultSet, từ đó convert về kiểu dữ liệu mà ta mong muốn.

    Truy vấn DELETE

    • Dùng để xóa bản ghi đang có trong table.
    • Cách viết câu truy vấn delete:
      DELETE FROM table_name WHERE condition;
    • Ví dụ thực hiện câu truy vấn delete:

    Truy vấn UPDATE:

    • Dùng để thay đổi giá trị của bản ghi đang có trong table.
    • Cách viết câu truy vấn:
      UPDATE table_name SET column1 = value1, column2 = value2, … WHERE condition;
    • Ví dụ thực hiện câu truy vấn:

    Kết luận:

    • Nên define các trường thành Constant để tránh gõ nhầm khi viết câu truy vấn.
    • Sử dụng lệnh executeUpdate để thực hiện thay đổi DB, dùng lệnh executeQuery để thực hiện việc lấy các bản ghi.
    • Các câu lệnh có thể gộp vào với nhau, nhưng ngăn cách với nhau bằng dấu ;.
    • Luôn đóng liên kết tới DB sau khi thực hiện xong câu truy vấn.
  • SQLite with FMDB in Swift (P1)

    SQLite with FMDB in Swift (P1)

    Đối với việc làm ứng dụng phần mềm, đa số đều phải sử dụng đến database. Core Data của Apple cung cấp là 1 trong những sự lựa chọn phổ biến đối với ứng dụng trên iphone.
    Theo í kiến cá nhân thì Core Data thường được dùng cho các database được tạo ra trong quá trình sử dụng, còn trong trường hợp nếu bạn có 1 database có sẵn để đưa vào app thì sao? Khi đó thì cần đưa 1 database đã được nhập sẵn vào app. SQlite là 1 lựa chọn tốt Ở bài viết này, mình sẽ giới thiệu cách sử dụng SQLite trong swift bằng thư viện FMDB.

    Nội dung bài viết

    • Giới thiệu về FMDB
    • Tạo đường dẫn cho 1 database
    • Khởi tạo 1 database
    • Tạo kết nối đến database
    • Thực hiện truy vấn

    FMDB Library:

    • FMDB là 1 thư viện hỗ trợ truy vấn Sqlite và quản lí dữ liệu hiệu quả.
    • Giúp bạn xử lí việc open connection/ close connection đến DB.
    • Có thể dùng cho cả swift hoặc obj-C.
    • Dễ dàng và nhanh chóng để tích hợp vào 1 project.
    • Cách sử dụng tương đối đơn giản, cho phép người dùng tự viết câu truy vấn theo ý muốn.

    Bài viết này tập trung vào việc sử dụng SQlite bằng thư viện FMDB, không hướng dẫn về phần cài đặt FMDB. Nếu bạn chưa biết về cách cài đặt thư viện, hãy xem thêm tại:

    https://guides.cocoapods.org/using/using-cocoapods.html

    Trong bài viết này, mình sẽ lấy 1 ví dụ về việc khởi tạo và viết các câu truy vấn đến 1 DB chứa các quyển sách.
    Khởi tạo struct Book:

    struct Book {
        let id: Int
        let name: String
        let author: String
        let price: Double
        
        func toString() {
            print("Book's info: id: \(id), name: \(name), author: \(author), price: \(price)")
        }
    }
    

    Tạo 1 class DatabaseManager để thực hiện việc truy vấn DB.
    Khởi tạo 1 biến database kiểu FMDatabase – Sẽ dùng để truy cập và thực hiện truy vấn đến DB.

    import Foundation
    import FMDB
    
    class DatabaseManager {
        static let shared = DatabaseManager()
        var database: FMDatabase!
    }

    Tạo đường dẫn để lưu DB:

    Nếu chưa có 1 DB có sẵn, thì khi tạo 1 DB cần phải có đường dẫn để lưu DB. Khởi tạo 1 đường dẫn cho 1 database có tên bookDB như sau:

    func getDatabasePath() -> String {
       let directoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
       let path = directoryPath.first!.appendingPathComponent("musicDB.db")
       print(path)
       return path.description
    }

    Sau khi print được ra đường dẫn, để xem database vừa tạo: Vào Finder, sử dụng tổ hợp phím Cmd + Shift + G, post đường dẫn vừa lấy được vào, Finder sẽ đưa đến database vừa tạo.

    Trong trường hợp đã có 1 DB bookDB có sẵn và được kéo vào trong app, thì lấy đường dẫn như sau:

    func getPathOfExistDB() -> String? {
       if let path = Bundle.main.path(forResource: "bookDB", ofType: ".db") {
           return path
       } 
       return nil
    }

    Khởi tạo 1 database

    func createDatabase() {
        if database == nil {
            if !FileManager.default.fileExists(atPath: getDatabasePath()) {
                database = FMDatabase(path: getDatabasePath())
            }
        }
    }

    Nếu 1 DB chưa tồn tại, thì khởi tạo 1 DB mới tại 1 đường dẫn đơn giản bằng cách khởi tạo 1 object FMDatabase tại 1 đường dẫn.

    Note: Việc check xem đã tồn tại 1 DB tại 1 path cụ thể trước khi khởi tạo rất quan trọng, bởi nếu ta path đó đã có sẵn 1 DB rồi thì DB đó sẽ bị hủy đi để khởi tạo 1 DB mới.

    Tạo kết nối đến Database:

    Note: Khi muốn truy suất đến DB thì phải mở 1 liên kết với DB, khi đã hoàn thành việc truy suất thì phải đóng liên kết lại.

    • Tạo liên kết tới DB:
    database.open()
    • Đóng liên kết tới DB:
    database.close()

    2 hàm trên đều trả về giá trị Bool để biết việc mở/đóng liên kết có thành công hay không.
    Ta có thể kết hợp việc tạo DB cùng với việc mở/đóng liên kết thành 1 hàm để tiện sử dụng:

    func openDatabaseConnectionAtPath(path: String) -> Bool {
        if database == nil {
            if !FileManager.default.fileExists(atPath: path) { // don’t want to create the database file again and destroy the original database.
                database = FMDatabase(path: path)
            }
        }
            
        if database != nil {
            if database.open() {
                print("Open Success")
                return true
            }
        }
        print("Open failed")
        return false
    }

    Tạo 1 bảng trong DB:

    Câu truy vấn tạo bảng:

    Viết câu truy vấn tạo bảng trong FMDB như sau:

    let query = "create table Book (id integer primary key autoincrement not null, name text not null, author text not null, price float not null default 0)"

    Để chạy câu truy vấn:

    database.executeUpdate(query, values: nil)

    Câu lệnh executeUpdate(…) dùng để thực hiện những câu truy vấn tạo ra sự thay đổi đến DB.

    • Thuộc tính values của hàm executeUpdate sẽ được nói ở phần sau, ở đây tạm thời để nil.
    • Hàm thực hiện câu truy vấn tạo bảng:
    func createBookTable() {
        if openDatabaseConnectionAtPath(path: getDatabasePath()) {
            let query = "create table Book id integer primary key autoincrement not null, name text not null, author text not null, price float not null default 0"
            do {
                try database.executeUpdate(query, values: nil)
            } catch let err {
                print("Execute query failed. error: \(err.localizedDescription)")
            }
        }
        database.close()
    }

    Hàm createBookTable ở trên sẽ tạo ra 1 DB nếu DB chưa tồn tại, còn nếu DB đã tồn tại rồi thì sẽ tạo liên kết, thực hiện truy vấn rồi đóng liên kết.
    Thực hiện câu truy vấn createBookTable:

    Kiểm tra kết quả:

    Ở phần tiếp theo, sẽ nói về các câu truy vấn insert, delete,…

  • Passing Data with Callback Swift

    Passing Data with Callback Swift

    Một trong những vấn đề cơ bản trong lập trình ứng dụng là truyền data giữa các view controller. Có rất nhiều cách để làm điều này, như là dùng protocol, notification,… Ở bài viết này, mình sẽ giới thiệu đến 1 cách nữa, đó là sử dụng callback.
    Bài viết này yêu cầu sự hiểu biết về closure. Nếu chưa hiểu về closure, bạn có thể đọc tại đây:

    Nội dung bài viết

    • Function type
    • Callback là gì
    • Lợi ích của callback
    • Truyền data bằng cách sử dụng callback.

    Function type:

    Mọi function đều có 1 kiểu dữ liệu cụ thể, được tạo bởi kiểu dữ liệu của các tham số truyền vàokiểu dữ liệu trả về của function đó.

    Ví dụ ở func trên, không có tham số truyền vào và không có kiểu dữ liệu trả về, nên function type của func trên là () -> ().

    Đối với func không có kiểu trả về, thì cũng có thể viết theo cách khác là func đó trả về kiểu Void.

    func calculate(a: Int, b: Int) -> Int {
        return a + b
    } 
    // Function này có kiểu dữ liệu là (Int, Int) -> Int
    

    Ở bài viết lần này, mình sẽ không nói sâu về function type, mà sẽ tập trung vào chủ đề passing data bằng callback. Vậy callback là gì?

    Callback là gì?

    • Callback có thể hiểu như là 1 closure được gán cho 1 biến.
    • Để sử dụng callback truyền data, bạn khai báo callback với kiểu dữ liệu là kiểu dữ liệu của data mà bạn muốn truyền đi.
    var onCalculate: (Int, Int) -> (Int)

    Ở trên là ví dụ cách khai báo 1 callback có kiểu dữ liệu (Int, Int) -> Int. Callback này sẽ truyền data là 2 tham số kiểu Int đi, và khi 1 nơi khác nhận được data này, nó sẽ xử lí data và trả về 1 kiểu Int.

    var onPrint: (String) -> Void

    Ở ví dụ này, callback sẽ truyền data kiểu String đi, và khi 1 nơi khác nhận được dữ liệu, nó sẽ xử lí dữ liệu theo kiểu Void.

    Note: Nếu muốn truyền đi dữ liệu kiểu khác, bạn chỉ cần đơn giản sửa callback thành kiểu dữ liệu bạn mong muốn truyền đi.

    Truyền data sử dụng callback

    Giả sử có 2 view controller như sau:

    • View Controller 1 có 1 label để hiện thị kết quả, và 1 button để push sang View Controller 2.
    • View Controller 2 có nhiệm vụ thực hiện tính toán tổng của 2 số kiểu Int và trả kết quả kiểu Int về cho View Controller 1 để hiển thị. -> VC2 muốn truyền đi 1 data kiểu Int.
    • View Controller 1 sau khi nhận được data của VC2, sẽ hiển thị lên label.

    VC1:

    class FirstViewController: UIViewController {
        @IBOutlet weak var myLabel: UILabel!
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        @IBAction func didTapButtonGoNext(_ sender: Any) {
            let storyboard = UIStoryboard(name: "Main", bundle: nil)        
            let secondVC = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
            
            // Viết code hiển thị kết quả ở đây
            
            navigationController?.pushViewController(secondVC, animated: true)
            
        }
    }

    VC2:

    class SecondViewController: UIViewController {
        // 1
        var completionSum: ((Int) -> (Void))?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // 3
            let result = calculate(num1: 9, num2: 8)
            completionSum?(result)
            
            // 4
            navigationController?.popViewController(animated: true)
        }
        
        // 2
        func calculate(num1: Int, num2: Int) -> Int {
            return num1 + num2
        }
    }
    1. Khởi tạo 1 callback kiểu (Int) -> (Void), tức là callback này sẽ truyền đi data kiểu Int, và khi nhận được data sẽ xử lí theo kiểu Void.
    2. Khai báo func calculate để tính tổng 2 số và trả về kết quả kiểu Int.
    3. Tính tổng 2 số 9 và 8. Gọi completionSum?(result) để truyền đi result.
      Ở đây có dấu ? ở callback vì callback này là kiểu optional. Khi callback chưa được khởi tạo thì trình biên dịch sẽ bỏ qua mà không gọi callback. Vì vậy hãy chắc chắn rằng bạn đã khởi tạo callback trước khi gọi chúng.
    4. Back về VC1 để hiển thị kết quả.

    Giờ thì quay trở lại VC1, add thêm đoạn code sau vào phần để trống để khởi tạo completionSum cho VC2, bằng cách gán completionSum bằng 1 closure có cùng kiểu dữ liệu.

    secondVC.completionSum = { [weak self] (result) in
        self?.myLabel.text = String(result)
    }

    Ở đây bạn đã viết đoạn code để xử lí dữ liệu nhận được từ VC2. Sau khi nhận được result kiểu Int, bạn sẽ xử lí dữ liệu theo kiểu Void bởi kiểu dữ liệu của callback là (Int) -> Void.

    Khi VC2 gọi completionSum để truyền result đi, VC1 sẽ ngay lập tức nhận được và gọi hàm update label ở trên. Trình biên dịch sẽ chạy theo trình tự như sau:

    • Bấm vào button 1 -> Khởi tạo VC2, khởi tạo completionSum cho VC2 -> push sang VC2.
    • VC2 tính toán kết quả -> gọi completionSum -> VC1 nhận được kết quả và update cho label -> VC2 pop về VC1 để hiển thị kết quả.

    Ngoài ra bạn có thể khởi tạo callback bằng cách gán callback bằng 1 func có sẵn có cùng kiểu dữ liệu.
    Ở VC1, khai báo 1 func như sau:

    func showResult(result:  Int) {
        myLabel.text = String(result)
    }

    Func này có kiểu dữ liệu (Int) -> Void, cùng kiểu dữ liệu với callback completionSum.
    Sửa lại đoạn khai báo completionSum ở VC1 thành như sau:

    secondVC.completionSum = showResult

    Cách làm này cùng tác dụng với cách khai báo ở trên nhưng sẽ làm cho func ngắn gọn và clear hơn.

    Lợi ích của việc dùng callback

    • Đối với việc chỉ cần truyền những data đơn giản, xử lí đơn giản thì có thể dùng callback chứ không phải tạo protocol, notification,…
    • Tính reuseable cao, và ngắn gọn.
  • Push Notification trên iOS Simulator

    Push Notification trên iOS Simulator

    Như các bạn đã biết, để dùng APNS (Apple Push Notification service) thì chúng ta cần phải có device thật. Nhưng chuyện đó đã là quá khứ khi ở bản 11.4 beta, Apple đã cho phép test push notification ngay trên simulator. Tuyệt vời !!! ?

    Để có thể push notification trên simulator, bạn cần:
    Bước 1: Tải Xcode 11.4 beta hoặc các phiên bản mới hơn tại link nè: https://developer.apple.com/download/

    Bước 2: Tạo project và grant permission 
    Appdelegate.swift, import framework UserNotifications, và yêu cầu quyền nhận notification ở hàm application(_:didFinishLaunchingWithOptions:)

    Bước 3: Tạo file APNS payload
    APNS payload là một file json dictionary chứa đựng các thông tin của Notification như kiểu thông báo, nội dung thông báo… Bạn có thể vào đây để xem thêm chi tiết:
    https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/generating_a_remote_notification

    Mình tạo file payload thêm 1 key “Simulator Target Bundle” như sau:

    Trong đó “yourBundleID” là bundleID app của bạn, bundle project của mình là “com.self.NotificationSimulator

    Bước 4:  Giờ kéo thả vào simulator thôi!
    Giờ bạn hãy kéo file payload vừa tạo vào simulator, xem điều kì diệu gì xảy ra nhé 

    Simulator đã có notification ?

    Ngoài cách kéo thả file APNS vào Simulator, ta còn có thể dùng câu lệnh Command để gửi noti. Ở Xcode 11.4 này đã có thêm command xcrun simctl push hỗ trợ việc bắn notification.

    xcrun simctl push <simulator-identifier> <path-to-payload-file>

    trong đó <simulator-identifier> là ID của simulator, <path-to-payload-file> là đường dẫn đến file payload. Bạn có thể lấy ID simulator như sau:

    Nếu bạn ngại việc copy identifier, bạn có thể dùng xcrun simctl push booted <path-to-payload-file> để push notification ngay trên simulator đang mở. Và kết quả:

    Kết luận

    Giờ đây ta có thể test push notification thật đơn giản trên simulator. Ta có 2 cách để test:
    – Kéo thả file APNS vào simulator
    – Trỏ đường dẫn file APNS hoặc Json payload qua command line
    Sau bài viết này, mình sẽ giới thiệu các bạn về Leanplum – một marketing platform cho mobile, và xem điểm giống và khác nhau giữa Leanplum vs Firebase nhé

    Nguồn: https://swiftsenpai.com/xcode/simulating-push-notifications-in-ios-simulator/

  • iOS: Slicing Image

    iOS: Slicing Image

    Nội dung bài viết:

    • Tổng quan
    • Cắt ảnh bằng Xcode Slicing Feature
    • Cắt ảnh bằng code.

    Tổng quan

    Trong 1 app iOS, sẽ có những buttons, views,… có kích thước khác nhau nhưng lại có cùng 1 background image.
    Nếu chỉ đơn giản dùng 1 background image cho các buttons, views,… đó thì bạn sẽ gặp phải tình trạng sau:

    Trong trường hợp này, việc sử dụng chung 1 image sẽ làm image bị co/dãn, làm xấu ảnh.
    Vậy nếu bạn chỉ muốn co/dãn phần giữa của ảnh, còn các phần ở 4 góc không bị co/dãn -> Hãy dùng slicing image.

    • Slicing image cho phép bạn có thể cắt 1 bức ảnh theo chiều dọc, chiều ngang hoặc cả ngang và dọc.
    • Nếu cắt theo chiều dọc, bạn sẽ chia bức ảnh làm 3 phần: Left, center và right. Trong trường hợp này, phần left và right là không co/dãn theo chiều ngang, phần center là phần co/dãn theo chiều ngang. (ảnh trên)
    • Nếu cắt theo chiều dọc, bạn sẽ chia bức ảnh làm 3 phần là top, center và bottom. Trong trường hợp này, phần top và bottom là không co/dãn theo chiều dọc, phần center là phần co/dãn theo chiều dọc (ảnh trên).
    • Nếu cắt theo chiều cả ngang và dọc, bạn sẽ chia bức ảnh làm 9 phần như ảnh trên và từng phần sẽ co/dãn như ảnh dưới.

    Cắt ảnh bằng Xcode Slicing Feature:

    1. Chọn vào phần Assets.xcassets trong Xcode.
    2. Chọn ảnh cần cắt.
    3. Chọn Editor -> Show Slicing hoặc chọn nút Show Slicing ở góc dưới bên phải.
    4. Chon Start Slicing
    1. Chọn cắt ảnh theo chiều ngang, chiều dọc hoặc cả ngang và dọc.
    1. Kéo các thanh để xác định phần bên phải, phần bên trái.
    • Phần 1: Phần không bị co/dãn bên trái theo chiều ngang.
    • Phần 2: Phần sẽ bị co/dãn theo chiều ngang.
    • Phần 3: Phần sẽ bị xóa khỏi ảnh mới.
    • Phần 4. Phần không bị co/dãn bên phải theo chiều ngang.
    Kết qủa: Ảnh bị co dãn phần ở giữa nhưng 2 bên không bị co dãn -> ảnh không có cảm giác bị méo

    Cắt ảnh bằng code:

    Bạn tạo ra 1 ảnh mới bằng cách cắt 1 ảnh có sẵn bằng hàm: resizableImage(withCapInsets:) hoặc resizableImage(withCapInsets:resizingMode:)

    if let img = UIImage(named: "button") {
        let resizable = img.resizableImage(withCapInsets: UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24), resizingMode: .stretch)
    }
    • Khai báo top và bottom # 0 nếu bạn muốn cắt ảnh theo chiều ngang.
    • Khai báo left và right # 0 nếu bạn muốn cắt ảnh theo chiều dọc.
    • Khai báo top, left, right và bottom # 0 nếu bạn muốn cắt ảnh theo cả ngang và dọc.

    Hi vọng qua bài viết này, bạn sẽ không gặp khó khăn khi apply 1 ảnh cho các buttons, views có size khác nhau.

    Link tham khảo: https://developer.apple.com/documentation/uikit/uiimage

  • Sự khác biệt giữa Struct và Class

    Sự khác biệt giữa Struct và Class

    Class và Struct là những thứ bất cứ 1 lập trình viên cũng thường xuyên sử dụng. Tuy nhiên, đối với những người mới thì việc phân biệt giữa class và struct là vẫn còn mơ hồ.
    Ở bài viết này, mình sẽ nói về các điểm khác nhau giữa Class và Struct.

    Nội dung bài viết:

    • Struct và Class là gì?
    • Điểm giống nhau giữa Struct và Class
    • Điểm khác nhau giữa Struct và Class
    • Khi nào nên sử dụng struct / class

    Struct và Class là gì?

    Struct và class là các cấu trúc linh hoạt, được sử dụng với nhiều mục đích khác nhau để trở thành các khối xây dựng chương trình của ban. Bạn định nghĩa các thuộc tính và phương thức để thêm vào các struct/class của bạn.

    Cách khởi tạo 1 struc và 1 class khá giống nhau.

    Điểm giống nhau giữa struct và class:

    Struct và Class đều có thể:

    • Định nghĩa, khai báo các thuộc tính và hàm.
    • Khai báo subscripts.
    class Rank {
        subscript (index: Int) -> String {
            switch index {
                case 1: return "First"
                case 2: return "Second"
                case 3: return "Three"
                default: return "Dont have rank"
            }
        }
    }
    
    let rank = Rank()
    print (rank[1]) // -> "First"
    print (rank[12]) // -> "Dont have rank"
    • Khai báo các initializers để khởi tạo.
    • Có thể mở rộng bằng extension.
    • Có thể implement các protocol để cung cấp các chức năng tiêu chuẩn.

    Điểm khác nhau giữa Struct và Class:

    Initialize:

    Khi định nghĩa 1 class, bạn bắt buộc phải khởi tạo 1 hàm init cho các thuộc tính không phải optional hoặc chưa có giá trị default.

    class Car {
        let id: Int = 1
        var color: UIColor?
        var price: Double
        
        init(price: Double) {
            self.price = price
        }
    }
    
    let car1 = Car(price: 5000)

    Còn khi định nghĩa 1 struct, bạn không cần phải khởi tạo 1 hàm init bởi khi đó Struct đã tự định nghĩa 1 hàm init default cho bạn.

    struct Car {
        let id: Int = 1
        var color: UIColor
        var price: Double
    }
    
    let car1 = Car(color: .red, price: 5000)

    Tuy nhiên, nếu bạn khai báo thêm các init khác thì hàm init default của struct sẽ bị mất.
    Vì vậy, để tránh điều này, chỉ cần khai báo các hàm init mới ở extension thì hàm init default sẽ không bị mất.

    Struct là Value types còn Class là Reference types

    Value type: 1 instance có kiểu là value type thì nó sẽ tự tạo ra các bản copy các giá trị của mình để truyền đi mỗi khi được nó đươc dùng để gán cho các instance khác, hoặc khi được dùng để truyền vào hàm. Bởi vậy, nếu bạn thay đổi giá trị các bản copy thì giá trị bản gốc cũng sẽ không bị thay đôi:

    let car1 = Car(price: 5000)
    var car2 = car1
    car2.price = 10000
    print(car1.price) // -> 5000.0
    print(car2.price) // -> 10000.0

    Reference type: Thay vì việc tạo ra các bản sao, thì 1 instance kiểu reference type sẽ tự truyền đi 1 tham chiếu tới chính nó khi được gán cho các insstance khác hoặc khi được truyền vào hàm.

    let car1 = Car(price: 5000)
    let car2 = car1
    car2.price = 10000
    print(car1.price) // -> 10000.0
    print(car2.price) // -> 10000.0
    print(car1 === car2) // -> true

    Có thể hiểu đơn giản rằng, car1 sẽ tự gán chính bản thân nó cho car2 chứ không tạo ra các bản copy như struct, bởi vậy khi thay đổi thuộc tính của car2 thì car1 cũng bị thay đổi theo.

    Note: Có thể kiểm tra 2 đối tượng có cùng trỏ tới 1 instance hay không bằng toán từ ===

    Class có thể kế thừa, còn struct thì không

    Class hỗ trợ kế thừa, có thể tạo ra các class con kế thừa từ class cha để mang những thuộc tính, phương thức của class cha. Có thể thấy class hỗ trợ lập trình OOP tốt hơn struct.

    Các phương thức trong struct nếu muốn thay đổi thuọc tính thì phải thêm mutating

    Struct là kiểu value type. Mặc định thì các thuộc tính của 1 biến kiểu value type không thể bị sửa đổi trong các hàm của biến đó.
    Tuy nhiên, nếu bạn muốn thay đổi thuộc tính của 1 Struct bằng 1 phương thức bên trong nó, bạn phải khai báo mutating vào trước phương thức để làm:

    Nếu 1 hàm thay đổi thuộc tính mà không báo mutating thì trình biên dịch sẽ báo lỗi

    Class hỗ trợ hàm deinit:

    Class cung cấp hàm deinit. Hàm này được gọi trước khi 1 class được giải phóng khỏi memory.

    Ví dụ về sự khác biệt struct và class thường dùng

    Ví dụ như trong ví dụ sau đây đối với hàm repeating rất hay được sử dụng:

    class Dog {
        var name = ""
    }
    
    let dogs = [Dog](repeating: Dog(), count: 3)
    dogs[0].name = "Green"
    dogs[1].name = "Red"
    dogs[2].name = "Blue"
    
    print("\(dogs[0].name) \(dogs[1].name) \(dogs[2].name)") // Blue Blue Blue

    Bởi Dog là 1 class, nên 3 đối tượng Dog trong mảng dogs sẽ cùng trỏ tới 1 đối tượng giống nhau -> Vì vậy nên thay đổi giá trị của dogs[3] cũng sẽ thay đổi giá trị của các dog còn lại trong array.

    Thử sửa Dog thành kiểu struct và print ra kết quả.

    Khi nào nên sử dụng struct / class

    Recommend sử dụng struct bởi:

    • Struct nhanh hơn class bởi struct sử dụng method dispatch là static dispatch, class sử dụng dynamic dispatch. Ngoài ra, struct lưu dữ liệu trong stack, còn class sử dụng stack + heap -> Xử lí trong class sẽ lâu hơn.
    • Class là 1 reference type. Do đó, nếu không cẩn thận khi truyền biến sẽ dễ gây ra lỗi ngoài ý muốn ( Xem phần value type vs reference type ở trên). -> Sử dụng struct sẽ an toàn hơn.

    Nên sử dụng class khi:

    • Cần sử dụng kế thừa.
    • Cần sử dụng reference type

    Kết luận:

    Việc phân biệt được sự khác nhau giữa class và struct vô cùng quan trọng để có thể sử dụng cho đúng cách. Hi vọng qua bài viết, các bạn sẽ hiểu rõ hơn được về sự khác biệt giữa class và struct.

  • Operation (P3)

    Operation (P3)

    Đến với phần cuối của loạt bài viết về Operation nhưng cũng không kém phần quan trọng, mình sẽ nói về Async Operation và Cancel Operation.

    Nội dung bài viết:

    • Cancel Operation
    • Async Operation
    • Demo

    Cancel Operation

    • Cách dùng khá đơn giản, chỉ cần gọi cancel() để cancel 1 operation. Tuy nhiên, 1 operation chỉ có thể bị cancel trước khi nó được bắt đầu được thực hiện.
    • Bản chất của việc gọi cancel là sẽ set state của operation thành isCancelled = true.
    operation.cancel()

    Cancel toàn bộ operation:

    Để cancel toàn bộ các operations trong 1 operation queue, chỉ cần gọi:

    operationQueue.cancelAllOperations()

    Note: Chỉ cancel những task chưa được thực hiện

    Vậy làm thế nào có thể cancel 1 operation đang thực hiện? Câu trả lời là sẽ kiểm tra state isCancelled trong thân hàm.
    Hãy xem ví dụ ở cuối bài để hiểu thêm.

    Async operation

    1 Operation nếu được khởi tạo default thì sẽ hoạt động theo kiểu synchronous. 1 vòng đời của operation khi đó theo các state là: isReady -> isExecuting -> isFinished.

    Nhưng nếu bạn muốn các operation của bạn hoạt động theo kiểu asynchoronous bởi diều đó chắc chắn sẽ khiến app của bạn họat động nhanh hơn?
    Điều đó là hoàn toàn có thể, tuy nhiên nếu operation hoạt động kiểu async, nó sẽ trả về quyền điều khiển ngay lập tức (xem lại bài GCD part 1). Vì vậy, state của operation đó sẽ trở thành isFinished ngay lập tức. Do đó, bạn sẽ phải làm thêm 1 vài việc để custom lại state của operation nếu bạn muốn operation đó chạy theo kiểu async.

    Cách làm ở đây về cơ bản là sẽ viết 1 async operation subclass để quản lí state, và giao tiếp với lớp cha Operation của nó thông qua KVO.
    Nghe có vẻ khá dài, nhưng đừng lo lắng, chỉ cần làm 1 lần thôi, lần sau bạn sẽ chỉ cần gọi và dùng.

    class AsyncOperation: Operation {
        // State enumaration
        enum State: String {
            case ready, excuting, finished
    
            // 1
            fileprivate var keyPath: String {
                return "is" + rawValue.capitalized
            }
        }
        // State property
        // 2
        var state: State = .ready {
            // 3
            willSet {
                debugPrint("New value \(newValue)")
                willChangeValue(forKey: newValue.keyPath)
                debugPrint("Will change value 1 for key \(newValue.keyPath)")
                willChangeValue(forKey: state.keyPath)
                debugPrint("Will change value 2 for key \(state.keyPath)")
            }
            didSet {
                debugPrint("old value \(oldValue)")
                didChangeValue(forKey: oldValue.keyPath)
                debugPrint("Did change value 1 for key \(oldValue.keyPath)")
                didChangeValue(forKey: state.keyPath)
                debugPrint("Did change value 2 for key \(state.keyPath)")
            }
        }
    }
    1. State của operation default sẽ là ready
    2. keyPath là 1 computed property sẽ giúp bạn lấy state hiện tại của operation.
    3. Bởi vì bạn cần gửi các notification khi bạn thay đổi state, bạn sẽ dùng didSet và willSet để hứng notification. -> Trong nhiều trường hợp có thể bạn sẽ không cần dùng đến, nhưng nên khai báo để dễ dàng quan sát, debug.

    Base Properties

    extension AsyncOperation {
        // 1
        open override var isReady: Bool {
            return super.isReady && state == .ready
        }
    
        open override var isExecuting: Bool {
            return state == .excuting
        }
    
    
        open override var isFinished: Bool {
            return state == .finished
        }
        // 2
        open override var isAsynchronous: Bool {
            return true
        }
        // 3
        open override func start() {
            // 4
            if isCancelled {
                state = .finished
                return
            }
    
            main()
        }
    
        open override func cancel() {
            super.cancel()
            state = .finished
        }
    }
    1. override lại các state vì bạn muốn tự quản lí state cho riêng mình.
    2. set thuộc tính isAsynchronous thành true để operation chạy async.
    3. override lại func start(). Khi đưa 1 operation vào queue, queue sẽ gọi start để thực hiện chạy 1 operation.
    4. Kiểm tra trước khi thực hiện xem operation này đã bị cancel chưa, nếu chưa thì sẽ bắt đầu thực hiện.

    Note: Không bao giờ gọi super.start() trong hàm start().
    Refer: https://developer.apple.com/documentation/foundation/operation/1416837-start

    Custom 1 Operation

    Mình sẽ custom 1 opeartion dùng để download ảnh, và check state isCancelled trong thân hàm để có thể dừng 1 operation đang chạy.

    1. Override lại hàm main cho operation. Đây là hàm operation sẽ chạy vào khi thực được gọi để thực hiện.
    2. set state thành .executing khi bắt đầu thực hiện operation.
    3. Luôn nhớ set state thành .finished khi operation được thực hiện xong
    1. check nếu operation đã bị cancel thì sẽ không download image.
    2. check nếu opeartion đã bị cancel thì sẽ không convert data thành image.
    3. Ở đây bạn có thể check nếu operation đã bị cancel thì không hiển thị image cũng được, tuy nhiên mình nghĩ download xong image rồi thì hiển thị cũng ok.

    Kết luận:

    • Operation thích hợp để dùng hơn GCD khi những task của bạn đòi hỏi sự kiểm soát state, hoặc những task có tính reuseable cao. Còn với những task đơn giản, hoặc không cần reuse nhiều thì có thể dùng GCD.
    • Operation là API bậc cao hơn GCD, nên cách dùng sẽ khó hiểu hơn, nên hãy cẩn trọng khi kiểm soát state của các custom Operation.

    Tham khảo: Ray wenderlich
    End

  • iOS/Swift: Animate your Launch screen

    iOS/Swift: Animate your Launch screen

    Lời mở đầu

    Chào mọi người, thông thường mành hình Launch screen là nơi để hiển thị logo của ứng dụng khi mà nó được khởi động, và mọi người thường để nó là một ảnh tĩnh để đảm bảo ứng dụng khởi động nhanh nhất có thể. Nhưng gần đây, mình có nhận được một yêu cầu của khách hàng về việc làm cho màn hình này trở nên sinh động hơn. Mình thấy nó cũng khá hay nên mình muốn chia sẻ với mọi người.

    Chia sẻ cách tạo Launch screen một cách sinh động

    Cách làm của chúng ta sẽ là tạo một LaunchScreen mới thay thế cho cái cũ. Trên màn hình LaunchScreen mới chúng ta sẽ thêm vào một số đối tượng và chúng ta sẽ làm cho nó chuyển động để tạo hiệu ứng. Ở phần hướng dẫn này mình sẽ dùng 2 UIImageView và 1 UILabel.
    Cụ thể mục đích của chúng ta sẽ làm chuyển động như hình này:

    Mình sẽ phải tách logo GST ra làm 2 phần và cho nó chuyển động từ 2 bên vào chính giữa và ghép thành một logo hoàn chỉnh.

    Đây là assets mình dùng trong project này:

    Bắt đầu

    Bước 1: Mở XCode và tạo mới Project (Single View App)

    Bước 2: Chúng ta cần file LaunchScreen.storyboard và tạo mới file LaunchScreen bằng cách:
    New File -> View -> đặt tên là LaunchScreen.xib.

    Bước 3: Mở tab General của Target Chọn lại Launch Screen File là LaunchScreen như hình dưới:

    Bước 4: Chỉnh sửa lại file LaunchSceen.xib để chuẩn bị cho việc sử dụng animtion

    Mở file LaunchScreen.xib chúng ta kéo vào 2 UIImageView và set image và tag cho từng lần lượt cho các view theo thứ tự từ trên xuống là 1, 2, 3 sao cho nó như hình dưới:

    Tiếp đến để đảm bảo nó chạy đúng với các kích thước màn hình khác nhau chúng ta cần set constraint cho các item trên màn hình.

    Ta cho imgeview với tag = 1 ẩn khỏi màn hình để sau chúng ta sẽ animate nó ra giữa màn hình vậy ta sẽ set constraint như hình dưới đây.

    Tương tự ta cũng set constraint imageview còn lại như hình dưới:

    Vậy là chúng ta đã chuẩn bị xong UI, giờ chúng ta cần đi vào code để thưc hiện animation.

    Bước 5. Tạo file LaunchScreenManager.swift để thực hiện animation bằng cách:
    Chọn new file -> Swift file -> đặt tên là LaunchScreenManager.swift

    Thêm code vào như dưới đây:

    import Foundation
    import UIKit
    
    class LaunchScreenManager {
        // dòng này dùng để tạo singleton: Nó đảm bảo sẽ chỉ tạo ra 1 instance duy nhất.
        static let instance = LaunchScreenManager()
    
        var view: UIView?// Đây sẽ là LaunchScreen của chúng ta
        var parentView: UIView?
    
        init() {}
    
        func loadView() -> UIView {
            let launchScreen = UINib(nibName: "LaunchScreen", bundle: nil).instantiate(withOwner: nil, options: nil)[0] as! UIView
            return launchScreen
        }
    
        // Phương thức này thực hiện việc thêm view(launch screen) vào màn hình
        // Và set lại frame và vị trí cho LaunchScreen view
        func fillParentViewWithView() {
            guard let view = view, let parentView = parentView else { return }
            parentView.addSubview(view)
            view.frame = parentView.bounds
            view.center = parentView.center
        }
        
        // MARK: - Animation
    
        // Phương thức này để setup launch screen view và gọi hàm thực hiện animation
        func animateAfterLaunch(_ parentViewPassedIn: UIView) {
            parentView = parentViewPassedIn
            view = loadView()
            fillParentViewWithView()
            // start animation
            startAnimation()
        }
    
        func startAnimation() {
    
            // Lúc này chúng ta cần lấy các view đã add vào file LaunchScreen.xib để thực hiện animation
            // Để tránh sai sót trong việc đánh tag cho các view -> crash app
            //Chúng ta cần kiểm tra các item này có bị nil hay không
            guard let logo1 = view?.viewWithTag(1),
                let logo2 = view?.viewWithTag(2),
                let label = view?.viewWithTag(3) else { return }
    
            let screen = UIScreen.main.bounds.size
    
            UIView.animateKeyframes(withDuration: 2, delay: 0, options: [], animations: {
                //0
                UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.1) {
                    logo1.transform = CGAffineTransform.identity.translatedBy(x: screen.width / 2 + logo1.frame.size.width / 2, y: 0)
                    logo2.transform = CGAffineTransform.identity.translatedBy(x: -(screen.width / 2 + logo2.frame.size.width / 2), y: 0)
                }
                UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.1) {
                    label.center.x = screen.width / 2
                }
                UIView.addKeyframe(withRelativeStartTime: 0.75, relativeDuration: 0.25) {
                    self.view?.alpha = 0
                }
            }) { (_) in
                self.view?.removeFromSuperview()
            }
        }
    }

    Bước cuối cùng:
    Mở file SceneDelegate.swift thêm đoạn code thực thi animation để khi app được khởi động sẽ chạy animation 😀

    Done, giờ chúng ta sẽ build app lên và tận hưởng 😀

    Kết quả

    Đối với việc tạo animation nếu mọi người quan tâm có thể tham khảo các bài viết của
    Vũ Đức Cương nhé!
    Part1
    Part2
    Part3

    Tổng kết

    Mình hi vọng bài viết của mình có thể giúp ích cho mọi người, để góp phần tạo ra các ứng dụng ngon hơn 😀

    Dưới đây là code project demo:

  • Một số animation cho UITableView

    Một số animation cho UITableView

    TableView là được sử dụng rất nhiều trong các ứng dụng của chúng ta. Vì vậy, việc tạo thêm một số hiệu ứng cho TableView khiến cho ứng dụng trở lên sinh động và bớt nhàm chán hơn. Chỉ với một vài câu lệnh, mọi thứ sẽ trở lên mới mẻ và dễ gần hơn rất nhiều lần.

    Hầu như các animation của tableview sẽ được tạo trong method dưới đây:

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        // Add animations here
    }

    Chúng ta bắt đầu với hiệu ứng đơn giản nhất nhưng được sử dụng nhiều nhất:

    Kết quả đạt được:

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
            cell.alpha = 0
            UIView.animate(withDuration: 0.5,
                           delay: 0.1 * Double(indexPath.row),
                animations: {
                    cell.alpha = 1
            })
        }

    Hiệu ứng Bounce animation:

    Bounce animation
    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
          cell.transform = CGAffineTransform(translationX: 0, y: 60)
            UIView.animate(
                withDuration: 1,
                delay: 0.05 * Double(indexPath.row),
                usingSpringWithDamping: 0.4,
                initialSpringVelocity: 0.1,
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
            })
    }

    Move and Fade Animation

    Kết hợp hai hiệu ứng trên và chung ta ngừng sử dụng hiệu ứng Spring cho Cell, một hiệu ứng mới được tạo thành.

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cell.transform = CGAffineTransform(translationX: 0, y: 30)
            cell.alpha = 0
    
            UIView.animate(
                withDuration: 1,
                delay: 0.05 * Double(indexPath.row),
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
                    cell.alpha = 1
            })
    }

    Slide in Animation

    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
            cell.transform = CGAffineTransform(translationX: tableView.bounds.width, y: 0)
    
            UIView.animate(
                withDuration: 1,
                delay: 0.5 * Double(indexPath.row),
                options: [.curveEaseInOut],
                animations: {
                    cell.transform = CGAffineTransform(translationX: 0, y: 0)
            })
        }

    Trên đây là một số ví dụ về việc thêm animation vào tableview bằng các hiệu ứng cơ bản của UIView( transform, alpha …) ngoài ra với 1 số hiệu ứng khác như Flip, Change color … cũng được sử dụng rất nhiều trong các hiệu ứng animation. Chúng ta hoàn toàn có thể tạo ra các animation mang mang màu sắc của cá nhân như các họa sĩ code vậy.
    Một lưu ý đó là khi muốn kiểm soát tốt hơn các animation thì chúng ta nên tạo ra một class riêng  để xử lý việc chạy animation. Nó đồng thời cũng kiểm soát việc animation chỉ chạy một lần duy nhất với tất cả các cell.