Swift Error convertPoint(CGPoint, to: UITableView) - swift

So I have a custom tableview and in that tableview I have a button, an image, and 2 labels. Each of these items gets filled with json data from a mysql server using php. I converted my project from Objective-C to Swift and in doing so I got this error. This is one of the most important codes in my project because since the user clicks the follow button it moves the array to other arrays, and in doing that it gives a special row number to the cell so all the images, labels, and the button knows which is which to display.
I tried switching it so .convert() but just errors so I left it how it was orginally.
The code for the button
// Follow Button
#IBAction func followButtonClick(_ sender: Any) {
// Adding row to tag
var buttonPosition = (sender as AnyObject).convertPoint(CGPoint.zero, to: self.myTableView)
var indexPath = self.myTableView.indexPathForRow(at: buttonPosition)!
// Creating an action per tag
if indexPath != nil {
// Showing Status Labels
var cell = self.myTableView.cellForRow(atIndexPath: indexPath)!
cell.firstStatusLabel.isHidden = false
cell.secondStatusLabel.isHidden = false
// Change Follow to Following
(sender as AnyObject).setImage(UIImage(named: "follow.png")!, for: .normal)
cell.followButton.isHidden = true
cell.followedButton.isHidden = false
self.myTableView.beginUpdates()
// ----- Inserting Cell to Section 0 -----
followedArray.insert(testArray[indexPath.row], at: 0)
myTableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
// ----- Removing Cell from Section 1 -----
testArray.remove(at: indexPath.row)
var rowToRemove = indexPath.row
self.myTableView.deleteRows(at: [IndexPath(row: rowToRemove, section: 1)], with: true)
self.myTableView.endUpdates()
}
}
The Error
Errors with new code
Pictures so its easier to read

convertPoint is changed in Swift 3 like convert(_:to:).
var buttonPosition = (sender as AnyObject).convert(CGPoint.zero, to: self.myTableView)
Check Apple Documentation for more details.
Edit:
For your first warning instead of usinf indexPath != nil you need to use if let and for that label error you need to cast your cell to customCell that you are using.
var buttonPosition = (sender as AnyObject).convert(CGPoint.zero, to: self.myTableView)
if let indexPath = self.tblSubscriber.indexPathForRow(at: buttonPosition) {
//Cast your `UITableViewCell` to CustomCell that you have used
var cell = self.myTableView.cellForRow(atIndexPath: indexPath) as! CustomCell
}
For your deleteRows error you need to specify the UITableViewRowAnimation not true or false.
self.myTableView.deleteRows(at: [IndexPath(row: rowToRemove, section: 1)], with: .automatic)
For more detail about UITableViewRowAnimation check documentation.

If you are trying this code in swift3 then the above method is updated to
func convert(_ point: CGPoint,
from view: UIView?) -> CGPoint you can use it like this
var buttonPosition = (sender as AnyObject).convert(CGPoint.zero, to: self.myTableView)
I have checked it in Playground and it is working.

Related

how to remove the cell from uitableview cell

