MVVM architecture

Updated:

🔷 MVVM

image

위의 그림으로 보면 예전부터 지금까지 많이 쓰여왔던 MVC (Model View Controller) 유사한 점이 있지만, 다른 점이 분명히 있습니다.

  • M(Model) : 앱의 데이터와 비지니스 로직을 캡슐화 합니다. 뷰와 독립적이여야 합니다

  • V(View) : 플렛폼에서 제공하는 시각적 요소들을 사용해 UI 를 정의합니다

  • VM(ViewModel) : 뷰와 모델 사이의 중재자로서 프로젠테이션 로직을 정의하고, 뷰에 모델의 데이터를 제공하며모델에는 업데이트를 요청합니다

🔶 MVC vs MVVM

뷰와 그 외의 요소간 의존성을 제거하는 데 집중하였다는 점이 큰 차이접 입니다. MVC 에서는 뷰 컨트롤러가 뷰를 소유하고 있고 직접 뷰에 데이터를 넘겨주거나 어떤 동작을 하도록 지시하기 때문에 아주 밀접한 연관이 되어 있습니다.

하지만, MVVM 은 단지 중개자 (전달자) 역활만 하는 뷰 모델이 뷰에 대해서 전혀 알지 못하고 View 는 UI 에 독립적인 코드를 작성할 수 있습니다

만역 뷰 와 연결된 데이터는 데이터 바인딩을 통해 해결합니다.

🔶 Model

모델은 MVC 에서의 모델과 동일한 역활을 합니다. 실제 앱이 처리하려는 문제와 관련된 비지니스 로직을 정의하며 데이터를 캡슐화 하고 저장합니다. 뷰나 뷰 모델에 영향을 받거나 그 존재에 관심을 가지지 않고 독자적으로 구성됩니다. 또한, 모델은 사용자에게 앱이 보이는 모습이나 제공 방식과 관련이 없어 특정한 앱 플랫폼에 한정되어 잇지 않고 다른 곳에서도 동일하게 사용할 수 잇습니다

🔶 View

뷰 역시 개념적으로는 MVC 의 역활과 동일합니다. 예를들어 버튼, 텍스트필드처럼 사용자와 상호 작용하는 컨트롤이나, 색, 글꼴 등 시각적 요소들을 포함해 사용자가 볼 수 있는 모든 요소를 이용해 UI 를 정의합니다. 사용자의 버튼 터치와 같은 이벤트가 발생했을 때 뷰가 아닌 뷰 컨트롤러가 처리한거 처럼 MVVM 에서도 뷰는 뷰 모델에 이벤트 발생 사실만 전달하면 됩니다. 뷰는 모델의 데이터를 직접 사용하거나 수정할 수 없고 뷰 모델에만 접근 가능합니다.

🔑 MVVM 의 View 와 MVC 에서의 View 의 차이점

  • MVVM 은 HTML 같은 언어를 고려하여 설계되었던 만큼 SwiftUI 와 같이 선언형 방식으로 뷰를 정의합니다

  • MVC 에서 뷰 컨트롤러가 뷰에 데이터를 전달해야 했던 것과 달리, MVVM 에서는 뷰가 직접 뷰 모델을 참조하여 필요한 값을 능동적으로 요청합니다. 바인딩 된 뷰 모델의 값이 변화하면 자동으로 뷰의 값이 함께 갱신되며 반대의 경우도 마찬가지입니다. MVVM 은 이 데이터 바인딩 구조에 의존하므로 어떤 식으로든 플렛폼에서 바인딩 형태의 기능을 제공할 수 있어야만 사용할 수 있습니다.

🔶 ViewModel

뷰 모델은 뷰에 대한 추상화와 데이터 바인딩을 위한 모델 구체화 역활을 수행합니다. 뷰 모델은 모델의 데이터를 참조하여 뷰에 적합한 형태로 바꿔주는 변환기 (Converter) 역활을 수행하고, 뷰는 모델에 직접 접근하는 대신 뷰 모델의 속성에 바인딩 합니다. 이때 뷰 모델의 값은 모델의 값과 완전히 동일할 수도 그렇지 않을 수도 있지만, 뷰는 이사실을 알수 없으며 뷰 모델이 가진 속성이 뷰가 표현할 수 있는 전부입니다. 이것이 뷰모델이라고 부르는 이유입니다.

또한, 뷰 모델은 뷰의 상태를 저장하고, 뷰에서 발생하는 액션에 따라 수행할 앱의 기능을 정의하는 명령(Commands)을 구현합니다. 이것은 모델의 데이터를 업데이트하거나 뷰 모델의 값을 변경하는 것일 수 있습니다. 그리고 뷰는 바인딩하고 있는 값의 변경을 인지한 뒤 해당 기능이 어떻게 표현될지를 정합니다. 이때 뷰 모델은 뷰가 무엇인지, 어떤 모습이 될지에 대해서는 전혀 알지 못합니다.

