5
\$\begingroup\$

Generally, UI code is painful and full of kludges (at least, mine was like that). Recently I have decided to put an end to it and try to learn how to write good and reusable UI code.

So here is the pattern (in a general sense of word) I came up with when subclassing UIViews:

  1. Subviews are properties and are initialized lazily;
  2. Lazy initialization creates subviews with specific configuration;
  3. Some of the parameters – especially platform-specific or those regarding the appearance – are extracted to class methods that are easily overridable in case it is needed;
  4. Positioning is done in -layoutSubviews
  5. Custom views = custom and more contextual delegate protocols
  6. To display app-specific models, it is possible to use a category on the view providing a method to bind model fields and view fields together (to avoid cluttering the controller.)

So far it has proven itself to be rather effective and producing compact code. Moreover, UI code is more segmented, each part doing its job and nothing more.

I would like to know whether it is done right and whether this pattern can be followed. Also it would be great to know if there are any possible pitfalls with it I don't see.

IMInputField.h

#import <UIKit/UIKit.h>

//----------------------------------------------------
#pragma mark - Delegate Protocol
//----------------------------------------------------

@class IMInputField;

@protocol IMInputFieldDelegate <NSObject>

- (void)inputFieldChangedText:(IMInputField *)field;

- (void)inputFieldBecameFirstResponder:(IMInputField *)field;

- (void)inputFieldResignedFirstResponder:(IMInputField *)field;

@end

//----------------------------------------------------
#pragma mark - Class
//----------------------------------------------------

@interface IMInputField : UIView

@property (nonatomic) NSString *text;
@property (nonatomic) UIView *inputAccessory;

@property (nonatomic, assign) BOOL exitWithReturn;

@property (nonatomic, weak) IBOutlet id<IMInputFieldDelegate> delegate;

@end

IMInputField.m

#import "IMInputField.h"

@interface IMInputField () <UITextViewDelegate>

@property (nonatomic, strong) UIImageView *backgroundImageView;
@property (nonatomic, strong) UITextView *textView;

@end

@implementation IMInputField

@dynamic inputAccessory, text;

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupAppearance];
    }
    return self;
}

- (void)awakeFromNib
{
    [self setupAppearance];
}

- (void)setupAppearance
{
    self.backgroundColor = [UIColor clearColor];
}

//----------------------------------------------------
#pragma mark - Elements
//----------------------------------------------------

+ (UIImage *)backgroundImage
{
    UIImage *ret = [[UIImage alloc] init];

    if ([UIDevice im_is_iOS7]) {
        ret = [UIImage stretchableImageNamed:@"textfield_background.png"];
    }
    else /* iOS 6 and older */ {
        ret = [UIImage stretchableImageNamed:@"text_field.png"];
    }

    return ret;
}

+ (UIFont *)defaultFont
{
    return [UIFont systemFontOfSize:17];
}

+ (UIEdgeInsets)textInsets
{
    UIEdgeInsets ret = UIEdgeInsetsZero;

    if ([UIDevice im_appPlatformIsIPad]) {
        ret = UIEdgeInsetsMake(4, 6, -4, -6);
    }
    else /* iPhone */ {
        ret = UIEdgeInsetsMake(0, 3, 0, -3);
    }

    return ret;
}

//----------------------------------------------------
#pragma mark - Structural
//----------------------------------------------------

- (UIImageView *)backgroundImageView
{
    if (_backgroundImageView == nil) {
        _backgroundImageView = [[UIImageView alloc] initWithImage:[self.class backgroundImage]];
        _backgroundImageView.backgroundColor = [UIColor clearColor];

        [self addSubview:_backgroundImageView];
        [self sendSubviewToBack:_backgroundImageView];
    }
    return _backgroundImageView;
}

- (UITextView *)textView
{
    if (_textView == nil) {
        _textView = [[UITextView alloc] initWithFrame:CGRectZero];
        _textView.delegate = self;
        _textView.autocorrectionType = UITextAutocorrectionTypeNo;
        _textView.font = [self.class defaultFont];
        _textView.backgroundColor = [UIColor clearColor];
        _textView.contentInset = [self.class textInsets];

        [self addSubview:_textView];
        [self bringSubviewToFront:_textView];
    }
    return _textView;
}

