2

I have a grid of UIButtons. When I hit an 'edit' button, I want a delete button to appear over each of these buttons, which when pressed, deletes the button (and associated data). A bit like apple's home screen, when you hold down a button and it starts to wiggle with an X in the corner.

According to this post: Subclass UIButton to add a property I can use Associative References to add a property to each of my buttons. I've tried to add a UIButton as a property of my custom UIButton but I can't seem to get it to appear and have the feeling this isn't the right way to go. Here's my custom button main:

    #import "UIButton+Property.h"
#import <objc/runtime.h>

@implementation UIButton(Property)

static char UIB_DELETEBUTTON_KEY;

@dynamic deleteButton;


- (void)setDeleteButton:(UIButton *)deleteButton {
    deleteButton = [UIButton buttonWithType:UIButtonTypeInfoDark];
    deleteButton.frame = CGRectMake(100, 100, 50, 50);
    objc_setAssociatedObject(self, &UIB_DELETEBUTTON_KEY, deleteButton, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIButton *)deleteButton {
    return (UIButton *)objc_getAssociatedObject(self, &UIB_DELETEBUTTON_KEY);
}

@end

And here's where I add the buttons programmatically:

//Create a custom button for each custom book doc
for (int i = 0; i < [customBookDocs count]; ++i) {
    BookDoc *customBookDoc = [customBookDocs objectAtIndex:i];
    NSString *bookTitle = customBookDoc.book.title;

    //create a button for each book
    CGRect frame = CGRectMake(xCoord, yCoord, 200, 200);
    UIButton *bookButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    bookButton.bookDoc = customBookDoc;
    [bookButton setFrame:frame];
    [bookButton setTitle:bookTitle forState:UIControlStateNormal];
    [bookButton addTarget:self action:@selector(bookButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    xCoord += 250;

    [self.view addSubview:bookButton];
    [self.view addSubview:bookButton.deleteButton];
}

Is there an easier more sensible way to do this? Or am I on the right track?

Community
  • 1
  • 1
Smikey
  • 8,106
  • 3
  • 46
  • 74
  • 1
    You never actually set the delete button. Also your associative method just overrides the delete button you set anyway. – Joe Apr 17 '12 at 15:28

1 Answers1

1

ORIGINAL RESPONSE BEGAN:

... Someone else may have more to say about that, but I'm not sure why you'd need to use object association here. You can certainly add another button to your button as a property using regular subclassing, which is the route that I would take. ...

EDITS BELOW:

I thought that I had subclassed a UI control directly, but I realized that I was mistaken when I went to look for the code. @Joe rightly pointed out in the comments that there are issues with directly subclassing UI controls.

I was able to implement something like the functionality you described without using Associated Objects, by creating a wrapper class to hold the button and its related delete button. It works, but it's not very flexible, so I would generally recommend @Joe's method as a better solution.

Here's the relevant code:

I threw all of the code into the appDelegate to keep it simple. I don't recommend that in real life.

AppDelegate.m:

@implementation AppDelegate

@synthesize window = _window;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    self.window.backgroundColor = [UIColor whiteColor];

    UIButton *toggleDeleteButtons = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [toggleDeleteButtons setFrame:CGRectMake(20, 45, 280, 45)];
    [toggleDeleteButtons setTitle:@"Toggle Delete" forState:UIControlStateNormal];
    [toggleDeleteButtons addTarget:self action:@selector(toggleDeleteButtonAction) forControlEvents:UIControlEventTouchUpInside];
    [[self window] addSubview:toggleDeleteButtons];

    ButtonWrapper *myButtonWrapper = [[ButtonWrapper alloc] init];
    [[myButtonWrapper button] setFrame:CGRectMake(20, 100, 200, 45)];
    [[myButtonWrapper button] setTitle:@"This is my button" forState:UIControlStateNormal];
    [[myButtonWrapper deleteButton] addTarget:self action:@selector(buttonDeleteRequested:) forControlEvents:UIControlEventTouchUpInside];
    [[myButtonWrapper deleteButton] setTag:0];
    [[self window] addSubview:[myButtonWrapper button]];
    buttonWrapper1 = myButtonWrapper;

    // Added instance called anotherButtonWrapper with tag 1, as above

    // Added instance called stillAnotherButtonWrapper with tag 2, as above

    [self.window makeKeyAndVisible];
    return YES;
}

- (void)toggleDeleteButtonAction {
    static BOOL deleteButtonsShown;

    [buttonWrapper1 showDeleteButton:!deleteButtonsShown];
    [buttonWrapper2 showDeleteButton:!deleteButtonsShown];
    [buttonWrapper3 showDeleteButton:!deleteButtonsShown];
    deleteButtonsShown = !deleteButtonsShown;
}

- (void)buttonDeleteRequested:(UIButton *)deleteButton {
    // delete the specified button here
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Delete" message:[NSString stringWithFormat:@"Delete was pressed on button %i",[deleteButton tag]]delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alert show];
}

ButtonWrapper.m:

@implementation ButtonWrapper

@synthesize button;
@synthesize deleteButton;

- (ButtonWrapper *)init {
    ButtonWrapper *newWrapper = [ButtonWrapper alloc];

    UIButton *myButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [myButton setFrame:CGRectZero];

    UIButton *myDeleteButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [myDeleteButton setFrame:CGRectMake(0, 0, 100, 40)];
    [myDeleteButton setTitle:@"Delete" forState:UIControlStateNormal];
    [myDeleteButton setHidden:TRUE];
    [myButton addSubview:myDeleteButton];

    [newWrapper setButton:myButton];
    [newWrapper setDeleteButton:myDeleteButton];

    return newWrapper;
}

- (void)showDeleteButton:(BOOL)showButton {
    if (showButton) {
        [[self deleteButton] setHidden:FALSE];
        [[self deleteButton] setEnabled:TRUE];    }
    else {
        [[self deleteButton] setHidden:TRUE];
        [[self deleteButton] setEnabled:FALSE];
    }
}
@end

This solution did not require me to implement all of the UI properties, but it did require extra work to hook up the embedded delegates, which is cumbersome. There may be a way to pass the delegates into the wrapper at initialization, but I couldn't make it work.

strings42
  • 543
  • 3
  • 9
  • 1
    UIButton is part of a class cluster so Associative References makes more sense. – Joe Apr 17 '12 at 15:30
  • +1 @Joe. I tend to subclass UIControl instead of UIButton for this reason, since the basic buttons are so fugly anyway you don't lose much and there isn't a lot to add to get button functionality back. – jrturton Apr 17 '12 at 15:33
  • I've read that in general subclassing UIButton is a bad idea, and requires a lot of code to replicate basic behaviour. That's why I tried Associative References, even though I'm not exactly sure what I'm doing there... But I could go back to subclassing UIButton if I can't figure it out. – Smikey Apr 17 '12 at 15:36
  • @Joe, can you expand on your comment? I've not had issues subclassing controls before, at least as far as adding properties goes. Then again, I didn't know about associative references until recently, so I'm curious to understand more about when it's appropriate to use them. What's the advantage in this case (or maybe I should ask, what's the disadvantage to having UIButton having a UIButton as a traditional property)? Lest you misunderstand, I'm not arguing with you at all, quite to the contrary, just trying to learn. – strings42 Apr 17 '12 at 16:13
  • @strings42 When you subclass a `UIButton` and attempt to create it any other way than `alloc/init` then you will not have access to any of the associated properties. e.g. `[UIButton buttonWithType:UIButtonTypeInfoDark]` will not return an instance of your subclassed class so you would not be able to use it. That's why you need to use a category with associative references. I would recommend that you try and create a new project and subclass `UIButton` then try to use it with `[UIButton buttonWithType:]` to see for yourself. – Joe Apr 17 '12 at 17:05
  • @Joe I see what you're talking about now. I thought I had subclassed a UI control straight up before, but I realized when I dug through the code that I was really just wrapping it, not subclassing it. I've whacked together a wrapper class that includes a "master" button and a delete button, but it's kind of a whack. I'm going to edit my answer to include that code, but I honestly think your way is better and I'll include that statement with the code. – strings42 Apr 17 '12 at 21:47
  • Very helpful discussion, thanks guys. @strings42, your approach looks to be what I was aiming for. FYI, I found a great custom implementation written by Heiko Maass which does exactly this, amongst many other spring board functions: https://github.com/heikomaass/HMLauncherView It's a different approach entirely. – Smikey Apr 19 '12 at 09:21