26
14

More than 5 years have passed since last update.

ObjC, Swiftが混在している環境で使える小技など

Last updated at Posted at 2017-12-16

ex-mixi Advent Calendar 2017 17日目の記事になります。

株式会社ミクシィには2009年に新卒で入社し、2014年の2月に転職しました。
同期は私含めて10人でしたが、転職したり、起業したり、ミクシィに出戻ったり、海外で生活してまた日本に帰ってきていたりとそれぞれ元気にやっているようで何よりです。
私の近況としては転職後、子供が生まれて現在3歳です。来年の頭にもう1人生まれる予定です。 子供を育てるのは大変ですが、日々の成長が著しく人間ってスゲー!と感じる日々を送っています。
つい先日も出張で3日ほど家を空けていたのですが帰宅後の翌朝に子供と話すと、最後に話した時よりも流暢にお話出来る様になっていて感動しました。

現職ではiOSアプリの開発やiOSアプリ向けのCI環境のメンテナンスなどをしています。
残念なことに他の記事の様なエモいこと1が思いつかなかったのでObjCもSwiftも書いてる、もしくは書く予定の方向けの記事を書くことにしました。

はじめに

この記事はXcode9.2、Swift4環境を想定しています。
Swift5など将来的には一部のTipが使えなくなっている可能性があるためご注意下さい。

筆者は2014年の末ぐらいからObjCがメインだったプロジェクトでSwiftを書いていますが、最近ではSwiftでしかiOSアプリを書いたことが無いという方も増えてきている様に思います。
しかし、Swift以前からメンテナンスされているアプリなどではまだまだObjCのコードが残っているというのが現実ではないでしょうか。
本記事はそういったSwift、ObjCが混在している環境で使える小技+αを紹介したいと思います。
本当に小技です。すみません。

ObjectiveCだけに公開するメソッドをSwiftで書く

ObjCのコードが混在しているプロジェクトで、Swiftのコードを書く場合に使うテクニックです。

Swiftで書かれたメソッドをObjC向けに公開したい場合に、引数がOptional Enum, Optional IntなどSwiftでしか利用できないメソッドがある為、ObjC用に新たにmethodを追加することがあります。

func anyMethod(_ e: SomeEnum?) {
    ...
}

@objc
func anyMethodForObjc(stringValue: String) {
    guard let someEnum = SomeEnum(rawValue: stringValue) else { return }
    anyMethod(someEnum)
}

@objcが付いているmethodもSwift側で呼び出せますが、このケースだとSwift側では呼び出す必要の無い不要なメソッドが増えただけになります。
以下の方法でこのメソッドをSwift側で非公開にすることが出来ます。

@objc
@available(swift 99, *) // Please wait for Swift99 released If you want to use this method.
func anyMethodforObjc(stringValue: String) {
    guard let someEnum = SomeEnum(rawValue: stringValue) else { return }
    anyMethod(someEnum)
}

func anyMethod(_ e: SomeEnum?) {
    ...
}

selfをOptional Bindingする

ObjCではお馴染み、libextobjcのweakify/stringifyマクロがSwiftでは利用出来ません。
Swiftでは通常selfをそのままOptional Binding出来ない為、if let strongSelf = selfの様にself?を別の名前に代入して利用する必要があります。

