Advertise here




Advertise here

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

A Model (Object) Is A Beautiful Thing

BrianSlickBrianSlick Treadmill Desk NinjaPosts: 10,689Tutorial Authors, Registered Users @ @ @ @ @ @ @ @
edited September 2013 in iPhone SDK Tutorials
In object-oriented programming, a common pattern is MVC: Model-View-Controller. For some reason, the Model typically seems to be the slowest concept to grasp for new developers. Views are easy to understand, probably because they are visual. Controllers are maybe not 100% understood, but at least there are plenty of examples so people plod through. Generally speaking, however, the model is often the most important component of an app, and the number of ways to handle the model badly are considerable. This is odd, because the good ways really are not that hard.

Of Thee I String

Most developers begin their programming lives with strings. Strings are easy and readable. And then for more complicated structures, they move onto arrays. Arrays are still relatively easy to understand, and aren't too hard to visualize. They are also easy to teach, which is why books and tutorials tend to begin here. And that's fine, we all need to start somewhere. Let's look at a simple list of colors:
NSArray *colorList = [NSArray arrayWithObjects:@"Blue", @"Green", @"Red", nil];
Straightforward, easy to read, a good starting point. If we were discussing table views, then it would be simple to take this array and use it to drive a table view. That's more than sufficient for static display, and helps to establish concepts. Another example:
NSArray *actors = [NSArray arrayWithObjects:@"Brad Pitt", @"William H. Macy", @"Jennifer Aniston", nil];
Still just strings, still quite easy to read. But now we've started to complicate things a bit, and certain downstream tasks get harder. Let's say I wanted to sort this list by last name. That's going to be a challenge, since each full name is a single string. I would need a way to identify the last name within the string. I could potentially carve each string into pieces, and take the second piece. But that would give me "H." for Mr. Macy. Ok, so I need to take the last piece instead. That should work as long as I haven't inadvertently entered "Pitt, Brad" somewhere.

Ideally we need to split this data up in a known fashion, probably at the time of creation. Relying on algorithms to extract information can be risky if the algorithm is wrong, so it is better to simply define things from the get go. In this case, that means keeping the first name separate from the last name. The question then becomes how to do that.

Don't Be A Dictionary

Level 2 in the programming learning curve leads to dictionaries. At first glance, dictionaries are great. They can contain anything, there is no hard structure, and in basic cases they can save data directly to plists without much trouble. I'll use a mutable dictionary here since the code is easier to understand, but a sample actor represented in a dictionary might look like this:
NSMutableDictionary *actor = [NSMutableDictionary dictionary];
[actor setObject:@"Brad" forKey:@"firstName"];
[actor setObject:@"" forKey:@"middleName"];
[actor setObject:@"Pitt" forKey:@"lastName"];
The string has been broken up into pieces, and can now be located via an identifier, the key. Retrieving a value is relatively simple:
NSString *actorFirstName = [actor objectForKey:@"firstName"];

Now instead of putting strings into an array, add dictionaries instead:
NSArray *actors = [NSArray arrayWithObjects:actor1, actor2, actor3, nil];
There are ways of sorting arrays using these keys, so it would seem that we've solved our problem. For now, at any rate.

The problems with using dictionaries take a while to reveal themselves. Let's say that you set aside this project for a couple of months to work on something else, and then come back to it in order to make changes. You find yourself looking at this line of code:
NSDictionary *actor = [actors objectAtIndex:2];
Ok, so you're referencing a single dictionary in an array, and you're calling it an actor. Great. Now then, what should "actor" have in it? Hrm, well, I sort of remember caring about the names, so probably names. Maybe their birthdays. Oh, it would be nice if there was a list of their movies too. I probably thought that was a good idea when I made this, so I'll just assume this data is there. Wonder what I called it. Probably "birthday". Great, let's see what happens:
NSString *birthday = [actor objectForKey:@"birthday"];
NSLog(@"birthday is: %@", birthday);
…
// Log output: birthday is: (null)
Uh oh. It's not there. I must have called it something else. And come to think of it, would it really be an NSString? It's a date, and we have NSDate objects. Might have been one of those. How can I find out? Well, I could log the entire dictionary:
NSLog(@"actor is: %@", [actor description])
Hrm, no birthday. Is that because I didn't have information for this actor, or is that because none of the actors have birthdays? Not sure. And there isn't a handy way to find out, either. All I can do is figure out where I first created these dictionaries, and see what is there.

