Tham gia ngay khoá học lập trình iOS, hình thức học tập rất linh hoạt cho bạn lựa chọn và sẽ có mức học phí khác nhau tuỳ theo bạn chọn học Online, Offline hoặc FlipLearning(Kết hợp giữa Online và Offline). Ngoài ra bạn có thể tham gia thực tập toàn thời gian tại Techmaster để rút ngắn thời gian học và tăng cơ hội việc làm.

Chúng ta sẽ đi luôn vào phần thực hành, đây là giao diện ứng dụng:

I)Tạo project

Chúng ta sẽ làm một ví dụ về phần đăng nhập tài khoản. Đầu tiền tôi sẽ tạo khung cho ví dụ này với các group sau:

Với mỗi group thì nó sẽ chứa các class liên quan đến tên group, ở đây có group extensions sau này các bạn có thể chuyển nó vào trong group Utils. Và chúng ta sẽ tạo giao diện gồm 2 textfields và 1 button.

II)Viết mã

View

Tôi sẽ làm phần giao diện trước, ở đây tôi sử dụng luôn Main.storyboard thực tế tôi thích sử dụng file xib hơn.

Với phần giao diện tôi sẽ chia thành một view chính có tên LoginFormView và các presenters như 2 cái textfields và 1 button.

Tôi sẽ tạo một group mới trong group presenters có tên Login:

Trong group này tôi tạo 3 files khác là LoginButtonPresentable, LoginTextFieldPresentable và LoginPasswordPresentable. Với LoginButtonPresentable tôi thêm đoạn mã sau:

import UIKit

protocol LoginButtonPresentable {
    var btn_bgColor: UIColor{get}
    var btn_textColor: UIColor{get}
}
protocol LoginButtonAction
{
    func login(email: String, password: String)
}

@objc protocol LoginFormViewModelDelegate
{
    @objc optional func showErrors(errors: [String])
}

Ở đây với protocol LoginButtonPresentable nó sẽ chứa các thuộc tính về phần hiển thị, 2 protocols còn lại phụ trách các action của button login và các action từ viewmodel trả về.

Ở 2 files tiếp theo tôi thêm 2 protocols sau:

protocol LoginTextFieldPresentable {
    var textColor: UIColor{get}
    var placeholderTextColor: UIColor{get}
    var borderStyle: UITextBorderStyle{get}
    var bottomBorderColor: UIColor{get}
    var bottomBorderWidth: CGFloat{get}
    var emailPlaceholder: String{get}
    var emailLeftView: UIView{get}
    var emailLeftViewMode: UITextFieldViewMode{get}
}

protocol LoginPasswordPresentable{
    var passwordPlaceholder: String {get}
    var passwordLeftView: UIView {get}
    var passwordLeftViewMode: UITextFieldViewMode {get}
}

Giống như protocol LoginButtonPresetable thì 2 protocols trên phụ trách về các thuộc tính hiển thị của 2 textfields.

Tiếp theo ở group Views tôi tạo 1 file mới có teen LoginFormView.swift và trong đấy tôi sẽ viêt các đoạn mã như sau:

import Foundation
import UIKit

typealias LoginFormPresentable = LoginButtonPresentable & LoginPasswordPresentable & LoginTextFieldPresentable & LoginButtonAction

Ở đây tôi định nghĩa một kiểu có tên LoginFormPresentable theo 4 protocols như trên.

Tiếp theo các bạn cần ánh xạ các đối tượng ngoài main.storyboard vào:

Tiếp theo tôi sẽ thêm thuộc tính presenter và 1 func configure

private var presenter: LoginFormPresentable?
    func configure(withPresenter presenter: LoginFormPresentable) {
        self.presenter = presenter

        
        emailTextField.attributedPlaceholder = NSAttributedString(string: presenter.emailPlaceholder, attributes: [NSForegroundColorAttributeName: presenter.placeholderTextColor])
        
        emailTextField.borderStyle = presenter.borderStyle
        emailTextField.textColor = presenter.textColor
        emailTextField.leftView = presenter.emailLeftView
        emailTextField.leftViewMode = presenter.emailLeftViewMode
        
        passwordTextField.attributedPlaceholder = NSAttributedString(string: presenter.passwordPlaceholder, attributes: [NSForegroundColorAttributeName: presenter.placeholderTextColor])
        
        passwordTextField.borderStyle = presenter.borderStyle
        passwordTextField.textColor = presenter.textColor
        passwordTextField.leftView = presenter.passwordLeftView
        passwordTextField.leftViewMode = presenter.passwordLeftViewMode
        
        btn_Login.backgroundColor = presenter.btn_bgColor
        btn_Login.setTitleColor(presenter.btn_textColor, for: .normal)
        
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }

