How to add associated object to uitapgesture? - objective-c

Currently I have in my code
tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(addCompoundToLabel:)];
My code would be cleaner if it was something like
tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(addCompoundToLabel:) withObject:data];
I read that associated objects allow you to pass objects in a gesture recognizer, but after reading the docs, I don't quite understand how to implement it into my code. An example implementation of associated objects would be appreciated. Thanks
EDIT:
this is what the beginning of addCompound looks like
- (void)addCompoundToLabel:(UIGestureRecognizer *)recognizer {
if( [recognizer state] == UIGestureRecognizerStateEnded ) {
FormulaLabel* label = (FormulaLabel*)[recognizer view];

Using associated objects is a messy solution. Instead, you could create a false target object, give it your UIView and your Data, and have access to both of them in the selector method. It is a lot more readable, and your code expresses your intent a lot better.
Here is a quick and dirty example:
#interface FalseTarget : NSObject {
MyViewController *_viewCtrl;
MyData *_data;
}
-(id)initWithViewCtrl:(MyViewController*)viewCtrl andData:(MyData*)data;
-(void)tap:(id)sender;
#end
#implementation
-(id)initWithViewCtrl:(MyViewController*)viewCtrl andData:(MyData*)data {
self = [super init];
if (self) {
_viewCtrl = viewCtrl;
_data = data;
}
return self;
}
-(void)tap:(id)sender {
[_viewCtrl processTapFromSender:sender withData:_data];
}
#end
Add processTapFromSender:withData: method to your controller:
-(void)processTapFromSender:(id)sender withData:(MyData*)data {
// Your action
}
Now you can create your tap recognizer like this:
FalseTarget *target = [[FalseTarget alloc] initWithViewCtrl:self andData:data];
tap = [[UITapGestureRecognizer alloc] initWithTarget:target action:#selector(tap:)];

Related

Conversion of custom init routines in Swift

I need to subclass SVModalWebViewController.m (GitHub), which is itself a subclass of UINavigationController, and am incorporating this code into a new app that will be created entirely in Swift. I've run into several issues, so I decided to first convert just this very simple class into Swift to debug.
Initialization is a little different in Swift compared with Objective-C (read more about Swift initialization here), which is why I'm not returning self in the Swift code.
Here's the original Obj-C code (minus the *.h, which instantiates barsTintColor):
#import "SVModalWebViewController.h"
#import "SVWebViewController.h"
#interface SVModalWebViewController ()
#property (nonatomic, strong) SVWebViewController *webViewController;
#end
#implementation SVModalWebViewController
#pragma mark - Initialization
- (id)initWithAddress:(NSString*)urlString {
return [self initWithURL:[NSURL URLWithString:urlString]];
}
- (id)initWithURL:(NSURL *)URL {
self.webViewController = [[SVWebViewController alloc] initWithURL:URL];
if (self = [super initWithRootViewController:self.webViewController]) {
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self.webViewController
action:#selector(doneButtonClicked:)];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
self.webViewController.navigationItem.leftBarButtonItem = doneButton;
else
self.webViewController.navigationItem.rightBarButtonItem = doneButton;
}
return self;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:NO];
self.webViewController.title = self.title;
self.navigationBar.tintColor = self.barsTintColor;
}
#end
And here's the entire Swift version of the class (I simplified to just one init method since I didn't need to init with URL specifically):
import UIKit
class SVModalWebViewController: UINavigationController {
var webViewController: SVWebViewController!
var barsTintColor: UIColor?
// This was added because of a compiler error indicating it needed to be added
init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
init(address urlString: String!) {
let url: NSURL = NSURL.URLWithString(urlString)
self.webViewController = SVWebViewController(URL: url)
super.init(rootViewController: self.webViewController)
let doneButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Done, target: self.webViewController, action: Selector("doneButtonClicked:"))
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
self.navigationItem.leftBarButtonItem = doneButton
} else {
self.navigationItem.rightBarButtonItem = doneButton
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// self.title may not be set, and is considered an optional in Swift, so we have to check first
if self.title {self.webViewController.title = self.title}
self.navigationBar.tintColor = self.barsTintColor
}
}
The issue I'm having relates to setting the Done button in the navigationItem. Currently they're not showing up at all when this code is called:
let webViewController = SVModalWebViewController(address: "http://www.google.com");
webViewController.barsTintColor = UIColor.blueColor()
self.presentModalViewController(webViewController, animated: true)
The modal view appears just fine and I'm able to correctly set the bar color property, but the Done button does not show up. It appears that I don't have proper access to the navigationItem of the UINavigationController. Note that I had to add the init with nibName method due to a compiler error without it. This wasn't required in the Obj-C code and I'll admit I'm not sure why it's needed in Swift - that could be part of the issue.
Any thoughts as to why I cannot set the self.navigationItem.leftBarButtonItem property of the UINavigationController? Thanks!
A UINavigationController does not have a navigationItem. Well, it does, but it's useless. It is the navigationItem of the child controller (in this case, the rootViewController, which you are also calling self.webViewController) that appears in the navigation bar.

