Category: iOS

  • iOS/Auto Layout – Phần 4: Làm sao để viết constraint dễ dàng hơn? Giới thiệu Snapkit

    iOS/Auto Layout – Phần 4: Làm sao để viết constraint dễ dàng hơn? Giới thiệu Snapkit

    Lời mở đầu

    Ở phần 3 chúng ta đã được làm quen với việc tạo constraints bằng code. Vậy nên bài viết này mình sẽ chia sẻ với các bạn về việc làm sao để tạo constraints bằng code dễ dàng hơn, tiết kiệm thời gian hơn. Và làm thế nào để debug UI.



    Sử dụng extension

    Việc sử dụng extension để mở rộng các class giúp chúng ta có thể tạo constraint dễ dàng và tiết kiệm nhiều thời gian. Việc của các bạn là nghĩ và tạo ra các func có thể hay được sử dụng.

    1. UIView

    Ví dụ chung ta có thể tạo ra một func trong extension UIView giúp chúng ta tạo các constraints liên kết với SuperView như sau:

    extension UIView {
    
        /// Returns a collection of constraints to anchor the bounds of the current view to the given view.
        ///
        /// - Parameter view: The view to anchor to.
        /// - Returns: The layout constraints needed for this constraint.
        func constraintsForAnchoringTo(boundsOf view: UIView) -> [NSLayoutConstraint] {
            return [
                topAnchor.constraint(equalTo: view.topAnchor),
                leadingAnchor.constraint(equalTo: view.leadingAnchor),
                view.bottomAnchor.constraint(equalTo: bottomAnchor),
                view.trailingAnchor.constraint(equalTo: trailingAnchor)
            ]
        }
    }

    Từ đây các bạn có thể tha hồ nghĩ thêm các func để giúp cho việc tạo constraints của mình trở nên đơn giản và tiết kiệm thời gian hơn nhiều so với việc hì hục ngồi code.

    1.2 Xử lí ưu tiên

    Khi bạn phải đặt độ ưu tiên cho các constraints của mình để tránh phá vỡ các constraint khác. Bạn có thể sẽ cần đến extension này:

    extension NSLayoutConstraint {
        
        /// Returns the constraint sender with the passed priority.
        ///
        /// - Parameter priority: The priority to be set.
        /// - Returns: The sended constraint adjusted with the new priority.
        func usingPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
            self.priority = priority
            return self
        }
        
    }
    
    extension UILayoutPriority {
        
        /// Creates a priority which is almost required, but not 100%.
        static var almostRequired: UILayoutPriority {
            return UILayoutPriority(rawValue: 999)
        }
        
        /// Creates a priority which is not required at all.
        static var notRequired: UILayoutPriority {
            return UILayoutPriority(rawValue: 0)
        }
    }

    Độ ưu tiên của constraint là phần khá là hay ho, nên mình sẽ dành một mục ở bài viết tiếp theo.

    1.3 Tạo Auto layout property wrapper

    Khi auto layout bằng code, để tránh việc phải viết đi viết lại nhiều lần dòng code dưới đây:
    translatesAutoresizingMaskIntoConstraints = false

    Chúng ta sẽ tạo ra Property Wrapper như dưới đây:

    @propertyWrapper
    public struct UsesAutoLayout<T: UIView> {
        public var wrappedValue: T {
            didSet {
                wrappedValue.translatesAutoresizingMaskIntoConstraints = false
            }
        }
    
        public init(wrappedValue: T) {
            self.wrappedValue = wrappedValue
            wrappedValue.translatesAutoresizingMaskIntoConstraints = false
        }
    }
    
    final class MyViewController {
        @UsesAutoLayout
        var label = UILabel()
    }
    

    Lưu ý: Nó không sử dụng được cho biến Local

    Giới thiệu về Snapkit

    Nếu như các bạn lười nghĩ và viết các extension để hỗ trợ cho việc tạo constraint bằng code. Thì chúng ta lại có một giải pháp khác là dùng của thằng khác :D. Của ăn sẵn nhiều khi cũng tốt mà. Tiện đây mình xin giới thiệu với các bạn về thư viện Snapkit một thư việt rất mạnh về Auto layout bằng code. Giúp chúng ta xử lí Autolayout một cách đơn giản, gọn gàng và tiết kiệm khá nhiều thời gian.

    Ưu điểm
    – Code nhìn gọn gàng, dễ hiểu hơn
    – Tiết kiệm thời gian khi code

    Nhược điểm
    – Sẽ có những thành viên không sử dụng snapkit mà dùng API default của apple dẫn đến thời gian đầu làm việc gặp khó khăn.

    Cài đặt

    CocoaPods, Carthage hoặc ném nó vào project của bạn.
    Tham khảo link này nhé.

    Cách sử dụng

    1.Tạo constraint

    func makeConstraintBySnapkit1() {
            let iView = UIView()
            let oView = UIView()
            iView.backgroundColor = .red
            oView.backgroundColor = .blue
    
            view.addSubview(oView)
            oView.addSubview(iView)
    
            oView.snp.makeConstraints { (make) in
                make.width.height.equalTo(200)
                make.center.equalToSuperview()
            }
            iView.snp.makeConstraints { (make) in
                make.top.leading.equalToSuperview().offset(20)
                make.bottom.trailing.equalToSuperview().offset(-20)
            }
        }

    Để tạo được constraint có kết quả như hình này bằng code sử dụng API mặc định của apple chúng ta sẽ phải viết khá nhiều dòng code.

    2. Giữ reference của constraint và thay đổi nó giá trị của nó khi cần.

    var myConstraint: Constraint?
    iView.snp.makeConstraints { (make) in
                self.myConstraint = make.top.leading.equalToSuperview().offset(20).constraint
                make.bottom.trailing.equalToSuperview().offset(-20)
            }
    myConstraint?.update(offset: 40)

    3. Update constraint

    Update constraint giúp chúng ta update tùy ý bất kể 1 constraint nào đó của View.
    Để update constraint ta chỉ cần viết như sau:

    iView.snp.updateConstraints { (edit) in
        edit.top.leading.equalToSuperview().offset(0)
    }

    Kết quả thu được sẽ là:

    4. Remake constraint

    Khác với updateContraint, remake constraint sẽ remove toàn bộ constraints cũ đi và bạn sẽ tạo lại constraint từ đầu như hàm makeConstraint.
    Câu lênh của nó sẽ như sau:

            iView.snp.remakeConstraints { (remake) in
                remake.top.leading.equalToSuperview().offset(20)
                remake.bottom.trailing.equalToSuperview()
            }

    Kết quả thu được

    Để có thể thành thạo sử dụng Auto Layout và Snapkit chúng ta chỉ có 1 cách đó là làm nhiều, nhiều nữa và nhiều mãi thế thôi =)) Cố gắng lên anh em :))

    Tổng kết

    Mình hi vọng bài viết này sẽ giúp các bạn phần nào sử dụng Auto Layout dễ dàng và tiết kiệm thời gian hơn.



  • 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.
  • iOS/Auto Layout – Phần 3: Anatomy of a Constraint, cách để tạo constraints bằng code

    iOS/Auto Layout – Phần 3: Anatomy of a Constraint, cách để tạo constraints bằng code

    Lời mở đầu

    Bài viết này của mình có 2 nội dung chính. Đầu tiên mình sẽ nói về cấu tạo của một constraint để các bạn có thể hình dung được nó hoạt động như thế nào. Thứ hai mình sẽ nói về việc làm thế nào để tạo một constraint sử dụng code mà không cần dùng đến Interface buider.



    Anatomy of a Constraint

    Layout của view hierarchy được định nghĩa là một chuỗi các phương trình tuyến tính. Mỗi ràng buộc đại diện cho một phương trình duy nhất. Mục tiêu của bạn là khai báo một loạt các phương trình có một và chỉ một giải pháp khả thi.

    Giờ chúng ta sẽ xem cấu tạo của một constraint nó như nào nhé:

    Constraint này nói rằng cạnh trái của view màu đỏ(Red view’ leading edge) phải là 8 points sau cạnh phải của view màu xanh(Blue view’s trailing edge). Phương trình của nó có các phần như sau:

    • Item 1: Đây là phần đầu tiên trong phương trình, trong trường hợp này nó là View màu đỏ. Phần này phải là một View hoặc là một layout guide.
    • Attribute 1: Là thuộc tính được constraint trên Item 1. Trường hợp này nó là cạnh trái của Red View(Red view’s leading edge).
    • Relationship: Mối quan hệ giữa bên trái và bên phải của phương trình. Giá trị của relationship có thể thuộc 1 trong 3 giá trị: bằng, lớn hơn hoặc nhỏ hơn. Trong trường hợp này bên trái và bên phải bẳng nhau.
    • Multiplier: Hệ số giá trị của Attribute được nhân với số này. Trong trường hợp này hệ số nhân là 1.0. Trong các trường hợp mặc định hệ số này là 1.0, nên ta có thể bỏ đi nếu không muốn thay đổi giá trị này.
    • Item 2: Là phần thứ 2 trong phương trình. Trong trường hợp này nó là Blue View. Không giống với Item 1, nó có thể để trống.
    • Attribute 2: Là thuộc tính được constraint trên Item 2. Trong trường hợp này là cạnh phải của BlueView. Nếu Item 2 để trống thì Attribute 2 sẽ không tồn tại
    • Constant: Một hằng số bù trừ, trong trường hợp này nó là 8.0. Giá trị này được thêm vào giá trị của thuộc tính 2(Value of Attribute 2).

    Hầu hết các constraint xác định một mối quan hệ giữa hai items trong giao diện người dùng. Những constraint này có thể đại diện cho các View hoặc Layout guide. Các Constraint cũng có thể xác đinh mối quan hệ giữa hai thuộc tính khác nhau của một item.
    Ví dụ: bạn có thể đặt Aspect ratio giữa chiều cao của 1 item với chiều rộng của nó. Bạn cũng có thể gán giá trị hằng số cho chiều cao hoặc chiều rộng của Item đó. Khi làm việc với giá trị Constant, Item 2 được để trống thì attribute 2 sẽ được gán là không phải thuộc tính và multiplier được gán là 0.0

    Auto Layout Attributes

    Trong Auto Layout, các thuộc tính được xác định là một tính năng có thể được ràng buộc(Constraint). Thông thường, nó bao gồm 4 cạnh: Trên(Top), Dưới(bottom), Trái(leading), Phải(trailing), cũng như chiều cai, chiều rộng và căn giữa theo chiều dọc và ngang. Các text cũng có một hoặc nhiều thuộc tính baseline.

    Các bạn có thể xem thêm NSLayoutAttribute enum.

    Cách tạo constraint bằng code

    Auto layout constraints cho phép chúng ta tạo ra các view tự động điều chỉnh theo các Size Class và Vị trí khác nhau. Các constraint sẽ đảm bảo view của bạn điều chỉnh phù hợp với bất kỳ thay đổi kích thước nào mà không phải cập nhật thủ công các khung hoặc vị trí của nó.

    Tuy nhiên, ngoài việc sử dụng Interface Builder để Auto Layout thì chúng ta có thể sử dụng code để Auto Layout.

    Việc viết AutoLayout có một số ưu điểm và nhược điểm như sau:
    Ưu điểm:
    – Dễ dàng merge code khi dùng Git
    – Dễ debug
    – Các constraint có thể dễ nhìn hơn
    Nhược điểm:
    – Không có đại diện trực quan. Việc này khiến bạn phải tưởng tượng thay vì như IB cho ta nhìn thấy Views của mình trên màn hình ở nhiều kích thước khác nhau.
    – Bạn có thể sẽ phải viết nhiều code layout lên trên ViewController của mình.

    Theo mình để có thể làm tốt Auto Layout bằng code thì mình nghĩ các bạn nên sử dụng thành thạo Interface Builder trước.
    Việc kết hợp giữa Interface Builder và constraint trong code cũng có thể là một giải pháp tốt. Tuy nhiên, hãy luôn nhớ rằng việc đó sẽ khiến nó trở nên khó hiểu hơn.

    Tạo constraints bằng cách sử dụng Layout Anchors

    Việc đầu tiên chúng ta cần làm là phải set thuộc tính translatesAutoresizingMaskIntoConstraints thành false. Việc này nhằm mục đích ngăn các view’s auto-resizing mask được chuyển sang Auto Layout constraints và ảnh hưởng đến các constraint của bạn.

    Tiếp theo chúng ta sẽ bắt đầu tạo một mảng chứa các constraints. Trong mảng này chúng ta sẽ định nghĩa các constraints mà bạn muốn gắn nó cho một View nào đó.

    Trong trường hợp này mình muốn tạo ra 1 view màu đỏ có width = 200, height = 200 và căn giữa màn hình. Vì vậy mình sẽ tạo ra một mảng constraint như dưới đây.

    override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            let myView = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
            myView.backgroundColor = .red
            view.addSubview(myView)
            myView.translatesAutoresizingMaskIntoConstraints = false
            let constraints = [
                myView.centerYAnchor.constraint(equalTo: view!.centerYAnchor),
                myView.centerXAnchor.constraint(equalTo: view!.centerXAnchor),
                myView.widthAnchor.constraint(equalToConstant: 200.0),
                myView.heightAnchor.constraint(equalToConstant: 200.0)
            ]
    
            NSLayoutConstraint.activate(constraints)
        }

    Đó là những dòng code cơ bản để có thể tạo constraint cho 1 view bằng code. Và nó khá dễ đọc và dễ hiểu.
    Dòng cuối cùng của đoạn code trên nhằm mục đích active một chuỗi các constraint mà bạn tạo ra.

    Kết quả chúng ta thu được sẽ như hình dưới đây:

    UIView cung cấp cho chúng ta 1 tập hợp các thuộc tính neo cho phép bạn thiết lập các mối quan hệ giữa các View với nhau.
    Dưới đây là danh sách các thuộc tính neo:

    extension UIView {
    
        /* Constraint creation conveniences. See NSLayoutAnchor.h for details.
         */
        @available(iOS 9.0, *)
        open var leadingAnchor: NSLayoutXAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var trailingAnchor: NSLayoutXAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var leftAnchor: NSLayoutXAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var rightAnchor: NSLayoutXAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var topAnchor: NSLayoutYAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var bottomAnchor: NSLayoutYAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var widthAnchor: NSLayoutDimension { get }
    
        @available(iOS 9.0, *)
        open var heightAnchor: NSLayoutDimension { get }
    
        @available(iOS 9.0, *)
        open var centerXAnchor: NSLayoutXAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var centerYAnchor: NSLayoutYAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var firstBaselineAnchor: NSLayoutYAxisAnchor { get }
    
        @available(iOS 9.0, *)
        open var lastBaselineAnchor: NSLayoutYAxisAnchor { get }
    }

    Với mỗi Anchor nó trả về các subclass từ NSLayoutAnchor đi kèm với một số phương thức phổ biến để thiết lập mối quan hệ. Nó bao gồm =, >, <, >=, <=. Cũng giống như khi chúng ta dùng Interface Builder. Đây là tài liệu để làm quen về nó Documents.

    NOTE: Như các bạn đã thấy, nó chỉ hỗ trợ từ iOS 9, Hầu hết các ứng dụng bây giờ đều sẽ hỗ trợ từ iOS 9 trở lên, vì vậy chúng ta không cần phải lo lắng quá nhiều về nó.

    Order of constraints

    Khi các bạn bắt đầu viết các constraint của mình, điều quan trọng là bạn phải nhớ thứ tự của các constraint của bạn khi bạn làm việc với các constants.

    let innerView = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
    let outerView = UIView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
    innerView.backgroundColor = .red
    outerView.backgroundColor = .blue
    
    view.addSubview(outerView)
    outerView.addSubview(innerView)
    innerView.translatesAutoresizingMaskIntoConstraints = false
    outerView.translatesAutoresizingMaskIntoConstraints = false
    let constraintsOuter = [
        outerView.centerYAnchor.constraint(equalTo: view!.centerYAnchor),
        outerView.centerXAnchor.constraint(equalTo: view!.centerXAnchor),
        outerView.widthAnchor.constraint(equalToConstant: 200.0),
        outerView.heightAnchor.constraint(equalToConstant: 200.0)
    ]
    
    NSLayoutConstraint.activate(constraintsOuter)
    
    let constraints = [
        innerView.topAnchor.constraint(equalTo: outerView.topAnchor),
        innerView.leftAnchor.constraint(equalTo: outerView.leftAnchor, constant: 40),
        innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor),
        innerView.rightAnchor.constraint(equalTo: outerView.rightAnchor, constant: -40)
    ]
    
    NSLayoutConstraint.activate(constraints)

    Kết quả như hình dưới đây:

    Mục đích của đoạn code này là tạo ra mảng constraint cho innerView sao cho top của nó bằng top của của outerView, bot = bot của outerView, và cách đều 2 bên = 40 points.
    let constraints = [
    innerView.topAnchor.constraint(equalTo: outerView.topAnchor),
    innerView.leftAnchor.constraint(equalTo: outerView.leftAnchor, constant: 40),
    innerView.bottomAnchor.constraint(equalTo: outerView.bottomAnchor),
    innerView.rightAnchor.constraint(equalTo: outerView.rightAnchor, constant: -40)
    ]
    Ở dòng được highlight kia giá trị của constant phải = -40. Vì lúc này vị trí của outerView đã được cố định, và outerView.rightAnchor lúc này được coi là gốc(Ox) của trục tọa độ. Vậy nên vị trí bên trái của gốc tọa độ Ox ta hiểu là giá trị âm và ngược lại.
    Ta có thể đổi ngược lại vị trí của innerView và outerView để được kết quả như trên hình theo đoạn có dưới đây:( Không nên viết theo kiểu này mà nên viết theo kiểu trên để dễ nhìn hơn)
    outerView.rightAnchor.constraint(equalTo: innerView.rightAnchor, constant: 40)

    Tương tự đối với top và bottom là trục Oy hướng xuống dưới, phía trên của gốc Oy là giá trị âm và ngược lại.

    NOTE: Khi các bạn thêm các constraint bằng code, các bạn nên làm theo thứ tự để code của ta dễ kiểm soát và dễ hiểu hơn.

    Một số Layout guides hay sử dụng

    UIVew cũng có một vài Layout guides có thể được sử dụng làm neo như sau:

    • layoutMarginGuide: Đặt các constraint và giữ lề của layout bằng 1 khoảng trống cố định (16 points).
    • readableContentGuide: Constraints chiều rộng của view giúp người dùng dễ đọc.
    • safeAreaLayoutGuide: Đặt các constraint giúp view của bạn không bị che bởi các thanh và nội dung khác.

    Đây là đoạn code mẫu sử dụng Layout guide(safeAreaLayoutGuide)

    let constraints = [
        outerSquare.topAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.topAnchor),
        outerSquare.leftAnchor.constraint(equalTo: viewController.view.safeAreaLayoutGuide.leftAnchor),
        outerSquare.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
        outerSquare.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor)
    ]

    Hỗ trợ ngôn ngữ từ Phải qua trái

    Có vẻ rất rõ ràng khi bạn sử dụng leftAnchor và rightAnchor, nhưng bạn sẽ phải nghĩ về việc sử dụng leadingAnchor và trailingAnchor để thay thế. Nhằm mục đích hỗ trợ các ngôn ngữ từ phải qua trái. Điều này rất quan trọng khi sử dụng views như là Labels trong trường hợp bạn muốn chúng được lật lại cho các ngôn ngữ từ phải sang trái.



    Phần kết

    Mình hi vọng bài viết này giúp các bạn có thể tạo các constraints bằng code sẽ dễ dàng hơn.
    Nó sẽ là một giải pháp thay thế tuyệt vời để thiết lâp các constraints khi bạn không muốn dùng Interface Builder.

  • iOS/Auto Layout – Phần 2: Sử dụng “XCode/Interface Builder” sao cho hiệu quả?

    iOS/Auto Layout – Phần 2: Sử dụng “XCode/Interface Builder” sao cho hiệu quả?

    Lời mở đầu

    Như các bạn đã biết, để giỏi một kĩ năng nào đó trước hết chúng ta phải biết nó là gì và cách sử dụng của nó như thế nào. Và luyện tập nó nhiều sẽ giúp các bạn nâng cao kỹ năng đó.
    Bài viết này mình sẽ giới thiệu tới các bạn cách tương tác với Interface Builder và sử dụng chúng trong Auto layout sao cho hiệu quả. Hi vọng nó sẽ giúp ích cho mọi người :v



    1. Interface Builder

    Interface builder là một phần trong ứng dụng XCode, nó giúp các nhà phát triển phần mềm iOS xây dựng giao diện trên đó.

    2. Tương tác với Interface Builder

    Dưới đây là hình ảnh về giao diện của nó

    1.2 View as …

    Ở thanh điều khiển này bạn có thể xem giao diện khác nhau như

    • Các thiết bị khác nhau từ kích thước lớn nhất tới nhỏ nhât, từ iphone đến iPad
    • Các Interface Style khác nhau như Dark mode, light mode
    • Các orientation(Chiều xem màn hình) khác nhau
    Cái này để kiểm tra trước xem việc auto layout của các bạn đã theo ý muốn hay chưa.

    1.3 Auto layout bar

    • Update Frames: Nút này sẽ được hiển thị khi mà một hoặc nhiều View đang không nằm đúng vị trí so với constraints nó đang có. Khi bấm vào chúng ta sẽ được giúp các view đó trở về đúng vị trí so với constraints của nó đang có.
    • Align: Để tạo các constraint căn chỉnh giữa các view, chẳng hạn như căn giữa, trái, phải, trên dưới …
    • Add new constraints: Để tạo các các constraint thông thường như trên, dưới, trái, phải, chiều rộng, chiều cao và tỉ lệ (aspect ratio)
    • Resolve AutoAuto Layout Issues: Để giải quyết các vấn đề về auto layout như:
      – Update constraint constants: Để cập nhật giá trị mới cho constraint đó theo đúng vị trí mà nó đang nằm trên SupperView.
      – Add missing Constraints: tự động thêm các constraint còn thiếu (Không nên dùng vì rất dễ dẫn đến việc thêm các constraint không mong muốn.
      – Clear constraint: Bỏ hết constraint
    • Embed In: Tạo 1 view mới và đưa hết các view được chọn vào trong đó. Mà có thể sẽ không mất đi các constraint hiện tại của các view được chọn.

    1.3.1 Align

    Add new alignment constraints

    Ở đây chúng ta thấy giao diện thêm các constraints về căn chỉnh được hiển thị

    Vertical in Container và Horizontally in container

    Vertically in container: giúp tạo constraint của các View được chọn gắn với super view. Trong đó các connstraint được tạo sẽ giúp các view đó căn giữa theo chiều dọc với super view của nó. Ở đây chúng ta có thể thay đổi constant của nó(mặc định là 0)

    Horizontally in container: Tương tự như trên nhưng theo chiều ngang.

    Kết quả khi add 2 contraint này cho button
    First Baselines

    Khác với 2 thằng ở trên, các thằng còn lại cần phải có 1 thằng “A” đã xác đinh được vị trí của nó(đầy đủ contraint). Thằng “A” này làm nhiệm vụ là cái mỏ neo giúp cho các thằng khác bám vào.

    Thằng này dùng để làm cho nhiều View( UILabel, UIButton …) có text nằm trên cùng hàng, không bị lệch lẹo.
    Để enable tính năng này lên chúng ta cần phải chọn ýt nhất 2 Views.

    NOTE: Nó căn theo đáy của text trong các views

    Kết quả
    Vertical center

    Tạo constraints giúp các views căn giữa theo chiều dọc
    Lưu ý: Nó căn theo frame size nên nó sẽ căn theo các frame của các views được thêm constraint

    Để hiểu rõ hơn chúng ta theo dõi hình dưới đây

    Horizontal center

    Tương tự như Vertical center nhưng theo chiều ngang.

    Bottom edges

    Dùng khi cần gắn constraint cho viewA luôn nằm ở đáy của viewB.
    Mặc định sẽ là viewA.bottom to viewB.bottom

    Top edges

    Dùng khi cần gắn constraint cho viewA nằm ở trên đầu của viewB.
    Mặc định sẽ là viewA.top to viewB.top

    Trailing edges

    Dùng khi cần gắn contraint cho viewA cùng căn trái 1 khoảng bằng căn trái của viewB
    Măc định sẽ là viewA.trailing to viewB.trailing

    Leading edges

    Dùng khi cần gắn constraints cho viewA cùng căn phải 1 khoảng bằng với căn phải của viewB.
    Mặc định sẽ là viewA.leading to viewB.leading

    Tất cả các Label được constraint xoay quanh trụ là Button.

    1.3.2 Add new constraints

    Thường dùng để thêm các constraints cho view được chọn như: trên, dưới, trái, phải, chiều rộng, chiều cao, tỉ lệ. Phần này thì mọi người dùng nhiều rồi nên mình không nói nhiều về nó.
    NOTE: Thông thường khi chọn XCode sẽ gợi ý gắn constraint với view gần với các cạnh của nó nhất(không tính view đè lên nhau)
    Tuy nhiên chúng ta có thể thay đổi constraint tới view đích bằng cách chọn mũi tên như hình dưới.

    Ở hình này bạn có thể thay đổi view đích
    • width: tạo constraint độ rộng của view
    • height: tạo constraint độ cao của view
    • equal width: Tạo constraints cho các view có cùng độ rộng (Chọn 2 view trở lên)
    • equal height: Tạo constraints cho các view có cùng độ cao (chọn 2 view trở lên)
    • Aspect ratio: tạo constraint tỉ lệ. Thường sử dụng cho những view sử dụng hình ảnh.

    NOTE: Bạn có thế chọn nhiều view rồi set equal width, height để set cho tất cả các view được chọn có cùng size.

    1.3.3 Embed In

    Embed In View
    Dùng khi bạn muốn gom các view được chọn vào 1 view lớn, để quản lý layout đễ hơn. Tính năng này sẽ tự tạo 1 một view lớn chứa và cách các view bên trong với inset cố định, mặc định là 20 points

    Embed In View Without Inset
    Khác với tính năng Embed In View tính năng này sẽ tạo với Inset = 0, cách dùng như Embed In View

    Embed In ScrollView
    Tự động tạo 1 scrollview và đưa tất cả các view được chọn vào bên trong nó.

    Embed In Stack View
    Tạo ra 1 stack view và đưa các view được chọn vào trong nó. Thằng này thường được dùng khi bạn có nhiều View có cùng kích thước. Mình sẽ viết 1 bài đi sâu vào thằng này sau.

    1.4 Add constraints bằng Chuột và bàn phím.

    Bạn có thể add constraint bằng cách:
    – Sử dụng chuột phải để kéo ViewA constraint với ViewB.
    – Bấm giữ phím control + chuột trái để kéo ViewA constraint với ViewB
    Khi đó 1 popup sẽ hiển thị cho chúng ta thêm Constraint một cách nhanh chóng.

    Lúc này bạn có thể chọn 1 constraint mà bạn muốn hoặc giữ Shift để chọn tạo nhiều constraint.
    Giữ phím Option để nhìn thấy chỉ số và tạo constraint với chỉ số đó.

    1.5 Thanh Menu

    Đối với mỗi người sẽ có sở thích khác nhau khi chọn hiển thị cái gì trên màn hình Interface Builder.
    Nhưng theo mình các bạn nên chọn hiển thị các mục như mình đã chọn :D, đặc biệt có mục
    Bounds Rectangles: Nó tạo ra các border xung quanh các View giúp các bạn dễ hình dung hơn khi thêm constraints.

    Preview mode

    Giúp các bạn có thể nhìn thấy Layout của mình hoạt động như nào mà không cần phải build app. Tính năng này Cực Kỳ hữu dụng khi các bạn muốn test layout. Nó còn tiết kiệm thời gian khi bạn đang làm trong 1 dự án lớn vì thời gian build app nó khá lâu.

    Ở chế độ preview bạn có thế bấm vào Layout để chỉnh lại bố cục của chế độ xem trước
    Hiện tại có 2 lựa chọn: Bên phải và xem ở dưới.

    Assistant

    Tính năng này chia đôi màn hình và tự động hiển thị file swift đang được kết nối với nó.
    Để chuyển qua tính năng này chúng ta mở thanh menu lên và chọn Assistant
    Hoặc dùng tổ hợp phím Option + Control + Command + Enter để mở nhanh.

    1.6 Xem và thay đổi các constraint trên 1 view

    Tab Show the size inspector

    Bạn có thể chọn 1 view và xem được tất cả các constraint liên quan tới nó ở tab Show The Size Inspector bên trên góc trái màn hình.
    Nếu muốn chỉnh sửa nhanh constant, priority và multiplier của constraint đó thì hãy bấm vào nút edit ở mỗi constraint.

    Nếu muốn xem và chỉnh sửa 1 constraint cụ thể thì các bạn có thể dùng chuột chon constraint đó và sửa nó. Ở Mục này ta có thể sửa được nhiều thông tin hơn, ta có thể thay đổi được constraint với 1 vị trí khác. Thậm chí cả 1 view khác luôn. Cụ thể xem dưới hình.



    Tổng kết

    Hi vọng bài viết của mình giúp các bạn sử dụng Xcode/Interface Builder dễ dàng hơn.

    Cảm ơn các bạn đã theo dõi bài viết.

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

  • iOS/Auto Layout – Phần 1: Auto layout là gì?

    iOS/Auto Layout – Phần 1: Auto layout là gì?

    Lời mở đầu

    Để có thể tạo ra các ứng dụng iOS thì việc sử dụng auto layout là không thể tránh khỏi. Đối với các Developer đã có kinh nghiệm thì có vẻ không khó khăn là bao. Nhưng đối với các iOS Developer mới họ thường khá lúng túng với công việc này. Vì vậy loạt bài viết lần này mình sẽ chia sẻ về Auto layout để các bạn có thể cảm thấy tự tin hơn khi sử dụng Auto Layout 😀



    Để sử dụng Auto Layout thì đầu tiên chúng ta phải hiểu nó là cái gì và sử dụng để làm gì. Vì vậy phần 1 mình sẽ nói về định nghĩa của Auto layout cho các bạn mới làm về iOS giúp các bạn hiều và dần làm quen với nó.
    OK, Chúng ta bắt đầu thôi :v

    Auto layout là gì?

    Auto layout là việc tự động tính toán kích thước và vị trí của tất cả các “view” trong view hierarchy của bạn, dựa trên các ràng buộc được đặt trên các “view” đó.

    Ví dụ: Bạn có thể giới hạn một Button sao cho Button nằm ở giữa theo chiều ngang với ImageView và sao cho cạnh trên của Button luôn cách 8 “points” so với cạnh dưới ImageView. Lúc này, nếu kích thước hoặc vị trí của ImageView thay đổi thì vị trí của Button sẽ tự động điều chỉnh cho phù hơp. (Hình 1)

    Cách tiếp cận dựa trên ràng buộc này để thiết kế cho phép bạn xây dựng giao diện người dùng tự động đáp ứng với cả những thay đổi bởi các tác nhân bên trong và bên ngoài(Internal and external changes).

    Hình 1

    NOTE:
    • “view”: Ở đây được hiểu là tập hợp các UI để hiển thị trên màn hình như: UILabel, UIButton, UIView .v.v.
    • “points”: Là đơn vị đo lường sử dụng trong thiết kế UI của iOS.

    Những tác nhân thay đổi view từ bên ngoài (External Changes)

    Nó xảy ra khi kích thước hoặc hình dạng của supperview thay đổi. Với mỗi thay đổi, bạn sẽ phải cập nhật lại layout của view hierarchy để sử dụng không gian có sẵn một cách tốt nhất. Dưới đây là một số tác nhân thay đổi bên ngoài phổ biến:
    • Người dùng thay đổi kích thước cửa sổ (window) trong OS X.
    • Người dùng truy cập hoặc rời khỏi Split View trên iPad.
    • Người dùng sử dụng tính năng xoay màn hình (Rotates) (iOS).
    • Khi các thanh cuộc gọi hoặc ghi âm xuất hiện hoặc biến mất (iOS).
    • Khi bạn muốn hỗ trợ các size class khác nhau.
    • Khi bạn muốn hỗ trợ các kích thước màn hình khác nhau

    Hầy hết những tác nhân thay đổi này có thể xảy ra trong thời gian chạy của ứng dụng và chúng yêu cầu phản hồi linh hoạt từ ứng dụng của bạn. Ngoài ra, như là việc hỗ trợ cho các màn hình với kích thước khác nhau, ứng dụng cũng cần thích nghi với các môi trường khác nhau. Mặc dù kích thước màn hình thường không thay đổi lúc runtime, nhưng việc tạo một giao diện người dùng dễ dàng thích thích nghi(adaptive) khiến cho ứng dụng của các bạn chạy tốt trên cả iPhone 4S, iPhone 6 Plus, iPhone X thậm chí là cả iPad. Auto Layout cũng là phần chủ đạo cho việc hỗ trợ Slide Over và Split view trên iPad.

    Những tác nhân thay đổi view từ bên trong (Internal Changes)

    Nó xảy ra khi kích thước của các view hoặc các controls(Thanh điều khiển) trong giao diện người dùng của bạn thay đổi.

    Dưới đây là một vài nguyên nhân phổ biến:

    • Nội dung bị thay đổi bởi những thay đổi từ ứng dụng.
    • Ứng dụng hỗ trợ đa quốc gia
    • Ứng dụng hỗ trợ Dynamic type (iOS): Cho phép thay đổi style của ứng dụng (font, fontsize, …)

    Khi mà mội dung của ứng dụng thay đổi, nội dung mới có thể yêu cầu một layout khác với layout hiện tại. Điều này thường xảy ra trong ứng dụng khi nó hiển thị text hoặc image(hình ảnh). Ví dụ, một ứng dụng tin tức cần phải điều chỉnh layout của nó dựa trên kích thước của từng loại tin tức. Tương tự, một ứng dụng ghép ảnh phải xử lí một loạt các kích thước của hình ảnh và tỉ lệ khung hình(aspect ratio).

    Việc làm ứng dụng hỗ trợ đa quốc gia là quá trình tạo ra một ứng dụng có thể hiển thị tốt ở trên các ngôn ngữ, khu vực và văn hóa khác nhau. Bố cục của ứng dụng hỗ trợ đa quốc gia phải tính đến những khác biệt này và cần hiển thị chính xác trong tất cả các ngôn ngữ và khu vực mà ứng dụng hỗ trợ.

    Ứng dụng hỗ trợ đa quốc gia có 3 tác dụng chính trên bố cục. Đầu tiên khi bạn chuyển giao diện sang một ngôn ngữ khác thì các Label sẽ yêu cầu phải có một khoảng trống khác. Ví dụ như: Tiếng đức thì cần nhiều không gian hơn tiếng anh. Tiếng nhật thì lại cần ít khoảng trống hơn nhiều.

    Thứ hai, Đinh dạng bạn sử dụng để hiển thị ngày và số có thể thay đổi giữa các khu vực, ngay cả khi ngôn ngữ không bị thay đổi. Mặc dù những thay đổi này thường không to bằng thay đổi ngôn ngữ, nhưng giao diện người dùng vẫn cần thích ứng với những thay đổi đó.

    Thứ ba, thay đổi ngôn ngữ có thể ảnh hưởng không chỉ đến kích thước của văn bản, mà cả tổ chức bố cục của nó. Các ngôn ngữ khác nhau sẽ sử dụng các hướng bố trí khác nhau. Ví dụ như: Tiếng Anh thì bố trí từ trái qua phải, còn tiếng Ả Rập và Do Thái thì ngược lại. Nói chung, thứ tự của các phần trong giao diện người dùng phải phù hợp với hướng bố trí bố cục trong ngôn ngữ đó. Nếu một Button nằm ở góc dưới bên phải của màn hình khi sử dụng Tiếng Anh thì nó nên được được nằm ở góc dưới bên trái khi sử dụng tiếng Ả Rập.

    Cuối cùng, nếu ứng dụng iOS của bạn hỗ trợ kiểu Dynamic type, người dùng có thể thay đổi kích thước, phông chữ được sử dụng trong ứng dụng của bạn. Điều này có thể thay đổi cả chiều cao lẫn chiều rộng của bất kỳ text trong giao diện người dùng của bạn. Nếu người dùng thay đổi kích thước phông chữ(font) của bạn khi ứng dụng đang chạy thì cả phông chữ (font) và bố cục(layout) đều phải thích ứng với thay đổi đó.

    Tổng kết

    Auto Layout là việc định nghĩa tập hợp các ràng buộc. Các ràng buộc này đại diện cho quan hệ giữa các view với nhau. Auto layout sau đó sẽ tính toán kích thước và vị trí của các view dựa trên các ràng buộc của chúng. Điều này tạo ra các bố cục tự đáp ứng được cả với thay đổi bên trong và bên ngoài.
    Để sử dụng auto layout chúng ta cần phải hiểu về logic đằng sau các layouts dựa trên các ràng buộc của nó.



    Cảm ơn các bạn đã theo dõi bài viết 😀

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