Swift Firebase: UIRefreshControl with .childAdded - swift

Ok, pls pardon me if this might be a very elementary question, but how do I refresh my tableView with only new items added to Firebase?
I am trying to implement a 'pull to refresh' for my tableView and populate my tableView with new items that have been added to Firebase. At viewDidLoad, I call .observeSingleEvent(.value) to display the table.
At the refresh function, I call .observe(.childAdded). However doing so will make the app consistently listen for things added, making my app consistently reloadingData. How do I code it such that it only refreshes when I do the 'pull to refresh'? My code so far:
lazy var refresh: UIRefreshControl = {
let refresh = UIRefreshControl(frame: CGRect(x: 50, y: 100, width: 20, height: 20))
refresh.addTarget(self, action: #selector(refreshData), for: .valueChanged)
return refresh
}()
var eventsArray: [FIRDataSnapshot]! = []
override func viewDidLoad() {
super.viewDidLoad()
ref = FIRDatabase.database().reference()
ref.child("events").observeSingleEvent(of: .value, with: { (snapshot) in
for snap in snapshot.children {
self.eventsArray.insert(snap as! FIRDataSnapshot, at: 0)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
func refreshData() {
self.eventsArray = []
ref.child("events").observe(.childAdded, with: { (snapshot) in
print(snapshot)
self.eventsArray.insert(snapshot, at: 0)
DispatchQueue.main.async {
self.refresh.endRefreshing()
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}
I also attempted observeSingleEvent(.childAdded) but it only pulls out one single entry (the very top entry) from Firebase.
I am not sure if my approach is correct in the first place. Any advice here pls, thanks.

Add
var lastKey: String? = nil
In the loop where you iterate over the children set the lastKey to the current snap's key.
lastKey = snap.key
Let me know if this works for you.
func refreshData() {
// Make sure you write a check to make sure lastKey isn't nil
ref.child("events").orderByKey().startAt(lastKey).observeSingleEvent(of: .value, with: { (snapshot) in
// You'll have to get rid of the first childsince it'll be a duplicate, I'm sure you can figure that out.
for snap in snapshot.children {
self.eventsArray.insert(snap as! FIRDataSnapshot, at: 0)
}
DispatchQueue.main.async {
self.refresh.endRefreshing()
self.tableView.reloadData()
}
}) { (error) in
print(error.localizedDescription)
}
}

Related

swift show loader while reading data from firebase

i have a list of music at my firebase real time database and i am retriving them but i have 1000 musics data and i want to show loader when i reading data and stop loader when if there is a error(internet connection, or something else) or reading completed.
when i turn off the internet i couldn't get the data and can't stop loader to show error alert like there is no internet connection.
please help me how to handle that problem.
here is my code
didload function called from viewdidload()
private var musicArray = [ItemModal]() {
didSet {
view?.updateTableView()
}
}
func didLoad() {
view?.showLoader()
getAllMusics { ItemModal in
self.musicArray = ItemModal
self.view?.hideLoader()
}
}
func getAllMusics(completion: #escaping ([ItemModal]) -> Void) {
var musicArray = [ItemModal]()
ref.child("music").observeSingleEvent(of: .value) { snapshot in
let enumerator = snapshot.children
while let rest = enumerator.nextObject() as? DataSnapshot {
guard let data = try? JSONSerialization.data(withJSONObject: rest.value as Any, options: []) else { return }
if let itemModal = try? JSONDecoder().decode(ItemModal.self, from: data) {
musicArray.append(itemModal)
}
}
completion(musicArray)
}
}
You can use reachability function by using https://github.com/ashleymills/Reachability.swift. To get to notify when the internet is turned off, you can implement reachabilityChanged Notification. In the selector method of reachabilityChanged, you can hide the loader.
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(reachabilityChanged), name: .reachabilityChanged)
}
#objc func changed() {
if reachability?.isReachable {
//Continue success implementation
} else {
view?.hideLoder
//Implement Error handling
}
}

Background thread Core Data object property changes doesn't reflect on UI

Let say I want to add a new item in Playlist entity of CoreData and put it in background thread and push back it to main thread then reflect it on tableView. Well, that code is working fine without background thread implementation.
But when I apply below background kinda code, after createPlaylist is executed, tableView becomes to empty space(without any items showed up), though print(self?.playlists.count) gives the correct rows count.
When dealing with GCD, I put some heavy code in background queue and push back to main queue for UI update in same closure. But it seems not worked here, I google a quit of time but still cannot anchor the issue.
import UIKit
import CoreData
class PlayListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var songs = [Song]()
var position = 0
let container = (UIApplication.shared.delegate as! AppDelegate).persistentContainer
private var playlists = [Playlist]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 1, alpha: 1)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "playlistCell")
configureLayout()
getAllPlaylists()
}
// MARK: Core data functions
func getAllPlaylists() {
do {
let context = self.container.viewContext
playlists = try context.fetch(Playlist.fetchRequest())
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
print("count: \(playlists.count)")
// printThreadStats()
} catch {
print("getAllPlaylists failed, \(error)")
}
}
func createPlaylist(name: String) {
container.performBackgroundTask { context in
let newPlaylist = Playlist(context: context)
newPlaylist.name = name
do {
try context.save()
self.playlists = try context.fetch(Playlist.fetchRequest())
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
print(self?.playlists.count)
}
} catch {
print("Create playlist failed, \(error)")
}
}
}
// MARK: tableView data source implementation
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return playlists.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let playlist = playlists[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "playlistCell", for: indexPath)
cell.textLabel?.text = playlist.name
// cell.detailTextLabel?.text = "2 songs"
return cell
}
auto generated fetchRequest and Property defining
import Foundation
import CoreData
extension Playlist {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Playlist> {
return NSFetchRequest<Playlist>(entityName: "Playlist")
}
#NSManaged public var name: String?
}
For the first call of func getAllPlaylists(), you are calling this on main thread from viewDidLoad(). So following lines are executed on main thread.
let context = self.container.viewContext
playlists = try context.fetch(Playlist.fetchRequest())
Next time inside the createPlaylist method, you are performing add playlist task in background context (not on main thread). So following lines are executed on background thread.
self.playlists = try context.fetch(Playlist.fetchRequest())
Also note that, first time we are using viewContext to fetch playlists and second time a backgroundContext. This mix up causes the UI to not show expected result.
I think these two methods could be simplified to -
func getAllPlaylists() {
do {
let context = self.container.viewContext
playlists = try context.fetch(Playlist.fetchRequest())
// DispatchQueue.main.async not necessary, we are already on main thread
self.tableView.reloadData()
print("count: \(playlists.count)")
} catch {
print("getAllPlaylists failed, \(error)")
}
}
func createPlaylist(name: String) {
container.performBackgroundTask { context in
let newPlaylist = Playlist(context: context)
newPlaylist.name = name
do {
try context.save()
DispatchQueue.main.async { [weak self] in
self?.getAllPlaylists()
}
} catch {
print("Create playlist failed, \(error)")
}
}
}
After 5 hours' digging today, I found the solution. I'd like put my solution and code below, because the stuff about "How to pass NSManagedObject instances between queues in CoreData" is quite rare && fragmentation, not friendly to newbies of SWIFT.
The thing is we want to do heavy CoreData task on background thread and reflect the changes in UI on foreground(main thread). Generally, we need to create a private queue context(privateMOC) and perform the heavy CoreData task on this private context, see below code.
For reuse purpose, I put CoreData functions separately.
import UIKit
import CoreData
struct CoreDataManager {
let managedObjectContext: NSManagedObjectContext
private let privateMOC = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let coreDataStack = CoreDataStack()
static let shared = CoreDataManager()
private init() {
self.managedObjectContext = coreDataStack.persistentContainer.viewContext
privateMOC.parent = self.managedObjectContext
}
func fetchAllPlaylists(completion: #escaping ([Playlist]?) -> Void) {
privateMOC.performAndWait {
do {
let playlists: [Playlist] = try privateMOC.fetch(Playlist.fetchRequest())
print("getAllPlaylists")
printThreadStats()
print("count: \(playlists.count)")
completion(playlists)
} catch {
print("fetchAllPlaylists failed, \(error), \(error.localizedDescription)")
completion(nil)
}
}
}
func createPlaylist(name: String) {
privateMOC.performAndWait {
let newPlaylist = Playlist(context: privateMOC)
newPlaylist.name = name
synchronize()
}
}
func deletePlaylist(playlist: Playlist) {
privateMOC.performAndWait {
privateMOC.delete(playlist)
synchronize()
}
}
func updatePlaylist(playlist: Playlist, newName: String) {
...
}
func removeAllFromEntity(entityName: String) {
...
}
func synchronize() {
do {
// We call save on the private context, which moves all of the changes into the main queue context without blocking the main queue.
try privateMOC.save()
managedObjectContext.performAndWait {
do {
try managedObjectContext.save()
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
} catch {
print("Could not synchonize data. \(error), \(error.localizedDescription)")
}
}
func printThreadStats() {
if Thread.isMainThread {
print("on the main thread")
} else {
print("off the main thread")
}
}
}
And Apple has a nice template for it Using a Private Queue to Support Concurrency
Another helpful link: Best practice: Core Data Concurrency
The real tricky thing is how to connect it with your view or viewController, the really implementation. See below ViewController code.
// 1
override func viewDidLoad() {
super.viewDidLoad()
// some layout code
// execute on background thread
DispatchQueue.global().async { [weak self] in
self?.fetchAndReload()
}
}
// 2
private func fetchAndReload() {
CoreDataManager.shared.fetchAllPlaylists(completion: { playlists in
guard let playlists = playlists else { return }
self.playlists = playlists
})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
// 3
#objc func createNewPlaylist(_ sender: Any?) {
let ac = UIAlertController(title: "Create New Playlist", message: "", preferredStyle: .alert)
ac.addTextField { textField in
textField.placeholder = "input your desired name"
}
ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
ac.addAction(UIAlertAction(title: "Done", style: .default, handler: { [weak self] _ in
guard let textField = ac.textFields?.first, let newName = textField.text, !newName.isEmpty else { return }
// check duplicate
if let playlists = self?.playlists {
if playlists.contains(where: { playlist in
playlist.name == newName
}) {
self?.duplicateNameAlert()
return
}
}
DispatchQueue.global().async { [weak self] in
CoreDataManager.shared.createPlaylist(name: newName)
self?.fetchAndReload()
}
}))
present(ac, animated: true)
}
Let me break down it:
First in viewDidload, we call fetchAndReload on background thread.
In fetchAndReload function, it brings out all the playlist(returns data with completion handler) and refresh the table on main thread.
We call createPlaylist(name: newName) in background thread and reload the table on main thread again.
Well, this is the 1st time I deal with Multi-threading in CoreData, if there is any mistake, please indicate it. Allright, that's it! Hope it could help someone.

uitableview scroll to top on data's first load

I am using reloadData() to load tableview data but my table view on its first load scrolls to top when it's loading data like so ... and that's not so nice to the user experience. Does anybody of you know the solution guys to stop this behavior. Thank you for your help !
I getting users from firebase and I am loading the images like so :
func getUsersFromFirebase(){
refUsers.observeSingleEvent(of: .value, with: { (snapshot) in
let usersNbr = snapshot.childrenCount
var counter = 0
var newUsers: [User] = []
for item in snapshot.children {
counter = counter + 1
let newitem:DataSnapshot = item as! DataSnapshot
let user = User(snapshot: item as!DataSnapshot,uid:newitem.key.description)
var newWishlists:[WishList]=[] self.refProducts.child(newitem.key.description).observeSingleEvent(of : DataEventType.value, with: { (snapshot) in
for item in snapshot.children {
let newItem:DataSnapshot = item as! DataSnapshot
let newWishlist=WishList.init(snapshot: newItem)
if(newWishlist.products.count != 0 && !newWishlist.name.trimmingCharacters(in: .whitespaces).isEmpty){
newWishlists.append(newWishlist)
}
}
user.wishLists=newWishlists
if(!(user.name=="" || user.image=="" ) ){
print("user privacy 1: " + user.isPrivate.description)
if(!user.isPrivate && user.wishLists.count != 0){
if(!user.isPrivate){
newUsers.append(user)
}
}
}
self.firebaseUsers = newUsers.shuffled()
GlobalVar.isAppFirstLoad=false
GlobalVar.allUsers = self.firebaseUsers
DispatchQueue.main.async {
self.reload(tableView: self.usersTableView)
print("user tableview reloaded 1")
}
})
}
}) { (error) in
print(error.localizedDescription)
MBProgressHUD.hideAllHUDs(for: self.usersTableView, animated: true)
}
}
func reload(tableView: UITableView) {
let contentOffset = tableView.contentOffset
tableView.reloadData()
tableView.layoutIfNeeded()
tableView.setContentOffset(contentOffset, animated: false)
}
I found the trick and its the .shuffled() that cause the behavior I think the array still shuffling when the tableview is reloading the data.
So anyone of you knows how the check if the array has finished shuffling?
So if you check in your code you have flag
GlobalVar.isAppFirstLoad=false
If you think with commom sense why it exist? you should check for
if condition that contains GlobalVar.isAppFirstLoad this value as true and in its block their will be some code that will be scrolling tableView to top.
Find it and remove it.

UIRefreshControl with RxSwift

Hi I am trying to make a UIRefreshControl work with RxSwift. Therefore I am using the Activity Indicator that is in the RxSwift Example.
In my viewModel I have the following function and variable to get my data.
// MARK: - Variables
var data = Variable<[Data]>([])
// MARK: - Public Interface
func getData() {
let request = Data.readAll()
_ = request.rxResult().subscribe(onNext: { response in
self.data.value = response.data
}, onError: { (Error) in
}, onCompleted: {
}, onDisposed: {
})
}
Then in my view controller I try to bind it to the UIRefreshcontrol and the collection view I have.
let refresher: UIRefreshControl = UIRefreshControl()
let indicator = ActivityIndicator()
indicator.asObservable()
.bindTo(refresher.rx.isRefreshing)
.addDisposableTo(disposeBag)
let resultObservable = viewModel.data.asObservable()
.trackActivity(indicator)
.bindTo(self.collectionView.rx.items(cellIdentifier: reuseCell, cellType: DataCollectionViewCell.self)) {
row, data, cell in
cell.configureCell(with: data)
}
resultObservable.addDisposableTo(disposeBag)
My question is, what am I missing to make this work? Right now if I start the app nothing happens except a black activity indicator that doesn't stop spinning.
I think you should prefer subscribing in ViewController and add that subscription in that view controller's dispose bag.
The following way I think is the correct way of using ActivityIndicator from RxExamples. following is a pseudo code.
/// ViewController.swift
import RxSwift
import RxCocoa
…
let refreshControl = UIRefreshControl()
refreshControl.rx.controlEvent(.valueChanged)
.bind(to:self.viewModel.inputs.loadPageTrigger)
.disposed(by: disposeBag)
self.viewModel.indicator
.bind(to:refreshControl.rx.isRefreshing)
.dispose(by:disposeBag)
…
/// ViewModel.swift
…
let loadTrigger = PublishSubject<Void>()
let indicator = ActivityIndicator().asDriver()
…
// Assuming rxResult returns Observable<Response>
let req = indicator.asObservable()
.sample(loadTrigger)
.flatMap { isLoading -> Observable<Response> in
if isLoading { return Observable.empty() }
return Data.readAll().rxResult()
}
.trackActivity(indicator)
.map { $0.data }
.do(onNext: { [unowned self] data in
self.data.value = data
})
…
refresher
.rx.controlEvent(UIControlEvents.valueChanged)
.subscribe(onNext: { [weak self] in
//Put your hide activity code here
self?.refresher.endRefreshing()
}, onCompleted: nil, onDisposed: nil)
.disposed(by: disposeBag)
Subscribe an event of refresher(UIRefreshControl)

How to reload UITableView without printing data twice?

I have a single view of SlackTextViewController which works as a UITableView. I'm switching between "states" using a public String allowing it to read 1 set of data in a certain state and another set of data in another state. The problem is when I switch back and forth to the original state it prints the data 2, 3, 4 times; as many as I go back and forth. I'm loading the data from a Firebase server and I think it may be a Firebase issue. Here is my code...
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if chatState == "ALL"
{
self.globalChat()
}else if chatState == "LOCAL"
{
self.localChat()
}
}
override func viewDidDisappear(animated: Bool) {
self.messageModels.removeAll()
tableView.reloadData()
ref.removeAllObservers()
}
func chatACTN()
{
if chatState == "ALL"
{
self.viewWillAppear(true)
}else if chatState == "LOCAL"
{
self.viewWillAppear(true)
}
}
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
let globalRef = ref.child("messages")
globalRef.keepSynced(true)
globalRef.queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
The problem is that each time you call globalChat() you're creating another new observer which results in having multiple observers adding the same items to self.messageModels. Thats why you're seeing the data as many times as you switch to the global state.
Since you want to clear the chat and load the last 100 each time you switch to global, there's no point in keeping the observer active when you switch to "Local".
Just remove the observer when you switch to Local, that should fix your problem.
From firebase docs:
- (void) removeAllObservers
Removes all observers at the current reference, but does not remove any observers at child references.
removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
So, ref.removeAllObservers() will not remove observers at ref.child("messages") level.
Using ref.child("messages").removeAllObservers at the beginning of localChat function to remove the observer you created in globalChat would be ok if you're only dealing with this one observer at this level but if you have more on the same level or you think you might add more in the future the best and safest way would be to remove the specific observer you created. To do that you should use the handle that is returned from starting an observer. Modify your code like this:
var globalChatHandle : FIRDatabaseHandle?
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
ref.child("messages").keepSynced(true)
globalChatHandle = ref.child("messages").queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
if globalChatHandle != nil {
ref.child("messages").removeObserverWithHandle(globalChatHandle)
}
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
And in viewDidDisappear method replace this
ref.removeAllObservers()
with
ref.child("messages").removeAllObservers()