Swift Firebase persistence - ios

I'm trying to enable Firebase Persistence but my code keeps crashing with:
terminating with uncaught exception of type NSException
I have tried the line of code under my viewDidLoad as well as under DataService but I still get the same crash. what do I need to do to resolve this problem I'm facing
import UIKit
import Firebase
class HomeTeamSelectionVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableView: UITableView!
var club: Clubs!
var player = [Players]()
var playerFirstName = String()
var playerLastName = String()
var playerSelected: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
FIRDatabase.database().persistenceEnabled = true //Correct use of????
CLUB_KEY = ""
CLUB_KEY = club.clubKey
tableView.dataSource = self
tableView.delegate = self
DataService.ds.REF_PLAYERS.queryOrdered(byChild: "clubKey").queryEqual(toValue: club.clubKey).observe(.value, with: { (snapshot) in
print("PLAYERS_COUNT: \(snapshot.childrenCount)")
print("PLAYERS_SNAPSHOT: \(snapshot)")
self.player = []
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
if let playerDict = snap.value as? Dictionary<String, AnyObject> {
let key = snap.key
let players = Players(playerKey: key, dictionary: playerDict)
self.player.append(players)
}
}
}
// self.tableView.reloadData()
}) { (error) in
print(error.localizedDescription)
print("CHET: local error")
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return player.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let players = player[indexPath.row]
if let cell = tableView.dequeueReusableCell(withIdentifier: "HomeTeamPlayersCell") as? HomeTeamPlayersCell {
cell.configureCell(players)
return cell
} else {
return HomeTeamPlayersCell()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let players: Players!
players = player[indexPath.row]
print (players.playerKey)
print (players.playerFirstName)
print (players.playerLastName)
dismiss(animated: true, completion: nil)
}
}

From Firebase documentation for persistenceEnabled property:
Note that this property must be set before creating your first Database reference and only needs to be called once per application.
As such, the standard practice is to set it once in your AppDelegate class. For instance:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
FIRApp.configure()
FIRDatabase.database().persistenceEnabled = true
...
return true
}

Related

Black bar between keyboard and textField

I ran into a little problem. I am taking a course on iOS development, and I ran into a problem. I'm a perfectionist, and I want to bring applications to perfection, but I can't figure out which way to dig. There is a small black line between the keyboard and the textField that clearly draws attention to itself.
How to be? What to do to remove it? Which way should I drip? Maybe this is a problem in Xcode 12.3? Could this be because IQKeyboardManagerSwift is conflicting with the current version of Xcode? The video I watched didn't have this problem.
AppDelegate.swift (Here I call up the keyboard):
import UIKit
import Firebase
import IQKeyboardManagerSwift
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
FirebaseApp.configure()
let db = Firestore.firestore()
print(db)
IQKeyboardManager.shared.enable = true
IQKeyboardManager.shared.enableAutoToolbar = false
IQKeyboardManager.shared.shouldResignOnTouchOutside = true
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
ChatViewController.swift (In this view the keyboard pops up. Here I added a clear button for the textField):
import UIKit
import Firebase
class ChatViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var messageTextfield: UITextField!
let db = Firestore.firestore()
var messages: [Message] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
title = K.appName
navigationItem.hidesBackButton = true
tableView.register(UINib(nibName: K.cellNibName, bundle: nil), forCellReuseIdentifier: K.cellIdentifier)
loadMessages()
// add clear button in text field
messageTextfield.clearButtonMode = .always
messageTextfield.clearButtonMode = .whileEditing
// попробовать сделать так, чтобы при нажатии на кнопку переходило на новый абзац в Textfield
}
func loadMessages() {
db.collection(K.FStore.collectionName).order(by: K.FStore.dateField).addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print("There was an issue retrieving data from firestore, \(e)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let messageSender = data[K.FStore.senderField] as? String, let messageBody = data[K.FStore.bodyField] as? String {
let newMessage = Message(sender: messageSender, body: messageBody)
self.messages.append(newMessage)
DispatchQueue.main.async {
self.tableView.reloadData()
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
}
}
}
}
}
#IBAction func sendPressed(_ sender: UIButton) {
if let messageBody = messageTextfield.text, let messageSender = Auth.auth().currentUser?.email {
db.collection(K.FStore.collectionName).addDocument(data: [
K.FStore.senderField: messageSender,
K.FStore.bodyField: messageBody,
K.FStore.dateField: Date().timeIntervalSince1970
]) { (error) in
if let e = error {
print("There was an issue saving data to firestore, \(e)")
} else {
print("Successfully saved data")
DispatchQueue.main.async {
self.messageTextfield.text = ""
}
}
}
}
}
#IBAction func logOutPressed(_ sender: UIBarButtonItem) {
do {
try Auth.auth().signOut()
navigationController?.popToRootViewController(animated: true)
} catch let signOutError as NSError {
print ("Error signing out: %#", signOutError)
}
}
}
extension ChatViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let message = messages[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! MessageCell
cell.label.text = message.body
if message.sender == Auth.auth().currentUser?.email {
cell.leftImageView.isHidden = true
cell.rightImageView.isHidden = false
cell.messageBubble.backgroundColor = UIColor(named: K.BrandColors.lightPurple)
cell.label.textColor = UIColor(named: K.BrandColors.purple)
} else {
cell.leftImageView.isHidden = false
cell.rightImageView.isHidden = true
cell.messageBubble.backgroundColor = UIColor(named: K.BrandColors.purple)
cell.label.textColor = UIColor(named: K.BrandColors.lightPurple)
}
return cell
}
}
The solution to this problem is to add this line of code:
IQKeyboardManager.shared.keyboardDistanceFromTextField = 0

