How can I change the type of a ViewController after creation? - xcode

I have a program that creates UIViewControllers in a UISplitView depending upon which row is selected in a UITableView. For example, if the user clicks the row called 'settings', the program creates a view controller called settingsTableVC (which displays in the left-hand pane of the UISplitViewController) and a detail view controller called settingsDetailVC, which appears in the right hand pane.
The logic works fine, but I have a lot of code for each row in the initial tableView, for creating and setting up each ViewController, and their public properties and it seems to me a simpler way would be to define two generic ViewControllers, then cast them to the correct type in a method. That way, I can define their public properties generically (as they are all basically the same properties, pointers to the masterViewController and the tableViewController)
E.G.
UIViewController *leftController = nil;
UIViewController *rightController = nil;'
Then, in the didSelectRowAtIndexPath:
leftController = [[SettingTableViewController alloc]init];
rightController = [[SettingsDetailViewController alloc]init];
leftController.masterVC = self;
rightController.tableVC = leftController;
However, the complier warns - 'UIViewController does not declare a property masterVC'.
How can I change the class of these ViewControllers at runtime?

You can do either of the following:
SettingsTableViewController *leftControllerCasted = (SettingsTableViewController *)leftController;
SettingsDetailViewController *rightControllerCasted = (SettingsDetailViewController *)rightController;
leftControllerCasted.masterVC = self;
rightControllerCasted.tableVC = leftController; // Doesn't matter if you assign it to leftController or leftControllerCasted
or
((SettingsTableViewController *)leftController).masterVC = self;
((SettingsDetailViewController *)rightController).tableVC = leftController;
You have created your objects correctly, but as you assign them a UIViewController pointer object type the compiler doesn't know your objects are of your custom class.

Related

Handling NSMenuDelegate menuWillOpen for changing targets

There are lots of related answers about using menuWillOpen. They all explain that one needs to set the menu's delegate first.
This is easy when I have just one target, like a Preferences window or the main application.
But what if I have a document based app, and I need to have the active document handle menuWillOpen? Then the delegate isn't a constant any more.
What's the proper way to handle this? Do I have to set the delegate to a single object (like the AppDelegate) and then forward the call to the active view controller (but how is that done correctly)? Or is there some other elegant way?
I came up with this code which appears to work:
// This is in my AppDelegate class, and the NSMenu's delegate points to it:
- (void)menuWillOpen:(NSMenu *)menu {
// Forward to active document controller
NSWindow *mainWindow = [NSApplication sharedApplication].mainWindow;
NSResponder *r = mainWindow.firstResponder;
while (r) {
if ([r respondsToSelector:_cmd]) {
[(id<NSMenuDelegate>)r menuWillOpen:menu];
return;
}
r = r.nextResponder;
}
}
It assumes that a controller down the responder chain implements menuWillOpen:

How to store a non-standard persistent attribute in Core Data (ex. NSRect)?

I'm struggling with my first Core Data app and not having the smoothest ride :o(
I have a drawing app with a baseclass called DrawingObject which subclasses NSManagedObject and has two properties:
#NSManaged var frameAsValue: NSValue
var frame: NSRect
DrawingObject has a subclass DrawingRectangle. All have corresponding entities with fully qualified classnames set. The frameAsValue attribute is marked as transformable and frame is marked as Undefined transient. The problem is that I get an unrecognised selector error for the frameAsValue property when creating a DrawingRectangle.
I've seen suggestions to transform NSRect to a string to save it to Core Data but this seems error prone (localization) and hackish (if thats a proper word ;o). Here is the code for DrawingObject:
class DrawingObject: NSManagedObject {
#NSManaged var frameAsValue: NSValue
var frame: NSRect = NSZeroRect {
didSet {
frameAsValue = NSValue(rect: frame)
}
}
override func awakeFromInsert() {
frame = NSMakeRect(0, 0, 0, 0)
}
override func awakeFromFetch() {
frame = frameAsValue.rectValue
}
}
I'm now assuming that you have to declare all of the inherited properties in the class hierarchy in each entity. I don't have time to test this now, but will be back soon.
You have to set parent entity of DrawingRectangle to DrawingObject to be able to inherit the properties from the parent entity (just like in the class-hierarchy). You also have to set parent--child entity relationships if you intend to save different subclasses of a parent-entity/parent-class into the same to-many relationship.
For examble if you want to save different subclasses of DrawingObjects in a #NSManaged var objects: NSSet in a Drawing for example, you have to set the parent relationships on the entities to correspond to your NSManagedObject class-hierarchy. So in this case you would set the parent of the DrawingRectangle entity to DrawingObject.
Otherwise NSRect is saved as in the code shown in the question by setting the frame-property of the DrawingObject-entity to transient and optional and the frameAsValue as transformable. You should not specify a valuetransformer so Core Data will use the default value transformer which works perfectly in this case.

NSWindowController in Swift. Subclassing and initializing with Nib

