I was having trouble with NSTokenFieldCell, so I proceeded to create a new project in Xcode to isolate the problem. Here is what I did:
Dropped a NSTableView into the main window;
selected the second column's text cell, and changed it's Class (via Identity Inspector) to NSTokenFieldCell;
implemented a minimum possible data source object, with the following code:
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return 1;
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
return #"aa, bb";
}
At first it seems to work fine, but if you double-click a cell to edit, then tab and shift+tab to switch cells back and forth, eventually the application crashes with a BAD ACCESS when the token field cell receive focus.
I'm using Xcode 4.2 in Lion 10.7.2, with all the default settings that come with a Mac OS X Cocoa Application template.
Looks like a bug in Cocoa. If you turn on zombies you'll see this:
2011-10-31 00:02:43.802 tokenfieldtest[35622:307] *** -[NSTokenFieldCell respondsToSelector:]: message sent to deallocated instance 0x1da761f10
I tried setting a delegate for the table and implementing - (NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row, returning a new NSTokenFieldCell every time (for just the token column), but I got the same error.
The original solution brought a new problem.
When NSTokenFieldCell is not fully displayed in NSTableView, entering the editing state and then exiting will cause the table view to display abnormally.
So I tried repeatedly to get a better solution:
class MyTokenFieldCell: NSTokenFieldCell {
override func fieldEditor(for controlView: NSView) -> NSTextView? {
return nil;
}
}
It may be that NSTableView's editor reuse mechanism for NSTokenFieldCell has problems, which caused the program to crash.
fieldEditorForView: is overwritten here, returning nil, which should cause the editor to be recreated every time when editing, avoiding reuse, and thus solves the crash problem.
The following is the original answer.
⚠️ Because the solution causes other problems, please ignore it.
I also encountered this problem. My solution is to temporarily keep the cells used by the table view.
Custom NSTokenFieldCell: after each copy, temporarily save the copy.
class MyTokenFieldCell: NSTokenFieldCell {
static var cells = [NSUserInterfaceItemIdentifier: [MyTokenFieldCell]]()
override func copy(with zone: NSZone? = nil) -> Any {
let cell = super.copy(with: zone)
guard let tokenFieldCell = cell as? MyTokenFieldCell else { return cell }
tokenFieldCell.identifier = self.identifier
guard let identifier = tokenFieldCell.identifier else { return cell }
var cells = MyTokenFieldCell.cells[identifier] ?? []
cells.append(tokenFieldCell)
if cells.count > 4 {
cells.removeFirst()
}
MyTokenFieldCell.cells[identifier] = cells
return cell
}
}
Implement the tableView(_:dataCellFor:row:) method of NSTableViewDelegate, provide MyTokenFieldCell for the table view, and set the identifier to: <columnIdentifier>:<row>
extension ViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, dataCellFor tableColumn: NSTableColumn?, row: Int) -> NSCell? {
guard let columnIdentifier = tableColumn?.identifier, columnIdentifier.rawValue == "token" else {
return tableColumn?.dataCell(forRow: row) as? NSCell
}
let cell = MyTokenFieldCell()
cell.isEditable = true
cell.identifier = .init("\(columnIdentifier.rawValue):\(row)")
return cell
}
}
Related
I'd like to use a delegate method written in Objective-C in Swift. The method is included in the MGSwipeTableCell framework (MGSwipeTableCell.h).
Objective-C:
-(BOOL) swipeTableCell:(MGSwipeTableCell*) cell tappedButtonAtIndex:(NSInteger) index direction:(MGSwipeDirection)direction fromExpansion:(BOOL) fromExpansion;
I try to convert it into Swift to and use the method:
func swipeTableCell(cell:MGSwipeTableCell, index:Int, direction:MGSwipeDirection, fromExpansion:Bool) -> Bool {
return true
}
But I don't know why but the function isn't getting called. Did I something wrong? I just want to get the indexPath of the swiped cell with this function.
You should implement MGSwipeTableCellDelegate protocol in your table view controller first. So you can just write:
class TableViewController : UITableViewController, MGSwipeTableCellDelegate {
....
....
....
func swipeTableCell(cell:MGSwipeTableCell, index:Int, direction:MGSwipeDirection, fromExpansion:Bool) -> Bool {
return true
}
}
and then when creating cells in cellForRowAtIndexPath: method you should create it like this:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let reuseIdentifier = "cell"
var cell = self.table.dequeueReusableCellWithIdentifier(reuseIdentifier) as! MGSwipeTableCell!
if cell == nil {
cell = MGSwipeTableCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: reuseIdentifier)
}
cell.delegate = self
return cell
}
Then you'll be able to track when swipe method is called because you set the cell delegate property.
It appears that the library you're trying to use has not adopted Objective-C nullability annotations yet, as such, any return values or arguments which are objects will be translated into Swift as implicitly unwrapped optionals (with the exclamation mark).
So, the signature you're looking for is this:
func swipeTableCell(cell: MGSwipeTableCell!, tappedButtonAtIndex: Int, direction: MGSwipeDirection, fromExpansion: Bool) -> Bool
But with that said, you need to add the protocol conformance anyway. If you do that first then try to write this method out, it should autocomplete to exactly how Swift expects it to look.
And then just make sure you're actually setting the cell's delegate property to whatever object is implement this method.
I have a UITableView that's working just fine. When I enable editing mode using the following code inside of viewDidLoad() method:
self.tableView.editing = true
I get the following error:
fatal error: unexpectedly found nil while unwrapping an Optional value
on this line:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController.sections!.count // error here
}
I checked and fetchedResultsController is NOT nil, but sections is.
This is not the case if editing mode is disabled.
What could be the cause?
To stop this particular error, you can simply return a default value in numberOfSectionsInTableView when fetchedResultsController.sections is nil:
Note the use of sections? instead of sections!, and the Nil Coalescing Operator ??:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController.sections?.count ?? 0 // 0 is the default
}
That doesn't explain why your fetchedResultsController is returning a nil sections array when your tableView is in editing mode.
I suspect that sections might be nil because you're setting editing in viewDidLoad and that's triggering a table view reload. At that point, fetchedResultsController may not have had enough time to fetch any results at all so it doesn't have any sections to return. It's possible that simply returning a default of 0 when sections is nil will be enough since then fetchedResultsController will have time to finish its loading and reload the table view with the proper data.
I thought the syntax was:
self.tableView.setEditing(true, animated: true)
I haven't modeled this in any code. So if it doesn't help, let me know and I will try again.
I'm trying to develop a simple application, which upon clicking a menu item, it shows a window containing an NSTableView.
The problem is that the app crashes just after NSTableView displaying the data. Full stack trace:
* thread #1: tid = 0x2107, 0x00007fff943bce90 libobjc.A.dylib\`objc_msgSend + 16,
stop reason = EXC_BAD_ACCESS (code=13, address=0x0)
frame #0: 0x00007fff943bce90 libobjc.A.dylib`objc_msgSend + 16
Since I'm using ARC, I should exclude any reference counting issue; but maybe I'm creating the controller (needed to create the window) in a bad way, and it's being free'd erroneously.
This is the code of the AppController that create and shows the window:
- (IBAction)showPreferences:(id)sender {
if(!preferencesWindow) {
preferencesWindow = [[[PreferencesWindowController alloc]
initWithWindowNibName:#"PreferencesWindow"] window];
}
[preferencesWindow makeKeyAndOrderFront:sender];
}
This code in PreferencesWindowController implements the dataSource protocol (needed by the NSTableView).
- (int)numberOfRowsInTableView:(NSTableView *)tabView {
return 1;
}
- (id)tableView:(NSTableView *)tabView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row {
NSString *val = [NSString stringWithFormat:#"%#[%d]", [tableColumn identifier], row];
return val;
}
It's not causing the crash per se. But if I remove the PreferencesWindowController from NSTableView's dataSource, it doesn't crash, so it should be somewhat related.
Where's the mistake?
EDIT: using the profiler (Instruments) with the zombies preset, I can see there's an object whose reference counter goes negative:
but anyway, the stack is outside the code I wrote. I can't put a breakpoint there, and I can't see which is the object being released twice (or I should say I don't know how to)
The line preferencesWindow = [[[PreferencesWindowController alloc] initWithWindowNibName:#"PreferencesWindow"] window] looks suspicious, because while you are referencing the window itself with a strong reference, it looks like you're letting ARC release the PreferencesWindowController.
Try storing the PreferencesWindowController object in its own strong variable/property and let me know.
This might be a very simple question but didn't yield any results when searching for it so here it is...
I am trying to work out a way to check if a certain view controller can perform a segue with identifier XYZ before calling the performSegueWithIdentifier: method.
Something along the lines of:
if ([self canPerformSegueWithIdentifier:#"SegueID"])
[self performSegueWithIdentifier:#"SegueID"];
Possible?
To check whether the segue existed or not, I simply surrounded the call with a try-and-catch block. Please see the code example below:
#try {
[self performSegueWithIdentifier:[dictionary valueForKey:#"segue"] sender:self];
}
#catch (NSException *exception) {
NSLog(#"Segue not found: %#", exception);
}
Hope this helps.
- (BOOL)canPerformSegueWithIdentifier:(NSString *)identifier
{
NSArray *segueTemplates = [self valueForKey:#"storyboardSegueTemplates"];
NSArray *filteredArray = [segueTemplates filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"identifier = %#", identifier]];
return filteredArray.count>0;
}
This post has been updated for Swift 4.
Here is a more correct Swift way to check if a segue exists:
extension UIViewController {
func canPerformSegue(withIdentifier id: String) -> Bool {
guard let segues = self.value(forKey: "storyboardSegueTemplates") as? [NSObject] else { return false }
return segues.first { $0.value(forKey: "identifier") as? String == id } != nil
}
/// Performs segue with passed identifier, if self can perform it.
func performSegueIfPossible(id: String?, sender: AnyObject? = nil) {
guard let id = id, canPerformSegue(withIdentifier: id) else { return }
self.performSegue(withIdentifier: id, sender: sender)
}
}
// 1
if canPerformSegue("test") {
performSegueIfPossible(id: "test") // or with sender: , sender: ...)
}
// 2
performSegueIfPossible(id: "test") // or with sender: , sender: ...)
As stated in the documentation:
Apps normally do not need to trigger segues directly.
Instead, you configure an object in Interface Builder associated with
the view controller, such as a control embedded in its view hierarchy,
to trigger the segue. However, you can call this method to trigger a
segue programmatically, perhaps in response to some action that cannot
be specified in the storyboard resource file. For example, you might
call it from a custom action handler used to process shake or
accelerometer events.
The view controller that receives this message must have been loaded
from a storyboard. If the view controller does not have an associated
storyboard, perhaps because you allocated and initialized it yourself,
this method throws an exception.
That being said, when you trigger the segue, normally it's because it's assumed that the UIViewController will be able to respond to it with a specific segue's identifier. I also agree with Dan F, you should try to avoid situations where an exception could be thrown. As the reason for you not to be able to do something like this:
if ([self canPerformSegueWithIdentifier:#"SegueID"])
[self performSegueWithIdentifier:#"SegueID"];
I am guessing that:
respondsToSelector: only checks if you are able to handle that message in runtime. In this case you can, because the class UIViewController is able to respond to performSegueWithIdentifier:sender:. To actually check if a method is able to handle a message with certain parameters, I guess it would be impossible, because in order to determine if it's possible it has to actually run it and when doing that the NSInvalidArgumentException will rise.
To actually create what you suggested, it would be helpful to receive a list of segue's id that the UIViewController is associated with. From the UIViewController documentation, I wasn't able to find anything that looks like that
As for now, I am guessing your best bet it's to keep going with the #try #catch #finally.
You can override the -(BOOL)shouldPerformSegueWithIdentifier:sender: method and do your logic there.
- (BOOL) shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"someSegue"]) {
if (!canIPerformSegue) {
return NO;
}
}
return YES;
}
Hope this helps.
Reference CanPerformSegue.swift
import UIKit
extension UIViewController{
func canPerformSegue(identifier: String) -> Bool {
guard let identifiers = value(forKey: "storyboardSegueTemplates") as? [NSObject] else {
return false
}
let canPerform = identifiers.contains { (object) -> Bool in
if let id = object.value(forKey: "_identifier") as? String {
return id == identifier
}else{
return false
}
}
return canPerform
}
}
Swift version of Evgeny Mikhaylov's answer, which worked for me:
I reuse a controller for two views. This helps me reuse code.
if(canPerformSegueWithIdentifier("segueFoo")) {
self.performSegueWithIdentifier("segueFoo", sender: nil)
}
else {
self.performSegueWithIdentifier("segueBar", sender: nil)
}
func canPerformSegueWithIdentifier(identifier: NSString) -> Bool {
let templates:NSArray = self.valueForKey("storyboardSegueTemplates") as! NSArray
let predicate:NSPredicate = NSPredicate(format: "identifier=%#", identifier)
let filteredtemplates = templates.filteredArrayUsingPredicate(predicate)
return (filteredtemplates.count>0)
}
It will be useful, before call performSegue, check native storyboard property on base UIViewController (for example screen was from StoryBoard or Manual Instance)
extension UIViewController {
func performSegueWithValidate(withIdentifier identifier: String, sender: Any?) {
if storyboard != nil {
performSegue(withIdentifier: identifier, sender: sender)
}
}
}
enter image description here
There is no way to check that using the standard functions, what you can do is subclass UIStoryboardSegue and store the information in the source view controller (which is passed to the constructor). In interface builder select "Custom" as the segue type as type the class name of your segue, then your constructor will be called for every segue instantiated and you can query the stored data if it exists.
You must also override the perform method to call [source presentModalViewController:destination animated:YES] or [source pushViewController:destination animated:YES] depending on what transition type you want.
Is it possible to navigate an NSTableView's editable cell around the NSTableView using arrow keys and enter/tab? For example, I want to make it feel more like a spreadsheet.
The users of this application are expected to edit quite a lot of cells (but not all of them), and I think it would be easier to do so if they didn't have to double-click on each cell.
In Sequel Pro we used a different (and in my eyes simpler) method: We implemented control:textView:doCommandBySelector: in the delegate of the TableView. This method is hard to find -- it can be found in the NSControlTextEditingDelegate Protocol Reference. (Remember that NSTableView is a subclass of NSControl)
Long story short, here's what we came up with (we didn't override left/right arrow keys, as those are used to navigate within the cell. We use Tab to go left/right)
Please note that this is just a snippet from the Sequel Pro source code, and does not work as is
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
NSUInteger row, column;
row = [tableView editedRow];
column = [tableView editedColumn];
// Trap down arrow key
if ( [textView methodForSelector:command] == [textView methodForSelector:#selector(moveDown:)] )
{
NSUInteger newRow = row+1;
if (newRow>=numRows) return TRUE; //check if we're already at the end of the list
if (column>= numColumns) return TRUE; //the column count could change
[tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableContentView editColumn:column row:newRow withEvent:nil select:YES];
return TRUE;
}
// Trap up arrow key
else if ( [textView methodForSelector:command] == [textView methodForSelector:#selector(moveUp:)] )
{
if (row==0) return TRUE; //already at the beginning of the list
NSUInteger newRow = row-1;
if (newRow>=numRows) return TRUE;
if (column>= numColumns) return TRUE;
[tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableContentView editColumn:column row:newRow withEvent:nil select:YES];
return TRUE;
}
Well it isn't easy but I managed to do it without having to use RRSpreadSheet or even another control. Here's what you have to do:
Create a subclass of NSTextView, this will be the field editor. For this example the name MyFieldEditorClass will be used and myFieldEditor will refer to an instance of this class.
Add a method to MyFieldEditorClass called "- (void) setLastKnownColumn:(unsigned)aCol andRow:(unsigned) aRow" or something similar, and have it save both the input parameter values somewhere.
Add another method called "setTableView:" and have it save the NSTableView object somewhere, or unless there is another way to get the NSTableView object from the field editor, use that.
Add another method called - (void) keyDown:(NSEvent *) event. This is actually overriding the NSResponder's keyDown:. The source code should be (be aware that StackOverflow's MarkDown is changing < and > to < and >):
- (void) keyDown:(NSEvent *) event
{
unsigned newRow = row, newCol = column;
switch ([event keyCode])
{
case 126: // Up
if (row)
newRow = row - 1;
break;
case 125: // Down
if (row < [theTable numberOfRows] - 1)
newRow = row + 1;
break;
case 123: // Left
if (column > 1)
newCol = column - 1;
break;
case 124: // Right
if (column < [theTable numberOfColumns] - 1)
newCol = column + 1;
break;
default:
[super keyDown:event];
return;
}
[theTable selectRow:newRow byExtendingSelection:NO];
[theTable editColumn:newCol row:newRow withEvent:nil select:YES];
row = newRow;
column = newCol;
}
Give the NSTableView in your nib a delegate, and in the delegate add the method:
- (BOOL) tableView:(NSTableView *)aTableView shouldEditColumn:(NSTableColumn *) aCol row:aRow
{
if ([aTableView isEqual:TheTableViewYouWantToChangeBehaviour])
[myFieldEditor setLastKnownColumn:[[aTableView tableColumns] indexOfObject:aCol] andRow:aRow];
return YES;
}
Finally, give the Table View's main window a delegate and add the method:
- (id) windowWillReturnFieldEditor:(NSWindow *) aWindow toObject:(id) anObject
{
if ([anObject isEqual:TheTableViewYouWantToChangeBehaviour])
{
if (!myFieldEditor)
{
myFieldEditor = [[MyFieldEditorClass alloc] init];
[myFieldEditor setTableView:anObject];
}
return myFieldEditor;
}
else
{
return nil;
}
}
Run the program and give it a go!
Rather than forcing NSTableView to do something it wasn't designed for, you may want to look at using something designed for this purpose. I've got an open source spreadsheet control which may do what you need, or you may at least be able to extend it to do what you need: MBTableGrid
I wanted to reply to the answers here but the reply button seems to be missing so I'm forced to proved an answer when I really just want to ask a question about the replies.
Anyway, I've seen a few answers for overriding the -keyDown event of the table view that say to subclass the TableView but according to every Objective-C book I've read so far, and several Apple training videos, you should very rarely if ever subclass one of the core classes. In fact every single one of them makes the point that C programmers have a fascination with subclassing and that's not how Objective-C works; that Objective-C is all about helpers and delegates not subclassing.
So, should I just ignore any of the responses that say to subclass as this seems to be in direct contradiction to the precepts of Objective-C?
--- Edit ---
I found something that worked without subclassing the NSTableView. While I do move the inheritance up one notch on the chain from NSObject to NSResponder I'm not totally subclassing the NSTableView. I'm just adding the ability to override the keyDown event.
I made the class I was using as a delegate inherit from NSResponder instead of NSObject and set the nextResponder to that class in awakeFromNib. I was then able to trap key presses using the keydown event. I of course connected the IBOutlet and set the delegate in Interface Builder.
Here's my code with the minimum needed to show the trapping of the key:
Header file
// AppController.h
#import <Cocoa/Cocoa.h>
#interface AppController : NSResponder {
IBOutlet NSTableView *toDoListView;
NSMutableArray *toDoArray;
}
-(int)numberOfRowsInTableView:(NSTableView *)aTableView;
-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex;
#end
Here's the m file.
// AppController.m
#import "AppController.h"
#implementation AppController
-(id)init
{
[super init];
toDoArray = [[NSMutableArray alloc] init];
return self;
}
-(void)dealloc
{
[toDoArray release];
toDoArray = nil;
[super dealloc];
}
-(void)awakeFromNib
{
[toDoListView setNextResponder:self];
}
-(int)numberOfRowsInTableView:(NSTableView *)aTableView
{
return [toDoArray count];
}
-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(int)rowIndex
{
NSString *value = [toDoArray objectAtIndex:rowIndex];
return value;
}
- (void)keyDown:(NSEvent *)theEvent
{
//NSLog(#"key pressed: %#", theEvent);
if (theEvent.keyCode == 51 || theEvent.keyCode == 117)
{
[toDoArray removeObjectAtIndex:[toDoListView selectedRow]];
[toDoListView reloadData];
}
}
#end