iOS Child Views, SRP and custom events - objective-c

I'm kind of new to iOS development and need some advice. I have a chat like app. The UI should have a child view for posting new messages to the server and one child view for viewing messages in a table view.
I've build both child views in Interface Builder as XIB:s. But I'm not sure how to use these on the main view controller. Can I use IB to add my custom views to the design surface? Or do I need to add these programmatically?
What is the best way to send messages or custom events between these two child views? I'd like to have them as decoupled as possible. Mostly I'd like to send an event when the user log on or off, so that the UI can react to these changes. I'd also like the table view with messages to know when a new messages is posted from the write view.
// Johan

In order to get the contents of a xib file you've got to load it first sending loadNibNamed:owner:options: message to NSBundle class.
Consider you have a UIView subclass named CustomView and CustomView.xib file. In the xib file each view has a tag. Your .h file would look like:
#interface CustomView : UIView
#property (nonatomic, assign) UILabel *someTextLabel; //use assign in order to not to override dealloc method
#end
.m
#implementation CustomView
- (id)init {
self = [super init];
if (self) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomView" owner:nil options:nil];
[self addSubview:[topLevelObjects objectAtIndex:0]]; //this object is a CustomView.xib view
self.someTextLabel = (UILabel *)[self viewWithTag:5]; //consider you have a UILabel on CustomView.xib that has its tag set to 5
}
return self;
}
#end
This is about how to use .xibs for your custom UIView subclasses. If your app is like a chat then you'll have to add them programmatically.
As for the best way to send messages between two custom views, you'll have to create a weak reference to each other in each of them.
in one
#property (nonatomic, assign) CustomView *customView;
in another
#property (nonatomic, assign) AnotherCustomView *anotherCustomView;
and just send them messages when some even happens
- (void)buttonPressed {
[customView handleButtonPressedEvent];
}
Let me know if this is clear.

Related

Loading xib window from status menu

I have MainMenu.Xib which has a status menu element. The MainMenu File Owner is mapped to AppDelegate.
I also have another Xib which is a Window and it's File Owner is mapped to a ViewController with the same name.
So what I have tried and it kind of works is I have created an action in the appDelegate and have mapped the menu item in the status menu to the action in the appDelete using the First Responder.
In the action I have put:
SubscriptionsViewController *vc = [[SubscriptionsViewController alloc] initWithNibName:#"Subscriptions" bundle:nil];
[vc view];
If I step through the code it the window shows up but then goes away. So I have two questions
1) I thought there was a way to load the xib with NSMenuItem without the need of the above code.
2) How do I keep the window from closing right away? Do I need to save the view pointer or something?
edit: format code.
1) I thought there was a way to load the xib with NSMenuItem without the need of the above code.
Since NSWindow is not inherited from NSView like in iOS (UIWindow:UIView), it makes no sense to use NSViewController to load window from a xib. Use subclass of NSObject instead.
#interface SubscriptionsViewController : NSObject
#property (assign, nonatomic) IBOutlet NSWindow *window;
#end
#implementation SubscriptionsViewController
- (id)init
{
self = [super init];
if (self) {
[NSBundle loadNibNamed:#"Subscriptions" owner:self];
}
return self;
}
#end
2) How do I keep the window from closing right away? Do I need to save the view pointer or something?
It depends on the context to retain the instance of subscriptionsViewController or not. You can use below code to display a window, where the instance of window is in nib -
self.subscriptionsViewController = [[SubscriptionsViewController alloc] init];
[self.subscriptionsViewController.window makeKeyAndOrderFront:self];
Remember if "Visible At Launch" is set in nib, then the window is visible when you instantiate subscriptionsViewController.
You may add your window into the MainMenu.xib instead of using an addition xib file and create an outlet in the AppDelegare.h as
#property (assign) IBOutlet NSWindow *window;
Then all you need to do is
window.isVisible = !window.isVisible;
in the necessary action method...

How can i make a UITabbar like this?

