Update Realtime data to UITableview row in swift - ios

I have the websocket server that server push the data every two seconds based on my subscription. I need to update the row based on the in the tableview.currently I am using Starscream module for websocket implementation. how to update specific rows value to every two seconds
import UIKit
import Starscream
struct Stocks: Codable {
let changepercent: String
let changeprice: String
let close: String
let currentprice: String
let high: String
let id: Int
let low: String
let name: String
let `open`: String
let type: String
let instid: String
let exchange: String
}
class ViewController: UIViewController, WebSocketDelegate,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var stocktableView: UITableView!
var arrContacts = [Stocks]()
var socket: WebSocket!
override func viewDidLoad() {
super.viewDidLoad()
var request = URLRequest(url: URL(string: "ws://192.168.18.97:8080/sss/landingstream")!)
//var request = URLRequest(url: URL(string: "ws://192.168.18.97:8080/Setrega/landingstream")!)
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket.delegate = self
socket.connect()
}
// MARK: Websocket Delegate Methods.
func websocketDidConnect(socket: WebSocketClient) {
print("websocket is connected")
}
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
if let e = error as? WSError {
print("websocket is disconnected: \(e.message)")
} else if let e = error {
print("websocket is disconnected: \(e.localizedDescription)")
} else {
print("websocket disconnected")
}
}
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print("Received text: \(text)")
let decoder = JSONDecoder()
do {
let iexEvent: Stocks = try decoder.decode(Stocks.self, from: text.data(using: .utf8)!)
DispatchQueue.main.async {
self.stocktableView.reloadData()
}
} catch {
print(error)
}
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
print("Received data: \(data.count)")
}
// MARK: Write Text Action
#IBAction func writeText(_ sender: UIBarButtonItem) {
socket.write(string: "{\"requestType\": \"INSTRUMENT_PRICE\",\"topic\": \"SBIN\"}")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.arrContacts.count;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "StocksCell", for: indexPath)
cell.textLabel?.text = arrContacts[indexPath.row].changeprice
return cell
}
// MARK: Disconnect Action
#IBAction func disconnect(_ sender: UIBarButtonItem) {
if socket.isConnected {
sender.title = "Connect"
socket.disconnect()
} else {
sender.title = "Disconnect"
socket.connect()
}
}
}

Instead of this:
DispatchQueue.main.async {
self.stocktableView.reloadData()
}
Try to find the rows that were changed with this function:
final func indexesOfStocks(stocks:[Stocks]) -> [Int] {
return stocks.reduce([]) { (currentResult, currentStocks) in
if let currentStockIndex = self.arrContacts.index(where: { currentStocks.id == $0.id }) {
return currentResult + [currentStockIndex]
}
return currentResult
}
}
Update property arrContacts:
final func updateArrContacts(indexesOfStocksValue:[Int], iexEvents:[Stocks]) {
for i in stride(from: 0, to: indexesOfStocksValue.count, by: 1) {
self.arrContacts[indexesOfStocksValue[i]] = iexEvents[i]
}
}
And reload rows for updates items only:
final func updateRows(stocksIndexes:[Int]) {
DispatchQueue.main.async {
self.stocktableView.performBatchUpdates({
let indexesToUpdate = stocksIndexes.reduce([], { (currentResult, currentStocksIndex) -> [IndexPath] in
if currentStocksIndex < self.stocktableView.numberOfRows(inSection: 0) {
return currentResult + [IndexPath.init(row: currentStocksIndex, section: 0)]
}
return currentResult
})
self.stocktableView.reloadRows(at: indexesToUpdate, with: UITableViewRowAnimation.automatic)
}) { (_) in
}
}
}
Now you can update rows with that code:
let indexesOfStocksValue = self.indexesOfStocks(stocks: iexEvents) // iexEvents is an array of Stocks
self.updateArrContacts(indexesOfStocksValue: indexesOfStocksValue, iexEvents: iexEvents)
self.updateRows(stocksIndexes: indexesOfStocksValue)
This solution is based on idea that after websocketDidReceiveMessage: only existing items in arrContacts should be updated. No new items will be added and no items will be removed.

Related

How to send Json Data to Table View Array? Swift