I need to take data from my firebase database and put it into labels, but the data needs to correspond to my tableView cell title

I'm building out a fairly simple app and have run into a problem. Right now My app has a home screen and a table view with the name of an Event in each cell. The data is loading fine from my database to my cell and when you click on the cell it takes you to a new View Controller. I want to put specific info into each label that corresponds to the name of my event. For example if the cell reads STEM Leadership Confrence when I click on it I want the three labels to have text that matches: the date, point value and time frame. Any help would be appreciated, thanks. (Swift 4 and Firebase RT database)
import UIKit
import Firebase
var refa: DatabaseReference!
class TableViewController: UITableViewController {
var posts = [eventStruct]()
#IBOutlet weak var Tableview: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
refa = Database.database().reference()
loadNews()
Tableview?.delegate = self
Tableview?.dataSource = self
}
struct eventStruct {
let Name: String!
let date: String!
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.performSegue(withIdentifier: "TableToSignUp", sender: self)
}
func loadNews() {
refa.child("Events").queryOrderedByKey().observe(.childAdded, with: { (snapshot) in
if let valueDictionary = snapshot.value as? [AnyHashable:String]
{
let Name = valueDictionary["Name"]
let date = valueDictionary["date"]
self.posts.insert(eventStruct(Name: Name, date: date), at: 0)
self.Tableview.reloadData()
}
})
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return posts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = Tableview.dequeueReusableCell(withIdentifier: "AllEventsCell", for: indexPath)
//let label1 = cell.viewWithTag(1) as? UILabel
cell.textLabel?.text = posts[indexPath.row].Name
//let cell2 = Tableview.dequeueReusableCell(withIdentifier: "AllEventsCell", for: indexPath)
return cell
}
}
class ViewController: UIViewController, GIDSignInUIDelegate {
#IBOutlet weak var Nav: UINavigationBar!
#IBOutlet weak var NumOfParticipants: UITextField!
#IBOutlet weak var TimeFrame: UITextField!
#IBOutlet weak var EndDate: UITextField!
#IBOutlet weak var Points: UITextField!
#IBOutlet weak var Create: UIButton!
#IBOutlet weak var ViewAll: UIButton!
#IBOutlet weak var CreateEventButton: UIButton!
#IBOutlet weak var ViewMyEventsButton: UIButton!
#IBOutlet weak var Name: UITextField!
var ref: DatabaseReference?
override func viewDidLoad() {
super.viewDidLoad()
CreateEventButton?.layer.cornerRadius = 30
CreateEventButton?.clipsToBounds = true
Create?.layer.cornerRadius = 30
Create?.clipsToBounds = true
ViewMyEventsButton?.layer.cornerRadius = 30
ViewMyEventsButton?.clipsToBounds = true
ViewAll?.layer.cornerRadius = 30
ViewAll?.clipsToBounds = true
GIDSignIn.sharedInstance().uiDelegate = self
GIDSignIn.sharedInstance().signIn()
// Do any additional setup after loading the view.
}
#IBAction func CreateEvent(_ sender: Any) {
let text: String = (Name.text)!
let date: String = (EndDate.text)!
let point: String = (Points.text)!
let timeFrame: String = (TimeFrame.text)!
let numOfpar: String = (NumOfParticipants.text)!
ref = Database.database().reference()
let now = Date()
let formatter = DateFormatter()
formatter.timeZone = TimeZone.current
formatter.dateFormat = "yyyy-MM-dd"
let dateString = formatter.string(from: now)
let newUserData = ["Name": text, "date": date, "Point Value": point, "Time Frame": timeFrame, "Volunteers Needed": numOfpar, "Date Created": dateString] as [String: Any]
ref?.child("Events").childByAutoId().updateChildValues(newUserData)
}
//,; ["Amount of Points": point];["Date End": date]
}
class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate{
let userDefault = UserDefaults()
func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) {
if let error = error{
print(error.localizedDescription)
return
}else{
let userId = user.userID // For client-side use only!
let idToken = user.authentication.idToken // Safe to send to the server
let fullName = user.profile.name
let givenName = user.profile.givenName
let familyName = user.profile.familyName
let email = user.profile.email
let str = email
let me = str?.count
let int = (me!-9)
let secondInt = (me!-8)
let XStr = str?.dropFirst(secondInt)
let NewStr = str?.dropFirst(int)
guard let authentication = user.authentication else{return}
let crendential = GoogleAuthProvider.credential(withIDToken: authentication.idToken, accessToken: authentication.accessToken)
Auth.auth().signInAndRetrieveData(with: crendential) {(result, error)in
if error == nil {
self.userDefault.set(true, forKey: "usersignedIn")
self.userDefault.synchronize()
if(NewStr == "gmail.com"){
self.window?.rootViewController?.performSegue(withIdentifier: "TeacherSegue", sender: self)
}
else if(XStr == "aisd.net"){
self.window?.rootViewController?.performSegue(withIdentifier: "TeacherSegue", sender: self)
}
}else {
//self.window?.rootViewController?.performSegue(withIdentifier: "Wegue", sender: self)
print(error?.localizedDescription ?? "me")
}
}
}
}
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
// Use Firebase library to configure APIs
FirebaseApp.configure()
GIDSignIn.sharedInstance().clientID = FirebaseApp.app()?.options.clientID
GIDSignIn.sharedInstance().delegate = self
return true
}
#available(iOS 9.0, *)
func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any])
-> Bool {
return GIDSignIn.sharedInstance().handle(url,
sourceApplication:options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String,
annotation: [:])
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
return GIDSignIn.sharedInstance().handle(url,
sourceApplication: sourceApplication,
annotation: annotation)
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}
Events-
-LdK2x1kNMB6fBw0Oorn
-LdLPGh-_SXIjM7DhjDV
-LdPVnBYQtAtSOyZM1IR-
Date: "2019-04-28"
Name:"STEM Leadership Confrence"
Point Value: "12"
Time Frame: "3-4"
You need to pass the information to the next UIViewController.
First, put all the information that you need inside your eventStruct and update your struct initialization inside the Firebase callback.
struct EventStruct {
...
var timeFrame: String!
var points: Int!
}
If you are using storyboards, you need to implement the tableView(_:didSelectRowAt:) method and use segue to perform the transition to the next view. This link will help you to add an identifier to your segue Xcode, where to assign the segue identifier
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailToSend: EventStruct = posts[indexPath.row]
performSegue(withIdentifier: "showDetailSegue", sender: detailToSend)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? ViewController, let detailToSend = sender as? EventStruct {
vc.detail = detailToSend
}
}
If you are using view code, you just need the tableView(_:didSelectRowAt:)
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let detailToSend: EventStruct = posts[indexPath.row]
let vc = ViewController()
vc.detail = detailToSend
self.present(vc, animated: true, completion: nil)
}
After that, in viewDidLoad() of your ViewController assign the information to the labels.
TimeFrame.text = detail.timeFrame
Another advice, take a look at swift best practices, like https://github.com/raywenderlich/swift-style-guide or use SwiftLint in your project.

