[Objective-C] アイコン付きラベルのカスタムラベルを作る



ちょっとしたアイコンの横にテキストが並ぶ、というのはよくあるUIパーツだと思います。
が、それをさくっと実装する方法がなく、アイコンとラベルを個別に用意して実装、というふうにしていました。

でもそれだと管理がめんどくさいし、AutoLayoutを使っても無駄に記述増えるしでめんどくさかったので、色々調べつつカスタムラベルを作ってみました。
コード量はそんなに多くないので全部載せちゃいます。

実行するとこんな感じ↓
![スクリーンショット](https://qiita-image-store.s3.amazonaws.com/0/5616/0c769864-e5f2-99af-6400-11740fc8ec2b.png "スクリーンショット")



```objc:IconLabel.h
@import UIKit;

@interface IconLabel : UILabel

@property (nonatomic, strong) UIImage *icon;
@property (nonatomic, strong) UIImageView *iconView;

@property (nonatomic, assign) CGFloat iconPadding;


/**
 *  生成メソッド
 *
 *  @param icon アイコンイメージ
 *
 *  @return インスタンス
 */
+ (instancetype)createWithIconImage:(UIImage *)icon;

@end
```



## 実装


```objc:IconLabel.m
#import "IconLabel.h"

@interface IconLabel ()

@end


@implementation IconLabel

@synthesize iconPadding = _iconPadding;
@synthesize icon        = _icon;

/**
 *  生成メソッド
 */
+ (instancetype)createWithIconImage:(UIImage *)icon
{
    return [[self.class alloc] initWithIconImage:icon];
}

- (instancetype)initWithIconImage:(UIImage *)icon
{
    self = [super init];
    if (self) {
        self.icon        = icon;
        self.iconPadding = 0;
        [self setupViews];
    }
    return self;
}


/**
 *  ビューのセットアップ
 */
- (void)setupViews
{
    self.iconView = [[UIImageView alloc] initWithImage:self.icon];
    [self addSubview:self.iconView];
}


/////////////////////////////////////////////////////////////////////////////
#pragma mark - Override methods

/**
 *  レイアウトサブビュー
 */
- (void)layoutSubviews
{
    CGRect frame = CGRectZero;
    frame.size     = self.iconView.bounds.size;
    frame.origin.x = 0;
    frame.origin.y = self.bounds.size.height / 2 - self.iconView.bounds.size.height / 2;
    self.iconView.frame = frame;
    [super layoutSubviews];
}


/**
 *  テキストの描画
 */
- (void)drawTextInRect:(CGRect)rect
{
    CGFloat padding = self.icon.size.width + self.iconPadding;
    rect.size.width += padding;
    rect.origin.x   += padding;
    [super drawTextInRect:rect];
}


/**
 *  sizeThatFits
 *  @override
 *
 *  @param size サイズ
 *
 *  @return アイコンを加味したサイズ
 */
- (CGSize)sizeThatFits:(CGSize)size
{
    CGSize returnSize = [super sizeThatFits:size];
    returnSize.width += self.icon.size.width + self.iconPadding;
    returnSize.height = MAX(self.icon.size.height, returnSize.height);
    return returnSize;
}


/**
 *  Intrinsicサイズ
 *  （AutoLayout利用時に呼ばれる）
 *
 *  @return アイコンを加味したサイズ
 */
- (CGSize)intrinsicContentSize
{
    CGSize returnSize = [super intrinsicContentSize];
    returnSize.width += self.icon.size.width + self.iconPadding;
    returnSize.height = MAX(self.icon.size.height, returnSize.height);
    return returnSize;
}


/////////////////////////////////////////////////////////////////////////////
#pragma mark - Dynamic properties

/**
 *  アイコン
 */
- (void)setIcon:(UIImage *)icon
{
    _icon = icon;
    self.iconView.image = icon;
    [self setNeedsDisplay];
}
- (UIImage *)icon
{
    return _icon;
}


/**
 *  アイコンのパディングを設定
 */
- (void)setIconPadding:(CGFloat)iconPadding
{
    _iconPadding = iconPadding;
    [self setNeedsDisplay];
}
- (CGFloat)iconPadding
{
    return _iconPadding;
}

@end
```


## overrideメソッド

[追記]

大事な点は以下の<del>3つ</del> 4つのオーバーライドしたメソッド。
（AutoLayoutを考慮するともうひとつオーバーライドするべきメソッドがありました）


### drawTextInRect:

このメソッドは与えられた`rect`（ラベルの`bounds`）の中のどこにテキストを描くかを決めるもの。
なので、これを適切にずらしてあげれば、今回のサンプルのようにアイコンの分だけ位置をずらしてテキストを描画できます。

*（`bounds`からはみ出したら単純にclipされます）*

```objc:override-drawTextInRect
/**
 *  テキストの描画
 */
- (void)drawTextInRect:(CGRect)rect
{
    CGFloat padding = self.icon.size.width + self.iconPadding;
    rect.size.width += padding;
    rect.origin.x   += padding;
    [super drawTextInRect:rect];
}
```


### sizeThatFits:

続いてオーバーライドする必要があるのがこれ。
`sizeToFit`を実行する際などにも利用されます。（逆に`sizeToFit`は Should not overrideとドキュメントにある）

このメソッドは`UIView`で定義され、サブクラス側で適切にオーバーライドすることで、そのサブクラスのビューがFitするためにどんなサイズが必要か、を定義することができます。
（逆にこれをオーバーライドしないとまったく意味のないメソッドになる）

なので今回は、`UILabel`が実装している`sizeThatFits:`が返すサイズに、さらにアイコン分のサイズを足して返しています。

```objc:override-sizeThatFits
/**
 *  sizeThatFits
 *  @override
 *
 *  @param size サイズ
 *
 *  @return アイコンを加味したサイズ
 */
- (CGSize)sizeThatFits:(CGSize)size
{
    CGSize returnSize = [super sizeThatFits:size];
    returnSize.width += self.icon.size.width + self.iconPadding;
    returnSize.height = MAX(self.icon.size.height, returnSize.height);
    return returnSize;
}
```


### layoutSubviews

最後にオーバーライドしたのが`layoutSubviews`。
やっていることは、指定されたアイコンをラベルの中央に整列させる、というもの。
このへんは実際に使いたいケースに合わせて調整するといいと思います。

```objc:override-layoutSubviews
/**
 *  レイアウトサブビュー
 */
- (void)layoutSubviews
{
    CGRect frame = CGRectZero;
    frame.size     = self.iconView.bounds.size;
    frame.origin.x = 0;
    frame.origin.y = self.bounds.size.height / 2 - self.iconView.bounds.size.height / 2;
    self.iconView.frame = frame;
    [super layoutSubviews];
}
```



### intrinsicContentSize

AutoLayoutを使うと、`sizeThatFits:`をオーバーライドしても値が利用されません。
その代わりに使われるのがこのメソッドです。
基本的には`sizeThatFits:`と似たものなので、アイコン分のサイズを足して返してあげれば大丈夫です。

（ちなみに「intrinsic」は **本質的な** という意味の英単語）

```objc:override-intrinsicContentSize
/**
 *  Intrinsicサイズ
 *  （AutoLayout利用時に呼ばれる）
 *
 *  @return アイコンを加味したサイズ
 */
- (CGSize)intrinsicContentSize
{
    CGSize returnSize = [super intrinsicContentSize];
    returnSize.width += self.icon.size.width + self.iconPadding;
    returnSize.height = MAX(self.icon.size.height, returnSize.height);
    return returnSize;
}
```