SwiftUI - MVVM 패턴을 사용해 만들어보는 알림 페이지

MVVM이란?

MVVM (Model-View-ViewModel) 패턴은 소프트웨어 개발에 있어서 사용자 인터페이스를 구현하기 위한 아키텍처 패턴 중 하나다. 이 패턴은 주로 UI 애플리케이션에서 사용되며, MVC (Model-View-Controller) 패턴을 발전시킨 형태로 볼 수 있다. MVVM은 아래 세 부분으로 구성된다.

 

Model

애플리케이션의 데이터와 비즈니스 로직을 담당. 데이터베이스, 유효성 검사, 객체 등이 여기에 해당된다.

View

사용자에게 보여지는 UI 부분으로 사용자의 입력을 받고, 표현하는 역할을 한다.

ViewModel

View를 표현하기 위한 데이터와 명령을 가지고 있으며, Model과 View 사이의 중재자 역할을 한다. View에 바인딩하여 데이터의 변화를 자동으로 반영하도록 설계된다.

 

어떤 상황에서 사용해야 하는가?

MVVM 패턴은 특히 UI의 복잡성이 높고, 사용자 인터페이스와 비즈니스 로직의 분리가 중요한 대규모 애플리케이션 또는 동적인 애플리케이션에서 유용하다. 데이터의 표현과 데이터 처리를 분리함으로써, 코드의 가독성과 유지보수성을 높일 수 있게된다.

 

상태 관리는 어떻게 해야 하는가?

대체로 ViewModel 내에서 상태 관리를 수행한다. ViewModel은 View에 표시될 데이터와 UI 상태를 포함하며, 이 데이터는 View와 바인딩되어 UI가 데이터의 변화를 반영하도록 한다. 상태 관리를 위해, Observable 객체나 프레임워크에서 제공하는 상태 관리 도구 (예: SwiftUI의 @State, @ObservableObject 등)를 사용할 수 있다. ViewModel 내에서 데이터 변화를 감지하고, 필요한 로직 처리 후 View에 데이터를 업데이트하게 된다.

 

DB에 데이터 입력은 어디서 구현하는지?

DB에 데이터 입력은 Model에서 구현한다. ViewModel은 View로부터 사용자의 입력을 받아 Model에 데이터를 저장하거나 수정하는 명령을 내릴 수 있다. 이 과정에서 ViewModel은 필요한 데이터 처리 (유효성 검사, 데이터 변환 등)를 수행하고, Model은 실제 데이터베이스에 데이터를 입력하거나 업데이트한다. Model은 DB와의 직접적인 통신을 담당하며, 데이터의 저장, 조회, 업데이트, 삭제 등의 작업을 실행하게 된다.

 


알림리스트 구현

SwiftUI와 MVVM 패턴을 사용하여 SQLite 데이터베이스에 알림 데이터를 삽입하고, 조회 및 업데이트하는 기능을 구현해보자. 먼저, SQLite 데이터베이스 작업을 위한 간단한 래퍼 클래스를 구현한다. 

 

NotificationModel.swift

import Foundation

struct NotificationModel: Identifiable {
    var id: Int64
    var title: String
    var message: String
    var date: Date
}

 

DatabaseManager.swift

import Foundation
import SQLite

class DatabaseManager {
    static let shared = DatabaseManager()
    private let db: Connection?

    private let notifications = Table("notifications")
    private let id = Expression<Int64>("id")
    private let title = Expression<String>("title")
    private let message = Expression<String>("message")
    private let date = Expression<Date>("date")

    private init() {
        let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        do {
            db = try Connection("\(path)/appdb.sqlite3")
            createTable()
        } catch {
            db = nil
            print("Unable to open database")
        }
    }

    func createTable() {
        do {
            try db?.run(notifications.create(ifNotExists: true) { t in
                t.column(id, primaryKey: .autoincrement)
                t.column(title)
                t.column(message)
                t.column(date)
            })
        } catch {
            print("Unable to create table")
        }
    }
    
    // Fetch all notifications
    func fetchNotifications() -> [NotificationModel] {
        var notificationsList = [NotificationModel]()
        
        do {
            for notification in try db!.prepare(notifications) {
                let notificationModel = NotificationModel(id: notification[id], title: notification[title], message: notification[message], date: notification[date])
                notificationsList.append(notificationModel)
            }
        } catch {
            print("Select failed")
        }
        
        return notificationsList
    }
    
    // Insert new notification
	func insertNotification(title: String, message: String, date: Date) {
    let insert = notifications.insert(self.title <- title, self.message <- message, self.date <- date)
        do {
            let _ = try db?.run(insert)
        } catch {
            print("Insert failed: \(error)")
        }
    }

    // Delete a notification
	func deleteNotification(id: Int64) {
    let notification = notifications.filter(self.id == id)
    do {
        let _ = try db?.run(notification.delete())
    } catch {
        print("Delete failed: \(error)")
    }
}

위 코드는 SQLite 데이터베이스에서 알림 데이터를 삽입하고 조회하는 기본적인 작업을 수행한다. DatabaseManager 클래스는 싱글톤 패턴으로 구현되어 앱 전체에서 하나의 인스턴스만 사용된다.

 

NotificationViewModel.swift

import Foundation
import Combine

class NotificationViewModel: ObservableObject {
    @Published var notifications = [NotificationModel]()
    
    init() {
        loadNotifications()
    }
    
    func loadNotifications() {
        notifications = DatabaseManager.shared.fetchNotifications()
    }
    
    func addNotification(title: String, message: String, date: Date) {
        DatabaseManager.shared.insertNotification(title: title, message: message, date: date)
        loadNotifications() // Reload the notifications to reflect changes
    }

    func removeNotification(id: Int64) {
        DatabaseManager.shared.deleteNotification(id: id)
        loadNotifications() // Reload the notifications to reflect changes
    }

}

NotificationViewModel 클래스는 ObservableObject 프로토콜을 준수하며, 알림 목록을 관찰 가능한 속성으로 가진다. SwiftUI 뷰는 이 속성에 대한 변경 사항을 관찰하고, 데이터가 변경될 때 UI를 업데이트한다.

NotificationListView.swift

import SwiftUI

struct NotificationListView: View {
    @ObservedObject var viewModel = NotificationViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.notifications, id: \.id) { notification in
                    VStack(alignment: .leading) {
                        Text(notification.title).font(.headline)
                        Text(notification.message).font(.subheadline)
                        Text("\(notification.date)").font(.caption)
                    }
                }
                .onDelete(perform: delete)
            }
            .navigationBarItems(trailing: Button(action: addNotification) {
                Text("Add")
            })
            .navigationTitle("Notifications")
        }
    }
    
    func addNotification() {
        viewModel.addNotification(title: "New Notification", message: "This is a test", date: Date())
    }

    func delete(at offsets: IndexSet) {
        for index in offsets {
            let notification = viewModel.notifications[index]
            viewModel.removeNotification(id: notification.id)
        }
    }
}

struct NotificationListView_Previews: PreviewProvider {
    static var previews: some View {
        NotificationListView()
    }
}

 SwiftUI의 List에 .onDelete(perform:) 메서드를 사용하여 스와이프하여 삭제 기능을 구현하고, navigationBarItems를 사용하여 알림 추가 기능을 구현했다. 실제 애플리케이션에서는 Delegate 등에서 Push 이벤트 발생시 알림 데이터를 동적으로 추가할 수 있도록 구성한다.