I've been researching and wrecking my brain attempting to get my JSON data to load into my tableview. I've tried placing the data in a Variable & I'm able to see the data in the console when I print it, however unable to push it to my table view.
Am I doing something wrong on the data page or am I not properly accessing the data within the loop?
I've tried putting the loop in the viewdidload but haven't been successful either.
// ViewController
import Foundation
import UIKit
import SDWebImage
class EntertainmentViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var A = EntertainmentApi()
var data = [EntertainmentPageData]()
var AA = EntertainmentApi().userFeedPosts
#IBOutlet weak var entPostTableView: UITableView!
override func viewDidLoad() {
func showTable() {
}
entPostTableView.register(EntertainmentViewrTableViewCell.nib(), forCellReuseIdentifier: EntertainmentViewrTableViewCell.identifier)
entPostTableView.delegate = self
entPostTableView.dataSource = self
super.viewDidLoad()
DispatchQueue.main.async {
self.entPostTableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell1 = tableView.dequeueReusableCell(withIdentifier: EntertainmentViewrTableViewCell.identifier, for: indexPath) as! EntertainmentViewrTableViewCell
customCell1.profileDisplayName.text = AA[indexPath.row].postDisplayName
self.AA.forEach({ (EntertainmentPageData) in
customCell1.configue(with: EntertainmentPageData.postDisplayName, PostImage: EntertainmentPageData.imageURLString, PostDescription: EntertainmentPageData.postDescription)
})
return customCell1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func item(for index: Int) -> EntertainmentPageData {
return data[index]
}
func numberOfItems() -> Int {
return data.count
}
}
//Data
import SwiftUI
import SDWebImage
public protocol EntertainmentPagePostItem {
/// The image for the card.
var imageURLString: String { get }
/// Rating from 0 to 5. If set to nil, rating view will not be displayed for the card.
var postDescription: String? { get }
/// Will be displayed in the title view below the card.
var postDisplayName: String { get }
}
public protocol EntertainmentPagePostDataSource: class {
/// CardSliderItem for the card at given index, counting from the top.
func item(for index: Int) -> EntertainmentPagePostItem
/// Total number of cards.
func numberOfItems() -> Int
}
struct HomePagePost: Codable {
var displayName: String
var cityStatus: String
var displayDescription: String
var displayImageURL: String
var lookingFor: String
var profileImager1: String?
var profileImager2: String?
var profileImager3: String?
var profileImager4: String?
}
struct EntertainmentPageData: Codable {
let postDisplayName: String
let imageURLString: String
let postDescription: String?
}
public class entPostFly: Codable {
let postDisplayName, imageURLString, postDescription: String
}
struct eItem: EntertainmentPagePostItem {
var postDisplayName: String
var imageURLString: String
var postDescription: String?
}
public class EntertainmentApi {
var userFeedPosts = [EntertainmentPageData]()
init() {
load()
}
func load() {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
self.userFeedPosts = entPostData
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
func getFeedPosts(completion: #escaping ([EntertainmentPageData]) -> () ) {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
completion(entPostData)
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
}
class Api {
func getHomePagePosts(completion: #escaping ([HomePagePost]) -> Void ) {
guard let apiURL = URL(string: "https://api.quickques.com/.....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let homePostData = try JSONDecoder().decode([HomePagePost].self, from: Data)
completion(homePostData)
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
func getImageData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
func getTopMostViewController() -> UIViewController? {
var topMostViewController = UIApplication.shared.keyWindow?.rootViewController
while let presentedViewController = topMostViewController?.presentedViewController {
topMostViewController = presentedViewController
}
return topMostViewController
}
First you have an empty function showTable inside your viewDidLoad - This does nothing. Presumably it is something hanging around from your various attempts. Delete that.
As you have probably worked out, your network fetch operation is going to occur asynchronously and you need to reload the table view once the data has been fetched.
You have some code in viewDidLoad that kind of tries to do this, but it isn't related to the fetch operation. It is just dispatched asynchronously on the next run loop cycle; This is probably still before the data has been fetched.
However, even if the data has been fetched, it won't show up because you are assigning userFeedPosts from a second instance of your API object to AA at initialisation time. This array is empty and will remain empty since Swift arrays are value types, not reference types. When userFeedPosts is updated, AA will hold the original empty array.
To load the data you need to
Start a load operation when the view loads
Pass a completion handler to that load operation to be invoked when the load is complete
Reload your table view with the new data
class EntertainmentViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var data = [EntertainmentPageData]()
#IBOutlet weak var entPostTableView: UITableView!
override func viewDidLoad() {
entPostTableView.register(EntertainmentViewrTableViewCell.nib(), forCellReuseIdentifier: EntertainmentViewrTableViewCell.identifier)
entPostTableView.delegate = self
entPostTableView.dataSource = self
super.viewDidLoad()
EntertainmentAPI.getFeedPosts { result in
DispatchQueue.main.async { // Ensure UI updates on main queue
switch result {
case .error(let error):
print("There was an error: \(error)")
case .success(let data):
self.data = data
self.entPostTableView.reloadData
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell1 = tableView.dequeueReusableCell(withIdentifier: EntertainmentViewrTableViewCell.identifier, for: indexPath) as! EntertainmentViewrTableViewCell
let post = data[indexPath.row)
customCell1.profileDisplayName.text = data[indexPath.row].postDisplayName
customCell1.configure(with: post.postDisplayName, PostImage: post.imageURLString, PostDescription: post.postDescription)
return customCell1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
public class EntertainmentAPI {
static func getFeedPosts(completion: #escaping ((Result<[EntertainmentPageData],Error>) -> Void) ) {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task = URLSession.shared.dataTask(with: apiURL) { data, apiResponse, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
/// TODO - Invoke the completion handler with a .failure case
return
}
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
completion(.success(entPostData))
}
catch {
completion(.failure(error))
}
}.resume()
}
}

Problem with saving data using Core Data in swift

I'm trying to save data to the core data and then display it on another view controller. I have a table view with custom cell, which have a button. I've created a selector, so when we tap on the button in each of the cell, it should save all the data from the cell. Here is my parent view controller:
import UIKit
import SafariServices
import CoreData
class ViewController: UIViewController, UISearchBarDelegate {
#IBOutlet weak var pecodeTableView: UITableView!
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var savedNews = [SavedNews]()
var newsTitle: String?
var newsAuthor: String?
var urlString: String?
var newsDate: String?
var isSaved: Bool = false
private var articles = [Article]()
private var viewModels = [NewsTableViewCellViewModel]()
private let searchVC = UISearchController(searchResultsController: nil)
override func viewDidLoad() {
super.viewDidLoad()
pecodeTableView.delegate = self
pecodeTableView.dataSource = self
pecodeTableView.register(UINib(nibName: S.CustomCell.customNewsCell, bundle: nil), forCellReuseIdentifier: S.CustomCell.customCellIdentifier)
fetchAllNews()
createSearchBar()
loadNews()
saveNews()
countNewsToCategory()
}
#IBAction func goToFavouritesNews(_ sender: UIButton) {
performSegue(withIdentifier: S.Segues.goToFav, sender: self)
}
private func fetchAllNews() {
APICaller.shared.getAllStories { [weak self] result in
switch result {
case .success(let articles):
self?.articles = articles
self?.viewModels = articles.compactMap({
NewsTableViewCellViewModel(author: $0.author ?? "Unknown", title: $0.title, subtitle: $0.description ?? "No description", imageURL: URL(string: $0.urlToImage ?? "")
)
})
DispatchQueue.main.async {
self?.pecodeTableView.reloadData()
}
case .failure(let error):
print(error)
}
}
}
private func createSearchBar() {
navigationItem.searchController = searchVC
searchVC.searchBar.delegate = self
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModels.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: S.CustomCell.customCellIdentifier, for: indexPath) as! CustomNewsCell
cell.configure(with: viewModels[indexPath.row])
cell.saveNewsBtn.tag = indexPath.row
cell.saveNewsBtn.addTarget(self, action: #selector(didTapCellButton(sender:)), for: .touchUpInside)
return cell
}
#objc func didTapCellButton(sender: UIButton) {
guard viewModels.indices.contains(sender.tag) else { return }
print("Done")// check element exist in tableview datasource
if !isSaved {
saveNews()
print("success")
}
//Configure selected button or update model
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let article = articles[indexPath.row]
guard let url = URL(string: article.url ?? "") else {
return
}
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
}
//Search
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let text = searchBar.text, !text.isEmpty else {
return
}
APICaller.shared.Search(with: text) { [weak self] result in
switch result {
case .success(let articles):
self?.articles = articles
self?.viewModels = articles.compactMap({
NewsTableViewCellViewModel(author: $0.author ?? "Unknown", title: $0.title, subtitle: $0.description ?? "No description", imageURL: URL(string: $0.urlToImage ?? "")
)
})
DispatchQueue.main.async {
self?.pecodeTableView.reloadData()
self?.searchVC.dismiss(animated: true, completion: nil)
}
case .failure(let error):
print(error)
}
}
}
}
extension ViewController {
func loadNews() {
let request: NSFetchRequest<SavedNews> = SavedNews.fetchRequest()
do {
let savedNews = try context.fetch(request)
//Handle saved news
if savedNews.count > 0 {
isSaved = true
}
} catch {
print("Error fetching data from context \(error)")
}
}
func saveNews() {
//Initialize the context
let news = SavedNews(context: self.context)
//Putting data
news.title = newsTitle
news.author = newsAuthor
news.publishedAt = newsDate
news.url = urlString
do {
try context.save()
} catch {
print("Error when saving data \(error)")
}
}
func countNewsToCategory() {
//Initialize the context
let request: NSFetchRequest<SavedNews> = SavedNews.fetchRequest()
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
])
request.predicate = predicate
do {
savedNews = try context.fetch(request)
} catch {
print("Error fetching data from category \(error)")
}
}
}
I don't know where is the problem, I've created a correct data model, but data could not be saved. Here is my model:
import Foundation
struct APIResponse: Codable {
let articles: [Article]
}
struct Article: Codable {
let author: String?
let source: Source
let title: String
let description: String?
let url: String?
let urlToImage: String?
let publishedAt: String
}
struct Source: Codable {
let name: String
}
And also my model in Core Data:
My second view controller, to which I want display the data:
import UIKit
import CoreData
class FavouriteNewsViewController: UIViewController {
#IBOutlet weak var favTableView: UITableView!
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var savedNews = [SavedNews]()
override func viewDidLoad() {
super.viewDidLoad()
favTableView.delegate = self
favTableView.delegate = self
loadSavedNews()
favTableView.register(UINib(nibName: S.FavouriteCell.favouriteCell, bundle: nil), forCellReuseIdentifier: S.FavouriteCell.favouriteCellIdentifier)
// Do any additional setup after loading the view.
}
}
extension FavouriteNewsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return savedNews.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = favTableView.dequeueReusableCell(withIdentifier: S.FavouriteCell.favouriteCellIdentifier, for: indexPath) as! FavouritesCell
print(savedNews)
let article = savedNews[indexPath.row]
if let articleTitle = article.title {
cell.favTitle.text = articleTitle
}
if let articleAuthor = article.author {
cell.favAuthor.text = articleAuthor
}
if let articleDesc = article.desc {
cell.favDesc.text = article.desc
}
return cell
}
}
extension FavouriteNewsViewController {
func loadSavedNews() {
let request: NSFetchRequest<SavedNews> = SavedNews.fetchRequest()
do {
savedNews = try context.fetch(request)
} catch {
print("Error fetching data from context \(error)")
}
}
func deleteNews(at indexPath: IndexPath) {
// Delete From NSObject
context.delete(savedNews[indexPath.row])
// Delete From current News list
savedNews.remove(at: indexPath.row)
// Save deletion
do {
try context.save()
} catch {
print("Error when saving data \(error)")
}
}
}
you did not assign your properties that you are trying to save
newsTitle,newsAuthor,newsDate,urlString
seems these properties have nil value . make sure these properties have valid value before save .

