iOS Geliştiricilerinin Yaptıklarını Bilmedikleri En Yaygın 10 Hata

Yayınlanan: 2022-03-11

Buggy uygulamasının App Store tarafından reddedilmesinden daha kötü olan tek şey nedir? Kabul ettirilmesi. Tek yıldızlı incelemeler gelmeye başladığında, iyileşmek neredeyse imkansız. Bu, şirketlere paraya ve geliştiricilerin işlerine mal olur.

iOS şu anda dünyanın en büyük ikinci mobil işletim sistemi. Ayrıca, en son sürümde kullanıcıların %85'inden fazlası ile çok yüksek bir benimseme oranına sahiptir. Tahmin edebileceğiniz gibi, etkileşim düzeyi yüksek kullanıcıların beklentileri yüksektir; uygulamanız veya güncellemeniz kusursuz değilse, bunu duyarsınız.

iOS geliştiricilerine yönelik talebin hızla artmasıyla, birçok mühendis mobil geliştirmeye geçti (Apple'a her gün 1.000'den fazla yeni uygulama gönderiliyor). Ancak gerçek iOS uzmanlığı, temel kodlamanın çok ötesine uzanır. Aşağıda, iOS geliştiricilerinin tuzağına düştüğü 10 yaygın hata ve bunlardan nasıl kaçınabileceğiniz yer almaktadır.

iOS kullanıcılarının %85'i en son işletim sistemi sürümünü kullanıyor. Bu, uygulamanızın veya güncellemenizin kusursuz olmasını bekledikleri anlamına gelir.
Cıvıldamak

Yaygın Hata No. 1: Asenkron Süreçleri Anlamamak

Yeni programcılar arasında çok yaygın bir hata türü, asenkron kodu yanlış kullanmaktır. Tipik bir senaryoyu ele alalım: Bir kullanıcı, tablo görünümünde bir ekran açar. Bazı veriler sunucudan alınır ve bir tablo görünümünde görüntülenir. Daha resmi yazabiliriz:

 @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; }

İlk bakışta her şey doğru görünüyor: Sunucudan veri alıyoruz ve ardından kullanıcı arayüzünü güncelliyoruz. Ancak sorun, verilerin alınmasının eşzamansız bir işlem olması ve yeni verileri hemen döndürmemesidir; bu, yeni verileri almadan önce reloadData çağrılacağı anlamına gelir. Bu hatayı düzeltmek için, 2. satırı blok içinde 1. satırdan hemen sonra hareket ettirmeliyiz.

 @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; }

Ancak, bu kodun hala beklendiği gibi davranmadığı durumlar olabilir, bu da bizi…

Yaygın Hata No. 2: Ana Kuyruktan Farklı Bir Konuda Kullanıcı Arayüzü ile İlgili Kodu Çalıştırma

Bir önceki yaygın hatadan düzeltilmiş bir kod örneği kullandığımızı düşünelim, ancak asenkron işlem başarıyla tamamlandıktan sonra bile tablo görünümümüz yeni verilerle güncellenmedi. Bu kadar basit bir kodda yanlış olan ne olabilir? Bunu anlamak için blok içinde bir kesme noktası ayarlayabilir ve bu bloğun hangi kuyrukta çağrıldığını öğrenebiliriz. Çağrımız, UI ile ilgili tüm kodların gerçekleştirilmesi gereken ana kuyrukta olmadığı için, açıklanan davranışın gerçekleşme olasılığı yüksektir.

Alamofire, AFNetworking ve Haneke gibi en popüler kitaplıklar, eşzamansız bir görev gerçekleştirdikten sonra ana kuyrukta completionBlock çağırmak üzere tasarlanmıştır. Ancak, buna her zaman güvenemezsiniz ve kodunuzu doğru kuyruğa göndermeyi unutmak kolaydır.

UI ile ilgili tüm kodunuzun ana kuyrukta olduğundan emin olmak için, onu o kuyruğa göndermeyi unutmayın:

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

Yaygın Hata No. 3: Eşzamanlılığı Yanlış Anlama ve Çoklu İş Parçacığı

