มาลองเขียนแอพ iOS ด้วย MVC Pattern

Programming

มาลองเขียนแอพ iOS ด้วย MVC Pattern

6 เดือนที่ผ่านมา

4 min read

Model

Model คือส่วนของ Data Layer ใช้สำหรับกำหนด Structure ของข้อมูล จะเห็นการใช้ struct อยู่บ่อยๆเพื่อกำหนดในส่วนนี้ ตัวอย่างเช่น UserModel ก็จะมีการกำหนดโครงสร้างของข้อมูลคร่าวประมาณนี้

struct UserModel {
  let firstName: String
  let lastName: String
  let email: String
}

View

View คือส่วนที่แสดงผล เป็นส่วนที่คนใช้งานแอพเรามองเห็น และเมื่อมี events ต่างๆ ก็จะถูกส่งไปที่ Controller จากนั้น Controller ก็จะเป็นคนจัดการว่า เพื่อเกิด event นี้จะให้ทำอะไรต่อ

Controller

Controller เป็นตัวกลางระหว่าง View กับ Model ที่คอยคุมการทำงานทั้งหมดของแอพ อย่างเช่น user กดปุ่มนี้ จะให้ทำอะไรต่อ ตรงนี้แหละเป็นหน้าที่ของ Controller ทั้งหมด เรียกได้ว่าแทบจะทุก events ที่ user มีการกระทำ

Workflow

ลองมาดูรูปเพื่อความเข้าใจมากขึ้น

Model-View-Controller design pattern
Model-View-Controller design pattern

Implement with App

หลังจากที่ได้รู้หลักการไปแล้ว ลองมาใช้กับแอพดูบ้าง โดยแอพที่จะทำก็เป็นแอพง่ายๆ คือจำลองการ login โดยตัวแอพจะใช้ UIKit และไม่ใช้ Storyboard โดยโครงสร้างของ Project จะเป็นประมาณนี้

 ├── Controller
 │   ├── HomeViewController.swift
 │   └── LoginViewController.swift
 ├── Model
 │   └── UserModel.swift
 ├── Service
 │   └── NetworkService.swift
 └── View
     ├── Home.swift
     └── LoginView.swift

Model

มาดูในส่วนของ Model กันก่อนเลย โดยในส่วนของโครงสร้างข้อมูล ก็จะกำหนดไว้ง่ายๆ ประมาณนี้

import Foundation
 
struct User {
    let firstName: String
    let lastname: String
    let email: String
}

View

ในส่วน View ก่อน โดยจะมี 2 ส่วน คือ

  • LoginView หน้าจอสำหรับ Login
  • HomeView คือส่วนที่แสดงผลหลังจาก Login

LoginView

ถ้าถามว่าใช้ Storyboard ได้ไหม ตอบเลยว่าได้ แต่ผมไม่ใช้ เพราะไม่ชอบลากวาง ชอบเขียน code มากกว่า 😅

import UIKit
 
class LoginView: UIView {
 
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
 
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
 
    lazy var emailField: UITextField = {
        let field = UITextField();
        field.layer.borderWidth = 0.5
        field.layer.cornerRadius = 6
        field.placeholder = "Email"
        field.keyboardType = .emailAddress
        field.autocapitalizationType = .none
 
        let paddingView = UIView(frame: CGRect(x: 0, y: 0,
                width: 10, height: field.frame.size.height))
        field.leftView = paddingView
        field.leftViewMode = .always
 
        return field
    }()
 
    lazy var passwordField: UITextField = {
        let field = UITextField()
        field.layer.borderWidth = 0.5
        field.layer.cornerRadius = 6
        field.placeholder = "Password"
        field.keyboardType = .default
        field.isSecureTextEntry = true
        field.autocapitalizationType = .none
 
        let paddingView = UIView(frame: CGRect(x: 0, y: 0,
                width: 10, height: field.frame.size.height))
        field.leftView = paddingView
        field.leftViewMode = .always
 
        return field
    }()
 
    lazy var loginButton: UIButton = {
        let button = UIButton()
        button.setTitle("Login", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 24, weight: .medium)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .systemIndigo
        button.alpha = 0.6
        button.layer.cornerRadius = 6
        button.isEnabled = false
 
        return button
    }()
 
 
 
