Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Combine - @Published property Array

I am currently doing a project using SwiftUI and Combine. I'm on Xcode11 Beta 5. I would like to fetch my Github repositories, display them and then be able to bookmark some of them.

I'm able to fetch them and display them. I'm using Combine with @Published property wrapper to update my views. To now, everything worked as expected.

So I moved on to the next step, bookmarking repositories. I would like to use Realm to persist these repositories.

My problem is that, I have an Observable class which have a @Published array of Repository. Repository is a decodable class. In my Repository class I have a simple class property isFavorite set to false for now.

In my repositories list, when I click on a repository to see details, I would like to have the possibility to bookmark it. So I retrieve its index by its id from my array of Repository and set its property isFavorite to true. But my view doesn't update. I mean I have a conditional rendering and it's stuck to false.

Here's my code :

import SwiftUI
import Combine

struct RepositoryList : View {
    @ObservedObject var store: Store

    func move(from source: IndexSet, to destination: Int) {
        store.repositories.swapAt(source.first!, destination)
    }

    @State private var isTapped = false
    @State private var bgColor = Color.white

    var body: some View {
        NavigationView {
            VStack {
                List {
                    Section(header: Text("\(String(store.repositories.count)) repositories")) {
                        ForEach(store.repositories) { repository in
                            NavigationLink(destination: RepositoryDetail(store: self.store, repository: repository)) {
                                RepositoryRow(repository: repository)
                            }.padding(.vertical, 8.0)
                        }.onDelete { index in
                            self.store.repositories.remove(at: index.first!)
                        }.onMove(perform: move)
                    }
                }
            }
        }.navigationBarTitle("Github user").navigationBarItems(trailing: EditButton())
    }
}

struct RepositoryDetail : View {
    @ObservedObject var store: Store
    var repository: Repository

    var repoIndex: Int {
        let repoIndex = store.repositories.firstIndex(where: {$0.id == repository.id})!
        print("### IS FAVORITE \(String(store.repositories[repoIndex].isFavorite))")
        return repoIndex
    }

    var body: some View {
        VStack(alignment: .leading) {
            Text(String(store.repositories[repoIndex].isFavorite))
            Button(action: { self.store.repositories[self.repoIndex].isFavorite.toggle() }) {
                if (self.store.repositories[self.repoIndex].isFavorite) {
                    Image(systemName: "star.fill").foregroundColor(Color.yellow)
                } else {
                    Image(systemName: "star").foregroundColor(Color.gray)
                }
            }
        }.navigationBarTitle(Text(repository.name), displayMode: .inline)
    }
}

class Store: ObservableObject {
    private var cancellable: AnyCancellable? = nil
    @Published var repositories: [Repository] = []

    init () {
        var urlComponents = URLComponents(string: "https://api.github.com/users/Hurobaki/repos")!
        var request = URLRequest(url: urlComponents.url!)
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        cancellable = URLSession.shared.send(request: request)
            .decode(type: [Repository].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                print("### .sink() received the completion", String(describing: completion))
                switch completion {
                case .finished:
                    break
                case .failure(_):
                   print("### ERROR")
                }
            }, receiveValue: { repositories in
                self.repositories = repositories
            })

    }
}

class Repository: Decodable, Identifiable {
    var id: Int = 0
    var name: String = ""
    var desc: String? = nil

    var isFavorite = false

    private enum CodingKeys: String, CodingKey {
        case id, name
        case desc = "description"
    }
}

I don't know why my Button component never display Image(systemName: "star.fill").foregroundColor(Color.yellow) When I print isFavorite property its value changes from true to false and vice versa but my view doesn't update.

I did the tutorial from Apple Developer where they do exactly the same thing, so I don't know why I have this error, what am I missing ?

Some help would be really appreciated and / or reviews on my code :)

Thank you

PS: In order not to post an even longer code I have uploaded it on Pastebin https://pastebin.com/zjDwQSGq

like image 972
Hurobaki Avatar asked Aug 04 '19 16:08

Hurobaki


1 Answers

thank you for posting your full code. It makes troubleshooting much easier.

Your problem is simple. You made Repository a class, and not a struct. That means when you changed .isFavorite, the object in the array does not change, because it is a reference type. Should you change and made Repository a struct instead, your code will work, because when you change .isFavorite you are really mutating a value and the Publisher will catch it.

If you don't know the difference between reference and value types, check this page: https://developer.apple.com/swift/blog/?id=10 that explains it in detail. But remember, classes are reference types and structs are value types.

If you still want to keep Repository a class, you can do it, but you will need to manually call send on the objectWillChange subject of your store:

Button(action: {
                self.store.objectWillChange.send()
                self.store.repositories[self.repoIndex].isFavorite.toggle()
            }) {
                if (self.store.repositories[self.repoIndex].isFavorite) {
                    Image(systemName: "star.fill").foregroundColor(Color.yellow)
                } else {
                    Image(systemName: "star").foregroundColor(Color.gray)
                }
            }
like image 103
kontiki Avatar answered Oct 05 '22 00:10

kontiki