Getting duplicate messages from offline database in xmpp every time when I open the chat screen?

I am trying to get groupchat offline messages from the offline database. But when I am trying to get the messages, then getting the duplicate messages multiple times. I don't know how to resolve this issue. Please help me to resolve this issue.
I attached the code of my XMPP implementation....
This is my XMPPController class where I can setup the client with the server.
class XMPPController: NSObject {
var xmppStream: XMPPStream
let hostName: String
let userJID: XMPPJID
let hostPort: UInt16
let password: String
var xmppStorage: XMPPMessageArchivingCoreDataStorage?
var messageArchiving: XMPPMessageArchiving?
init(hostName: String, userJIDString: String, hostPort: UInt16 = 5222, password: String) throws {
guard let userJID = XMPPJID(string: userJIDString) else {
throw XMPPControllerError.wrongUserJID
}
self.hostName = hostName
self.userJID = userJID
self.hostPort = hostPort
self.password = password
// Stream Configuration
self.xmppStream = XMPPStream()
self.xmppStream.hostName = hostName
self.xmppStream.hostPort = hostPort
self.xmppStream.startTLSPolicy = XMPPStreamStartTLSPolicy.allowed
self.xmppStream.myJID = userJID
super.init()
self.xmppStream.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppStorage = XMPPMessageArchivingCoreDataStorage()
messageArchiving = XMPPMessageArchiving(messageArchivingStorage: xmppStorage)
messageArchiving?.clientSideMessageArchivingOnly = false
messageArchiving?.activate(xmppStream)
messageArchiving?.addDelegate(self, delegateQueue: DispatchQueue.main)
}
func connect() {
if !self.xmppStream.isDisconnected {
return
}
try! self.xmppStream.connect(withTimeout: XMPPStreamTimeoutNone)
}
}
extension XMPPController: XMPPStreamDelegate {
func xmppStreamDidConnect(_ stream: XMPPStream) {
print("Stream: Connected")
try! stream.authenticate(withPassword: self.password)
}
func xmppStreamDidAuthenticate(_ sender: XMPPStream) {
self.xmppStream.send(XMPPPresence())
print("Stream: Authenticated")
}
func xmppStream(_ sender: XMPPStream, didReceive message: XMPPMessage) {
print("Message XMPPController Called")
}
}
This is my ViewController class implementation. In this, we can create and join room and send messages in group.
class GroupMessagesViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var inputTextField: UITextField!
#IBOutlet weak var photosButton: UIPhotosButton!
var xmppController: XMPPController!
var messages = [Message]()
var xmppRoom: XMPPRoom!
var isXMPPRoomDidReceiveMsgCalled = false
override func viewDidLoad() {
super.viewDidLoad()
// Group Chat
self.createOrJoinRoom()
}
deinit {
xmppRoom.deactivate()
xmppRoom.removeDelegate(self)
xmppRoom = nil
}
func retrieveGroupMessagesFromOfflineDb() {
let fetchRequest = NSFetchRequest<XMPPMessageArchiving_Message_CoreDataObject>(entityName: "XMPPMessageArchiving_Message_CoreDataObject")
fetchRequest.returnsDistinctResults = true
// let predicate = NSPredicate(format: "bareJidStr == %#", "groupdemo#conference.chat.webolutionit.co.uk")
// fetchRequest.predicate = predicate
let predicate = NSPredicate(format: "messageStr CONTAINS[cd] %# AND bareJidStr == %#", "groupchat", "groupdemo#conference.\(XMPPKeys.host)")
fetchRequest.predicate = predicate
let context = xmppController.xmppStorage?.mainThreadManagedObjectContext
do {
let messageHistoryArray = try context?.fetch(fetchRequest)
if messageHistoryArray?.count != 0 {
self.messages.removeAll()
messageHistoryArray?.forEach({ (msg) in
var message: Message!
if msg.body.contains(".jpg") {
message = Message(message: msg.body, from: msg.message.fromStr, to: msg.message.toStr, type: msg.message.type, contentType: .photo)
}
else {
message = Message(message: msg.body, from: msg.message.fromStr, to: msg.message.toStr, type: msg.message.type, contentType: .text)
}
self.messages.append(message)
})
isXMPPRoomDidReceiveMsgCalled = true
self.tableView.reloadData()
tableView.scrollToBottom()
}
}
catch {
isXMPPRoomDidReceiveMsgCalled = true
print(error.localizedDescription)
}
}
func createOrJoinRoom() {
let roomStorage = XMPPRoomMemoryStorage()
let roomId = XMPPJID(string: "groupdemo#conference.\(XMPPKeys.host)")
xmppRoom = XMPPRoom(roomStorage: roomStorage!, jid: roomId!, dispatchQueue: DispatchQueue.main)
let userId = XMPPJID(string: "testUser#chat.\(XMPPKeys.receiverJID)")
xmppRoom.inviteUser(userId!, withMessage: "Join Please")
xmppRoom.activate(xmppController.xmppStream)
xmppRoom.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppRoom.join(usingNickname: XMPPKeys.senderJID, history: nil, password: nil)
}
// MARK:- IBACTIONS
#IBAction func backBtnPressed(_ sender: UIButton) {
_ = self.navigationController?.popViewController(animated: true)
}
#IBAction func sendBtnPressed(_ sender: UIButton) {
if let text = inputTextField.text {
if text != "" {
let message = XMLElement.element(withName: "message") as! XMLElement
message.addAttribute(withName: "from", stringValue: XMPPKeys.senderJID)
message.addAttribute(withName: "to", stringValue: "groupdemo#conference.\(XMPPKeys.host)")
message.addAttribute(withName: "type", stringValue: "groupchat")
message.addAttribute(withName: "id", stringValue: xmppController.xmppStream.generateUUID)
let uuid = UIDevice.current.identifierForVendor?.uuidString
message.addAttribute(withName: "uuid", stringValue: uuid.leoSafe())
let body = XMLElement.element(withName: "body") as! XMLElement
body.stringValue = text
message.addChild(body)
xmppController.xmppStream.send(message)
}
}
}
}
//MARK: UITableView Delegate & DataSource
extension GroupMessagesViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(messages.count)
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let message = messages[indexPath.row]
let currentUser = message.to.leoSafe().components(separatedBy: "/")[0]
if message.contentType == .text {
let cell = tableView.dequeueReusableCell(withIdentifier: XMPPKeys.senderJID.contains(currentUser) ? "MessageTableViewCell" : "UserMessageTableViewCell") as! MessageTableViewCell
cell.set(message)
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: currentUser == XMPPKeys.senderJID ? "MessageAttachmentTableViewCell" : "UserMessageAttachmentTableViewCell") as! MessageAttachmentTableViewCell
cell.delegate = self
cell.set(message)
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard tableView.isDragging else { return }
cell.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
UIView.animate(withDuration: 0.3, animations: {
cell.transform = CGAffineTransform.identity
})
}
}
//MARK: UItextField Delegate
extension GroupMessagesViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return textField.resignFirstResponder()
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return true
}
}
//MARK: MessageTableViewCellDelegate Delegate
extension GroupMessagesViewController: MessageTableViewCellDelegate {
func messageTableViewCellUpdate() {
tableView.beginUpdates()
tableView.endUpdates()
}
}
extension GroupMessagesViewController: XMPPRoomDelegate {
func xmppRoomDidJoin(_ sender: XMPPRoom) {
print("Room Joined")
sender.fetchConfigurationForm()
let a = xmppRoom.fetchMembersList()
self.retrieveGroupMessagesFromOfflineDb()
}
func xmppRoomDidCreate(_ sender: XMPPRoom) {
print("Room Created")
}
func xmppRoomDidLeave(_ sender: XMPPRoom) {
print("Room Leave")
}
func xmppRoomDidDestroy(_ sender: XMPPRoom) {
print("Room Destroyed")
}
func xmppRoom(_ sender: XMPPRoom, didReceive message: XMPPMessage, fromOccupant occupantJID: XMPPJID) {
print("Group Message Retrieved")
if isXMPPRoomDidReceiveMsgCalled {
if message.isGroupChatMessageWithBody {
var msg: Message!
if (message.body?.contains(".jpg"))! {
msg = Message(message: message.body, from: message.fromStr, to: message.toStr, type: message.type!, contentType: .photo)
}
else {
msg = Message(message: message.body, from: message.fromStr, to: message.toStr, type: message.type!, contentType: .text)
}
self.messages.append(msg)
self.tableView.reloadData()
self.tableView.scrollToBottom()
inputTextField.text = ""
}
}
}
func xmppRoom(_ sender: XMPPRoom, didFetchModeratorsList items: [Any]) {
print(items)
}
func xmppRoom(_ sender: XMPPRoom, didFetchMembersList items: [Any]) {
print(items)
}
}
// XMPPMUCDelegate METHODS
// ACCEPTING GROUP INVITATION
extension GroupMessagesViewController: XMPPMUCDelegate {
func xmppMUC(_ sender: XMPPMUC, roomJID: XMPPJID, didReceiveInvitation message: XMPPMessage) {
let x = message.element(forName: "x", xmlnsPrefix: XMPPMUCUserNamespace)
let invite = x?.elements(forName: "invite")
if !(invite?.isEmpty)! {
let conferenceRoomId = message.attribute(forName: "from")?.stringValue
self.joinMultiUserChat(roomName: conferenceRoomId.leoSafe())
}
}
func joinMultiUserChat(roomName: String) {
let roomJID = XMPPJID(string: roomName)
let xmppStorage = XMPPRoomMemoryStorage()
xmppRoom = XMPPRoom(roomStorage: xmppStorage!, jid: roomJID!, dispatchQueue: DispatchQueue.main)
xmppRoom.activate(xmppController.xmppStream)
xmppRoom.addDelegate(self, delegateQueue: DispatchQueue.main)
xmppRoom.join(usingNickname: xmppController!.xmppStream.myJID!.full, history: nil)
}
}

