Điều hướng với MVVM trong iOS

Tôi đã áp dụng MVVM cho vài project và thực sự rất ấn tượng với nó. Đặc biệt nếu như bạn từ MVC chuyển sang thì bạn sẽ chỉ phải thêm một lớp nữa vào trong kiến trúc của mình đó là: view model. Tuy nhiên, trong MVVM, chúng ta ném business logic ra khỏi View Controller(VC), và nhận ra là còn presentation data và routing, chúng vẫn còn trong VC và cần được chuyển ra ngoài.

Ví dụ tôi có một màn hình login như sau:

 

 

Luồng di chuyển sẽ như sau:

  • Login > Home Screen
  • Sign Up > Sign Up Screen
  • Forgot Password (?) > Forgot Password Screen

Như các bạn thấy thì chỉ cần storyboard và 3 segue thì chúng ta đã dễ dàng implement luồng cho màn hình trên, thế nhưng sự thật không đơn giản như vậy. Ví dụ, bình thường app sẽ di chuyển tới màn Home Screen sau khi bấm nút Login, thế nhưng password của user có thể sẽ bị hết hạn hoặc bị hack, và bạn cần phải chuyển hướng tới màn đổi password. Và thế là luồng di chuyển sẽ trở thành như sau:

  • Login > Home Screen HOẶC Change Password Screen

Đến đây thì chúng ta không thể dùng storyboard được nữa và đành phải nhờ cậy vào VC:

func loginButtonTapped() {
   // Start network request...
   // Upon response:
   if viewModel.shouldChangePassword {
      performSegue(id: "ChangePasswordScreen", sender: nil)
   } else {
      performSegue(id: "HomeScreen", sender: nil)
   }
}

Logic điều hướng này không nên đặt ở trong VC. Nếu bạn muốn làm nhẹ bớt các VC thì hãy cân nhắc kỹ trước những khối lệnh if. Theo cách nhìn của tôi thì VC chỉ nên chứa code liên quan đến view và không nên chứa các khối lệnh rẽ nhánh. Do đó chúng ta hãy cùng khai báo một protocol và đưa khối lệnh if ra ngoài VC, chúng ta cần khai báo những đối tượng sau:

  • Route ID: A string identifier like segue ID. ID dùng để định danh giống như segue identifier
  • Context: VC hiện tại
  • Optional parameters: Một vài dữ liệu tạm thời dùng cho việc di chuyển (Tapped row index..vv.)
protocol Router {
   func route(
      to routeID: String, 
      from context: UIViewController, 
      parameters: Any?
   )
}

Và ở VC thì chúng ta chỉ khai báo tên của luồng di chuyển và không quan tâm nó sẽ di chuyển đến đâu. Đó sẽ là việc của router.

class LoginViewController: UIViewController {
 
   enum Route: String {
      case login
      case signUp
      case forgotPassword
   }
 
   var viewModel: LoginViewModel!
   var router: Router!
 
   ...
 
   func loginButtonTapped() {
      router.route(to: Route.login.rawValue, from: self)
   }
 
   func signUpTapped() {
      router.route(to: Route.signUp.rawValue, from: self)
   }
 
   func forgotPasswordTapped() {
      router.route(to: Route.forgotPassword.rawValue, from: self)
   }
}

Như đã nói ở trên thì nút login có thể điều hướng tới màn hình home screen hoặc change password screen. Vậy router sẽ làm cách nào để chọn đúng đường đi?. Ở những trường hợp này thì router cần truy cập tới view model, qua đó có thể đọc được logic business trong view model và di chuyển.

Lưu ý là VC đã retain tới VM và Router, do vậy đối tượng Router phải là kiểu tham chiếu weak/unowned nếu không sẽ xảy ra trường hợp retain cycle.

class LoginRouter: Router {
 
   unowned var viewModel: LoginViewModel
 
   init(viewModel: LoginViewModel) {
      self.viewModel = viewModel
   }
 
   func route(
      to routeID: String, 
      from context: UIViewController, 
      parameters: Any?) 
   {
      guard let route = LoginVC.Route(rawValue: routeID) else {
         return
      }
      switch route {
      case .login:
         if viewModel.shouldChangePassword {
            // Push change-password-screen.
         } else {
            // Push home-screen.
         }
      case .signUp:
         // Push sign-up-screen:
         let vc = SignUpViewController()
         let vm = SignUpViewModel()
         vc.viewModel = vm
         vc.router = SignUpRouter(viewModel: vm)
         context.navigationController.push(vc, animated: true)
      case . forgotPasswordScreen:
         // Push forgot-password-screen.
      }
   }
}

Tổng kết lại:

  • Chúng ta đã gỡ bỏ hoàn toàn code điều hướng ra khỏi VC, qua đó tuân thủ theo nguyên tắc "chia để trị". Mỗi một VC chỉ đảm bảo xử lý đúng với vai trò của nó.
  • Sau này khi có thay đổi về design hay logic, thì bạn cung sẽ dễ dàng điều chỉnh hơn mà không sợ hiệu ứng domino xảy ra.
  • Cuối cùng là bạn sẽ không phải dùng đến storyboard segues. Theo tôi thì Storyboard chỉ nên phụ trách về layout.

Apple đưa ra công cụ Swift Playgrounds để giúp những người mới bắt đầu học lập trình Apple đưa ra công cụ Swift Playgrounds để giúp những người mới bắt đầu học lập... Hồ Sỹ Hùng Blog Home Tuyển tập các công cụ lập trình hữu dụng cho lập trình viên iOS Tuyển tập các công cụ lập trình hữu dụng cho lập trình viên iOS Nguyễn Duy Khánh
Nguyễn Duy Khánh

iOS Developer, Former Student and Content Editor of TechMaster