iOS開発者が犯していることを知らない10の最も一般的な間違い

公開: 2022-03-11

バグのあるアプリがAppStoreに拒否されるよりも悪いことは何ですか? それを受け入れてもらう。 1つ星のレビューが入り始めると、回復することはほとんど不可能です。 これには企業のお金と開発者の仕事がかかります。

iOSは現在、世界で2番目に大きいモバイルオペレーティングシステムです。 また、採用率も非常に高く、85%以上のユーザーが最新バージョンを使用しています。 ご想像のとおり、エンゲージメントの高いユーザーには大きな期待が寄せられています。アプリやアップデートに問題がなければ、そのことを聞くことができます。

iOS開発者の需要が急増し続けているため、多くのエンジニアがモバイル開発に切り替えています(毎日1,000を超える新しいアプリがAppleに提出されています)。 しかし、真のiOSの専門知識は、基本的なコーディングをはるかに超えています。 以下は、iOS開発者が捕食する10の一般的な間違いと、それらを回避する方法です。

iOSユーザーの85%が最新のOSバージョンを使用しています。 つまり、彼らはあなたのアプリやアップデートが完璧であることを期待しています。
つぶやき

よくある間違いその1:非同期プロセスを理解していない

新しいプログラマーの間で非常によくあるタイプの間違いは、非同期コードを不適切に処理することです。 典型的なシナリオを考えてみましょう。ユーザーがテーブルビューで画面を開きます。 一部のデータはサーバーからフェッチされ、テーブルビューに表示されます。 より正式に書くことができます:

 @property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 }]; [self.tableView reloadData]; // 2 } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }

一見すると、すべてが正しく見えます。サーバーからデータをフェッチしてから、UIを更新します。 ただし、問題は、データのフェッチが非同期プロセスであり、新しいデータをすぐに返さないことです。つまり、新しいデータを受信する前にreloadDataが呼び出されます。 この間違いを修正するには、ブロック内の行#1の直後に行#2を移動する必要があります。

 @property (nonatomic, strong) NSArray *dataFromServer; - (void)viewDidLoad { __weak __typeof(self) weakSelf = self; [[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){ weakSelf.dataFromServer = newData; // 1 [weakSelf.tableView reloadData]; // 2 }]; } // and other data source delegate methods - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataFromServer.count; }

ただし、このコードがまだ期待どおりに動作しない場合があります。これにより、…

よくある間違いその2:メインキュー以外のスレッドでUI関連のコードを実行する

以前のよくある間違いから修正されたコード例を使用したと想像してみてください。ただし、非同期プロセスが正常に完了した後でも、テーブルビューは新しいデータで更新されません。 このような単純なコードの何が問題になっているのでしょうか。 それを理解するために、ブロック内にブレークポイントを設定し、このブロックがどのキューで呼び出されているかを調べることができます。 UI関連のすべてのコードを実行する必要があるメインキューに呼び出しがないため、説明されている動作が発生している可能性が高くなります。

Alamofire、AFNetworking、Hanekeなどの最も一般的なライブラリは、非同期タスクの実行後にメインキューでcompletionBlockを呼び出すように設計されています。 ただし、これに常に依存できるとは限らず、コードを適切なキューにディスパッチすることを忘れがちです。

UI関連のすべてのコードがメインキューにあることを確認するには、そのキューにコードをディスパッチすることを忘れないでください。

 dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });

よくある間違いその3:並行性とマルチスレッドの誤解

並行性は、非常に鋭いナイフに例えることができます。注意が必要でない場合や十分な経験がない場合は簡単に自分を切ることができますが、適切かつ安全に使用する方法を知っていれば、非常に便利で効率的です。

並行性の使用を回避することはできますが、構築しているアプリの種類に関係なく、並行性なしでは実行できない可能性が非常に高くなります。 同時実行性は、アプリケーションに大きなメリットをもたらす可能性があります。 特に:

  • ほとんどすべてのアプリケーションには、Webサービスへの呼び出しがあります(たとえば、重い計算を実行したり、データベースからデータを読み取ったりするため)。 これらのタスクがメインキューで実行されると、アプリケーションはしばらくの間フリーズし、応答しなくなります。 さらに、これに時間がかかりすぎると、iOSはアプリを完全にシャットダウンします。 これらのタスクを別のキューに移動すると、ユーザーは、アプリがフリーズしているように見えることなく、操作の実行中にアプリケーションを引き続き使用できます。
  • 最近のiOSデバイスには複数のコアがありますが、並行して実行できるのに、なぜユーザーはタスクが順番に終了するのを待つ必要があるのでしょうか。

しかし、並行性の利点は、複雑さと、再現が非常に難しい競合状態などの厄介なバグを導入する可能性がなければ得られません。

実際の例をいくつか考えてみましょう(簡単にするために一部のコードは省略されていることに注意してください)。

ケース1

 final class SpinLock { private var lock = OS_SPINLOCK_INIT func withLock<Return>(@noescape body: () -> Return) -> Return { OSSpinLockLock(&lock) defer { OSSpinLockUnlock(&lock) } return body() } } class ThreadSafeVar<Value> { private let lock: ReadWriteLock private var _value: Value var value: Value { get { return lock.withReadLock { return _value } } set { lock.withWriteLock { _value = newValue } } } }

マルチスレッドコード:

 let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something }