🔶 MVVM 장단점

👉 장점

  • 이미 다른곳에서 사용중이거나 변경하기 어려운 기좀 모델을 사용해야 할때, 모델에 대한 수정 없이도 어댑터 역활을 하는 뷰 모델을 통해 뷰에서 활용 할 수 있습니다

  • 개발 프로세스에서 뷰와 앱의 로직에 대해 독립적으로 동시에 작업할 수 있습니다. 디자이너는 뷰에 집중하고 개발자는 뷰 모델 및 모델에 대해서만 작업할 수 있게 나눠서 작업을 진행 할 수 잇습니다.

  • MVVM 을 사용하면 UI 코드를 분리해 순수하게 앱의 기능과 비지니스 로직에 집중해서 테스트를 진행 할 수 있습니다

👉 단점

  • 명령형과 비교해 데이터 바인딩으로 연결된 선언형 방식의 프로그래밍은 디버깅이 까다롭습니다

  • 적절하게 일반화된 뷰 모델을 초기 설계하기가 쉽지 않습니다

  • 모델의 데이터를 뷰 모델에서 다시 작성함에 따라 추가 코드들이 발생 합니다

🔶 @ObservedObject, ObservableObject 을 사용해서 MVVM 사용 예시

// MARK: -  Model
struct FruitModel: Identifiable {
	let id: String = UUID().uuidString
	let name: String
	let count: Int
}


// MARK: -  ViewModel
// class know observing what is happening within this view model
class FruitViewModel: ObservableObject {
	// publish property weapper : the same thing as the at state except it's within a class
	// Notice somthing changed you miught have to update something
	@Published var fruitArray: [FruitModel] = []
	@Published var isLoading: Bool = false

	// MARK: -  FUNCTION

	func getFruits() {
		let fruit1 = FruitModel(name: "Orange", count: 1)
		let fruit2 = FruitModel(name: "Banana", count: 2)
		let fruit3 = FruitModel(name: "Watermelon", count: 88)

		// able ProgressView()
		isLoading = true
		// 3 sec delay
		DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
			self.fruitArray.append(fruit1)
			self.fruitArray.append(fruit2)
			self.fruitArray.append(fruit3)
			self.isLoading = false
		}

	}

	func deleteFruit(index: IndexSet) {
		fruitArray.remove(atOffsets: index)
	}
}

// MARK: -  View
struct ViewModel: View {
	// MARK: -  PROPERTY

	// @State var fruitArray : [FruitModel] = []
	// @State 을 대신해서 @Published 된 state 를 가져다 와서 쓰는방법 : @ObservedObject
	@ObservedObject var fruitViewModel: FruitViewModel = FruitViewModel()

	// MARK: -  BODY
var body: some View {
NavigationView {
List {
if fruitViewModel.isLoading {
ProgressView()
} else {

ForEach(fruitViewModel.fruitArray) { fruit in
  HStack {
    Text("\(fruit.count)")
      .foregroundColor(.red)
    Text(fruit.name)
      .font(.headline)
      .bold()
  }
}
.onDelete (perform: fruitViewModel.deleteFruit)
}
}
.listStyle(GroupedListStyle())
.navigationTitle("Fruit List")
.onAppear {
fruitViewModel.getFruits()
}
}
}
}

스크린샷

🔶 @StateObject 사용해서 MVVM 사용 예시

  • @ObservedObject has the downside that if this view gets recreated. so if it gets refreshed for whatever reason maybe there are some animations like there’s something else going on the app that just causes this view to reload

  • Observed object would also reload and that’s just how the observed object is made. So persis stored data like model data could be better on the app to use anther property wrapper use that is called at @StateObject

  • @StateObject ist the same thing as an observable object except basically if this view reloads if it re-renders but to use @StateObject it will not refresh

  • This is better for most cases when the view model is holding all of data because that underlying data is not really

// MARK: -  ViewModel
// class know observing what is happening within this view model
class FruitViewModel: ObservableObject {
	// publish property weapper : the same thing as the at state except it's within a class
	// Notice somthing changed you miught have to update something
	@Published var fruitArray: [FruitModel] = []
	@Published var isLoading: Bool = false

	init() {
		getFruits()
	}

	// MARK: -  FUNCTION

