I have a NSMutableArray and I'm trying to create a search bar. I have been looking for hours and I can't find anything that really helps me because I'm using the NSMutableArray with Firebase. I'm kinda new to Swift. I have created a table view and a search bar. The table view is showing data from the NSMutableArray. How can I make it so when the user is searching on the search bar then the whole post comes up?
If there is a post that contains 2 Strings and an image and one String is "A City" and the other String is the title of the post, and let's say that when the user is searching only for the title of the post or only the city, then the entire post shows. If the result could be on the same viewcontroller like on the same tableview.
I have written this so far in my viewDidLoad:
func setUpSearchBar() {
let searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
searchController.dimsBackgroundDuringPresentation = true
definesPresentationContext = true
}
And my array is like this:
var posts = NSMutableArray()
The only thing really that I have written is so that the search bar is setup but not really functional because I have not found anything that could help me and since I'm new to this is not either sure how to make the search bar work at all. All the tutorials I have to find so far is very normal arrays like:
var post = ["pear", "orange", "apple"]
But I cant find on how to make the search iterate through my post NSMutableArray.
I would be really happy if anyone could possibly help me through this. I don't want straight answers if possible with some explanation so I can learn this and create this myself.
this is the code that downloads the data and displays it in the TableView
import UIKit
import FirebaseStorage
import FirebaseDatabase
import FirebaseAuth
import FirebaseCore
import Firebase
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var postsTableView: UITableView!
var posts = NSMutableArray()
override func viewDidLoad() {
super.viewDidLoad()
setUpSearchBar()
loadData()
self.postsTableView.delegate = self
self.postsTableView.dataSource = self
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func loadData() {
Database.database().reference().child("posts").queryOrdered(byChild: "timeorder").observeSingleEvent(of: .value) { (snapshot) in
if let postsDictionary = snapshot.value as? [String: AnyObject] {
for post in postsDictionary {
self.posts.add(post.value)
}
self.postsTableView.reloadData()
}
}
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return self.posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! PostTableViewCell
// Configure the cell...
let post = self.posts[indexPath.row] as! [String: AnyObject]
cell.titleLabel.text = post["title"] as? String
cell.contentTextView.text = post["content"] as? String
cell.dateAndTimeLabel.text = post["time"] as? String
cell.usernameLabel.text = post["username"] as? String
cell.locationAdressLabel.text = post["adress"] as? String
if let imageName = post["image"] as? String {
let imageRef = Storage.storage().reference().child("images/\(imageName)")
imageRef.getData(maxSize: 25 * 1024 * 1024) { (data, error) -> Void in
if error == nil {
//successfull
let downloadedImage = UIImage(data: data!)
cell.postsImageView.image = downloadedImage
}else {
// error
print("there was an error downloading image: \(String(describing: error?.localizedDescription))")
}
}
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 500.0
}
#IBAction func goToProfilePage(_ sender: Any) {
let logoutSuccess = self.storyboard?.instantiateViewController(withIdentifier: "ProfileVC")
self.present(logoutSuccess!, animated: true, completion: nil)
}
#IBAction func navigateButton(_ sender: Any) {
let navigate = self.storyboard?.instantiateViewController(withIdentifier: "NavigateVC")
self.present(navigate!, animated: true, completion: nil)
}
#IBAction func chooseCountry(_ sender: Any) {
let navigate = self.storyboard?.instantiateViewController(withIdentifier: "CountryVC")
self.present(navigate!, animated: true, completion: nil)
}
func setUpSearchBar() {
let searchController = UISearchController(searchResultsController: nil)
navigationItem.searchController = searchController
searchController.dimsBackgroundDuringPresentation = true
definesPresentationContext = true
}
And Here is The Code That uploads the post to Firebase
import UIKit
import FirebaseDatabase
import FirebaseStorage
import FirebaseAuth
import Firebase
class PostViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
#IBOutlet weak var addImageButton: UIBarButtonItem!
#IBOutlet weak var pickCountryPicker: UIPickerView!
#IBOutlet weak var choosenCountryLabel: UILabel!
#IBOutlet weak var titleTextField: UITextField!
#IBOutlet weak var contentTextView: UITextView!
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var locationAdressTextField: UITextField!
var imageFileName = ""
var timeStamps = ""
var secondTimeStamps = ""
override func viewDidLoad() {
super.viewDidLoad()
let currentDateTime = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
timeStamps = "\(dateFormatter.string(from: currentDateTime))"
let secondDateFormatter = DateFormatter()
secondDateFormatter.dateFormat = "yyyy-MM-dd HH:00:00"
secondTimeStamps = "\(secondDateFormatter.string(from: currentDateTime))"
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func postButton(_ sender: Any) {
if (self.imageFileName != "") {
if choosenCountryLabel.text == "Afghanistan" {
// image has finshed the uploading, Saving Post!!!
if let uid = Auth.auth().currentUser?.uid {
Database.database().reference().child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
if let userDictionary = snapshot.value as? [String: AnyObject] {
for user in userDictionary{
if let username = user.value as? String {
if let streetAdress = self.locationAdressTextField.text {
if let title = self.titleTextField.text {
if let content = self.contentTextView.text {
let postObject: Dictionary<String, Any> = [
"uid" : uid,
"title" : title,
"content" : content,
"username" : username,
"time" : self.timeStamps,
"timeorder" : self.secondTimeStamps,
"image" : self.imageFileName,
"adress" : streetAdress
]
Database.database().reference().child("posts").childByAutoId().setValue(postObject)
Database.database().reference().child("Afghanistanposts").childByAutoId().setValue(postObject)
Database.database().reference().child(uid).childByAutoId().setValue(postObject)
let alertPosting = UIAlertController(title: "Successfull upload", message: "Your acty was successfully uploaded.", preferredStyle: .alert)
alertPosting.addAction(UIAlertAction(title: "OK", style: .default, handler: { (action) in
let vc = self.storyboard?.instantiateViewController(withIdentifier: "AfghanistanVC")
self.present(vc!, animated: true, completion: nil)
}))
self.present(alertPosting, animated: true, completion: nil)
print("Posted Succesfully to Firebase, Saving Post!!!")
}
}
}
}
}
}
})
}
}
}else{
let alertNotPosting = UIAlertController(title: "Seems like you got connection problems", message: "Your image has not been uploaded. Please Wait 10 seconds and try again.", preferredStyle: .alert)
alertNotPosting.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alertNotPosting, animated: true, completion: nil)
}
#IBAction func addImage(_ sender: Any) {
if addImageButton.isEnabled == false {
let alertNotPosting = UIAlertController(title: "Image already Picked", message: "sorry you already have an image picked", preferredStyle: .alert)
alertNotPosting.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alertNotPosting, animated: true, completion: nil)
}else {
let picker = UIImagePickerController()
picker.delegate = self
self.present(picker, animated: true, completion: nil)
}
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
self.imageView.image = pickedImage
uploadImage(image: pickedImage)
picker.dismiss(animated: true, completion: nil)
addImageButton.isEnabled = false
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true, completion: nil)
}
func uploadImage(image: UIImage){
let randomName = randomStringWithLength(length: 10)
let imageData = UIImageJPEGRepresentation(image, 1.0)
let uploadRef = Storage.storage().reference().child("images/\(randomName).jpg")
let uploadTask = uploadRef.putData(imageData!, metadata: nil) { metadata, error in
if error == nil {
//success
print("SuccessFully uploaded")
self.imageFileName = "\(randomName as String).jpg"
}else {
// not success = error
print("error with the Image: \(String(describing: error?.localizedDescription))")
}
}
}
func randomStringWithLength(length: Int) -> NSString {
let characters: NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var randomString: NSMutableString = NSMutableString(capacity: length)
for i in 0..<length {
var len = UInt32(characters.length)
var rand = arc4random_uniform(len)
randomString.appendFormat("%C", characters.character(at: Int(rand)))
}
return randomString
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return countries[row]
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return countries.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
choosenCountryLabel.text = countries[row]
}
}
and below this is a array full of countries for the picker and is not nessecary to show.
Why not just cast NSMutableArray to a Swift array? For safety I recommend using a guard statement like so:
guard let swiftArrayPosts = posts as? [String] else { return }
Then you can use Swift higher-order functions like filter to find the posts you want:
let filteredPosts = swiftArrayPosts.filter { $0 == "MyQuery" }
Then assign filteredPosts to whatever data source you are using to display them.
Related
The array saved in UIPickerView as a dictionary type UserDefaults in EventViewController disappears when it returns to ViewController.
Save the character put in UITextField with #IBAction of Button.
I want to keep the saved string.
I don't know the solution.
Xcode 12.2
class EventViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate {
#IBOutlet weak var partsPickerView: UIPickerView!
#IBOutlet weak var partsLabel: UILabel!
#IBOutlet weak var menuPickerView: UIPickerView!
#IBOutlet weak var menuLabel: UILabel!
#IBOutlet weak var menuTextField: UITextField!
Put a dictionary type array
var menuDataList: [String: [String]] = [
"Leg": ["Squat","Leg press","Leg extension"],
"Back": ["Deadlift","Bent over row","Chinning"],
"Chest": ["Barbell bench press","Dumbbell bench press","Incline Dumbbell Bench Press"]
]
var partsDataList: [String] = [
"Leg","Back","Chest"
]
var selectedParts = ""
Delegate settings
override func viewDidLoad() {
super.viewDidLoad()
partsPickerView.delegate = self
partsPickerView.dataSource = self
menuPickerView.delegate = self
menuPickerView.dataSource = self
menuTextField.delegate = self
partsPickerView.tag = 1
menuPickerView.tag = 2
selectedParts = partsDataList[0]
}
Instantiate UserDefaults
let userDefaults = UserDefaults.standard
let keyMenuDataList = "newMenu"
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
Add event button
#IBAction func didTapAddMenuButton(_ sender: Any) {
if (menuTextField.text?.isEmpty ?? true == false) {
let okAlert = UIAlertController(title: "保存されました。", message: "", preferredStyle: .alert)
let closeAction = UIAlertAction(title: "閉じる", style: .default) { (action: UIAlertAction) in }
okAlert.addAction(closeAction)
present(okAlert, animated: true, completion: nil)
if let text = menuTextField.text {
menuDataList[selectedParts]?.append(text)
menuDataList = userDefaults.dictionary(forKey: "keyMenuDataList") as? [String: [String]] ?? [:]
userDefaults.set(menuDataList, forKey: "keyMenuDataList")
}
} else {
let ngAlert = UIAlertController(title: "テキストが空です。", message: "", preferredStyle: .alert)
let closeAction = UIAlertAction(title: "閉じる", style: .default) { (action: UIAlertAction) in }
ngAlert.addAction(closeAction)
present(ngAlert, animated: true, completion: nil)
}
menuPickerView.reloadAllComponents()
}
UIPickerView settings
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView,
numberOfRowsInComponent component: Int) -> Int {
if pickerView.tag == 1{
return partsDataList.count
} else if pickerView.tag == 2{
return menuDataList[selectedParts]?.count ?? 0
} else {
return 0
}
}
func pickerView(_ picker: UIPickerView,
titleForRow row: Int,
forComponent component: Int) -> String? {
if picker.tag == 1 {
return partsDataList[row]
} else if picker.tag == 2 {
return menuDataList[selectedParts]?[row] ?? ""
} else {
return ""
}
}
func pickerView(_ pickerView: UIPickerView,
didSelectRow row: Int,
inComponent component: Int) {
if pickerView.tag == 1 {
partsLabel.text = partsDataList[row]
selectedParts = partsDataList[row]
menuPickerView.reloadAllComponents()
} else if pickerView.tag == 2 {
menuLabel.text = menuDataList[selectedParts]?[row] ?? ""
} else {
return
}
}
Let's see your code:
menuDataList = userDefaults.dictionary(forKey: "keyMenuDataList") as? [String: [String]] ?? [:]
userDefaults.set(menuDataList, forKey: "keyMenuDataList")
You assigned the userDefaults to menuDataList. But when you first get value for key from userDefaults, it's value is a nil. So your code is like:
menuDataList = [:];
userDefaults.set([:], forKey: "keyMenuDataList")
Your see, the userDefaults will always be [:] and you clean the menuDataList again and again.
This code: menuDataList = userDefaults.dictionary(forKey: "keyMenuDataList") as? [String: [String]] ?? [:] is useless, you can just remove it and try again.
Did you register the UserDefaults keys?
You need to call this function on every time you launch your app again.
UserDefaults.standard.register(defaults: [String : Any])
I moved this to viewDidLoad and solved it.
menuDataList = userDefaults.dictionary(forKey: "keyMenuDataList") as? [String: [String]] ?? [:]
override func viewDidLoad() {
super.viewDidLoad()
partsPickerView.delegate = self
partsPickerView.dataSource = self
menuPickerView.delegate = self
menuPickerView.dataSource = self
menuTextField.delegate = self
partsPickerView.tag = 1
menuPickerView.tag = 2
selectedParts = partsDataList[0]
menuDataList = userDefaults.dictionary(forKey: "keyMenuDataList") as? [String: [String]] ?? [:]
}
#IBAction func didTapAddMenuButton(_ sender: Any) {
if (menuTextField.text?.isEmpty ?? true == false) {
let okAlert = UIAlertController(title: "保存されました。", message: "", preferredStyle: .alert)
let closeAction = UIAlertAction(title: "閉じる", style: .default) { (action: UIAlertAction) in }
okAlert.addAction(closeAction)
present(okAlert, animated: true, completion: nil)
if let text = menuTextField.text {
menuDataList[selectedParts]?.append(text)
userDefaults.set(menuDataList, forKey: "keyMenuDataList")
}
} else {
let ngAlert = UIAlertController(title: "テキストが空です。", message: "", preferredStyle: .alert)
let closeAction = UIAlertAction(title: "閉じる", style: .default) { (action: UIAlertAction) in }
ngAlert.addAction(closeAction)
present(ngAlert, animated: true, completion: nil)
}
menuPickerView.reloadAllComponents()
}
I have created a pickerView to pick value into custom textField bar on top of the ViewController to filter data values in UITableView. As I have a nested type of JSON so couldn't properly get how to use filter for it.
I want to implement textField as a search bar with TableView. screenshot attached.
Value which I need to filter is Parkings.Address -> textField
textFieldDidEndEditing function added for text search.
Model:
struct JSONDB: Codable {
var ParkingInfoList: [`Parkings`]
}
struct Parkings: Codable {
var Id: String
var Name: String
var Tel: String
var CarTotal: String
var CarRemaining: String
var Address: String
var ServiceTime: String
var ChargeInfo: String
var ParkinfInfo: String?
var Lat: String
var Lng: String
}
struct JSONDB2: Codable {
var ParkingCityList: [City]
}
struct City: Codable {
var CityCode: String
var CityChtName: String
var CityArea: [String]
}
JSON Data:
{
"ParkingInfoList": [
{
"Id": "K82",
"Name": "瑞湖停車場",
"Tel": "0226573430",
"Address": "文德段5小段297地號;民權東路6段15巷50號空地",
"CarTotal": "46",
"CarRemaining": "暫時無提供資訊",
"MotorTotal": "暫時無提供資訊",
"MotorRemaining": "暫時無提供資訊",
"ServiceTime": "24小時",
"ChargeInfo": "計次:50元/次,(每次以2.5小時為限)。月租:3,000元/月。",
"ParkingInfo": "一般平面式小型車46格(含身心障礙停車位2格)。",
"City": "TPE",
"Area": "內湖區",
"Lat": "25.06854351",
"Lng": "121.5769806",
"SourceUpdateTime": "2020-09-24T00:00:00"
},
ViewController Code:
import UIKit
class SelectedTableViewController: UITableViewController, UIPickerViewDelegate, UIPickerViewDataSource,UITextFieldDelegate {
#IBOutlet weak var myTextfield: UITextField!
let myPickerView = UIPickerView()
var selectedArea = ""
var loadingData = [City]()
var parkingData = [Parkings]()
var okData : Parkings?
var searchActive = false
var arrfilter = [Parkings]()
let apiAddress = "https://lifeinfo.megatime.com.tw/lifeInfo/QueryParkingCityList.ashx"
let urlSession = URLSession(configuration: .default)
override func viewDidLoad() {
super.viewDidLoad()
self.myPickerView.delegate = self
self.myPickerView.dataSource = self
self.myTextfield.delegate = self
downloadInfo(withAddress: apiAddress)
myTextfield.inputView = myPickerView
myPickerView.showsSelectionIndicator = true
}
func downloadInfo(withAddress webAddress: String ){
if let url = URL(string: webAddress){
let task = urlSession.dataTask(with: url) { (data, response, error) in
if error != nil{
let errorcode = (error! as NSError).code
if errorcode == -1009{
print("No Internet Connection")
}else{
print("Something Wrong")
}
return
}
if let loadedData = data{
print("Got Data")
do{
let decoder = JSONDecoder()
let JSONdb = try decoder.decode(JSONDB2.self, from: loadedData)
print("JSONDB2 OK")
self.loadingData = JSONdb.ParkingCityList
DispatchQueue.main.async {
self.tableView.reloadData()
}
}catch{
print(error)
}
}
}
task.resume()
}
}
func initPickerView(touchAt sender:UITextField){
let toolBar = UIToolbar()
toolBar.barStyle = UIBarStyle.default
toolBar.isTranslucent = true
toolBar.tintColor = .systemBlue
toolBar.sizeToFit()
let doneButton = UIBarButtonItem(title: "check", style: .plain, target: self, action: #selector(submit))
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let cancelButton = UIBarButtonItem(title: "cancel", style: .plain, target: self, action: #selector(cancel))
toolBar.setItems([cancelButton, spaceButton, doneButton], animated: false)
toolBar.isUserInteractionEnabled = true
myTextfield.inputAccessoryView = toolBar
myTextfield.inputView = myPickerView
}
#objc func cancel(){
self.myTextfield?.resignFirstResponder()
}
#objc func submit(sender: UIBarButtonItem){
DispatchQueue.main.async { [self] in
self.myTextfield.text = "taipei \(self.selectedArea)"
self.myTextfield?.resignFirstResponder()
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
2
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if component == 0{
return 1
}
return loadingData[0].CityArea.count
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if component == 0{
return "taipei"
}
let test = loadingData[0].CityArea[row]
// print(test)
return test
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
if textField == self.myTextfield {
myTextfield.isHidden = false
myPickerView.removeFromSuperview()
self.initPickerView(touchAt: textField)
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
myTextfield.text = "臺北市 \(loadingData[0].CityArea[row])"
self.selectedArea = loadingData[0].CityArea[row]
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if myTextfield.text == ""{
return parkingData.count
}else {
myTextfield.text = selectedArea
}
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SelectedTableViewCell", for: indexPath) as! SelectedTableViewCell
let data = parkingData[indexPath.row]
cell.lbRemaining.text = data.CarRemaining
cell.lbName.text = data.Name
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.okData = self.parkingData[indexPath.row]
performSegue(withIdentifier: "DetailSegue", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "DetailSegue"{
let detail = segue.destination as! DetailTableViewController
detail.okData = self.okData
}
}
}
Below is the filter part I already tried but failed:
try 1: predicate way but show error: "Unable to parse the format string "SELF.Address CONTAINTS[C] %#""
try 2: filter way but show error: "Cannot convert value of type 'Parkings' to type 'NSString' in coercion "
func textFieldDidEndEditing(_ textField: UITextField) {
guard let searchText:String = myTextfield.text else {
return}
\\ try 1: predicate way but show error: "Unable to parse the format string \"SELF.Address CONTAINTS[C] %#\""
let predicate:NSPredicate = NSPredicate(format: "SELF.Address CONTAINTS[C] %#", searchText)
let array = (parkingData as NSArray).filtered(using: predicate)
print("array =\(array)")
\\ try 2: filter way but show error: "Cannot convert value of type 'Parkings' to type 'NSString' in coercion "
arrfilter = parkingData.filter({ (test) -> Bool in
let tmp: NSString = test as NSString
let range = tmp.range(of: searchText, options: NSString.CompareOptions.caseInsensitive)
return range.location != NSNotFound
})
if(arrfilter.count == 0){
searchActive = false;
} else {
searchActive = true;
}
self.tableView.reloadData()
}
The final present should shown like this
I am trying to display users for a messaging app in a TableView however when I scroll it updates the cells and therefore pulls the data again thus lagging. I am using a prototype cell in a storyboard.
import UIKit
import FirebaseAuth
import FirebaseDatabase
class TableViewCell: UITableViewCell {
#IBOutlet weak var profilePicture: UIImageView!
#IBOutlet weak var statusImage: UIImageView!
#IBOutlet weak var nameLabel: UILabel!
override func prepareForReuse() -> Void {
profilePicture.image = nil
statusImage.backgroundColor = UIColor.systemGreen
nameLabel.text = nil
}
}
class MainController: UITableViewController {
#IBOutlet weak var tableViews: UITableView!
var users = [String]()
var decryptedUsers = [String]()
#IBAction func signOut(_ sender: Any) {
do {
let userid = Auth.auth().currentUser?.uid
let dbReference = Database.database().reference().child("Users").child(userid!).child("status")
dbReference.observeSingleEvent(of: .value, andPreviousSiblingKeyWith: { snapshot,error in
if (snapshot.value as? String == "online") {
dbReference.setValue("offline")
}
})
try Auth.auth().signOut()
self.performSegue(withIdentifier: "backToSignIn", sender: nil)
} catch {
let alert = UIAlertController(title: "Error", message: "There was an error signing out. Please ensure that you are connected to the internet.", preferredStyle: UIAlertController.Style.alert)
let okButton = UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil)
alert.addAction(okButton)
self.present(alert, animated: true, completion: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
setOnline()
fetchUsers()
}
static var userslist = [String]()
func fetchUsers() {
let userid = (Auth.auth().currentUser?.uid)!
let dbRef = Database.database().reference().child("Chatlist").child(userid)
dbRef.observe(.value) { (snapshot, error) in
self.users.removeAll()
for yeet in snapshot.children {
let yeetsnap = yeet as! DataSnapshot
self.users.append(yeetsnap.childSnapshot(forPath: "id").value as! String)
print(self.users)
self.tableViews.reloadData()
}
}
}
func setOnline() {
let userid = Auth.auth().currentUser?.uid
let dbReference = Database.database().reference().child("Users").child(userid!).child("status")
dbReference.observeSingleEvent(of: .value, andPreviousSiblingKeyWith: { snapshot,error in
if (snapshot.value as? String == "offline") {
dbReference.setValue("online")
}
})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: TableViewCell?
cell = nil
if (cell == nil) {
cell = (tableView.dequeueReusableCell(withIdentifier: "CellProto") as? TableViewCell)!
DispatchQueue.main.async {
cell!.profilePicture.layer.masksToBounds = true
cell!.profilePicture.layer.cornerRadius = (cell!.profilePicture.bounds.width) / 2
cell!.statusImage.layer.masksToBounds = true
cell!.statusImage.layer.cornerRadius = (cell!.statusImage.bounds.width) / 2
let dbRef = Database.database().reference().child("Users").child(self.users[indexPath.row])
dbRef.observeSingleEvent(of: .value) { (snapshot) in
cell!.nameLabel?.text = snapshot.childSnapshot(forPath: "username").value as? String
let urlstring = snapshot.childSnapshot(forPath: "imageURL").value as? String
if (urlstring != "default") {
let url = URL(string: urlstring!)
let data = try? Data(contentsOf: url!)
cell!.profilePicture.image = UIImage(data: data!)
}
let statusstring = snapshot.childSnapshot(forPath: "status").value as? String
if (statusstring == "online") {
cell!.statusImage.backgroundColor = UIColor.systemGreen
} else if (statusstring == "offline") {
cell!.statusImage.backgroundColor = UIColor.systemGray
} else if (statusstring == "appearoffline") {
cell!.statusImage.backgroundColor = UIColor.systemGray
} else if (statusstring == "dnd") {
cell!.statusImage.backgroundColor = UIColor.systemRed
}
}
}
}
print("test")
return cell!
}
}
Do not load the data every time within the cellForRow. This reloads the data every time you scroll, causing the table to lag.
A better approach would be to load the data once by putting the database retrieving code in here:
func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
// retrieve the data here by calling a separate function
}
Whatever you do, it is not a good coding approach to load data within the cellForRow method.
You can also retrieve the data by calling the function whenever the user swipes down from the top of the table to reload the table.
One more option would be to only load a number of rows at once (10 max for example), then when the user scroll to the bottom of the 10 rows and keeps scrolling it will then retrieve the next 10 datapoints for the next 10 rows in the table.
How can I update or insert a row in a tableview without reloading all the data?
In the ClientsViewController a list of clients is shown alphabetically and separated by sections (first letter of the client`s name).
When I update a client, the tableview shows 2 entries (old and new one) of the same client. When I try to add a client, it crashes.
I think the problem is with indexing.
class ClientsViewController: UITableViewController {
var sortedFirstLetters: [String] = []
var sections: [[Client]] = [[]]
var tableArray = [Client]()
var client: Client?
var refresher: UIRefreshControl!
#IBOutlet var noClientsView: UIView!
#IBAction func unwindToClients(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? ClientViewController, let client = sourceViewController.client {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing client.
tableArray[selectedIndexPath.row] = client
tableView.reloadRows(at: [selectedIndexPath], with: .automatic)
}
else {
// Add a client.
let newIndexPath = IndexPath(row: tableArray.count, section: 0)
tableArray.append(client)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
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 }
}
// 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()
refreshControl.endRefreshing()
}
}
extension ClientsViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: #selector(ClientsViewController.handleRefresh(_:)), for: UIControlEvents.valueChanged)
tableView.backgroundView = noClientsView
getClients() //for only the 1st time ==> when view is created ==> ok ish
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getClients() // not a good idea to make a request to the server everytime the view appears on the screen.
}
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]? {
print(sortedFirstLetters)
return sortedFirstLetters
}
override func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let name = sections[indexPath.section][indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "ClientCell", for: indexPath)
cell.textLabel?.text = name.name
cell.detailTextLabel?.text = name.city + " - " + name.province
return cell
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if(sections[section].count > 0) {
tableView.backgroundView = nil
}
return sections[section].count
}
func getClients() {
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 {
self.tableView.reloadData()
}
} )
}
//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 }
}
}
}
Bellow is ClientViewController and Client struct.
class ClientViewController: UITableViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var addressTextField: UITextField!
#IBOutlet weak var cityTextField: UITextField!
#IBOutlet weak var provinceTextField: UITextField!
#IBOutlet weak var postalCodeTextField: UITextField!
#IBOutlet weak var contactsLabel: UILabel!
let numberOfRowsAtSection: [Int] = [4, 2]
var client: Client?
var selectedProvince: String?
override func viewWillAppear(_ animated: Bool) {
self.title = "New"
if (client?.client_id) != nil {
self.title = "Edit"
nameTextField.text = client?.name
provinceTextField.text = client?.province
cityTextField.text = client?.city
addressTextField.text = client?.address
postalCodeTextField.text = client?.postal_code
selectedProvince = client?.province
}
}
#objc func save(sender: UIButton!) {
let name = nameTextField.text ?? ""
let address = addressTextField.text ?? ""
let city = cityTextField.text ?? ""
let province = selectedProvince ?? ""
let postal_code = postalCodeTextField.text ?? ""
var endPoint: String
if (client?.client_id) != nil {
endPoint = "api/clients/update"
} else {
endPoint = "api/clients/add"
}
client = Client(name:name, client_id: client?.client_id, postal_code: postal_code, province: province, city: city, address: address)
let requestBody = makeJSONData(client)
makeRequestPost(endpoint: endPoint,
requestType: "POST",
requestBody: requestBody,
view: view,
completionHandler: { (response : ApiContainer<Client>?, error : Error?) in
if let error = error {
print("error calling POST on /todos")
print(error)
return
}
let b = (response?.meta)!
let a = (response?.result[0])
let client_id = a?.client_id
self.client?.client_id = client_id
if(b.sucess == "yes") {
//change message and use the custom func like on error.
let alert = UIAlertController(title: "Success!", message: "All good", preferredStyle: .alert)
let OKAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: {
(_)in
self.performSegue(withIdentifier: "unwindToClients", sender: self)
})
alert.addAction(OKAction)
DispatchQueue.main.async(execute: {
self.present(alert, animated: true, completion: nil)
})
}
else
{
self.showAlert(title: "Error", message: "Error Creating Client")
//return
}
} )
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.sectionHeaderHeight = 50.0;
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(save))
let thePicker = UIPickerView()
provinceTextField.inputView = thePicker
thePicker.delegate = self
// ToolBar
let toolBar = UIToolbar()
toolBar.barStyle = .default
toolBar.isTranslucent = true
toolBar.tintColor = UIColor(red: 92/255, green: 216/255, blue: 255/255, alpha: 1)
toolBar.sizeToFit()
// Adding Button ToolBar
let doneButton = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(ClientDetailViewController.doneClick))
let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let cancelButton = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(ClientDetailViewController.cancelClick))
toolBar.setItems([cancelButton, spaceButton, doneButton], animated: false)
toolBar.isUserInteractionEnabled = true
provinceTextField.inputAccessoryView = toolBar
}
#objc func doneClick() {
provinceTextField.resignFirstResponder()
}
#objc func cancelClick() {
provinceTextField.resignFirstResponder()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var rows: Int = 0
if section < numberOfRowsAtSection.count {
rows = numberOfRowsAtSection[section]
}
return rows
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let backItem = UIBarButtonItem()
backItem.title = "Client"
navigationItem.backBarButtonItem = backItem
if segue.identifier == "unwindToClients",
let destination = segue.destination as? ClientsViewController
{
destination.client = client
}
if segue.identifier == "showContacts",
let destination = segue.destination as? ContactsViewController
{
destination.client = client
}
}
// MARK: - Picker view
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView( _ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return provinces.count
}
func pickerView( _ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return provinces[row].name
}
func pickerView( _ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
provinceTextField.text = provinces[row].name
}
}
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()
}
}
Append the items in datasource at index where you want to insert row in table. than follow steps 2,3,4.
Call method tbl.beginupdate
call insertRowsAtIndexPaths method of table view
Call tbl.endupdate
you can update tableView rows using this..
self.tableView.reloadRows(at: [indexPath!], with: .top)
I have a controller "Feed" which lists multiple posts via a table (a title and image) from Firebase.
On touch of a button, it brings to a "Feed Details" controller, where I would like the data (title image and caption) from the post clicked previously (parent) being display. (see screenshot 2)
At the moment On Click, I just got static information, none of the information are being fetch from Firebase. They are all being called correctly in the main screen, however. So the problem is when it segue to the "DetailsController"
How is it possible to fetch the details from the item click previously ??
Currently this is my feed controller:
//
// Feed.swift
// MobileAppDemo
//
// Created by Mikko Hilpinen on 31.10.2016.
// Copyright © 2016 Mikkomario. All rights reserved.
//
import UIKit
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
import SwiftKeychainWrapper
import SwiftyJSON
class FeedVC: UIViewController, UITableViewDataSource, UIImagePickerControllerDelegate, UINavigationControllerDelegate
{
#IBOutlet weak var addImageView: UIImageView!
#IBOutlet weak var feedTableView: UITableView!
#IBOutlet weak var titleInputView: InputTextView!
#IBOutlet weak var linkbutton: UIButton!
#IBOutlet weak var captionInputView: InputTextView!
private var posts = [Post]()
private var imagePicker = UIImagePickerController()
private var imageSelected = false
private var readPosts: ObserveTask?
override func viewDidLoad()
{
super.viewDidLoad()
imagePicker.delegate = self
imagePicker.allowsEditing = true
feedTableView.dataSource = self
feedTableView.rowHeight = UITableViewAutomaticDimension
feedTableView.estimatedRowHeight = 320
readPosts = Post.observeList(from: Post.parentReference.queryOrdered(byChild: Post.PROPERTY_CREATED))
{
posts in
self.posts = posts.reversed()
self.feedTableView.reloadData()
}
}
func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
// here you need to add
{
if let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as? MessageCell
{
let post = posts[indexPath.row]
cell.configureCell(tableView: tableView, post: post)
cell.linkbutton.tag = indexPath.row
cell.linkbutton.addTarget(self, action: #selector(FeedVC.toFeedDetailAction(_:)), for: .touchUpInside)
return cell
}
else
{
fatalError()
}
}
func toFeedDetailAction(_ sender: UIButton) {
let FeedDetailsController = self.storyboard?.instantiateViewController(withIdentifier: "FeedDetailsController") as! FeedDetailsController
FeedDetailsController.post = posts[sender.tag]
self.navigationController?.pushViewController(FeedDetailsController, animated: true)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])
{
if let image = info[UIImagePickerControllerEditedImage] as? UIImage
{
addImageView.image = image
imageSelected = true
}
picker.dismiss(animated: true, completion: nil)
}
#IBAction func selectImagePressed(_ sender: AnyObject)
{
present(imagePicker, animated: true, completion: nil)
}
#IBAction func postButtonPressed(_ sender: AnyObject)
{
guard let caption = captionInputView.text, !caption.isEmpty else
{
// TODO: Inform the user
print("POST: Caption must be entered")
return
}
guard let title = titleInputView.text, !title.isEmpty else
{
// TODO: Inform the user
print("POST: title must be entered")
return
}
guard let image = addImageView.image, imageSelected else
{
print("POST: Image must be selected")
return
}
guard let currentUserId = User.currentUserId else
{
print("POST: Can't post before logging in")
return
}
imageSelected = false
addImageView.image = UIImage(named: "add-image")
captionInputView.text = nil
titleInputView.text = nil
// Uploads the image
if let imageData = UIImageJPEGRepresentation(image, 0.2)
{
let imageUid = NSUUID().uuidString
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpeg"
Storage.REF_POST_IMAGES.child(imageUid).put(imageData, metadata: metadata)
{
(metadata, error) in
if let error = error
{
print("STORAGE: Failed to upload image to storage \(error)")
}
if let downloadURL = metadata?.downloadURL()?.absoluteString
{
// Caches the image for faster display
Storage.imageCache.setObject(image, forKey: downloadURL as NSString)
print("STORAGE: Successfully uploaded image to storage")
_ = Post.post(caption: caption, title: title, imageUrl: downloadURL, creatorId: currentUserId)
}
}
}
}
#IBAction func signOutButtonPressed(_ sender: AnyObject)
{
// Doesn't listen to posts anymore
readPosts?.stop()
try! FIRAuth.auth()?.signOut()
User.currentUserId = nil
dismiss(animated: true, completion: nil)
}
}
and my Feed Details:
//
// FeedDetails.swift
// MobileAppDemo
//
// Created by Mikko Hilpinen on 31.10.2016.
// Copyright © 2016 Mikkomario. All rights reserved.
//
import UIKit
import FirebaseAuth
import FirebaseDatabase
import FirebaseStorage
import SwiftKeychainWrapper
import SwiftyJSON
class FeedDetailsController: UIViewController, UITableViewDataSource, UIImagePickerControllerDelegate, UINavigationControllerDelegate
{
#IBOutlet weak var addImageView: UIImageView!
#IBOutlet weak var feedTableView: UITableView!
#IBOutlet weak var titleInputView: InputTextView!
#IBOutlet weak var linkbutton: UIButton!
#IBOutlet weak var captionInputView: InputTextView!
var post: Post!
private var posts = [Post]()
private var imagePicker = UIImagePickerController()
private var imageSelected = false
private var readPosts: ObserveTask?
override func viewDidLoad()
{
super.viewDidLoad()
imagePicker.delegate = self
imagePicker.allowsEditing = true
readPosts = Post.observeList(from: Post.parentReference.queryOrdered(byChild: Post.PROPERTY_CREATED))
{
posts in
self.posts = posts.reversed()
}
}
func numberOfSections(in tableView: UITableView) -> Int
{
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return posts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
// here you need to add
{
if let cell = tableView.dequeueReusableCell(withIdentifier: "MessageCell", for: indexPath) as? MessageCell
{
let post = posts[indexPath.row]
cell.configureCell(tableView: tableView, post: post)
cell.linkbutton.tag = indexPath.row
cell.linkbutton.addTarget(self, action: #selector(FeedVC.toFeedDetailAction(_:)), for: .touchUpInside)
return cell
}
else
{
fatalError()
}
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any])
{
if let image = info[UIImagePickerControllerEditedImage] as? UIImage
{
addImageView.image = image
imageSelected = true
}
picker.dismiss(animated: true, completion: nil)
}
#IBAction func selectImagePressed(_ sender: AnyObject)
{
present(imagePicker, animated: true, completion: nil)
}
#IBAction func postButtonPressed(_ sender: AnyObject)
{
guard let caption = captionInputView.text, !caption.isEmpty else
{
// TODO: Inform the user
print("POST: Caption must be entered")
return
}
guard let title = titleInputView.text, !title.isEmpty else
{
// TODO: Inform the user
print("POST: title must be entered")
return
}
guard let image = addImageView.image, imageSelected else
{
print("POST: Image must be selected")
return
}
guard let currentUserId = User.currentUserId else
{
print("POST: Can't post before logging in")
return
}
imageSelected = false
addImageView.image = UIImage(named: "add-image")
captionInputView.text = nil
titleInputView.text = nil
// Uploads the image
if let imageData = UIImageJPEGRepresentation(image, 0.2)
{
let imageUid = NSUUID().uuidString
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpeg"
Storage.REF_POST_IMAGES.child(imageUid).put(imageData, metadata: metadata)
{
(metadata, error) in
if let error = error
{
print("STORAGE: Failed to upload image to storage \(error)")
}
if let downloadURL = metadata?.downloadURL()?.absoluteString
{
// Caches the image for faster display
Storage.imageCache.setObject(image, forKey: downloadURL as NSString)
print("STORAGE: Successfully uploaded image to storage")
_ = Post.post(caption: caption, title: title, imageUrl: downloadURL, creatorId: currentUserId)
}
}
}
}
#IBAction func signOutButtonPressed(_ sender: AnyObject)
{
// Doesn't listen to posts anymore
readPosts?.stop()
try! FIRAuth.auth()?.signOut()
User.currentUserId = nil
dismiss(animated: true, completion: nil)
}
}
You need to implement prepare(for:sender:) in your table view (a UITableViewDelegate method) and add the data you need to display to your detail view controller there.
Okay, I tried using your code but there are so many missing elements that it was returning errors so I don't know if this will compile but it should do.
First:
I assume the the private var posts = [Post]() in both classes hold the same value of the posts
Second:
you need to add UITableViewDelegate to your superclass
class FeedVC: UIViewController, UITableViewDataSource, UITableViewDelegate, ...
Third:
add the following in your FeedVC:
private var selectedIndexPath: Int = 0
and this in your FeedDetailsController
var selectedIndexPath: Int = 0 // cannot be private to get accessed from FeedVC
and this Delegate function in FeedVC:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedIndexPath = indexPath.row
}
and the prepare for segue in FeedVC:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let newViewController = segue.destination as! FeedDetailsController
newViewController.selectedIndexPath = selectedIndexPath
}
Lastly you can now use the selectedIndexPath within posts to get the chosen post
let post = posts[selectedIndexPath]
Hope This Helps!