🎉

Vue3,Swift UIのMVVMを学んでみた

2024/05/16に公開

この記事を書こうと思った理由

私は、現職でNuxt3をもちいて、フロントエンド開発を行っています。
今までなんとなく使っていたvueについて調べたところ、MVVMというアーキテクチャを使っていることから、しっかりとしらべて、自分なりに理解したことをここにアウトプットします。
また、アーキテクチャを理解することで、より早く開発ができると思ったことと、工数の見積もりを行う際に指標にできると思ったのでアーキテクチャを理解したいと思いました。

特に、データ双方向バインディングについて、理解しにくかったので、解説していきます。

この記事で解説すること

この記事で解説することは以下の4つです!

  • MVVMとは?
  • データ双方向バインディングとは?
  • Nuxt3とSwiftでの例
  • 身近な技術と比較、違いのまとめ

それでは、データ双方向バインディングを中心に、Nuxt3とSwiftを用いたMVVMの例を解説していきます。


1.MVVMとは?

MVVMの基本概念

MVVMは、Model-View-ViewModelの略で、ソフトウェアのアーキテクチャパターンの一つです。特にUI開発において広く使われています。各コンポーネントの役割は以下の通りです。

  • Model: データやビジネスロジックを扱う部分。アプリケーションの状態やルールを保持します。
  • View: ユーザーインターフェース。ユーザーが直接操作する部分です。
  • ViewModel: ModelとViewの仲介役。Viewのロジックを保持し、ModelとViewをつなげる役割を果たします。

2.データ双方向バインディングとは?

データ双方向バインディングの基本概念

データ双方向バインディングは、UIコンポーネントとデータモデルの間で自動的にデータの同期を行う技術です。これにより、UIとデータの整合性が保たれ、コードの簡潔さと保守性が向上します。具体的には、ユーザーがUIで行った操作が即座にデータモデルに反映され、データモデルの変更も自動的にUIに反映されます。

3.Nuxt3とSwiftでの例

Nuxt3でのデータ双方向バインディングの例

シンプルなTodoListアプリで解説。

Model (models/Todo.js)

export default class Todo {
  constructor(id, title, completed = false) {
    this.id = id;
    this.title = title;
    this.completed = completed;
  }
}

ViewModel (composables/useTodos.js)

import { ref } from 'vue'
import Todo from '../models/Todo'

const todos = ref([])

const addTodo = (title) => {
  todos.value.push(new Todo(
    todos.value.length + 1,
    title
  ))
}

const toggleTodo = (id) => {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.completed = !todo.completed
}

export const useTodos = () => {
  return { todos, addTodo, toggleTodo }
}

View (pages/index.vue)

<template>
  <div>
    <h1>Todo List</h1>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input type="checkbox" v-model="todo.completed" />
        <span>{{ todo.title }}</span>
      </li>
    </ul>
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a new todo" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useTodos } from '../composables/useTodos'

const { todos, addTodo } = useTodos()
const newTodo = ref('')

const handleAddTodo = () => {
  if (newTodo.value.trim() === '') return
  addTodo(newTodo.value)
  newTodo.value = ''
}
</script>

この例では、v-modelディレクティブを使用して、input要素とtodo.completedの双方向バインディングを実現しています。これにより、チェックボックスの状態が変わるとtodosの状態も自動的に更新されます。
Reactなどの一方通行型バイディングとは違い、お互いを参照、監視し合っているのでコード数が少なくて済むんですね。

Swiftでのデータ双方向バインディングの例

Swiftでは、SwiftUIを使ってデータ双方向バインディングを実現します。以下に同様のTodoリストの例です。
個人的には、こちらの方がわかりやすいかと思います。

Model (Todo.swift)

import Foundation

struct Todo: Identifiable {
    var id: UUID = UUID()
    var title: String
    var completed: Bool = false
}

ViewModel (TodoViewModel.swift)

import SwiftUI

class TodoViewModel: ObservableObject {
    @Published var todos: [Todo] = [
        Todo(title: "Learn SwiftUI"),
        Todo(title: "Build a project")
    ]
    