//----------------------------------------------------
#pragma mark - Layout
//----------------------------------------------------

- (void)layoutSubviews
{
    [super layoutSubviews];

    self.backgroundImageView.frame = self.bounds;
    self.textView.frame = self.bounds;
}

//----------------------------------------------------
#pragma mark - Delegate Communication
//----------------------------------------------------

- (void)reportTextChanged
{
    if ([_delegate respondsToSelector:@selector(inputFieldChangedText:)]) {
        [_delegate inputFieldChangedText:self];
    }
}

- (void)reportResignedFirstResponder
{
    if ([_delegate respondsToSelector:@selector(inputFieldResignedFirstResponder:)]) {
        [_delegate inputFieldResignedFirstResponder:self];
    }
}

- (void)reportBecameFirstResponder
{
    if ([_delegate respondsToSelector:@selector(inputFieldBecameFirstResponder:)]) {
        [_delegate inputFieldBecameFirstResponder:self];
    }
}

//----------------------------------------------------
#pragma mark - UITextViewDelegate
//----------------------------------------------------

- (BOOL)textView:(UITextView *)textView 
shouldChangeTextInRange:(NSRange)range 
 replacementText:(NSString *)text
{
    BOOL ret = YES;

    if (self.exitWithReturn && [text isEqualToString:@"\n"]) {
        ret = NO;
        [textView resignFirstResponder];
    }

    return ret;
}

- (void)textViewDidChange:(UITextView *)textView
{
    [self reportTextChanged];
}

- (void)textViewDidBeginEditing:(UITextView *)textView
{
    [self reportBecameFirstResponder];
}

- (void)textViewDidEndEditing:(UITextView *)textView
{
    [self reportResignedFirstResponder];
}

//----------------------------------------------------
#pragma mark - Properties
//----------------------------------------------------

- (NSString *)text
{
    return self.textView.text;
}

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

- (UIView *)inputAccessory
{
    return self.textView.inputAccessoryView;
}

- (void)setInputAccessory:(UIView *)inputAccessory
{
    [self.textView setInputAccessoryView:inputAccessory];
}
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

The first thing that stands out to me immediately is the lack of @required/@optional in the @protocol.

This is mostly a readability thing, but you should certainly mark each of your methods to make your intent explicit. Once you've done this, internally, you can skip the respondsToSelector: check for any methods marked as @required.


I'm not familiar with these:

  • im_is_iOS7
  • im_appPlatformIsIPad

I mean, I can guess at what they do. Are they part of UIDevice and I just don't know about them? If so, ignore this. But if not...

They're not defined in either of your files, and I see no #imports. If you want this code to truly be reusable, everything needs to be self-contained. You can create a UIDevice class category in this same .m if you only need to add a couple methods to it.


The three methods in your section labeled Elements could be improved using a model something like this:

+ (UIImage *)backgroundImage {
    static UIImage *ret = nil;

    if (!ret) {
        if ([UIDevice im_is_iOS7]) {
            ret = [UIImage stretchableImageNamed:@"textfield_background.png"];
        } else /* iOS 6 and older */ {
            ret = [UIImage stretchableImageNamed:@"text_field.png"];
        }
    }

    return ret;
}

This pattern can be applied to all three methods. There's no need to go through the process of creating or reassigning these values each time the method is called. Background image is only different depending on iOS version, which won't change while your app is running. And textInserts is only different depending on whether or not the device is an iPad... and that will never change. And default font never changes.

There's not a big difference here, but using this pattern, you'll run ever so slightly faster, and potentially be using a bit less memory.


[self addSubview:_textView];
[self bringSubviewToFront:_textView];

I'm quite sure the bringSubviewToFront is redundant here. addSubview: adds the view at the front. The only place I can imagine bringSubviewToFront is perhaps if you added an else to match if (_textView == nil). If _textView isn't nil, then we can at least make sure it's the front-most view.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.