Search code examples
swiftrealmalamofirerx-swift

Observe JSON response if changed from already saved in Database(Realm)


Building an App which is using Alamofire and Realm for Network call and data storage respectively with help of RxSwift. Every thing is working fine but the need of the day is to prevent always refresh views on network call. For now app's behaviour is like I replicate JSON response to DB and then update views from DB. But to always get latest response app needs to call Network API on every viewWillAppear. But I don't want to get all DB data and search if there is something changed from new response and then display it. So is there any thing in Swift or Alamofire or Realm that I can observe on if the data is different from previously loaded in database and then only app will update its view.

                self?.notificationToken = Database.singleton.fetchStudentsForAttendence(byCLassId: classId).observe { [weak self] change in
                switch change {
                case .initial(let initial):
                    TestDebug.debugInfo(fileName: "", message: "INIT: \(initial)")
                    self!.viewModel.getStudentsData(id: classId)
                case .update(_, let deletions, let insertions, let modifications):
                    print(modifications)
                    TestDebug.debugInfo(fileName: "", message: "MODIFY: \(modifications)")
                    TestDebug.debugInfo(fileName: "", message: "MODIFY: \(insertions)")
                case .error(let error):
                    TestDebug.debugInfo(fileName: "", message: "ERROR:\(error)")
                }
            }

this is how I am observing data now, but As I every time save response into database when I call the API, and I am using case .initial to monitor that and as database always have been refreshed and this block calls every time. I need something that monitors that data value changed in DB. Is there something in Realm for that ?

Link to GIF

Okay I am doing it like this, there is a viewController in which i have a container view which has Collection view as a child View.

private lazy var studentsViewController: AttandenceView = {
    let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
    var viewController = storyboard.instantiateViewController(withIdentifier: "attendenceCV") as! AttandenceView
    self.add(asChildViewController: viewController, to: studentsView)
    return viewController
}()  //this way I am adding collectionView's container view.

here is the ViewModel code from where I am geting my data and creating an observable for CollectionView.

class AttendenceVM {

    //MARK: Properties
    let disposeBag = DisposeBag()
    let studentCells = BehaviorRelay<[StudentModel]>(value: [])

    var studentCell : Observable<[StudentModel]> {
        return studentCells.asObservable().debug("CELL")
    }

    var notificationToken : NotificationToken? = nil

    deinit {
        notificationToken?.invalidate()
    }
    func getStudentsData(id: Int) {
        let studentsData = (Database.singleton.fetchStudentsForAttendence(byCLassId: id))
        self.notificationToken = studentsData.observe{[weak self] change in
            TestDebug.debugInfo(fileName: "", message: "Switch:::: change")
            switch change {
            case .initial(let initial):
                TestDebug.debugInfo(fileName: "", message: "INIT: \(initial)")
                self!.studentCells.accept(Array(studentsData))
            case .update(_, let deletions, let insertions, let modifications):
                TestDebug.debugInfo(fileName: "", message: "MODIF::: \(modifications)")
                self!.studentCells.accept(Array(studentsData))
            case .error(let error):
                print(error)
            }
        }
        //self.studentCells.accept(studentsData)
    }
}

then I am population collectionView in its class separately, by doing this.

class AttandenceView: UIViewController, UICollectionViewDelegateFlowLayout {

    //MARK: - Outlets
    @IBOutlet weak var studentsView: UICollectionView!

    let studentCells = BehaviorRelay<[StudentModel]>(value: [])
    let scanStudentCells = BehaviorRelay<[ClassStudent]>(value: [])



    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        let flowLayout = UICollectionViewFlowLayout()
        let size = CGSize(width: 105, height: 135)
        flowLayout.itemSize = size
        studentsView.setCollectionViewLayout(flowLayout, animated: true)
        studentsView.rx.setDelegate(self).disposed(by: disposeBag)
        setupBinding()
    }

    func setupBinding() {

        studentsView.register(UINib(nibName: "StudentCVCell", bundle: nil), forCellWithReuseIdentifier: "studentCV")


            //Cell creation
            scanStudentCells.asObservable().debug("Cell Creation").bind(to: studentsView.rx.items(cellIdentifier: "studentCV", cellType: StudentCVCell.self)) {
                (row , element, cell) in
                if (element.attandance == 1 ) {
                    // update view accordingly
                } else if (element.attandance == 0) {
                    // update view accordingly
                } else if (element.attandance == 2) {
                     // update view accordingly  

                }
                cell.viewModel2 = element
                }.disposed(by: disposeBag)

            //Item Display
            studentsView.rx
                .willDisplayCell
                .subscribe(onNext: ({ (cell,indexPath) in
                    cell.alpha = 0
                    let transform = CATransform3DTranslate(CATransform3DIdentity, -250, 0, 0)
                    cell.layer.transform = transform
                    UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .curveEaseOut, animations: {
                        cell.alpha = 1
                        cell.layer.transform = CATransform3DIdentity
                    }, completion: nil)
                })).disposed(by: disposeBag)

            // item selection with model details.
            Observable
                .zip(
                    studentsView
                        .rx
                        .itemSelected,
                    studentsView
                        .rx
                        .modelSelected(StudentModel.self))
                .bind { [weak self] indexPath, model in

                    let cell = self?.studentsView.cellForItem(at: indexPath) as? StudentCVCell
                    if (model.attandance == 0) {
                       // update view accordingly  

                    } else if (model.attandance == 1) {
                        // update view accordingly  

                    } else if (model.attandance == 2) {
                        // update view accordingly  
                    }

                }.disposed(by: disposeBag)

        } 