Eşzamanlılık gerçekten keskin bir bıçakla karşılaştırılabilir: Dikkatli veya yeterince deneyimli değilseniz, kendinizi kolayca kesebilirsiniz, ancak onu doğru ve güvenli bir şekilde nasıl kullanacağınızı bildiğinizde son derece yararlı ve verimlidir.

Eşzamanlılığı kullanmaktan kaçınmayı deneyebilirsiniz, ancak ne tür uygulamalar oluşturuyor olursanız olun, onsuz yapamazsınız. Eşzamanlılık, uygulamanız için önemli avantajlar sağlayabilir. Özellikle:

  • Hemen hemen her uygulamanın web hizmetlerine çağrıları vardır (örneğin, bazı ağır hesaplamalar yapmak veya bir veritabanından veri okumak için). Bu görevler ana kuyrukta gerçekleştirilirse, uygulama bir süre donacak ve yanıt vermemesine neden olacaktır. Üstelik bu çok uzun sürerse iOS uygulamayı tamamen kapatacaktır. Bu görevleri başka bir kuyruğa taşımak, kullanıcının işlem gerçekleştirilirken uygulama donmuş görünmeden uygulamayı kullanmaya devam etmesine olanak tanır.
  • Modern iOS cihazlarının birden fazla çekirdeği vardır, öyleyse kullanıcı paralel olarak gerçekleştirilebilecekken görevlerin sırayla tamamlanmasını neden beklesin?

Ancak eşzamanlılığın avantajları, karmaşıklık ve yeniden üretilmesi gerçekten zor olan yarış koşulları gibi budaklı hataları ortaya çıkarma potansiyeli olmadan gelmez.

Gerçek dünyadan bazı örnekleri ele alalım (basitlik için bazı kodların kullanılmadığına dikkat edin).

Dava 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 } } } }

Çok iş parçacıklı kod:

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

İlk bakışta, her şey senkronize edilir ve beklendiği gibi çalışması gerektiği gibi görünür, çünkü ThreadSaveVar counter sarar ve iş parçacığını güvenli hale getirir. Ne yazık ki, bu doğru değildir, çünkü iki iş parçacığı aynı anda artış satırına ulaşabilir ve bunun sonucunda counter.value == someValue asla doğru olmaz. Geçici bir çözüm olarak, artırıldıktan sonra değerini döndüren ThreadSafeCounter yapabiliriz:

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

2. durum

 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 } } }

Bu durumda, diziye erişimi eşitlemek için dispatch_barrier_sync kullanıldı. Bu, erişim senkronizasyonunu sağlamak için favori bir kalıptır. Ne yazık ki, bu kod, struct'a her öğe eklediğimizde bir kopya oluşturduğunu ve böylece her seferinde yeni bir eşitleme kuyruğuna sahip olduğunu hesaba katmaz.

Burada ilk bakışta doğru görünse de beklendiği gibi çalışmayabilir. Ayrıca test etmek ve hata ayıklamak için çok çalışma gerektirir, ancak sonunda uygulamanızın hızını ve yanıt verme hızını artırabilirsiniz.

4 Numaralı Ortak Hata: Değişken Nesnelerin Tuzaklarını Bilmemek

Swift, değer türleriyle ilgili hatalardan kaçınmada çok yardımcı olur, ancak yine de Objective-C kullanan birçok geliştirici var. Değişken nesneler çok tehlikelidir ve gizli sorunlara yol açabilir. Değişmez nesnelerin işlevlerden döndürülmesi gerektiği iyi bilinen bir kuraldır, ancak çoğu geliştirici nedenini bilmiyor. Aşağıdaki kodu ele alalım:

 // 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

NSMutableArray , NSArray öğesinin bir alt sınıfı olduğundan yukarıdaki kod doğrudur. Peki bu kodda ne yanlış gidebilir?

İlk ve en belirgin şey, başka bir geliştiricinin gelip aşağıdakileri yapabilmesidir:

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

Bu kod sınıfınızı karıştıracaktır. Ancak bu durumda, bu bir kod kokusudur ve parçaları almak o geliştiriciye bırakılmıştır.

Ancak durum çok daha kötü ve beklenmedik bir davranış sergiliyor:

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

