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:
- Subviews are properties and are initialized lazily;
- Lazy initialization creates subviews with specific configuration;
- 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;
- Positioning is done in -layoutSubviews
- Custom views = custom and more contextual delegate protocols
- 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];
}