I was implementing the photo media item and noticed some extreme performance issues. When I started debugging I noticed that messageDataForItemAtIndexPath was getting called 4 times for each item, whereas in another sample project I set up it was being called 10 times
Here's a dummy project I set up to test and it's calling each item 10 times even though there is only one message item.
This was causing problems for me as that's the function where I start my async download of media photos and calling the download media request 10 times for each item was causing problems for me.
Am I mis-understanding something basic here?
import UIKit
import JSQMessagesViewController
class ViewController: JSQMessagesViewController {
var messages = [JSQMessage]()
override func viewDidLoad() {
super.viewDidLoad()
self.senderId = "1"
self.senderDisplayName = "me"
let message = JSQMessage(senderId: "admin", displayName: "Admin", text:"hello from admin")
self.messages.append(message)
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.messages.count
}
override func collectionView(collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
return JSQMessagesBubbleImageFactory().incomingMessagesBubbleImageWithColor(UIColor.blueColor())
}
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
return nil
}
override func collectionView(collectionView: JSQMessagesCollectionView!, messageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageData! {
//why is this called 10 times?
let msg = self.messages[indexPath.item];
let text = msg.text
print(text + ":" + String(indexPath.item))
return JSQMessage(senderId: msg.senderId, displayName: msg.senderDisplayName, text: text)
}
}
This will output in the console
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
hello from admin:0
It looks like this is by design.
Answered here:
https://github.com/jessesquires/JSQMessagesViewController/issues/1211
Related
When a cell is deleted, the item at the end of the list takes the place of the item that just got deleted. This only happens when there are more than 3 items in the list.
In the gif below I delete numbers 3 and 4 which leaves me with numbers 1,2,5 in the simulator. HOWEVER in the Realm file I have numbers 1,2,4. I have no clue why it does this?
Data Model
import Foundation
import RealmSwift
class Item: Object {
#objc dynamic var name = ""
}
View Controller
import UIKit
import RealmSwift
class ListViewController: UITableViewController {
let realm = try! Realm()
var itemArray : Results<Item>?
var item:Item?
override func viewDidLoad() {
super.viewDidLoad()
self.itemArray = realm.objects(Item.self)
}
#IBAction func addButtonPressed(_ sender: UIBarButtonItem) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Item", message: "", preferredStyle: .alert)
alert.view.tintColor = UIColor.red
let action = UIAlertAction(title: "Add Item", style: .default) { (action) in
let newItem = Item()
newItem.name = textField.text!
try! self.realm.write {
self.realm.add(newItem)
}
self.tableView.reloadData()
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Create new item"
textField = alertTextField
}
alert.addAction(action)
present(alert, animated: true, completion: nil)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.itemArray!.count//Size of the Array
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)//Asigns the Protocol Cell
let data = self.itemArray![indexPath.row]
cell.textLabel?.text = data.name
return cell
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if let item = itemArray?[indexPath.row] {
try! self.realm.write {
self.realm.delete(item)
}
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
}
You are retrieving an unsorted result set from Realm, which as per documentation does not necessarily retain insertion order after deletions (basically when you remove 3, then 5 is shifted in its place):
Note that the order of Results is only guaranteed to stay consistent when the query is sorted. For performance reasons, insertion order is not guaranteed to be preserved.
So there are two things you can do:
1.) sort the result set
2.) instead of assuming you're only deleting a single object and otherwise have no movements of any sort, you can rely on Realm's own diffing + change set evaluation with a notification token so that you receive a change set for any possible change that happens to the result set.
// see https://realm.io/docs/swift/latest/#collection-notifications
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
Goals
I'm trying to update a few records and delete them from NSTableView after that. http://take.ms/6r3IV
Expected Results
I expect that my records will be updated and NSTableView will be reloaded to reflect my changes.
Actual Results
My application crashes. http://take.ms/q6SWw
Steps to Reproduce
What are steps we can follow to reproduce this issue?
Code Sample
https://gist.github.com/msamoylov/27e1b6c9255b254f033f44d6de115d20
Version of Realm and Tooling
Realm version: 2.0.0 (installed as the Dynamic Framework)
Xcode version: 8.0 (8A218a)
macOS version: 10.12
It seems that I wasn't handling notifications properly. So, I ended up rewriting my code in this way:
import Cocoa
import RealmSwift
class ExerciseListViewController: NSViewController {
#IBOutlet weak var exerciseTableView: NSTableView!
let realm = try! Realm()
var exercises = try! Realm().objects(Exercise.self).filter("preferred = false").sorted(byProperty: "priority")
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
exerciseTableView.dataSource = self
exerciseTableView.delegate = self
exerciseTableView.doubleAction = #selector(self.addPreferredExercises(_:))
notificationToken = exercises.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard let exerciseTableView = self?.exerciseTableView else { return }
switch changes {
case .initial:
exerciseTableView.reloadData()
break
case .update(_, let deletions, let insertions, _):
exerciseTableView.beginUpdates()
exerciseTableView.insertRows(at: IndexSet(insertions), withAnimation: .slideDown)
exerciseTableView.removeRows(at: IndexSet(deletions), withAnimation: .effectFade)
exerciseTableView.endUpdates()
if self?.exercises.count == 0 {
self?.dismiss(nil)
}
break
case .error(let error):
fatalError("\(error)")
break
}
}
}
deinit {
notificationToken?.stop()
}
#IBAction func addPreferredExercises(_ sender: AnyObject) {
if exerciseTableView.selectedRowIndexes.isEmpty {
let alert = NSAlert()
alert.messageText = "Hint"
alert.informativeText = "Please select at least one exercise from the list."
alert.alertStyle = .informational
alert.beginSheetModal(for: view.window!, completionHandler: nil)
}
try! realm.write {
for index in exerciseTableView.selectedRowIndexes {
if exercises.indices.contains(index) {
let exercise = exercises[index]
exercise.preferred = true
}
}
}
}
}
extension ExerciseListViewController: NSTableViewDataSource, NSTableViewDelegate {
func numberOfRows(in tableView: NSTableView) -> Int {
return exercises.count
}
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
return exercises.indices.contains(row) ? exercises[row].name : nil
}
func tableView(_ tableView: NSTableView, toolTipFor cell: NSCell, rect: NSRectPointer, tableColumn: NSTableColumn?, row: Int, mouseLocation: NSPoint) -> String {
let exercise = exercises[row]
let comment = exercise.comment as String!
return comment!
}
}
It still doesn't work 100% correct with multiple selections, but at least it's not crashing the app anymore. For some reason exerciseTableView.selectedRowIndexes contains non-existing indexes.
I have two functions in a view controller. The first function parses JSON and makes an array; another generates a table with the array data. The problem is that it seems that the first function cannot send its array data to the second function.
Here is the code:-
class secondViewController: UIViewController, UITableViewDataSource {
let chartTitle:[String] = ["Name",......]
func parseJSON(){
let url = NSURL(string: "http://00000.us-west-2.elasticbeanstalk.com/index.php?000000")
let request = NSURLRequest(URL: url!)
do {
let data = try NSURLConnection.sendSynchronousRequest(request, returningResponse: nil)
do {
let json = try NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers)
var name = json["Name"]
var chartContent:[String] = ["\(name)",.....] //Contents of current chart contents
} catch{
//Handle Exception
}
} catch{
//Handle Exception
}
}
override func viewDidLoad() {
parseJSON()
...
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { //currnet table information.
let cell = UITableViewCell()
cell.textLabel?.text = chartTitle[indexPath.row] + "\t\t\t\t\t here comes info" + chartContent[indexPath.row]
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return chartTitle.count
}
}
This code has an error at the tableView function:
Use of unresolved identifier 'chartContent'
I tried to declare the variables outside the first function which is right under the class secondViewController but there was another error on UITableViewDataSource.
Any solution for these?
Charttitle is defined outside any procedure, so it's available everywhere. Chartcontent is defined in a block, so it's usable just in it's block
Its because chartContent is a local variable just available to parseJson func only and its scope is till that func block. You have to create this variable the same way you dis chartTitle to be available throughout the class.
In my app. There will be user list and message list as my code below
message list code (load the list from parse)
#IBOutlet var messageTableView: UITableView!
var messageArray:[String] = ["Lope"]
override func viewDidLoad() {
super.viewDidLoad()
retrieveMessages()
}
func retrieveMessages() {
var query = PFQuery(className:"Messages")
var user = PFUser.currentUser()
query.whereKey("user", equalTo:user.objectId)
query.findObjectsInBackgroundWithBlock { [weak self]
(objects:[AnyObject]?, error:NSError?) -> Void in
println(objects)
println("succeed")
let messages = objects
for object in objects!{
if let message = object["messageTextColumn"] as? String {
println(object)
self?.messageArray.append(message)
}
}
self?.tableView.reloadData()
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messageArray.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("messageCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = messageArray[indexPath.row]
return cell
}
add message code (add new message to parse)
class addMessageViewController: UIViewController {
#IBOutlet weak var addMessageText: UITextField!
#IBAction func addMessage(sender: AnyObject) {
var newMessage = addMessageText.text
let message = PFObject(className: "Messages")
var user = PFUser.currentUser()
message["messageTextColumn"] = newMessage
message["user"] = user.objectId
message.saveInBackgroundWithBlock {(success: Bool, error: NSError?) -> Void in
if (success) {
println("added to Message Class")
println(user)
message.saveInBackground()
} else {
// Error saving message
}
}
}
I want to use parse local datastore to store these data in my app locally so that my app won't have to use the internet connect all the time and when the user is not connect to the internet the user list and message list will still appear.
The problem is I don't know what method in local datastore should I use where should I put the local datastore code in "add message code" to save the new message and in "message list code" to query it to my app locally and if there's any update, It will do later after our local "message list" has been loaded. Any help is appreciated.
Thanks!
To begin with Parse data store, you need to opt in from your app delegate:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
// Opt in for Parse Local data store *Before Parse.setApplicationId*
Parse.enableLocalDatastore()
Parse.setApplicationId("YOUR PARSE APP ID",
clientKey: "YOUR PARSE CLIENT ID")
//... other code that you might need when app did finish launching
return true
}
Later when you save a new message you will use:
message.saveEventually()
This will save in the local data store, and eventually (when internet will be available) in the remote data store.
From here you might also be interested in the use of Parse data pinning.
See Parse doc for more.
Hope this helps
This is the previous screen.
Then, I clicked the search bar:
Code is below:
// MARK: - searchController
func initSearchController() {
self.resultSearchController = ({
let controller = UISearchController(searchResultsController: nil)
controller.searchResultsUpdater = self
controller.dimsBackgroundDuringPresentation = false
controller.searchBar.sizeToFit()
controller.searchBar.backgroundColor = UIColor.clearColor()
self.tableView.tableHeaderView = controller.searchBar
return controller
})()
}
Please help me. Thanks.
Set the title of the section to nil, if there are no rows in that section, this would hide the section title
// Set the title of the section header, return nil if no rows in that section
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if self.tableView(tableView, numberOfRowsInSection: section) == 0 {
return nil
} else {
return "section title \(section)"
}
}
This will work, assuming tableView(tableView: UITableView, numberOfRowsInSection section: Int) is implemented properly to reflect the number of sections based on the search result.
If you want to remove the section title altogether when the search bar is active, then just add this
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if self.resultSearchController.active {
return nil
} else {
return "section title \(section)"
}
}