    func addTodo(title: String) {
        let newTodo = Todo(title: title)
        todos.append(newTodo)
    }
    
    func toggleTodoCompleted(id: UUID) {
        if let index = todos.firstIndex(where: { $0.id == id }) {
            todos[index].completed.toggle()
        }
    }
}

View (ContentView.swift)

import SwiftUI

struct ContentView: View {
    @StateObject private var viewModel = TodoViewModel()
    @State private var newTodo = ""

    var body: some View {
        VStack {
            TextField("Add a new todo", text: $newTodo, onCommit: {
                viewModel.addTodo(title: newTodo)
                newTodo = ""
            })
            .textFieldStyle(RoundedBorderTextFieldStyle())
            .padding()

            List {
                ForEach(viewModel.todos) { todo in
                    HStack {
                        Text(todo.title)
                        Spacer()
                        Button(action: {
                            viewModel.toggleTodoCompleted(id: todo.id)
                        }) {
                            Image(systemName: todo.completed ? "checkmark.circle.fill" : "circle")
                        }
                    }
                }
            }
        }
        .padding()
    }
}

この例では、@StateObjectと@Publishedを使ってデータ双方向バインディングを実現しています。TextFieldのtextプロパティとnewTodoがバインディングされており、ユーザーが入力するとその値が即座にnewTodoに反映されます。なので、vueでいうrefの更新やreactのuseStateの更新といったことが必要ないんです。

4.身近な技術と比較、違いのまとめ

MVCとの比較

MVC: Model-View-Controllerは、ViewとModelをControllerが仲介します。

Controllerはユーザーの入力を処理し、Modelを更新し、その結果をViewに反映します。データバインディングは明示的に行う必要があります。ruby on railsやlaravel、 Spring frameworkなどのバックエンドで使われている印象が強いです。

  • なぜ、バックエンドなのか
    バックエンド開発とフロントエンド開発を分けて行う傾向が強まっているため、バックエンド開発者はviewを気にせず、ModelとControllerに集中することができます。これにより、より強固で柔軟な機能を作りやすくなります。バックエンドフレームワークでMVCが取り入れられている理由には、以下の点が挙げられます。

  • 関心の分離: MVCアーキテクチャは、コードの関心ごとを明確に分離します。これにより、データ処理、ビジネスロジック、ユーザーインターフェースを独立して開発できるため、コードの理解、保守、再利用が容易になります。

  • 役割の分担: 各コンポーネント(Model、View、Controller)に役割を分けることで、特定の部分に集中して開発でき、全体の品質向上につながります。バックエンド開発者はModelとControllerに集中し、効率的に開発を進めることができます。

MVVM: ViewModelがViewとModelの双方向データバインディングを管理します。

ViewはViewModelを監視し、変更があれば自動的に更新されます。データバインディングが自動的に行われ、コードがシンプルになります。vueやswiftなどのUIを作るフレームワークで多く採用されているみたいです。

Nuxt3とSwiftの違い

Nuxt3: Vue.jsのフレームワークで、v-modelディレクティブを用いて双方向バインディングを簡単に実現します。コンポーネントベースの開発が特徴です。
Swift: SwiftUIは、宣言的なUIフレームワークで、@StateObjectや@Publishedを用いて双方向バインディングを実現します。Swift言語を用いた開発が特徴です。


全体のまとめ

このように、Nuxt3とSwiftはフレームワークは異なりますが、基本的な考え方やロジックの組み方は非常に似ています。そのため、アーキテクチャを学ぶことで、アプリケーションを迅速に作成するスキルや、さまざまな言語・フレームワークの理解を深めることができます。

また、アーキテクチャを理解することで、開発プロセス全体の見通しが良くなり、工数の見積もりも正確に行えるようになります。プロジェクトの各部分にどれだけの時間とリソースが必要かを予測しやすくなるため、効率的なプロジェクト管理が可能になります。

皆さんもぜひ、アーキテクチャを学んでみてはいかがでしょうか

Discussion