Search code examples
swiftswiftuirealm

Realm Swift - collection changes listener in SwiftUI


I'm using Realm in my SwiftUI project to store some of my data. Currently I store there ConversationObject and SettingObject types. Each of objects have it's corresponding Repository

(e.g. ConversationsRepository):

final class ConversationsRepository: ConversationsRepositoryProtocol {
    @Inject private var realmManager: RealmManaging
    
    init() {}
    
    func getConversations() -> AnyPublisher<[Conversation], Error> {
        realmManager.objects(ofType: ConversationObject.self)
            .map { $0.map({ $0.toModel }) }
            .eraseToAnyPublisher()
    }

    [...]
}

which utilises generic RealmManager

class RealmManager: RealmManaging {
    let realm = try? Realm()
    private let realmQueue: DispatchQueue = DispatchQueue(label: "realmQueue")
    private let updatePolicy: Realm.UpdatePolicy = .modified
    
    private enum RealmError: Error {
        case realmConstructionError
    }
    
    private init() {}
    
    static let shared: RealmManaging = {
        RealmManager()
    }()
    
    func objects<T: Object>(ofType: T.Type) -> AnyPublisher<[T], Error> {
        Future { promise in
            self.realmQueue.async {
                do {
                    let realm = try Realm()
                    
                    if realm.isInWriteTransaction || realm.isPerformingAsynchronousWriteOperations {
                        try realm.commitWrite()
                    }
                    
                    let objects = realm.objects(T.self)
                    
                    promise(Result.success(objects.compactMap { $0 }))
                } catch {
                    promise(Result.failure(error))
                }
            }
        }
        .eraseToAnyPublisher()
    }

    [...]
}

In my project every View

struct ChatView: View {
    @StateObject private var chatViewModel: ChatViewModel = ChatViewModel()

    [...]
}

has it's ViewModel which manages Repository data and passes it to corresponding View

final class ChatViewModel: ObservableObject {
    @Inject private var conversationsRepository: ConversationsRepositoryProtocol
    
    @Published var currentConversation: Conversation?
    @Published var conversations: [Conversation] = []
    
    private var cancelBag: Set<AnyCancellable> = Set<AnyCancellable>()
    
    init() {
        fetchConversations()
    }
    
    func fetchConversations() {
        conversationsRepository.getConversations()
            .receive(on: DispatchQueue.main)
            .sink { _ in
            } receiveValue: { [weak self] fetchedConversations in
                guard let self = self,
                      !fetchedConversations.isEmpty else {
                    self?.addNewConversation()
                    return
                }
                
                self.conversations = fetchedConversations
                self.sortConversations()
                self.currentConversation = self.conversations.last
            }
            .store(in: &cancelBag)
    }
}

My problem is I have ChatView which displays data for ConversationObject collection, but I have SettingsView in which I can delete all ConversationObjects. Both views' viewmodels use ConversationRepository to do their stuff. But I want the data in ChatViewModel reload when I delete ConversationObject data in SettingsView

I want to create observers that I could use in various ViewModels so that they could have latest data for my Realm collections and react to changes mutating their views. I know that I could just simply use Realm object inside my ViewModels and create Realm Notification Token there, but I want to avoid using Realm object explicitly in my ViewModels, would prefer to do indirectly by calling some repository function which later calls generic RealmManager function, unless it is not possible.


Solution

  • I have decided to go with Realm's built-in features that are created especially for SwiftUI. Therefore in every view which ViewModel's should have Realm's data I implemented

    @ObservedResults(ConversationObject.self) var conversationsObjects
    

    It is then passed to ViewModel with modifier

    .onChange(of: conversationsObjects) { _, updatedConversationsObjects in
        chatViewModel.getConversations(from: updatedConversationsObjects)
    }
    

    Later, the method getConversations converts it from Realm Object to my other structure that is assigned to @Published var and this data is displayed on screen

    The same technique is applied to SettingsObjects, SettingsView and SettingsViewModel respectively. By using this approach I was able to get automatically refreshing Realm data I want.