一見すると、 ThreadSaveVarcounterをラップしてスレッドセーフにするため、すべてが同期され、期待どおりに機能するように見えます。 残念ながら、これは正しくありません。2つのスレッドが同時に増分行に到達し、 counter.value == someValueが結果としてtrueになることはないためです。 回避策として、インクリメント後に値を返すThreadSafeCounterを作成できます。

 class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }

ケース2

 struct SynchronizedDataArray { private let synchronizationQueue = dispatch_queue_create("queue_name", nil) private var _data = [DataType]() var data: [DataType] { var dataInternal = [DataType]() dispatch_sync(self.synchronizationQueue) { dataInternal = self._data } return dataInternal } mutating func append(item: DataType) { appendItems([item]) } mutating func appendItems(items: [DataType]) { dispatch_barrier_sync(synchronizationQueue) { self._data += items } } }

この場合、 dispatch_barrier_syncを使用してアレイへのアクセスを同期しました。 これは、アクセスの同期を確保するためのお気に入りのパターンです。 残念ながら、このコードでは、アイテムを追加するたびに構造体がコピーを作成することを考慮していないため、毎回新しい同期キューがあります。

ここでは、一見正しいように見えても、期待どおりに動作しない場合があります。 また、テストとデバッグには多くの作業が必要ですが、最終的には、アプリの速度と応答性を向上させることができます。

よくある間違いその4:可変オブジェクトの落とし穴を知らない

Swiftは値型の間違いを避けるのに非常に役立ちますが、Objective-Cを使用する開発者はまだたくさんいます。 可変オブジェクトは非常に危険であり、隠れた問題を引き起こす可能性があります。 不変オブジェクトを関数から返すことはよく知られているルールですが、ほとんどの開発者はその理由を知りません。 次のコードを考えてみましょう。

 // Box.h @interface Box: NSObject @property (nonatomic, readonly, strong) NSArray <Box *> *boxes; @end // Box.m @interface Box() @property (nonatomic, strong) NSMutableArray <Box *> *m_boxes; - (void)addBox:(Box *)box; @end @implementation Box - (instancetype)init { self = [super init]; if (self) { _m_boxes = [NSMutableArray array]; } return self; } - (void)addBox:(Box *)box { [self.m_boxes addObject:box]; } - (NSArray *)boxes { return self.m_boxes; } @end

NSMutableArrayNSArrayのサブクラスであるため、上記のコードは正しいです。 では、このコードで何がうまくいかないのでしょうか?

最初で最も明白なことは、別の開発者がやって来て次のことを行う可能性があるということです。

 NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }

このコードはクラスを台無しにします。 しかし、その場合、それはコードの臭いであり、その断片を拾うのはその開発者に任されています。

ただし、これははるかに悪いケースであり、予期しない動作を示しています。

 Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes];

ここでの期待は、 [newChildBoxes count] > [childBoxes count]ですが、そうでない場合はどうなりますか? 次に、クラスは、すでに返された値を変更するため、適切に設計されていません。 不平等が真であってはならないと思われる場合は、UIViewと[view subviews]を試してみてください。

幸い、最初の例のゲッターを書き直すことで、コードを簡単に修正できます。

 - (NSArray *)boxes { return [self.m_boxes copy]; }

よくある間違いその5: NSDictionaryが内部でどのように機能するかを理解していない

カスタムクラスとNSDictionaryを使用したことがある場合、辞書キーとしてのNSCopyingに準拠していないと、クラスを使用できないことに気付くかもしれません。 ほとんどの開発者は、Appleがなぜその制限を追加したのかを自問したことはありません。 Appleがキーをコピーして、元のオブジェクトの代わりにそのコピーを使用するのはなぜですか?

これを理解するための鍵は、 NSDictionaryが内部でどのように機能するかを理解することです。 技術的には、これは単なるハッシュテーブルです。 キーのオブジェクトを追加しながら、高レベルでどのように機能するかを簡単に要約してみましょう(簡単にするために、テーブルのサイズ変更とパフォーマンスの最適化はここでは省略されています)。