Thuộc tính presenter có kiểm LoginFormPresentable chứa tất cả các thành phần và sự kiện liên quan đến login.

Func configure khởi tạo với tham số có kiểu LoginFormPresentable, class này hoàn toàn chỉ có tác dụng lấy dữ liệu trong viewmodel lên để hiển thị và khi có action thì thông báo cho viewmodel nên hoàn toàn trong này các bạn sẽ không thấy phần xử lý logic cũng như set màu,… ở đây toàn toàn nó chỉ gọi đến các thuộc tính ở trong presenter.

Tiêp theo tôi thêm hàm xử lý thêm boder:

override func layoutSubviews() {
        super.layoutSubviews()
        if let color = self.presenter?.bottomBorderColor, let width = self.presenter?.bottomBorderWidth {
            emailTextField.setBottomBorder(withColor: color, borderWidth: width)
            passwordTextField.setBottomBorder(withColor: color, borderWidth: width)
        }
    }

Khoan hãy chạy vì ở đây tôi gọi đến hàm setBottomBorder mà UITextField không cung cập nên tôi sẽ cần tạo 1 extention của UITextField ở trong group Extentions:

import UIKit

extension UITextField {
    func setBottomBorder(withColor color:UIColor, borderWidth width: CGFloat) {
        let border = CALayer()
        border.borderColor = color.cgColor
        border.frame = CGRect(x: 0, y: self.frame.size.height - width,
                              width: self.frame.size.width, height: self.frame.size.height)

        border.borderWidth = width

        self.layer.addSublayer(border)
        self.layer.masksToBounds = true
    }
    
}

Và sự kiện khi người dùng ấn enter thì sẽ xuống dòng:

//MARK: UITextFieldDelegate
extension LoginFormView {
    func textFieldShouldReturn(textField: UITextField) -> Bool {
        let tag = textField.tag
        
        if(tag == emailTextField.tag) {
            passwordTextField.becomeFirstResponder()
        } else if(tag == passwordTextField.tag) {
            textField.resignFirstResponder()
            doLogin()
            return false
        }
        
        return true
    }
}

Ở đây các bạn có thể thấy khi người dùng ấn enter ở ô password thì ứng dụng sẽ login vậy nên tôi sẽ gọi đến hàm doLogin.

func doLogin() {
        self.presenter?.login(email: emailTextField.text ?? "", password: passwordTextField.text ?? "")
    }

Ở đây để thông báo cũng như cung cập thông tin cho viewmodel xử lý tôi gọi đến func login và truyền các tham số vào.

Và còn một sự kiện là khi người dùng ấn vào nút đăng nhập thì tôi cũng cần gọi đến func doLogin. Trước tiên tôi sẽ ánh xạ sự kiện khi người dùng ấn và sau đó gọi đến doLogin

@IBAction func loginDidTap(sender: UIButton) {
        print("action")
        doLogin()
    }

Model:

Ở phần đầu tôi chỉ viết chức năng cho phần kiểm tra dữ liệu nên tôi sẽ chưa tạo Models và sẽ qua phần tiếp theo.

ViewModel:

Như các bạn biết thì ViewModel sẽ phụ trách toàn bộ logic liên quan đến dữ liệu, vậy với chức năng đăng nhập thì chúng ta bước đầu sẽ cần kiểm tra xem email và password có điền đúng định dạng và khi điền sai định dạng thì nó sẽ báo cho viewcontroller thiển thị lên 1 alert.

Đầu tiên tôi tạo file LoginFormViewModel.swift ở trong group ViewModels. Tôi sẽ viết các đoạn mã như sau:

//1
internal var errors: [String] = []
//2
    var delegate: LoginFormViewModelDelegate?
    init() {
    }
    //MARK: Error Checking
//3
    func cleanErrors() {
        errors.removeAll()
    }
    func addError(error: String)
    {
        errors.append(error)
    }

//1 Ở đây do người dùng có thể điền sai định dạng của email và password nên tôi sử dụng mảng, ở đây các bạn hoàn toàn cỏ thể sử dụng string và cộng dồn cũng được.

//2 Tôi khai báo 1 delegate gồm các sự kiện khi người dùng login ở đây nó có sự kiện thông báo ngược lại cho viewcontroller biết trạng thái login của tài khoản.

//3 Đây là 2 funcs cho phép thêm và xoá errors.

