NSCollectionView crashes when creating items - swift

My NSCollectionView crashes when calling makeItem(withIdentifier identifier: String, for indexPath: IndexPath). numberOfItemsInSection returns the correct value. If I call makeItem... in viewDidLoad rather than in itemForRepresentedObject I see an error indicating that the indexPath is out of bounds. How can this be?
The collection view loads like this:
class TagCollectionViewController: NSViewController, NSCollectionViewDataSource {
fileprivate static let itemIdentifier = "TagItem"
#IBOutlet var collectionView: NSCollectionView!
fileprivate var tags = List<Tag>.init()
override func viewDidLoad() {
super.viewDidLoad()
let nib = NSNib(nibNamed: "TagCollectionViewItem", bundle: nil)
collectionView.register(nib, forItemWithIdentifier: TagCollectionViewController.itemIdentifier)
collectionView.dataSource = self
}
(The List collection is a Realm class)
During viewWillAppear() the tags collection is populated from a ReSwift state:
override func viewWillAppear() {
for image in mainStore.state.selectedImages {
for tag in image.tags {
tags.append(tag)
}
}
super.viewWillAppear()
}

Solved it.
When I created the .xib for the item I added an NSCollectionViewItem object but didn't wire up the view to my custom view.
To recap, for anyone who gets caught out by this, the steps to creating a NSCollectionViewItem are:
Create the nib and configure your views
Add an NSCollectionViewItem object to your nib
Wire up your view (and any other views) to the object
Register the nib with the collectionView in your view controller

Related

Swift: Update Layout and Content of ViewController when dismissing presented ViewController

I have a UIView which displays some information such as a user's Name and more, including a list of objects that all get pulled from my database. This works fine.
However, I now have a ViewController that gets presented on top of the current ViewController. In this presented ViewController, I am adding Data to my Database. When dismissing that view, I want the original ViewController to update all of its content to be up to date.
Right now, all my views are getting layedout in ViewDidLoad, meaning that they only really get loaded once and don't reload later on. I have managed to update Layout by calling self.view.layoutIfNeeded(), but if I understand correctly, this only updates constraint. Of course, I could call a new init of my original view controller. This would make it reload, but I would like to avoid that.
Another Idea I had was to set up all my content in the ViewWillAppear, which should maybe then update anytime my view controller is about to be visible. However, I don't know how to go about doing this. Can I just move all my setup code to viewWillAppear? Does this have any disadvantages?
TLDR: Is there a way to update a stackview with new elements without having to reload the full ViewController over ViewWillAppear?
The UITableView element works very smoothly with database data. If you fetch the data from your database inside viewDidLoad in your first view controller, and store it in an array, the UITableView (if you set up its dataSource correctly) will automatically populate the table with the new values from the second view controller. With this method, there is no need to use ViewWillAppear at all.
It sounds like as of now, you're using Views (inside a VStack)? to display individual objects from the database. If you want to keep whatever custom style/layout you're using with your views, this can be done by defining a custom subclass of UITableViewCell and selecting the "Also create XIB file" option. The XIB file lets you customize how the cells in your UITableView look.
Here is a simple example to show the database values in the first view controller automatically updating. I didn't include the custom XIB file (these are all default UITableViewCells), to keep it streamlined.
FIRST VIEW CONTROLLER
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var dataTable: UITableView!
var tableRows: [DataItem] = []
func loadData() {
let request: NSFetchRequest<DataItem> = DataItem.fetchRequest()
do {
tableRows = try Global_Context.fetch(request)
} catch {
print("Error loading data: \(error)")
}
}
override func viewDidLoad() {
super.viewDidLoad()
dataTable.dataSource = self
loadData()
}
#IBAction func goForward(_ sender: UIButton) {
self.performSegue(withIdentifier: "toSecond", sender: self)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableRows.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dataTableCell", for: indexPath)
cell.textLabel?.text = tableRows[indexPath.row].name
return cell
}
}
let Global_Context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveContext () {
if Global_Context.hasChanges {
do {
try Global_Context.save()
} catch {
let nserror = error as NSError
print("Error saving database context: \(nserror), \(nserror.userInfo)")
}
}
}
SECOND VIEW CONTROLLER:
import UIKit
import CoreData
class AddViewController: UIViewController {
#IBOutlet weak var itemEntry: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
itemEntry.delegate = self
}
#IBAction func addNewItem(_ sender: UIButton) {
let newDataItem = DataItem(context: Global_Context)
newDataItem.name = itemEntry.text
saveContext()
}
#IBAction func goBack(_ sender: UIButton) {
self.performSegue(withIdentifier: "toFirst", sender: self)
}
}
extension AddViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.endEditing(true)
return true
}
}
Main.storyboard:
Once you set up your view controller as a UITableViewDataSource (as in the example code), the table view should make things simpler by eliminating any need to manually manage individual Views.
Is this the functionality you were looking for? (Note about the example: it was set up in Xcode with "Use Core Data" enabled.)
Here is a link to the official documentation:
https://developer.apple.com/documentation/uikit/uitableview

