0

I'm using this code to add some buttons above the on-screen keyboard in my iOS app. Note that I'm using the older inputAccessoryView method on phones and pre-iOS 9 devices, and the newer inputAssistantItem on iOS 9 tablets:

UITextView *textInputMultiline = [[UITextView alloc] initWithFrame:frame];
TextInputToolbar *textInputToolbar = [TextInputToolbar alloc]; // custom class
(void)[textInputToolbar initWithNibName:@"TextInputToolbar" bundle:nil];
textInputToolbar.textView = textInputMultiline;
if ((self.appDelegate.isTablet)&&([[[UIDevice currentDevice] systemVersion] compare:@"9.0"] != NSOrderedAscending)) {
    NSMutableArray *barButtonItems = [NSMutableArray array];
    [barButtonItems addObject:[[UIBarButtonItem alloc] initWithTitle:@"button 1" style:UIBarButtonItemStylePlain target:textInputToolbar action:@selector(button1)]];
    [barButtonItems addObject:[[UIBarButtonItem alloc] initWithTitle:@"button 2" style:UIBarButtonItemStylePlain target:textInputToolbar action:@selector(button2)]];
    [barButtonItems addObject:[[UIBarButtonItem alloc] initWithTitle:@"button 3" style:UIBarButtonItemStylePlain target:textInputToolbar action:@selector(button3)]];
    UIBarButtonItem *representativeItem = nil;
    UIBarButtonItemGroup *group = [[UIBarButtonItemGroup alloc] initWithBarButtonItems:barButtonItems representativeItem:representativeItem];
    textInputMultiline.inputAssistantItem.trailingBarButtonGroups = [NSArray arrayWithObject:group];
} else {
    textInputMultiline.inputAccessoryView = textInputToolbar.view;
}

My custom toolbar class looks like this:

@interface TextInputToolbar : UIViewController {
    UITextView *textView;

    IBOutlet UIButton *button1;
    IBOutlet UIButton *button2;
    IBOutlet UIButton *button3;
}

@property (nonatomic, strong) UITextView *textView;

- (void)insertText:(NSString *)text;

- (IBAction)button1;
- (IBAction)button2;
- (IBAction)button3;

@end

And...

#import "TextInputToolbar.h"

@implementation TextInputToolbar

@synthesize textView;

- (void)viewDidLoad {
    NSLog(@"viewDidLoad");
    [super viewDidLoad];
}

- (void)insertText:(NSString *)text {
    [self.textView insertText:text];
}

- (IBAction)button1 {
    NSLog(@"button1");
    [self insertText:@"1"];
}

- (IBAction)button2 {
    NSLog(@"button2");
    [self insertText:@"2"];
}

- (IBAction)button3 {
    NSLog(@"button3");
    [self insertText:@"3"];
}

@end

This worked as expected when my app was not using ARC. I recently updated to ARC, which only required minimal changes to the code above (I previously had autoreleases on the UIBarButtonItems, and didn't have the (void) cast before the initWithNibName), and now the buttons still appear as expected, but don't work. On iOS 8, I get a crash when tapping one of the buttons ([CALayer button1]: unrecognized selector sent to instance, which I think indicates an invalid memory pointer), and on iOS 9, nothing happens when I tap a button, and the logging in the button methods is not called.

I have a copy of my project before updating to ARC, and when I go back and run that on my iOS 8 or iOS 9 devices, the toolbar buttons work again. So it seems ARC is either the source of the problem or the trigger of a different problem.

If I point a barButtonItem to self, like this...

[barButtonItems addObject:[[UIBarButtonItem alloc] initWithTitle:@"button 3" style:UIBarButtonItemStylePlain target:self action:@selector(test)]];

...the method call is received as expected. If I change a barButtonItem selector to an invalid method, like this...

[barButtonItems addObject:[[UIBarButtonItem alloc] initWithTitle:@"button 3" style:UIBarButtonItemStylePlain target:textInputToolbar action:@selector(flkjfd)]];

...nothing happens. That suggests to me that textInputToolbar has somehow become nil by the time the button selector is called, because if it weren't nil, this would generate an unrecognized selector crash.

But I know that the TextInputToolbar class and its view are loading, because the logging in viewDidLoad occurs, and because the view appears as the inputAccessoryView an phones and iOS 8 tablets.

Any idea what's happening, or what else I can do to troubleshoot?

arlomedia
  • 8,534
  • 5
  • 60
  • 108

2 Answers2

1

Your code was never correct and only worked because of a leak. You're basically losing / not retaining the view controller. It used to just continue to exist and work, but under ARC it's released so there's nothing to respond to the buttons. ARC has fixed your memory issue and made you aware that there's a problem, though not in an idea way.

To fix, retain the view controller while its view is being used.

Also, I'm not sure where you learnt to do:

TextInputToolbar *textInputToolbar = [TextInputToolbar alloc]; // custom class
(void)[textInputToolbar initWithNibName:@"TextInputToolbar" bundle:nil];

but you shouldn't. Do that all on one line. Don't ignore the object returned from init calls - it may be different to the one you originally called it on.

Wain
  • 118,658
  • 15
  • 128
  • 151
  • You're right, I should have had an autorelease on textInputToolbar, which I just confirmed breaks the functionality in non-ARC. Can you show me how to correctly retain and release textInputToolbar in this situation? Do I need to use an instance variable of the parent view controller instead of a local variable, or can I somehow do everything within this block of code? – arlomedia Jul 12 '16 at 17:19
  • You need to add a property onto the containing view controller, an instance variable, yes. – Wain Jul 12 '16 at 19:45
1

There is what happened when call these codes:

UITextView *textInputMultiline = [[UITextView alloc] initWithFrame:frame];

//alloc textInputToolbar (textInputToolbar.retaincount = 1)
TextInputToolbar *textInputToolbar = [TextInputToolbar alloc];
textInputToolbar.textView = textInputMultiline;

if ((self.appDelegate.isTablet)&&([[[UIDevice currentDevice] systemVersion] compare:@"9.0"] != NSOrderedAscending)) {
    NSMutableArray *barButtonItems = [NSMutableArray array];
    //add button items....
    UIBarButtonItem *representativeItem = nil;

    //alloc UIBarButtonItemGroup (group.retaincount = 1)
    UIBarButtonItemGroup *group = [[UIBarButtonItemGroup alloc] initWithBarButtonItems:barButtonItems representativeItem:representativeItem];

    //strong reference group (group.retaincount = 2)
    textInputMultiline.inputAssistantItem.trailingBarButtonGroups = [NSArray arrayWithObject:group];
    //autorelease group 

} else {
    //strong reference textInputToolbar.view (textInputToolbar.view.retaincount = 2)
    textInputMultiline.inputAccessoryView = textInputToolbar.view;
}

//autorelease textInputToolbar (textInputToolbar.retaincount = 0, textInputToolbar.view.retaincount = 1)

In iOS 8, textInputToolbar will be dealloc but it's view will not. That's why you can see the buttons but when you click them, the observer had became as a wild pointer, runtime cannot find the function so it crashed.

In iOS 9, textInputToolbar will be dealloc also. Because of you create the button items and set the observe(weak reference) target out of the InputToolbar, when the textInputToolbar dealloc, the observe became nil.So the function would not be called.

Hao
  • 201
  • 1
  • 3