Remember that a view should be dumb. It doesn't care what it displays. The notes view doesn't match that description. It can access the array of notes through its view model. That is a code smell and something we need to change. You learn how to do that in this episode.
A Protocol-Oriented Approach
We start with a protocol to hide the Note object behind. Create a group with name Protocols and add a Swift file with name NotePresentable.swift. Declare a protocol with name NotePresentable.
import Foundation
protocol NotePresentable {
}
The protocol defines two properties of type String, title and contents.
import Foundation
protocol NotePresentable {
// MARK: - Properties
var title: String { get }
var contents: String { get }
}
Open Note.swift and conform the Note struct to the NotePresentable protocol. We don't need to make any changes to the Note struct because it automatically conforms to the NotePresentable protocol.
import Foundation
struct Note: Decodable, Identifiable, NotePresentable {
// MARK: - Properties
let id: Int
let title: String
let contents: String
}
Open NotesViewModel.swift. We change the type of the notes property to array of NotePresentable objects, [NotePresentable].
@Published private(set) var notes: [NotePresentable] = []
Build and run the application. The compiler doesn't seem to agree with the changes we made. The elements a List view displays need to conform to the Identifiable protocol. The Note struct conforms to the Identifiable protocol, but a NotePresentable object doesn't meet that requirement.

I won't explain why the compiler throws an error. That would result in a lengthy discussion about protocols and associated types. That's a topic for another episode. We can resolve the compiler error by wrapping the Note object in a view model.
Creating Another View Model
Revisit the Note struct and remove the NotePresentable protocol from the Note declaration. That's no longer necessary.
import Foundation
struct Note: Decodable, Identifiable {
// MARK: - Properties
let id: Int
let title: String
let contents: String
}
Add a Swift file with name NoteViewModel.swift to the Views group. Declare a struct with name NoteViewModel that conforms to the Identifiable protocol.
import Foundation
struct NoteViewModel: Identifiable {
}
The NoteViewModel struct defines a private, constant property note of type Note. Because we declare the property privately, we don't get a memberwise initializer for free. We declare an initializer that takes a Note object as its only argument, assigning it to the note property.
import Foundation
struct NoteViewModel: Identifiable {
// MARK: - Properties
private let note: Note
// MARK: - Initialization
init(note: Note) {
self.note = note
}
}
To conform to the Identifiable protocol, we need to expose a property with name id. The property returns the identifier of the Note object.
import Foundation
struct NoteViewModel: Identifiable {
// MARK: - Properties
private let note: Note
// MARK: -
var id: Int {
note.id
}
// MARK: - Initialization
init(note: Note) {
self.note = note
}
}
Before we put the NoteViewModel struct to use, we conform NoteViewModel to the NotePresentable protocol in an extension. Conforming to NotePresentable is straightforward. The title property returns the note's title and the contents property returns the note's contents.
extension NoteViewModel: NotePresentable {
var title: String {
note.title
}
var contents: String {
note.contents
}
}
Revisit NotesViewModel.swift and change the type of the notes property to an array of NoteViewModel objects, [NoteViewModel].
@Published private(set) var notes: [NoteViewModel] = []
This also means we need to map the result of the fetchNotes() method to an array of NoteViewModel objects. We invoke the map(_:) method on the array of Note objects, passing in a reference to the initializer of the NoteViewModel struct.
func start() {
Task {
do {
notes = try await apiService.fetchNotes()
.map(NoteViewModel.init)
} catch {
print("Unable to Fetch Notes \(error)")
}
}
}
Finishing Touches
Before we end this episode, I would like to apply a few finishing touches to the implementation. We start by renaming the notes property of the NotesViewModel class to noteViewModels. That's a more accurate description and avoids confusion.
@Published private(set) var noteViewModels: [NoteViewModel] = []
We also update the property's name in the start() method and in the computed body property of the NotesView struct.
func start() {
Task {
do {
noteViewModels = try await apiService.fetchNotes()
.map(NoteViewModel.init)
} catch {
print("Unable to Fetch Notes \(error)")
}
}
}
var body: some View {
NavigationView {
List(viewModel.noteViewModels) { note in
NoteView(
title: note.title,
contents: note.contents
)
}
.navigationTitle("Notes")
}
.onAppear {
viewModel.start()
}
}
Open NoteView.swift and replace the title and contents properties with a property with name presentable of type NotePresentable. We also need to update the body of the computed body property.
struct NoteView: View {
// MARK: - Properties
let presentable: NotePresentable
// MARK: - View
var body: some View {
VStack(alignment: .leading) {
Text(presentable.title)
.font(.title)
Text(presentable.contents)
.font(.body)
}
}
}
These changes break the preview, but the fix is easy. We declare a private, nested struct with name PreviewPresentable that conforms to NotePresentable. The PreviewPresentable struct defines a computed title property and a computed contents property to satisfy the requirements of the NotePresentable protocol. We update the initializer of the NoteView struct in the static previews property, passing in a PreviewPresentable object.
struct NoteView_Previews: PreviewProvider {
private struct PreviewPresentable: NotePresentable {
// MARK: - Properties
var title: String {
"My Note"
}
var contents: String {
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}
}
static var previews: some View {
NoteView(presentable: PreviewPresentable())
}
}
Revisit NotesView.swift one more time and update the initializer of the NoteView struct in the computed body property of NotesView.
var body: some View {
NavigationView {
List(viewModel.noteViewModels) { presentable in
NoteView(presentable: presentable)
}
.navigationTitle("Notes")
}
.onAppear {
viewModel.start()
}
}
What's Next?
None of the views have direct access to the model layer and that is a welcome change. Because the view model is injected into the notes view, we can carefully control what data the preview displays and where that data comes from. The NotePresentable protocol isn't strictly necessary, but I typically choose for a protocol if a view does nothing but display data. The note view is a fine example of that.
The Notes application summarizes what the Model-View-ViewModel pattern is about and how we can extract business logic from the view and move it to the view model. We continue to apply this pattern in the remainder of this series.