しかし、以下の様に`でクオートをするとOptional Bindingが可能になります。

let handler = { [weak self] () in
    guard let `self` = self else { return }
    self.hoge() // It can pass compiling.
}

この方法はSwift4でも利用できていますが、意図したものでは無いようです。
将来的に出来なくなるかもしれません。しかし同様の動作は提供されると思います。
https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20160104/005468.html

環境によってコードの内容を切り替える

ObjCのMacroと同じ様な方法で、Swiftでも開発者のみに公開する機能など、環境によってコードをFlagで落としたり処理を分けたりすることが可能です。
(やりすぎると環境によってBuildが通らなかったりするのでご利用は計画的に。)
Flagはxcconfigファイルなどで定義してBuild Scheme毎に切り替えるなどすると便利です。

Development.xconfig

SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEVELOPMENT_ENV DEBUG_FUNCTION NEW_FEATURE
struct APIClient {
    private var serverHost: String {
        #if DEVELOPMENT_ENV || TESTING_ENV
            return "dev-env.com"
        #elseif PRODUCTION_ENV
            return "production-env.com"
        #else
            fatalError()
        #endif
    }

    private func request(completion: (Response?, Error?) -> Void) {
        #if NEW_FEATURE
            newFeaure.request(completion: completion)
        #else
            currentFeaure.request(completion: completion)
        #endif
    }

#if DEBUG_FUNCTION
    func debugging() {
        ...
    }
#endif
}

SwiftLintでコードをチェックする

複数人での開発では、ルールベースで自動的にコードを警告、補完できた方がレビュー時などに無駄なチェックを省けて生産的です。
SwiftLintは導入していないのであれば導入しておくことをおすすめします。
https://github.com/realm/SwiftLint

Danger向けのPluginもあるので、Pull Requestなどコードレビューのタイミングで自動でルールのチェックをすることも可能です。
https://rubygems.org/gems/danger-swiftlint

AssetやLocalizable.stringdictファイルをSwiftのコードとして扱う。

通常、assetやstrings, stringdictファイルを扱う場合以下の様にkeyにあたる部分をハードコーディングします。

let UserName = NSLocalizedString("user.name.default", comment: "")
let image = UIImage(named: "default_profile_image")

しかし、このコードではassetが消されたり、多言語ファイルのキーが消えた場合などに当たり前ですがビルドは失敗しません。
SwiftGenなどのツールを使うことで、AssetファイルからSwiftのコードを生成することができます。
対応するAssetやkeyが消えるとSwift側に生成されたコードが削除される為、それを利用している箇所でビルドが失敗します。SwiftGenはCocoaPodsでも管理可能なのでpod installでのインストールも可能です。

# assetからswiftのファイルを生成する。
./Pods/SwiftGen/bin/swiftgen xcassets \
     --template swift4 \
     --output /output/filepath/xcassets.swift \
     /dir/to/search/for/imageset/assets

生成されたコードは次の様にSwiftのコードとして記述可能です。

// Copy from https://github.com/SwiftGen/SwiftGen/blob/master/README.md

// You can create new images with the convenience constructor like this:
let bananaImage = UIImage(asset: Asset.Exotic.banana)  // iOS
let privateImage = NSImage(asset: Asset.private)  // macOS

// Or as an alternative, you can refer to enum instance and call .image on it:
let sameBananaImage = Asset.Exotic.banana.image
let samePrivateImage = Asset.private.image

// Simple strings
let message = L10n.alertMessage
let title = L10n.alertTitle

// with parameters, note that each argument needs to be of the correct type
let apples = L10n.applesCount(3)
let bananas = L10n.bananasOwner(5, "Olivier")

Templateを自前で用意することも可能です。
補完も効く様になるためkeyを直接書くよりミスも減るのでは無いでしょうか。

ObjC側では常にNullability, Lightweight-Genericsを書く

2015年に発表された内容で時間も経っているので今更の内容ですが。
https://developer.apple.com/swift/blog/?id=25

ObjCだけのprojectでもNullabilityは有用ですが、既存のObjcで書かれたコードをSwiftから利用する場合はより重要になります。
ObjC側でNullabilityが無いとSwift側では!(implicitly unwrapped optional)となってしまって、本来Optional Bindingが必要な値が入るケースもcompile時に気づかず実行時にクラッシュするということもあります。
Swift登場前に書かれたObjCのコードであれば、Nullabilityが書かれていないものもあると思います。
Bridging-Headerに追加する際や、新規でファイルを追加した時などにNullablityを書くようにしておくとSwift側でOptional or Non-Optionalで扱うことが出来るようになります。

Interface定義でNonnullデフォルトにする。

NS_ASSUME_NONNULL_BEGIN/ NS_ASSUME_NONNULL_ENDを使うことでデフォルトをnonnullにすることができます。nullableなものだけを明示すれば良くなるので新規追加時はこちらがおすすめです。

NS_ASSUME_NONNULL_BEGIN
@interface YourClassName : NSObject

@property (nonatomic, strong) NSString *nonnullProperty;
@property (nonatomic, strong, nullable) NSString *nullableProperty;

- (NSString *)nonnullStringWithNonnullValue:(nullable NSString *)value;
- (nullable NSString *)nullableStringWithNullableValue:(nullable NSString *)value;

@end
NS_ASSUME_NONNULL_END

Lightweight-Generics

ObjC側で、NSDictionary, NSArray, NSSetなどを利用するメソッドやプロパティを公開する場合はObjectTypeを明示します。 ObjectTypeが指定されていないとSwift側でAnyとして扱う必要があるので、不必要な型チェックが増えることになります。

@propperty (nonatomic, readonly, nullable) NSArray<__kindof UIView *> *subViews;

- (nonnull NSArray<__kindof UIButton *> *)buttons;

Swiftでは以下のI/Fとして扱えます。

var subViews: [UIView]? { get }
func buttons() -> [UIButton]

最後まで読んでいただいてありがとうございました。
そんな古いネタいまさら!?というものも織り交ぜつつ、そういえばやってない。。という方に刺されば嬉しいなと思います。
明日はkakky0312さんのエントリになります。
よろしくお願いします。

おまけ

先日出張で韓国に行っていたのですが、その時食べたタッカルビの写真を貼っておきます。
日本ではチーズタッカルビが流行っているようですが本場の(チーズなし)タッカルビは辛かったです。

鶏肉、キャベツ、トッポギ、エゴマ、サツマイモをコチュジャンで炒めたシンプルな料理です。
このタッカルビも十分に辛かったのですが、韓国では唐辛子を下さいとお願いすると日本のネギのノリで青唐辛子を出してくれたりします。
辛いものが好きな人は機会があればぜひ唐辛子を追加で頼んで食べてみて下さい。

サムギョプサルに合わせるのもおすすめです。


  1. 双子が生まれていきなり子供が4人になってリモートワークしたり、新規事業を買い取って社長やったり、本一冊書いたりなど.... 

26
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
14