10 Kesalahan Paling Umum yang Tidak Diketahui Pengembang iOS
Diterbitkan: 2022-03-11Apa satu-satunya hal yang lebih buruk daripada aplikasi buggy ditolak oleh App Store? Setelah itu diterima. Setelah ulasan satu bintang mulai bergulir, hampir tidak mungkin untuk dipulihkan. Ini biaya uang perusahaan dan pengembang pekerjaan mereka.
iOS sekarang menjadi sistem operasi seluler terbesar kedua di dunia. Ini juga memiliki tingkat adopsi yang sangat tinggi, dengan lebih dari 85% pengguna menggunakan versi terbaru. Seperti yang Anda harapkan, pengguna yang sangat terlibat memiliki harapan yang tinggi—jika aplikasi atau pembaruan Anda tidak sempurna, Anda akan mendengarnya.
Dengan permintaan untuk pengembang iOS yang terus meroket, banyak insinyur telah beralih ke pengembangan seluler (lebih dari 1.000 aplikasi baru dikirimkan ke Apple setiap hari). Tetapi keahlian iOS sejati jauh melampaui pengkodean dasar. Di bawah ini adalah 10 kesalahan umum yang menjadi mangsa pengembang iOS, dan bagaimana Anda dapat menghindarinya.
Kesalahan Umum No. 1: Tidak Memahami Proses Asinkron
Jenis kesalahan yang sangat umum di antara programmer baru adalah menangani kode asinkron dengan tidak benar. Mari kita pertimbangkan skenario tipikal: Seorang pengguna membuka layar dengan tampilan tabel. Beberapa data diambil dari server dan ditampilkan dalam tampilan tabel. Kita dapat menulisnya secara lebih formal:
@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; } Sekilas, semuanya tampak benar: Kami mengambil data dari server dan kemudian memperbarui UI. Namun, masalahnya adalah mengambil data adalah proses asinkron dan tidak akan segera mengembalikan data baru, yang berarti reloadData akan dipanggil sebelum menerima data baru. Untuk memperbaiki kesalahan ini, kita harus memindahkan baris #2 tepat setelah baris #1 di dalam blok.
@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; }Namun, mungkin ada situasi di mana kode ini masih tidak berperilaku seperti yang diharapkan, yang membawa kita ke …
Kesalahan Umum No. 2: Menjalankan Kode terkait UI pada Thread Selain Antrian Utama
Mari kita bayangkan kita menggunakan contoh kode yang diperbaiki dari kesalahan umum sebelumnya, tetapi tampilan tabel kita masih belum diperbarui dengan data baru bahkan setelah proses asinkron berhasil diselesaikan. Apa yang mungkin salah dengan kode sederhana seperti itu? Untuk memahaminya, kita dapat mengatur breakpoint di dalam blok dan mencari tahu di antrian mana blok ini dipanggil. Ada kemungkinan besar perilaku yang dijelaskan terjadi karena panggilan kita tidak berada dalam antrian utama, di mana semua kode terkait UI harus dijalankan.
Pustaka paling populer—seperti Alamofire, AFNetworking, dan Haneke—dirancang untuk memanggil completionBlock pada antrian utama setelah melakukan tugas asinkron. Namun, Anda tidak selalu dapat mengandalkan ini dan mudah lupa untuk mengirimkan kode Anda ke antrean yang benar.
Untuk memastikan semua kode terkait UI Anda ada di antrean utama, jangan lupa untuk mengirimkannya ke antrean itu:
dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadData]; });Kesalahan Umum No. 3: Kesalahpahaman Concurrency dan Multithreading
Concurrency dapat dibandingkan dengan pisau yang sangat tajam: Anda dapat dengan mudah memotong diri sendiri jika Anda tidak berhati-hati atau cukup berpengalaman, tetapi itu sangat berguna dan efisien setelah Anda tahu cara menggunakannya dengan benar dan aman.
Anda dapat mencoba menghindari penggunaan konkurensi, tetapi apa pun jenis aplikasi yang Anda buat, ada kemungkinan besar Anda tidak dapat melakukannya tanpanya. Concurrency dapat memiliki manfaat yang signifikan untuk aplikasi Anda. Terutama:
- Hampir setiap aplikasi memiliki panggilan ke layanan web (misalnya, untuk melakukan beberapa perhitungan berat atau membaca data dari database). Jika tugas-tugas ini dilakukan pada antrian utama, aplikasi akan membeku selama beberapa waktu, membuatnya tidak responsif. Selain itu, jika ini memakan waktu terlalu lama, iOS akan mematikan aplikasi sepenuhnya. Memindahkan tugas-tugas ini ke antrean lain memungkinkan pengguna untuk terus menggunakan aplikasi saat operasi sedang dilakukan tanpa aplikasi tampak membeku.
- Perangkat iOS modern memiliki lebih dari satu inti, jadi mengapa pengguna harus menunggu tugas selesai secara berurutan ketika dapat dilakukan secara paralel?
Tetapi keuntungan dari konkurensi tidak datang tanpa kerumitan dan potensi untuk memperkenalkan bug degil, seperti kondisi balapan yang sangat sulit untuk direproduksi.
Mari kita pertimbangkan beberapa contoh dunia nyata (perhatikan bahwa beberapa kode dihilangkan untuk kesederhanaan).
Kasus 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 } } } }Kode multithread:
let counter = ThreadSafeVar<Int>(value: 0) // this code might be called from several threads counter.value += 1 if (counter.value == someValue) { // do something } Sekilas, semuanya disinkronkan dan tampak seolah-olah berfungsi seperti yang diharapkan, karena ThreadSaveVar membungkus counter dan membuatnya aman. Sayangnya, ini tidak benar, karena dua utas mungkin mencapai garis kenaikan secara bersamaan dan sebagai hasilnya counter.value == someValue tidak akan pernah menjadi true. Sebagai solusinya, kita dapat membuat ThreadSafeCounter yang mengembalikan nilainya setelah bertambah:
class ThreadSafeCounter { private var value: Int32 = 0 func increment() -> Int { return Int(OSAtomicIncrement32(&value)) } }Kasus 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 } } } Dalam hal ini, dispatch_barrier_sync digunakan untuk menyinkronkan akses ke array. Ini adalah pola favorit untuk memastikan sinkronisasi akses. Sayangnya, kode ini tidak memperhitungkan bahwa struct membuat salinan setiap kali kita menambahkan item ke dalamnya, sehingga memiliki antrian sinkronisasi baru setiap kali.
Di sini, meskipun terlihat benar pada pandangan pertama, mungkin tidak berfungsi seperti yang diharapkan. Ini juga membutuhkan banyak pekerjaan untuk menguji dan men-debugnya, tetapi pada akhirnya, Anda dapat meningkatkan kecepatan dan daya tanggap aplikasi Anda.
Kesalahan Umum No. 4: Tidak Mengetahui Jebakan Objek yang Dapat Berubah
Swift sangat membantu dalam menghindari kesalahan dengan tipe nilai, tetapi masih banyak pengembang yang menggunakan Objective-C. Objek yang bisa berubah sangat berbahaya dan dapat menyebabkan masalah tersembunyi. Ini adalah aturan yang terkenal bahwa objek yang tidak dapat diubah harus dikembalikan dari fungsi, tetapi sebagian besar pengembang tidak tahu mengapa. Mari kita perhatikan kode berikut:
// 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 Kode di atas benar, karena NSMutableArray adalah subclass dari NSArray . Jadi apa yang bisa salah dengan kode ini?
Hal pertama dan paling jelas adalah bahwa pengembang lain mungkin datang dan melakukan hal berikut:
NSArray<Box *> *childBoxes = [box boxes]; if ([childBoxes isKindOfClass:[NSMutableArray class]]) { // add more boxes to childBoxes }Kode ini akan mengacaukan kelas Anda. Tapi dalam hal ini, itu adalah bau kode, dan terserah pengembang untuk mengambil potongannya.
Namun, inilah kasusnya, yang jauh lebih buruk dan menunjukkan perilaku yang tidak terduga:
Box *box = [[Box alloc] init]; NSArray<Box *> *childBoxes = [box boxes]; [box addBox:[[Box alloc] init]]; NSArray<Box *> *newChildBoxes = [box boxes]; Harapannya di sini adalah [newChildBoxes count] > [childBoxes count] , tetapi bagaimana jika tidak? Kemudian kelas tidak dirancang dengan baik karena memutasikan nilai yang sudah dikembalikan. Jika Anda yakin bahwa ketidaksetaraan seharusnya tidak benar, cobalah bereksperimen dengan UIView dan [view subviews] .
Untungnya, kami dapat dengan mudah memperbaiki kode kami, dengan menulis ulang pengambil dari contoh pertama:
- (NSArray *)boxes { return [self.m_boxes copy]; } Kesalahan Umum No. 5: Tidak Memahami Bagaimana iOS NSDictionary Bekerja Secara Internal
Jika Anda pernah bekerja dengan kelas khusus dan NSDictionary , Anda mungkin menyadari bahwa Anda tidak dapat menggunakan kelas Anda jika tidak sesuai dengan NSCopying sebagai kunci kamus. Sebagian besar pengembang tidak pernah bertanya pada diri sendiri mengapa Apple menambahkan pembatasan itu. Mengapa Apple menyalin kunci dan menggunakan salinan itu alih-alih objek aslinya?
Kunci untuk memahami ini adalah mencari tahu bagaimana NSDictionary bekerja secara internal. Secara teknis, itu hanya tabel hash. Mari kita cepat rekap cara kerjanya pada tingkat tinggi sambil menambahkan objek untuk kunci (pengubahan ukuran tabel dan pengoptimalan kinerja dihilangkan di sini untuk kesederhanaan):
Langkah 1: Ini menghitung
hash(Key). Langkah 2: Berdasarkan hash, ia mencari tempat untuk meletakkan objek. Biasanya, ini dilakukan dengan mengambil modulus dari nilai hash dengan panjang kamus. Indeks yang dihasilkan kemudian digunakan untuk menyimpan pasangan Kunci/Nilai. Langkah 3: Jika tidak ada objek di lokasi itu, itu membuat daftar tertaut dan menyimpan catatan kita (objek dan kunci). Jika tidak, itu akan menambahkan catatan ke akhir daftar.
Sekarang, mari kita jelaskan bagaimana record diambil dari kamus:
Langkah 1: Ini menghitung
hash(Key). Langkah 2: Ini mencari Kunci dengan hash. Jika tidak ada data,nildikembalikan. Langkah 3: Jika ada linked list, iterasi melalui Object sampai[storedkey isEqual:Key].
Dengan pemahaman tentang apa yang terjadi di bawah tenda, dua kesimpulan dapat ditarik:
- Jika hash kunci berubah, rekaman harus dipindahkan ke daftar tertaut lainnya.
- Kunci harus unik.
Mari kita periksa ini di kelas sederhana:
@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 Sekarang bayangkan NSDictionary tidak menyalin kunci:
NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init]; Person *p = [[Person alloc] init]; p.name = @"Job Snow"; gotCharactersRating[p] = @10;Oh! Kami memiliki kesalahan ketik di sana! Mari kita perbaiki!
p.name = @"Jon Snow";Apa yang harus terjadi dengan kamus kita? Karena namanya telah bermutasi, kami sekarang memiliki hash yang berbeda. Sekarang objek kita terletak di tempat yang salah (masih memiliki nilai hash lama, karena kamus tidak mengetahui tentang perubahan data), dan tidak terlalu jelas hash apa yang harus kita gunakan untuk mencari data di kamus. Mungkin ada kasus yang lebih buruk. Bayangkan jika kita sudah memiliki "Jon Snow" di kamus kita dengan peringkat 5. Kamus akan berakhir dengan dua nilai berbeda untuk Kunci yang sama.
Seperti yang Anda lihat, ada banyak masalah yang dapat muncul karena memiliki kunci yang dapat diubah di NSDictionary . Praktik terbaik untuk menghindari masalah seperti itu adalah menyalin objek sebelum menyimpannya, dan menandai properti sebagai copy . Latihan ini juga akan membantu Anda menjaga kelas tetap konsisten.
Kesalahan Umum No. 6: Menggunakan Papan Cerita, Bukan XIB
Sebagian besar pengembang iOS baru mengikuti saran Apple dan menggunakan storyboard secara default untuk UI. Namun, ada banyak kekurangan dan hanya sedikit (dapat diperdebatkan) keuntungan dalam menggunakan storyboard.
Kelemahan storyboard meliputi:
- Sangat sulit untuk memodifikasi storyboard untuk beberapa anggota tim. Secara teknis, Anda dapat menggunakan banyak storyboard, tetapi satu-satunya keuntungan, dalam hal ini, adalah memungkinkan adanya pemisahan antara pengontrol di storyboard.
- Pengontrol dan nama segues dari storyboard adalah string, jadi Anda harus memasukkan kembali semua string tersebut di seluruh kode Anda (dan suatu hari Anda akan memecahkannya), atau mempertahankan daftar besar konstanta storyboard. Anda bisa menggunakan SBConstants, tetapi mengganti nama di storyboard masih bukan tugas yang mudah.
- Storyboard memaksa Anda menjadi desain non-modular. Saat bekerja dengan storyboard, hanya ada sedikit insentif untuk membuat tampilan Anda dapat digunakan kembali. Ini mungkin dapat diterima untuk produk yang layak minimum (MVP) atau prototipe UI cepat, tetapi dalam aplikasi nyata Anda mungkin perlu menggunakan tampilan yang sama beberapa kali di seluruh aplikasi Anda.
Keuntungan storyboard (bisa diperdebatkan):
- Seluruh navigasi aplikasi dapat dilihat sekilas. Namun, aplikasi nyata dapat memiliki lebih dari sepuluh pengontrol, terhubung ke arah yang berbeda. Papan cerita dengan koneksi seperti itu terlihat seperti bola benang dan tidak memberikan pemahaman tingkat tinggi tentang aliran data.
- Tabel statis. Ini adalah satu-satunya keuntungan nyata yang dapat saya pikirkan. Masalahnya adalah 90 persen tabel statis cenderung berubah menjadi tabel dinamis selama evolusi aplikasi dan tabel dinamis dapat lebih mudah ditangani oleh XIB.
Kesalahan Umum No. 7: Membingungkan Objek dan Perbandingan Pointer
Saat membandingkan dua objek, kita dapat mempertimbangkan dua kesetaraan: penunjuk dan kesetaraan objek.
Kesetaraan pointer adalah situasi ketika kedua pointer menunjuk ke objek yang sama. Di Objective-C, kami menggunakan operator == untuk membandingkan dua pointer. Kesetaraan objek adalah situasi ketika dua objek mewakili dua objek yang identik secara logis, seperti pengguna yang sama dari database. Di Objective-C, kami menggunakan isEqual , atau bahkan lebih baik, ketik isEqualToString , isEqualToDate , dll. operator tertentu untuk membandingkan dua objek.
Perhatikan kode berikut:
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); } Apa yang akan dicetak ke konsol ketika kita menjalankan kode itu? Kita akan mendapatkan a is equal to b , karena kedua objek a dan b menunjuk ke objek yang sama di memori.
Tapi sekarang mari kita ubah baris 2 menjadi:
NSString *b = [[@"a" mutableCopy] copy]; Sekarang kita mendapatkan a is NOT equal to b karena pointer ini sekarang menunjuk ke objek yang berbeda meskipun objek tersebut memiliki nilai yang sama.
Masalah ini dapat dihindari dengan mengandalkan isEqual , atau ketik fungsi tertentu. Dalam contoh kode kami, kami harus mengganti baris 3 dengan kode berikut agar selalu berfungsi dengan baik:
if ([a isEqual:b]) {Kesalahan Umum No. 8: Menggunakan Nilai Hardcoded
Ada dua masalah utama dengan nilai hard-coded:
- Seringkali tidak jelas apa yang mereka wakili.
- Mereka perlu dimasukkan kembali (atau disalin dan ditempel) ketika mereka perlu digunakan di banyak tempat dalam kode.
Perhatikan contoh berikut:
if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) { // do something } or [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"]; ... [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];Apa yang diwakili oleh 172800? Mengapa itu digunakan? Mungkin tidak jelas bahwa ini sesuai dengan jumlah detik dalam 2 hari (ada 24 x 60 x 60, atau 86.400, detik dalam sehari).
Daripada menggunakan nilai hard-coded, Anda bisa mendefinisikan nilai menggunakan pernyataan #define . Sebagai contoh:
#define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell" #define adalah makro praprosesor yang menggantikan definisi bernama dengan nilainya dalam kode. Jadi, jika Anda memiliki #define di file header dan mengimpornya di suatu tempat, semua kemunculan nilai yang ditentukan dalam file itu juga akan diganti.
Ini bekerja dengan baik, kecuali untuk satu masalah. Untuk mengilustrasikan masalah yang tersisa, pertimbangkan kode berikut:
#define X = 3 ... CGFloat y = X / 2; Apa yang Anda harapkan dari nilai y setelah kode ini dijalankan? Jika Anda mengatakan 1,5, Anda salah. y akan sama dengan 1 ( bukan 1,5) setelah kode ini dijalankan. Mengapa? Jawabannya adalah #define tidak memiliki informasi tentang tipenya. Jadi, dalam kasus kami, kami memiliki pembagian dua nilai Int (3 dan 2), yang menghasilkan Int (yaitu, 1) yang kemudian dilemparkan ke Float .
Ini dapat dihindari dengan menggunakan konstanta yang, menurut definisi, diketik:
static const CGFloat X = 3; ... CGFloat y = X / 2; // y will now equal 1.5, as expectedKesalahan Umum No. 9: Menggunakan Kata Kunci Default dalam Pernyataan Switch
Menggunakan kata kunci default dalam pernyataan switch dapat menyebabkan bug dan perilaku tak terduga. Pertimbangkan kode berikut di Objective-C:
typedef NS_ENUM(NSUInteger, UserType) { UserTypeAdmin, UserTypeRegular }; - (BOOL)canEditUserWithType:(UserType)userType { switch (userType) { case UserTypeAdmin: return YES; default: return NO; } }Kode yang sama ditulis dalam Swift:
enum UserType { case Admin, Regular } func canEditUserWithType(type: UserType) -> Bool { switch(type) { case .Admin: return true default: return false } } Kode ini berfungsi sebagaimana dimaksud, memungkinkan hanya pengguna admin yang dapat mengubah catatan lain. Namun, apa yang mungkin terjadi jika kita menambahkan tipe pengguna lain, "manajer", yang seharusnya dapat mengedit catatan juga? Jika kita lupa memperbarui pernyataan switch ini, kode akan dikompilasi, tetapi tidak akan berfungsi seperti yang diharapkan. Namun, jika pengembang menggunakan nilai enum alih-alih kata kunci default sejak awal, pengawasan akan diidentifikasi pada waktu kompilasi, dan dapat diperbaiki sebelum pengujian atau produksi. Berikut adalah cara yang baik untuk menangani ini di 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; } }Kode yang sama ditulis dalam 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 } } Kesalahan Umum No. 10: Menggunakan NSLog untuk Logging
Banyak pengembang iOS menggunakan NSLog di aplikasi mereka untuk masuk, tetapi sebagian besar waktu ini adalah kesalahan besar. Jika kita memeriksa dokumentasi Apple untuk deskripsi fungsi NSLog , kita akan melihatnya sangat sederhana:
void NSLog(NSString *format, ...); Apa yang mungkin salah dengan itu? Bahkan, tidak ada. Namun, jika Anda menghubungkan perangkat Anda ke penyelenggara Xcode, Anda akan melihat semua pesan debug Anda di sana. Untuk alasan ini saja, Anda tidak boleh menggunakan NSLog untuk logging: sangat mudah untuk menampilkan beberapa data internal yang tidak diinginkan, ditambah lagi terlihat tidak profesional.
Pendekatan yang lebih baik adalah mengganti NSLogs dengan CocoaLumberjack yang dapat dikonfigurasi atau kerangka kerja logging lainnya.
Bungkus
iOS adalah platform yang sangat kuat dan berkembang pesat. Apple melakukan upaya besar-besaran yang berkelanjutan untuk memperkenalkan perangkat keras dan fitur baru untuk iOS itu sendiri, sementara juga terus memperluas bahasa Swift.
Meningkatkan keterampilan Objective-C dan Swift Anda akan menjadikan Anda pengembang iOS yang hebat dan menawarkan peluang untuk mengerjakan proyek yang menantang menggunakan teknologi mutakhir.

