UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x79fe0f20> {length = 2, path = 0 - 4} - swift

When I load the first time my UICollectionView does not have any problem, Im using custom layouts and a serachbar, when I search some text it crashes throwing the exception that the cell with an index path that does not exist, Im using the search bar like the next code:
import UIKit
import AVFoundation
class CategoryViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UISearchBarDelegate {
var ListArray : JSON! = []
var SelectedIds: [Int] = []
var SearchActive : Bool = false
var SearchArray = [Int]()
#IBOutlet weak var SearchCategories: UISearchBar!
#IBOutlet weak var CategoryCollection: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
if let layout = self.CategoryCollection.collectionViewLayout as? InterestsLayout {
layout.delegate = self
}
self.CategoryCollection.backgroundColor = UIColor.clearColor()
self.CategoryCollection.contentInset = UIEdgeInsets(top: 18, left: 3, bottom: 10, right: 3)
// Search Delegate
self.SearchCategories.delegate = self
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.LoadData()
}
func LoadData() {
MUBService.categoriesList(self) { (categories_list) -> () in
self.ListArray = categories_list
self.CategoryCollection.reloadData()
self.view.hideLoading()
}
}
func dismissKeyboard() {
view.endEditing(true)
self.SearchCategories.showsCancelButton = false
}
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
self.SearchCategories.showsCancelButton = true
self.SearchActive = true
}
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
self.SearchActive = false
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
self.SearchActive = false
self.dismissKeyboard()
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
self.SearchActive = false
}
func searchBar(searchBar: UISearchBar, let textDidChange searchText: String) {
self.SearchArray = []
for (index, object) in self.ListArray["categories"] {
let name = object["name"].string!
if name.localizedStandardContainsString(searchText) == true {
self.SearchArray.append(Int(index)!)
}
}
if(self.SearchArray.count == 0){
self.SearchActive = false;
} else {
self.SearchActive = true;
}
self.CategoryCollection.reloadData()
}
}
extension CategoryViewController {
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
self.CategoryCollection?.collectionViewLayout.invalidateLayout()
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if(self.SearchActive && self.SearchArray.count > 0) {
return self.SearchArray.count
}
return self.ListArray["categories"].count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CategoryCell", forIndexPath: indexPath) as! CategoryCell
let row = indexPath.row
if(self.SearchActive && self.SearchArray.count > 0) {
let category = self.ListArray["categories"][self.SearchArray[row]]
cell.configureWithPhoto(category, selected: self.ListArray["selected"])
}else{
let category = self.ListArray["categories"][row]
cell.configureWithPhoto(category, selected: self.ListArray["selected"])
}
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
let cell = self.CategoryCollection.cellForItemAtIndexPath(indexPath) as! CategoryCell
cell.changeBackGroundColor()
if (cell.is_active == true){
self.SelectedIds.append(cell.id)
}else{
self.SelectedIds.removeObject(cell.id)
}
}
#IBAction func RegisterDidTouch(sender: AnyObject) {
MUBService.setMyCategories(self.SelectedIds, view_controller: self) { (categories_selected) -> () in
self.performSegueWithIdentifier("HomeTabBarFromCategoriesSegue", sender: self)
}
}
}
extension CategoryViewController : InterestsLayoutDelegate {
// 1. Returns the photo height
func collectionView(collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:NSIndexPath , withWidth width:CGFloat) -> CGFloat {
var row = indexPath.row
if(self.SearchActive && self.SearchArray.count > 0) {
row = self.SearchArray[row]
}
let category = self.ListArray["categories"][row]
let url = NSURL(string:category["image"].string!)
let data = NSData(contentsOfURL:url!)
let image = UIImage(data:data!)!
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let rect = AVMakeRectWithAspectRatioInsideRect((image.size), boundingRect)
return rect.size.height
}
// 2. Returns the annotation size based on the text
func collectionView(collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: NSIndexPath, withWidth width: CGFloat) -> CGFloat {
let annotationPadding = CGFloat(4)
let annotationHeaderHeight = CGFloat(17)
var row = indexPath.row
if(self.SearchActive && self.SearchArray.count > 0) {
row = self.SearchArray[row]
}
let category = self.ListArray["categories"][row]
let font = UIFont(name: "AvenirNext-Regular", size: 10)!
let rect = NSString(string: category["name"].string!).boundingRectWithSize(CGSize(width: width, height: CGFloat(MAXFLOAT)), options: .UsesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
let commentHeight = ceil(rect.height)
var height = annotationPadding + annotationHeaderHeight + commentHeight + annotationPadding
if (height != 70){
height = 70
}
return 70
}
}
I don't understand what is happening, thanks a lot for your help

I've faced the same problem. Here an explanation: if you use a custom collectionViewLayout and you have a cache for layout attributes (best practice so you don't have to calculate every time attributes), you have to override invalidateLayout method in your custom layout class and purge your cache.
Here's my layout attributes array
private var cache = [UICollectionViewLayoutAttributes]()
Here the overrided method
override func invalidateLayout() {
super.invalidateLayout()
cache.removeAll()
}
I call layout invalidation in my textDidChange delegate
func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.characters.count > 0 {
// search and reload data source
self.searchBarActive = true
self.filterContentForSearchText(searchText)
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.reloadData()
}else{
self.searchBarActive = false
self.collectionView?.collectionViewLayout.invalidateLayout()
self.collectionView?.reloadData()
}
}