Get IndexPath from a UITableViewCell in a multiple sections UITableView to respond a Notification

I have a tableView with multiple sections and i want to show in a cell (via notification) the progress of a download that is being handle by Alamofire.
Right now, i already have the notification post working and passing as info, an episode object, like this:
let info = ["episode": episode, "progress": progress.fractionCompleted] as [String : Any]
NotificationCenter.default.post(name: .downloadProgress, object: nil, userInfo: info)
Each of cells have an episode object. So i want to find the IndexPath of a cell that have an episode object that matches with the episode object that is being passed from a notification.
I can't figure out how can loop through my cells to find which one have that episode and get it's indexPath so i can respond to the notification properly.
I tried to get the index of the array that is being dataSource but as the tableView has multiple sections, this is not working.
Can someone help me? Thanks
My TableViewController:
//
// EpisodesViewController.swift
// Podee
//
// Created by Vinícius Barcelos on 21/07/18.
// Copyright © 2018 Vinícius Barcelos. All rights reserved.
//
import UIKit
import RealmSwift
import Kingfisher
class EpisodesTableViewController: UITableViewController {
//MARK:- Variables
var episodes: Results<Episode> = RealmService.shared.read(object: Episode.self).sorted(byKeyPath: "pubDate", ascending: true)
let episodesCellId = "episodesCellId"
var notificationToken: NotificationToken?
var episodesDictionary = Dictionary<Date, [Episode]>()
var dateDays = [Date]()
//MARK:- Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
setupObservers()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}
deinit {
self.notificationToken?.invalidate()
//NotificationCenter.default.removeObserver(self, name: NSNotification.Name.downloadProgress, object: nil)
}
//MARK:- Setup
fileprivate func setupObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleDownloadProgressNotification(notification:)), name: .downloadProgress, object: nil)
}
}
#objc func handleDownloadProgressNotification(notification:Notification) {
////////
}
//MARK:- Tableview methods
override func numberOfSections(in tableView: UITableView) -> Int {
return episodesDictionary.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let key = dateDays[section]
guard let datesValues = episodesDictionary[key] else {
return 0
}
return datesValues.count
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMMM"
return dateFormatter.string(from: dateDays[section])
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: episodesCellId, for: indexPath) as! EpisodesTableViewCell
let key = dateDays[indexPath.section]
if let podcastValues = episodesDictionary[key] {
cell.delegate = self
cell.progressBar.isHidden = true
cell.episode = podcastValues[indexPath.row]
}
return cell
}
}
Download code:
// Start download
Alamofire.request(episode.streamURL).downloadProgress { (progress) in
// Send a notification about the download progress
let info = ["episode": episode, "progress": progress.fractionCompleted] as [String : Any]
NotificationCenter.default.post(name: .downloadProgress, object: nil, userInfo: info)
//print(progress)
// Check data
}.responseData { (responseData) in ......
Modify your function of download and add the following parameters
func downloadFile(url: String,date: Date, index: Int){
let utilityQueue = DispatchQueue.global(qos: .utility)
Alamofire.download(url)
.downloadProgress(queue: utilityQueue) { progress in
let info: [String: AnyHashable] = ["date": date,
"index" : index,
"progress": progress.fractionCompleted
]
NotificationCenter.default.post(name: .downloadProgress, object: nil, userInfo: info)
}
.responseData { response in
......
}
}
In your viewcontroller, replace the function with following code:
#objc func handleDownloadProgressNotification(notification:Notification) {
var dateDays = [Date]()
guard let info = notification.userInfo,
let date = info["date"] as? Date,
let index = info["index"] as? Int,
let progress = info["progress"] as? Double,
let section = dateDays.index(where: {$0 == date})
else {return}
let indexPath = IndexPath(item: index, section: section)
}
In the download function we are passing the date and index of the row from where you started the download and you are returning it back with notification. you can also send section and row index to download function. it's mainly upto you how you want to track the row. you could've also set delegate instead of notification to track the download progress