Im trying to dynamically arranging table view when user select "type 3". It works when user select "type 3", "type 3-1" would be added in the tableview. However the program crashed when user select other than type3-1. I dont know how can I execute the "rows.remove(at:2)" before the override function is called. Any suggestion would appreciate!
class GuestViewController: UITableViewController {
var rows:[[[String:Any]]] = [[["type":RowType.DetailContent,
"subType":DCType.DCRightContent,
"name":CPFFields.CID,
"content":"9637"],
["type":RowType.DetailContent,
"subType":DCType.DCInput,
"name":CPFFields.VISIA]],
[["type":RowType.DetailTextView,
"CPFType":CPFFields.UV,
"title":CPFFields.preferenceTitle]],
[["type":RowType.DetailContent,
"subType":DCType.DCSelection,
"name":CPFFields.Phototherapy,
"title":CPFFields.anestheticTitle],
["type":RowType.DetailTextView,
"CPFType":CPFFields.Phototherapy,
"title":CPFFields.preferenceTitle]],
]
var isNewGuestSelected : Bool = false
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rows[section].count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = rows[indexPath.section][indexPath.row]
let type = item["type"] as! RowType
if type == RowType.DetailContent
{
let cell = tableView.dequeueReusableCell(withIdentifier: "DetailNameCell", for: indexPath) as! DetailContentCell
let cpfType = item["name"] as? CPFFields ?? .Customer
cell.name.text = CPFFields.localizedString(from: cpfType)
if let field = item["title"] as? CPFFields
{
cell.name.text = CPFFields.localizedString(from: field)
}
cell.moreSlectionLeftSpace = true
var content:String? = ""
cell.type = cpfType
switch cpfType {
case .CID:
content = (profile?.birthDate.dateFromDateString?.stringForPaitentId ?? "") + (profile?.name ?? "")
case .CT:
content = ""
if let profile = profile
{
content = CPFCustomerType.localizedString(from: profile.type)
//New Guest
if(content == CPFCustomerType.type1.rawValue){
rows[0].insert(["type":RowType.DetailContent,
"subType":DCType.DCRightContent,
"name":CPFFields.CID,
"content":"9637"], at: 1)
isNewGuestSelected = true
} else{
if isNewGuestSelected == true{
rows[0].remove(at: 1)
isNewGuestSelected = false
}
}
}
let subType = item["subType"] as! DCType
cell.setcontentType(type: subType, content: content)
return cell
}
I expected not to see "rows[0][2]" after running "rows[0].remove(at:1)".
However the log is printing
rows[0][0]
rows[0][1]
rows[0][2]
then
it crashed at "let item = rows[indexPath.section][indexPath.row]"
because it is out of range
You are modifying your content while rendering, thus after numberOfRows:inSection: was called. Therefore the tableView is trying to access an element that no longer exists, since you removed it.
Your cycle:
→ number of rows 4
→ removed item, contents now has 3 items
→ cell for item 0
→ cell for item 1
→ cell for item 2
- cell for item 3 → crash
Consider replacing the logic you have here outside of the cellForRow method, and doing these operations before you reload your tableView.
You should use the tableView:cellForRow:atIndexPath strictly for dequeueing your cells and configuring them; not for modifying the underlying data store since funky things like you're experiencing now can happen.
If you provide a bit more context I can probably tell you where to place your code to fix this issue.
Actually, the solution is quite simple. I just added tableView.reloadData() after removing the array, and the UI can then be updated.
if isNewGuestSelected == true{
rows[0].remove(at: 1)
isNewGuestSelected = false
tableView.reloadData()
}

Search Bar crashing app when inputting characters

I have a UITableView that is populating locations and a Search Bar set as the header of that UITableView.
Whenever certain characters are entered, or a certain amount of characters are entered, the app crashes, giving me no error code.
Sometimes the app crashes after inputting one character, maybe 2 characters, maybe 3, or maybe 4. There seems to be no apparent reason behind the crashing.
The search function properly searches and populates the filtered results, but for no apparent reason, crashes if a seemingly arbitrary amount of characters are inputted.
I have tried using the exception breakpoint tool already, and it is providing me with no new information. I think it has something to do with if there are no search results.
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Locations..."
navigationItem.hidesSearchBarWhenScrolling = false
searchController.hidesNavigationBarDuringPresentation = false
locationTableView.tableHeaderView = searchController.searchBar
searchController.searchBar.sizeToFit()
searchController.searchBar.showsCancelButton = false
searchController.searchBar.barTintColor = UIColor.white
filteredData = locationList
// Sets this view controller as presenting view controller for the search interface
definesPresentationContext = true
locationList = createArray()
// Reload the table
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func updateSearchResults(for searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
func searchBarIsEmpty() -> Bool {
// Returns true if the text is empty or nil
return searchController.searchBar.text?.isEmpty ?? true
}
func filterContentForSearchText(_ searchText: String) {
filteredData = locationList.filter({( locationName : Location) -> Bool in
return locationName.locationName.lowercased().contains(searchText.lowercased())
})
let range = NSMakeRange(0, self.locationTableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.locationTableView.reloadSections(sections as IndexSet, with: .fade)
}
func isFiltering() -> Bool {
return searchController.isActive && !searchBarIsEmpty()
}
func locationTableView(_ locationTableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering() {
return filteredData.count
}
return locationList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let locationCell = locationTableView.dequeueReusableCell(withIdentifier: "locationCell", for: indexPath) as! locationCell
let location: Location
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
locationCell.setLocation(location: location)
return locationCell
}
The expected result is that the UITableView should populate with filtered results. Instead, it populates them and crashes if too many characters are inputted (usually 1-4 characters).
EDIT 1: I have found through debugging the error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
appears on Line 2 on this block of code:
if isFiltering() {
location = filteredData[indexPath.row]
} else {
location = locationList[indexPath.row]
}
EDIT 2: This is the tutorial I used.
https://www.raywenderlich.com/472-uisearchcontroller-tutorial-getting-started
Seems like you are expecting the tableView to provide YOU with the number of sections... it is supposed to be driven by your own datasource.
Since you are not providing a numberOfSections in your data source I'm assuming it is 1. If all of your rows are in 1 section, all of the nifty reloading you are doing could be greatly simplified.
I suggest you read up on UITableView dataSource protocol at https://developer.apple.com/documentation/uikit/uitableviewdatasource
Reviewing the tutorial you are reading, it seems it is using a reloadData() which forces the tableView to ignore previous number of rows and reload its content with a new number of rows. And based on your findings so far, I would assume that is part of the root cause, with the tableview wrongly assuming a pre-determined number of rows and attempting to retrieve cells that are no longer within range.

TableView cell: Constraint change conflicts tableView.endUpdates()

I have a messaging application written in Swift.
It does have message bubbles: if the message is longer than 200 chars it is being shortened.
Whenever the user clicks on a message it gets selected:
If the message was shortened, I replace the text with the original long text: Therefore I need to call tableView.beginUpdates() and tableView.endUpdates()
Plus I have to change the timeLabel's height constraint with UIView.animate()
But the two seems to conflict each other, and makes a weird animation: (watch the end)
https://youtu.be/QLGtUg1AmFw
Code:
func selectWorldMessage(indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) as? WorldMessageCell {
cell.messageLabel.text = data[indexPath.row].originalMessage
self.lastContentOffsetY = self.tableView.contentOffset.y
self.tableView.beginUpdates()
UIView.animate(withDuration: duration) {
cell.timeLabelHeightConstraint.constant = 18
cell.layoutIfNeeded()
}
self.tableView.endUpdates()
self.lastContentOffsetY = nil
cell.layoutIfNeeded()
WorldMessageIdsStore.shared.nearby.saveCellHeight(indexPath: indexPath, height: cell.frame.size.height, expanded : true, depending: indexPath.section == 1 ? true : false )
}
}
#objc func deselectSelectedWorldMessage(){
if (currentSelectedIndexPath == nil){
return
}
let indexPath = currentSelectedIndexPath!
tableView.deselectRow(at: indexPath, animated: false)
if let cell = tableView.cellForRow(at: indexPath) as? WorldMessageCell {
cell.messageLabel.text = data[indexPath.row].shortenedMessage
self.lastContentOffsetY = self.tableView.contentOffset.y
self.tableView.beginUpdates()
UIView.animate(withDuration: duration) {
cell.timeLabelHeightConstraint.constant = 0
cell.layoutIfNeeded()
}
self.tableView.endUpdates()
self.lastContentOffsetY = nil
cell.layoutIfNeeded()
}
currentSelectedIndexPath = nil
}
Is there a way to animate cell height change & constraint change in the same time?
I can not place self.tableView.beginUpdates() and self.tableView.endUpdates() in the UIView.animate(){ ... } part, because it would cause the new appearing rows to flicker.
.
.
.
UPDATE 1
So If I palce the self.tableView.beginUpdates() and self.tableView.endUpdates() inside the UIView.animate(){ ... }, then the animation works fine. But as I mentioned it causes flicker when new rows are appearing.
Video:
https://youtu.be/8Sex3DoESkQ
UPDATE 2 !!
So If I set the UIview.animate's duration to 0.3 everything works fine. I don't really understand why.
This animation can be sticky.
I think your main problem here is bubble shrinking more then it should. If you run your animation in slow animation mode (By the way, the simulator has this option, very useful for debugging) you will notice:
To sort this out, you can make your label vertical content compression resistants
higher, and check label has top and bottom constraints with Hi priority too, so it would not get cut this way.
https://medium.com/#abhimuralidharan/ios-content-hugging-and-content-compression-resistance-priorities-476fb5828ef
Try using the following code to overcome flicking problem
tableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.middle, animated: true)
Alternative solution
try using this handsome method to update your cell with animation instead of beginUpdates() and endUpdates() methods
tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.bottom )

