Interaction Object

As a developer I often find myself in the situation where I have to face the problem to store and get some kind of information in the most possible handy way.
In particular, this happens when you design a static menu list (e.g side panel or harburger menu) using a UITableView.

In this article I will go to explain how to implement it using the concept of Interaction Object in order to keep our code clean and testable.


The problem

In my last two companies I used to work, I was in the situation to create a menu list like so:

Hamburger Menu

As a static menu, it was not populated from any datasource, therefore I thought “what is the best way to represent the menu items?”.
A naïve and rude approach would be something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    switch (indexPath.row) {

        //....get/create a cell etc.

        case 0:
        {
            cell.title = @"Home";
            break;
        }

        case 1:
        {
            cell.title = @"Favourites";
            break;
        }

        //...
    }

    //..

    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    switch (indexPath.row) {

        case 0:
        {
            [self goToHome];
            break;
        }

        case 1:
        {
           [self goToFavourites];
            break;
        }

        //...
    }

    //..
}

……

Shit

Yes, that’s ugly and not scalable.

What if you want to add another cell later? And if you want the new cell to be somewhere in the middle? You’d have to change every case’s number. What if you want to display different cells according with a condition (i.e. user/admin)?

In short, this is not a good approach at all. So… how can we go with this?


The solution

As a menu, any cell needs 3 basic and fundamental things:

  • An image;
  • A title;
  • An action.

The idea is to have a component (MGInteractionObject, precisely) to store these information, and having then an array of these components in order to associate them to the cells. Moreover, this approach will give us the opportunity to manage the array, choosing which items to show and which not to, according to certain events or conditions.

Therefore, the header file would look similar to:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import <Foundation/Foundation.h>

typedef void(^performBlock)();

typedef NS_ENUM(NSUInteger, MGActionType) {
    MGActionTypeBlock        = 1,
    MGActionTypeSelector
};

@interface MGInteractionObject : NSObject

@property (copy, nonatomic, readonly) NSString *title;
@property (copy, nonatomic, readonly) NSString *imageName;

@property (assign, nonatomic, readonly) MGActionType actionType;
@property (assign, nonatomic, readonly) SEL selector;
@property (copy, nonatomic, readonly) performBlock block;

- (instancetype)initWithTitle:(NSString *)title imageName:(NSString *)imageName selector:(SEL)selector;
- (instancetype)initWithTitle:(NSString *)title imageName:(NSString *)imageName performBlock:(performBlock)block;

@end

Basically, as I said, we store a title for the menu item, an image and an action, which will be performed by a block, or by a selector when the user taps on the menu entry. Finally, we have an enumeration in order to present to the developer the different ways to perform the action.
For simplicity, here I reported just some basic information to store, but it could be expanded a lot further, for example with the following properties:

1
2
3
4
5
@property (copy, nonatomic, readonly) NSString *highlightedTitle;
@property (copy, nonatomic, readonly) NSString *highlightedImageName;
@property (strong, nonatomic, readonly) UIColor *backgroundColor;
@property (strong, nonatomic, readonly) UIColor *highlightedBackgroundColor;
//..etc

Now that we the header is defined, it’s the time to have start move on to the implementation:

Now that we know the header, it’s the time to have a look to the implementation code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#import "MGInteractionObject.h"

@implementation MGInteractionObject

#pragma mark - Public init
- (id)init {
    @throw [NSException
        exceptionWithName:NSInternalInconsistencyException
                   reason:@"Must use initWithTitle:imageName:selector: or initWithTitle:imageName:performBlock:"
                 userInfo:nil];
}

- (instancetype)initWithTitle:(NSString *)title imageName:(NSString *)imageName selector:(SEL)selector
{
    return [self initWithTitle:title imageName:imageName selector:selector performBlock:nil];
}

- (instancetype)initWithTitle:(NSString *)title imageName:(NSString *)imageName performBlock:(performBlock)block
{
    return [self initWithTitle:title imageName:imageName selector:nil performBlock:block];
}

#pragma mark - Private init
- (instancetype)initWithTitle:(NSString *)title imageName:(NSString *)imageName selector:(SEL)selector performBlock:(performBlock)block
{
    if (self = [super init]) {
        _title = [title copy];
        _imageName = [imageName copy];
        _selector = selector;
        _block = [block copy];

        _actionType = (selector) ? MGActionTypeSelector : MGActionTypeBlock;
    }
    return self;
}

@end

Simple as that.
Now that we have the base element, let’s proceed to use it properly.

How to use it

What we need to do now, is to create an interaction object for each menu item.
Supposing to have 4 items:

  • Home;
  • Favourites;
  • Stores;
  • Admin options.

we can simply go to create 4 interaction objects like these:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (void)mg_createMenuInteractionItemsForUser:(MGUser *)user
{

    __weak typeof (self) weakSelf = self;

    MGInteractionObject *objHome =
        [[MGInteractionObject alloc] initWithTitle:@"Home"
                                         imageName:@"icon_home"
                                   performSelector:@selector(openHome)];

    MGInteractionObject *objFavourites =
        [[MGInteractionObject alloc] initWithTitle:@"Favourites"
                                         imageName:@"icon_favourites"
                                      performBlock:^{
                                                    //Do something    
                                                    }];

    MGInteractionObject *objStores =
        [[MGInteractionObject alloc] initWithTitle:@"Stores"
                                         imageName:@"icon_stores"
                                   performSelector:@selector(openStores)];


    //Prepare tableButtons
    NSArray *tableButtons;
    if(user.isAdmin) {

        MGInteractionObject *objAdmin =
            [[MGInteractionObject alloc] initWithTitle:@"Admin options"
                                             imageName:@"icon_admin"
                                          performBlock:^{
                                                        //Do something        
                                                        }];

        tableButtons = @[
                        objHome,
                        objFavourites,
                        objStores,
                        objAdmin
                        ];
    } else {
        tableButtons = @[
                        objHome,
                        objFavourites,
                        objStores
                        ];
    }

    return tableButtons;
}

Having these 4 objects, we return an array of MGInteractionObject, according with the conditions we want to consider. In this example, it is checked if the user is an admin (user.isAdmin) to decide if add or not the admin interaction object, in the array tableButtons.

Now all we need to do yet is to reload the tableview to show the content. Here is the implementation of tableView:cellForRowAtIndexPath:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"InteractionCell";

    MGInteractionCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

    if(!cell) {
        cell = [[MGInteractionCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }

    //Get the interaction object
    MGInteractionObject *interactionObject = _tableButtons[indexPath.row];

    //Set the interaction object for this cell to configure it
    cell.interactionObject = interactionObject;

    return cell;
}

Here we are using a custom cell MGInteractionCell which has the property interactionObject, which through the setter method, configures the cell.

Obviously, it could be also used a normal UITableViewCell setting the title and the image directly from this method.

Finally, when a user taps on a cell, the method tableView:didSelectRowAtIndexPath: is called:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    //Get the interaction object
    MGInteractionObject *interactionObject = _tableButtons[indexPath.row];

    //Perform the block or the selector
    switch (interactionObject.interactionType) {
        case MGActionTypeBlock:
            interactionObject.block();
            break;
        case MGActionTypeSelector: {
            if ([self respondsToSelector:interactionObject.selector]) {
                [self performSelector:interactionObject.selector withObject:nil];
            }
            break;
        }
    }
}

Here we check the interaction type and make a call to the block or perform the selector.

And..

..that’s it!

Happy

Follow me on Twitter!

Enjoy! ;)


Comments