How can I go back to the previous ViewController from UICollectionViewCell?

There are two pages(view controllers) in Main.storyboard: FirstViewController and SecondViewController. I have added a button into FirstViewController. Here is the button click scope:
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MainPage") as! SecondViewController
controller.modalPresentationStyle = .fullScreen
show(controller, sender: nil)
I have added UICollectionView into SecondViewController. UICollectionViewCell is also added (with .xib file) I have added back button in to .xib file. And I wanted to write below code for back button action(please focus func btnBack)
import UIKit
class ViewCell: UICollectionViewCell {
#IBOutlet weak var lblOutlet: UILabel!
#IBOutlet weak var contentViewOutlet: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
#IBAction func btnBack(_ sender: UIButton) {
present(controller, animated: true, completion: nil)
}
}
but as you known, ViewCell has no member dismiss. What i am trying to do is going back to the FirstViewController with this back button in UICollectionViewCell. And yes, app user see this back button in each ui collection records.
May be you can use protocol & delegate pattern to pop back from second view controller. You can add in your CollectionViewCell file a protocol like
protocol popBackDelegate {
func popBack()
}
then inject it in your CollectionViewCell
var popBackDelegate: popBackDelegate!
After that you can call it in your btnBack action
popBackDelegate.popBack()
Now you have to bind it your second view controller's collection view's cellforitem event
cell.popBackDelegate = self
Lastly you can conform this protocol func in your view controller and pop back in that.
Hopefully I helped you with that.
I would suggest add a callback in ViewCell for such small interaction as below,
class ViewCell: UICollectionViewCell {
#IBOutlet weak var lblOutlet: UILabel!
#IBOutlet weak var contentViewOutlet: UIView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
public var dismiss: (()-> Void)? = nil
#IBAction func btnBack(_ sender: UIButton) {
self.dismiss?()
}
}
Now, in your ViewController bind that callback inside cellForItemAt as,
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: ViewCell = // Your code to dequeue cell
cell.dismiss = { [weak self] in
self?.dismiss(animated: true, completion: nil) // OR Pop whatever
}
}

Swift add button to UICollectionViewCell that opens new controller

I have a UICollectionViewCell header on a UICollectionViewController, and I've added a button to it. I would like for the button, when clicked, to push a new view controller atop the current one. The problem is that the button doesn't have access to the navigation controller of the UICollectionViewController, so I there's no way to directly push a controller from, say, a connector to the buttn (that I know of). Is there any way to achieve this? Maybe something can be overriden, such as a collectionView function. Thanks!
If you just want to process the cell selection there is a handy method in the UICollectionViewDelegate that you can implement to get the index path of the pressed cell.
If your goal is to have a custom button inside the cell (or maybe even several) you can use delegation pattern to retrieve user actions to your controller to than process in any way, including pushing/presenting new controllers. Assign the controller's instance (the one managing the collection view) to the delegate member of your cell.
Define a protocol that I would call something like MyCustomCellDelegate (replace MyCustomCell with a more appropriate name for your case). Something like MyCustomCellDelegate: class { func didPressButtonX() }
Declare an optional delegate property in your cell subclass. weak var delegate: MyCustomCellDelegate?
Implement your delegate protocol by the class you want to respond to button presses (or any other interactions defined by your protocol).
Every time you create/dequeue a cell for your UICollectionView to use you set the delegate property to the view controller managing the collection view. cell.delegate = self (if done inside the view controller itself).
After receiving the UI event inside your custom cell use your delegate property to retrieve the action to the controller (or with ever object you used when assigning the property). Something like: delegate?.didPressButtonX()
In your class that implements MyCustomCellDelegate use the method to push the new controller.
Below I will provide sample code that should give more details on the implementation of the proposed solution:
// In your UICollectionViewCell subclass file
protocol MyCustomCellDelegate: class {
func didPressButtonX()
func didPressButtonY()
}
MyCustomCell: UICollectionViewCell {
weak var delegate: MyCustomCellDelegate?
#IBOutlet var buttonX: UIButton!
#IBOutlet var buttonY: UIButton!
#IBAction func didPressButtonX(sender: Any) {
delegate?.didPressButtonX()
}
#IBAction func didPressButtonY(sender: Any) {
delegate?.didPressButtonY()
}
}
// Now in your UICollectionViewController subclass file
MyCustomCollectionViewController: UICollectionViewController {
// ...
override func collectionView(UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier identifier: "YourCellIdentifierGoesHere", for indexPath: indexPath) as! MyCustomCell
// In here we assign the delegate member of the cell to make sure once
// an UI event occurs the cell will call methods implemented by our controller
cell.delegate = self
// further cell setup if needed ...
return cell
}
}
// In order for the instance of our controller to be used as cell's delegate
// we implement the protocol that we defined earlier in the cell file
extension MyCustomCollectionViewController: MyCustomCellDelegate {
func didPressButtonX() {
print("X button was pressed")
// now lets finally push some new controller
let yourNextCoolViewController = UIViewController()
self.push(yourNextCoolViewController, animated: true)
// OR if you are using segues
self.performSegue(withIdentifier: "YourSegueIdentifierGoesHere", sender: self)
}
func didPressButtonY() {
print("Y button was pressed")
}
}