The point that I'm making here in a roundabout way is that dictionaries do not document themselves. Every NSDictionary looks the same from the outside. It is the contents that matter, and short of manual interrogation, there is no easy way to find out what the contents of a given dictionary are or should be. If you wanted to be diligent, you could put something in the comments somewhere like this:
// I hereby define an "actor" dictionary as containing the following data:
// First name shall be an NSString stored with the key "firstName"
// Middle name shall be an NSString stored with the key "middleName"
// Last name shall be an NSString stored with the key "lastName"
You could, but no one takes the time to do this. And because dictionaries are so handy and flexible, you can add and remove keys all day long, and totally forget to update this list, at which point the list just becomes another point of confusion, rather than an aid. Without this list, you do not have an easy way to figure out what this dictionary should contain. You will have to waste time researching your own code and doing logging just to figure out what this should be.

But wait, there's more. Let's say a common activity in our actor program is to display the full name of the actor. That might look like this:
NSString *firstName = [actor objectForKey:@"firstName"];
NSString *lastName = [actor objectForKey:@"lastName"];
NSString *fullName = [NSString stringWithFormat:@"%@ %@", firstName, lastName];
Blech. That's a lot of code, copy-pasted all over the place. And I forgot the middle initial. And sometimes it outputs "Bob (null)". So I need to make it more complicated, add some error handling, and then re-copy-paste it all over the place. Ah ha! I know a little about OOP, I'll just make a helper method somewhere. The only thing I know how to grab from anywhere is the app delegate, so I'll put it there!
- (NSString *)fullNameFromActor:(NSDictionary *)actor;
Brilliant! Well, not so much. I mean, this will of course work. But look how far separated it is from your data. All of these actors are defined way over in some view controller (or better yet, a data controller) somewhere, but this method is here in the app delegate. Any time that you have one of these dictionaries in your hand, you have to go all the way over to the app delegate to get this method. More advanced programmers might be tempted to move this code into a Category, and that's certainly an option, but a bit beyond the scope of this article and not a good solution for a case like this anyway.

The ideal solution to this problem would include some handy form of documentation that can be easily referenced later, and would also provide a convenient place to store helper methods. And that ideal solution really isn't that hard to do.

Keep reading for Part 2...
Post edited by BrianSlick on
Professional iOS App Development. Available for hire.
BriTer Ideas LLC - WWW | Facebook | Twitter | LinkedIn

BTIKit | BTICoreDataKit | SlickShopper 2 | Leave a PayPal donation