In a test Swift project, I am subclassing NSWindowController. My NSWindowController subclass is designed to work with a particular Nib file. It is desirable, then, that when my window controller is initialized, the nib file is automatically loaded by the window controller instance. In Objective-C, this was achieved by doing:
#implementation MyWindowController
- (id)init {
self = [super initWithWindowNibName:"MyWindowNib"]
if (self) {
// whatever
}
return self
}
#end
Now, in Swift this is not possible: init() cannot call super.init(windowNibName:), because the later is declared not as a designated initializer, but as a convenience one by NSWindowController.
How can this be done in Swift? I don't see a strightforward way of doing it.
P.S.: I have seen other questions regarding this topic, but, as long as I've been able to understand, the solutions all point to initialize the Window Controller by calling init(windowNibName:). Please note that this is not the desired beheaviour. The Window Controller should be initialized with init(), and it should be the Window Controller itself who "picks up" its Nib file and loads it.
If you use the init() just to call super.init(windowNibName:), you could instead just override the windowNibName variable.
override var windowNibName: String {
get {
return "MyWindowNib"
}
}
Then there should be no need to mess with the initializers.
You can create your own convenience initializer instead:
override convenience init() {
self.init(windowNibName: "MyWindowNib")
}
You should instead opt in to replacing all designated initializers in your subclass, simply delegating to super where appropriate. Confer https://stackoverflow.com/a/24220904/1460929

How to manage multiple windows in Cocoa apps with Interface Builder

I have this application with 3 classes: AppController, Profile, ProfileBuilder. I also need 3 windows: one for each class. I tried keeping all 3 as subclasses of NSObject and applying initWithNibName to an NSWindowController class WindowController variable but when I tried outputting some values on each window it wouldn't work, and also the window resulted as null using NSLog. I was wondering what was the best way to manage multiple windows, perhaps all from a same class like an AppWindowsController involving as least as possible specific code in the other classes, and keeping, if possible, the other classes as subclasses of NSObject and not NSWindowController. So if there is, maybe a way to control the behavior of the windows remotely, adding as least as possible code inside the specific classes, just to keep them as clear as possible and uniquely focused on their content. Thanks, hope I made myself clear, I'm actually pretty new to the Cocoa framework.
You should be able to load the nib files with your windows in an init method for your different classes. For example, in Profile, you could do something like this:
-(id)init {
if (self = [super init]) {
NSArray *array;
BOOL success = [[NSBundle mainBundle] loadNibNamed:#"ProfileWindow" owner: self topLevelObjects:&array];
if (success) {
for (id obj in array) {
if ([obj isKindOfClass:[NSWindow class]]) {
self.profileWindow = obj;
}
}
[self.profileWindow makeKeyAndOrderFront:self];
}
}
return self;
}
profileWindow is a property (typed as strong). In the xib file, I set the File's Owner to Profile.
I just like to improve the solution of rdelmar.
You don't need to iterate over the array to find the NSWindow class.
If you define profileWindow as an outlet and connect it in the IB, the call
[[NSBundle mainBundle] loadNibNamed:#"ProfileWindow" owner:self topLevelObjects:&array];
will assign the window object to your outlet, the array stuff is not required.
The key here is the owner object which act as interface. In the IB you can define the class type of the owner and if so, see its outlets.

NSArrayController creating, modifying and then selecting a new object

The main view of my NSPersistentDocument based application is a table view (bound to an NSArrayController) showing the list of records, below it there is an "add record" button. I want the button to cause the following (supposedly trivial) behavior.
Create an new object
Set some defaults to the new object (that are stored in the main document and not available globally)
Add it to the table view.
Here are the things that I tried or dismissed:
use the NSArrayController "add" action - problem: will not return the new object and implementation is deferred so it is impossible to modify the newly created object
Override the init of the data class - will not work - I need to access data that is stored in the document class instance
Subclass NSArrayController and override "newObject" - again - will not work because I need to access data that is stored in the document.
Following code "almost" worked:
- (IBAction)newRecord:(id)sender
{
MyDataClass *newRecord = [recordsArrayController newObject];
newRecord.setting1=self.defaultSetting1;
newRecord.setting2=self.defaultSetting2;
// ... etc...
[recordsArrayController addObject:newRecord];
[recordsTable scrollRowToVisible:[recordsTable selectedRow]];
[newRecord release];
}
This code actually works well, for unsaved documents. But if I save the document and re-open it then clicking on the add button results in the new record showing twice in the table.
Obviously the "addObject" is redundant (although it works fine in unsaved documents) but without it the new object is not selected.
Simple case that should work:
MyDataClass *newRecord = [controller newObject];
// configure newRecord
[controller addObject:newRecord];
[newRecord release];
In order for the new object to be selected, the controller needs to have been configured for -setSelectsInsertedObjects:YES previously.
But, there's an alternative which I'd consider more proper. Subclass NSArrayController like so (slighty pseudo-code):
#interface MyRecordController : NSArrayController
#property id recordSetting1;
#property id recordSetting2;
#end
#implementation MyRecordController
#synthesize recordSetting1;
#synthesize recordSetting2;
- (id)newObject
{
id result = [super newObject];
newRecord.setting1 = self.recordSetting1;
newRecord.setting2 = self.recordSetting2;
return result;
}
#end
So, your code then becomes:
- (IBAction)newRecord:(id)sender
{
recordsArrayController.recordSetting1 = self.defaultSetting1;
recordsArrayController.recordSetting2 = self.defaultSetting2;
[recordsArrayController add:self];
}
Really, all you need to do is modify your code to omit the addObject: call. To make your new object selected, just do this:
[recordsArrayController setSelectedObjects:[NSArray arrayWithObject:newObject]];
before you do your call to swcrollRowToVisible:. You're right that the addObject: call is unneeded. As you've seen, it's ending up in the array controller twice.
Also, you won't need to call [newRecord release]. The documentation says the object is retained by the array controller. It's not failing now because it's being retained a second time when you do addObject:.

Resources