Tiếp theo tôi sẽ thêm 2 funcs cho phép kiểm tra xem định dạng email và password nhập có đúng hay không:

    func checkEmailErrors(email: String) -> Bool {
        let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"
        let validator = NSPredicate(format: "SELF MATCHES %@", regex)
        let valid = validator.evaluate(with: email);
        
        if !valid {
            addError(error: "Hãy điền đúng định dạng")
        }
        
        return valid
    }
    
    func checkPasswordErrors(password: String) -> Bool {
        let valid = !password.isEmpty && password.characters.count >= 6
        
        if !valid {
            addError(error: "Mật khẩu phải trên 5 ký tự")
        }
        
        return valid
    }

 Ở bài này tôi muốn mỗi một textfield sẽ có một cái image bên trái nên tôi sẽ thêm func thực hiện nhiệm vụ này:

func leftViewForTextField(image: UIImage?) -> UIImageView {
        //5px as padding
        let imageView = UIImageView(frame: CGRect(x:0, y:0, width:15 + 5, height:15))
        
        imageView.image = image
        imageView.contentMode = .scaleAspectFit
        
        return imageView
    }

Và tiếp theo là các extention của LoginFormViewModel cho phép hiển thị màu sắc và định dạng các thành phần trên LoginForm:

//MARK: Email Presentable
extension LoginFormViewModel {
    var emailPlaceholder: String { return "techmaster@email.com" }
    var emailLeftView: UIView {
        return leftViewForTextField(image: UIImage(named: "icn-textfield-email"))
    }
    var emailLeftViewMode: UITextFieldViewMode { return .always }
    
    var textColor: UIColor { return UIColor.white }
    var placeholderTextColor: UIColor { return UIColor(white: 0.85, alpha: 1) }
    var borderStyle: UITextBorderStyle { return .none }
    var bottomBorderColor: UIColor { return UIColor.white }
    var bottomBorderWidth: CGFloat { return 1 }
}

//MARK: Password Presentable
extension LoginFormViewModel {
    var passwordPlaceholder: String { return "password" }
    var passwordLeftView: UIView {
        return leftViewForTextField(image: UIImage(named: "icn-textfield-password"))
    }
    var passwordLeftViewMode: UITextFieldViewMode { return .always }
}

//MARK: Login Button Presentable
extension LoginFormViewModel
{
    var btn_bgColor: UIColor{ return UIColor(red: 248/255, green: 55/255, blue: 102/255, alpha: 1)}
    var btn_textColor: UIColor{ return UIColor.white}
}

//MARK: Login Button Action
extension LoginFormViewModel {
    
    func login(email: String, password: String) {
        cleanErrors()
        
        let validEmail = checkEmailErrors(email: email)
        let validPassword = checkPasswordErrors(password: password)
        
        if !validEmail || !validPassword {
            self.delegate?.showErrors!(errors: self.errors)
            return
        }
        //login progress
    }

}

ViewController:

Như các bạn thấy thì ở View phụ trách phần hiển thị dữ liệu, ViewModel sẽ xử lý logic, và cuối cùng tôi sẽ cần 1 viewcontroller để lắp ráp tất cả các phần trên lại, tôi sẽ tậo mối class mới có thên LoginViewController ở trong group viewcontrollers.

import UIKit

class LoginViewController: UIViewController, LoginFormViewModelDelegate {
    @IBOutlet weak var loginFormView: LoginFormView!
    var loginViewModel: LoginFormViewModel?
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        // Do any additional setup after loading the view.
    }
    func setupViews()
    {
        loginViewModel = LoginFormViewModel()
        loginViewModel?.delegate = self
        loginFormView.configure(withPresenter: loginViewModel!)
    }
    func showErrors(errors: [String]) {
        let message = errors.joined(separator: "\n")
        let errorAlert = UIAlertController(title: "Lỗi", message: message, preferredStyle: .alert)
        errorAlert.addAction(UIAlertAction(title: "Đồng ý", style: .default, handler: nil))
        self.present(errorAlert, animated: true, completion: nil)
    }

}

Với class LoginViewController tôi cho nó tuân thủ theo LoginFormViewModelDelegate để khi bên ViewModel xử lý logic xong nó sẽ báo lại cho ViewController để hiển thị lên lỗi sử dụng func showErrros.

Thuộc tính loginFormView có kiểu LoginFormView cũng được ánh xạ vào.

Thuộc tính loginViewModel. Ở viewcontroller là nơi tôi quản lý việc lắp ghép viewmodel và view với func setupViews().

Vậy phần 1 đã kết thúc, nếu các bạn thấy hay hãy chia sẽ để tôi có động lực chỉnh sửa cũng như viết tiếp phần 2. 

Tham gia ngay khoá học lập trình iOS, hình thức học tập rất linh hoạt cho bạn lựa chọn và sẽ có mức học phí khác nhau tuỳ theo bạn chọn học Online, Offline hoặc FlipLearning(Kết hợp giữa Online và Offline). Ngoài ra bạn có thể tham gia thực tập toàn thời gian tại Techmaster để rút ngắn thời gian học và tăng cơ hội việc làm.