Buradaki beklenti, [newChildBoxes count] > [childBoxes count] , ama ya değilse? O zaman sınıf iyi tasarlanmamıştır çünkü zaten döndürülmüş bir değeri değiştirir. Eşitsizliğin doğru olmaması gerektiğine inanıyorsanız, UIView ve [view subviews] ile denemeler yapmayı deneyin.

Neyse ki, ilk örnekteki alıcıyı yeniden yazarak kodumuzu kolayca düzeltebiliriz:

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

5 No'lu Genel Hata: iOS NSDictionary Dahili Olarak Nasıl Çalıştığını Anlamamak

Daha önce özel bir sınıf ve NSDictionary ile çalıştıysanız, sözlük anahtarı olarak NSCopying uymuyorsa sınıfınızı kullanamayacağınızı fark edebilirsiniz. Çoğu geliştirici, Apple'ın neden bu kısıtlamayı eklediğini kendilerine hiç sormadı. Apple neden anahtarı kopyalayıp orijinal nesne yerine bu kopyayı kullanıyor?

Bunu anlamanın anahtarı, NSDictionary dahili olarak nasıl çalıştığını bulmaktır. Teknik olarak, bu sadece bir karma tablo. Anahtar için bir nesne eklerken yüksek düzeyde nasıl çalıştığını hızlıca özetleyelim (basitlik için tablo yeniden boyutlandırma ve performans optimizasyonu burada atlanmıştır):

Adım 1: hash(Key) hesaplar. Adım 2: Hash'e göre nesneyi koyacak bir yer arar. Genellikle bu, sözlük uzunluğu ile karma değerinin modülü alınarak yapılır. Ortaya çıkan dizin daha sonra Anahtar/Değer çiftini depolamak için kullanılır. Adım 3: O konumda herhangi bir nesne yoksa, bağlantılı bir liste oluşturur ve kaydımızı (nesne ve anahtar) saklar. Aksi takdirde, kaydı listenin sonuna ekler.

Şimdi sözlükten bir kaydın nasıl getirildiğini açıklayalım:

Adım 1: hash(Key) hesaplar. Adım 2: Bir Anahtarı hash ile arar. Veri yoksa, nil döndürülür. Adım 3: Bağlantılı bir liste varsa, [storedkey isEqual:Key] kadar Object üzerinden yinelenir.

Kaputun altında neler olduğuna dair bu anlayışla, iki sonuç çıkarılabilir:

  1. Anahtarın karması değişirse, kayıt başka bir bağlantılı listeye taşınmalıdır.
  2. Anahtarlar benzersiz olmalıdır.

Bunu basit bir sınıf üzerinde inceleyelim:

 @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

Şimdi NSDictionary anahtarları kopyalamadığını hayal edin:

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

Ah! Orada bir yazım hatası var! Hadi düzeltelim!

 p.name = @"Jon Snow";

Sözlüğümüze ne olmalı? İsim mutasyona uğradığı için artık farklı bir hash'imiz var. Şimdi nesnemiz yanlış yerde yatıyor (sözlük veri değişikliğini bilmediğinden hala eski karma değerine sahip) ve sözlükte veri aramak için hangi karma değeri kullanmamız gerektiği gerçekten açık değil. Daha da kötü bir durum olabilir. Sözlüğümüzde zaten 5 dereceli bir “Jon Snow” olduğunu hayal edin. Sözlük aynı Anahtar için iki farklı değerle sonuçlanacaktı.

Gördüğünüz gibi, NSDictionary değişken anahtarlara sahip olmaktan kaynaklanabilecek birçok sorun var. Bu tür sorunları önlemek için en iyi uygulama, nesneyi saklamadan önce kopyalamak ve özellikleri copy olarak işaretlemektir. Bu uygulama aynı zamanda sınıfınızı tutarlı tutmanıza da yardımcı olacaktır.

Yaygın Hata No. 6: XIB'ler Yerine Storyboard'ları Kullanmak

Çoğu yeni iOS geliştiricisi, Apple'ın önerisini takip eder ve kullanıcı arayüzü için varsayılan olarak storyboard'ları kullanır. Bununla birlikte, storyboard kullanmanın birçok dezavantajı ve yalnızca birkaç (tartışmalı) avantajı vardır.

