Load data to NSTableView in swift - objective-c

I am new to IOS/IOX development. I tried many sites to load data to my table on OSX swift using NSTableView. But all was failure.
The procedure i did was refering View-Based NSTableView in Swift - How to
Drag and droped a NSTableView, which was perfectly seen when I run the code.
Selected the first column and gave Identifier as "List" after setting number of columns to 1
In the appdelegate.swift I pasted as
class AppDelegate: NSObject, NSApplicationDelegate,NSTableViewDataSource,NSTableViewDelegate {
func applicationDidFinishLaunching(aNotification: NSNotification?) {
}
func applicationWillTerminate(aNotification: NSNotification?) {
}
func numberOfRowsInTableView(aTableView: NSTableView!) -> Int {
println("reached 1")
return 10
}
func tableView(tableView: NSTableView, viewForTableColumn: NSTableColumn, row: Int) -> NSView {
println("reached 2")
var cell = tableView.makeViewWithIdentifier("List", owner: self) as NSTableCellView
cell.textField.stringValue = "Hey, this is a cell"
return cell
}
}
Then i tried to Set the delegate and datasource as the Appdelegate for the table view by selecting table and pointing to "App Delegate" in "Application Scene". But it was not linking
When i run the program , table with no cells was loaded
Any help is greatly appreciated.

The easiest way would be selecting your table view in the XIB file and then in the Utilities panel on the right chose the third icon from the right (the arrow pointing right). Also make sure you show the document outline by clicking the icon in the lower left of the Interface Builder window. From the Utilities panel you can just drag from the delegate and datasource circle to your AppDelegate in the document outline.

func tableView(tableView: NSTableView, viewForTableColumn tableColumn: NSTableColumn?, row: Int) -> NSView?
{
if tableView.identifier=="tableViewIdentifier" {
let cellView = tableView.makeViewWithIdentifier("List", owner: self) as! NSTableCellView
cellView.textField?.stringValue = "Hey, this is a cell"
return cellView
}
return nil
}
set identifier to tableview of dropped table also set identifier to table column, set the delegate and datasource to the view controller in which the table is contained.

In your case, when using storyboards, you should make your view controller a data source and delegate for your table view, because they're contained in one scene. The class itself is called ViewController in Xcode project templates.
So, just move the code from application delegate to view controller and connect all the things in storyboard.

Related

Preventing contextual menu showing on specific cell in a view based NSTableView