How to print data in empty array

I'm trying to print the chat array that is declared as a empty global variable in a table. The data that I'm trying to print is received using web sockets. I'm assigning the data in the messageReceived function, and I know that the data is getting to the program because I'm printing in a label, but the moment that I'm trying to print it in the table is simple not working. All of this is in the ViewController.swift:
import UIKit
import Starscream
var messagetext: String = ""
var tabletext: String = ""
var chat = [String] ()
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
var socket = WebSocket(url: URL(string: "ws://localhost:1337/")!, protocols: ["chat"])
#IBOutlet weak var chatMessage: UILabel!
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var tableView: UITableView!
#IBAction func buttonClick(_ sender: Any) {
messagetext = textField.text!
sendMessage(messagetext)
}
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
socket.delegate = self
socket.connect()
navigationItem.hidesBackButton = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
func textFieldDidEndEditing(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return (true)
}
deinit{
socket.disconnect(forceTimeout: 0)
socket.delegate = nil
}
}
// MARK: - FilePrivate
fileprivate extension ViewController {
func sendMessage(_ messager: String) {
socket.write(string: messager)
}
func messageReceived(_ message: String) {
chatMessage.text = message
chat.append(message)
}
}
// MARK: - WebSocketDelegate
extension ViewController : WebSocketDelegate {
public func websocketDidConnect(_ socket: Starscream.WebSocket) {
}
public func websocketDidDisconnect(_ socket: Starscream.WebSocket, error: NSError?) {
performSegue(withIdentifier: "websocketDisconnected", sender: self)
}
public func websocketDidReceiveMessage(_ socket: Starscream.WebSocket, text: String) {
// 1
guard let data = text.data(using: .utf16),
let jsonData = try? JSONSerialization.jsonObject(with: data),
let jsonDict = jsonData as? [String: Any],
let messageType = jsonDict["type"] as? String else {
return
}
// 2
if messageType == "message",
let messageData = jsonDict["data"] as? [String: Any],
let messageText = messageData["text"] as? String {
messageReceived(messageText)
}
}
public func websocketDidReceiveData(_ socket: Starscream.WebSocket, data: Data) {
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return(chat.count)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "cell")
cell.textLabel?.text = chat[indexPath.row] as! String
return(cell)
}
}
Assuming that you are sure about there is data to be received by your view controller, The issue would be: the tableview data source methods are called before receiving any data, which means chat data source array is still empty, thus there is no data to display.
The solution for your case is to make sure to reload the tableview after receiving data (updating the value of chat data source array), which means in your case after appending a message to chat in messageReceived method by calling reloadData() UITableView instance method:
func messageReceived(_ message: String) {
chatMessage.text = message
chat.append(message)
// here we go:
tableView.reloadData()
}
In your message received handler, issue a tableview.reloadData()
Cheers!
You need to tell the tableview that there is new data. You also need to allow for the fact that the network operation probably occurred on a background queue and UI updates must be on the main queue:
func messageReceived(_ message: String) {
DispatchQueue.main.async {
let newRow = IndexPath(row: chat.count, section:0)
chatMessage.text = message
chat.append(message)
tableView.insertRows(at:[newRow],with: .automatic)
}
}