Following is the whole code for Main Viewcontroller

class AttendanceViewController: MainViewController {
    let viewModel: AttendenceVM = AttendenceVM()
    private let disposeBag = DisposeBag()
    let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
    let notificationCenter = NotificationCenter.default
    var students : Results<StudentModel>? = nil
    var notificationToken: NotificationToken? = nil

    private lazy var studentsViewController: AttandenceView = {
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
        var viewController = storyboard.instantiateViewController(withIdentifier: "attendenceCV") as! AttandenceView
        self.add(asChildViewController: viewController, to: studentsView)
        return viewController
    }()
     override func viewDidLoad() {
        super.viewDidLoad()

        if AppFunctions.getAssignedClassId(forKey: "assignedClassId") != 0 { // && pref id isAssigned == true
            let id = AppFunctions.getAssignedClassId(forKey: "assignedClassId")
            self.viewModel.getStudentsData(id: id)
        }

        bindViewModel()

    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        classNameLbl.text = "Attandence: Class \(AppFunctions.getAssignedClassName(forKey: "assignedClassName"))"
        students = Database.singleton.fetchStudents(byAttandence: 0, byclassId: AppFunctions.getAssignedClassId(forKey: "assignedClassId"))
        notificationToken = students?.observe {[weak self] change in
            self!.studentAbsentLbl.text = "Students Absent (\(String(describing: self!.students!.count)))"
        }
        if AppFunctions.getAssignedClassId(forKey: "assignedClassId") != 0 { // && pref id isAssigned == true
            let id = AppFunctions.getAssignedClassId(forKey: "assignedClassId")
            getAssignedClassData(classId: id)
        }
    }
    deinit {
        notificationToken?.invalidate()
    }
    func getAssignedClassData(classId: Int) {
        return APIService.singelton
            .getClassById(classId: classId)
            .subscribe({ [weak self] _ in
                TestDebug.debugInfo(fileName: "", message: "\(classId)")
//                self?.notificationToken = Database.singleton.fetchStudentsForAttendence(byCLassId: classId).observe { [weak self] change in
//                    switch change {
//                    case .initial(let initial):
//                        TestDebug.debugInfo(fileName: "", message: "INIT: \(initial)")
//                        //self!.viewModel.getStudentsData(id: classId)
//                    case .update(_, let deletions, let insertions, let modifications):
//                        print(modifications)
//                        TestDebug.debugInfo(fileName: "", message: "MODIFY: \(modifications)")
//                        TestDebug.debugInfo(fileName: "", message: "MODIFY: \(insertions)")
//                    case .error(let error):
//                        TestDebug.debugInfo(fileName: "", message: "ERROR:\(error)")
//                    }
//                }
            })
            .disposed(by: self.disposeBag)
    }

    func bindViewModel() {
        viewModel
            .studentCell
            .asObservable()
            .observeOn(MainScheduler.instance)
            .bind(to: studentsViewController.studentCells)
            .disposed(by: disposeBag)

    }
}

Solution

  • The question is a bit unclear so this may be totally off base. Let me add a couple of pieces of info that may fit into an answer.

    Realm results objects are live updating objects. If, for example, you load in some Dog Objects

    var dogResults: Results<DogClass>? = nil //define a class var
    self.dogResults = realm.objects(Dog.self) //populate the class var
    

    and then if you change a dog name somewhere else in your code, the dogResults class var will contain the updated name of that dog.

    So, you don’t need to continually refresh that results object as it’s done automatically.

    If you want to be notified of those changes, you add an observer to the dogResults class var.

    self.notificationToken = self.dogResults!.observe { (changes: RealmCollectionChange) in
    

    When you first add an observer, the .initial block is called once and only once. It is not called any time after that. That’s where you would say, refresh your tableView to present the initial data.

    When data changes, the .update block is called. You can choose to just reload the tableView again or can make fine-grained changes to your tableView based on the changed data.

    Your question states

    But to always get latest response app needs to call Network API on every viewWillAppear.

    which isn’t necessary as the class var dogResults always contains updated info.

    and along those same lines

    every time save response into database when I call the API

    isn’t necessary as the only time you will need to update the UI is from within the .update block.

    Lastly, this piece of code seems to be out of place

    self!.viewModel.getStudentsData(id: classId)
    

    however, there’s not enough code in the question to understand why that’s being called in .initial - you may want to consider using the approach presented above instead of polling for updates.