Darius Mandres
Darius Mandres

Reputation: 928

SwiftUI Keeping data up to date across views and view models

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 postIds, 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

Answers (2)

Cenk Bilgen
Cenk Bilgen

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

mahan
mahan

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.

More info:


https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

Note: this approach is similar to Cenk Bilgen

Upvotes: 1

Related Questions