How to pass data from ViewModel Layer to View Layer

I don't know how to pass data from my viewModel to my view and finally show the data in the view, my view model class is:
class MainViewModel {
let sessionController: SessionController
weak var mainViewCoordinator: MainViewCoordinator?
public var fakeUsers: [User]?
init(sessionController: SessionController = SessionController()) {
self.sessionController = sessionController
}
func viewDidLoad() {
Service.instance.execute(resource: User.fake) { (result) in
print("\n result \(result)\n")
self.fakeUsers = result.value
}
}
}
I'm using Unbox Swift JSON decoder, and the printed result is is and array of User objects something like this:
([
User(id: "5851ac2615801e2348e4ea07",
birthDate: "2016-09-17T07:22:09.985Z",
msisdn: "912 065 979",
email: "rosa_bravo#yahoo.com",
profileImageUrl: "https://s3.amazonaws.com/...",
repPoints: 41607,
created: "2016-12-14T20:31:34.185Z",
displayName: "Victoria Escobedo"),
User(id: "5851ac2615801e2348e4ea09",
birthDate: "2016-05-06T11:38:23.678Z",
msisdn: "958842030",
email: "francisca_barrios#gmail.com",
profileImageUrl: "https://s3.amazonaws.com/...",
repPoints: 71408,
created: "2016-12-14T20:31:34.198Z",
displayName: "Gonzalo Rascón"),
User(id: "5851ac2615801e2348e4ea08",
birthDate: "2016-05-29T18:12:32.423Z",
msisdn: "905534639",
email: "ral0#gmail.com",
profileImageUrl: "https://s3.amazonaws.com/...",
repPoints: 24164,
created: "2016-12-14T20:31:34.195Z",
displayName: "Ramiro Dueñas"),
...
])
And I would like to pass the result to the View Layer (MainViewController) to show each User in table view cells:
class MainViewController: UIViewController, UITableViewDataSource {
#IBOutlet var tableview:UITableView!
var viewModel: MainViewModel = MainViewModel()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.viewDidLoad()
self.tableview?.reloadData()
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Action", style: .plain, target: self, action: #selector(action))
}
func action() {
viewModel.userDidSelectItem(identifier: "xxxx")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.fakeUsers?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) ->UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "fakeUserObjectCell") as! MainTableViewCell
cell.fakeUserObject = viewModel.fakeUsers?[(indexPath as NSIndexPath).row]
return cell
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "detailSegue" {
let detailMainViewController = segue.destination as! DetailMainViewController
if let indexPath = tableview.indexPath(for: sender as! MainTableViewCell) {
detailMainViewController.id = viewModel.fakeUsers?[(indexPath as NSIndexPath).row].id
}
}
}
}
I know I have to implement self.tableview?.reloadData() to show the fakeusers but I don't know how to pass the data and finally show it.
The following Service.instance.execute() in viewModel.viewDidLoad() is a async call. It have not completed the fetching and you have call self.tableview?.reloadData() Therefore this likely resulted in 0 rows.
I would suggest to change ViewModel viewDidLoad() to a completion call
func viewDidLoad() {
Service.instance.execute(resource: User.fake) { (result) in
print("\n result \(result)\n")
self.fakeUsers = result.value
}
}
Like this:
func loadUsers(completion: () -> ()) {
Service.instance.execute(resource: User.fake) { (result) in
print("\n result \(result)\n")
self.fakeUsers = result.value
completion()
}
}
Therefore in the MainViewController, viewDidLoad, you can change the call to only reloadData() when is ready.
override func viewDidLoad() {
super.viewDidLoad()
viewModel.loadUsers {
self.tableview?.reloadData()
}
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Action", style: .plain, target: self, action: #selector(action))
}
Declare a public var public var fakeUsers: [User]? in MainViewModel
Update fakeUsers in MainViewModel's viewDidLoad()
No need of a fakeUsers var in MainViewController
numberOfRowsInSection in MainViewController now returns the count of fakeUsers set by MainViewModel like:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.fakeUsers?.count ?? 0
}
Similarly, for cellForRowAt indexPath:
EDIT:
The Service.instance.execute API call won't return immediately, but when it does, the data array should be updated with new content & the table requested to be refreshed.
But now the problem is that we cannot have table access in MainViewModel. So, when the Service fetch completes, we can notify MainViewController to perform a table.reloadData()
I hope you can come up with the code for this bit, you'll need to start here:
class MainViewModel {
...
func viewDidLoad() {
Service.instance.execute(resource: User.fake) { (result) in
print("\n result \(result)\n")
self.fakeUsers = result.value
// Edu, the table refresh notification is to be fired from here
}
}
}

Resources