Simple MVVM in SwiftUI (with async await network request)

For developers who are too busy to read, I will briefly demonstrate how to develop a SwiftUI app using the MVVM pattern.

After creating a SwiftUI project in Xcode, you will usually get two Swift files, one with the same name as your project and the other one is generally called ContentView (may change over time, but currently it is still called ContentView).

Next, let’s create a View Model for ContentView.

Create a new Swift file and name it ContentViewViewModel, and remember to import SwiftUI at the top. Your code should look like this for now:

1
import SwiftUI

Now comes the highlight of the article. Because I want to create a View Model for ContentView instead of other Views, I can use a Swift feature called extension to extend ContentView, so that I can use what I am going to write in ContentView.

1
2
3
4
5
6
7
8
import SwiftUI

extension ContentView {
@MainActor
class ViewModel: ObservableObject {
@Published var greetings = "Greetings!"
}
}

Go back to ContentView and rewrite it like this:

1
2
3
4
5
6
7
8
9
10
11
import SwiftUI

struct ContentView: View {
@StateObject private var viewModel = ViewModel()

var body: some View {
VStack {
Text(viewModel.greetings)
}
}
}

Great, we have already implemented the simplest MVVM. However, I am not going to end it here. I want to add the function of network requests to ContentViewViewModel to better meet the actual development needs.

Here, I will use a Mock API to simulate some data, which can be found at https://jsonplaceholder.typicode.com/.

I will use the address https://jsonplaceholder.typicode.com/posts, which returns a structure similar to this:

1
2
3
4
5
6
7
8
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
]

I will create the model in ContentViewViewModel, which makes it more convenient. In actual cases, it is recommended to create a separate file to build the model.

1
2
3
4
struct Post: Decodable, Identifiable {
let userId, id: Int
let title, body: String
}

Then, let’s write a function for requesting data in ContentViewViewModel, and call it in init() to achieve automatic request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import SwiftUI

extension ContentView {
@MainActor
class ViewModel: ObservableObject {
@Published var posts: [Post] = []

init() {
Task {
do {
try await getPosts()
} catch {
print(error) // If there are other errors that do not cause a crash, print them here.
}
}
}

func getPosts() async throws {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
fatalError() // If the URL is problematic, the app will crash here. NOT RECOMMENDED IN ACTUAL DEVELOPMENT.
}

let (data, _) = try await URLSession.shared.data(from: url)
let posts = try JSONDecoder().decode([Post].self, from: data)

self.posts = posts
}
}
}

struct Post: Decodable, Identifiable {
let userId, id: Int
let title, body: String
}

Finally, go back to ContentView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI

struct ContentView: View {
@StateObject private var viewModel = ViewModel()

var body: some View {
NavigationView {
if !viewModel.posts.isEmpty {
List(viewModel.posts) { post in
Text(post.title)
}
.navigationTitle("Posts")
}
}
}
}

Mission accomplished, now you already have some concepts about developing SwiftUI App using MVVM pattern.