Swift - Update/add/delete item on a table view with sections and source

How can I update/add/delete an item on a table view with sections and its source?
Im having issues when trying to update a cell. Sometimes it happens on delete and add too.
It seems that a problem on sections is triggering it.
1- Data is loaded from a JSON response from a server.
2 - This data is sorted alphabetically and sections based on the first letter from the name is created adding each client to its indexed letter.
Im adding to print-screens:
Before:
After:
I renamed 'Bowl' to 'Bowl 2' and it 'created' a new entry, keeping the old and new value. If I refresh (pull), it gets fixed.
Also, sometimes, it removes 'Abc MacDon' and after refreshing, it gets fixed.
class ClientsViewController: UITableViewController {
var sortedFirstLetters: [String] = []
var sections: [[Client]] = [[]]
var tableArray = [Client]()
var client: Client?
var wasDeleted: Bool?
var refresher: UIRefreshControl!
#IBOutlet weak var noClientsLabel: UILabel!
#IBOutlet var noClientsView: UIView!
#IBAction func unwindToClients(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? ClientViewController,
let client = sourceViewController.client,
let wasDeleted = sourceViewController.wasDeleted {
if(wasDeleted) {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
print("Delteted")
tableArray.remove(at: selectedIndexPath.row)
// DispatchQueue.main.async {
// // Deleting the row in the tableView
// if self.tableView.numberOfRows(inSection: selectedIndexPath.section) > 1 {
// self.tableView.deleteRows(at: [selectedIndexPath], with: UITableViewRowAnimation.bottom)
// } else {
// let indexSet = NSMutableIndexSet()
// indexSet.add(selectedIndexPath.section)
// self.tableView.deleteSections(indexSet as IndexSet, with: UITableViewRowAnimation.bottom)
// }
//
// }
}
}
else {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing client.
tableArray[selectedIndexPath.row] = client
//tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
print("update")
print(tableArray)
}
else {
// Add a client.
tableArray.append(client)
print("add")
}
}
self.prepareData()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let secondScene = segue.destination as! ClientViewController
if segue.identifier == "ShowDetail", let indexPath = self.tableView.indexPathForSelectedRow {
let currentPhoto = sections[indexPath.section][indexPath.row]
secondScene.client = currentPhoto
}
else if segue.identifier == "AddItem" {
print("add")
}
else {
fatalError("The selected cell is not being displayed by the table")
}
}
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
getClients()
}
}
extension ClientsViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: #selector(ClientsViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
tableView.backgroundView = nil
noClientsLabel.text = ""
getClients() //for only the 1st time ==> when view is created ==> ok ish
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if(self.tableArray.count > 0) {
return sortedFirstLetters[section]
}
else {
return ""
}
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return sortedFirstLetters
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = sections[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ClientCell", for: indexPath)
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = item.city + " - " + item.province
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].count
}
func getClients() {
print("called server")
self.refreshControl?.beginRefreshing()
self.tableView.setContentOffset(CGPoint(x:0, y:-100), animated: true)
makeRequest(endpoint: "api/clients/all",
parameters: [:],
completionHandler: { (container : ApiContainer<Client>?, error : Error?) in
if let error = error {
print("error calling POST on /getClients")
print(error)
return
}
self.tableArray = (container?.result)!
self.prepareData()
DispatchQueue.main.async {
if(self.tableArray.isEmpty)
{
self.noClientsLabel.text = "bNo Clients"
self.tableView.backgroundView?.isHidden = false
self.noClientsLabel.text = ""
print("all")
}
else{
print("nothing")
}
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
}
} )
}
//sorts and makes the index
func prepareData() {
let firstLetters = self.tableArray.map { $0.nameFirstLetter }
let uniqueFirstLetters = Array(Set(firstLetters))
self.sortedFirstLetters = uniqueFirstLetters.sorted()
self.sections = self.sortedFirstLetters.map { firstLetter in
return self.tableArray
.filter { $0.nameFirstLetter == firstLetter }
.sorted { $0.name < $1.name }
}
}
}
Struct
struct Client: Codable {
var client_id: Int!
let name: String!
let postal_code: String!
let province: String!
let city: String!
let address: String!
init(name: String, client_id: Int! = nil, postal_code: String, province: String, city: String, address: String) {
self.client_id = client_id
self.name = name
self.postal_code = postal_code
self.province = province
self.city = city
self.address = address
}
var nameFirstLetter: String {
return String(self.name[self.name.startIndex]).uppercased()
}
}
code after some interaction with Hardik
import UIKit
import Foundation
class ClientsViewController: UITableViewController {
var sortedFirstLetters: [String] = []
var sections: [[Client]] = [[]]
// var tableArray = [Client]()
var tableArray : [Client] = [Client]()
var client: Client?
var wasDeleted: Bool?
var refresher: UIRefreshControl!
#IBOutlet weak var noClientsLabel: UILabel!
#IBOutlet var noClientsView: UIView!
#IBAction func unwindToClients(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? ClientViewController,
let client = sourceViewController.client,
let wasDeleted = sourceViewController.wasDeleted {
if(wasDeleted) {
if let index = self.tableArray.index(where: { (item) -> Bool in
item.client_id == client.client_id
}) {
self.tableArray.remove(at: index)
print("Delteted")
// Find the client in the tableArray by the client id and remove it from the tableArray
// I am writing an example code here, this is not tested so just get the logic from here.
//self.tableArray.remove(at: selectedIndexPath.row)
}
}
else {
if self.tableArray.contains(where: { (item) -> Bool in
item.client_id == client.client_id
}) {
//Find the item in the tableArray by the client id and update it there too
// I am writing an example code here, this is not tested so just get the logic from here.
//if let index = self.tableArray.index(where: { (item) -> Bool in
// item.id == client.id
//}) {
self.tableArray[index] = client
//self.tableArray.replace(client, at: index)
//}
print("update")
print(tableArray)
}
else {
// Add a client.
tableArray.append(client)
print("add")
}
}
// Now update the sections Array and it will have all the correct values
self.prepareData()
DispatchQueue.main.async {
self.tableView.reloadData()
}
// if(wasDeleted) {
// if let selectedIndexPath = tableView.indexPathForSelectedRow {
// print("Delteted")
// sections[selectedIndexPath.section].remove(at: selectedIndexPath.row)
// }
//
// }
// else {
// if let selectedIndexPath = tableView.indexPathForSelectedRow {
// // Update an existing client.
// sections[selectedIndexPath.section][selectedIndexPath.row] = client
// //tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
// print("update")
// print(tableArray)
// }
// else {
// // Add a client.
// tableArray.append(client)
// print("add")
// self.prepareData()
// }
// }
//
// DispatchQueue.main.async {
//
// self.tableView.reloadData()
// }
// if(wasDeleted) {
// if let selectedIndexPath = tableView.indexPathForSelectedRow {
// print("Delteted")
// tableArray.remove(at: selectedIndexPath.row)
//// DispatchQueue.main.async {
//// // Deleting the row in the tableView
//// if self.tableView.numberOfRows(inSection: selectedIndexPath.section) > 1 {
//// self.tableView.deleteRows(at: [selectedIndexPath], with: UITableViewRowAnimation.bottom)
//// } else {
//// let indexSet = NSMutableIndexSet()
//// indexSet.add(selectedIndexPath.section)
//// self.tableView.deleteSections(indexSet as IndexSet, with: UITableViewRowAnimation.bottom)
//// }
////
//// }
// }
//
// }
// else {
// if let selectedIndexPath = tableView.indexPathForSelectedRow {
// // Update an existing client.
// tableArray[selectedIndexPath.row] = client
// //tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
// print("update")
// print(tableArray)
// }
// else {
// // Add a client.
// tableArray.append(client)
// print("add")
// }
// }
// self.prepareData()
// DispatchQueue.main.async {
//
// self.tableView.reloadData()
// }
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let secondScene = segue.destination as! ClientViewController
if segue.identifier == "ShowDetail", let indexPath = self.tableView.indexPathForSelectedRow {
let currentPhoto = sections[indexPath.section][indexPath.row]
secondScene.client = currentPhoto
}
else if segue.identifier == "AddItem" {
print("add")
}
else {
fatalError("The selected cell is not being displayed by the table")
}
}
#objc func handleRefresh(_ refreshControl: UIRefreshControl) {
getClients()
}
}
extension ClientsViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: #selector(ClientsViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
tableView.backgroundView = nil
noClientsLabel.text = ""
getClients() //for only the 1st time ==> when view is created ==> ok ish
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if(self.tableArray.count > 0) {
return sortedFirstLetters[section]
}
else {
return ""
}
}
override func sectionIndexTitles(for tableView: UITableView) -> [String]? {
return sortedFirstLetters
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = sections[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ClientCell", for: indexPath)
cell.textLabel?.text = item.name
cell.detailTextLabel?.text = item.city + " - " + item.province
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sections[section].count
}
func getClients() {
print("called server")
self.refreshControl?.beginRefreshing()
self.tableView.setContentOffset(CGPoint(x:0, y:-100), animated: true)
makeRequest(endpoint: "api/clients/all",
parameters: [:],
completionHandler: { (container : ApiContainer<Client>?, error : Error?) in
if let error = error {
print("error calling POST on /getClients")
print(error)
return
}
self.tableArray = (container?.result)!
self.prepareData()
DispatchQueue.main.async {
if(self.tableArray.isEmpty)
{
self.noClientsLabel.text = "bNo Clients"
self.tableView.backgroundView?.isHidden = false
self.noClientsLabel.text = ""
print("all")
}
else{
print("nothing")
}
self.tableView.reloadData()
self.refreshControl?.endRefreshing()
}
} )
}
//sorts and makes the index
func prepareData() {
let firstLetters = self.tableArray.map { $0.nameFirstLetter }
let uniqueFirstLetters = Array(Set(firstLetters))
self.sortedFirstLetters = uniqueFirstLetters.sorted()
self.sections = self.sortedFirstLetters.map { firstLetter in
return self.tableArray
.filter { $0.nameFirstLetter == firstLetter }
.sorted { $0.name < $1.name }
}
}
}
You are editing the wrong datasource. You should edit or update the sections array not tableArray.
Change your code in unwindToClients here :
if(wasDeleted) {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
print("Delteted")
tableArray.remove(at: selectedIndexPath.row)
}
}
else {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing client.
tableArray[selectedIndexPath.row] = client
//tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
print("update")
print(tableArray)
}
else {
// Add a client.
tableArray.append(client)
print("add")
}
}
self.prepareData()
DispatchQueue.main.async {
self.tableView.reloadData()
}
with this :
if(wasDeleted) {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
print("Delteted")
sections[selectedIndexPath.section].remove(at: selectedIndexPath.row)
}
}
else {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing client.
sections[selectedIndexPath.section][selectedIndexPath.row] = client
//tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
print("update")
print(tableArray)
}
else {
// Add a client.
tableArray.append(client)
print("add")
self.prepareData()
}
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
To fix the incorrect section headers, try doing something like this:
if(wasDeleted) {
if let index = self.tableArray.index(where: { (item) -> Bool in
item.id == client.id
}) {
print("Delteted")
// Find the client in the tableArray by the client id and remove it from the tableArray
// I am writing an example code here, this is not tested so just get the logic from here.
//self.tableArray.remove(at: index)
}
}
else {
if self.tableArray.contains(client) {
//Find the item in the tableArray by the client id and update it there too
// I am writing an example code here, this is not tested so just get the logic from here.
//if let index = self.tableArray.index(where: { (item) -> Bool in
// item.id == client.id
//}) {
// self.tableArray.replace(client, at: index)
//}
print("update")
print(tableArray)
}
else {
// Add a client.
tableArray.append(client)
print("add")
}
}
// Now update the sections Array and it will have all the correct values
self.prepareData()
DispatchQueue.main.async {
self.tableView.reloadData()
}