I would like to create a UITabbar like below but i don't know what is the logic to do that.
Here is the large answer:
First of all, you will need to create a UIView subclass to get a view that looks like the bar that you want. It can be composed by a background UIImageView and three buttons.
Then, the best thing would be to create a subclass of the UITabBarController and in its viewDidLoad or at any point where the flow will go through just once, you instantiate one view of type specified at first point. You should place the frame of this view in order to hide the original tabbar of the controller.
This would be the custom bar header file:
#interface CustomBar : UIView
{
}
#property (nonatomic, retain) UIImageView *backgroundView;
#property (nonatomic, retain) NSArray *buttons;
#end
You can easily complete the implementation. You can try to look for how to instantiate it with a nib file to make it easier to design it. In order to test, you can first just set the background color to green or something visible.
Then, this would be the subclass of the UITabBarController class:
#interface CustomTabBarController : UITabBarController
#property (nonatomic, retain) CustomBar *customBar;
#end
#implementation CustomTabBarController
- (void)viewDidLoad
{
[super viewDidLoad];
self.customBar = [[[CustomBar alloc] initWithFrame:[self.tabBar frame]] autorelease];
[self.view addSubview:self.customBar];
}
#end
Please, remember to implement the dealloc if you are not using ARC.
The thing I am not sorting out here is how to create the communication between the buttons from the custombar and the tabbarcontroller. This should be solved by delegates. If you need help with that, I will complete that too.
Good luck!

Loading XIB file without a UIViewController

I'd like to design a UIView and some sub-views (UIWebView, UIToolbar, some UIBarButtonItems, a progress indicator and so-forth) using the Interface Builder, but I think it's unnecessary to do this traditionally, by using a UIViewController, using presentViewController:animated etc.
So, I created a custom class, with the .h file code as follows:
#interface FileInteractionManager : NSObject {
}
#property (nonatomic, retain) IBOutlet UIView *fileView;
#property (nonatomic, retain) IBOutlet UIWebView *fileWebView;
#property (nonatomic, retain) IBOutlet UIBarButtonItem *printButton;
#property (nonatomic, retain) IBOutlet UIBarButtonItem *optionsButton;
#property (nonatomic, retain) IBOutlet UIBarButtonItem *doneButton;
My .m file is as follows:
#implementation FileInteractionManager
#synthesize fileView, fileWebView, doneButton, optionsButton, printButton;
-(id)init {
NSArray *array = [[NSBundle mainBundle] loadNibNamed:#"FileInteractionView" owner:self options:nil];
NSLog(#"Load success!");
return self;
}
Finally, I create a stand-alone xib file named 'FileInteractionView.xib', change the file's owner to the custom class I created above, and wire up the IBOutlets.
When I call the init method on my class, I can see in the debugger that all my IBOutlet objects are instantiated properly.
My questions are:
Is the loadNibNamed:owner:options: method the right way to load my stand-alone .xib file? I don't like the fact that this method returns an array I have no use for (the top-level object returned matches my variable fileView, but I've already linked them through the Interface Builder).
Is my general approach correct in solving my problem? I carried out the above steps because I wanted a simple UIView object that I could add to my existing UIViewController, rather than present and dismiss a whole new UIViewController.
I use a little different approach. I create a subclass of UIView (MyCustomView i.e.) then the xib with the UI of the view and change the (main) view class the the one just defined. In the xib then you can link the outlet to the custom view itself (not the file owner).
Finally in the class definition I create a function like this:
+ (id) newFromNib
{
NSArray *nibArray = [[UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil] instantiateWithOwner:nil options:nil];
return nibArray[0];
}
Just a couple of notes:
1) this's a class method, you can use "self" just for stuff like "NSStringFromClass([self class])" but the real object is the variable returned
2) this example suppose the xib have the same name of the class (via NSStringFromClass([self class]) so I can copy-paste it without changing anything ;) ) and that your view is the first one defined in the xib (the standard). If you store more than a "main" view inside one xib pick the right element.
so where I need MyCustomView I do something like:
MyCustomView* mycv = [MyCustomView newFromNib];
then set frame/center and add to superview...
I think this way is pretty usefull if you have a "library" of complex UI elements and want to design them via xib then add when needed.
il Malvagio Dottor Prosciutto answer is nice. Here is a possible alternative.
Load nib in NS_DESIGNATED_INITIALIZER and become owner of subview
If we accept the xib to only hold a subview instead of the view itself, then we can load the subview in initWithFrame: and keep an ownership construction in xib.
#interface MyCustomView ()
#property (strong, nonatomic) IBOutlet UIView *subview;
#end
#implementation MyCustomView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil];
[self addSubview:self.subview];
return self;
}
#end

Objective-c proper delegation