Related

Xcode Visual Memory Debugger. What does it mean if some object is not released and this object and is a not UIViewController

When I close all VC and go back to my root I see that some element views and cells, like HeaderViewNew, DisposalsShimmering, exist in a heap, these elements are declared like lazy var, why is this element not released from memory if I close VC that use it, it is normal, or I did some mistake?
I know that if I have a few examples of VC in memory this is a retail cycle, but with this element inside the heap hierarchy I am a little confused, and using info inside the Memory graph (main window) I can't solve this problem
Example of an object that has not been released:
import UIKit
protocol HeaderMenuOptionSelected: AnyObject {
func menuSelected(index: Int, title: String)
}
class HeadeViewMenu: UIView, UITableViewDelegate, UITableViewDataSource {
private var hiddenRows: Array<Int> = Array()
private var cellSize: CGFloat = 60
var selectedIndex: Int!
var tabBarHeight: CGFloat!
var tabBar: UIView!
let tabBarView = UIView()
var menuHeight: CGFloat = 0
var P_menuHeightConstraint: NSLayoutConstraint!
var menuArr: Array<HeaderMenuListOfOptions> = Array()
weak var delegate: HeaderMenuOptionSelected?
func clearMenuOptions() {
menuArr.removeAll()
P_menu.reloadData()
}
func setupListOfOptions(menuList: [HeaderMenuListOfOptions]) {
menuArr = menuList
P_menu.reloadData()
}
func setupMenuHeight(countOfElement: Int) {
menuHeight = cellSize * CGFloat(countOfElement)
}
// This trick need to use because if user open menu but user level not yet applied, after fetching setting from server need to update menu in menu open, but only ONE time
private var firstTimeOpen = true
func updateMenuHeight() {
guard firstTimeOpen != false else { return }
firstTimeOpen = false
if (P_menuHeightConstraint.constant != 0) {
menuHeight = cellSize * CGFloat(menuArr.count)
P_menuHeightConstraint.constant = menuHeight
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
} completion: { anim in
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
menuArr.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HeaderMenuCell
// iOS way to set default text label
// var content = cell.defaultContentConfiguration()
// content.text = "test"
// cell.contentConfiguration = content
cell.P_label.text = menuArr[indexPath.row].title
cell.selectionStyle = .none
cell.P_label.isHidden = needHideElement(index: indexPath.row)
cell.P_img.isHidden = needHideElement(index: indexPath.row)
cell.P_countOfElements.isHidden = needHideElement(index: indexPath.row)
if selectedIndex == indexPath.row {
cell.P_img.image = UIImage(named: menuArr[indexPath.row].selectedImg)
cell.P_label.textColor = menuArr[indexPath.row].selectedColor
cell.initCount(count: menuArr[indexPath.row].count)
} else {
cell.P_img.image = UIImage(named: menuArr[indexPath.row].img)
cell.P_label.textColor = menuArr[indexPath.row].unselectedColor
cell.initCount(count: menuArr[indexPath.row].count)
}
return cell
}
func openPageByImitationGesture(index: Int) {
hideMenu()
delegate?.menuSelected(index: index, title: menuArr[index].title)
selectedIndex = index
P_menu.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("index menu \(indexPath.row)")
hideMenu()
// FeedbackGenerator.shared.interactionFeedback()
// guard selectedIndex != indexPath.row else { return }
delegate?.menuSelected(index: indexPath.row, title: menuArr[indexPath.row].title)
// let cell = tableView.cellForRow(at: indexPath) as! HeaderMenuCell
selectedIndex = indexPath.row
tableView.reloadData()
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return getRowHeight(index: indexPath.row)
}
func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
print("index unhi....\(indexPath.row)")
}
lazy var P_menu: UITableView = {
let view = UITableView()
view.translatesAutoresizingMaskIntoConstraints = false
view.delegate = self
view.dataSource = self
view.register(HeaderMenuCell.self, forCellReuseIdentifier: "cell")
view.isScrollEnabled = false
view.separatorColor = .clear
view.separatorStyle = .none
return view
}()
lazy var P_menuView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.2
view.layer.shadowOffset = CGSize(width: 0, height: 5)
view.layer.shadowRadius = 2
return view
}()
lazy var P_backgroundView: UIView = {
let view = UIView()
view.backgroundColor = .black
view.translatesAutoresizingMaskIntoConstraints = false
view.alpha = 0
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(clickAtBackgroundView))
tapGesture.numberOfTapsRequired = 1
tapGesture.numberOfTouchesRequired = 1
view.addGestureRecognizer(tapGesture)
view.isUserInteractionEnabled = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
self.translatesAutoresizingMaskIntoConstraints = false
setupView()
}
deinit {
print("deinit called NewHeaderMenu")
}
func setupView() {
self.isHidden = true
self.addSubview(P_backgroundView)
self.addSubview(P_menuView)
self.P_menuView.addSubview(P_menu)
P_backgroundView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
P_backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
P_backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
P_backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
P_menu.topAnchor.constraint(equalTo: P_menuView.topAnchor).isActive = true
P_menu.leadingAnchor.constraint(equalTo: P_menuView.leadingAnchor).isActive = true
P_menu.trailingAnchor.constraint(equalTo: P_menuView.trailingAnchor).isActive = true
P_menu.bottomAnchor.constraint(equalTo: P_menuView.bottomAnchor).isActive = true
P_menuView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
P_menuView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
P_menuView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
P_menuHeightConstraint = P_menuView.heightAnchor.constraint(equalToConstant: 0)
P_menuHeightConstraint.isActive = true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
func openMenu() {
self.isHidden = false
UIView.animate(withDuration: 0.1) {
self.P_backgroundView.alpha = 0.2
self.hideTabBar()
} completion: { anim in
if (anim == true) {
self.P_menuHeightConstraint.constant = self.menuHeight
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
} completion: { anim in
}
}
}
}
func hideMenu() {
P_menuHeightConstraint.constant = 0
UIView.animate(withDuration: 0.2) {
self.layoutIfNeeded()
} completion: { anim in
if (anim == true) {
UIView.animate(withDuration: 0.1) {
self.P_backgroundView.alpha = 0
self.showTabBar()
} completion: { anim in
if (anim == true) {
self.isHidden = true
}
}
}
}
}
func needToHideRows(rows: [Int]) {
hiddenRows = rows
let countOfElements = (menuArr.count - rows.count)
print("Menu count of elements \(countOfElements)")
setupMenuHeight(countOfElement: countOfElements)
P_menu.reloadData()
}
func unhideAllElements(count: Int) {
hiddenRows = []
setupMenuHeight(countOfElement: count)
P_menu.reloadData()
}
func getRowHeight(index: Int) -> CGFloat {
var size: CGFloat = menuHeight / CGFloat(menuArr.count - hiddenRows.count)
hiddenRows.forEach { obj in
if (index == obj) {
size = 0
}
}
return size
}
func needHideElement(index: Int) -> Bool {
var needToHide = false
hiddenRows.forEach { obj in
if (index == obj) {
needToHide = true
}
}
return needToHide
}
func setupTabBar(view: UIView) {
guard tabBarHeight != nil else {
return
}
tabBar = view
tabBar.addSubview(tabBarView)
tabBarView.translatesAutoresizingMaskIntoConstraints = false
tabBarView.backgroundColor = .black
tabBarView.heightAnchor.constraint(equalToConstant: tabBarHeight).isActive = true
tabBarView.leadingAnchor.constraint(equalTo: tabBar.leadingAnchor).isActive = true
tabBarView.trailingAnchor.constraint(equalTo: tabBar.trailingAnchor).isActive = true
tabBarView.bottomAnchor.constraint(equalTo: tabBar.bottomAnchor).isActive = true
tabBarView.alpha = 0
}
func hideTabBar() {
UIView.animate(withDuration: 0.1) {
self.tabBarView.alpha = 0.2
}
}
func showTabBar() {
UIView.animate(withDuration: 0.1) {
self.tabBarView.alpha = 0
}
}
#objc func clickAtBackgroundView() {
hideMenu()
delegate?.menuSelected(index: selectedIndex, title: menuArr[selectedIndex].title)
}
func selectOptionNonProgrammatically(index: Int) {
// This function need for fixind problem when user open app and app had list of sevec filters for disposal tab
hideMenu()
// delegate?.menuSelected(index: index, title: title)
delegate?.menuSelected(index: index, title: menuArr[index].title)
}
func updateCounter(index: Int, count: Int?) {
guard count != nil && count != 0 else { return }
menuArr[index].count = count
P_menu.reloadData()
}
}
Inside VC that contained this element I do next:
1. Init
lazy var P_headerViewNewMenu: HeadeViewMenu = {
let view = HeadeViewMenu()
view.delegate = self
return view
}()
2. Deinit
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
P_headerViewNewMenu.delegate = nil
}
3. Init constraints
self.view.addSubview(P_headerViewNewMenu)
P_headerViewNewMenu.topAnchor.constraint(equalTo: P_headerViewNew.bottomAnchor).isActive = true
P_headerViewNewMenu.leadingAnchor.constraint(equalTo: P_headerViewNew.leadingAnchor).isActive = true
P_headerViewNewMenu.trailingAnchor.constraint(equalTo: P_headerViewNew.trailingAnchor).isActive = true
P_headerViewNewMenu.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
P_headerViewNewMenu.tabBarHeight = self.tabBarController?.tabBar.frame.height
P_headerViewNewMenu.setupTabBar(view: (self.tabBarController?.view)!)
I found the problem with be help of matt, years of experience of asking the right question ;) Problem was inside P_headerViewNewMenu.setupTabBar(view: (self.tabBarController?.view)!)
link to the object was not released.
Because VC was deallocated, and all objects continue to be in the heap, I haven't been understood that these are all objects that not called deinit, but VC was released from memory