Storyboard dezavantajları şunları içerir:

  1. Birkaç ekip üyesi için bir storyboard'u değiştirmek gerçekten zor. Teknik olarak, birçok storyboard kullanabilirsiniz, ancak bu durumda tek avantaj, storyboard'da kontrolörler arasında segue olmasını mümkün kılmaktır.
  2. Film şeridindeki denetleyiciler ve segue adları dizelerdir, bu nedenle tüm bu dizeleri kodunuz boyunca yeniden girmeniz (ve bir gün kıracaksınız) veya büyük bir storyboard sabitleri listesi tutmanız gerekir. SBConstants'ı kullanabilirsiniz, ancak storyboard'da yeniden adlandırmak hala kolay bir iş değil.
  3. Storyboard'lar sizi modüler olmayan bir tasarıma zorlar. Bir storyboard ile çalışırken, görüşlerinizi yeniden kullanılabilir hale getirmek için çok az teşvik vardır. Bu, minimum geçerli ürün (MVP) veya hızlı UI prototiplemesi için kabul edilebilir olabilir, ancak gerçek uygulamalarda aynı görünümü uygulamanızda birkaç kez kullanmanız gerekebilir.

Storyboard (tartışmalı) avantajları:

  1. Tüm uygulama navigasyonu bir bakışta görülebilir. Ancak, gerçek uygulamalar, farklı yönlere bağlı ondan fazla kontrolöre sahip olabilir. Bu tür bağlantılara sahip storyboard'lar bir iplik yumağı gibi görünür ve herhangi bir üst düzey veri akışı anlayışı vermez.
  2. Statik tablolar. Aklıma gelen tek gerçek avantaj bu. Sorun şu ki, uygulama geliştirme sırasında statik tabloların yüzde 90'ı dinamik tablolara dönüşme eğiliminde ve dinamik bir tablo XIB'ler tarafından daha kolay işlenebilir.

7 No'lu Ortak Hata: Kafa Karıştırıcı Nesne ve İşaretçi Karşılaştırması

İki nesneyi karşılaştırırken iki eşitlik düşünebiliriz: işaretçi ve nesne eşitliği.

İşaretçi eşitliği, her iki işaretçinin de aynı nesneyi gösterdiği bir durumdur. Objective-C'de, iki işaretçiyi karşılaştırmak için == operatörünü kullanırız. Nesne eşitliği, bir veritabanındaki aynı kullanıcı gibi, iki nesnenin mantıksal olarak özdeş iki nesneyi temsil ettiği bir durumdur. Objective-C'de, iki nesneyi karşılaştırmak için isEqual veya daha da iyisi türe özgü isEqualToString , isEqualToDate vb. operatörler kullanıyoruz.

Aşağıdaki kodu göz önünde bulundurun:

 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); }

Bu kodu çalıştırdığımızda konsola ne yazdırılacak? Hem a hem de b nesneleri bellekte aynı nesneye işaret ettiğinden, a is equal to b elde ederiz.

Ama şimdi 2. satırı şu şekilde değiştirelim:

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

Şimdi, bu nesneler aynı değerlere sahip olsa bile, bu işaretçiler şimdi farklı nesneleri işaret ettiğinden, a is NOT equal to b elde ederiz.