Is there any way of preventing a contextual menu (and the associated selection "ring" around the cell view) being shown when right-clicking on a specific cell in a view-based NSTableView ?
I'm not talking about disabling the right-click action on ALL the cells, but only on specific ones.
I've obviously tried all the delegate methods dealing with selection changes but none works because the selectedRow property is not changing, only the clickedRow does.
So basically I'm looking for something equivalent to
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool
but for the clicked row not the selected row.
Note: the questions is about NSTableView on macOS and not the UITableViewon iOS.
I've found a way to do what I wanted, although looks like a little to involved for something that should be simpler. So I welcome any simpler solution.
It can be done by subclassing NSTableView :
class MyTableView : NSTableView {
override func menu(for event: NSEvent) -> NSMenu? {
let clickedPoint = self.convert(event.locationInWindow, from: nil)
let row = self.row(at: clickedPoint)
// no contextual menu for the last row
return row == self.numberOfRows - 1 ? nil : super.menu(for: event)
}
}
This example prevents the contextual menu to be shown for the last row, but a more generic solution could be implemented by adding a delegate with a method to return the menu for each cell.
Instead of subclassing NSTableView, a much easier approach is to set a menu delegate and remove all items within public func menuNeedsUpdate(_ menu: NSMenu) delegate method.
Example:
class MyViewController: NSViewController {
override func viewDidLoad() {
let menu = NSMenu()
menu.delegate = self
tableView.menu = menu
}
}
extension MyViewController: NSMenuDelegate {
public func menuNeedsUpdate(_ menu: NSMenu) {
//This will prevent menu from showing
menu.removeAllItems()
//Check if user has clicked on the cell or somewhere inside tableView
//area that is not populated with cells
guard tableView.clickedRow >= 0 else { return }
//Get model
let item = items[tableView.clickedRow]
//For cells that need context menu, add necessary menu items
if item.needsContextMenu {
menu.addItem(NSMenuItem(title: "Edit", action: #selector(tableViewEditItemClicked(_:)), keyEquivalent: "e"))
menu.addItem(NSMenuItem(title: "Delete", action: #selector(tableViewEditItemClicked(_:)), keyEquivalent: "d"))
}
}
}

ImageView Not Displaying on Reusable TableViewCell

I have a TableView and TableViewCell created on my ViewController. The cell will be reusable (importing name + photo from database). Currently only the name cell text shows up.
How can I get the image to show above the cell text(name). Or will I need to create a separate cell subclass? Trying to make something like this:
Storyboard Setup
Update
Added ImageView(inside the cell) to storyboard and made an outlet to FeedCell.swift:
#IBOutlet weak var setImage: UIImageView!
Added to ViewController's ViewDidLoad:
self.tableView.registerClass(FeedCell.self, forCellReuseIdentifier: "RestaurantCell")
Updated ViewControllers cellForRowAtIndexPath
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as UITableViewCell
var imageSet: PFObject?{
didSet{
self.fetchImage(restaurantArray[indexPath.row] as? PFObject, completionHandler: {
(image, error) -> () in
if image != nil {
FeedCell().setImage.image = image
cell.imageView?.image = image!
cell.imageView?.image = image
cell.accessoryView = UIImageView()
}else{
//alert user: no image or put placeholder image
}
})
}
}
return cell
}
You have a "RestaurantCell", but from the Storyboard screenshot you've included in your question, it looks like it's just a completely blank cell with an empty content view.
If you don't want to create a custom UITableViewCell, you should change that prototype RestaurantCell to have both a centered image and a label just underneath it, both exposed via properties that have unique tags set with them. Then, when the properties get set, you can update the view corresponding to the tag (e.g. view # 1 might be the image, view # 2 might be the name, etc.).
Or, you can simply subclass UITableViewCell and then you'll be able to use IBOutlets, which will be a lot cleaner versus using manual property setters and tags.
Then you can easily set the image and the label text and it should appear just beautifully in the Munchery app.
You might want to also include extra IBOutlets to set the Chef's imageView (Chef Alex is one of my favorites) and the cart button ("+Add", which I think swaps with "In The Bag"?).
EDITED TO BE MORE SPECIFIC
Get rid of "FeedCell().setImage.image = image". That's not doing anything good.
Change
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as UITableViewCell
to:
let cell = tableView.dequeueReusableCellWithIdentifier("RestaurantCell", forIndexPath: indexPath) as FeedCell
Also, is the IBOutlet in FeedCell connected to imageView or is the outlet named something else?

Segue Unwind back to the last specific View controller

Is there a way to have one button unwind back to a specific view controller? For example suppose I have ViewController A and B. Both modally segue to ViewController C. Now I understand how to segue unwind back to one of the previous view controllers (As was explained here) but how do I go back to the specific view controller that presented VC C?
So to sum it up, what I'm trying to figure out is...
If A segued to C then I want to unwind back to A when the button is selected. If B segued to C then I want to unwind back to B when the button is selected.
You should use the standard way to return from a modal segue. Put this in your 'back' button...
[[self presentingViewController] dismissViewControllerAnimated:YES];
This doesn't use the storyboard or a segue for the return, just code.
You could use the presentingViewController 'title' property to steer the unwind process to the desired originating ViewController. This solution would also allow you to pass data from your ViewController C to ViewControllers A and B.
1) Select the View Controller button (the yellow one) at the top of the storyboard canvas of ViewController A.
2) In the Attributes Inspector (View Controller section) give ViewController A a title.
3) Include the following code in ViewController A:
#IBAction func returned(segue: UIStoryboardSegue) {
// Don't need any code here
}
4) Repeat steps 1, 2 and 3 for ViewController B.
5) Override the prepareForSegue function in ViewController C as follows:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if self.presentingViewController!.title! == "ViewControllerATitle" {
let destination = segue.destinationViewController as! ViewControllerA
destination.someFunction(someArgument)
}
else if self.presentingViewController!.title! == "ViewControllerBTitle" {
let destination = segue.destinationViewController as! ViewControllerB
destination.someFunction(someArgument)
}
}

How to use single storyboard uiviewcontroller for multiple subclass