Trying to create new NSTextView every time the attributed string exceeds a certain height?

I'm trying to add a new NSTextView to the last index in my collection view every time the attributed string exceeds a certain bounds. The code works perfectly until the 5th item then it starts its starts creating an item every time the enter button is pressed. I'm thinking its a bug but im not sure. if any one can show me a better way to do it or improve the current code I have I would appreciate it. Below is my code:
Here is the CollectionViewItem
class DocumentItem: NSCollectionViewItem {
var itemView: DocumentTextView?
override func viewDidLoad() {
super.viewDidLoad()
self.itemView?.wantsLayer = true
// Do view setup here.
}
override func loadView() {
self.itemView = DocumentTextView(frame: NSZeroRect)
self.view = self.itemView!
}
func getView() -> DocumentTextView {
return self.itemView!
}
}
Here is the collectionView datasource
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return DocList.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DocumentItem"), for: indexPath)
return item
}
Here is the NSTextView subclass
class DocumentTextView: NSTextView {
var theContainer = NSTextContainer()
var theStorage = NSTextStorage()
var theManager = NSLayoutManager()
var table = NSTextTable()
var pdfPage: PDFPage?
override init(frame frameRect: NSRect) {
super.init(frame: NSRect(origin: frameRect.origin, size: NSSize(width: 800, height: 1131 )), textContainer: theContainer)
theStorage.addLayoutManager(theManager)
theManager.addTextContainer(theContainer)
self.textContainerInset = CGSize(width: 50, height: 50)
self.textContainer?.widthTracksTextView = true
self.textContainer?.heightTracksTextView = true
self.textContainer?.lineBreakMode = .byWordWrapping
self.maxSize = NSSize(width: 800, height: 1131)
self.backgroundColor = NSColor.fromHexString("ffffff")!
self.isRichText = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Here is the function bringing the bug
func textDidChange(_ notification: Notification) {
var textView = notification.object as? DocumentTextView
let numberOfItems = theDocumentOutlineView.numberOfItems(inSection: 0)
let theLastTextView = theDocumentOutlineView.item(at: numberOfItems - 1) as! DocumentItem
if textView == theLastTextView.itemView {
print(textView?.attributedString().size())
if (textView?.attributedString().size().height)! >= 1106.0 {
self.DocList.append(2)
var set = Set<IndexPath>()
set.insert(NSIndexPath(forItem: self.DocList.count - 1 , inSection: 0) as IndexPath)
theDocumentOutlineView.insertItems(at: set)
theDocumentOutlineView.scrollToItems(at: set, scrollPosition: NSCollectionView.ScrollPosition.top)
var newFirstResponder = theDocumentOutlineView.item(at: self.DocList.count - 1) as! DocumentItem
newFirstResponder.itemView?.delegate = self
self.view.window?.makeFirstResponder(newFirstResponder.itemView)
}
}
}
Here's my test project, maybe it helps. The delegate of the text view is its view controller, the NSCollectionViewItem. The view controller of the collection view also receives NSText.didChangeNotification notifications to check the length of the text. heightTracksTextView of the text container is false.
ViewController:
class ViewController: NSViewController, NSCollectionViewDataSource, NSCollectionViewDelegate {
#IBOutlet weak var collectionView: NSCollectionView!
var docList: [DocumentObject] = [DocumentObject(index: 0, string: NSAttributedString(string: "New 0"))]
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(DocumentItem.self, forItemWithIdentifier: NSUserInterfaceItemIdentifier("DocumentItem"))
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange),
name: NSText.didChangeNotification, object: nil)
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return docList.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
if let item = collectionView.makeItem(withIdentifier: NSUserInterfaceItemIdentifier("DocumentItem"),
for: indexPath) as? DocumentItem {
item.representedObject = docList[indexPath.item]
return item
}
return NSCollectionViewItem()
}
#objc func textDidChange(_ notification: Notification) {
if let textView = notification.object as? NSTextView {
if let theLastItem = self.collectionView.item(at: self.docList.count - 1) as? DocumentItem,
textView === theLastItem.itemView {
//if textView.attributedString().size().height >= 1106.0 {
print("\(textView.attributedString().size().height) \(textView.layoutManager!.usedRect(for: textView.textContainer!).size.height)")
if let textContainer = textView.textContainer,
let heigth = textView.layoutManager?.usedRect(for: textContainer).size.height,
heigth >= 1106.0 {
DispatchQueue.main.async {
if let window = self.view.window,
window.makeFirstResponder(nil) { // end editing of previous item
self.docList.append(DocumentObject(index: self.docList.count - 1, string: NSAttributedString(string: "New \(self.docList.count)")))
let set: Set = [IndexPath(item: self.docList.count - 1, section: 0)]
self.collectionView.insertItems(at: set)
self.collectionView.scrollToItems(at: set, scrollPosition: NSCollectionView.ScrollPosition.top)
if let newItem = self.collectionView.item(at: self.docList.count - 1) as? DocumentItem {
window.makeFirstResponder(newItem.itemView)
}
}
}
}
}
}
}
}
DocumentItem:
class DocumentItem: NSCollectionViewItem, NSTextViewDelegate {
var itemView: DocumentTextView?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
override func loadView() {
self.itemView = DocumentTextView(frame: NSZeroRect)
self.view = self.itemView!
self.itemView?.delegate = self
}
override var representedObject: Any? {
didSet {
if let item = representedObject as? DocumentObject {
itemView?.textStorage?.setAttributedString(item.string)
}
}
}
func textDidEndEditing(_ notification: Notification) {
if let item = representedObject as? DocumentObject {
item.string = itemView?.textStorage?.copy() as! NSAttributedString
}
}
}
DocumentObject:
class DocumentObject {
var index: Int
var string: NSAttributedString
init(index: Int, string: NSAttributedString) {
self.index = index
self.string = string
}
}

