Swiftならこう書くシリーズ 10選
CouplesアプリのiOSエンジニア、Johnです!
1年前CouplesのObjective-CコードをSwiftに書き換えてから、ベストプラクティスもガンガン変わってきました。それでObjective-CからSwiftに移行する時の10個Tipsにランキングを付けて、10位から紹介させていただきます!
10. 配列の操作ならSwiftの SequenceType メソッドを使用する
Swiftの Array や Set などは SequenceType プロトコルを実装していて、いくつかの便利なメソッドが実装されています。
例えば、配列が空かどうかのチェックには、lengthを使わず isEmpty を使うべきです:
Swift
Objective-C
if array.isEmpty {
// ...
if (array.length <= 0) {
// ...
配列のindexを操作する時 indices というpropertyを使い、使ったコードを減らすと良いです:
例えば、すべての indexをループする時は:
Swift
Objective-C
for i in array.indices {
// ...
}
for (NSUInteger i = 0; i < array.length; ++i) {
// ...
}
最初・最後のindexを取得するには:
Swift
Objective-C
let firstIndex = array.indices.first
let lastIndex = array.indices.last
NSUInteger firstIndex = 0;
NSUInteger lastIndex = array.length - 1;
最後の n のindex以外を取得するには:
Swift
Objective-C
for i in array.indices.dropLast(n) {
// ...
}
for (NSUInteger i = 0; i < (array.length - n); ++i) {
// ...
}
最初の n のindex以外を取得するには:
Swift
Objective-C
for i in array.indices.dropFirst(n) {
// ...
}
for (NSUInteger i = n; i < array.length; ++i) {
// ...
}
このように書くと目的が分かりやすくなり、コードの可読性が上がります。
9. シングルトンの代わりに staticの関数やプロパティを使用する enum で作成する
Objective-Cではstaticのプロパティが書けなかったので、 sharedInstance で実装したシングルトンパターンがよくあります。
Swiftでは、 static var や static func も書くことができるので、インスタンスを作る必要がありません。
Swift
Objective-C
final enum GlobalSettings {
static var isICloudEnabled = false;
static func saveSettings() {
// ...
}
}
@interface GlobalSettings
@property (nonatomic) BOOL isICloudEnabled;
@end
@implementation GlobalSettings
+ (GlobalSettings *)sharedInstance {
static GlobalSettings *instance;
static dispatch_once_t once = 0;
dispatch_once(&once, ^{
instance = GlobalSettings()
});
return instance;
}
- (void)saveSettings {
// ...
}
@end
if GlobalSettings.isICloudEnabled {
GlobalSettings.isICloudEnabled = false
// ...
}
GlobalSettings.saveSettings()
if (GlobalSettings.sharedInstance.isICloudEnabled) {
GlobalSettings.sharedInstance.isICloudEnabled = NO;
// ...
}
[GlobalSettings.sharedInstance saveSettings];
enum を使った理由は、空の struct と class だとインスタンスの初期化ができてしまいますが、 空の enum ではできません。なので、名前空間やシングルトンを宣言するには enum が良いと思います。
Swift
struct SettingsStruct {}
class SettingsClass {}
enum SettingsEnum {}
let s = SettingsStruct() // OK
let c = SettingsClass() // OK
let e = SettingsEnum() // コンパイルエラー: 'SettingsEnum' cannot be constructed because it has no accessible initializers
8. isKindOfClass() の代わりに is を使用する
isKindOfClass: を使うメリットがなくなりました。 is が短くて、 class 以外にも使えます。
Swift
Objective-C
if dictionary["value"] is String {
// ...
if ([dictionary[@"value"] isKindOfClass:[NSString class]]) {
// ...
switch–case にも is 条件が使えます:
Swift
switch dictionary["value"] {
case is String: print("It's a string!")
case is Int: print("It's a number!")
case is NSObject: print("It's an object!")
// ...
}
7. self はクロージャー内でも宣言できる
Objective-Cの経験者は以下の weak–strong パターンをよく使っていると思います:
Objective-C
typeof(self) __weak weakSelf = self;
[session requestWithCompletion:^{
typeof(self) __strong self = weakSelf;
[self showDialog];
}];
Objective-Cのブロック内でも変数名を self で宣言できます。これはブロック外の self をキャプチャーさせないためです。
ただ、Swiftではこのような宣言が通常は不可能なので、 strongSelf や sSelf などが宣言時によく使用されていると思います。
Swift
session.requestWithCompletion { [weak self] in
guard let self = self else { // コンパイルエラー
return
}
self.showDialog() // またコンパイルエラー。"self?.showDialog()" で書かないといけない
}
でも実は、以下のように記述することで self の宣言が可能になります。
Swift
session.requestWithCompletion { [weak self] in
guard let `self` = self else { // OK!
return
}
self.showDialog() // OK! オプショナル不要!
}
6. CGGeometryユーティリティのSwift版を使用する
Objective-Cでは CGGeometry.h にあるグローバル関数や定数を使うのはベストプラクティスになっています。SwiftではそのままCGGeometryのユーティリティが呼べますが、 CGRect 及び, CGSize, と CGPoint などにはもっとシンプルなものがextensionで実装されています:
Swift
Objective-C
let width = view.frame.width
let bottom = view.frame.maxY
CGFloat width = CGRectGetWidth(view.frame)
CGFloat right = CGRectGetMaxX(view.frame)
CGRectZero や, CGRectNull CGSizeZero, CGPointZero などの定数はSwiftで CGRect.zero, CGRect.null, CGSize.zero, CGPoint.zero で書けます:
Swift
Objective-C
let view = UIView(frame: .zero)
UIView *view = UIView(frame: CGRectZero)
昔は CGRect などの関数はグローバルで実装されていて、操作したいstructとその処理のパラメーターを両方引数に渡す必要がありましたが、Swiftではメンバー関数でわかりやすく対応されています:
Swift
Objective-C
let isInside = view.bounds.contains(subview.center)
BOOL isInside = CGRectContainsPoint(view.bounds, subview.center)
さらに、比較も簡単になりました。以前は CGSizeEqualToSize(...) のような関数で比較を行っていましたが、Swiftでは == などのオペレータが利用できるようになっています。
Swift
Objective-C
if imageView.bounds.size == image.size {
// ...
if (CGSizeEqualToSize(imageView.bounds.size, image.size)) {
// ...
5. Swiftのセッターチェーニングで可変のプロパティが設定できる
Objective-Cではstructのプロパティをチェイニングしても設定できません。Swiftでは普通にできるようになりました:
Swift
Objective-C
self.view.frame.origin.y = 20 // OK!
self.view.frame.origin.y = 20; // 何も設定されない!
// You need to do use a temporary variable:
CGRect frame = self.view.frame;
frame.origin.y = 20;
self.view.frame = frame; // OK!
4. do {} で複数行をグルーピングできる
Objective-Cの時は {} でコードをよく整理していました。Swiftでは do 文で同じこともできます。
Swift
Objective-C
do {
let titleLabel = self.titleLabel
titleLabel.text = "News"
titleLabel.textColor = .blueColor()
}
do {
let subtitleLabel = self.subtitleLabel
subtitleLabel.text = "2016/2/18"
subtitleLabel.textColor = .greenColor()
}
{
UILabel *titleLabel = self.titleLabel;
titleLabel.text = @"News";
titleLabel.textColor = [UIColor blueColor];
}
{
UILabel *subtitleLabel = self.subtitleLabel;
subtitleLabel.text = @"2016/2/18"
subtitleLabel.textColor = [UIColor greenColor];
}
もう一つのテクニックは、無名なクロージャーで書く方法です:
Swift
lazy var screenImage: UIImage = {
let largeImage = UIImage(named: "large_image")
// ...
let aspectFitImage = // ... resize image
return aspectFitImage
}()
3. respondsToSelector: の使用をやめて、SwiftのAvailability APIを使用する
Objective-Cでは、ランタイムでメソッドが使えるかどうかを確認するため respondsToSelector: を利用するべきだとよく言われていますが。Swiftではコンパイル時の分析が強化されて、 `#available() を使った方が良くなりました。
Swift
Objective-C
let manager = CLLocationManager()
if #available(iOS 9, *) {
manager.allowsBackgroundLocationUpdates = true
}
CLLocationManager *manager = [CLLocationManager new];
if ([manager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
manager.allowsBackgroundLocationUpdates = YES;
}
新しいAPIを使うともしMinimum Deployment Versionがメソッドの対応したバージョンより古い場合、 #available() でラッピングしないとコンパイルエラーが発生します:
Swift
let manager = CLLocationManager()
manager.allowsBackgroundLocationUpdates = true
// コンパイルエラー: 'allowsBackgroundLocationUpdates' is only available on iOS 9.0 or newer
2. 可能な限りジェネリクスやプロトコルで実装する
Swiftのジェネリクスを利用して関数を宣言できると様々な型に使えるようになり、フレキシブルなコードが書けます。例えば、よくある clamp() 関数はこのように書きます:
Swift
Objective-C
func clamp(minValue: T, _ value: T, _ maxValue: T) -> T {
return min(max(value, minValue), maxValue)
}
let clampedInt: Int = clamp(1, 155, 100) // = 100
let clampedDouble: Double = clamp(1.0, 155.123 , 100.0) // = 100.0
__attribute__((overloadable))
NSInteger clamp(NSInteger min, NSInteger value, NSInteger max) {
return MIN(max, MAX(min, value));
}
__attribute__((overloadable))
double clamp(double min, double value, double max) {
return MIN(max, MAX(min, value));
}
NSInteger clampedInt = clamp(1, 155, 100); // = 100
double clampedDouble = clamp(1.0, 155.123, 100.0); // 100.0
ジェネリクスで対応したメソッドはすべてのナンバー型(Int, Double, Floatなど)でそのまま使えます。Objective-Cの時はすべてのオーバロードを実装する必要がありました。
1. Swiftのパターンマッチングをマスターしよう!
Swiftの一番好きな機能がやはりパターンマッチングです。 if 文も、 switch/case 、 guard 、 for 、 while と repeat ループ、そして do/catch のエラーハンドラーも、全部パターンマッチングが使えます。
よく使っている箇所は prepareForSegue(_:) の時です:
Swift
Objective-C
switch (segue.destinationViewController, segue.identifier) {
case (let controller as SomeViewController, _):
// ... 処理をする
case (let controller as AnotherViewController, "SomeIdentifier"?):
// ... 処理をする
case (let controller as AnotherViewController, "AnotherIdentifier"?):
// ... 処理をする
default:
break
}
if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]) {
// ... 処理をする
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
&& [segue.identifier isEqualToString:@"SomeIdentifier"]) {
// ... 処理をする
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
&& [segue.identifier isEqualToString:@"AnotherIdentifier"]) {
// ... 処理をする
}
この case がとっても便利で、他のところにも使えるようになりました:
Swift
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {
// ... 処理をする
}
// UIButtonじゃないsubviewがスキップされる
for case (let button as UIButton) in self.subviews {
// ... 処理をする
}
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {
// ... 処理をする
}
do {
try removeDatabase(path)
}
catch MyCustomError.Cancelled { // カスタム型
cancellation()
}
catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {
// 無視する
completion()
}
catch let error as NSError {
failure(error)
}
パターンマッチングになれると、コードの可読性が確実に上がります。
終わりに
Objective-CからSwiftへのコード移行がAppleさんのおかげでスムーズでできましたが、やはり言語が変わるとベストプラクティスも考え直すべきです。ぜひ、Swiftの新しいAPIや新しいプログラミング思考を使ってみましょう!
エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!
CouplesアプリのiOSエンジニア、Johnです!
1年前CouplesのObjective-CコードをSwiftに書き換えてから、ベストプラクティスもガンガン変わってきました。それでObjective-CからSwiftに移行する時の10個Tipsにランキングを付けて、10位から紹介させていただきます!
10. 配列の操作ならSwiftの SequenceType メソッドを使用する
Swiftの Array や Set などは SequenceType プロトコルを実装していて、いくつかの便利なメソッドが実装されています。
例えば、配列が空かどうかのチェックには、lengthを使わず isEmpty を使うべきです:
| Swift | Objective-C |
|---|---|
if array.isEmpty {
// ...
|
if (array.length <= 0) {
// ...
|
配列のindexを操作する時 indices というpropertyを使い、使ったコードを減らすと良いです:
例えば、すべての indexをループする時は:
| Swift | Objective-C |
|---|---|
for i in array.indices {
// ...
}
|
for (NSUInteger i = 0; i < array.length; ++i) {
// ...
}
|
最初・最後のindexを取得するには:
| Swift | Objective-C |
|---|---|
let firstIndex = array.indices.first let lastIndex = array.indices.last |
NSUInteger firstIndex = 0; NSUInteger lastIndex = array.length - 1; |
最後の n のindex以外を取得するには:
| Swift | Objective-C |
|---|---|
for i in array.indices.dropLast(n) {
// ...
}
|
for (NSUInteger i = 0; i < (array.length - n); ++i) {
// ...
}
|
最初の n のindex以外を取得するには:
| Swift | Objective-C |
|---|---|
for i in array.indices.dropFirst(n) {
// ...
}
|
for (NSUInteger i = n; i < array.length; ++i) {
// ...
}
|
このように書くと目的が分かりやすくなり、コードの可読性が上がります。
9. シングルトンの代わりに staticの関数やプロパティを使用する enum で作成する
Objective-Cではstaticのプロパティが書けなかったので、 sharedInstance で実装したシングルトンパターンがよくあります。
Swiftでは、 static var や static func も書くことができるので、インスタンスを作る必要がありません。
| Swift | Objective-C |
|---|---|
final enum GlobalSettings {
static var isICloudEnabled = false;
static func saveSettings() {
// ...
}
}
|
@interface GlobalSettings
@property (nonatomic) BOOL isICloudEnabled;
@end
@implementation GlobalSettings
+ (GlobalSettings *)sharedInstance {
static GlobalSettings *instance;
static dispatch_once_t once = 0;
dispatch_once(&once, ^{
instance = GlobalSettings()
});
return instance;
}
- (void)saveSettings {
// ...
}
@end
|
if GlobalSettings.isICloudEnabled {
GlobalSettings.isICloudEnabled = false
// ...
}
GlobalSettings.saveSettings()
|
if (GlobalSettings.sharedInstance.isICloudEnabled) {
GlobalSettings.sharedInstance.isICloudEnabled = NO;
// ...
}
[GlobalSettings.sharedInstance saveSettings];
|
enum を使った理由は、空の struct と class だとインスタンスの初期化ができてしまいますが、 空の enum ではできません。なので、名前空間やシングルトンを宣言するには enum が良いと思います。
| Swift |
|---|
struct SettingsStruct {}
class SettingsClass {}
enum SettingsEnum {}
let s = SettingsStruct() // OK
let c = SettingsClass() // OK
let e = SettingsEnum() // コンパイルエラー: 'SettingsEnum' cannot be constructed because it has no accessible initializers
|
8. isKindOfClass() の代わりに is を使用する
isKindOfClass: を使うメリットがなくなりました。 is が短くて、 class 以外にも使えます。
| Swift | Objective-C |
|---|---|
if dictionary["value"] is String {
// ...
|
if ([dictionary[@"value"] isKindOfClass:[NSString class]]) {
// ...
|
switch–case にも is 条件が使えます:
| Swift |
|---|
switch dictionary["value"] {
case is String: print("It's a string!")
case is Int: print("It's a number!")
case is NSObject: print("It's an object!")
// ...
}
|
7. self はクロージャー内でも宣言できる
Objective-Cの経験者は以下の weak–strong パターンをよく使っていると思います:
| Objective-C |
|---|
typeof(self) __weak weakSelf = self;
[session requestWithCompletion:^{
typeof(self) __strong self = weakSelf;
[self showDialog];
}];
|
Objective-Cのブロック内でも変数名を self で宣言できます。これはブロック外の self をキャプチャーさせないためです。
ただ、Swiftではこのような宣言が通常は不可能なので、 strongSelf や sSelf などが宣言時によく使用されていると思います。
| Swift |
|---|
session.requestWithCompletion { [weak self] in
guard let self = self else { // コンパイルエラー
return
}
self.showDialog() // またコンパイルエラー。"self?.showDialog()" で書かないといけない
}
|
でも実は、以下のように記述することで self の宣言が可能になります。
| Swift |
|---|
session.requestWithCompletion { [weak self] in
guard let `self` = self else { // OK!
return
}
self.showDialog() // OK! オプショナル不要!
}
|
6. CGGeometryユーティリティのSwift版を使用する
Objective-Cでは CGGeometry.h にあるグローバル関数や定数を使うのはベストプラクティスになっています。SwiftではそのままCGGeometryのユーティリティが呼べますが、 CGRect 及び, CGSize, と CGPoint などにはもっとシンプルなものがextensionで実装されています:
| Swift | Objective-C |
|---|---|
let width = view.frame.width let bottom = view.frame.maxY |
CGFloat width = CGRectGetWidth(view.frame) CGFloat right = CGRectGetMaxX(view.frame) |
CGRectZero や, CGRectNull CGSizeZero, CGPointZero などの定数はSwiftで CGRect.zero, CGRect.null, CGSize.zero, CGPoint.zero で書けます:
| Swift | Objective-C |
|---|---|
let view = UIView(frame: .zero) |
UIView *view = UIView(frame: CGRectZero) |
昔は CGRect などの関数はグローバルで実装されていて、操作したいstructとその処理のパラメーターを両方引数に渡す必要がありましたが、Swiftではメンバー関数でわかりやすく対応されています:
| Swift | Objective-C |
|---|---|
let isInside = view.bounds.contains(subview.center) |
BOOL isInside = CGRectContainsPoint(view.bounds, subview.center) |
さらに、比較も簡単になりました。以前は CGSizeEqualToSize(...) のような関数で比較を行っていましたが、Swiftでは == などのオペレータが利用できるようになっています。
| Swift | Objective-C |
|---|---|
if imageView.bounds.size == image.size {
// ...
|
if (CGSizeEqualToSize(imageView.bounds.size, image.size)) {
// ...
|
5. Swiftのセッターチェーニングで可変のプロパティが設定できる
Objective-Cではstructのプロパティをチェイニングしても設定できません。Swiftでは普通にできるようになりました:
| Swift | Objective-C |
|---|---|
self.view.frame.origin.y = 20 // OK! |
self.view.frame.origin.y = 20; // 何も設定されない! // You need to do use a temporary variable: CGRect frame = self.view.frame; frame.origin.y = 20; self.view.frame = frame; // OK! |
4. do {} で複数行をグルーピングできる
Objective-Cの時は {} でコードをよく整理していました。Swiftでは do 文で同じこともできます。
| Swift | Objective-C |
|---|---|
do {
let titleLabel = self.titleLabel
titleLabel.text = "News"
titleLabel.textColor = .blueColor()
}
do {
let subtitleLabel = self.subtitleLabel
subtitleLabel.text = "2016/2/18"
subtitleLabel.textColor = .greenColor()
}
|
{
UILabel *titleLabel = self.titleLabel;
titleLabel.text = @"News";
titleLabel.textColor = [UIColor blueColor];
}
{
UILabel *subtitleLabel = self.subtitleLabel;
subtitleLabel.text = @"2016/2/18"
subtitleLabel.textColor = [UIColor greenColor];
}
|
もう一つのテクニックは、無名なクロージャーで書く方法です:
| Swift |
|---|
lazy var screenImage: UIImage = {
let largeImage = UIImage(named: "large_image")
// ...
let aspectFitImage = // ... resize image
return aspectFitImage
}()
|
3. respondsToSelector: の使用をやめて、SwiftのAvailability APIを使用する
Objective-Cでは、ランタイムでメソッドが使えるかどうかを確認するため respondsToSelector: を利用するべきだとよく言われていますが。Swiftではコンパイル時の分析が強化されて、 `#available() を使った方が良くなりました。
| Swift | Objective-C |
|---|---|
let manager = CLLocationManager()
if #available(iOS 9, *) {
manager.allowsBackgroundLocationUpdates = true
}
|
CLLocationManager *manager = [CLLocationManager new];
if ([manager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
manager.allowsBackgroundLocationUpdates = YES;
}
|
新しいAPIを使うともしMinimum Deployment Versionがメソッドの対応したバージョンより古い場合、 #available() でラッピングしないとコンパイルエラーが発生します:
| Swift |
|---|
let manager = CLLocationManager() manager.allowsBackgroundLocationUpdates = true // コンパイルエラー: 'allowsBackgroundLocationUpdates' is only available on iOS 9.0 or newer |
2. 可能な限りジェネリクスやプロトコルで実装する
Swiftのジェネリクスを利用して関数を宣言できると様々な型に使えるようになり、フレキシブルなコードが書けます。例えば、よくある clamp() 関数はこのように書きます:
| Swift | Objective-C |
|---|---|
func clamp(minValue: T, _ value: T, _ maxValue: T) -> T {
return min(max(value, minValue), maxValue)
}
let clampedInt: Int = clamp(1, 155, 100) // = 100
let clampedDouble: Double = clamp(1.0, 155.123 , 100.0) // = 100.0
|
__attribute__((overloadable))
NSInteger clamp(NSInteger min, NSInteger value, NSInteger max) {
return MIN(max, MAX(min, value));
}
__attribute__((overloadable))
double clamp(double min, double value, double max) {
return MIN(max, MAX(min, value));
}
NSInteger clampedInt = clamp(1, 155, 100); // = 100
double clampedDouble = clamp(1.0, 155.123, 100.0); // 100.0
|
ジェネリクスで対応したメソッドはすべてのナンバー型(Int, Double, Floatなど)でそのまま使えます。Objective-Cの時はすべてのオーバロードを実装する必要がありました。
1. Swiftのパターンマッチングをマスターしよう!
Swiftの一番好きな機能がやはりパターンマッチングです。 if 文も、 switch/case 、 guard 、 for 、 while と repeat ループ、そして do/catch のエラーハンドラーも、全部パターンマッチングが使えます。
よく使っている箇所は prepareForSegue(_:) の時です:
| Swift | Objective-C |
|---|---|
switch (segue.destinationViewController, segue.identifier) {
case (let controller as SomeViewController, _):
// ... 処理をする
case (let controller as AnotherViewController, "SomeIdentifier"?):
// ... 処理をする
case (let controller as AnotherViewController, "AnotherIdentifier"?):
// ... 処理をする
default:
break
}
|
if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]) {
// ... 処理をする
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
&& [segue.identifier isEqualToString:@"SomeIdentifier"]) {
// ... 処理をする
}
else if ([segue.destinationViewController isKindOfClass:[SomeViewController class]]
&& [segue.identifier isEqualToString:@"AnotherIdentifier"]) {
// ... 処理をする
}
|
この case がとっても便利で、他のところにも使えるようになりました:
| Swift |
|---|
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {
// ... 処理をする
}
|
// UIButtonじゃないsubviewがスキップされる
for case (let button as UIButton) in self.subviews {
// ... 処理をする
}
|
if case (let controller as AnotherViewController) = segue.controller where segue.identifier == "SomeIdentifier" {
// ... 処理をする
}
|
do {
try removeDatabase(path)
}
catch MyCustomError.Cancelled { // カスタム型
cancellation()
}
catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {
// 無視する
completion()
}
catch let error as NSError {
failure(error)
}
|
パターンマッチングになれると、コードの可読性が確実に上がります。
終わりに
Objective-CからSwiftへのコード移行がAppleさんのおかげでスムーズでできましたが、やはり言語が変わるとベストプラクティスも考え直すべきです。ぜひ、Swiftの新しいAPIや新しいプログラミング思考を使ってみましょう!
エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!