I'm new to objective-c and, maybe I haven't grassped the concept of delegation very clearly yet, but i hope to do it by using it. I'm trying to implement a delegation in my app.
Idea is that i have class TableViewController which has NSMutableArray used for TableView initialization. I need to reinitialize this Array from my DropDown class. I'v tried to do that using delegation but failed to do it yet, maybe there is something wrong with it. I could pass TableViewController to DropDown class and edit the table via object. But i'd like to get it done using delegation.
Here is my TableViewController.h
#protocol TableViewControllerdelegate;
#interface TableViewController : UIViewController<UITableViewDataSource,UITableViewDelegate,MFMessageComposeViewControllerDelegate>
{
ControllerType controllerType;
}
#property (retain, nonatomic) IBOutlet UITableView *tableView;
#property (retain, nonatomic) NSMutableArray *dataArray;
#property (retain, nonatomic) NSArray *imageArray;
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil andType:(ControllerType)type;
- (void)sendSMS: (NSString *) sms;
#end;
Here is my DropDown.h
#import "TableViewController.h"
#interface DropDownExample : UITableViewController <VPPDropDownDelegate, UIActionSheetDelegate> {
#private
VPPDropDown *_dropDownSelection;
VPPDropDown *_dropDownSelection1;
VPPDropDown *_dropDownSelection2;
VPPDropDown *_dropDownSelection3;
VPPDropDown *_dropDownSelection4;
VPPDropDown *_dropDownDisclosure;
VPPDropDown *_msg;
VPPDropDown *_dropDownCustom;
NSIndexPath *_ipToDeselect;
}
+ (bool) uncheck:(UITableViewCell *) cell andData:(NSString *) data;
- (void)reloadData;
#end
And this is how i try to edit my tableview object array
TableViewController *newControll = (TableViewController*)[UIApplication sharedApplication].delegate;
NSMutableArray *arrayWithInfo = [[NSMutableArray alloc] initWithObjects:AMLocalizedString(#"Status", nil),AMLocalizedString(#"Call", nil),AMLocalizedString(#"Location", nil),AMLocalizedString(#"Control", nil),AMLocalizedString(#"Sim", nil),AMLocalizedString(#"Object", nil),AMLocalizedString(#"Info", nil),nil];
newControll.dataArray = arrayWithInfo;
[arrayWithInfo release];
[newControll.tableView reloadData];
I get it running, but it get's '-[AppDelegate setDataArray:]: unrecognized selector sent to instance after reaching this code.
OK, I am not sure if I got this right but it finally clicked for me what delegation is and why I need it. Hopefully you'll understand too once you read through my scenario.
History
Previously, in my UITabBar app, I wanted to show a custom form view overlaid on top of my view controller to enter name and email.
Later I also needed to show the same custom overlay on top of another view controller on another tab.
At the time I didn't really know what delegation was for, so the first method I used to tackle this problem was NSNotificationCenter. I duplicated the same code to my second view controller and hooked it up to a button press event.
On pressing a button on the second view controller on another tab, it certainly showed my custom overlay, just like my first view controller.
However, this is where the problem starts.
The Problem
I needed to close my custom form view. So using NSNotificationCenter, I posted a notification and the listener callback method for the notification was told to close my custom view.
The problem was, using NSNotificationCenter, all listeners both in my first tab and my second tab responded to the posted notification and as a result, instead of closing just the custom form view overlaid on top of my second view controller, it closed ALL my custom view, regardless of where the custom view was opened from.
What I wanted was when I tap on the "X" button to close my custom form view, I only want it to close it for that single instance of the custom view, not all the other ones I had opened.
The Solution: Delegation
This is where it finally clicked for me - delegation.
With delegation, I tell each instance of my custom form view who the delegate was, and if I was to tap on the "X" button to close my custom view, it only close it for that single instance that was opened, all the other view controllers were untouched.
Some Code
Right, down to some code.
Not sure if this is the best way to do it (correct me if I am wrong) but this is how I do it:
// ------------------------------------------------------------
// Custom Form class .h file
// ------------------------------------------------------------
#protocol MyCustomFormDelegate <NSObject>
// if you don't put a #optional before any method, then they become required
// in other words, you must implement these methods
-(void)sendButtonPressed;
-(void)closeButtonPressed;
// example: these two methods here does not need to be implemented
#optional
-(void)optionalMethod1;
-(void)optioinalMethod2;
#end
#interface MyCustomFormView : UIView
{
...
id<MyCustomFormDelegate> delegate;
}
...
#property (nonatomic, retain) id<MyCustomFormDelegate> delegate;
#end
// ------------------------------------------------------------
// Custom Form class .m file
// ------------------------------------------------------------
...
#implementation TruckPickerView
#synthesize delegate;
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if(self)
{
...
[btnSend addTarget:self selector:#selector(sendEmail) forControlEvent:UIControlEventTouchUpInside];
...
[btnClose addTarget:self selector:#selector(closeForm) forControlEvent:UIControlEventTouchUpInside];
}
return self;
}
-(void)sendEmail
{
// code sends email
...
// ------------------------------------------------------------
// tell the delegate to execute the delegate callback method
//
// note: the implementation will be defined in the
// view controller (see below)
// ------------------------------------------------------------
[delegate sendButtonPressed];
}
-(void)closeForm
{
// ------------------------------------------------------------
// tell the delegate to execute the delgate callback method
//
// note: the implementation will be defined in the
// view controller (see below)
// ------------------------------------------------------------
[delegate closeButtonPressed];
}
// ------------------------------------------------------------
// view controller .h file
// ------------------------------------------------------------
#import "MyCustomFormView.h"
// conform to our delegate protocol
#interface MyViewController <MyCustomFormDelegate>
{
...
// create a single instance of our custom view
MyCustomFormView *customForm;
}
#property (nonatomic, retain) MyCustomFormView *customForm;
// ------------------------------------------------------------
// view controller .m file
// ------------------------------------------------------------
#synthesize customForm;
-(void)viewDidLoad
{
customForm = [[MyCustomFormView alloc] initWithFrame:....];
// tell our custom form this view controller is the delegate
customForm.delegate = self;
// only show the custom form when user tap on the designated button
customForm.hidden = YES;
[self.view addSubview:customForm];
}
-(void)dealloc
{
...
[customForm release];
[super dealloc];
}
// helper method to show and hide the custom form
-(void)showForm
{
customForm.hidden = NO;
}
-(void)hideForm
{
customForm.hidden = YES;
}
// ------------------------------------------------------------
// implement the two defined required delegate methods
// ------------------------------------------------------------
-(void)sendButtonPressed
{
...
// email has been sent, do something then close
// the custom form view afterwards
...
[self hideForm];
}
-(void)closeButtonPressed
{
// Don't send email, just close the custom form view
[self hideForm];
}
You get that error, because (as the error says) you're sending a setDataArray: message to your app delegate (the AppDelegate class).
[UIApplication sharedApplication].delegate;
This will return the delegate of you app. There are a couple of ways to find out which class is your app's delegate, but usually it's called AppDelegate (as in your case) and it's implementing the UIApplicationDelegate protocol too.
You can't simply cast that to a completely different class. If your app delegate has an ivar or property of type TableViewController you have to use accessors to get it. If it's a property, you can use the dot notation. If it's an ivar, you can either implement a getter method that returns the ivar, or make it a property instead.
// assuming your app delegate has a TableViewController property called myTableViewController.
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
TableViewController *tableViewController = appDelegate.myTableViewController;
This will fix the error, but your use of the delegate pattern is wrong too. I don't see where you're using any custom delegates. You forward declare a TableViewControllerdelegate protocol, but I don't see any declaration of it, or I don't see where you're trying to use it.

Using id<protocol> for file's owner in Interface Builder?

I have a custom UITableViewCell that I am instantiating from a nib using instantiateWithOwner:(id)owner options:(NSDictionary *)options. When the nib is instantiated, I am saving it to an IBOutlet defined in my view controller, which is set as the file's owner in the .xib file. Everything's been working great.
I've now come across the need to use this custom cell in multiple view controllers. I was hoping that I could define a protocol (e.g. CustomCellOwner), which multiple view controllers could implement. The protocol would simply define the IBOutlet used to reference the cell when instantiated.
So ideally, I would like to set "file's owner" to:
id <CustomCellOwner>
in Interface Builder.
However, Interface Builder only seems to allow you to set file's owner to a known class, not to an id implementing a protocol?
Is there any way to do this? Or, a simpler way to approach this problem?
Thanks!
This isn't the solution you're asking for, but you could make a UIViewController subclass that you subclass for each view controller that needs to use your nib. Something like:
#interface CustomCellOwnerViewController : UIViewController
#property (nonatomic, strong) IBOutlet UIButton *someButton;
-(IBAction)doSomething;
#end
And then use that as the base class for each:
#interface FirstView : CustomCellOwnerViewController
Then you could simply set File's Owner to CustomCellOwnerViewController with no problems.
Just an idea.
I ran into this today and didn't find a good solution. I did however hack it so that it seems to work ok. It definitely feels like a hack though.
First I created a "fakeOwner" class like this:
#interface fakeOwner : NSObject
#property (nonatomic, assign) IBOutlet MyBaseCell* itemTableCell;
#end
#implementation fakeOwner
#synthesize itemTableCell;
#end
I then set the object's owner in the XIB as fakeOwner and connected the outlet. Then for each controller that wants to use these cells I add the same property and create the class like this:
[[NSBundle mainBundle] loadNibNamed:#"MyBaseCell" owner:self options:nil];
MyBaseCell* itemCell = self.itemTableCell;
self.itemTableCell = nil;
Since the fakeOwner and my controller have the same IBOutlet, loading the cell with the controller as the owner causes the connection to happen even though that isn't what is explicitly set in the XIB.
Not 100% if the memory management is right currently (I think it's ok), but other than that it seems to work great. I would love to see a better way of doing this though.
Making a fake owner will work; however, such a solution may be fragile and inextensible. In a sense, the cell owns itself, but even that is technically incorrect. The truth is that UITableViewCells do not have owners.
The proper way to implement a custom table view cells is to first create a custom subclass of UITableViewCell. In this class you will define all of the IBOutlets and such for the cell. Here is a sample of a header file:
#interface RBPersonCell : UITableViewCell
#property (nonatomic, strong) IBOutlet UILabel * nameLabel;
#property (nonatomic, strong) IBOutlet UILabel * ageLabel;
- (void)setupWithPerson:(Person *)person;
#end
From there, I have a convenience method that creates the cell from the nib, if necessary:
+ (id)cellForTableView:(UITableView *)tableView reuseIdentifier:(NSString *)reuseID fromNib:(UINib *)nib {
if (!reuseID)
reuseID = [self cellIdentifier];
id cell = [tableView dequeueReusableCellWithIdentifier:reuseID];
if (!cell) {
NSArray * nibObjects = [nib instantiateWithOwner:nil options:nil];
// Sanity check.
NSAssert2(([nibObjects count] > 0) &&
[[nibObjects objectAtIndex:0] isKindOfClass:[self class]],
#"Nib '%#' does not appear to contain a valid %#",
[self nibName], NSStringFromClass([self class]));
cell = [nibObjects objectAtIndex:0];
}
return cell;
}
This method encapsulates all of the creation code so I never have to see it or rewrite it. It assumes that the custom cell is the first root view in the nib. This is a fairly safe assumption since you should only have the custom cell as a root view.
With all this code in place, you are ready to work in Interface Builder. You first need to set the custom class in the identity inspect. Next, don't forget to set your cell identifier. For convenience, it's best to use the name of the custom class. When you drag your connections, rather than drag them to File's Owner, drag your connections to the custom cell itself.
Most of what I have learned about custom table view cells comes from iOS Recipes recipes 15-16. Here is a free extract directly from The Pragmatic Bookshelf. You can check out that book for more details.
EDIT:
I finally got around to open sourcing my RBSmartTableViewCell class. You can find it on my GitHub. You should find this class more useful than the code directly from iOS Recipes, since my class treats all cells the same, regardless of whether they are constructed using XIBs, UIStoryboard, or code. This repo also includes working samples.
In iOS 5.0 there is now the registerNib:forCellReuseIdentifier: method on UITableView which I believe attempts to solve a similar problem.
From the documentation:
When you register a nib object with the table view and later call the dequeueReusableCellWithIdentifier: method, passing in the registered identifier, the table view instantiates the cell from the nib object if it is not already in the reuse queue.
This could be an alternative approach depending on your requirements.
Another option might be to create a lightweight 'factory' object that handles the creation of the cells for you. This object would be the FilesOwner in interface builder, with the rootObject outlet set appropriately.
#interface NibLoader : NSObject
#property (nonatomic, strong) UINib * nib;
#property (nonatomic, strong) IBOutlet id rootObject;
- (id)initWithNibName:(NSString *)name bundle:(NSBundle *)bundleOrNil;
- (id)instantiateRootObject;
#end
#implementation NibLoader
#synthesize nib, rootObject;
- (id)initWithNibName:(NSString *)name bundle:(NSBundle *)bundleOrNil {
self = [super init];
if (self) {
self.nib = [UINib nibWithNibName:name bundle:bundleOrNil];
}
return self;
}
- (id)instantiateRootObject {
self.rootObject = nil;
[self.nib instantiateWithOwner:self options:nil];
NSAssert(self.rootObject != nil, #"NibLoader: Nib did not set rootObject.");
return self.rootObject;
}
#end
Then in the view controllers:
NibLoader *customCellLoader = [[NibLoader alloc] initWithNibName:#"CustomCell" bundle:nil];
self.customCell = customCellLoader.instantiateRootObject;
I prefer explicitly setting the root object instead of searching through the array returned from instantiateWithOwner:options: because I know the position of the objects in this array has changed in the past.