Swift Toolbar button triggers a exec_bad_instruction only on one scene

I'm changing my app to start to use toolbar buttons for the main pages instead of a menu. All of the menu links work and the toolbar buttons work on every page except one. When I click on the button the app freezes, and Xcode brings me to this line:
'weak var itemDetailPage = segue.destination as? ItemDetailViewController' with this error:
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
All the variables are nil.
Any help would be monumental!
Here is the code for the view controller it is specifically happening on:
import UIKit
import AVFoundation
import Alamofire
#objc protocol FavRefreshLikeCountsDelegate: class {
func updateLikeCounts(_ likeCount: Int)
}
#objc protocol FavRefreshReviewCountsDelegate: class {
func updateReviewCounts(_ reviewCount: Int)
}
class FavouriteItemsViewController: UICollectionViewController {
var populationItems = false
var selectedSubCategoryId:Int = 0
var selectedCityId:Int = 1
var selectedCityLat: String!
var selectedCityLng: String!
var items = [ItemModel]()
var currentPage = 0
var loginUserId:Int = 0
weak var selectedCell : AnnotatedPhotoCell!
#IBOutlet weak var menuButton: UIBarButtonItem!
var defaultValue: CGPoint!
override func viewDidLoad() {
if self.revealViewController() != nil {
menuButton.target = self.revealViewController()
menuButton.action = #selector(SWRevealViewController.revealToggle(_:))
self.view.addGestureRecognizer(self.revealViewController().panGestureRecognizer())
}
collectionView!.register(AnnotatedPhotoCell.classForCoder(), forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "AnnotatedPhotoCell")
if let layout = collectionView?.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
loadLoginUserId()
loadFavouriteItems()
defaultValue = collectionView?.frame.origin
animateCollectionView()
}
override func viewDidAppear(_ animated: Bool) {
updateNavigationStuff()
}
override var preferredStatusBarStyle : UIStatusBarStyle {
return UIStatusBarStyle.lightContent
}
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
loadFavouriteItems()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
weak var itemCell = sender as? AnnotatedPhotoCell
weak var itemDetailPage = segue.destination as? ItemDetailViewController
itemDetailPage!.selectedItemId = Int((itemCell!.item?.itemId)!)!
itemDetailPage!.selectedShopId = Int((itemCell!.item?.itemShopId)!)!
itemDetailPage!.favRefreshLikeCountsDelegate = self
itemDetailPage!.favRefreshReviewCountsDelegate = self
selectedCell = itemCell
updateBackButton()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
var cell : AnnotatedPhotoCell
cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AnnotatedPhotoCell", for: indexPath) as! AnnotatedPhotoCell
cell.item = items[(indexPath as NSIndexPath).item]
let imageURL = configs.imageUrl + items[(indexPath as NSIndexPath).item].itemImage
cell.imageView?.loadImage(urlString: imageURL) { (status, url, image, msg) in
if(status == STATUS.success) {
print(url + " is loaded successfully.")
self.items[indexPath.item].itemImageBlob = image
}else {
print("Error in loading image" + msg)
}
}
cell.imageView.alpha = 0
cell.captionLabel.alpha = 0
UIView.animate(withDuration: 1, delay: 0, options: UIViewAnimationOptions.curveEaseIn, animations: {
cell.imageView.alpha = 1.0
cell.captionLabel.alpha = 1.0
}, completion: nil)
return cell
}
func updateBackButton() {
let backItem = UIBarButtonItem()
backItem.title = ""
navigationItem.backBarButtonItem = backItem
}
func updateNavigationStuff() {
self.navigationController?.navigationBar.topItem?.title = language.favouritePageTitle
self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedStringKey.font: UIFont(name: customFont.boldFontName, size: CGFloat(customFont.boldFontSize))!, NSAttributedStringKey.foregroundColor:UIColor.white]
self.navigationController!.navigationBar.barTintColor = Common.instance.colorWithHexString(configs.barColorCode)
}
func loadLoginUserId() {
let plistPath = Common.instance.getLoginUserInfoPlist()
let myDict = NSDictionary(contentsOfFile: plistPath)
if let dict = myDict {
loginUserId = Int(dict.object(forKey: "_login_user_id") as! String)!
print("Login User Id : " + String(loginUserId))
} else {
print("WARNING: Couldn't create dictionary from LoginUserInfo.plist! Default values will be used!")
}
}
func loadFavouriteItems() {
if self.currentPage == 0 {
_ = EZLoadingActivity.show("Loading...", disableUI: true)
}
Alamofire.request(APIRouters.GetFavouriteItems( loginUserId, configs.pageSize, self.currentPage)).responseCollection {
(response: DataResponse<[Item]>) in
if self.currentPage == 0 {
_ = EZLoadingActivity.hide()
}
if response.result.isSuccess {
if let items: [Item] = response.result.value {
if(items.count > 0) {
for item in items {
let oneItem = ItemModel(item: item)
self.items.append(oneItem)
self.currentPage+=1
}
}
}
self.collectionView!.reloadData()
} else {
print(response)
}
}
}
func animateCollectionView() {
moveOffScreen()
UIView.animate(withDuration: 1, delay: 0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 0.9, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.collectionView?.frame.origin = self.defaultValue
}, completion: nil)
}
fileprivate func moveOffScreen() {
collectionView?.frame.origin = CGPoint(x: (collectionView?.frame.origin.x)!,
y: (collectionView?.frame.origin.y)! + UIScreen.main.bounds.size.height)
}
}
extension FavouriteItemsViewController : PinterestLayoutDelegate {
func collectionView(_ collectionView:UICollectionView, heightForPhotoAtIndexPath indexPath:IndexPath , withWidth width:CGFloat) -> CGFloat {
let item = items[(indexPath as NSIndexPath).item]
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let size = CGSize(width: item.itemImageWidth, height: item.itemImageHeight)
var rect : CGRect
if item.itemImageBlob != nil {
rect = AVMakeRect(aspectRatio: item.itemImageBlob!.size, insideRect: boundingRect)
}else{
rect = AVMakeRect(aspectRatio: size, insideRect: boundingRect)
}
return rect.size.height
}
func collectionView(_ collectionView: UICollectionView, heightForAnnotationAtIndexPath indexPath: IndexPath, withWidth width: CGFloat) -> CGFloat {
let annotationPadding = CGFloat(4)
let annotationHeaderHeight = CGFloat(17)
let height = annotationPadding + annotationHeaderHeight + annotationPadding + 30
return height
}
}
extension FavouriteItemsViewController : FavRefreshLikeCountsDelegate, FavRefreshReviewCountsDelegate {
func updateLikeCounts(_ likeCount: Int) {
if selectedCell != nil {
selectedCell.lblLikeCount.text = "\(likeCount)"
}
}
func updateReviewCounts(_ reviewCount: Int) {
if selectedCell != nil {
selectedCell.lblReviewCount.text = "\(reviewCount)"
}
}
}
You should try be checking your ItemDetailViewController.
for example:
if segue.identifier == "ItemDetailVC" {
weak var itemCell = sender as? AnnotatedPhotoCell
weak var itemDetailPage = segue.destination as? ItemDetailViewController
...
}

