I'm using JSQMessagesViewController to implement chat in my iOS app. I need to display some system messages in the middle of the screen (see attached picture). I was hoping that I can achieve that by using the message bottom label and not showing the message bubble. But I haven't found a way to hide the message bubble. Is it possible? Thanks.
I sort of achieved what I need by overriding collectionView sizeForItemAtIndexPath function and return a height of kJSQMessagesCollectionViewCellLabelHeightDefault, then return nil for both messageBubbleImageDataForItemAtIndexPath and avatarImageDataForItemAtIndexPath
override func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let userMessageCellSize = super.collectionView(collectionView, layout: collectionViewLayout, sizeForItemAtIndexPath: indexPath)
let message = messages[indexPath.item]
if message.type == MessageType.System {
// for system message, only show bottom label, plus the top label when timestamp needs to be displayed
// this will hide the avatar image and message bubble which are not needed for system messages.
var newHeight: CGFloat = 0
if (shouldDisplayTimestamp(indexPath)) {
newHeight = kJSQMessagesCollectionViewCellLabelHeightDefault * 2
} else {
newHeight = kJSQMessagesCollectionViewCellLabelHeightDefault
}
return CGSizeMake(userMessageCellSize.width, newHeight)
} else {
return super.collectionView(collectionView, layout: collectionViewLayout, sizeForItemAtIndexPath: indexPath)
}
}
override func collectionView(collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item]
if message.type == MessageType.System {
return nil
}
if message.senderId == senderId{
return self.outgoingBubbleImageView
} else {
return self.incomingBubbleImageView
}
}
override func collectionView(collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAtIndexPath indexPath: NSIndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.item]
if message.type == MessageType.System {
return nil
}
if let avatar = avatars[message.senderDisplayName] {
return avatar
} else {
setupAvatarImage(message.senderDisplayName, imageUrl: message.profileImgUrl, incoming: true)
return avatars[message.senderDisplayName]
}
}
Related
This is the data model I used to import a json file.
struct ActionResult: Codable {
let data: [Datum]
}
struct Datum: Codable {
let goalTitle, goalDescription, goalImage: String
let action: [Action]
}
struct Action: Codable {
let actionTitle: String
let actionGoal: String
Now I am trying to create a searchbar, but it only searches in one section, although I have not defined a section in the data model but it is picking either the goalTitle or actionGoal.
var index = 0
var action: Action? // this is for the segue
var result: ActionResult? {
didSet {
guard let result = result else { return }
allSectionDataActionMap = Dictionary(uniqueKeysWithValues: result.data.enumerated().map { ($0.0, ($0.1, $0.1.action)) })
updateFilteredData()
}
}
var allSectionDataActionMap = [Int: (datum: Datum, actions: [Action])]()
// Maps the section index to the Datum & filtered [Action]
var filteredSectionDataActions = [Int: (datum: Datum, actions: [Action])]()
let searchController = UISearchController()
This is part of the setup code.
override func numberOfSections(in tableView: UITableView) -> Int {
return filteredSectionDataActions.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredSectionDataActions[section]?.actions.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ActionTableView.dequeueReusableCell(withIdentifier: "ActionCell", for: indexPath) as! ActionTableCell
let action = filteredSectionDataActions[indexPath.section]?.actions[indexPath.row]
// setup cell for action
cell.actionItem.text = action?.actionTitle
cell.actionImage.image = UIImage(named: action!.actionImage)
cell.actionImage.layer.cornerRadius = cell.actionImage.frame.size.width / 2
cell.actionGoal.text = action?.actionGoal
// cell.actionBenefit.text = action?.actionBenefit
// cell.actionCalculator.text = action?.actionCalculator
This is the table view code
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
updateFilteredData(for: searchText.lowercased())
tableView.reloadData()
}
func updateFilteredData(for searchText: String = String()) {
if searchText.isEmpty {
filteredSectionDataActions = allSectionDataActionMap
} else {
for (index, (datum, actions)) in allSectionDataActionMap {
let filteredActions = actions.filter { $0.actionTitle.lowercased().contains(searchText) }
if filteredActions.isEmpty {
filteredSectionDataActions[index] = (datum, actions)
} else {
filteredSectionDataActions[index] = (datum, filteredActions)
}
This is for the searchbar.
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()
}
}
I have one table view using a different cell based on what tab you select. It is working, although the 1st one seems to be controlling the height. I have one row in the first tab but 2 in the second and both just show 1.
I'm wondering if I need to clear dequeueReusableCell on tab select or something like that. Any Ideas?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if activeTab == "tab1" {
return someService.tab1.count
} else {
return someService.tab2.count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if activeTab == "tab1" {
if let cell = tableView.dequeueReusableCell(withIdentifier: "Tab1Cell") as? Tab1Cell {
let tab1 = someService.tab1[indexPath.row]
cell.updateView(tab1: tab1)
return cell
} else {
return Tab1Cell()
}
} else {
if let cell = tableView.dequeueReusableCell(withIdentifier: "Tab2Cell") as? Tab2Cell {
let tab2 = someService.tab2[indexPath.row]
cell.updateView(tab2: tab2)
return cell
} else {
return Tab2Cell()
}
}
}
I'm also fixing the scroll with this:
func fixTableViewSize() {
let cells = self.notesTable.visibleCells
var heightOfTableView = 0
for cell in cells {
print("cell")
heightOfTableView += Int(cell.frame.height)
}
self.tableViewHeightConstraint.constant = CGFloat(heightOfTableView)
self.buttonFromTableTopConstraint.constant = CGFloat(heightOfTableView)
}
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)"
}
}
I have implemented a basic example of an ios app using Realm.io
I'd like to be able to reorder table rows in my iOS app and save the order back to Realm.
Realm model contains a property called position for this purpose.
P.S: Sorry for so much code.
import UIKit
import Realm
class Cell: UITableViewCell {
var position: Int!
init(style: UITableViewCellStyle, reuseIdentifier: String!) {
super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
}
}
class Language: RLMObject {
var title = ""
var position = Int()
}
class ManagerLanguagesController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
var array = RLMArray()
var notificationToken: RLMNotificationToken?
var editButton = UIBarButtonItem()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
notificationToken = RLMRealm.defaultRealm().addNotificationBlock { note, realm in
self.reloadData()
}
reloadData()
}
override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return Int(array.count)
}
func setupUI() {
tableView.registerClass(Cell.self, forCellReuseIdentifier: "cell")
self.title = "Languages"
var addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "add")
editButton = UIBarButtonItem(title: "Edit", style: .Plain, target: self, action: "edit")
var buttons = [addButton, editButton]
self.navigationItem.rightBarButtonItems = buttons
}
func add() {
var addLanguageView:UIViewController = self.storyboard.instantiateViewControllerWithIdentifier("newLanguage") as UIViewController
self.navigationController.presentViewController(addLanguageView, animated: true, completion: nil)
}
func edit () {
if tableView.editing {
/* FROM THIS POINT I'M PROBABLY DOING SOMETHING WRONG.. IT IS NOT WORKING */
var positionArray = NSMutableArray()
let realm = RLMRealm.defaultRealm()
var i = 0
for var row = 0; row < tableView.numberOfRowsInSection(0); row++ {
var cellPath = NSIndexPath(forRow: row, inSection: 0)
var cell:Cell = tableView.cellForRowAtIndexPath(cellPath) as Cell
positionArray.addObject(cell.position)
}
realm.beginWriteTransaction()
for row: RLMObject in array {
row["position"] = positionArray[i]
i++
}
realm.commitWriteTransaction()
/* -- NOT WORKING END -- */
tableView.setEditing(false, animated: true)
editButton.style = UIBarButtonItemStyle.Plain
editButton.title = "Edit"
} else{
tableView.setEditing(true, animated: true)
editButton.title = "Done"
editButton.style = UIBarButtonItemStyle.Done
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell? {
let cell = tableView!.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as Cell
let object = array[UInt(indexPath!.row)] as Language
cell.textLabel.text = object.title
cell.position = object.position // I have implemented this to be able to retain initial positions for each row and maybe use this when reordering..
return cell
}
override func tableView(tableView: UITableView!, canMoveRowAtIndexPath indexPath: NSIndexPath!) -> Bool {
return true
}
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
// println("Old index: \(sourceIndexPath.indexAtPosition(sourceIndexPath.length - 1)+1)")
// println("New index: \(destinationIndexPath.indexAtPosition(sourceIndexPath.length - 1)+1)")
// Maybe something needs to be implemented here instead...
}
func reloadData() {
array = Language.allObjects().arraySortedByProperty("position", ascending: true)
tableView.reloadData()
}
}
Thanks in advance
Instead of using a position property, you could instead keep an ordered array as a property on another object. This way you don't have to keep the position up to date and instead arrange your objects as needed:
class Language: RLMObject {
dynamic var title = ""
}
class LanguageList: RLMObject {
dynamic var languages = RLMArray(objectClassName: "Language")
}
class ManagerLanguagesController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
// create our list
var realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.addObject(LanguageList())
realm.commitWriteTransaction()
...
}
// helper to get the RLMArray of languages in our list
func array() -> RLMArray {
return (LanguageList.allObjects().firstObject() as LanguageList).languages
}
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
var languages = array()
var object = languages.objectAtIndex(UInt(sourceIndexPath.row)) as Language
var realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
languages.removeObjectAtIndex(UInt(sourceIndexPath.row))
languages.insertObject(object, atIndex: UInt(destinationIndexPath.row))
realm.commitWriteTransaction()
}
...
}
this work for me to move rows in tableview using realm with swift 2.2:
func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
let aux = TimesHome.mutableCopy() as! NSMutableArray
let itemToMove = aux[fromIndexPath.row]
let realm = try! Realm()
realm.beginWrite()
aux.removeObjectAtIndex(fromIndexPath.row)
aux.insertObject(itemToMove, atIndex: toIndexPath.row)
try! realm.commitWrite()
TimesHome = aux
let times = realm.objects(ParciaisTimes)
if times.count > 0 {
for tm in times {
for i in 1...aux.count {
if aux[i-1].valueForKey("time_id") as! Int == tm.time_id {
realm.beginWrite()
tm.ordem = i
try! realm.commitWrite()
}
}
}
}
}