'Binding not working as expected when button is pressed

I have a button that's supplied with data and this is used to unfollow or follow a user. What should happen is that I press the button, it changes the isFollowing property on the user, and then the text updates from unfollow to follow. However, this doesn't work. Here's my code that can be put into a playground (simplified for the purposes of this just to show the core elements):

struct User: Hashable, Identifiable {
  let id = UUID()
  var isFollowing: Bool
}

final class MyModel: ObservableObject {
  @Published var users: [User] = [User(isFollowing: true)]

  func unfollow(_ user: Binding<User>) async throws {
    user.wrappedValue.isFollowing = false
  }
}

struct ContainerView: View {
  @EnvironmentObject private var model: MyModel

  var body: some View {
    UserListView(users: $model.users)
  }
}

final class PagedUsers: ObservableObject {
  @Published var loadedUsers: [Binding<User>] = []
  @Binding var totalUsers: [User]

  init(totalUsers: Binding<[User]>) {
    self._totalUsers = totalUsers
    let firstUser = $totalUsers.first!
    loadedUsers.append(firstUser)
  }
}

struct UserListView: View {
  @StateObject private var pagedUsers: PagedUsers

  init(users: Binding<[User]>) {
    self._pagedUsers = StateObject(wrappedValue: PagedUsers(totalUsers: users))
  }

  var body: some View {
    ForEach(pagedUsers.loadedUsers) { user in
      MyView(user: user)
    }
  }
}


struct MyView: View {
  @EnvironmentObject private var model: MyModel
  @Binding var user: User

  var body: some View {
    
    Button(
      action: {
        Task {
          do {
            try await model.unfollow($user)
          } catch {
            print("Error!", error)
          }
        }
      },
      label: {
        Text(user.isFollowing ? "Unfollow" : "Follow")
      }
    )
  }
}

PlaygroundPage.current.setLiveView(
  ContainerView()
    .environmentObject(MyModel())
)

I think it's not working because of something to do with the passing of bindings, but I can't quite work out why. Possibly it's the setup of PagedUsers? However, this needs to be there because in my app code I essentially pass all the user data to it, and return "pages" of users from this, which gets added to as the user scrolls.



Solution 1:[1]

I don't fully understand why you need two classes for users ... why not put them in one in different @Published vars?

IMHO then you don't need any bindings at all! Here is a working code with only one class, hopefully you can build from this:

import SwiftUI

@main
struct AppMain: App {
    
    @StateObject private var model = MyModel()
    
    var body: some Scene {
        WindowGroup {
            ContainerView()
                .environmentObject(model)
        }
    }
}


struct User: Hashable, Identifiable {
    let id = UUID()
    var isFollowing: Bool
}

class MyModel: ObservableObject {
    @Published var users: [User] = [User(isFollowing: true), User(isFollowing: true), User(isFollowing: true)]
    
    func unfollow(_ user: User) async throws {
        if let index = users.firstIndex(where: {$0.id == user.id}) {
            self.objectWillChange.send()
            users[index].isFollowing = false
        }
    }
}


struct ContainerView: View {
    @EnvironmentObject private var model: MyModel
    
    var body: some View {
        UserListView(users: model.users)
    }
}


struct UserListView: View {
    
    let users: [User]
    
    var body: some View {
        List {
            ForEach(users) { user in
                MyView(user: user)
            }
        }
    }
}


struct MyView: View {
    @EnvironmentObject private var model: MyModel
    var user: User
    
    var body: some View {
        
        Button(
            action: {
                Task {
                    do {
                        try await model.unfollow(user)
                    } catch {
                        print("Error!", error)
                    }
                }
            },
            label: {
                Text(user.isFollowing ? "Unfollow" : "Follow")
            }
        )
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 ChrisR