How to display search results, and no result text if no result?

I am trying to display only results instead of all data from the parse server. I tried different methods (mostly same), but I failed. I know that I miss something.
My question is how to display only results as well as no result text on the screen if no result.
You can find my approach below.
Thank you very much in advance.
class SearchTableViewController: UITableViewController, UISearchBarDelegate, UISearchResultsUpdating {
#IBOutlet weak var searchBar: UISearchBar!
var searchActive : Bool = false
var data:[PFObject]!
var noteObjects: NSMutableArray! = NSMutableArray()
var filtered:[PFObject]!
var refresher: UIRefreshControl = UIRefreshControl()
var searchController = UISearchController()
override func viewDidLoad() {
super.viewDidLoad()
search()
searchBar.delegate = self
self.refresher.addTarget(self, action: #selector(refresh), for: .valueChanged)
self.tableView?.addSubview(refresher)
self.searchDisplayController?.searchResultsTableView.rowHeight = tableView.rowHeight;
}
#objc func refresh() {
print("Refreshed")
self.refresher.endRefreshing()
search()
}
#objc func search(searchText: String? = nil){
let query = PFQuery(className: "NewUsers")
query.whereKey("user", equalTo: PFUser.current() as Any)
if(searchText != nil){
query.whereKey("name", contains: searchText)
}
query.findObjectsInBackground { (objects, error) in
if error == nil {
if let cars = objects {
for object in cars {
if (object as PFObject?) != nil {
self.data = objects
self.tableView.reloadData()
}
}
}
}
}
}
func updateSearchResults(for searchController: UISearchController) {
self.filtered.removeAll(keepingCapacity: false)
let searchText = searchController.searchBar.text
let query: PFQuery = PFQuery(className: "NewUsers")
if searchController.isActive == true {
query.whereKey("name", matchesRegex: searchText!, modifiers: "i")
self.tableView.reloadData()
}
query.findObjectsInBackground { (results, error) in
self.filtered = results
self.tableView.reloadData()
}
print(searchText as Any)
}
var shouldUseSearchResult : Bool {
if let searchText = searchController.searchBar.text {
if searchText.isEmpty {
return false
}
}
return searchController.isActive
}
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 {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if data != nil {
return self.data.count
}
return 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! SearchTableViewCell
let obj = self.data[indexPath.row]
self.name = (obj["name"] as? String)!
self.surname = (obj["surname"] as? String)!
self.birthdate = (obj["birthday"] as? String)!
self.age = (obj["age"] as? String)!
self.city = (obj["city"] as? String)!
cell.dateLabel.text = ": " + birthday
cell.ageLabel.text = ": \(age)"
cell.userLabel.text = ": " + name + " " + surname
return cell
}
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchActive = true;
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
searchActive = false;
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchActive = false;
self.searchBar.endEditing(true)
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchActive = false;
self.searchBar.endEditing(true)
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
search(searchText: searchText)
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
self.searchBar.resignFirstResponder()
}
}
func numberOfSections(in tableView: UITableView) -> Int {
if (self.data.count == 0)
{
let noDataLabel: UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.tableView.bounds.size.width, height: tableView.bounds.size.height))
noDataLabel.text = "No Result Found."
noDataLabel.textColor = UIColor.red
noDataLabel.textAlignment = .center
self.tableView.backgroundView = noDataLabel
self.tableView.backgroundColor = UIColor.white
self.tablevÄ°EW.separatorStyle = .none
}
return 1
}
self.arrayDealPage is array & self.dealsTable is tableView Hope, it might help u and delegate and datasource protocol added.