Can not call class method

I'm building a tag-based application and want to call the same function from each tab (ViewController).
I'm trying to do it in the following way:
#import "optionsMenu.h"
- (IBAction) optionsButton:(id)sender{
UIView *optionsView = [options showOptions:4];
NSLog(#"options view tag %d", optionsView.tag);
}
optionsMenu.h file:
#import <UIKit/UIKit.h>
#interface optionsMenu : UIView
- (UIView*) showOptions: (NSInteger) tabNumber;
#end
optionsMenu.m file:
#import "optionsMenu.h"
#implementation optionsMenu
- (UIView*) showOptions:(NSInteger) tabNumber{
NSLog(#"show options called");
UIView* optionsView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
optionsView.opaque = NO;
optionsView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5f];
//creating several buttons on optionsView
optionsView.tag = 100;
return optionsView;
}
#end
The result is that i never get the "show options called" debug message and thus optionsView.tag is always 0.
What am i doing wrong?
I understand this is most probably an easy and stupid question, but i am not able to solve it myself.
Any feedback is appreciated.
First thing to note is that this is an instance method (and not a Class method as described in the question title). This means that in order to call this method you should have alloc/init an instance of your Class and send the message to the instance. For example:
// Also note here that Class names (by convention) begin with
// an uppercase letter, so OptionsMenu should be preffered
optionsMenu *options = [[optionsMenu alloc] init];
UIView *optionsView = [options showOptions:4];
Now, if you just want to create a Class method that returns a preconfigured UIView, you could try something like this (provided that you do not need access to ivars in your method):
// In your header file
+ (UIView *)showOptions:(NSInteger)tabNumber;
// In your implementation file
+ (UIView *)showOptions:(NSInteger)tabNumber{
NSLog(#"show options called");
UIView *optionsView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
optionsView.opaque = NO;
optionsView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5f];
//creating several buttons on optionsView
optionsView.tag = 100;
return optionsView;
}
And finally send the message like this:
UIView *optionsView = [optionsMenu showOptions:4]; //Sending message to Class here
Finally do not forget of course to add your view as a subview in order to display it.
I hope that this makes sense...

How do I get NSTextFinder to show up

I have a mac cocoa app with a webview that contains some text. I would like to search through that text using the default find bar provided by NSTextFinder. As easy as this may seem reading through the NSTextFinder class reference, I cannot get the find bar to show up. What am I missing?
As a sidenote:
- Yes, I tried setting findBarContainer to a different view, same thing. I reverted back to the scroll view to eliminate complexity in debugging
- performTextFinderAction is called to perform the find operation
**App Delegate:**
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.textFinderController = [[NSTextFinder alloc] init];
self.webView = [[STEWebView alloc] initWithFrame:CGRectMake(0, 0, self.window.frame.size.width, 200)];
[[self.window contentView] addSubview:self.webView];
[self.textFinderController setClient:self.webView];
[self.textFinderController setFindBarContainer:self.webView.enclosingScrollView];
[[self.webView mainFrame] loadHTMLString:#"sample string" baseURL:NULL];
}
- (IBAction)performTextFinderAction:(id)sender {
[self.textFinderController performAction:[sender tag]];
}
**STEWebView**
#interface STEWebView : WebView <NSTextFinderClient>
#end
#implementation STEWebView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
- (NSUInteger) stringLength {
return [[self stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"] length];
}
- (NSString *)string {
return [self stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"];
}
In my tests, WebView.enclosingScrollView was null.
// [self.textFinderController setFindBarContainer:self.webView.enclosingScrollView];
NSLog(#"%#", self.webView.enclosingScrollView);
Using the following category on NSView, it is possible to find the nested subview that extends NSScrollView, and set that as the container, allowing the NSTextFinder to display beautifully within a WebView
#interface NSView (ScrollView)
- (NSScrollView *) scrollView;
#end
#implementation NSView (ScrollView)
- (NSScrollView *) scrollView {
if ([self isKindOfClass:[NSScrollView class]]) {
return (NSScrollView *)self;
}
if ([self.subviews count] == 0) {
return nil;
}
for (NSView *subview in self.subviews) {
NSView *scrollView = [subview scrollView];
if (scrollView != nil) {
return (NSScrollView *)scrollView;
}
}
return nil;
}
#end
And in your applicationDidFinishLaunching:aNotification:
[self.textFinderController setFindBarContainer:[self scrollView]];
To get the Find Bar to appear (as opposed to the default Find Panel), you simply have to use the setUsesFindBar: method.
In your case, you'll want to do (in your applicationDidFinishLaunching:aNotification method):
[textFinderController setUsesFindBar:YES];
//Optionally, incremental searching is a nice feature
[textFinderController setIncrementalSearchingEnabled:YES];
Finally got this to show up.
First set your NSTextFinder instances' client to a class implementing the <NSTextFinderClient> protocol:
self.textFinder.client = self.textFinderController;
Next, make sure your NSTextFinder has a findBarContainer set to the webView category described by Michael Robinson, or get the scrollview within the webView yourself:
self.textFinder.findBarContainer = [self.webView scrollView];
Set the find bar position above the content (or wherever you wish):
[self.webView scrollView].findBarPosition = NSScrollViewFindBarPositionAboveContent;
Finally, tell it to show up:
[self.textFinder performAction:NSTextFinderActionShowFindInterface];
It should show up in your webView:
Also, not sure if it makes a difference, but I have the NSTextFinder in the XIB, with a referencing outlet:
#property (strong) IBOutlet NSTextFinder *textFinder;
You may also be able to get it by simply initing it like normal: self.textFinder = [[NSTextFinder alloc] init];

Working on custom component: subclass UIView or UIViewController?

I'm working on a custom implementation of UISegmentedControl.
I'd like to create a component that able to receive config data and from which obtain a custom View similar to UISegmentedControl.
I started subclassing a UIView and i can create a custom UISegmentedControl with this code:
CustomSegment *segment = [[CustomSegment alloc]
initWithTitles:[NSArray arrayWithObjects:#"one",#"two",nil]];
[self.window addSubview:segment];
But now i'd like to improve my class and add some more customizable parameters to it.
For example i'd like add a custom separators, define the button fonts and so on... here my doubt:
Is it better to work on a UIView subClass or you suggest me to subclass a UIViewController, where i can manage View hierarchy in method like -(void)loadView and -(void)viewDidLoad ?
In a simple UIView subclass, when i launch the custom init method, i setup immediately subviews... while using a UIViewController i can call custom init and define how my subview is builded into -(void)loadView.
Don't use an UIViewController, just extend the UIView class like you did and keep extending its functionality.
Remember to save a pointer to each subview you add (i.e. buttons) in order to be able to access them later.
Define custom setters, for example, a custom setter for changing a button label title would be:
- (void) setButton1Title:(NSString*)str forState:(UIControlState)state{
//You can add some control here
if ([str length] > 20) return;
[_button1 setTitle:str forState:state]; //_button1 is my reference to the button
}
And so on. Don't provide direct access to your subviews, use methods instead.
Also, you can use "layoutSubviews" method to define how your views are going to be displayed in your custom view.
Hope it helps you.
Edit: In your case, I don't see why using lauoutSubviews method but I want to show you what I was trying to say.
Lets say that for example I need to create an UIView class to represent a "Contact" object in my application.
This is what I would do:
#interface ContactView : UIView{
UILabel* _nameLabel;
UILabel* _ageLabel;
Contact* _contact;
}
#property (retain) Contact* contact;
#end
#implementation ContactView
#synthetize contact = _contact;
-(id)initWithContact:(Contact*)c{
self = [super init];
if (self) {
_nameLabel = [[UILabel alloc] init];
_nameLabel.frame = CGRectZero;
[self addSubview:_nameLabel];
[_nameLabel release];
_ageLabel = [[UILabel alloc] init];
_ageLabel.frame = CGRectZero;
[self addSubview:_ageLabel];
[_ageLabel release];
self.contact = c;
}
}
- (void) layoutSubviews{
[super layoutSubviews];
_nameLabel.frame = CGRectMake(0.0f, 0.0f, 200.0f, 25.0f);
_ageLabel.frame = CGRectMake(0.0f, 25.0f, 200.0f, 25.0f);
if (self.contact){
_nameLabel.text = self.contact.name;
_ageLabel.text = self.contact.age;
}else{
_nameLabel.text = #"Unavailable";
_ageLabel.text = #"Unavailable";
}
}
- (void) setContact:(Contact*)c{
self.contact = c;
[self layoutSubviews];
}
#end
Check out how the "layoutSubiews" is used to set the correct frame and data to the labels.
Usually, I use it a lot when creating custom UITableViewCells where you have to reuse the view.
Let me know if I'm being confusing.

Releasing "referential" variables in method scope

In objective-c (cocoa touch) I have a series of UIViewControllers that I am switching between.
- (void)switchViews:(id)sender
{
UIButton *button = (UIButton *)sender;
UIViewController *nextViewController;
int tag = button.tag;
switch (tag)
{
// -- has already been created
case kFinancialButton:
nextViewController = financials;
break;
case kSocialButton:
if (social == nil)
{
SocialViewController *socialViewController = [[SocialViewController alloc] initWithNibName:#"SocialViewController" bundle:nil];
social = socialViewController;
[socialViewController release];
}
nextViewController = social;
break;
case kTicketingButton:
if (ticketing == nil)
{
TicketingViewController *ticketingViewController = [[TicketingViewController alloc] initWithNibName:#"TicketingViewController" bundle:nil];
ticketing = ticketingViewController;
[ticketingViewController release];
}
nextViewController = ticketing;
break;
}
///////
------> // -- [button/nextViewController release]????
///////
[self setActiveButton:button];
}
As you can see, I'm assigning one of the view controllers to "nextViewController". What I'm wondering is if I need to release this "local" variable or if it's ok to leave alone since it's just pointing to one of my view controllers (which I release in dealloc). I don't think "tag" needs to be released since it's a "primitive", correct? How about button? I don't quite understand what should and shouldn't be released explicitly so perhaps I'm being overcautious. Thanks in advance.
In general you only have to release a variable that you've retain'd init'd or copy'd.
Edit:
After reading your code a little bit more, it seems like you'd have other issues with bad values. The below code makes a little more sense to me. This assumes that financials, social, and ticketing are all #synthesized ivars.
- (void)switchViews:(id)sender
{
UIButton *button = (UIButton *)sender;
UIViewController *nextViewController;
int tag = button.tag;
switch (tag)
{
// -- has already been created
case kFinancialButton:
nextViewController = self.financials;
break;
case kSocialButton:
if (!social) {
self.social = [[[SocialViewController alloc] initWithNibName:#"SocialViewController" bundle:nil] autorelease];
}
nextViewController = self.social;
break;
case kTicketingButton:
if (!ticketing) {
self.ticketing = [[[TicketingViewController alloc] initWithNibName:#"TicketingViewController" bundle:nil] autorelease];
}
nextViewController = self.ticketing;
break;
}
// Do something with nextViewController I'd assume
[self setActiveButton:button];
}