Firebase chat app crashes when clicking on table cell

I downloaded a demo chat application that runs perfectly but when I implement it into my own app it crashes and the code is exactly the same. The app gives you the ability to create a chat room and my app works up to this point but when you click on the chat room name that now appears on the table I get the following error:
Assertion failure in -[Irish_League_Grounds.ChatViewController viewWillAppear:], /Users/ryanball/Desktop/Irish League
Grounds/Pods/JSQMessagesViewController/JSQMessagesViewController/Controllers/JSQMessagesViewController.m:277
2017-05-17 17:32:55.815 Irish League Grounds[20456:681491]
Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'Invalid parameter not
satisfying: self.senderDisplayName != nil'
Here is my code for the two view controllers I'm segueing between:
import UIKit
import Firebase
enum Section: Int {
case createNewChannelSection = 0
case currentChannelsSection
}
class ChannelListViewController: UITableViewController {
// MARK: Properties
var senderDisplayName: String?
var newChannelTextField: UITextField?
private var channelRefHandle: FIRDatabaseHandle?
private var channels: [Channel] = []
private lazy var channelRef: FIRDatabaseReference = FIRDatabase.database().reference().child("channels")
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
title = "RW RIC"
observeChannels()
}
deinit {
if let refHandle = channelRefHandle {
channelRef.removeObserver(withHandle: refHandle)
}
}
// MARK :Actions
#IBAction func createChannel(_ sender: AnyObject) {
if let name = newChannelTextField?.text {
let newChannelRef = channelRef.childByAutoId()
let channelItem = [
"name": name
]
newChannelRef.setValue(channelItem)
}
}
// MARK: Firebase related methods
private func observeChannels() {
// We can use the observe method to listen for new
// channels being written to the Firebase DB
channelRefHandle = channelRef.observe(.childAdded, with: { (snapshot) -> Void in
let channelData = snapshot.value as! Dictionary<String, AnyObject>
let id = snapshot.key
if let name = channelData["name"] as! String!, name.characters.count > 0 {
self.channels.append(Channel(id: id, name: name))
self.tableView.reloadData()
} else {
print("Error! Could not decode channel data")
}
})
}
// MARK: Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let channel = sender as? Channel {
let chatVc = segue.destination as! ChatViewController
chatVc.senderDisplayName = senderDisplayName
chatVc.channel = channel
chatVc.channelRef = channelRef.child(channel.id)
}
}
// MARK: UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let currentSection: Section = Section(rawValue: section) {
switch currentSection {
case .createNewChannelSection:
return 1
case .currentChannelsSection:
return channels.count
}
} else {
return 0
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = (indexPath as NSIndexPath).section == Section.createNewChannelSection.rawValue ? "NewChannel" : "ExistingChannel"
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
if (indexPath as NSIndexPath).section == Section.createNewChannelSection.rawValue {
if let createNewChannelCell = cell as? CreateChannelCell {
newChannelTextField = createNewChannelCell.newChannelNameField
}
} else if (indexPath as NSIndexPath).section == Section.currentChannelsSection.rawValue {
cell.textLabel?.text = channels[(indexPath as NSIndexPath).row].name
}
return cell
}
// MARK: UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if (indexPath as NSIndexPath).section == Section.currentChannelsSection.rawValue {
let channel = channels[(indexPath as NSIndexPath).row]
self.performSegue(withIdentifier: "ShowChannel", sender: channel)
}
}
}
Here is the second view controller:
import UIKit
import Photos
import Firebase
import JSQMessagesViewController
final class ChatViewController: JSQMessagesViewController {
// MARK: Properties
private let imageURLNotSetKey = "NOTSET"
var channelRef: FIRDatabaseReference?
private lazy var messageRef: FIRDatabaseReference = self.channelRef!.child("messages")
fileprivate lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "gs://chatchat-871d0.appspot.com")
private lazy var userIsTypingRef: FIRDatabaseReference = self.channelRef!.child("typingIndicator").child(self.senderId)
private lazy var usersTypingQuery: FIRDatabaseQuery = self.channelRef!.child("typingIndicator").queryOrderedByValue().queryEqual(toValue: true)
private var newMessageRefHandle: FIRDatabaseHandle?
private var updatedMessageRefHandle: FIRDatabaseHandle?
private var messages: [JSQMessage] = []
private var photoMessageMap = [String: JSQPhotoMediaItem]()
private var localTyping = false
var channel: Channel? {
didSet {
title = channel?.name
}
}
var isTyping: Bool {
get {
return localTyping
}
set {
localTyping = newValue
userIsTypingRef.setValue(newValue)
}
}
lazy var outgoingBubbleImageView: JSQMessagesBubbleImage = self.setupOutgoingBubble()
lazy var incomingBubbleImageView: JSQMessagesBubbleImage = self.setupIncomingBubble()
// MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.senderId = FIRAuth.auth()?.currentUser?.uid
observeMessages()
// No avatars
collectionView!.collectionViewLayout.incomingAvatarViewSize = CGSize.zero
collectionView!.collectionViewLayout.outgoingAvatarViewSize = CGSize.zero
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
observeTyping()
}
deinit {
if let refHandle = newMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
if let refHandle = updatedMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
}
// MARK: Collection view data source (and related) methods
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item] // 1
if message.senderId == senderId { // 2
return outgoingBubbleImageView
} else { // 3
return incomingBubbleImageView
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
let message = messages[indexPath.item]
if message.senderId == senderId { // 1
cell.textView?.textColor = UIColor.white // 2
} else {
cell.textView?.textColor = UIColor.black // 3
}
return cell
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForMessageBubbleTopLabelAt indexPath: IndexPath!) -> CGFloat {
return 15
}
override func collectionView(_ collectionView: JSQMessagesCollectionView?, attributedTextForMessageBubbleTopLabelAt indexPath: IndexPath!) -> NSAttributedString? {
let message = messages[indexPath.item]
switch message.senderId {
case senderId:
return nil
default:
guard let senderDisplayName = message.senderDisplayName else {
assertionFailure()
return nil
}
return NSAttributedString(string: senderDisplayName)
}
}
// MARK: Firebase related methods
private func observeMessages() {
messageRef = channelRef!.child("messages")
let messageQuery = messageRef.queryLimited(toLast:25)
// We can use the observe method to listen for new
// messages being written to the Firebase DB
newMessageRefHandle = messageQuery.observe(.childAdded, with: { (snapshot) -> Void in
let messageData = snapshot.value as! Dictionary<String, String>
if let id = messageData["senderId"] as String!, let name = messageData["senderName"] as String!, let text = messageData["text"] as String!, text.characters.count > 0 {
self.addMessage(withId: id, name: name, text: text)
self.finishReceivingMessage()
} else if let id = messageData["senderId"] as String!, let photoURL = messageData["photoURL"] as String! {
if let mediaItem = JSQPhotoMediaItem(maskAsOutgoing: id == self.senderId) {
self.addPhotoMessage(withId: id, key: snapshot.key, mediaItem: mediaItem)
if photoURL.hasPrefix("gs://") {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: nil)
}
}
} else {
print("Error! Could not decode message data")
}
})
// We can also use the observer method to listen for
// changes to existing messages.
// We use this to be notified when a photo has been stored
// to the Firebase Storage, so we can update the message data
updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
let messageData = snapshot.value as! Dictionary<String, String>
if let photoURL = messageData["photoURL"] as String! {
// The photo has been updated.
if let mediaItem = self.photoMessageMap[key] {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key)
}
}
})
}
private func fetchImageDataAtURL(_ photoURL: String, forMediaItem mediaItem: JSQPhotoMediaItem, clearsPhotoMessageMapOnSuccessForKey key: String?) {
let storageRef = FIRStorage.storage().reference(forURL: photoURL)
storageRef.data(withMaxSize: INT64_MAX){ (data, error) in
if let error = error {
print("Error downloading image data: \(error)")
return
}
storageRef.metadata(completion: { (metadata, metadataErr) in
if let error = metadataErr {
print("Error downloading metadata: \(error)")
return
}
if (metadata?.contentType == "image/gif") {
mediaItem.image = UIImage.gifWithData(data!)
} else {
mediaItem.image = UIImage.init(data: data!)
}
self.collectionView.reloadData()
guard key != nil else {
return
}
self.photoMessageMap.removeValue(forKey: key!)
})
}
}
private func observeTyping() {
let typingIndicatorRef = channelRef!.child("typingIndicator")
userIsTypingRef = typingIndicatorRef.child(senderId)
userIsTypingRef.onDisconnectRemoveValue()
usersTypingQuery = typingIndicatorRef.queryOrderedByValue().queryEqual(toValue: true)
usersTypingQuery.observe(.value) { (data: FIRDataSnapshot) in
// You're the only typing, don't show the indicator
if data.childrenCount == 1 && self.isTyping {
return
}
// Are there others typing?
self.showTypingIndicator = data.childrenCount > 0
self.scrollToBottom(animated: true)
}
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
// 1
let itemRef = messageRef.childByAutoId()
// 2
let messageItem = [
"senderId": senderId!,
"senderName": senderDisplayName!,
"text": text!,
]
// 3
itemRef.setValue(messageItem)
// 4
JSQSystemSoundPlayer.jsq_playMessageSentSound()
// 5
finishSendingMessage()
isTyping = false
}
func sendPhotoMessage() -> String? {
let itemRef = messageRef.childByAutoId()
let messageItem = [
"photoURL": imageURLNotSetKey,
"senderId": senderId!,
]
itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
return itemRef.key
}
func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
let itemRef = messageRef.child(key)
itemRef.updateChildValues(["photoURL": url])
}
// MARK: UI and User Interaction
private func setupOutgoingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.outgoingMessagesBubbleImage(with: UIColor.jsq_messageBubbleBlue())
}
private func setupIncomingBubble() -> JSQMessagesBubbleImage {
let bubbleImageFactory = JSQMessagesBubbleImageFactory()
return bubbleImageFactory!.incomingMessagesBubbleImage(with: UIColor.jsq_messageBubbleLightGray())
}
override func didPressAccessoryButton(_ sender: UIButton) {
let picker = UIImagePickerController()
picker.delegate = self
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera)) {
picker.sourceType = UIImagePickerControllerSourceType.camera
} else {
picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
}
present(picker, animated: true, completion:nil)
}
private func addMessage(withId id: String, name: String, text: String) {
if let message = JSQMessage(senderId: id, displayName: name, text: text) {
messages.append(message)
}
}
private func addPhotoMessage(withId id: String, key: String, mediaItem: JSQPhotoMediaItem) {
if let message = JSQMessage(senderId: id, displayName: "", media: mediaItem) {
messages.append(message)
if (mediaItem.image == nil) {
photoMessageMap[key] = mediaItem
}
collectionView.reloadData()
}
}
// MARK: UITextViewDelegate methods
override func textViewDidChange(_ textView: UITextView) {
super.textViewDidChange(textView)
// If the text is not empty, the user is typing
isTyping = textView.text != ""
}
}
// MARK: Image Picker Delegate
extension ChatViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String : Any]) {
picker.dismiss(animated: true, completion:nil)
// 1
if let photoReferenceUrl = info[UIImagePickerControllerReferenceURL] as? URL {
// Handle picking a Photo from the Photo Library
// 2
let assets = PHAsset.fetchAssets(withALAssetURLs: [photoReferenceUrl], options: nil)
let asset = assets.firstObject
// 3
if let key = sendPhotoMessage() {
// 4
asset?.requestContentEditingInput(with: nil, completionHandler: { (contentEditingInput, info) in
let imageFileURL = contentEditingInput?.fullSizeImageURL
// 5
let path = "\(FIRAuth.auth()?.currentUser?.uid)/\(Int(Date.timeIntervalSinceReferenceDate * 1000))/\(photoReferenceUrl.lastPathComponent)"
// 6
self.storageRef.child(path).putFile(imageFileURL!, metadata: nil) { (metadata, error) in
if let error = error {
print("Error uploading photo: \(error.localizedDescription)")
return
}
// 7
self.setImageURL(self.storageRef.child((metadata?.path)!).description, forPhotoMessageWithKey: key)
}
})
}
} else {
// Handle picking a Photo from the Camera - TODO
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion:nil)
}
}
The problem lies with this section of your code:
guard let senderDisplayName = message.senderDisplayName else {
assertionFailure()
return nil
}
assertionFailure() must link to another function that runs assert(false) or some equivalent to that. assert(_) is meant to verify that a particular parameter or comparison returns true, or if it does not, it will crash the app. The app will not crash if it is a production build (like those on the App Store) because asserts are meant for debugging purposes.
Basically, the guard statement is necessary to verify that message.senderDisplayName is unwrappable to some value (not nil). If message.senderDisplayName is nil, then there is no point in running the code below the guard and the contents of the guard should be run instead. assertionFailure() will crash the app during testing and during production it will be ignored. When it is ignored, nil will be returned for the function and it will continue on as nothing happened.

Resources