読者です 読者をやめる 読者になる 読者になる

Timers Tech Blog

カップル専用アプリ「Pairy」, 子育て夫婦アプリ「Famm」を運営している Timers inc. の公式Tech Blogです。

Objective-C→Swiftでどう書き換えたらいいの?をまとめてみた

初めまして、iOSエンジニアのすーです!4月からTimers Inc.で働き始めました。
社内では最年少です。笑

現在Fammでは、新規開発を行いつつ、Objective-Cで書かれたコードを少しずつSwift化するのを行っています。
既に、新規でコードを書く時はSwiftで書くように心がけ、暇がある時に慎重にObjective-Cで書いたコードを書き換えています。

そんなときに、「Objective-Cでこう書いてあったのをSwiftで書く時どうしたらいいの?」っていうケースに幾つか遭遇し、それを社内wikiにまとめたら、
「これTech blogに投稿できるんじゃね?」ってなったので、改めて内容を整理しつつ、公開することにしました!
SwiftからObjective-Cに徐々に移行していくときに遭遇しそうな内容をピックアップしています。

まだまだ私達もObjective-CからSwiftに模索しながら移行しているので、もっと良いベストプラクティスがあるかもしれないですが、これからSwiftに書き換えていくぞ!っていう人たちの参考になればと思います!

プロパティのsetter内部で何か処理を書いていた場合

Objective-Cの時に、

@property(nonatomic, assign) NSInteger currentPage;

- (int)setCurrentPage:(NSInteger)currentPage {
    _currentPage = currentPage;
    // ... 何かしらの処理
}

みたいな感じで、setter内部で変数への代入後に何かの処理を行うような場合があるかと思います。
そういう場合は、didSetを使って書き換えてあげます。

var currentPage: Int {
    didSet {
        // ... 何かしらの処理
    }
}

getter内部で、一度だけ変数を初期化して返している場合

私達が開発しているプロジェクトでは、よくgetterメソッドに、
その変数が初期化されてなかったら初期化して返し、初期化済みだったらそのまま返すような処理を書いている箇所がありました。

@property(nonatomic) UIView *someView;

- (UIVIew *)someView {
    if (_someView) {
        return _someView // 初期化済みなのでそのまま返す
    }
    _someView = [[UIView alloc] initWithFrame:CGRectZero];
    // ... 何かしらの処理
    return _someView; // 生成して返す
}

こうした処理をSwiftで書き換える時には、lazy varを使って書き換えてあげます。

lazy var someView: UIView = {
    let view = UIView(frame: .zero)
    // ... 何かしらの処理
    return view
}()

lazy varを使ってあげることで、呼び出すまでインスタンスが生成されないことと、生成済みであればそれをそのまま返すということが実現できます。
ちなみに、lazy varでの書き方は、このようにclosureを即時実行させてするパターンと、関数を用意するパターンとがあります。
try! Swiftでも紹介されていたので、興味がある人は見てみると良いでしょう。

readonlyなプロパティにしたい

Objective-Cでは、

@property(nonatomic, readonly) UIView *contentView;

みたいな感じで、readonlyなプロパティが作れました。
Swiftで同様のことをするには、private(set)をつけてあげます。

この、private(set)は、「privateスコープのみ、setが許される」といったものになります。

private(set) var contentView: UIView

ちなみに、ライブラリを作ったりする場合に、外からのアクセスを可能にしつつ、readonlyなプロパティにする場合は、

private(set) public var contentView: UIView

みたいな感じになります。privateとpublicが書いてあるとちょっと変な感じがしますね。

UIViewのinit

SwiftでUIViewを継承したクラスを作成して、イニシャライザを定義する場合は、以下のように定義します。
decoderを使う方のイニシャライザにはrequiredを、
frameを使う方のイニシャライザにはoverrideを付けます。

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

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

これ、毎回間違えたり、つけ忘れたりしちゃうんですよね。。

getter指定されたプロパティ

@property(nonatomic, getter=isTapped) BOOL tapped;

みたいな、getter指定されたプロパティをSwiftで書き換える時に、getter名の指定ができないので、
こういう名前で呼び出したい時は、別途計算型プロパティを宣言します。

var tapped: Bool // こっちは格納型プロパティでの宣言
var isTapped: Bool { // こっちは計算型プロパティでの宣言
    return tapped
}

self.tapped = true
if (self.isTapped) {
    // ....
}

でも、Swiftになってから、わざわざこういう風にgetterを用意することが少なくなりました。

定数宣言やマクロで定義した定数

Objective-Cで、

(.hファイル内)
#define ANIMATION_DURATION 0.4
extern static NSString *const UserDidLoginNotificationKey;

@interface SomeViewController
// ...
@end

//--------------
//--------------
(.mファイル内)
static NSString *const UserDidLoginNotificationKey = @"UserDidLoginNotification";

@implementation SomeViewController
// ...
@end

みたいな感じで定数宣言をしていた場合は、以下のように書き換えます。

let AnimationDuration = 0.4
let UserDidLoginNotificationKey = "UserDidLoginNotification"

class SomeViewController: UIViewController {
    // ...
}

