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 ?
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)
}
}
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.