Reputation: 928
I come from a react-native
background and I am having trouble understanding a specific part of MVVM in SwiftUI: having multiple views of the same entities.
I am building a simple social media app with posts. These posts can be viewed on the Homepage
, and on the Profile
page of any user.
This is what the view models look like
struct HomepagePosts: ObservableObject {
@Published var posts = [Post]()
... rest of logic
}
struct ProfilePosts: ObservableObject {
@Published var posts = [Post]()
... rest of logic
}
My problem is the following. If a user just posted a post, that post will show up on the homepage and on his profile. What if the user decides to update the post? How will the post be updated in all places?
In React this is done by normalizing state. Instead of keeping separate posts
arrays, you keep 2 arrays of postId
s, and one dictionary with the postId
as key and the entire Post
object as the value. That way, updating the post updates it in one place, and every view updates directly.
How do you do this in SwiftUI?
Upvotes: 4
Views: 1356
Reputation: 1435
Here's some code to illustrate the comment above. I believe you want to structure it something like this, with only one model object.
struct Post: Identifiable {
let id: String
let text: String
}
class Model: ObservableObject {
@Published var posts: [Post] = []
}
@main
struct TestApp: App {
// @StateObject var model = Model()
var body: some Scene {
WindowGroup {
Homepage()
// .environmentObject(model)
}
}
}
struct Homepage: View {
// @EnvironmentObject var model: Model
@StateObject var model = Model() // holds array of homepage posts
@State private var newPost: String = ""
@State private var showProfile = false
var body: some View {
VStack {
TextField("new post", text: $newPost) { _ in
} onCommit: {
model.posts.append(Post(id: UUID().uuidString, text: newPost))
newPost = ""
}
Divider()
ScrollView {
ForEach(model.posts) { post in
Text(post.text).padding()
}
}
Divider()
Button("Profile") {
showProfile = true
}
.sheet(isPresented: $showProfile) {
Profile()
}
}
.padding()
}
}
struct Profile: View {
// @EnvironmentObject var model: Model
@StateObject var model = Model() // holds array of profile posts
var body: some View {
Text("You have \(model.posts.count) posts")
}
}
Upvotes: 2
Reputation: 14935
struct
can not confirm to the ObservableObject
protocol. Only class
can do so. Therefore, HomepagePosts
and ProfilePosts
have to be of type class
.
You do not have to make two difference classes for posts. Use a single ObservableObject
for the data shared between the views.
struct Post: Identifiable {
let id: UUID = UUID()
let content: String
}
class PostModel: ObservableObject {
@Published var posts: [Post] = []
}
Pass a single instance of PostModel
to both HomepagePosts
and ProfilePosts
.
struct ContentView: View {
@ObservedObject var postModel: PostModel = PostModel()
var body: some View {
TabView {
ProfilePostsView(postModel: postModel)
.tabItem {
Label("Profile", systemImage: "person")
}
HomepageView(postModel: postModel)
.tabItem {
Label("Home", systemImage: "house")
}
}
}
}
All the code ready for test.
struct Post: Identifiable {
let id: UUID = UUID()
let content: String
}
class PostModel: ObservableObject {
@Published var posts: [Post] = []
}
struct ContentView: View {
@ObservedObject var postModel: PostModel = PostModel()
var body: some View {
TabView {
ProfilePostsView(postModel: postModel)
.tabItem {
Label("Profile", systemImage: "person")
}
HomepageView(postModel: postModel)
.tabItem {
Label("Home", systemImage: "house")
}
}
}
}
struct ProfilePostsView: View {
@ObservedObject var postModel: PostModel
var body: some View {
NavigationView {
VStack {
List(postModel.posts) { post in
Text(post.content)
}
}
}
}
}
struct HomepageView: View {
@ObservedObject var postModel: PostModel
var body: some View {
NavigationView {
VStack {
Button(action: {
self.postModel.posts.append(Post(content: "Hello \(Date())"))
}, label: {
Text("Add new Posts")
})
List(postModel.posts) { post in
Text( post.content)
}
}
}
}
}
In the code above, if you add new posts in HomepageView
, the same posts are automatically added in the second tab (ProfilePostsView
). They will be updated if you do so.
postModel
can also be a property of HomepagePosts
and ProfilePosts
.
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
Note: this approach is similar to Cenk Bilgen
Upvotes: 1