Let say I have a storyboard that contains UINavigationController as initial view controller. Its root view controller is subclass of UITableViewController, which is BasicViewController. It has IBAction which is connected to right navigation button of the navigation bar
From there I would like to use the storyboard as a template for other views without having to create additional storyboards. Say these views will have exactly the same interface but with root view controller of class SpecificViewController1 and SpecificViewController2 which are subclasses of BasicViewController.
Those 2 view controllers would have the same functionality and interface except for the IBAction method.
It would be like the following:
#interface BasicViewController : UITableViewController
#interface SpecificViewController1 : BasicViewController
#interface SpecificViewController2 : BasicViewController
Can I do something like that?
Can I just instantiate the storyboard of BasicViewController but have root view controller to subclass SpecificViewController1 and SpecificViewController2?
Thanks.
great question - but unfortunately only a lame answer. I don't believe that it is currently possible to do what you propose because there are no initializers in UIStoryboard that allow overriding the view controller associated with the storyboard as defined in the object details in the storyboard on initialization. It's at initialization that all the UI elements in the stoaryboard are linked up to their properties in the view controller.
It will by default initialize with the view controller that is specified in the storyboard definition.
If you are trying to gain reuse of UI elements you created in the storyboard, they still must be linked or associated to properties in which ever view controller is using them for them to be able to "tell" the view controller about events.
It's not that much of a big deal copying over a storyboard layout especially if you only need a similar design for 3 views, however if you do, you must make sure that all the previous associations are cleared, or it will get crashes when it tries to communicate to the previous view controller. You will be able to recognize them as KVO error messages in the log output.
A couple of approaches you could take:
store the UI elements in a UIView - in a xib file and instantiate it from your base class and add it as a sub view in the main view, typically self.view. Then you would simply use the storyboard layout with basically blank view controllers holding their place in the storyboard but with the correct view controller sub class assigned to them. Since they would inherit from the base, they would get that view.
create the layout in code and install it from your base view controller. Obviously this approach defeats the purpose of using the storyboard, but may be the way to go in your case. If you have other parts of the app that would benefit from the storyboard approach, it's ok to deviate here and there if appropriate. In this case, like above, you would just use bank view controllers with your subclass assigned and let the base view controller install the UI.
It would be nice if Apple came up with a way to do what you propose, but the issue of having the graphic elements pre-linked with the controller subclass would still be an issue.
have a great New Year!!
be well
The code of line we are looking for is:
object_setClass(AnyObject!, AnyClass!)
In Storyboard -> add UIViewController give it a ParentVC class name.
class ParentVC: UIViewController {
var type: Int?
override func awakeFromNib() {
if type = 0 {
object_setClass(self, ChildVC1.self)
}
if type = 1 {
object_setClass(self, ChildVC2.self)
}
}
override func viewDidLoad() { }
}
class ChildVC1: ParentVC {
override func viewDidLoad() {
super.viewDidLoad()
println(type)
// Console prints out 0
}
}
class ChildVC2: ParentVC {
override func viewDidLoad() {
super.viewDidLoad()
println(type)
// Console prints out 1
}
}
As the accepted answer states, it doesn't look like it is possible to do with storyboards.
My solution is to use Nib's - just like devs used them before storyboards. If you want to have a reusable, subclassable view controller (or even a view), my recommendation is to use Nibs.
SubclassMyViewController *myViewController = [[SubclassMyViewController alloc] initWithNibName:#"MyViewController" bundle:nil];
When you connect all your outlets to the "File Owner" in the MyViewController.xib you are NOT specifying what class the Nib should be loaded as, you are just specifying key-value pairs: "this view should be connected to this instance variable name." When calling [SubclassMyViewController alloc] initWithNibName: the initialization process specifies what view controller will be used to "control" the view you created in the nib.
It is possible to have a storyboard instantiate different subclasses of a custom view controller, though it involves a slightly unorthodox technique: overriding the alloc method for the view controller. When the custom view controller is created, the overridden alloc method in fact returns the result of running alloc on the subclass.
I should preface the answer with the proviso that, although I have tested it in various scenarios and received no errors, I can't ensure that it will cope with more complex set ups (but I see no reason why it shouldn't work). Also, I have not submitted any apps using this method, so there is the outside chance that it might be rejected by Apple's review process (though again I see no reason why it should).
For demonstration purposes, I have a subclass of UIViewController called TestViewController, which has a UILabel IBOutlet, and an IBAction. In my storyboard, I have added a view controller and amended its class to TestViewController, and hooked up the IBOutlet to a UILabel and the IBAction to a UIButton. I present the TestViewController by way of a modal segue triggered by a UIButton on the preceding viewController.
To control which class is instantiated, I have added a static variable and associated class methods so get/set the subclass to be used (I guess one could adopt other ways of determining which subclass is to be instantiated):
TestViewController.m:
#import "TestViewController.h"
#interface TestViewController ()
#end
#implementation TestViewController
static NSString *_classForStoryboard;
+(NSString *)classForStoryboard {
return [_classForStoryboard copy];
}
+(void)setClassForStoryBoard:(NSString *)classString {
if ([NSClassFromString(classString) isSubclassOfClass:[self class]]) {
_classForStoryboard = [classString copy];
} else {
NSLog(#"Warning: %# is not a subclass of %#, reverting to base class", classString, NSStringFromClass([self class]));
_classForStoryboard = nil;
}
}
+(instancetype)alloc {
if (_classForStoryboard == nil) {
return [super alloc];
} else {
if (NSClassFromString(_classForStoryboard) != [self class]) {
TestViewController *subclassedVC = [NSClassFromString(_classForStoryboard) alloc];
return subclassedVC;
} else {
return [super alloc];
}
}
}
For my test I have two subclasses of TestViewController: RedTestViewController and GreenTestViewController. The subclasses each have additional properties and each override viewDidLoad to change the background colour of the view and update the text of the UILabel IBOutlet:
RedTestViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor redColor];
self.testLabel.text = #"Set by RedTestVC";
}
GreenTestViewController.m:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
self.testLabel.text = #"Set by GreenTestVC";
}
On some occasions I might want to instantiate TestViewController itself, on other occasions RedTestViewController or GreenTestViewController. In the preceding view controller, I do this at random as follows:
NSInteger vcIndex = arc4random_uniform(4);
if (vcIndex == 0) {
NSLog(#"Chose TestVC");
[TestViewController setClassForStoryBoard:#"TestViewController"];
} else if (vcIndex == 1) {
NSLog(#"Chose RedVC");
[TestViewController setClassForStoryBoard:#"RedTestViewController"];
} else if (vcIndex == 2) {
NSLog(#"Chose BlueVC");
[TestViewController setClassForStoryBoard:#"BlueTestViewController"];
} else {
NSLog(#"Chose GreenVC");
[TestViewController setClassForStoryBoard:#"GreenTestViewController"];
}
Note that the setClassForStoryBoard method checks to ensure that the class name requested is indeed a subclass of TestViewController, to avoid any mix-ups. The reference above to BlueTestViewController is there to test this functionality.
Basing particularly on nickgzzjr and Jiří Zahálka answers plus comment under the second one from CocoaBob I've prepared short generic method doing exactly what OP needs. You need only to check storyboard name and View Controllers storyboard ID
class func instantiate<T: BasicViewController>(as _: T.Type) -> T? {
let storyboard = UIStoryboard(name: "StoryboardName", bundle: nil)
guard let instance = storyboard.instantiateViewController(withIdentifier: "Identifier") as? BasicViewController else {
return nil
}
object_setClass(instance, T.self)
return instance as? T
}
Optionals are added to avoid force unwrap (swiftlint warnings), but method returns correct objects.
Also: you need to initialize properties existing only in subclass before reading them from casted objects (if subclass has those properties and BasicViewController does not). Those properties won't be initialized automatically and attempt to read them before initialization will lead to crash. Because they are there in effect of casting it's very likely that even weak variables won't be set to nil (will contain garbage).
try this, after instantiateViewControllerWithIdentifier.
- (void)setClass:(Class)c {
object_setClass(self, c);
}
like :
SubViewController *vc = [sb instantiateViewControllerWithIdentifier:#"MainViewController"];
[vc setClass:[SubViewController class]];
Although it's not strictly a subclass, you can:
option-drag the base class view controller in the Document Outline to make a copy
Move the new view controller copy to a separate place on the storyboard
Change Class to the subclass view controller in the Identity Inspector
Here's an example from a Bloc tutorial I wrote, subclassing ViewController with WhiskeyViewController:
This allows you to create subclasses of view controller subclasses in the storyboard. You can then use instantiateViewControllerWithIdentifier: to create specific subclasses.
This approach is a bit inflexible: later modifications within the storyboard to the base class controller don't propagate to the subclass. If you have a lot of subclasses you may be better off with one of the other solutions, but this will do in a pinch.
Objc_setclass method doesn't create an instance of childvc. But while popping out of childvc, deinit of childvc is being call. Since there is no memory allocated separetely for childvc, app crashes. Basecontroller has an instance , whereas child vc doesn't have.
If you are not too reliant on storyboards, you can create a separate .xib file for the controller.
Set the appropriate File's Owner and outlets to the MainViewController and override init(nibName:bundle:) in the Main VC so that its children can access the same Nib and its outlets.
Your code should look like this:
class MainViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: "MainViewController", bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
button.tintColor = .red
}
}
And your Child VC will be able to reuse its parent's nib:
class ChildViewController: MainViewController {
override func viewDidLoad() {
super.viewDidLoad()
button.tintColor = .blue
}
}
There is a simple, obvious, everyday solution.
Simply put the existing storyboard/controller inside the new storyobard/controller. I.E. as a container view.
This is the exactly analogous concept to "subclassing", for, view controllers.
Everything works exactly as in a subclass.
Just as you commonly put a view subview inside another view, naturally you commonly put a view controller inside another view controller.
How else can could you do it?
It's a basic part of iOS, as simple as the concept "subview".
It's this easy ...
/*
Search screen is just a modification of our List screen.
*/
import UIKit
class Search: UIViewController {
var list: List!
override func viewDidLoad() {
super.viewDidLoad()
list = (_sb("List") as! List
addChild(list)
view.addSubview(list.view)
list.view.bindEdgesToSuperview()
list.didMove(toParent: self)
}
}
You now obviously have list to do whatever you want with
list.mode = .blah
list.tableview.reloadData()
list.heading = 'Search!'
list.searchBar.isHidden = false
etc etc.
Container views are "just like" subclassing in the same way that "subviews" are "just like" subclassing.
Of course obviously, you can't "sublcass a layout" - what would that even mean?
("Subclassing" relates to OO software and has no connection to "layouts".)
Obviously when you want to re-use a view, you just subview it inside another view.
When you want to re-use a controller layout, you just container view it inside another controller.
This is like the most basic mechanism of iOS!!
Note - for years now it's been trivial to dynamically load another view controller as a container view. Explained in the last section: https://stackoverflow.com/a/23403979/294884
Note - "_sb" is just an obvious macro we use to save typing,
func _sb(_ s: String)->UIViewController {
// by convention, for a screen "SomeScreen.storyboard" the
// storyboardID must be SomeScreenID
return UIStoryboard(name: s, bundle: nil)
.instantiateViewController(withIdentifier: s + "ID")
}
Thanks for #Jiří Zahálka's inspiring answer, I replied my solution 4 years ago here, but #Sayka suggested me to post it as an answer, so here it is.
In my projects, normally, if I'm using Storyboard for a UIViewController subclass, I always prepare a static method called instantiate() in that subclass, to create an instance from Storyboard easily. So for solve OP's question, if we want to share the same Storyboard for different subclasses, we can simply setClass() to that instance before returning it.
class func instantiate() -> SubClass {
let instance = (UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SuperClass") as? SuperClass)!
object_setClass(instance, SubClass.self)
return (instance as? SubClass)!
}
Here is a Swift solution which does not rely on Objective-C class swapping hacks.
It uses instantiateViewController(identifier:creator:) (iOS 13+).
I assume you have the view controller in a storyboard, with identifier template. The class assigned to the view controller in the storyboard should be the superclass:
let storyboard = UIStoryboard(name: "main", bundle: nil)
let viewController = storyboard.instantiateViewController(identifier: "template") { coder in
// The coder provides access to the storyboard data.
// We can now init the preferred UIViewController subclass.
if useSubclass {
return SpecialViewController(coder: coder)
} else {
return BaseViewController(coder: coder)
}
}
Here is the documentation
Probably most flexible way is to use reusable views.
(Create a View in separate XIB file or Container view and add it to each subclass view controller scene in storyboard)
Taking answers from here and there, I came up with this neat solution.
Create a parent view controller with this function.
class ParentViewController: UIViewController {
func convert<T: ParentViewController>(to _: T.Type) {
object_setClass(self, T.self)
}
}
This allows the compiler to ensure that the child view controller inherits from the parent view controller.
Then whenever you want to segue to this controller using a sub class you can do:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let parentViewController = segue.destination as? ParentViewController {
ParentViewController.convert(to: ChildViewController.self)
}
}
The cool part is that you can add a storyboard reference to itself, and then keep calling the "next" child view controller.
Cocoabob's comment from Jiří Zahálka answer helped me to get this solution and it worked well.
func openChildA() {
let storyboard = UIStoryboard(name: "Main", bundle: nil);
let parentController = storyboard
.instantiateViewController(withIdentifier: "ParentStoryboardID")
as! ParentClass;
object_setClass(parentController, ChildA.self)
self.present(parentController, animated: true, completion: nil);
}
It is plain simple. Just define the BaseViewController in a xib and then use it like this:
let baseVC: BaseViewController = BaseViewController(nibName: "BaseViewController", bundle: nil)
let subclassVC: ChildViewController = ChildViewController(nibName: "BaseViewController", bundle: nil)
To make is simple you can extract the identifier to a field and the loading to a method like:
public static var baseNibIdentifier: String {
return "BaseViewController"
}
public static func loadFromBaseNib<T>() -> T where T : UIViewController {
return T(nibName: self.baseNibIdentifier, bundle: nil)
}
Then you can use it like this:
let baseVC: BaseViewController = BaseViewController.loadFromBaseNib()
let subclassVC: ChildViewController = ChildViewController.loadFromBaseNib()

In a storyboard, how do I make a custom cell for use with multiple controllers?

I'm trying to use storyboards in an app I'm working on. In the app there are Lists and Users and each contains a collection of the other (members of a list, lists owned by a user). So, accordingly, I have ListCell and UserCell classes. The goal is to have those be re-usable throughout the app (ie, in any of my tableview controllers).
That's where I'm running into a problem.
How do I create a custom tableview cell in the storyboard that can be re-used in any view controller?
Here are the specific things I've tried so far.
In Controller #1, added a prototype cell, set the class to my UITableViewCell subclass, set the reuse id, added the labels and wired them to the class's outlets. In Controller #2, added an empty prototype cell, set it to the same class and reuse id as before. When it runs, the labels never appear when the cells are shown in Controller #2. Works fine in Controller #1.
Designed each cell type in a different NIB and wired up to the appropriate cell class. In storyboard, added an empty prototype cell and set its class and reuse id to refer to my cell class. In controllers' viewDidLoad methods, registered those NIB files for the reuse id. When shown, cells in both controllers were empty like the prototype.
Kept prototypes in both controllers empty and set class and reuse id to my cell class. Constructed the cells' UI entirely in code. Cells work perfectly in all controllers.
In the second case I suspect that the prototype is always overriding the NIB and if I killed the prototype cells, registering my NIB for the reuse id would work. But then I wouldn't be able to setup segues from the cells to other frames, which is really the whole point of using storyboards.
At the end of the day, I want two things: wire up tableview based flows in the storyboard and define cell layouts visually rather than in code. I can't see how to get both of those so far.
As I understand it, you want to:
Design a cell in IB which can be used in multiple storyboard scenes.
Configure unique storyboard segues from that cell, depending on the scene the cell is in.
Unfortunately, there is currently no way to do this. To understand why your previous attempts didn't work, you need to understand more about how storyboards and prototype table view cells work. (If you don't care about why these other attempts didn't work, feel free to leave now. I've got no magical workarounds for you, other than suggesting that you file a bug.)
A storyboard is, in essence, not much more than a collection of .xib files. When you load up a table view controller that has some prototype cells out of a storyboard, here's what happens:
Each prototype cell is actually its own embedded mini-nib. So when the table view controller is loading up, it runs through each of the prototype cell's nibs and calls -[UITableView registerNib:forCellReuseIdentifier:].
The table view asks the controller for the cells.
You probably call -[UITableView dequeueReusableCellWithIdentifier:]
When you request a cell with a given reuse identifier, it checks whether it has a nib registered. If it does, it instantiates an instance of that cell. This is composed of the following steps:
Look at the class of the cell, as defined in the cell's nib. Call [[CellClass alloc] initWithCoder:].
The -initWithCoder: method goes through and adds subviews and sets properties that were defined in the nib. (IBOutlets probably get hooked up here as well, though I haven't tested that; it may happen in -awakeFromNib)
You configure your cell however you want.
The important thing to note here is there is a distinction between the class of the cell and the visual appearance of the cell. You could create two separate prototype cells of the same class, but with their subviews laid out completely differently. In fact, if you use the default UITableViewCell styles, this is exactly what's happening. The "Default" style and the "Subtitle" style, for example, are both represented by the same UITableViewCell class.
This is important: The class of the cell does not have a one-to-one correlation with a particular view hierarchy. The view hierarchy is determined entirely by what's in the prototype cell that was registered with this particular controller.
Note, as well, that the cell's reuse identifier was not registered in some global cell dispensary. The reuse identifier is only used within the context of a single UITableView instance.
Given this information, let's look at what happened in your above attempts.
In Controller #1, added a prototype cell, set the class to my
UITableViewCell subclass, set the reuse id, added the labels and wired
them to the class's outlets. In Controller #2, added an empty
prototype cell, set it to the same class and reuse id as before. When
it runs, the labels never appear when the cells are shown in
Controller #2. Works fine in Controller #1.
This is expected. While both cells had the same class, the view hierarchy that was passed to the cell in Controller #2 was entirely devoid of subviews. So you got an empty cell, which is exactly what you put in the prototype.
Designed each cell type in a different NIB and wired up to the
appropriate cell class. In storyboard, added an empty prototype cell
and set its class and reuse id to refer to my cell class. In
controllers' viewDidLoad methods, registered those NIB files for the
reuse id. When shown, cells in both controllers were empty like the
prototype.
Again, this is expected. The reuse identifier is not shared between storyboard scenes or nibs, so the fact that all of these distinct cells had the same reuse identifier was meaningless. The cell you get back from the tableview will have an appearance that matches the prototype cell in that scene of the storyboard.
This solution was close, though. As you noted, you could just programmatically call -[UITableView registerNib:forCellReuseIdentifier:], passing the UINib containing the cell, and you'd get back that same cell. (This isn't because the prototype was "overriding" the nib; you simply hadn't registered the nib with the tableview, so it was still looking at the nib embedded in the storyboard.) Unfortunately, there's a flaw with this approach — there's no way to hook up storyboard segues to a cell in a standalone nib.
Kept prototypes in both controllers empty and set class and reuse id
to my cell class. Constructed the cells' UI entirely in code. Cells
work perfectly in all controllers.
Naturally. Hopefully, this is unsurprising.
So, that's why it didn't work. You can design your cells in standalone nibs and use them in multiple storyboard scenes; you just can't currently hook up storyboard segues to those cells. Hopefully, though, you've learned something in the process of reading this.
In spite of the great answer by BJ Homer I feel like I have a solution. As far as my testing goes, it works.
Concept: Create a custom class for the xib cell. There you can wait for a touch event and perform the segue programmatically. Now all we need is a reference to the controller performing the Segue. My solution is to set it in tableView:cellForRowAtIndexPath:.
Example
I have a DetailedTaskCell.xib containing a table cell which I'd like to use in multiple table views:
There is a custom class TaskGuessTableCell for that cell:
This is where the magic happens.
// TaskGuessTableCell.h
#import <Foundation/Foundation.h>
#interface TaskGuessTableCell : UITableViewCell
#property (nonatomic, weak) UIViewController *controller;
#end
// TashGuessTableCell.m
#import "TaskGuessTableCell.h"
#implementation TaskGuessTableCell
#synthesize controller;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSIndexPath *path = [controller.tableView indexPathForCell:self];
[controller.tableView selectRowAtIndexPath:path animated:NO scrollPosition:UITableViewScrollPositionNone];
[controller performSegueWithIdentifier:#"FinishedTask" sender:controller];
[super touchesEnded:touches withEvent:event];
}
#end
I have multiple Segues but they all have the same name: "FinishedTask". If you need to be flexible here, I suggest to add another property.
The ViewController looks like this:
// LogbookViewController.m
#import "LogbookViewController.h"
#import "TaskGuessTableCell.h"
#implementation LogbookViewController
- (void)viewDidLoad
{
[super viewDidLoad]
// register custom nib
[self.tableView registerNib:[UINib nibWithNibName:#"DetailedTaskCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:#"DetailedTaskCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TaskGuessTableCell *cell;
cell = [tableView dequeueReusableCellWithIdentifier:#"DetailedTaskCell"];
cell.controller = self; // <-- the line that matters
// if you added the seque property to the cell class, set that one here
// cell.segue = #"TheSegueYouNeedToTrigger";
cell.taskTitle.text = [entry title];
// set other outlet values etc. ...
return cell;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:#"FinishedTask"])
{
// do what you have to do, as usual
}
}
#end
There might be more elegant ways to achieve the same but - it works! :)
I was looking for this and I found this answer by Richard Venable. It works for me.
iOS 5 includes a new method on UITableView: registerNib:forCellReuseIdentifier:
To use it, put a UITableViewCell in a nib. It has to be the only root
object in the nib.
You can register the nib after loading your tableView, then when you
call dequeueReusableCellWithIdentifier: with the cell identifier, it
will pull it from the nib, just like if you had used a Storyboard
prototype cell.
BJ Homer has given an excellent explanation of what is going on.
From a practical standpoint I'd add that, given you can't have cells as xibs AND connect segues, the best one to choose is having the cell as a xib - transitions are far easier to maintain than cell layouts and properties across multiple places, and your segues are likely to be different from your different controllers anyway. You can define the segue directly from your table view controller to the next controller, and perform it in code. .
A further note is that having your cell as a separate xib file prevents you being able to connect any actions etc. directly to the table view controller (I haven't worked this out, anyway - you can't define file's owner as anything meaningful). I am working around this by defining a protocol that the cell's table view controller is expected to conform to and adding the controller as a weak property, similar to a delegate, in cellForRowAtIndexPath.
Swift 3
BJ Homer gave an excellent explanation, It helps me understand the concept. To make a custom cell reusable in storyboard, which can be used in any TableViewController we have to mix the Storyboard and xib approach. Suppose we have a cell named as CustomCell which is to be used in the TableViewControllerOne and TableViewControllerTwo. I am making it in steps.
1. File > New > Click File > Select Cocoa Touch Class > click Next > Give Name Of your class(for example CustomCell) > select Subclass as UITableVieCell > Tick the also create XIB file checkbox and press Next.
2. Customize the cell as you want and set the identifier in attribute inspector for cell, here we ll set as CellIdentifier. This identifier will be used in your ViewController to identify and reuse the Cell.
3. Now we just have to register this cell in our ViewController viewDidLoad. No need of any initialization method.
4. Now we can use this custom cell in any tableView.
In TableViewControllerOne
let reuseIdentifier = "CellIdentifier"
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: reuseIdentifier)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier:reuseIdentifier, for: indexPath) as! CustomCell
return cell!
}
I found a way to load the cell for the same VC, not tested for the segues. This could be a workaround for creating the cell in a separate nib
Let's say that you have one VC and 2 tables and you want to design a cell in storyboard and use it in both tables.
(ex: a table and a search field with a UISearchController with a table for results and you want to use the same Cell in both)
When the controller asks for the cell do this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * identifier = #"CELL_ID";
ContactsCell *cell = [self.YOURTABLEVIEW dequeueReusableCellWithIdentifier:identifier];
// Ignore the "tableView" argument
}
And here you have your cell from the storyboard
If I understand your question correctly, this is fairly easy. Create a UIViewController in your storyboard that will hold your prototype cells and create a static shared instance that loads itself from the storyboard. To handle view controller segues, use the manual segue outlet and trigger on table view delegate didSelectRow (the manual segue outlet is the middle icon at the top of the view controller in the storyboard, in between 'First Responder' and 'Exit').
XCode 12.5, iOS 13.6
// A cell with a single UILabel
class UILabelCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
}
// A cell with a signle UISwitch
class UISwitchCell: UITableViewCell {
#IBOutlet weak var uiSwitch: UISwitch!
}
// The TableViewController to hold the prototype cells.
class CellPrototypeTableViewController: UITableViewController {
// Loads the view controller from the storyboard
static let shared: CellPrototypeTableViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "cellProtoypeVC") as! CellPrototypeTableViewController
viewController.loadViewIfNeeded() // Make sure to force view controller to load the view!
return viewController
}()
// Helper methods to deque the cells
func dequeUILabeCell() -> UILabelCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "uiLabelCell") as! UILabelCell
return cell
}
func dequeUISwitchCell() -> UISwitchCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "uiSwitchCell") as! UISwitchCell
return cell
}
}
Use:
class MyTableViewController: UITableViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Dequeue the cells from the shared instance
switch indexPath.row {
case 0:
let uiLabelCell = CellPrototypeTableViewController.shared.dequeUILabeCell()
uiLabelCell.label.text = "Hello World"
return uiLabelCell
case 1:
let uiSwitchCell = CellPrototypeTableViewController.shared.dequeUISwitchCell()
uiSwitchCell.uiSwitch.isOn = false
return uiSwitchCell
default:
fatalError("IndexPath out of bounds")
}
}
// Handling Segues
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.row {
case 0: self.performSegue(withIdentifier: "first", sender: nil)
case 1: self.performSegue(withIdentifier: "second", sender: nil)
default:
fatalError("IndexPath out of bounds")
}
}
}