The tableView doesn't show the cells with results from my search. The search is working fine, I verified all the data, but when I assign the cell.localName?.text to the value, it's always nil. Not sure what is wrong. I've found another post with same issue in Objective-C and the answer there was that I need to use the cell from the real tableView, by adding self.tableView when dequeueing the cell, but it didn't work for me. Any suggestions?
Here is my code:
class SearchResultsController: UITableViewController, UISearchResultsUpdating {
var localNames: [String] = []
var filteredNames: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.registerClass(localCell.self, forCellReuseIdentifier: "localCell")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
let searchedString = searchController.searchBar.text
filteredNames.removeAll(keepCapacity: true)
if !searchedString.isEmpty {
let filter: String -> Bool = {name in
let range = name.rangeOfString(searchedString, options: NSStringCompareOptions.CaseInsensitiveSearch)
return range != nil
}
let matches = localNames.filter(filter)
filteredNames += matches
}
tableView.reloadData()
}
// MARK: - Table view data source
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
return filteredNames.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: localCell = self.tableView.dequeueReusableCellWithIdentifier("localCell") as localCell
cell.localName?.text = filteredNames[indexPath.row]
println(cell.localName?.text)
cell.localAddress?.text = "text"
cell.scheduleData?.text = "text"
cell.salaData?.text = "text"
cell.wifiData?.text = "text"
return cell
}
If your cell is returning nil then create one yourself using the following after your dequeueReusableCellWithIdentifier call:
if(cell == nil){
cell = UITableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "localCell")
}
Also, if you are using a UISearchController you should use 2 different arrays for populating your UITableView rather than manipulating the same array.
You can do this by checking whether the search controller is active inside your tableView delegate methods and acting accordingly.
Ok, so I found the solution and posting in case someone else will have the same issue as I had. I was using the custom cell from a prototype cell from storyboard. The resultController creates a new tableView that knows nothing about the custom cell, that's why it returns always nil. To fix it, I made a reference to the firstly used tableView and not a new one, or get the cell from a real table, this way my custom cell was recognised.
Related
I have a viewControl called PostViewController which has a UITableView of posts. I also have a class called PostCell which defines the UITableViewCell. I made a button function in PostCell called likeButtonClicked to favour a post similar to twitter.
#IBAction func likesButtonClicked(_ sender: Any) { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil, userInfo: ["cell":self, "likesButton":likesButton!]) }
This is to pass the cell indexPath and the button name to PostViewController. I need indexPath to increase the likes by 1 and the button name to change its image to pink when post is favoured.
I then subscribed to the notification in viewDidLoad of PostViewController.
NotificationCenter.default.addObserver(self, selector: #selector(postLiked), name: NSNotification.Name(rawValue: "likeButtonClicked"), object: nil)
I then wrote this function in the same viewController
#objc func postLiked(notification: Notification){
if let cell = notification.userInfo?["cell"] as? UITableViewCell{
let likesButton = notification.userInfo?["likesButton"] as? SpringButton
if let indexPath = postsTableView.indexPath(for: cell){
let post = posts[indexPath.row]
postId = post.id
PostAPI.getPostById(postId: postId) { post in
//Check if the same post were already favoured.
if !self.liked || self.oldPostId != self.postId{
self.newLikes = post.likes + 1
self.liked = true
self.oldPostId = self.postId
}else{
self.newLikes = self.newLikes - 1
self.liked = false
}
PostAPI.favourPost(postId: self.postId, likes: self.newLikes) {
PostAPI.getPostById(postId: self.postId) { postResponse in
let post = postResponse
self.posts[indexPath.row] = post
let cellNumber = IndexPath(row: indexPath.row, section: indexPath.section)
self.reloadRowData(cellNumber: cellNumber){
if !self.liked{
likesButton?.tintColor = .systemPink
}else{
likesButton?.tintColor = .darkGray
}
}
}
}
}
}
}
}
func reloadRowData(cellNumber: IndexPath, completion: #escaping () -> ()) {
self.postsTableView.reloadRows(at: [cellNumber], with: .none)
completion()
}
Please tell me why the last 4 lines of postLiked function is executed before reloadRowData function, which causes the button to change its color to pink then returns immediately to gray when it should stay pink.
Any help will be most appreciated.
Thank you.
I expect the specific problem is your call to reloadRows. As the docs note:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out. Call this method if you want to alert the user that the value of a cell is changing. If, however, notifying the user is not important—that is, you just want to change the value that a cell is displaying—you can get the cell for a particular row and set its new value.
So this is likely creating an entirely new cell, and then the later code modifies the old cell that is being removed from the table. (So you see the change, and then the cell is removed and replaced.)
I would start by getting rid of the entire reloadRowData call.
Generally, though, this code is fragile, and I'd redesign it. The cell should take care of setting the tint colors itself based on the data. You generally shouldn't be reaching into a cell and manipulating its subviews. This will cause you a problem when cells are recycled (for example, when this scrolls off screen). All the configuration should be done in cellForRow(at:), and the cell should observe its Post and update itself when there are changes.
Data should live in the Model. The View should observe the Model and react. The Model should not reach into the View and manipulate anything.
As a side: your reloadRowData looks async, but it's not. There's no reason for a completion handler. It could just call return.
A table view's .reloadRows(at:...) (and .reloadData()) functions are async processes.
So your reloadRowData() func is returning before the table view actually reloads the row(s).
This is a rather unusual approach - both in using NotificationCenter for your cells to communicate with the controller, and in trying to change the button's tint color by holding a reference to the button.
The tint color really should be set in cellForRowAt, based on your data source.
Edit
My description of table view data reloading being asynchronous was misleading.
The point I wanted to make was this...
If I change my data, call .reloadData(), and then change my data again:
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
the table view will not display "You Say Hello" even though we call .reloadData() when those are the values of myData.
Here's a complete, simple example to demonstrate:
class TestTableViewController: UITableViewController {
var myData: [String] = ["Some", "Song", "Lyrics"]
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = myData[indexPath.row]
return c
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// call my func when a row is selected
self.myReload()
}
func myReload() {
// set myData values
myData = ["You", "Say", "Hello"]
// call .reloadData here
tableView.reloadData()
// change myData values
myData = ["I", "Say", "Goodbye"]
// table view doesn't reload itself we exit this func
// so we'll get
// I
// Say
// Goodbye
}
}
So, among the other issues described by Rob Napier in his answer, your original code was trying to change the tint color of an object in a cell before the table reloaded its data.
I think lazy var {}() should run once. But today I meet this problem troubles me. And I have on any thought. I'm crying.
Here is reproduction class:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private lazy var hotStockTableView: UITableView = {
let tableView = UITableView()
print("hotStockTableView ======> \(tableView) \n")
tableView.delegate = self
tableView.dataSource = self
// Here cause crash.
tableView.tableFooterView = UIView() // if comment this line, everthing is ok.
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(hotStockTableView)
hotStockTableView.frame = view.bounds
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("tableView =====> \(tableView) \n")
if tableView == hotStockTableView {
return 5
} else {
return 0
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
}
cell?.textLabel?.text = "\(indexPath.row)"
return cell!
}
}
Could someone help me?
The problem is that your closure to create a table view ends up being recursive.
When you set a tableFooterView on the table, it immediately tries to find out if that footer should be drawn / visible / onscreen immediately.
To know if the footer should be visible, the table view has to find out how many rows should currently be displayed, so it asks the data source (your view controller) for the number of rows in the the first section
Inside your method tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int, you have the line if tableView == hotStockTableView {. This tries to access the lazy hotStockTableView var, which still hasn't completed initializing, since that closure is what is calling this code in the first place
So the lazy var closure is kicked off again and recursively goes through the same steps over and over and over until stack overflow and crash.
Fortunately, the solution is easy. If you haven't set the data source yet then adding the table view footer will never call your number of rows data source method. So just change the order of code in your lazy closure to the following:
private lazy var hotStockTableView: UITableView = {
let tableView = UITableView()
print("hotStockTableView ======> \(tableView) \n")
tableView.tableFooterView = UIView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
Now your code will work just fine.
About lazy variables there is a good answer on SO:
As far as thread safety is concerned, lazy var are not thread safe
in Swift.
That means if two different threads try to access the same lazy var
at the same time, before such variable has been initialized it is
possible that one of the threads will access partially constructed
instance.
I'd suggest trying to set delegate and dataSource after the lazy view is created. It might be accessed by delegate or dataSource before it is fully initialized.
I have largely populated tableView running in my project. I am trying to shuffle the tableView array at once only.I found the similar code from this link How do I shuffle an array in Swift? code works ok..But the code shuffles TableView array every time when i swipe(up/down). I just want to restrict the code to happen once only...
I am using the below code.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var indexArray = Array(MY ARRAY NAME.indices)
var index = indexArray.endIndex
let indexGenerator: AnyGenerator<Int> = anyGenerator {
if index == indexArray.startIndex { return nil }
index = index.advancedBy(-1, limit: indexArray.startIndex)
let randomIndex = Int(arc4random_uniform(UInt32(index)))
if randomIndex != index {
swap(&indexArray[randomIndex], &indexArray[index])
}
return indexArray[index]
}
let permutationGenerator = PermutationGenerator(elements: MY ARRAY NAME, indices: AnySequence(indexGenerator))
let newArray = Array(permutationGenerator)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
////////////////////////////////////
cell.textLabel?.text = newArray[indexPath.row] //use of unsolved identifier "newArray"
////////////////////////////////////
return cell
}
Thanks in Advance.
Just call this inside of your viewDidLoad and it should shuffle your table view cells only once. There's no need to call it in every cell.
I am using UISegmentControl to display objective type questions in table view. But, if I select one segment in any one of cell, then if I scroll, some segment values are changed. I dont know how to solve that.
Kindly guide me.
Cell size : 160px
Segment tint color : blue color
Coding
//UIViewController
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = segTblVw.dequeueReusableCellWithIdentifier("segment", forIndexPath: indexPath) as! segmentTblCell
return cell
}
//UITableViewCell CLASS
class segmentTblCell: UITableViewCell {
#IBOutlet weak var segMent: UISegmentedControl!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
Screen shot below:
You're having this problem because of how dequeueReusableCellWithIdentifier: works. Each time a cell get scrolled out of screen, it enters a cache area where it will be reused.
Let's say you have 100 cells. All their segmentedControl objects are set to first. You tap on one to change it's value. As the cell moves out of view, it enters the cache, where it will be dequeued if you scroll down further.
It's important to understand this, because the segmentedControl object is not actually changing. It looks like it's changing because of the dequeue behaviour.
To solve this problem, you will need to implement a dataSource that stores the segmentedControl object's value so you can reinitialize it correctly every time a cell is dequeued.
Method 1: Prevent reusability of cells by, Holding all cell objects in an array
var arraysCells : NSMutableArray = []//globally declare this
in viewDidLoad()
for num in yourQuestionArray//this loop is to create all cells at beginning
{
var nib:Array = NSBundle.mainBundle().loadNibNamed("SegmentTableViewCell", owner: self, options: nil)
var cell = nib[0] as? SegmentTableViewCell
arraysCells.addObject(cell!);
}
in tableViewDelegate,
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return arraysCells.objectAtIndex(indexPath.row) as! UITableViewCell
}
you can find the selected segment values (answer) by iterating arraysCells
NOTE: Method 1 will be slow, if you have big number of cells
Method 2: Reuse the cell as normal, but save the states(enterd values) Using Delegate and arrays.
in custom UITableViewCell
#objc protocol SegmentTableViewCellDelegate {
func controller(controller: SegmentTableViewCell, selectedSegmentIndex:Int, indexPath : NSIndexPath)
}
class SegmentTableViewCell: UITableViewCell {
var delegate: AnyObject?
var indexPath : NSIndexPath?
#IBOutlet weak var segment: UISegmentedControl! //outlet of segmented Control
#IBAction func onSegmentValueChanged(sender: UISegmentedControl/*if the parameter type is AnyObject changed it as UISegmentedControl*/)//action for Segment
{
self.delegate?.controller(self, selectedSegmentIndex: sender.selectedSegmentIndex, indexPath: indexPath!)
}
in viewController
class MasterViewController: SegmentTableViewCellDelegate{
var selectedAnswerIndex : NSMutableArray = [] //globally declare this
var selectedSegmentsIndexPath : NSMutableArray = [] //globally declare this
func controller(controller: SegmentTableViewCell, selectedSegmentIndex:Int, indexPath : NSIndexPath)
{
if(selectedSegmentsIndexPath.containsObject(indexPath))
{
selectedAnswerIndex.removeObjectAtIndex(selectedSegmentsIndexPath.indexOfObject(indexPath))
selectedSegmentsIndexPath.removeObject(indexPath)
}
selectedAnswerIndex.addObject(selectedSegmentIndex)
selectedSegmentsIndexPath.addObject(indexPath)
}
in cellForRowAtIndexPath (tableView Delegate)
if(selectedSegmentsIndexPath.containsObject(indexPath))
{
cell?.segment.selectedSegmentIndex = selectedAnswerIndex.objectAtIndex(selectedSegmentsIndexPath.indexOfObject(indexPath)) as! Int
}
cell?.delegate = self
cell?.indexPath = indexPath
you can get the result by
for index in selectedSegmentsIndexPath
{
var cellIndexPath = index as! NSIndexPath
var answer : Int = selectedAnswerIndex.objectAtIndex(selectedSegmentsIndexPath.indexOfObject(cellIndexPath)) as! Int
NSLog("You have enterd answer \(answer) for question number \(cellIndexPath.row)")
}
#KelvinLau's is perfect
you can do that by using var segmentedTracker : [NSIndexPath:Int] = [:]
on segmentedValueChanged set the value of the selectedIndex ie: segmentedTracker[indexPath] = valueOf the selected index
then in cellForRowAtIndexPath check for the value let selected = [segmentedTracker]
cell.yourSegmentedControlReference.selectedIndex = selected
please note this is a pseudocode I don't remember the properties name. From here you can figure it out by urself
I think to use UISegmentControl in UITableViewCell may be wrong.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UISegmentedControl_Class/
I have never seen the kind of that in iOS application.
The problem is that UITableViewCell is reused by dequeueReusableCellWithIdentifier method. So some UISegmentControl values are changed when scrolling.
Although it is not best solution, you can use Static Cells. What you need to do is that only switch Static cells. And If so, you don't write code of UITableViewCell.
Year 2018: Updated Answer
Find my easiest answer in this UISegement inside UITableViewCell
=======================================================
Year 2015
I have tested in my own way. My coding is below. Kindly guide me, whether it is right way or wrong way? My problem get solved. This code stops reusable cell.
My Coding Below:
//UIViewController
var globalCell = segmentTblCell() //CUSTOM UITableViewCell Class
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = segTblVw.dequeueReusableCellWithIdentifier("segment", forIndexPath: indexPath) as! segmentTblCell
globalCell = segTblVw.dequeueReusableCellWithIdentifier("segment", forIndexPath: indexPath) as! segmentTblCell //THIS LINE - STOPS REUSABLE TABLE.
return cell
}
I am attempting to fetch data from an object created on Parse.com into a custom cell that contains labels and images. The code I implemented thus far runs but my tableview remains empty and at runtime displays the following error. ERROR: Thread 1: Exc_BAD_INSTRUCTION (Code =EXC_1386_INVOP, subcode=0x0). Can someone please explain why this is occurring I am new to programming in Xcode.
#objc
protocol ViewControllerDelegate {
optional func toggleLeftPanel()
optional func toggleRightPanel()
optional func collapseSidePanels()
}
class ViewController: UITableViewController, UITableViewDataSource, UITableViewDelegate
{
var delegate: ViewControllerDelegate?
var arrayOfParties: [Information] = [Information]()
#IBAction func menuTapped(sender: AnyObject) {
delegate?.toggleLeftPanel?()
}
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.loadData()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
//return self.arrayOfParties.count
return self.arrayOfParties.count
}
//Function to adjust the height of the custom cell
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
return 230
}
override func tableView(tableView: UITableView,cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell: CustomCell = tableView.dequeueReusableCellWithIdentifier("Cell") as! CustomCell
let party = arrayOfParties[indexPath.row]
cell.setCell(party.partyName, promoterLabelText: party.promoterName, partyCostLabelText: party.partyCost, partyFlyerUIImage: party.flyerImage, promoterUIImage: party.promoterImage)
return cell
}
func loadData()
{
var findDataParse:PFQuery = PFQuery(className: "flyerDataFetch")
findDataParse.findObjectsInBackgroundWithBlock{
(objects: [AnyObject]?, error: NSError?)->Void in
if (error == nil){
for object in objects!
{
var party1 = Information(partyName: (object["partyName"] as? String)!, promoterName: (object["promoterName"] as? String)!, partyCost: (object["partyCost"] as? String)!, flyerImage: "", promoterImage: "")
self.arrayOfParties.append(party1)
}
}
else {
// something went wron
}
}
}
}
The problem is that you probably have cells in the tableview already that don't have the new labels/images.
You need to tell the program to insert a type of text where there is no input of the new type.
In this case I have already since a month back worked on a project, and now I started inserting more labels into my cell, thats where the problem pops up, so the previous cells don't have this information.
So I just tell the system to check all cells, if a cell doesn't have the needed text, it should just enter some placeholder text (in this case = "").
if let content = text.objectForKey("Content") as? String {
cell.TextView.alpha = 1.0
cell.TextView.text = content
}
else{
cell.TextView.alpha = 1.0
cell.TextView.text = ""
}
I hope this helps, I have only explained how to do, I haven't really told you were in your code to do it, but from what I see you should input the code right under the part where you declare "Party1".