[iOS] ViewModel에서 UI Framework를 다루어도 될까?

이정훈·2024년 5월 22일
0

iOS

목록 보기
4/6
post-thumbnail

이번 포스트에서는 MVVM, 그 중에서도 핵심인 ViewModel에 대해 필자가 고민 했었던 내용을 정리해보려고 한다. 또한 이 포스트에서 MVVM에 대한 전반적인 개념에 대한 내용을 담고 있지는 않다.

👀 빠르게 결론부터

시작에 앞서 결론부터 말하면 MVVMViewModelUI Framework에 대해 독립적이어야 한다. 쉽게 말해 ViewModel에서 import SwiftUI 혹은 import UIKit을 해서는 안된다는 것이다.

ViewModel에서 UI Framework를 가져온다고 에러가 발생하는 것은 아니지만 유지보수 측면에서 ViewModelUI Framework와 독립적이어야 한다고 생각한다.

ViewModelUI Framework에 대해 독립적인 경우 ViewUIKit으로 구현되어 있든, SwiftUI로 구현되어 있든 상관없이 ViewModel을 관리할 수 있으며, 다시 말해 관심사의 분리가 완벽하게 이루어질 수 있다. 즉, ViewView와 관련된 일만 하고 ViewModelView에 표현할 데이터만 잘 관리하면 되는 것이다.

ViewModelUI Framework에서 자유로워질 수 있다..!

잘못된 예시 (UIImage)

아래의 예시는 ViewModel에서 UI Framework와 관련된 데이터를 가지고 있는 간단한 예시이다.

import Combine
import SwiftUI

final class ViewModel: ObservableObject {
	@Published var image: UIImage?
}

필자도 처음에는 위의 코드를 보고 긴가민가 했다. 왜냐하면 애플 공식 문서에서 소개하는 UIImage 타입은 아래와 같이 소개 되어있기 때문이다.

UIImage

An object that manages image data in your app.

UIImage는 앱에서 이미지 데이터를 관리하는 객체라고 소개 되어 있다. 그래서 처음에는 '이미지 데이터를 관리하는 객체..? 그럼 이것 자체가 UI는 아니니까 상관 없지 않을까..?'라고 생각하면서도 상단에 import SwiftUI가 마음에 걸리는 것은 어쩔 수가 없었다.

하지만 여러 고민 끝에 내린 결론은 UI Framework의 레퍼런스를 받아와 데이터를 구성 혹은 변경 하는 것도 일종의 UI 관련 작업으로 간주하고 이것을 ViewModel과 분리하는 것이 맞다고 생각했다.

UI Framework와 분리

위의 코드에서 UIImageViewModel과 어떻게 분리할 수 있을까? 이것에 대한 대답으로 UIImage 클래스에는 아래와 같은 두 가지의 메서드가 존재한다.

extension UIImage {
	...
    
	public func pngData() -> Data?

	public func jpegData(compressionQuality: CGFloat) -> Data?
    
    ...
}

두 메서드는 모두 png 혹은 jpeg 포맷에 대한 Data? 타입을 반환한다.

따라서 위의 ViewModel을 아래와 같이 Data? 타입으로 변환해 줄 수 있고 이렇게 되면 UI Framework에 대해 독립적으로 구현할 수 있다.

import Combine
import Foundation

final class ViewModel: ObservableObject {
    @Published var imageData: Data?
}

그럼 View에서는 이 Data? 타입의 객체를 어떻게 활용할 수 있을까?

import SwiftUI

struct ContentView: View {
    @ObservedObject private var viewModel: ViewModel = ViewModel()
    @State private var isShowingSheet: Bool = false
    
    var body: some View {
        VStack {
            if let imageData = viewModel.imageData,
               let uiImage = UIImage(data: imageData) {
                Image(uiImage: uiImage)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
            
            Spacer()
            
            Button(action: {
                isShowingSheet.toggle()
            }, label: {
                Text("이미지 선택")
                    .padding(8)
            })
            .buttonBorderShape(.roundedRectangle)
            .cornerRadius(20)
            .buttonStyle(.borderedProminent)
        }
        .padding()
        .sheet(isPresented: $isShowingSheet) {
            ImagePicker()
                .environmentObject(viewModel)
        }
    }
}

위의 코드에서 볼 수 있듯이 View에서는 UIImage(data:)를 사용하여 ViewModelData? 타입 객체를 통해 UIImage 타입의 객체를 생성할 수 있고 다시 이 UIImage 타입의 객체는 Image(uiImage:)로 전달하여 View를 그릴 수 있게 된다.

추가적으로 이미지를 가져오는 단계에서는 위에서 언급한 pngData(), jpegData(compressionQuality:)를 통해 Data? 타입으로 변환해 주면 된다.

import UIKit
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @EnvironmentObject private var viewModel: ViewModel
    @Environment(\.dismiss) private var dismiss
    
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
        let imagePicker = UIImagePickerController()
        imagePicker.sourceType = .photoLibrary
        imagePicker.delegate = context.coordinator
        
        return imagePicker
    }
        
    func updateUIViewController(_ uiViewController: UIImagePickerController,
                                context: UIViewControllerRepresentableContext<ImagePicker>) {}
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(imagePicker: self)
    }
}

//MARK: - Coordinator
extension ImagePicker {
    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        private var imagePicker: ImagePicker

        init(imagePicker: ImagePicker) {
            self.imagePicker = imagePicker
        }
        
        //MARK: - Delegate method executed after Image Selection
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
                imagePicker.viewModel.imageData = image.pngData()    //UIImage에서 Data? 타입으로 변경
            }
            
            imagePicker.dismiss()    //사진 선택 후 View 닫음
        }
    }
}

ImagePicker를 만드는 방법은 해당 게시물 참고.

Reference

https://jisoo.net/2018/12/09/what-is-mvvm.html
https://codekodo.tistory.com/211

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글