scroll back to the previous collectionview cell

I am using the code below to scroll to the next cell which works perfectly but how do I scroll back to the previous cell?
let cellItems = CollectionView.indexPathsForVisibleItems
CollectionView.scrollToItem(at: cellItems.max()!, at: .centeredVertically, animated: true)
First of all, the indexPathsForVisibleItems method does not guarantee order. You need to sort it firstly:
let sortedIndexes = collectionView.indexPathsForVisibleItems.sorted(<)
If you want to scroll to the previous cell, you need to store previous cell IndexPath somewhere in your class:
var previousCellIndexPath: IndexPath?
And than you can scroll to this cell:
func scrollToPreviousCell() {
guard let previousCellIndexPath = self.previousCellIndexPath else { return }
collectionView.scrollToItem(at: previousIndexPath, at: centeredVertically, animated: true)
}

CollectionView Cell and Progress Bar - Progress Bar showing in wrong Cell after scrolling

Ive been searching for a answer to this one for days now and cant seem to figure it out. I have a Collection View with custom cell. When you double tap a cell in the Collection View it will either download a file or delete it if its been downloaded before.
During the download a progress bar displays the progress of the download then displays a small icon in the top left corner. When deleting it removes the icon.
If you download from one cell and delete from another while first download is in progress it works fine but only if both cells were visible within the collection view.
if i download from one cell, then scroll offscreen and delete from a cell that is not in same screen as the cell that is being download from, it removes the corner image as usual then displays the progress bar of the cell that is being download from.
I don't know if this is an error with how i am reusing cells??? It doesn't seem to have anything to do with how i am updating the cell or collection view which works in all cases except after scrolling.
Below is 2 functions that download or delete file:
func downloadDataToDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath){
let downloadedAudio = PFObject(className: "downloadedAudio")
// save all files with unique name / object id
let selectedObjectId = self.partArray[selectedIndexPath.item].id
let selectedPartName = self.partArray[selectedIndexPath.item].name
let query = PFQuery(className: "Part")
query.whereKey("objectId", equalTo: selectedObjectId)
query.getFirstObjectInBackground { (object, error) in
if error != nil || object == nil {
print("No object for the index selected.")
} else {
//print("there is an object, getting the file.")
downloadedAudio.add(object?.object(forKey: "partAudio") as! PFFile, forKey: selectedPartName)
let downloadedFile = object?.object(forKey: "partAudio") as! PFFile
// get the data first so we can track progress
downloadedFile.getDataInBackground({ (success, error) in
if (success != nil) {
// pin the audio if there is data
downloadedAudio.pinInBackground(block: { (success, error) in
if success {
// reload the cell
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: false, cell: cell)
self.inProgress -= 1
cell.isUserInteractionEnabled = true
}
})
}
// track the progress of the data
}, progressBlock: { (percent) in
self.activityIndicatorView.stopAnimating()
cell.progessBar.isHidden = false
//cell.progessBar.transform = cell.progessBar.transform.scaledBy(x: 1, y: 1.1)
cell.contentView.bringSubview(toFront: cell.progessBar)
cell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
cell.isUserInteractionEnabled = false
})
}
}
}
func removeDataFromDevice(cell: JourneyCollectionViewCell, selectedIndexPath: IndexPath, object: PFObject) {
let selectedPartName = self.partArray[selectedIndexPath.item].name
// unpin the object from the LocalDataStore
PFObject.unpinAll(inBackground: [object], block: { (success, error) in
if success {
// reduce inProgress
self.inProgress -= 1
self.reloadCell(selectedIndexPath: selectedIndexPath, hideProgress: true, hideImage: true, cell: cell)
}
})
}
and this is how I'm reloading the cell
func reloadCell(selectedIndexPath: IndexPath, hideProgress: Bool, hideImage: Bool, cell: JourneyCollectionViewCell) {
cell.progessBar.isHidden = hideProgress
cell.imageDownloaded.isHidden = hideImage
self.collectionView.reloadItems(at: [selectedIndexPath])
}
----------- EDIT -------------
This is my cellForItem at function. Presently i am using a query to look on local drive and see if the file exists and then adding the corner image if it is. This is the first time i have used a query in this place, usually it is a query at login to populate an array but that is for a more static collection of data than what i am trying to achieve here by letting the user download and delete files.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: JourneyCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! JourneyCollectionViewCell
cell.imageCell.file = self.partArray[indexPath.item].image
cell.imageCell.loadInBackground()
cell.imageCell.layer.masksToBounds = true
// not sure if its good to run a query here as its constantly updated.
// query if file is on LDS and add image to indicate
let cellPartName = self.partArray[indexPath.item].name
let checkQuery = PFQuery(className: "downloadedAudio")
checkQuery.whereKeyExists(cellPartName)
checkQuery.fromLocalDatastore()
checkQuery.getFirstObjectInBackground(block: { (object, error) in
if error != nil || object == nil {
//print("The file does not exist locally on the device, remove the image.")
cell.imageDownloaded.isHidden = true
cell.imageDownloaded.image = UIImage(named: "")
cell.progessBar.isHidden = true
} else {
//print("the file already exists on the device, add the image.")
cell.contentView.bringSubview(toFront: cell.imageDownloaded)
cell.imageDownloaded.isHidden = false
cell.imageDownloaded.image = UIImage(named: "download-1")
}
})
return cell
}
This is a normal feature of "reuse" cells, for efficient memory management purposes. What you need to do is reset the cell values in below function:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
}
By reset, I mean set the cells to their default state, prior to you making any updates such as adding the left corner icon or the status bar.
You need to make sure the arrays that you are feeding the collectionview data from is maintained properly. For example, if you have an array A =[1,2,3] and you delete A[1], then array A needs to be [1,3].
So i tried placing the progress view programatically, i tried prepareForReuse in the custom cell class, neither resolved this issue directly, though i will keep using prepareForReuse as i think its a cleaner way to manage the cell than i had been.
What seems to have worked was relocating the cell within the progressBlock
if let downloadingCell = self.collectionView.cellForItem(at: selectedIndexPath) as? JourneyCollectionViewCell { downloadingCell.progessBar.isHidden = false
downloadingCell.contentView.bringSubview(toFront: downloadingCell.progessBar)
downloadingCell.progessBar.setProgress(Float(percent) / Float(100), animated: true)
downloadingCell.setNeedsDisplay()
downloadingCell.isUserInteractionEnabled = false
}