    private func setupView() {
        backgroundColor = .systemBackground
        addSubview(loginButton)
        addSubview(emailField)
        addSubview(passwordField)
 
        loginButton.translatesAutoresizingMaskIntoConstraints = false
        emailField.translatesAutoresizingMaskIntoConstraints = false
        passwordField.translatesAutoresizingMaskIntoConstraints = false
 
        emailField.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        emailField.widthAnchor.constraint(equalToConstant: 200).isActive = true
        emailField.heightAnchor.constraint(equalToConstant: 50).isActive = true
        emailField.bottomAnchor.constraint(equalTo: passwordField.topAnchor,constant: -15).isActive = true
 
        passwordField.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        passwordField.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        passwordField.widthAnchor.constraint(equalToConstant: 200).isActive = true
        passwordField.heightAnchor.constraint(equalToConstant: 50).isActive = true
 
        loginButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        loginButton.topAnchor.constraint(equalTo: passwordField.bottomAnchor, constant: 15).isActive = true
        loginButton.widthAnchor.constraint(equalToConstant: 100).isActive = true
        loginButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
 
    }
}

ในส่วน LoginView ก็จะมีหน้าตาประมาณนี้

LoginView
LoginView

HomeView

ใน HomeView ก็ไม่มีอะไรมาก หลังจากที่ login ผ่าน ก็จะแสดงชื่อแค่นั้น

 
import UIKit
 
class HomeView: UIView {
 
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
 
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
 
    lazy var welcomeLabel: UILabel = {
        let label = UILabel()
        label.text = "Welcome"
        label.font = .systemFont(ofSize: 32, weight: .medium)
 
        return label
    }()
 
    private func setupView() {
        backgroundColor = .systemBackground
        addSubview(welcomeLabel)
        welcomeLabel.translatesAutoresizingMaskIntoConstraints = false
 
        welcomeLabel.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        welcomeLabel.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    }
}
LoginView
LoginView

Controller

LoginViewController

import UIKit
 
class LoginViewController: UIViewController, UITextFieldDelegate {
    let loginView = LoginView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(loginView)
        setupUI()
        loginView.emailField.delegate = self
        loginView.passwordField.delegate = self
        
        loginView.emailField.addTarget(self, action: #selector(validateField), for: .editingChanged)
        loginView.passwordField.addTarget(self, action: #selector(validateField), for: .editingChanged)
        
       
    }
    
    func setupUI() {
        loginView.translatesAutoresizingMaskIntoConstraints = false
        loginView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        loginView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if textField == loginView.emailField {
            loginView.passwordField.becomeFirstResponder()
        } else if textField == loginView.passwordField {
            performLogin()
        }
        
        return true
    }
    
    @objc func loginButtonTapped() {
        performLogin()
    }
    
    @objc private func validateField() {
        loginView.loginButton.isEnabled = loginView.emailField.text != "" && loginView.passwordField.text != ""
        
        if loginView.loginButton.isEnabled {
            loginView.loginButton.alpha = 1
            loginView.loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
        } else {
            loginView.loginButton.alpha = 0.5
        }
    }
    
    func performLogin() {
        let email = loginView.emailField.text ?? ""
        let password = loginView.passwordField.text ?? ""
        print("logging...")
 
        
        let homeVC = HomeViewController()
        
        NetworkService.share.login(email: email, password: password) { success in
            if success {
                self.present(homeVC, animated: true)
                self.loginView.emailField.text = ""
                self.loginView.passwordField.text = ""
            } else {
                let alertController = UIAlertController(title: "เข้าไม่ได้อ่ะ", message: "แน่ใจนะว่า Email กับ Password ถูก ?", preferredStyle: .alert)
                let tryAction = UIAlertAction(title: "ลองใหม่", style: .default) { (action) in
                    print(action.title!)
                }
                alertController.addAction(tryAction)
                self.present(alertController, animated: true)
            }
        }
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    } 
}

HomeViewController

import UIKit
 
class HomeViewController: UIViewController {
    let homeView = HomeView()
    var user: User!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        user = NetworkService.share.getUser()
        greetingUser()
        view.addSubview(homeView)
        
        homeView.translatesAutoresizingMaskIntoConstraints = false
        homeView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        homeView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
    }
    
    private func greetingUser() {
        homeView.welcomeLabel.text = "👋 \(user.firstName) \(user.lastname)"
    }
}

Reference

Tags:

Swift MVC iOS