トップレベルに記述した定数はinternalの範囲で他のファイルからも見えるようになります。もし、そのファイル内のみ見えるようにする場合は、privateをつけてあげます。
基本的にはこれで良いのですが...、もし書き換え途中でObjective-Cからも定数を使いたい場合は、
この方法だと参照できない ので、以下のいずれかの方法で書き換えます。

  • 方法1 : クラスの中に定数を入れる

  • Swift

class SomeViewController: UIViewController {
    static let AnimationDuration = 0.4
    static let UserDidLoginNotificationKey = "UserDidLoginNotification"
    // ...
}

staticを付けて、NSObjectを継承したclassの中に入れてあげます。そうすると、

NSString *notificationKey = [SomeViewController UserDidLoginNotificationKey];
let notificationKey = SomeViewController.UserDidLoginNotificationKey

とどちらからも参照することが可能になります。

  • 方法2 : ConstantやKeyクラス的なものを作る

方法1と似ていますが、グローバルに使う定数等を、Constantクラス等を作ってそこに集約して呼ぶ、という方法もあります。
Swiftのみだったら、structでもclassでも大丈夫ですし、ネストしたclassでより細かく分けることも可能ですが、Objective-Cから見えるようにするには、NSObjectを継承したclassにする必要があります。

class Constant: NSObject {
    static let AnimationDuration = 0.4
    static let UserDidLoginNotificationKey = "UserDidLoginNotification"

    override private init() {
        super.init()
    }
}
NSString *notificationKey = [Constant UserDidLoginNotificationKey];
let notificationKey = Constant.UserDidLoginNotificationKey

特に、SwiftObjective-Cに書き換えている過渡期では、どちらからも参照できないと困るケースもあるので、スマートに行けない部分がありますが、徐々に綺麗にしていけるかと思います。

どちらからも参照できるEnumを定義したい

Swiftのみだったら、enumの型は何でも良いのですが、もし書き換えの過渡期でどちらからも参照できるEnumにする場合は、

@objc SomeType: Int {
    case A
    case B
    case C
    case D

    // ※このfunctionはSwiftからのみ呼び出せる
    func doSomething() {
    }
}

と、 @objc をつけた上で、型を Int にしてあげます。

そうすると、

typedef NS_ENUM(NSInteger, SomeType) {
    SomeTypeA,
    SomeTypeB,
    SomeTypeC,
    SomeTypeD
}

と変換されて、Objective-Cからも見えるようになります。
ただし、doSomething()の例のように、関数は呼び出せないので注意です。
暫定的にどちらからも参照する場合はとりあえず @objc をつけてInt型のenumとして定義しておき、Swiftのみから使うようになったら @objc を取って好きな型に書き換えましょう。

クラス名を文字列で取得したい

Objective-Cでは、

NSString *className = NSStringFromClass([HogeClass class]);
NSString *className = NSStringFromClass([self class]);

といった形でクラス名を取得していましたが、Swiftでは以下のようにして取得します。

let className = String(Hoge.self)
let className = String(Hoge().dynamicType.self)

Swift2.0(?)から、Stringのイニシャライザに、クラスの型を渡してあげることで、型の名前を文字列で受け取れるようになったので、これを使います。

NSStringで使っていたpath操作系のメソッド

NSString *dirPath = @"/path/to";
NSString* path = [dirPath stringByAppendingPathComponent:@"foo.txt"];

NSStringでは、ファイルパスに関するメソッド(パスの連結や拡張子の追加、削除など)がサポートされているのですが、Swiftで書き換える時に問題が...。
なんと、これらのメソッドが、Swift2.0以降String型で使えなくなっています。

NSURLに同様のメソッドが用意されていますが、String→NSURL→Stringと変換しないといけないのでちょっとダルいです。

でも、StringはasでNSStringにキャストできるので、

let dirPath = "path/to"
let path = (dirPath as NSString).stringByAppendingPathComponent("foo.txt")

としてあげれば、今までどおりStringでファイルパスをあれこれしてあげることができます! 私個人でQiitaに書いた記事になりますが、こちらにもまとめてあります。

最後に

今年の秋には、Swift3.0がやってくるかも?ということで、ますますObjective-CよりもSwift!という風になっていくのは間違いないかと思います。
書き換えのコスト、リスクはありますが、今頑張ることで後で楽に、よりSwiftの素晴らしい機能を享受できるんじゃないかなって思っています。
僕も、1日1クラスSwift化!くらいの意気込みで頑張ろうと思います。

また、今回は一から全てSwift onlyで書き換える!Objective-Cからの参照はないよ!
というよりも、プロジェクトでの開発をしながら徐々に書き換えていく中でのプラクティスとしてまとめたので、同じような感じで進めている/進めようと思っている人の役に立てればと思います。

積極採用中!!

これからiOSチームではバリバリSwiftを使っていくので、Swiftで書いてみたい!っていうエンジニアさん大歓迎です! iOS以外にも、Android/サーバーサイドエンジニアも募集してます!


子育て家族アプリFamm、カップル専用アプリPairyを運営する Timers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings