Undefined Title

Undefined Title

あるUILabelのサブクラスをSwiftで書きなおした

iPhone6の発表イベントが9/9に決まって、iOS8 + Xcode6も同発表と共に解禁になるはず。 Swiftで書いてもリリースできなかったんでイマイチやる気起きなかったけど、 ようやくぼちぼちSwiftやる気が出てきた。ということで、 試しにひとつ既存の簡単なObjective-CクラスをSwiftで書きなおしてみた。

もともとのクラスはこんな感じ。

  • UILabelのサブクラス。
  • 数字を表示するためのカスタムビュー。バッジ的な表現用。
  • (id)initがひとつ、(void)layoutToViewでセレクタが2種類の3つのメソッド。
  • CocoaPodsで入れたFrameAccessorを利用。

コードはこう。座標指定が思いっきりリテラルなのは...まあ見なかったことにしてくれ。

//  BadgeView.m
#import "BadgeView.h"
#import "Constants.h"
#import <FrameAccessor.h>

@implementation BadgeView

- (id)init
{
    self = [super init];

    UILabel *badge = self;
    badge.font = [badge.font fontWithSize:kSubFontPointSize];
    badge.textColor = UIColor.whiteColor;
    badge.textAlignment = NSTextAlignmentCenter;
    badge.layer.backgroundColor = [UIColor.mainTintColor colorWithAlphaComponent:0.77].CGColor;
    badge.layer.cornerRadius = 3;

    return self;
}

- (void)layoutToView:(UIView *)parent withValue:(NSString *)value
{
    UILabel *badge = self;
    badge.text = value;
    [badge sizeToFit];
    badge.width = MIN(22, badge.width + 8 + 8);
    badge.x = 11.0;
    badge.y = -18 + (9.25) + 4;
}

- (void)layoutToView:(UIView *)parent withUInteger:(NSUInteger)value
{
    NSString *val = [NSString stringWithFormat:@"%lu", value];
    [self layoutToView:parent withValue:val];
}

@end

それが、こうなった。

//  BadgeView.swift
class BadgeView : UILabel {

    required init(coder: NSCoder) {
        fatalError("NSCoding not supported")
    }

    override init(frame: CGRect) {
        super.init(frame:frame)
    }

    override init() {
        super.init()

        var badge = self;
        badge.font = badge.font.fontWithSize(kSubFontPointSize)
        badge.textColor = UIColor.whiteColor()
        badge.textAlignment = NSTextAlignment.Center
        badge.layer.backgroundColor = UIColor.mainTintColor().colorWithAlphaComponent(0.77).CGColor
        badge.layer.cornerRadius = 3
    }

    func layoutToView(parent: UIView, withValue text: String) {
        var badge = self
        badge.text = text
        badge.sizeToFit()
        badge.width = min(22, badge.width + 8 + 8)
        badge.x = 11.0;
        badge.y = -18 + (9.25) + 4;
    }

    func layoutToView(parent: UIView, withUInteger val: Int) {
        var v = NSString(format:"%lu", val);
        layoutToView(parent, withValue:v);
    }
}

特徴的なところをひとつひとつ見てみる。

ちなみに以下の説明で、Objective-Cとかの用語間違ってるかもしれないので、そこはひとつおおらかな気持ちで見つけたらやさしく教えて欲しい。

import

#import "BadgeView.h"
#import "Constants.h"
#import <FrameAccessor.h>

BadgeView.hヘッダファイルはSwiftなのでもちろん必要なし。 "Constants.h"<FrameAccessor.h>はObjective-C Swiftブリッジのヘッダ {プロジェクト名}-Bridging-Header.hへ移動。

ということで、importはSwiftからはなくなった。

Required & Designated initializers

指定イニシャライザとかそういうやつ。 もとのObjective-Cのコードにはないコードの追加が必要だった。

required init(coder: NSCoder) {
    fatalError("NSCoding not supported")
}
// - (id)initWithCoder:(NSCoder *)aDecoder; に対応している
.../BadgeView.swift:8:7: Class 'BadgeView' does not implement its superclass's required members

定義しないと上のコンパイルエラーが発生した。

override init(frame: CGRect) {
    super.init(frame:frame)
}
// - (instancetype)initWithFrame:(CGRect)frame; に対応している
.../BadgeView.swift: 8: 7: fatal error: use of unimplemented initializer 'init(frame:)' for class '{プロジェクト名}.BadgeView'

定義しないと上のランタイムエラーが発生した。

Method definition, invokation & static field

- (void)layoutToView:(UIView *)parent withValue:(NSString *)value
- (void)layoutToView:(UIView *)parent withUInteger:(NSUInteger)value
func layoutToView(parent: UIView, withValue text: String)
func layoutToView(parent: UIView, withUInteger val: Int)

Swiftでもセレクタ的な表現にする。 textとかvalを付けない場合はwithValuewithUIIntegerがメソッド内でのパラメータ名になる。 Objective-Cのコードから呼ばれてもいいような名前をつけておいたほうが無難。

layoutToView(parent, withValue:v);

ちなみに呼び出しはこう。ひとつめのキーワードは書かずに、ふたつめから書く。

badge.font = [badge.font fontWithSize:kSubFontPointSize];
badge.font = badge.font.fontWithSize(kSubFontPointSize)

この辺は分かりやすい。.fontはプロパティ。

badge.textColor = UIColor.whiteColor;
badge.textColor = UIColor.whiteColor()

Objective-Cでのクラス定数は、Swiftではクラスメソッド呼び出しになる。

badge.layer.backgroundColor = [UIColor.mainTintColor colorWithAlphaComponent:0.77].CGColor;
badge.layer.backgroundColor = UIColor.mainTintColor().colorWithAlphaComponent(0.77).CGColor

カテゴリで定義したクラスメソッドmainTintColorも素直に呼べた。

@interface UIColor (TextColor)
+ (UIColor *)mainTintColor;
...

もともとの定義はこんな感じ。

Constant, enum & etc

extern CGFloat const kSubFontPointSize;
badge.font = badge.font.fontWithSize(kSubFontPointSize)

Objective-Cで定義した定数は、上記のように普通に参照できた。

badge.textAlignment = NSTextAlignmentCenter;
badge.textAlignment = NSTextAlignment.Center

Cocoaフレームワークの定数は大半がenumで定義してあるようだ。

badge.textAlignment = .Center

右辺の型名は省略可能。ただXcode6+beta5上ではドットをタイプして補完(Ctrl + space)しても補完候補は出てこなかった。 Xcode6 GM版では表示してくれるといいな。

また、Objective-CのMINは、Swiftではminという関数が定義されていた。

func min<T : Comparable>(x: T, y: T) -> T

定義はこう。

@property

プロパティは変更なしでそのまま使える。

badge.x = 11.0;
badge.x = 11.0
@property (nonatomic) CGFloat x;

ちなみにこのxはFrameAccessorで定義されているプロパティ。 Objective-Cで書かれたFrameworkもほぼ透過的に使える。

雑感

  • .h, .mじゃなくて.swiftひとつで書けるのやっぱり嬉しい。
  • UIViewのサブクラスは指定イニシャライザの定義を避けられないのかな?ちょっとめんどくさい。
  • やっぱりだけど、全部Swiftで書き直したくなってくる。