ステップ1: hash(Key)を計算します。 ステップ2:ハッシュに基づいて、オブジェクトを配置する場所を探します。 通常、これは、辞書の長さでハッシュ値のモジュラスを取得することによって行われます。 結果のインデックスは、キーと値のペアを格納するために使用されます。 ステップ3:その場所にオブジェクトがない場合は、リンクリストが作成され、レコード(オブジェクトとキー)が保存されます。 それ以外の場合は、レコードをリストの最後に追加します。

それでは、レコードがディクショナリからどのようにフェッチされるかを説明しましょう。

ステップ1: hash(Key)を計算します。 ステップ2:ハッシュでキーを検索します。 データがない場合は、 nilが返されます。 ステップ3:リンクリストがある場合は、 [storedkey isEqual:Key]までオブジェクトを繰り返し処理します。

内部で何が起こっているかをこのように理解することで、2つの結論を導き出すことができます。

  1. キーのハッシュが変更された場合は、レコードを別のリンクリストに移動する必要があります。
  2. キーは一意である必要があります。

簡単なクラスでこれを調べてみましょう。

 @interface Person @property NSMutableString *name; @end @implementation Person - (BOOL)isEqual:(id)object { if (self == object) { return YES; } if (![object isKindOfClass:[Person class]]) { return NO; } return [self.name isEqualToSting:((Person *)object).name]; } - (NSUInteger)hash { return [self.name hash]; } @end

ここで、 NSDictionaryがキーをコピーしないと想像してください。

 NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;

おー! そこにタイプミスがあります! 直そう!

 p.name = @"Jon Snow";

私たちの辞書はどうなりますか? 名前が変更されたため、別のハッシュが作成されました。 これで、オブジェクトが間違った場所にあり(辞書はデータの変更を認識しないため、古いハッシュ値が残っています)、辞書でデータを検索するためにどのハッシュを使用する必要があるかが明確ではありません。 さらに悪いケースがあるかもしれません。 辞書に評価5の「JonSnow」がすでに含まれている場合を想像してみてください。辞書は、同じキーに対して2つの異なる値になります。

ご覧のとおり、 NSDictionaryに可変キーがあると発生する可能性のある多くの問題があります。 このような問題を回避するためのベストプラクティスは、オブジェクトを保存する前にオブジェクトをコピーし、プロパティをcopyとしてマークすることです。 この練習は、クラスの一貫性を保つのにも役立ちます。

よくある間違いその6:XIBの代わりにストーリーボードを使用する

ほとんどの新しいiOS開発者は、Appleの提案に従い、UIにデフォルトでストーリーボードを使用します。 ただし、ストーリーボードを使用することには多くの欠点があり、(議論の余地のある)利点はごくわずかです。

ストーリーボードの欠点は次のとおりです。

  1. 複数のチームメンバーのストーリーボードを変更するのは非常に困難です。 技術的には、多くのストーリーボードを使用できますが、その場合の唯一の利点は、ストーリーボード上のコントローラー間にセグエを設定できることです。
  2. ストーリーボードのコントローラーとセグエの名前は文字列であるため、コード全体でこれらの文字列をすべて再入力するか(いつかそれ壊す)、ストーリーボード定数の膨大なリストを維持する必要があります。 SBConstantsを使用することもできますが、ストーリーボードでの名前の変更はまだ簡単な作業ではありません。
  3. ストーリーボードは、非モジュラー設計にあなたを強制します。 ストーリーボードで作業している間、ビューを再利用可能にするインセンティブはほとんどありません。 これは、Minimum Viable Product(MVP)またはクイックUIプロトタイピングでは許容できる場合がありますが、実際のアプリケーションでは、アプリ全体で同じビューを数回使用する必要がある場合があります。

ストーリーボード(議論の余地のある)の利点:

  1. アプリのナビゲーション全体を一目で確認できます。 ただし、実際のアプリケーションでは、さまざまな方向に接続された10個を超えるコントローラーを使用できます。 このような接続を備えたストーリーボードは、糸の玉のように見え、データフローの高レベルの理解を提供しません。
  2. 静的テーブル。 これが私が考えることができる唯一の本当の利点です。 問題は、静的テーブルの90%がアプリの進化中に動的テーブルに変わる傾向があり、動的テーブルをXIBでより簡単に処理できることです。

よくある間違いNo.7​​:紛らわしいオブジェクトとポインターの比較

2つのオブジェクトを比較する場合、ポインターとオブジェクトの同等性という2つの同等性を考慮することができます。

ポインターの同等性は、両方のポインターが同じオブジェクトを指している場合の状況です。 Objective-Cでは、2つのポインターを比較するために==演算子を使用します。 オブジェクトの同等性は、データベースの同じユーザーのように、2つのオブジェクトが2つの論理的に同一のオブジェクトを表す状況です。 Objective-Cでは、2つのオブジェクトを比較するために、 isEqual 、またはさらに良いタイプ固有のisEqualToStringisEqualToDateなどの演算子を使用します。