How to let a UIViewController to inherits another UIViewController IBOutlets?

After Implementing the following (Class Inheritance):
class UIViewControllerA: UIViewControllerB {
}
How to let UIViewControllerA to inherits UIViewControllerB IBOutlets? How can I connect the components at Storyboard to the subclass UIViewControllerA?
If your goal is to let IBOutlets to be inherited, you should do the following:
1- Add the IBOutlets in the Super Class:
Super class (UIViewController) should not be directly connected to any ViewController at the storyboard, it should be generic. When adding the IBOutlets to the super class, they should not be connected to any component, sub classes should do that. Also, you might want to do some work (that's why you should apply this mechanism) for the IBOutlets in the super class.
Super Class should be similar to:
class SuperViewController: UIViewController, UITableViewDataSource {
//MARK:- IBOutlets
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// setting text to the label
label.text = "Hello"
// conforming to table view data source
tableView.dataSource = self
}
// handling the data source for the tableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
cell?.textLabel?.text = "Hello!"
return cell!
}
}
2- Inherit Super Class and Connect the IBOutlets:
Simply, your sub class should be similar to:
class ViewController: SuperViewController {
override func viewDidLoad() {
// calling the super class version
super.viewDidLoad()
}
}
From storyboard, assign the view controller to ViewController (Sub Class), then rebuild the project (cmd + b).
Now, after selecting the desired View Controller and selecting "Connection Inspector", you should see -in the IBOutlets section-:
you can manually connect them to the UI components that exists in your sub class ViewController (drag from the empty circle to the component). they should look like:
And that's it! Your sub class's table view and label should inherit what's included in the super class.
Hope this helped.

Using the contentView property on a custom tableView cell (being passed as a header) how to prevent it from nullifying the custom attributes?

For example here is my custom cell:
protocol SectionHeaderTableViewCellDelegate {
func didSelectUserHeaderTableViewCell(sectionHeader: SectionHeaderTableViewCell, selected: Bool, type: Type)
}
class SectionHeaderTableViewCell: UITableViewCell {
#IBOutlet weak var labelContainerView: LabelContainerView!
#IBOutlet weak var sectionTitleLabel: UILabel!
#IBOutlet weak var plusButton: UIButton!
var type: Type?
var delegate: SectionHeaderTableViewCellDelegate?
var dog: Dog?
let sections = [Type.Meals, Type.Exercise, Type.Health, Type.Training, Type.Misc]
}
extension SectionHeaderTableViewCell {
#IBAction func plusButtonPressed(sender: AnyObject) {
if let type = type {
delegate?.didSelectUserHeaderTableViewCell(self, selected: plusButton.selected, type: type )
}
}
In my controller if I add a return of header.contenView I get the desired results of the header staying in place but unfortunately it nullifies the button included in the custom header preventing it from being called. Otherwise if I simply just return header the button on the custom header cell works as expected but the header moves with the row being deleted which is obviously unsightly and not what I want.
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let header = tableView.dequeueReusableCellWithIdentifier("sectionHeader") as? SectionHeaderTableViewCell else { return UITableViewCell() }
header.delegate = self
header.updateDogWithGender(dog)
header.type = header.sections[section]
header.sectionTitleLabel.text = header.sections[section].rawValue
return header.contentView
}
moving headers
In case anyone runs into a similar situation the solution was to create a Nib file and customize it as you see fit. Create a nib file by going to File -> New File -> iOS -> User Interface -> and selecting View. Create Nib file. I added my views and buttons to get the look I wanted. customize Nib. From there I changed the custom cell class to be UITableViewHeaderFooterView instead and reconnected my outlets and actions to the new Nib file.
class SectionHeaderView: UITableViewHeaderFooterView {... previous code from above }
Back in the controller update the viewForHeaderInSection function to load a nib instead :
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = NSBundle.mainBundle().loadNibNamed("SectionHeader", owner: self, options: nil).first as? SectionHeaderView
header?.delegate = self
header?.updateDogWithGender(dog)
header?.type = header?.sections[section]
header?.sectionTitleLabel.text = header?.sections[section].rawValue
return header
}
By the way we declared the property first at the end of the loadNibNamed property because it returns an array of AnyObjects and since my Nib file only contains one UIView that houses a label and a button I only needed the first and only item in the array. Thanks to my mentor James for figuring this out!