infinite loop when selected programmatically a cell

I have a tableview with a textfield in every row.
I need to reload the tableview and programmatically select the row the user had selected.
The user can write what he wants. The data will be deleted when the textfield's editing has ended and added when the textfield has begun editing.
But I get a infinite loop. Please cloud you help me?
My code :
import UIKit
class OptionsItemViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var textFiedlDelegate: UITextField? = nil
var categorySelected: Category?
var options: [String] = []
var nameOptions: [String] = []
var cellSelected: Int = 0
var viewHeight: CGFloat = 0
var selectedRow: IndexPath? = nil
var tableviewNeedToReload: Bool = false
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var keyboardAlwaysShow: UITextField!
#IBOutlet weak var newFeatureButton: UIBarButtonItem!
private let db = DataBase()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = self
self.textFiedlDelegate?.delegate = self
self.title = categorySelected!.name
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
db.getItemOptions(predicateFormat: "id == \(self.categorySelected!.id)", completion: { results in
self.categorySelected = results.first!
self.options = self.categorySelected!.options as! [String]
DispatchQueue.main.async {
self.tableView.reloadData()
}
})
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(false)
self.viewHeight = self.view.frame.size.height
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(false)
var index = 0
while index < self.options.count {
if self.options[index] != "" {
index += 1
} else {
self.options.remove(at: index)
}
db.setCategoryOptions(category: self.categorySelected!, options: self.options, index: cellSelected)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
#IBAction func newFeature(_ sender: Any) {
if self.options.last != "" {
let indexPath: IndexPath = IndexPath(row: self.options.count, section: 0)
self.options.append("")
self.tableView.reloadData()
let cell = tableView(self.tableView, cellForRowAt: indexPath) as! CellItemOptions
cell.nameOptionsItem.becomeFirstResponder()
}
}
// MARK: - TableView Functions
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return options.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let option = options[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: CellItemOptions.identifier, for: indexPath) as! CellItemOptions
cell.nameOptionsItem.delegate = self
cell.configureCell(with: option)
return cell
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.cellSelected = options.index(of: textField.text!)!
let indexPath: IndexPath = IndexPath(row: self.cellSelected, section: 0)
self.tableView.reloadData()
let cell = self.tableView.cellForRow(at: indexPath) as! CellItemOptions
cell.nameOptionsItem.becomeFirstResponder()
}
func textFieldDidEndEditing(_ textField: UITextField) {
if textField.text! == "" {
if self.options[cellSelected] != "" {
db.setRemoveDetailsItem(category: self.categorySelected!, index: cellSelected)
}
self.options.remove(at: cellSelected)
} else {
self.options[cellSelected] = "\(textField.text!)"
db.setAddDetailsItem(category: self.categorySelected!, index: cellSelected)
}
db.setCategoryOptions(category: self.categorySelected!, options: self.options, index: cellSelected)
}
// MARK: - Keyboard
func keyboardWillShow(_ notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.size.height == self.viewHeight {
self.view.frame.size.height -= keyboardSize.height
}
}
}
func keyboardWillHide(_ notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
if self.view.frame.origin.y != self.viewHeight {
self.view.frame.size.height += keyboardSize.height
}
}
}
}
class CellItemOptions: UITableViewCell {
static let identifier = "OptionsItemCell"
#IBOutlet weak var nameOptionsItem: UITextField!
private let tableView = OptionsItemViewController()
func configureCell(with cell: String) {
nameOptionsItem.text = cell
}
}
EDIT :
The loop is due to the reload data...
Like I reload data in textFieldDidBeginEditing(), the view is reloaded more and more ... And I need to textFieldDidBeginEditing() to know the row selected by the user.