Replies

  • BrianSlickBrianSlick Treadmill Desk Ninja Posts: 10,689Tutorial Authors, Registered Users @ @ @ @ @ @ @ @
    edited September 2013
    Subclassy

    Creating a custom model subclass is straightforward. Create a new file, starting with Objective-C class, then choose to subclass NSObject. By convention, class names should begin with a capital letter, so "Actor", not "actor". For bonus points in avoiding name clashes, use some kind of prefix. I use BTI, so I would name it BTIActor. Do NOT use NS. Do NOT use UI. These are Apple's territories. So NSActor and UIActor are wrong. Period. Wrong. Don't do it. No exceptions.

    So we have a new class now:
    // BTIActor.h
    @interface BTIActor : NSObject
    {
    }
    
    @end
    
    …what do we do with it. Well, with the dictionary, we were thinking in terms of keys. Here in a model class, you think in terms of properties instead. This is no different than defining a label property in a view controller. Let's begin:
    // BTIActor.h
    @interface BTIActor : NSObject
    {
    }
    
    @property (nonatomic, copy) NSString *firstName;
    @property (nonatomic, copy) NSString *middleName;
    @property (nonatomic, copy) NSString *lastName;
    @property (nonatomic, copy) NSDate *birthday; 
    
    @end
    
    Immediately we have a much, much better situation than we had with the dictionaries. First of all, there is a list of each element in this model. 3 strings and a date. No keys to remember or mistype, it's all right here. And also notice that we know what everything is. Ah, the birthday is a date object. Nothing to remember, nothing to get wrong. With dictionaries, it is always objectForKey:. What kind of object? I don't remember. Have to go look it up, uh, somewhere. With this class, all I have to do is click on the .h file anytime I want to know something. It's easy for me to find today, and it's easy for me to find in 3 months. It is inherently self-documenting.

    And I'm not even done yet. How about that helper method? Put it here, too:
    // BTIActor.h
    @interface BTIActor : NSObject
    {
    }
    
    @property (nonatomic, copy) NSString *firstName;
    @property (nonatomic, copy) NSString *middleName;
    @property (nonatomic, copy) NSString *lastName;
    @property (nonatomic, copy) NSDate *birthday;
    
    - (NSString *)fullName; 
    
    @end
    
    The data remains broken up so that you can still get to individual elements (last name) if you need to, but you can make as many handy methods as you need all right here on the object that can use them. Want to do last name, first name?
    - (NSString *)reversedFullName;
    
    How old is this actor right now?
    - (NSInteger)currentAge;
    
    On and on and on. If you have a need, make a method for it here, then you can use it wherever you use this model. Granted, the implementations of these methods may not necessarily be any simpler this way, but at least you have everything you need right here.

    The .m file wouldn't be all that different from an average view controller. Synthesize your properties and implement your methods:
    // BTIActor.m
    #import "BTIActor.h"
    
    @implementation BTIActor
    
    @synthesize firstName = _firstName;
    @synthesize lastName = _lastName;
    ...
    - (NSString *)fullName
    {
       return [NSString stringWithFormat:@"%@ %@", [self firstName], [self lastName]];
    } 
    ... 
    @end
    
    There are a variety of other things to do here. There should be a dealloc method (unless using ARC). Might want an init method, too. But chances are, this is all stuff you've done elsewhere before, most likely in a view controller. Same idea here.

    Model objects are easy to create, so there isn't much of a reason to hesitate to use them. Describing a car?
    // BTICar.h
    @interface BTICar : NSObject
    {
    }
    
    @property (nonatomic, copy) NSString *make;
    @property (nonatomic, copy) NSString *model;
    @property (nonatomic, assign) NSInteger *year;
    @property (nonatomic, assign) NSInteger *numberOfDoors;
    @property (nonatomic, assign) CGFloat engineDisplacement; 
    
    @end
    
    Describing a shirt?
    // BTIShirt.h
    @interface BTIShirt : NSObject
    {
    }
    
    @property (nonatomic, copy) NSString *size;  // S, M, L, XL
    @property (nonatomic, retain) UIColor *color;
    @property (nonatomic, assign) BOOL isLongSleeve;
    @property (nonatomic, assign) BOOL hasPoppedCollar; 
    
    @end
    
    If you can figure out how to put this stuff into a dictionary, then you can do the same thing even better with a model class. There really is nothing to be afraid of here. So kick a dictionary to the curb today!

    Work Your Model

    How do you use a model class? Well, the first thing you need to do is add this to the .m file wherever you want to use them:
    #import "MySuperAwesomeModel.h"
    
    Now you can use it. Let's revisit the dictionary version:
    NSMutableDictionary *actor = [NSMutableDictionary dictionary];
    [actor setObject:@"Brad" forKey:@"firstName"];
    [actor setObject:@"" forKey:@"middleName"];
    [actor setObject:@"Pitt" forKey:@"lastName"];
    
    Doing this same thing with the model object:
    BTIActor *actor = [[BTIActor alloc] init];
    [actor setFirstName:@"Brad"];
    [actor setMiddleName:@""];
    [actor setLastName:@"Pitt"];
    
    It's cleaner, easier to read, and as a double-bonus, Xcode will help you type this stuff. Xcode does not help at all when typing @strings. So you are much more likely to make a typo. If you make a typo with dictionary keys, you won't figure it out until much letter. Xcode will not warn you. The key will simply not be what you expect it to be. That's hard to troubleshoot.

    Use them in arrays just like you did with dictionaries:
    [actors addObject:actor];
    ...
    BTIActor *someActor = [actors objectAtIndex:1];
    
    Oh yeah, and don't forget the helper method:
    NSString *fullName = [someActor fullName];
    [[self nameLabel] setText:fullName];
    
    That's more or less all there is to it.

    There are a couple of other aspects to model objects that I will address with a future post, particularly related to saving and loading these objects. If you are in a hurry to do so, do some Googling for "NSCoding".
    Post edited by BrianSlick on
    Professional iOS App Development. Available for hire.
    BriTer Ideas LLC - WWW | Facebook | Twitter | LinkedIn

    BTIKit | BTICoreDataKit | SlickShopper 2 | Leave a PayPal donation
  • BrianSlickBrianSlick Treadmill Desk Ninja Posts: 10,689Tutorial Authors, Registered Users @ @ @ @ @ @ @ @
    edited March 2012
    Another space reserved
    Professional iOS App Development. Available for hire.
    BriTer Ideas LLC - WWW | Facebook | Twitter | LinkedIn

    BTIKit | BTICoreDataKit | SlickShopper 2 | Leave a PayPal donation
  • BrianSlickBrianSlick Treadmill Desk Ninja Posts: 10,689Tutorial Authors, Registered Users @ @ @ @ @ @ @ @
    edited March 2012
    And one more space reserved
    Professional iOS App Development. Available for hire.
    BriTer Ideas LLC - WWW | Facebook | Twitter | LinkedIn

    BTIKit | BTICoreDataKit | SlickShopper 2 | Leave a PayPal donation
  • _Mac_Mac Posts: 148Registered Users
    edited May 2012
    Thank's BrianSlick for this great tutorial!
    I've definitly "kicked Dictionaries & Categories to the curb" after reading this.

    You do also have a wonderful way, not tech dry, to spread your're knowledge with humor and good, easy to understand IRL cases.

    Keep up the good work!

    P.s. Did you write any more tutorials on objects "adding, reading & handle them" as you mentioned last in this tutorial? Would love to read them...
    -- Happy Coding <img src="http://www.iphonedevsdk.com/forum/images/smilies/smile.gif"; border="0" alt="" title="Smile" class="inlineimg" />
  • iSDKiSDK Posts: 1,353Tutorial Authors, Registered Users @ @ @ @
    edited June 2012
    Nice tutorial Brian!

    Just wanted to say that I think it would help if you showed the readers how they can initialise a model without having to set every property manually, via an -init method:
    
    ...
    
    - (id) initWithAttributes:(NSDictionary *) attr;
    
    ...
    
    
    - (id) initWithAttributes:(NSDictionary *) attr {
        
        if ((self = [super init])) {
            
            [self setFirstName:[attr objectForKey:@"firstName"]];
            [self setMiddleName:[attr objectForKey:@"middleName"]];
            [self setLastName:[attr objectForKey:@"lastName"]];
            [self setBirthday:[attr objectForKey:@"birthday"]];
            [self setAge:[[attr objectForKey:@"age"] integerValue]];
            
        }
        
        return self;
    }
    

    Furthermore, instead of allowing the properties to be written as well as being read (in some cases) readonly properties can be used, with an NSDictionary being the only retained object.
    
    @interface MKDeveloperModel : NSObject {
        
        NSDictionary *_attributes;
        
    }
    
    - (id) initWithAttributes:(NSDictionary *) attributes;
    
    @property (nonatomic, readonly) NSURL *websiteURL, *blogURL, *iphoneDevSDKProfileURL;
    
    @property (nonatomic, readonly) NSString *twitterUsername, *facebookUsername, *emailAddress;
    
    @property (nonatomic, readonly) NSArray *appNames;
    
    
    @end
    
    
    
    #import "MKDeveloperModel.h"
    
    @implementation MKDeveloperModel
    @synthesize appNames, blogURL, websiteURL, emailAddress, twitterUsername, facebookUsername, iphoneDevSDKProfileURL;
    
    
    - (id) initWithAttributes:(NSDictionary *)attributes {
        if ((self = [super init])) {
            
            _attributes = [attributes retain];
            
        }
        
        return self;
    }
    
    /*
        
     You can use [NSURL URLWithString:... in replace of [_attributes objectForKey:...]; if the object is an NSString.
     
    */
    
    - (NSURL *) websiteURL {
        return [_attributes objectForKey:@"websiteURL"]; 
    }
    
    
    - (NSURL *) blogURL {
        return [_attributes objectForKey:@"blogURL"];
    }
    
    - (NSURL *) iphoneDevSDKProfileURL {
        return [_attributes objectForKey:@"iphoneDevSDKProfileURL"];
    }
    
    - (NSString *) twitterUsername {
        return [_attributes objectForKey:@"twitterUsername"];
    }
    
    - (NSString *) facebookUsername {
        return [_attributes objectForKey:@"facebookUsername"];
    }
    
    - (NSString *) emailAddress {
        return [_attributes objectForKey:@"emailAddress"];
    }
    
    - (NSArray *) appNames {
        return [_attributes objectForKey:@"appNames"];
    }
    
    - (void) dealloc {
        [_attributes release];
        [super dealloc];
    }
    
    
    @end
    
    

    Usage:
    ...
    
    MKDeveloperModel *model = [[MKDeveloperModel alloc] initWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSURL URLWithString:@"http://maxk.me"], @"websiteURL", [NSURL URLWithString:@"http://blog.maxk.me"], @"blogURL", [NSURL URLWithString:@"http://www.iphonedevsdk.com/forum/members/isdk.html"], @"iphoneDevSDKProfileURL", @"_max_k", @"twitterUsername", @"", @"facebookUsername", @"my_email_addy", @"emailAddress", [NSArray arrayWithObjects:@"iReactions", @"Nucleus", @"Debt Clinic", @"Medvedi", @"SocialPic", @"CityWise", @"LineTime", nil], @"appNames", nil]];
        
    NSLog(@"%@ : %@ : %@ : %@ : %@", model.websiteURL, model.blogURL, model.twitterUsername, model.emailAddress, model.appNames);
    
    ...
    
  • BrianSlickBrianSlick Treadmill Desk Ninja Posts: 10,689Tutorial Authors, Registered Users @ @ @ @ @ @ @ @
    edited June 2012
    I don't like custom initializers in general, and only do them if one (or more) of the properties is required for the object to function properly.

    Also, your example here isn't very useful based on your description. A method like this would be useful for converting the object to/from a dictionary. But from a "save effort while creating the object" standpoint, it really doesn't. You'd have to already have this dictionary in order for the method to save any effort, but then you've moved the effort to creating the dictionary.

    NSCoding is generally a better approach anyway.
    Professional iOS App Development. Available for hire.
    BriTer Ideas LLC - WWW | Facebook | Twitter | LinkedIn

    BTIKit | BTICoreDataKit | SlickShopper 2 | Leave a PayPal donation
  • iSDKiSDK Posts: 1,353Tutorial Authors, Registered Users @ @ @ @
    edited June 2012
    Fair points, I understand that it isn't particularly useful, I just tend to use custom initialisers a lot as I do a bucket-load of HTTP requests returning JSON responses (broken down into NSArrays and NSDictionarys) leaving the custom initialisers as an easier option.

    Please see the update in the above post, I showed another approach to creating models.
  • BrianSlickBrianSlick Treadmill Desk Ninja Posts: 10,689Tutorial Authors, Registered Users @ @ @ @ @ @ @ @
    edited June 2012
    Oh, for web stuff, sure. Though I tend to go more towards a initWithXMLData: naming convention.

    I guess I don't see the point of keeping the dictionary around in that model. You're going through the effort of defining properties, but leaving them as read-only. So effectively, you have a read-only model. I suppose that could make sense in certain circumstances, but in general I don't see what is being gained. For all of the effort you are going through, you could make a custom setter (or just a method, not even a property) for the dictionary that extracts the necessary information and populates the properties. I suppose you might be speeding up the initial object creation, but then every single time you access a property after that, it would be slower because every single getter involves a dictionary lookup. Chances are that you will ping aspects of an object many, many more times than you would create it, so you've slowed down the stuff that is important.

    And then this really pegs my don't-get-the-point-ometer:
    MKDeveloperModel *model = [[MKDeveloperModel alloc] initWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSURL URLWithString:@"http://maxk.me"], @"websiteURL", [NSURL URLWithString:@"http://blog.maxk.me"], @"blogURL", [NSURL URLWithString:@"http://www.iphonedevsdk.com/forum/members/isdk.html"], @"iphoneDevSDKProfileURL", @"_max_k", @"twitterUsername", @"", @"facebookUsername", @"my_email_addy", @"emailAddress", [NSArray arrayWithObjects:@"iReactions", @"Nucleus", @"Debt Clinic", @"Medvedi", @"SocialPic", @"CityWise", @"LineTime", nil], @"appNames", nil]];
    

    The whole point of this article was to do away with this mess. Effectively all that you've done here is change "NSDictionary" to "MKDeveloperModel", which might be useful for debugging, but otherwise you haven't done anything to simplify life.

    (not to mention that you should be #defining constants for your keys)
    Professional iOS App Development. Available for hire.
    BriTer Ideas LLC - WWW | Facebook | Twitter | LinkedIn

    BTIKit | BTICoreDataKit | SlickShopper 2 | Leave a PayPal donation
  • iSDKiSDK Posts: 1,353Tutorial Authors, Registered Users @ @ @ @
    edited June 2012
    BrianSlick wrote: »

    And then this really pegs my don't-get-the-point-ometer:
    MKDeveloperModel *model = [[MKDeveloperModel alloc] initWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSURL URLWithString:@"http://maxk.me"], @"websiteURL", [NSURL URLWithString:@"http://blog.maxk.me"], @"blogURL", [NSURL URLWithString:@"http://www.iphonedevsdk.com/forum/members/isdk.html"], @"iphoneDevSDKProfileURL", @"_max_k", @"twitterUsername", @"", @"facebookUsername", @"my_email_addy", @"emailAddress", [NSArray arrayWithObjects:@"iReactions", @"Nucleus", @"Debt Clinic", @"Medvedi", @"SocialPic", @"CityWise", @"LineTime", nil], @"appNames", nil]];
    

    (not to mention that you should be #defining constants for your keys)

    Thy were just for the example, the attributes dictionary could be defined elsewhere and then passed to the object for creation.

    As for # defining the constants, I didn't think to put that in for the example, got spot though!
  • TanderTander South AfricaPosts: 243New Users @ @
    Excellent post BrianSlick - this has definitely helped me with my current project!

    Thanks!
Sign In or Register to comment.