Bu sorun, isEqual güvenerek veya belirli işlevler yazarak önlenebilir. Kod örneğimizde, her zaman düzgün çalışması için 3. satırı aşağıdaki kodla değiştirmeliyiz:

 if ([a isEqual:b]) {

8 Numaralı Genel Hata: Sabit Kodlanmış Değerleri Kullanma

Sabit kodlanmış değerlerle ilgili iki temel sorun vardır:

  1. Çoğu zaman neyi temsil ettikleri net değildir.
  2. Kodda birden çok yerde kullanılmaları gerektiğinde yeniden girilmeleri (veya kopyalanıp yapıştırılmaları) gerekir.

Aşağıdaki örneği göz önünde bulundurun:

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

172800 neyi temsil eder? Neden kullanılıyor? Bunun 2 gündeki saniye sayısına karşılık geldiği muhtemelen açık değildir (bir günde 24 x 60 x 60 veya 86.400 saniye vardır).

Sabit kodlanmış değerler kullanmak yerine, #define ifadesini kullanarak bir değer tanımlayabilirsiniz. Örneğin:

 #define SECONDS_PER_DAY 86400 #define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

#define , adlandırılmış tanımı koddaki değeriyle değiştiren bir önişlemci makrosudur. Bu nedenle, bir başlık dosyasında #define varsa ve onu bir yere aktarırsanız, o dosyada tanımlanan değerin tüm oluşumları da değiştirilecektir.

Bu, bir sorun dışında iyi çalışıyor. Kalan sorunu göstermek için aşağıdaki kodu göz önünde bulundurun:

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

Bu kod yürütüldükten sonra y değerinin ne olmasını beklersiniz? 1.5 dediyseniz yanılıyorsunuz. Bu kod yürütüldükten sonra y 1'e (1.5 değil ) eşit olacaktır. Niye ya? Cevap, #define tür hakkında hiçbir bilgisinin olmamasıdır. Yani, bizim durumumuzda, iki Int değerinin (3 ve 2) bir bölümü var, bu da bir Int (yani, 1) ile sonuçlanır ve daha sonra bir Float öğesine dönüştürülür.

Bunun yerine, tanım gereği yazılan sabitler kullanılarak bu önlenebilir:

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

9 No'lu Genel Hata: Bir Switch İfadesinde Varsayılan Anahtar Kelimeyi Kullanma

Bir switch ifadesinde default anahtar sözcüğünü kullanmak, hatalara ve beklenmeyen davranışlara yol açabilir. Objective-C'de aşağıdaki kodu göz önünde bulundurun:

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

Swift'de yazılmış aynı kod:

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

Bu kod amaçlandığı gibi çalışır ve yalnızca yönetici kullanıcıların diğer kayıtları değiştirmesine izin verir. Ancak, kayıtları da düzenleyebilmesi gereken başka bir kullanıcı türü olan “yönetici” eklesek ne olabilir? Bu switch ifadesini güncellemeyi unutursak kod derlenir ancak beklendiği gibi çalışmaz. Bununla birlikte, geliştirici en baştan varsayılan anahtar kelime yerine enum değerlerini kullandıysa, gözetim derleme zamanında belirlenecek ve teste veya üretime geçmeden önce düzeltilebilir. Bunu Objective-C'de ele almanın iyi bir yolu:

 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'de yazılmış aynı kod:

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

Yaygın Hata No. 10: Günlük Kayıt İçin NSLog Kullanımı

Birçok iOS geliştiricisi, günlük kaydı için uygulamalarında NSLog kullanır, ancak çoğu zaman bu korkunç bir hatadır. NSLog işlev açıklaması için Apple belgelerini kontrol edersek, bunun çok basit olduğunu göreceğiz:

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

Ne yanlış gidebilir ki? Aslında, hiçbir şey. Ancak, cihazınızı Xcode düzenleyicisine bağlarsanız, tüm hata ayıklama mesajlarınızı orada görürsünüz. Yalnızca bu nedenle, NSLog günlük kaydı için asla kullanmamalısınız: bazı istenmeyen dahili verileri göstermek kolaydır, ayrıca profesyonelce görünmemektedir.

Daha iyi bir yaklaşım, NSLogs yapılandırılabilir CocoaLumberjack veya başka bir kayıt çerçevesi ile değiştirmektir.

Sarmak

iOS çok güçlü ve hızla gelişen bir platformdur. Apple, iOS'un kendisi için yeni donanım ve özellikler sunmak için sürekli olarak büyük bir çaba sarf ederken, aynı zamanda Swift dilini sürekli olarak genişletiyor.

Objective-C ve Swift becerilerinizi geliştirmek, sizi harika bir iOS geliştiricisi yapacak ve en son teknolojileri kullanarak zorlu projeler üzerinde çalışma fırsatları sunacaktır.

İlgili: Bir iOS Geliştirici Kılavuzu: Objective-C'den Learning Swift'e