次のコードを検討してください。

 NSString *a = @"a"; // 1 NSString *b = @"a"; // 2 if (a == b) { // 3 NSLog(@"%@ is equal to %@", a, b); } else { NSLog(@"%@ is NOT equal to %@", a, b); }

そのコードを実行すると、コンソールに何が出力されますか? オブジェクトabの両方がメモリ内の同じオブジェクトを指しているため、aはbにa is equal to bます。

しかし、2行目を次のように変更しましょう。

 NSString *b = [[@"a" mutableCopy] copy];

これらのオブジェクトが同じ値を持っていても、これらのポインタが異なるオブジェクトを指しているためa is NOT equal to b

この問題は、 isEqualまたはタイプ固有の関数に依存することで回避できます。 このコード例では、常に正しく機能するように、3行目を次のコードに置き換える必要があります。

 if ([a isEqual:b]) {

よくある間違いその8:ハードコードされた値の使用

ハードコードされた値には、2つの主要な問題があります。

  1. それらが何を表しているのかが明確でないことがよくあります。
  2. コード内の複数の場所で使用する必要がある場合は、再入力(またはコピーして貼り付ける)する必要があります。

次の例を考えてみましょう。

 if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];

172800は何を表していますか? なぜ使われているのですか? これが2日間の秒数に対応することはおそらく明らかではありません(1日あたり24 x 60 x 60、つまり86,400秒あります)。

ハードコードされた値を使用する代わりに、 #defineステートメントを使用して値を定義できます。 例えば:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#defineは、名前付き定義をコード内の値に置き換えるプリプロセッサマクロです。 したがって、ヘッダーファイルに#defineがあり、それをどこかにインポートすると、そのファイルで定義されている値のすべてのオカレンスも置き換えられます。

これは、1つの問題を除いて、うまく機能します。 残りの問題を説明するために、次のコードを検討してください。

 #define X = 3 ... CGFloat y = X / 2;

このコードの実行後、 yの値はどうなると思いますか? 1.5と言った場合、あなたは間違っています。 このコードが実行された後、 yは1(1.5ではなく)に等しくなります。 なんで? 答えは、 #defineには型に関する情報がないということです。 したがって、この場合、2つのInt値(3と2)の除算があり、その結果、 Int (つまり、1)がFloatにキャストされます。

これは、定義上、次のように入力される定数を代わりに使用することで回避できます。

 static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expected

よくある間違いその9:Switchステートメントでのデフォルトキーワードの使用

switchステートメントでdefaultキーワードを使用すると、バグや予期しない動作が発生する可能性があります。 Objective-Cの次のコードを検討してください。

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }

Swiftで書かれた同じコード:

 enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } }

このコードは意図したとおりに機能し、管理者ユーザーのみが他のレコードを変更できるようにします。 ただし、レコードを編集できるはずの別のユーザータイプ「manager」を追加するとどうなるでしょうか。 このswitchステートメントを更新するのを忘れると、コードはコンパイルされますが、期待どおりに機能しません。 ただし、開発者が最初からデフォルトのキーワードの代わりに列挙値を使用した場合、見落としはコンパイル時に識別され、テストまたは本番環境に移行する前に修正される可能性があります。 これをObjective-Cで処理する良い方法は次のとおりです。

 typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular, UserTypeManager }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: case UserTypeManager: return YES; case UserTypeRegular: return NO; } }

Swiftで書かれた同じコード:

 enum UserType { case Admin, Regular, Manager } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Manager: fallthrough case .Admin: return true case .Regular: return false } }

よくある間違いその10:ロギングにNSLogを使用する

多くのiOS開発者は、ログ記録のためにアプリでNSLogを使用していますが、ほとんどの場合、これはひどい間違いです。 NSLog関数の説明についてAppleのドキュメントを確認すると、非常に単純であることがわかります。

 void NSLog(NSString *format, ...);

何がうまくいかない可能性がありますか? 実際、何もありません。 ただし、デバイスをXcodeオーガナイザーに接続すると、そこにすべてのデバッグメッセージが表示されます。 この理由だけで、ロギングにNSLogを使用しないでください。不要な内部データを表示するのは簡単で、専門的ではないように見えます。

より良いアプローチは、 NSLogsを構成可能なCocoaLumberjackまたはその他のロギングフレームワークに置き換えることです。

要約

iOSは非常に強力で、急速に進化しているプラ​​ットフォームです。 Appleは、Swift言語を継続的に拡張しながら、iOS自体に新しいハードウェアと機能を導入するために大規模な継続的な取り組みを行っています。

Objective-CとSwiftのスキルを向上させると、優れたiOS開発者になり、最先端のテクノロジーを使用してやりがいのあるプロジェクトに取り組む機会が得られます。

関連: iOS開発者ガイド:Objective-CからLearning Swift