	func getFruits() {
		let fruit1 = FruitModel(name: "Orange", count: 1)
		let fruit2 = FruitModel(name: "Banana", count: 2)
		let fruit3 = FruitModel(name: "Watermelon", count: 88)

		// able ProgressView()
		isLoading = true
		// 3 sec delay
		DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
			self.fruitArray.append(fruit1)
			self.fruitArray.append(fruit2)
			self.fruitArray.append(fruit3)
			self.isLoading = false
		}

	}

	func deleteFruit(index: IndexSet) {
		fruitArray.remove(atOffsets: index)
	}
}

// MARK: -  View
struct ViewModel: View {
	// MARK: -  PROPERTY

	// @State var fruitArray : [FruitModel] = []
	// @State 을 대신해서 @Published 된 state 를 가져다 와서 쓰는방법 : @ObservedObject

	// @StateObject -> Use this on creation / init
	// @ObservedObject -> Use this for subViews
	@StateObject var fruitViewModel: FruitViewModel = FruitViewModel()

	// MARK: -  BODY
	var body: some View {
	NavigationView {
List {
if fruitViewModel.isLoading {
ProgressView()
} else {

ForEach(fruitViewModel.fruitArray) { fruit in
HStack {
	Text("\(fruit.count)")
		.foregroundColor(.red)
	Text(fruit.name)
		.font(.headline)
		.bold()
}
}
.onDelete (perform: fruitViewModel.deleteFruit)
}
}
.listStyle(GroupedListStyle())
.navigationTitle("Fruit List")
.navigationBarItems(trailing:
	NavigationLink(
		destination: SecondScreen(fruitViewModel: fruitViewModel),
		label: {
			Image(systemName: "arrow.right")
			.font(.title)
		})

)
}
}
}

// MARK: -  SecondScreen
struct SecondScreen: View {

@Environment(\.presentationMode) var presentationMode
// subView to use @ObservedObject
@ObservedObject var fruitViewModel: FruitViewModel

var body: some View {
ZStack {
	Color.green.ignoresSafeArea()

	VStack {
		ForEach(fruitViewModel.fruitArray) { fruit in
			Text(fruit.name)
				.foregroundColor(.white)
				.font(.headline)
		}
	}
} //: ZSTACK
}
}

스크린샷

🔶 @EnvironmentObject 사용해서 MVVM 사용 예시

// MARK: -  ViewModel
class EnvironmentViewModel: ObservableObject {
	@Published var dataArray: [String] = []

	init() {
		getData()
	}

	func getData() {
		self.dataArray.append(contentsOf: ["iPhone", "iPad", "iMac", "Apple Watch"])
	}
}

// MARK: -  View
struct EnvironmentObjectBootCamp: View {

	// MARK: -  Property
	// viewModel initializing
	@StateObject var viewModel: EnvironmentViewModel = EnvironmentViewModel()

var body: some View {
NavigationView {
List {
ForEach(viewModel.dataArray, id: \.self) { item in
	NavigationLink(
		destination: DetailView(selectedItem: item),
		label: {
			Text(item)
		})
}
}
.navigationTitle("iOS Devices")
}
.environmentObject(viewModel)
}
}

struct DetailView: View {

let selectedItem: String

var body: some View {
ZStack {
// background
Color.orange.ignoresSafeArea()

// foreground
NavigationLink(
destination: FinalView(),
label: {
	Text(selectedItem)
		.font(.headline)
		.foregroundColor(.orange)
		.padding()
		.padding(.horizontal)
		.background(Color.white)
		.cornerRadius(30)
})
} //: ZSTACK
}
}


struct FinalView: View {

	@EnvironmentObject var viewModel: EnvironmentViewModel

	var body: some View {
ZStack {
	// background
	LinearGradient(
		gradient: Gradient(colors: [Color(#colorLiteral(red: 0.09019608051, green: 0, blue: 0.3019607961, alpha: 1)), Color(#colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1))]),
		startPoint: .top,
		endPoint: .bottomTrailing)
		.ignoresSafeArea()

	// foreground
	ScrollView {
		VStack (spacing: 20) {
			ForEach(viewModel.dataArray, id: \.self) { item in
				Text(item)
			}
		} //: VSTACK
		.foregroundColor(.white)
		.font(.largeTitle)
	} //: SCROLL
} //: ZSTACK
}

스크린샷


🗃 Reference

Implementing Clean MVVM With SwiftUI - https://betterprogramming.pub/implementing-clean-mvvm-with-swiftui-10c52d503d67

How to use @ObservableObject and @StateObject in SwiftU - https://www.youtube.com/watch?v=-yjKAb0Pj60&list=PLwvDm4VfkdphqETTBf-DdjCoAvhai1QpO&index=51

스윗한 SwiftUI - https://book.jacobko.info/#/book/1190014815

Categories:

Updated:

Leave a comment