Quantcast
Channel: Nelson 寫些 iOS 開發的東東
Viewing all 20 articles
Browse latest View live

如何正確設定 AFNetworking 的安全連線

$
0
0

TL;DR

前一陣子 AFNetworking被爆出存在安全性漏洞,它們也針對這件事情發出聲明稿

簡單的說,就是建議開發者使用最新版的 AFNetworking,並且啟用安全連線。不過它們也承認這一部份的說明文件沒有寫得很齊全,所以困擾了不少開發者。

今天花了一點時間研究,順手把它記錄下來。安全相關的東西不是我的專長,所以如果有任何錯誤的地方,請留言告訴我。

取得安全憑證

1. 確認有使用安全連線

如果你跟遠端伺服器是透過 HTTP 連線,那就不是安全連線,如果是 HTTPS 那就是安全連線。

2. 準備好網站的安全憑證

接下來我們需要憑證檔(Certification file),它的副檔名是 .cer,你可以跟你們的網站管理員詢問,通常他們都知道怎麼拿到這個檔案。

如果你的網站管理員沒有 .cer檔,只有 .crt檔,那你可以透過以下這行指令轉檔,要注意的是它是採用 DER編碼格式(請自行將 myWebsite替換成你想要的名字):

openssl x509 -in myWebsite.crt -out myWebsite.cer -outform der

如果很不幸的,你的網站管理員連 .crt檔都沒有,那你也可以使用下列這一整行指令從你們的網站取得憑證(請自行將 www.mywebsite.com替換成你們的網址):

openssl s_client -connect www.mywebsite.com:443 </dev/null 2>/dev/null | openssl x509 -outform DER > myWebsite.cer

現在你有一個憑證檔了。

3. 將憑證加入你的專案

將你的憑證拖拉放到 Xcode 專案底下,記得要把 Copy items if neededAdd to targets打勾。

好了,事前準備都做完,接著我們來設定 AFNetworking。

設定 AFNetworking

1. Pinning Mode

AFNetworking 的安全相關設定放在 AFSecurityPolicy,它定義了三種 SSL Pinning Mode:

/*
 `AFSSLPinningModeNone`
 Do not used pinned certificates to validate servers.

 `AFSSLPinningModePublicKey`
 Validate host certificates against public keys of pinned certificates.

 `AFSSLPinningModeCertificate`
 Validate host certificates against pinned certificates.
*/
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,
    AFSSLPinningModePublicKey,
    AFSSLPinningModeCertificate,
};

關於 pinning mode 詳細的說明可以參考這篇文章,簡單的說就是你可以將憑證跟你的 APP 一起打包,藉由此機制來避免中間人偽造憑證的風險。

  • AFSSLPinningModeNone : 你不必將憑證跟你的 APP 一起打包,完全信任伺服器的憑證
  • AFSSLPinningModeCertificate : 比對伺服器憑證跟你的憑證是否完全匹配
  • AFSSLPinningModePublicKey : 只比對伺服器憑證的 public key 跟你的憑證的 public key 是否匹配

那要選用何種模式比較好呢?

AFSSLPinningModeCertificate比較安全但也比較麻煩,它會比對你打包的憑證跟伺服器的憑證是否一致。因為你的憑證是跟 APP 一起打包的,這也就代表說如果你的憑證過期了或是變動了,你就得出一版新的 APP 而且舊版 APP 的憑證就失效了。你也可以在每次 APP 啟動時,就自動連到某個伺服器下載最新的憑證,不過此時這個下載連線就會是有風險的。

AFSSLPinningModePublicKey則是只有比對憑證裡的 public key,所以即使伺服器憑證有所變動,只要 public key 不變,就能通過驗證。

所以如果你能確保每個使用者總是使用最新版本的 APP(例如是公司企業內部專用的),那就可以考慮 AFSSLPinningModeCertificate,否則的話選擇 AFSSLPinningModePublicKey是比較實際的作法。

2. Certification Chain

/**
 Whether to evaluate an entire SSL certificate chain, or just the leaf certificate. Defaults to `YES`.
 */
@property (nonatomic, assign) BOOL validatesCertificateChain;

你的憑證是某家機構發出的,該機構的憑證是由更高一級的機構發出的,一路往上追,最後會到一個根機構,這樣一串由各機構發出的憑證稱為 certification chain。

如果你把 validatesCertificateChain設為 YES,那就得把這一整串憑證都打包進你的 APP,必須每個驗證都通過才算通過。如果設為 NO,只需要打包你自己的憑證就夠了。

Update: validatesCertificateChain這個選項已經在 AFNetworking v2.6.0 拿掉了。

3. 如何使用 AFSecurityPolicy

這裡以最新版的 AFNetworking 為例,假設你有一個 APIManager處理所有的 API call,它繼承自 AFHTTPSessionManager,我們可以設定它的 security policy 如下:

@interface APIManager : AFHTTPSessionManager
+ (APIManager *)sharedInstance;
@end

@implementation APIManager
+ (APIManager *)sharedInstance {
  static APIManager *_sharedClient = nil;
  static dispatch_once_t onceToken;

  dispatch_once(&onceToken, ^{
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    _sharedClient = [[APIManager alloc] initWithBaseURL:nil sessionConfiguration:sessionConfiguration];
    AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
     policy.validatesCertificateChain = NO; // v2.6.0 之後沒有這個選項了
    _sharedClient.securityPolicy = policy;
  });

  return _sharedClient;
}
@end

好了,到此大功告成,你已經正確設定好安全連線了!

錯誤排解

Q: 為什麼我可以連上其他的網址?我不是應該只能連上憑證綁定的網址嗎?
  1. 檢查 validatesDomainName是否設為 NO 了,是的話就將它改成 YES。
  2. 檢查是否連到 http開頭的網址,非安全連線是不受限制的。
Q: 為什麼有打包憑證了,還是會連線失敗?
  1. 確認你的憑證有加到你的 target 裡頭,拖拉到 Xcode 時要把 Copy items if neededAdd to targets打勾。
  2. 如果 validatesCertificateChain是 YES,記得把它改成 NO,或是把上級憑證也一同打包進 APP。(v2.6.0 之後沒有這個選項了)

參考文件

https://github.com/AFNetworking/AFNetworking/issues/2673
https://github.com/rnapier/RNPinnedCertValidator
http://stackoverflow.com/a/24625969
http://oncenote.com/2014/10/21/Security-1-HTTPS/


ReactiveCocoa 範例 - 處理網路請求

$
0
0

前言

最近這半年來我開始使用 ReactiveCocoa開發 APP,覺得它真的是很適合一般的 APP 使用情境,在我看來,它可以很漂亮的解決 當 XX 發生的時候,就執行 YY的需求。今天這篇文章來分享一下,我是如何使用 ReactiveCocoa強化舊有的網路請求功能。

我是用 AFNetworking來實現網路請求功能,然後用 Mantle來建立我的 model。現在我有個 APIManager繼承自 AFHTTPSessionManager,它專門負責跟 server 之間的 API call,然後有個 User model,它繼承自 MTLModel <MTLJSONSerializing>。我打算完成的功能是「根據 email 取得使用者的資料」。

第一版:單純使用 AFNetworking

最一開始的版本,我在 APIManager建立一個 getUserByEmail:success:failure: method,然後在 UserViewController呼叫它並處理成功與失敗的後續動作。

APIManager

- (NSURLSessionDataTask *)getUserByEmail:(NSString *)email success:(void (^)(User *user))success failure:(void (^)(NSError *error))failure {
  NSString *path = @"https://your.server.address/api/user";
  NSDictionary *params = @{ @"email":email };
  return [self GET:path parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
    if (success) {
      NSError *error = nil;
      User *user = [MTLJSONAdapter modelOfClass:[User class] fromJSONDictionary:responseObject error:&error];
      if (error) {
        if (failure) {
          failure(error);
        }
      } else {
        success(user);
      }
    }
  } failure:^(NSURLSessionDataTask *task, NSError *error) {
    if (failure) {
      failure(error);
    }
  }];
}

UserViewController

- (void)getUserInfo {
  [SVProgressHUD show];
  [[APIManager sharedInstance] getUserByEmail:@"test@gmail.com" success:^(User *user) {
    [SVProgressHUD dismiss];
    // Update data
    // Update UI
  } failure:^(NSError *error) {
    [SVProgressHUD dismiss];
    // Data error handling
    // UI error handling
  }];
}

第二版:用 RACSignal 包起來

原本是直接回傳 NSURLSessionDataTask,現在改成用 ReactiveCocoaRACSignal包起來。當然 UserViewController也要稍微修改一下來呼應這個改變。

APIManager

- (RACSignal *)getUserByEmail:(NSString *)email {
  return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    NSString *path = @"https://your.server.address/api/user";
    NSDictionary *params = @{ @"email":email };
    
    NSURLSessionDataTask *task = [self dataTaskWithHTTPMethod:@"GET" URLString:path parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
      NSError *error = nil;
      User *user = [MTLJSONAdapter modelOfClass:[User class] fromJSONDictionary:responseObject error:&error];
      if (error) {
        [subscriber sendError:error];
      } else {
        [subscriber sendNext:user];
        [subscriber sendCompleted];
      }
    } failure:^(NSURLSessionDataTask *task, NSError *error) {
      [subscriber sendError:error];
    }];

    [task resume];
    
    return [RACDisposable disposableWithBlock:^{
      [task cancel];
    }];
  }];
}

UserViewController

- (void)getUserInfo {
  [SVProgressHUD show];
  [[[APIManager sharedInstance] getUserByEmail:@"test@gmail.com"]
   subscribeNext:^(User *user) {
    [SVProgressHUD dismiss];
    // Update data
    // Update UI
   } error:(NSError *error) {
    [SVProgressHUD dismiss];
    // Data error handling
    // UI error handling
   }];
}

第三版:加入 AFNetworking-RACExtensions

如果每支 API 都要改寫成這樣的話,程式碼將會變得非常冗長,還好網路上早就有人幫忙開發 AFNetworking-RACExtensions來處理這件事。所以接下來我要改寫 APIManagerUserViewController則不需要變動。改寫之後的程式碼,看起來是不是清爽多了呢!

APIManager

- (RACSignal *)getUserByEmail:(NSString *)email {
  NSString *path = @"https://your.server.address/api/user";
  NSDictionary *params = @{ @"email":email };
  return [[self rac_GET:path parameters:params] flattenMap:^RACStream *(RACTuple *tuple) {
    NSError *error = nil;
    User *user = [MTLJSONAdapter modelOfClass:[User class] fromJSONDictionary:tuple.first error:&error];
    return error ? [RACSignal error:error] : [RACSignal return:user];
  }];
}

第四版:改成 MVVM 模式

既然我們都用 ReactiveCocoa了,那就順便把程式架構從 MVC(Model-View-Controller)改成 MVVM(Model-View-ViewModel)吧,更多有關 MVVM 的說明可以參考 objc.io 的這篇文章以及 ReactiveViewModel的說明文件.

在這個版本,APIManager不用修改,然後我們多了一個 UserViewModel。藉由這樣的改動,我們讓 UserViewController變得更簡潔,它專心處理跟 UI 有關的部分,跟資料邏輯相關的部分則是搬到 UserViewModel去處理。

UserViewModel

- (RACSignal *)getUserByEmail:(NSString *)email {
  return [[[[APIManager sharedInstance] getUserByEmail:email]
  doNext:^(User *user) {
    // View model updates data
  }]
  doError:^(NSError *error) {
    // View model error handling
  }];
}

UserViewController

- (void)getUserInfo {
  [SVProgressHUD show];
  [[self.viewModel getUserByEmail:@"test@gmail.com"]
   subscribeNext:^(User *user) {
    [SVProgressHUD dismiss];
    // Update UI
   } error:(NSError *error) {
    [SVProgressHUD dismiss];
    // UI error handling
   }];
}

看起來似乎差不多?

或許你會覺得,單純使用 AFNetworking跟使用 ReactiveCocoa改寫的差異不大,對 UserViewController 來說只是從原本的 successfailure block 改成 subscribeNextsubscribeError block。

這是因為我們的例子很單純,如果後續還有許多動作要執行的話,使用 ReactiveCocoa就顯得方便許多。以下的例子取自 ReactiveCocoa ReadMe 的 Chaining dependent operations,你可以自己比較看看兩者的差異。

Dependencies are most often found in network requests, where a previous request to the server needs to complete before the next one can be constructed, and so on:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
            NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];

ReactiveCocoa makes this pattern particularly easy:

[[[[client logIn]
    then:^{
        return [client loadCachedMessages];
    }]
    flattenMap:^(NSArray *messages) {
        return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeError:^(NSError *error) {
        [self presentError:error];
    } completed:^{
        NSLog(@"Fetched all messages.");
    }];

以上就是幫單純的網路請求加上 ReactiveCocoa超能力的過程,如果你有任何意見或建議的話,歡迎在底下留言。

開發產品學到的一些事(上)

$
0
0

這幾年都在 start-up 打滾,跟著做了一些產品,也有一些小小心得,就紀錄下來跟各位分享討論。

不要一開始就寫程式

你(或你們公司)想到了一個好點子,這時你要做的第一件事是什麼?馬上捲起袖子把點子實作出來嗎?千萬不是!你們要做的第一件事是確認真的有客戶需要這個點子,而不是草率投入大量時間、人力、金錢成本,最後做出來的東西卻可能沒人要。

或許你會想說東西都還沒做出個雛形來,怎麼知道有沒有客戶呢?方法有很多種,例如你們可以把點子解釋給陌生人聽,看看對方的反應如何。或是先快速開發一個 landing page 來介紹你們的點子,並收集使用者的回饋。

確定真的有人需要這個點子,並且確定這個點子真的需要寫程式之後,再開始動手寫。

寫好 User Story

你可以不寫 Spec,但千萬不能沒寫 User Story。User Story 的寫法很簡單,長得大概就像這樣:「為了解決什麼問題,身為一個使用者,我希望在某種情況下,能夠做某件事」。

寫 User Story 有幾個好處:

  1. 可以讓所有人都看得懂

    因為 User Story 就是用大家都明白的語言,把想要做的事寫出來,所以無論是不是技術背景的人,都可以看懂「為何需要這個功能、這個功能是為了解決什麼問題」。

  2. 可以幫助整理思路

    因為你得把你在什麼情境底下想要完成什麼功能寫出來,在寫的過程當中就可以整理自己的思路,檢查這樣的需求是否合理。然後因為大家都能看懂,所以無論是否有技術背景的人,都可以一起來討論,這樣就可以幫忙找出盲點,並且完善整個需求。

  3. 可以讓開發者用最適合的解法解決問題

    為什麼我會覺得不需要寫 Spec 呢,因為寫 Spec 的人不一定是要開發的人,所以很容易就寫出很莫名其妙的 Spec。更糟糕的是,有可能一開始就寫 Spec,少了互相討論完善的階段,結果最後整個歪掉。

    所以我會說不要寫 Spec,寫 User Story 就好,然後把 User Story 寫完整。有經驗的開發者看到 User Story 自然就會找出最適合的方式去解決問題。千萬不要外行領導內行,亂下指導棋。

  4. 方便驗收

    既然 User Story 都寫得那麼完善了,要驗收的時候只要一一對照 User Story 就可以知道是否完成所有需求。對驗收人員來說,看 User Story 就會知道這個功能到底是什麼,也就代表很容易就能理解要驗收什麼。

一定要使用版本控制系統

版本控制系統的好處我就不多說了,無論是單打獨鬥或是團隊合作都應該要用版本控制系統。我個人最推薦的當然是 Git,雖然它的入門門檻頗高,但熟悉它之後所帶來的好處真是讓人無法抗拒的。

近年來 Git 有越來越多好用的 GUI,已經大大降低初學者上手的難度,SourceTreeTowerGitUp都是不錯的選擇。至於要如何將版本控制系統整合到你的開發流程,可以先參考 Git FlowGitHub Flow,再逐步調整成適合你們團隊的作法。

有了版本控制,當然就得記得要下版本號,如果不知道該怎麼決定版本號,可以參考語意化版本號的作法。

不必一開始就寫測試

可能有些人覺得這是邪魔歪道,不過我真心這麼覺得:「你應該寫測試,但你不應該一開始就寫測試」。

身處在 start-up,無論前期的思慮有多麼周到,產品的需求還是很容易就會變更。如果採用「測試先行」的話,最後你會發現絕大多數開發的時間都拿去寫測試了,而且因為需求在前期很容易變更,所以你的測試也很容易一改再改因而難以重複使用。(P.S. 降低需求變更機率的一個方法就是好好寫 User story)

所以我會建議,等產品到達一定的穩定度之後(例如主要的功能或畫面都已經固定),再來補上測試。到時可以特地安排一段時間專心來寫測試,或是在需要重構程式的時候邊重構邊補上測試。


如果你覺得這篇文章還算有道理,那...下集在這裡

開發產品學到的一些事(下)

$
0
0

一個 APP 開發工程師在 start-up 打滾幾年之後,得到的一些心得跟體會,上集在這裡

善用追蹤工具

做任何決定之前都要有所本,不要突然「通靈」了就做出莫名其妙的決策。那要根據什麼做決定呢?很簡單,讓數據說話。善用至少一款追蹤工具(GAMixpanelFlurryKissmetricsKeen IOCustomer.ioSegment等等)並在上線第一天就開始追蹤使用者的行為,這樣才能知道有多少使用者在用你的產品,以及如何用你的產品。總之,就是要透過數據收集與分析,來了解你的使用者。

日後當你要做 growth hacking 的時候,追蹤工具更是不可少,你會需要這些追蹤工具來幫你統計,看哪些修改會有助於你的業務成長,哪些修改反而會讓業務衰退,以及成長或衰退了多少。

如果有開發 APP 的話,你也會需要想辦法取得 crash log,這樣才知道程式掛在哪,我推薦使用 Crashlytics幫你收集分析這些 crash log,它非常的好用。

愛用第三方元件

近年來 open source 越來越受到歡迎,網路上有各式各樣的第三方元件讓你取用,所以真的沒必要每一樣功能都自幹,如果有現成的可以符合你的需求,就大膽的用吧。再加上第三方元件的管理程式(像是蘋果開發者常用的 CocoaPodsCarthage)也越做越好,早期會遇到的第三方元件版本控管問題已經很少見了,所以我建議各位可以盡量用第三方元件,或是將第三方元件修改成符合自己需求,沒事不要自己造輪子。

方便的更新機制

隨著時間過去,你的產品需求一定會有所變化,該怎麼向後相容以及如何要求使用者升級就變成一件不得不面對的問題,這裡我有幾點建議讓你參考。

  • 設計 API 的時候要考慮版本化

    同一支 API 背後的商業邏輯很有可能會變化,如果一開始設計的時候沒有考慮版本化,你就會為了 API 名稱而大傷腦筋:可能原本的叫做 checkout,後來叫做 checkout_new,再後來叫做 the_new_checkout

    但如果一開始有考慮版本化的話,你就可以一開始叫做 checkout/v1,後來叫做 checkout/v2。或是直接改 API end point,例如一開始的 end point 是 api.myserver.com/v1/,後來的是 api.myserver.com/v2

  • 呼叫 API 的時候附上環境參數

    雖然現在送 request 的時候,大部分都會在 header 附上 client 的一些資訊,但每個 client 送出的 request header 格式有可能並不統一,所以要 server 去 parse header 來取得所需資訊,其實成本蠻高的。

    比較方便的方法是,client 每次在呼叫 API 的時候就主動附帶一些參數讓 server 好判斷。例如可以送 platform參數指明是 ios / android / webdevice指明是 phone / tablet / desktopversion表示程式的版本等等。Server 就可以根據這些參數有不同的邏輯與回應資料,例如根據 device回傳不同尺寸的圖片網址,或是根據 version來得知對方是否使用最新版本。

  • In-App Announcement

    如果一開始就有設計 in-app announcement 機制的話,就可以更輕易讓使用者得知最新消息。例如有新版本可以下載了,讓使用者知道有什麼新功能並引導使用者去下載。或是有在舉辦什麼活動的時候,也可以透過這個機制讓使用者知道(例如 Uber 時常會舉辦不同活動,車子圖示也會跟著變)。或是有什麼新貼圖、新佈景主題、新內容,也可以讓使用者知道(例如 Line 或各款遊戲)。

  • 儲存資料的時候要考慮版本化

    有很多時候我們必須儲存一些資料在本機,可能是透過寫入設定檔或是寫入資料庫,如果儲存資料的時候有考慮到版本化,之後要處理資料相容或是要將舊資料轉換成新資料的時候,都會相對簡單許多。

寫文件

所有人都知道寫文件的重要,但是大概沒人會喜歡寫文件,但其實文件沒那麼難寫。文件存在的理由是什麼?不就是為了日後的查詢參考嗎。

所以 User Story 就是文件的一部分,你一邊設計產品的同時,就一邊在寫文件了。這樣有沒有覺得寫 User Story 很划算,會不會更有動力寫好它!

程式碼註解也是文件的一種,現在有很多工具能夠將註解轉換成說明檔,日後只要註解有所變動,說明檔就會自動更新,這可以幫忙省下超多時間。像我們的後端都會在程式碼裡頭註解說明這支 API 的用途是什麼、傳入的參數是代表什麼意思、是什麼型態、是否可以不傳、回傳值是什麼、有可能產生哪些 error,對應的 error code 是什麼等等。我個人覺得,維護程式碼的註解比額外維護一份說明文件(可能是 wiki 或是 doc 等等)簡單多了。

文件也不是寫完丟在一旁就算了,它是讓人日後可以查詢參考的,所以最好可以把所有文件統一放在一個地方,然後有個方便的作法讓人查詢(可能是規劃良好的目錄結構,或是提供搜尋功能等等)。

簡單來說,文件有三大重點:要完整、要保持最新版本、要讓人找得到。

畫 Wireframe

通常設計師會畫好 wireframe 讓工程師知道整個使用流程,明白該從哪個畫面跳到哪個畫面。如果很不幸的,你們公司沒有這種東西(可能是不見了或根本就沒有),那 APP 開發工程師就認份一點自己畫一個吧。對工程師來說,畫這個並不難,只要把 APP 每個畫面都擷取下來,然後用箭頭把彼此之間的前後關係串起來就可以了。

擁有一份完整的 wireframe 的好處在於,當你們想要增刪或修改某些功能的時候,可以把 wireframe 拿出來,看看增刪或修改這個功能之後,整個使用流程是否順暢合理。千萬不要功能都做完,才發現流程變得卡卡的,這樣浪費的成本太高了。


以上就是我的一些心得,對上集有興趣的人可以看這裡

老實說,就算每一點都做到了,也不保證你的產品會成功,但絕對會讓你開發一款新產品時比較不會走歪,就算歪了也可以早一點救回來,降低你犯錯的成本。

產生 Auto Layout Constraints 的程式碼要放在哪裡

$
0
0

由於種種原因,所以到目前為止,我在公司的專案還是沒有用 Interface Builder,所有的 UI 完全用程式碼一行一行刻出來,當然這也包含 auto layout 相關的程式碼。

如果是一個 UIViewController的話,程式碼架構大概長這樣:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupConstraints];
}

- (void)setupConstraints {
    // Create and add layout constraints to view and subviews.
}

如果是 UIView的話,大概是這樣:

@interface CustomView()
@property (nonatomic, assign) BOOL didSetupConstraints;
@end

@implementation CustomView
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        // Do something....
        [self setNeedsUpdateConstraints];
        [self updateConstraintsIfNeeded];
    }
    return self;
}

- (void)updateConstraints {
    if (!self.didSetupConstraints) {
        // Create and add constraints to self and subviews.
        self.didSetupConstraints = YES;
    }
    [super updateConstraints];
}
@end

我在網路上看到的作法也都是這樣。UIViewController還沒什麼問題,只是 UIView還要多一個 BOOL 來判斷要不要加入 constraints,讓我感到很不自然,全身不舒服。

最近在爬文的時候,看到這一篇文章,裡頭提到了一段來自 Apple 工程師的回應:

In general, if the constraints will only be created once, it should be done in an initialization method (such as -init or -viewDidLoad, and so forth). Save -updateConstraints for things that are expected to change over the course of running the App.

如果你不會在程式運行途中去新增或移除 constraints,那就把建立 constraints 的工作放在 initviewDidLoad階段。中途會新增或移除的動作才放到 updateConstraints。你可以建立 constraint 之後再修改它的 constant屬性,這不算是新增或移除 constraint。

嗯,所以之後我可以開心的寫 auto layout 了:)

Update:
今天看到這一篇文章還有 WWDC 2015 的影片,覺得應該要更新一下我的結論:updateConstraints因為會一次處理所有的 constraint,所以它的效能比較好,適合在需要「大量的新增、修改、刪除 constraints」的時候使用。如果你沒有遇到效能問題,那可以不用它。

必備的 Xcode plugins

$
0
0

不得不說 Xcode 的功能跟其他 IDE 相比,真的挺陽春的,多虧網路上一堆熱心的開發者,幫忙開發了不少 plugins,稍稍補強了 Xcode 的功能。底下就列出我目前有用到的 plugins,當作一個備忘,也歡迎大家跟我分享好用的 plugins。

強烈推薦使用 Alcatraz管理這些 plugins!


Backlight

高亮度當前列,這是很多編輯器都有的功能,不知道為何 Xcode 沒有內建。

BBUFullIssueNavigator

當有 issue 產生的時候,顯示完整的 issue 內容,而不是只有顯示前幾行。

BBUncrustifyPlugin-Xcode

支援 Uncrustify 跟 ClangFormat 這兩種程式碼整理工具,可以方便的讓程式碼擁有一致的風格。

DerivedData Exterminator

有時候 Xcode 會因為舊的 derived data 而有奇妙的問題,這個工具讓你可以快速的清除 derived data。

FuzzyAutocomplete

最好用的程式碼自動補完工具,能夠大幅減少打字的次數,加快開發速度。尤其 Objective-C 的程式碼通常都很長,有了這個工具之後真的差很多。

HTYCopyIssue

開發難免會出現 error,這個工具可以幫忙快速複製錯誤訊息,然後一鍵搜尋 Google 或 StackOverflow 有關這個錯誤的資訊。

IntelliPaste

讓你更方便的複製貼上 method 跟 RGB color。

KSImageNamed

自動補完圖片檔名,並且還提供圖片預覽功能,讓你不會選錯圖片。

MLAutoReplace

透過自訂的設定檔,可以自動取代某些字串,加快開發速度。

ColorSense for Xcode

方便預覽與輸入顏色的工具,調整 UI 的時候非常好用。

Dash Plugin for Xcode

將 Dash 整合到 Xcode,方便開發者查詢文件。

ProjectWindowName

它會改變 Project/Workspace window title,將 project name 附加到 file name 前面。如果你會同時開啟多個 project 或 workspace,這個工具能讓你輕鬆辨別每個檔案。

SCXcodeSwitchExpander

自動補完 switch-case,減少許多打字次數,又可以避免漏打某個選項,非常方便。

SCXcodeTabSwitcher

⌘cmd + [1..9]切換分頁。

VVDocumenter-Xcode

幫你快速產生註解,並且符合 appledoc,Doxygen,或 HeaderDoc 格式。

XcodePlus Delete Line

這個工具做的事情很簡單,就是透過熱鍵快速刪除一行或多行程式碼。

XReset

不用啟動模擬器就能重設模擬器的設定與內容。

自動解決 Xcode project file 的合併衝突

$
0
0

之前寫過一篇文章,分享如何 讓 Xcode 專案易於版本控制的方法,最近又看到另外一個小技巧,跟我原本的方法結合起來的話,基本上就萬無一失了!

這個方法的步驟如下:

  1. 建立一個 .gitattributes
  2. 輸入 *.pbxproj merge=union
  3. commit 這個 .gitattributes

這些動作告訴 Git 「針對 .pbxproj 檔要使用 union 的 merge 策略」,翻成白話就是「要包含對方的修改跟自己的修改」。

在之前的文章裡頭,我們已經將檔案照檔名排序了,所以當遇到合併衝突的時候,可以安心的包含對方的修改跟自己的修改。

參考來源

如何在 Debug mode 自動停用 ATS

$
0
0

蘋果在 WWDC 2015 上發表了 App Transport Security (ATS),大力推廣網路安全連線。對於這樣的發展,我個人是樂見其成的,也相信在蘋果的影響力之下,安全連線也會進一步的普及。

不過對工程師來說,這代表你又要修改程式,以便符合蘋果規範了。在修改的過程中,我就遇到了一個問題:Release 版的 app 是會走安全連線的,但在 Debug 的時候它是連到本機(localhost),這不是安全連線。該怎麼讓它在 Release / Debug 都能正常連線呢?

因為 ATS 的設定是寫在 Info.plist裡頭,所以最直覺的想法就是建立兩份 plist分別給 Release / Debug 用。但實際情況是,這兩份 plist的重複性會很高,而且你得隨時維護彼此之間的設定同步。一聽就知道這樣很容易出現人為失誤,不妥!

還好我找到另一個解決辦法,測試之後確實有效。

1.

Info.plist裡頭寫入你的 ATS 設定,在這裡要注意的是,一定要包含 NSAllowsArbitraryLoads這筆鍵值,至於它的值是 YES 或 NO 倒是無所謂,因為我們等一下會自動判斷要設為什麼值。

2.

選擇你的 target,切換到 Build Phases分頁,點擊 +按鈕並選擇 New Run Script Phase,新增的這個 script 名稱你可以自訂。

3.

script 內容如下:

#Disables ATS in debug builds.
INFOPLIST="${TARGET_BUILD_DIR}"/"${INFOPLIST_PATH}"
case "${CONFIGURATION}" in
"Release"|"Adhoc")
/usr/libexec/PlistBuddy -c "Set :NSAppTransportSecurity:NSAllowsArbitraryLoads NO" "${INFOPLIST}"
;;
"Debug")
/usr/libexec/PlistBuddy -c "Set :NSAppTransportSecurity:NSAllowsArbitraryLoads YES" "${INFOPLIST}"
;; 
esac

它做的事情就是去判斷你目前的 build mode,如果是 ReleaseAdhoc,就把 NSAllowsArbitraryLoads設為 NO;如果是 Debug就設為 YES。


同場加映:如何正確設定 AFNetworking 的安全連線


我的第一個 Xcode plugin - Auto Highlight Symbol

$
0
0

前不久我寫了一篇文章記錄我有在用的 Xcode plugins,看到網路上那麼多的 plugins 都開放原始碼,讓我不由得也手癢想要寫一個自己來用,所以我的第一個 Xcode plugin 就這麼產生了。

Auto Highlight Symbol

在使用 Xcode 看程式碼的時候,都很希望它能像其他 IDE 一樣,有自動高亮度同一個變數的功能(例如 Sublime Text、Eclipse),這樣就能一眼看出這個變數在哪些地方被使用到。雖然 Xcode 號稱它也有這個功能,但它卻沒有做得很好,它只是在每個變數底部加上白色虛線,一個不小心就會忽略。

我的第一個 plugin,Auto Highlight Symbol,就是為了解決這個問題。當你選中某個變數的時候,所有出現這個變數的地方都會加上一個背景色,當然你可以設定想要的顏色。

我把程式碼放在這裡,歡迎一起來修改它,讓它變得更好。

如何安裝

安裝方式非常簡單,你可以

  1. 透過 Alcatraz安裝,或
  2. 下載原始碼,用 Xcode 開啟專案,按下 Cmd+B
  3. 記得要重開 Xcode 才會生效

疑難排解

Q: 為何安裝完畢之後沒有效果?

A: 選單「Editor -> Auto Highlight Symbol」要打勾。

Q: 為何安裝完畢之後沒有出現選單?

A: 請參考這篇文件

解決 Xcode 無法任意設定字型的問題

$
0
0

最近灌了一台全新的電腦,用的是最新的 OS X El Capitan + Xcode 7.1,身為一位開發者,裝好 Xcode 要做的第一件事當然是把佈景主題的字型換成看得順眼的等寬字型(我個人偏好使用 Adobe Source Code Pro)。只是不知道為什麼,當我要調整字型的時候,卻發現字型挑選器無法讓我看到更多的字型:

問了一下社群的朋友,發現有不少安裝「全新的電腦 + Xcode」的人都有遇到這個問題,還好解決方法很簡單:

  1. 切到「~/Library/Developer/Xcode/UserData/FontAndColorThemes」目錄,這裡存放使用者自訂的佈景主題
  2. 用你喜歡的文字編輯器(例如 Sublime Text 或 Atom)開啟你要修改的主題檔
  3. 搜尋 <key>DVTSourceTextSyntaxFonts</key>
  4. 它底下的 dict 就是字型設定了,把這裡的字型名稱取代為你要的字型就可以了
  5. 要注意的是,這裡的字型名稱是「PostScript 名稱」,你可以在「字體簿」裡頭看到這項資訊

字體簿

改好之後存檔再重開 Xcode,世界又和平了 :D

NSDateFormatter Cheat Sheet for Dash

$
0
0

Dash是 Mac 系統下最好用的 API 文件瀏覽器,我認真覺得每個程式設計師都應該買一套,它真的能大幅提高你的生產力!

它除了可以拿來瀏覽多種 API 文件之外,還能讓使用者自行上傳 cheat sheet,我整理了一個 NSDateFormatter的 cheat sheet 方便查閱,有需要的人可以參考下圖,下載來使用。

它的 cheat sheet 是 ruby 格式,如果你有發現任何錯誤,或是想要新增更多資料,歡迎修改之後提交給官方

用 Objective-C 實作 Redux 架構

$
0
0

前言

有一定的程式設計經驗之後,會愈來愈感受到程式架構的重要性,在 iOS app 開發的世界裡,最常見的莫過於 MVC 架構,因為它夠簡單而且是蘋果推薦的架構。但當你的程式越來越龐大,流程越來越複雜的時候,就會發現 MVC 架構已經無法滿足需求了。這幾年最為人所知的就是 MVP / MVVM / VIPER / Coordinator 這幾個模式。

我認為這些模式的著眼點都在於「UI」:它們假設你有一套辦法去存取或修改資料,然後它們提出的方案是關於如何處理「界面顯示 / 使用者互動 / 資料存取」之間的關係。

當程式越長越大,要儲存的狀態越來越多,不同畫面之間需要同步的資料也越來越多,我們該如何管理資料的存取、確保其一致性與正確性呢?Facebook 之前提出了 Flux 架構,後來有人提出改良版的 Redux 架構,不管是 Flux 還是 Redux,其重點都是在於「資料的流動是單向的,資料只有一份,並且只有一個角色可以修改資料」。

Flux / Redux 一開始提出是給網站使用的架構,後來有人把它套用到 iOS 開發,不過我查到的資料都是使用 Swift 實作。無可否認使用 Swift 來實作這套架構的確比較方便,只是我很好奇用 Objective-C 的話會有多困難,以下就是我的一些開發過程。

Redux 的四個角色

從上圖可以看到,Redux 架構很簡單,只有四個角色:

Action

  • 單純的資料結構。
  • 表示它所代表的動作類型,以及附帶的資料。

Store

  • 負責收到 Action。
  • 負責把 Action 跟最新的 State 傳給 Reducer。
  • 負責修改 State,並讓外界可以取得最新的 State。
  • 負責送出「State 已經更新」的通知給感興趣的人。

State

  • 單純的資料結構。
  • 代表整個 app 需要的所有資料。

Reducer

  • 單純的函式。
  • 輸入是「Action」跟「State」,輸出是「修改過的 State」。

例子:文章列表

現在我們要來寫一個很簡單的 app,它唯一的功能就是跟伺服器要求最新的文章列表,然後一筆筆顯示處理。假設我們的網路功能跟 UI 都設計好了,那該怎麼套用 Redux 架構來處理資料的部分呢?

Action

我會建議一開始由 Action 先規劃。這個例子裡的 Action 很單純,就是用一個 property 來記錄 action type,再用一個 property 來記錄 payload。因為有些 type 不需要附帶資料,所以 payload 是 nullable。這裡我規劃了兩個 type,第一個是取得文章列表之後我需要 SetPosts來更新 State 裡頭的文章列表,第二個是 AppendPosts,當我取得下一頁的文章列表之後我要把它附加到 State 原有的列表裡。

/// TLBAction.h
typedef NS_ENUM (NSInteger, TLBActionType) {
  TLBActionTypeSetPosts,
  TLBActionTypeAppendPosts,
};

@interface TLBAction : NSObject
@property (nonatomic, assign, readonly) TLBActionType type;
@property (nonatomic, strong, readonly, nullable) id payload;
- (instancetype)initWithActionType:(TLBActionType)type payload:(nullable id)payload;
@end

/// TLBAction.m
@interface TLBAction ()
@property (nonatomic, assign, readwrite) TLBActionType type;
@property (nonatomic, strong, readwrite, nullable) id payload;
@end

@implementation TLBAction
- (instancetype)initWithActionType:(TLBActionType)type payload:(id)payload {
  if (self = [super init]) {
    _type = type;
    _payload = payload;
  }
  return self;
}
@end

State

State 沒什麼好說的,就是一個單純的資料結構,用來儲存會用到的資料。值得一提的是,只要存原始資料就好,可以藉由原始資料推算出的資料不需要存起來。

/// TLBState.h
@interface TLBState : NSObject <NSCopying>
@property (nonatomic, strong) NSOrderedSet <NSString *> *posts;
@end

Reducer

Reducer 是唯一知道該怎麼修改 State 的地方,一個 Reducer 可能只會修改 State 的某一部分。當 Action 越來越多、State 越來越大的時候,也可以將多個 Reducer 合成一個更大的 Reducer。

在原始的 Redux 定義裡頭,Reducer 的格式是 func(state, action) -> state,傳舊的 state 進去會先產生一個新的 state 再來修改這個新 state,而不是直接修改舊的 state。但在 Objective-C 的世界,這代表在每個 Reducer 裡頭都得產生一個新的 state instance,Reducer 一多的情況就可能對效能造成影響。所以我在這裡把它定義成 typedef void (^TLBReduceBlock)(TLBState **, TLBAction *),傳入的是 state 的記憶體位址,在 Reducer 裡頭就可以直接去修改 state,避免一直產生新的 instance 的問題。

要注意的是,你不應該預期 Reducer 會以怎樣的順序被呼叫,它應該是一個 pure function。

/// TLBReducer.h
typedef void (^TLBReduceBlock)(TLBState **, TLBAction *);

@interface TLBReducer : NSObject
+ (NSArray *)availableReduceBlocks;
@end

/// TLBReducer.m
@implementation TLBReducer
+ (NSArray *)availableReduceBlocks {
  return @[
    [self postActionsReducer]
  ];
}

+ (TLBReduceBlock)postActionsReducer {
  TLBReduceBlock block = ^(TLBState **state, TLBAction *action) {
    if (state == NULL) {
      return;
    }

    TLBState *newState = *state;
    switch (action.type) {
      case TLBActionTypeSetPosts: {
        newState.posts = [NSOrderedSet orderedSetWithArray:action.payload];
        break;
      }

      case TLBActionTypeAppendPosts: {
        NSMutableOrderedSet *set = [newState.posts mutableCopy];
        [set addObjectsFromArray:action.payload];
        newState.posts = [set copy];
        break;
      }

      default: {
        break;
      }
    }
  };
  return block;
}
@end

Store

一個 app 只會有一個 Store,所以它會是一個 singleton。外界會要求它去 dispatch 一個 action,它就會讓全部的 Reducer 依序處理這個 action,並且為了確保一次只有一個 Action 被執行,所以我建立了一個 serial queue 來處理。最後把處理過的結果寫回 State,並通知感興趣的人 State 已更新。通知有很多種實作方式,在這裡我是用 ReactiveCocoaRACSignal讓別人來訂閱。

/// TLBStore.h
@interface TLBStore : NSObject
@property (nonatomic, strong, readonly) RACSignal *stateObserver;

+ (instancetype)shardInstance;
- (void)dispatchAction:(TLBAction *)action;
- (TLBState *)currentState;
@end

/// TLBStore.m
@interface TLBStore ()
@property (nonatomic, strong, readwrite) RACSignal *stateObserver;
@property (nonatomic, strong) TLBState *state;
@property (nonatomic, strong) NSArray <TLBReduceBlock> *reducers;
@property (nonatomic, strong) dispatch_queue_t serialQueue;
@end

@implementation TLBStore
+ (instancetype)shardInstance {
  static TLBStore *_sharedInstance = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _sharedInstance = [[TLBStore alloc] init];
  });
  return _sharedInstance;
}

- (instancetype)init {
  if (self = [super init]) {
    _serialQueue = dispatch_queue_create("Redux Store Action Queue", DISPATCH_QUEUE_SERIAL);
  }
  return self;
}

- (void)dispatchAction:(TLBAction *)action {
  dispatch_async(self.serialQueue, ^{
    TLBState *newState = [self.state copy];
    for (TLBReduceBlock block in self.reducers) {
      block(&newState, action);
    }
    self.state = newState;
  });
}

- (TLBState *)currentState {
  return [self.state copy];
}

- (RACSignal *)stateObserver {
  if (!_stateObserver) {
    _stateObserver = [RACObserve(self, state) replayLast];
  }
  return _stateObserver;
}

- (TLBState *)state {
  if (!_state) {
    _state = [[TLBState alloc] init];
  }
  return _state;
}

- (NSArray <TLBReduceBlock> *)reducers {
  if (!_reducers) {
    _reducers = [TLBReducer availableReduceBlocks];
  }
  return _reducers;
}
@end

整個串起來

假如現在我有一個 UIViewController,我要跟伺服器請求文章列表,取得列表之後就更新我的 tableView,那使用 ReactiveCocoa程式碼長得大概像這樣。

/// TLBPostListViewController.m
@interface TLBPostListViewController () <UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, strong) NSOrderedSet <NSString *> *posts;
@property (nonatomic, strong) RACDisposable *stateObserver;
@end

@implementation TLBPostListViewController
- (void)dealloc {
  [_stateObserver dispose];
  _stateObserver = nil;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  [[[TLBNetworkManager shardManager] fetchPost] subscribeNext:^(NSArray *posts) {
    // 送出 action 之後就不理會它了,因為我們會監聽 state 的變化
    TLBAction *action = [[TLBAction alloc] initWithActionType:TLBActionTypeSetPosts payload:posts];
    [[TLBStore shardInstance] dispatchAction:action];
  }];

  @weakify(self);
  // 監聽 state 的變化
  self.stateObserver = [[TLBStore shardInstance].stateObserver subscribeNext:^(TLBState *state) {
    @strongify(self);
    if (![self.posts isEqualToOrderedSet:state.posts]) {
      self.posts = [state.posts copy];
      [self.tableView reloadData];
    }
  }];    
}
@end

結論

Redux 只是一個處理資料的方案,它可以跟 MVC / MVVM / VIPER / Coordinator 等架構相互配合,因為它們要處理的是不同問題。我覺得使用 Redux 有以下這些優點:

  • 架構清晰,每個角色該做什麼事都有明確規定。
  • 資料有統一的處理方式,而且資料來源只有一個,確保資料的一致性。
  • 團隊可以寫出統一風格的程式碼。
  • 可與其他 UI 相關的架構一同使用。

當然它也有缺點:

  • 多出不少程式碼。
  • 架構變得比較複雜,簡單的小專案不適合用它。
  • 會多吃一些記憶體。
  • 速度會稍微慢一點(但對大多數人來說應該感覺不出來)。

總結來說,每個架構有其適合的場景,你要先瞭解要解決的問題再來選擇要使用的架構,不要太早優化也不要過度設計了。

Q&A

Q: 我覺得這個例子很單純,根本不需要用到 Redux?
A: 沒錯!我只是為了舉例,現實情況下如果是像這麼簡單的專案,千萬不要搞得如此複雜!

Q: 現實情況下,State會變得很大一包,可以切小一點嗎?
A: 我覺得可能有兩種解法:

  • 針對每個 feature 或頁面,建立 sub-store,這個 sub-store 提供每個 feature 或頁面需要的 sub-state
  • 針對每個 feature 或頁面,建立 State category,這個 category 提供每個 feature 或頁面需要的 sub-state

不管是哪個方法,原始的資料依然全部都存在 State裡頭,sub-state 的資料都是從原始 State 推導而來。

Q: 如果我的資料是用資料庫(或其他方式)儲存的,該怎麼辦?
A: 你應該在資料持久層上面再加一層存取層,由 Store去跟存取層溝通,由存取層決定該怎麼把資料實際存到資料庫(或其他地方)。

參考資料

讓 Xcode 8 再度支援 plugins

$
0
0

眾所皆知的,Xcode 8 把之前的 plugins 都擋掉了,然後推出了 Source Editor Extension,但 Source Editor Extension 的能力還很有限,基本上只能對「文字」操作。

所以就有人提出了一個解法,把 Xcode 8 app unsign,這樣就能讓 plugins 再度作用了。具體作法如下:

  1. 下載 MakeXcodePluginsWork
  2. chmod 755 makeXcodePluginsWork然後執行它
  3. 啟動 Xcode,原有的 plugins 都回來了

這樣會把 Xcode.app 的 signing 關掉,會變得比較不安全(其實就算跟之前一樣而已),所以請自己小心,後果自負。附帶一提,Xcode 8 開始內建一些功能,有些 plugins 可以退休了,例如:

  • 可以高亮度當前列,從【Preferences -> Fonts & Colors】設定「Current Line」顏色
  • 可以產生註解文件,熱鍵是【Command + Option + /】

為何 Startup 不該用 Swift

$
0
0

最近跟朋友聊天,聊到說我不建議 startup 使用 Swift 開發 app,趁著有空紀錄一下為何我會這麼說。

還是得先聲明一下,Swift 是一個很酷的語言,我沒有不喜歡它,只是站在公司的角度,我認為 startup 不應該使用 Swift 開發它們的主力產品,而是應該用 Objective-C。

主要是因為以下幾點理由:

Swift 還不穩定

Swift 是一個很新的語言,大家都還在摸索怎樣才是 best practice,但它同時也是一個快速成長與變化的語言,可能去年的 best practice 今年就不適用了。此外它也是一個尚未穩定的語言,剛推出的 Swift 3 還不能向下相容呢。

身為 iOS 開發者,每年都要為了升級 iOS 跟 Xcode 花一番心力。如果選擇用 Swift,那就得多花時間來處理 Swift 版本升級,甚至還要處理第三方套件因為升級而無法使用的問題。這是可以避免的,你只要一開始使用 Objective-C 開發即可。

對 startup 來說時間特別寶貴,我認為把時間花在升級 Swift 並不值得,更別說還得承擔升級之後 app 壞掉的風險。

將有經驗的工程師拒於門外

如果工程師只會 Swift 不會 Objecitve-C,代表他的 iOS 開發資歷並不久,如果使用 Objecitve-C 開發,至少可以確保找來的工程師經驗會多一點,對 startup 來說找到有經驗的人是很重要的事(雖然通常很難...)

也有一些有經驗的工程師因為種種原因還沒學習 Swift,如果哪天公司跟這樣的工程師搭上線,卻因為對方不會 Swift 所以無法提供更多有用的建議給你,這樣不是很可惜嗎?

Objective-C 沒有不好

如果你的公司裡頭有工程師提議要用 Swift 開發,理由是因為「Swift 很潮、想玩玩看」,那你們公司可能找了一位只想追求新技術的人。

使用新技術沒有不好,但要有充分的理由,你開公司找人來上班不是為了實驗新技術的。Objective-C 很成熟,無論是 best practice 或是資源都很完整,沒有什麼理由不用它。


以上都是站在 startup 的角度來說明為何不要用 Swift 開發主力產品,如果你們是一個大公司,或者工程團隊有強者坐鎮,或者你只是想寫個 side project 玩玩看,當然就無所謂。

一個 AFNetworking 的 retain cycle 問題

$
0
0

AFNetworking封裝了網路連線的許多工作,讓 iOS/Mac 開發者可以用簡潔的寫法去處理連線,但你知道要如何正確使用,才不會出現 retain cycle 嗎?

舉個例子

舉個最簡單的例子,我們可能會在自訂的 UIViewController 裡頭建立一個 AFHTTPSessionManager,透過它來進行網路連線,大部分的寫法大概如下:

@interface MyViewController ()
@property (nonatomic, strong) AFHTTPSessionManager *manager;
@end

@implementation MyViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.manager = [AFHTTPSessionManager manager];
    // Do something with self.manager...
}
@end

簡單直覺,對吧?但是你會發現在 view controller 被摧毀之後,這個 AFHTTPSessionManager還存留在記憶體裡面,不信的話可以用 Xcode 的 Memory Graph 檢查看看。

你不信邪,所以你可能會試著在 dealloc把它設為 nil,這樣總該沒問題了吧?

- (void)dealloc {
    _manager = nil;
}

可惜的是,這樣做沒有用,它依然存在記憶體裡。怎!麽!可!能!原因在一開始就提到了,因為它有 retain cycle。正確釋放的做法如下,你需要先呼叫 invalidateSessionCancelingTasks:這個函式:

- (void)dealloc {
    [_manager invalidateSessionCancelingTasks:YES];
    _manager = nil;
}

為什麼會這樣

因為 AFHTTPSessionManager擁有一個 NSURLSession *session property,而且把這個 session 的 delegate設為 self,而 NSURLSessiondelegate設為 retain。所以它們互相擁有彼此,造成了 retain cycle。

當我們呼叫 invalidateSessionCancelingTasks:函式,它會去呼叫 NSURLSessioninvalidateAndCancelfinishTasksAndInvalidate。根據蘋果文件,呼叫這兩個函式之後,NSURLSession才會斷開它與 delegate 的關聯。至此,才打破 retain cycle。

解法

有兩個解法,第一個就是如上所述,記得最後要呼叫 invalidateSessionCancelingTasks:來結束任務。第二個就是把 AFHTTPSessionManager寫成 singleton,這樣有 retain cycle 也無所謂了。


如何解決 NSTimer 造成的 retain cycle

$
0
0

故事是這麼開始的

最近在替公司 app 做健康檢查,找到一些 memory leaks 的問題,其中一個就是由 NSTimer所引起的 retain cycle。

NSTimer是個很容易造成 retain cycle 的物件,無論是新手或是老手都很可能一個不留意就踩到這個坑。舉個很常見的例子,這樣寫就產生 retain cycle 了:

@interface MyViewController()
@property (nonatomic, strong) NSTimer *timer;
@end
  
@implementation MyViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
}
- (void)dealloc {
  [_timer invalidate];
  _timer = nil;
}
- (void)timerFired:(NSTimer *timer) {
  // Do something...
}
@end

發生什麼事

根據文件說明

The timer maintains a strong reference to targetuntil it (the timer) is invalidated.

也就是說,view controller 擁有這個 timer,而 timer 也擁有 view controller。你可能會想說「我不是在 dealloc把 timer invalidate 了嗎?」但問題在於因為 retain cycle 已經造成 view controller 無法被釋放,所以 dealloc 不會被呼叫,timer 也就不會被 invalidate。

有人可能會想說「那我傳 weakSelf 給 target 不行嗎?」,答案是不行的,timer 依然會抓住 self 喔!

那如果我把 timer 改成 weak property呢?這是蘋果文件裡頭建議的寫法,也的確可以打破 retain cycle,但「self 會被 timer 抓住,timer 會被 runloop 抓住」,所以還是無法被釋放。

Note in particular that run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.

解法

常見的解法有兩種,第一種是透過 proxy (middleman) 連結 target 跟 timer,我覺得比較麻煩所以不採用,有興趣的人可以看看參考資料。第二種是利用 block,寫起來簡單許多,蘋果也在 iOS 10 開始提供相關的兩支 API:

  • + scheduledTimerWithTimeInterval:repeats:block:
  • + timerWithTimeInterval:repeats:block:

因為還要支援舊版的 iOS,所以我透過 category 幫 NSTimer 加上兩支名稱故意雷同的 API,檔案放在我的 GitHub。廢話不多說,直接看程式碼:

@interface NSTimer (Block)
+ (NSTimer *)cht_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
@end

@implementation NSTimer (Block)
+ (NSTimer *)cht_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block {
    NSTimer *t = [self scheduledTimerWithTimeInterval:interval
                                               target:self
                                             selector:@selector(cht_invokeBlock:)
                                             userInfo:[block copy]
                                              repeats:repeats];
    return t;
}

+ (void)cht_invokeBlock:(NSTimer *)timer {
    void (^block)(NSTimer *) = timer.userInfo;
    if (block) {
        block(timer);
    }
}
@end
為什麼 block 要回傳 timer?

因為有可能你沒有用一個 property 儲存 timer,但你有需要用到它,這時你就可以在 block 裡頭使用了。就算你有用 property 存起來好了,這樣的設計也可以讓你在 block 裡頭不必 weak-strong dance就能使用 timer。

為什麼 API 不提供 userInfo 設定

因為你現在可以使用 block而不是舊有的 target-action了,何必還要在 userInfo塞資料呢。而且假設提供 userInfo 設定的話,使用者還得知道 timer.userInfo的某個部份是 block,另一個部份才是他設定的資料,這樣很難用很容易出錯。

用法

程式碼還蠻好懂的,就不多做解釋了。有了 block 版本的 NSTimer 之後,開頭的例子就可以改寫成:

@interface MyViewController()
@property (nonatomic, weak) NSTimer *timer;  // 這裡用 weak 就可以了
@end

@implementation MyViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  __weak typeof(self)weakSelf = self;
  self.timer = [NSTimer cht_scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer *timer) {
    __strong typeof(weakSelf)self = weakSelf;
    [self timerFired:timer];
  }];
}
- (void)dealloc {
  [_timer invalidate];
  _timer = nil;
}
- (void)timerFired:(NSTimer *timer) {
  // Do something...
}
@end

參考資料

為何 Git-Flow 可能不適合你

$
0
0

什麼是 Git-Flow

Git-FlowVincent Driessen在 2010 年提出的一套 Git 分支模型,簡單的說,它有 masterdevelop這兩個主要的分支,以及 feature / release / hotfix這三個支援型分支,至於各個分支的用途看圖片應該就懂了,或是看原文有更詳細的說明。

由於當時大家對如何使用 Git 還處於摸索的階段,所以當這套規範被提出並且大家發現真的滿好用的之後,它很快就被廣泛的接受。

Git-Flow 適合什麼團隊或專案

沒有萬用解法是適合所有團隊的,你應該根據實際情況做出適當調整,況且 Git-Flow 是在 2010 年提出的,經過這麼多年的時空背景早就不一樣了。專案類型、成員對 Git 的熟悉度、產品釋出頻率、成員人數多寡等等因素都會影響到要使用怎樣的開發流程。

那麼誰適合使用 Git-Flow 呢?

  • 它的分支以及操作那麼多,所以我覺得團隊裡至少要有一兩位對 Git 有一定的熟悉度,這樣才清楚要怎麼操作並能夠指導其他成員。
  • 它有多條 feature分支,代表這個專案會有多人同時開發 feature 或解 bug。
  • 它有一個 hotfix分支,代表這個專案釋出新版本的速度應該不快,所以在下個版本釋出之前需要透過 hotfix 的做法修補錯誤。

如果你也覺得 Git-Flow 似乎不怎麼適合你們團隊卻又說不出個所以然來,看到這裡心裡應該有一些想法了。可能你們團員都不是很熟悉 Git,或是開發者只有一兩個人,或是專案釋出的速度很快或版本號對你們沒有意義(例如 web 或 backend 開發),那 GitHub Flow應該會更適合你們。或者你們團隊已經大到一個程度了,也可以考慮看看 Google 跟 Facebook 採用的 Trunk Model

我們的做法

首先要介紹一下我們的背景資料:

  • 團隊成員對 Git 的操作都有一定的熟悉度
  • 我們開發 iOS app
  • 我們提供 template app 給客戶,有些客戶需要做客製化
  • 我們有在跑 Scrum,大約每六週就會開發並驗收完一個新版本
  • 由於 iOS 需要送審,加上我們客戶很多,所以從內部驗收完到所有客戶上架新版本,大約還要 2-4 週

我們參考了 Git-Flow 並加以調整,最後我們有以下四種分支:master / develop / task / release

master

master上的每個 commit 都是正式發行的版本,所以每個 commit 都會打上一個 tag,例如 v1.2.3或是 v1.2.3-客戶A,我們把 master 分支當作版本倉庫,需要哪個版本就去 master 或是 tag 找。

develop

develop上的就是最新的程式碼,每個 commit 都要能夠編譯成功,我們的 nightly-build 就是抓 develop 分支。

task

不管是要做新的 feature 或是要解 issue,對開發者來說都是一種 task,這就是該分支的命名由來。task分支總是從 develop長出來,當某個 task 分支通過測試及 code review 之後,它會先 rebase 到最新的 develop,再 --no-ff的 merge 回 develop,然後這個 task 就可以砍掉了。

release

當該次開發週期要做的任務都告一段落了,我們就會從 develop開出一條 release分支,讓其他同仁做測試與驗收,如果測試驗收過程中有找到任何問題,就修在這條 release 分支上;如果很不幸的該次測試驗收的過程特別久,久到我們都開始開新的 task分支了,那我們就會定期地把最新的 release merge 回 develop,免得累積多了不好合回來。

最後測試驗收都通過,也成功上架 app store 了,我們就會做以下這些事:

  • 把該 release 分支 merge 回 develop。
  • 把該 release 分支的所有檔案(除了 .git資料夾)複製一份出來,切到 master分支然後把所有檔案(除了 .git資料夾)都刪除,然後把剛剛複製的檔案搬過來。你沒看錯,我們用直接覆蓋檔案的方式,而不是用 merge 指令,這樣會省掉很多麻煩。
  • master分支打上新的 tag。
  • 把該 release 分支砍掉。

或許你會好奇為何沒有 hotfix分支,這是因為我們新版本釋出週期夠短,與其額外開一個 hotfix,倒不如直接修正在下一版一起送審就好。以上就是我們 iOS 團隊所採用的流程,如果你也喜歡這樣的開發流程,歡迎成為我們的一員

如何為各個 Pod 指定 Swift 版本

$
0
0

最近升上 Swift 4.2,發現我用到的 Pods 有些還沒支援 4.2 導致編譯錯誤。解決方法也很簡單,只要指定每個 Pod target 的 SWIFT_VERSION4.0即可。

但是我們不能手動在 Xcode 裡頭調整,因為 CocoaPods 會把 Pods 的 SWIFT_VERSION設為跟你的 project 一樣,所以下次 pod install又會被改掉。

我們可以在 Podfilepost_install來自動修改,只要在 Podfile結尾加入以下片段即可。

post_install do |installer|
  installer.pods_project.targets.each do |target|
      # 我們也可以懶惰不用 if,讓所有 pod 的版本都設為一樣的
      if ['RxSwift', 'RxSwiftExt', 'RxCocoa', 'RxDataSources', 'ProtocolBuffers-Swift'].include? target.name
          target.build_configurations.each do |config|
              config.build_settings['SWIFT_VERSION'] = '4.0'
          end
      end
  end
end

參考來源:https://stackoverflow.com/a/46690240

iOS App 如何支援 RTL 語言

$
0
0

公司的產品越做越大,前一陣子幫公司的 app 加上阿拉伯文介面,開發過程也累積了一些支援 RTL 語系的心得,藉這個機會跟大家分享。

找到 Native Speaker

對於書寫方向是「從左到右」的我們來說,最困難的其實不是看不懂這個語言,而是不知道這樣的 layout 是否正確,因為 layout 絕對不是全部都無腦的換成「從右到左」就好。所以如果情況許可,最好找個 native speaker 讓團隊諮詢(例如找個當地的員工,或是請當地大學生來打工之類),這樣可以節省不少來回確認界面的時間。

仔細閱讀蘋果文件

很多開發的注意事項都寫在這份蘋果文件裡頭了,開發前跟開發時務必要多次閱讀,會有很大的幫助。另外,不只是 RD 需要閱讀這份文件,PM、QA、Designer 也應該看過,才不會發生 RD 做出正確界面,結果其他人以為是錯的(例如多媒體播放器的控制元件是不需要 RTL 的)。

程式開發的一些小技巧

圖片左右翻轉

如果有特殊需求的話,可以考慮對圖片 localized,這樣就可以提供適當翻轉過的圖片給特定語言,或是你也可以透過程式碼去翻轉圖片。但是假如你需要的就只是左右翻轉的圖片(例如 arrow 或 bullet-list 的圖片),可以很簡單的透過以下方式取得:

UIImage *image = [UIImage imageNamed:@"xxx"];
// 對圖片做一些必要處理
// .....
// 最後再翻轉圖片
image = [image imageFlippedForRightToLeftLayoutDirection];

要注意的是,翻轉一定要放在最後一步,這樣才會得到預期結果。

使用 Auto Layout 及相關技術

正確使用 auto layout 的 leadingtrailing,加上將 textAlignment設為 NSTextAlignmentNatural,就可以解決九成以上的 RTL 佈局。如果你的程式還沒支援 auto layout,可以趁機逐步轉換過去。

手動計算 Frame

由於種種因素讓你還無法轉成 auto layout(例如為了效能考量,或是有些地方就是用 frame 比較容易,或是轉換成本太大),這時候你就需要判斷現在是否在 RTL 環境來調整 frame。

if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) {
    // RTL 佈局
} else {
    // LTR 佈局
}
翻轉再翻轉

還有一種情況是不方便用 auto layout,但是手動計算 frame 又有很多額外因素要考量讓你不想計算。舉個常見的例子:有上下兩個可以左右滑動的 scroll view,上面是多個 tab,下面是點選 tab 之後要捲動到特定範圍。常見的做法是上方每個 tab 都有一個 index,下方則是根據選中的 index 計算 contentOffset。在 RTL 並且要手動計算 frame 的情況下,你會發現 index 處理起來很麻煩。

這裡有一個小技巧,我們可以先對最外層的 container view(例如 UIScrollView)左右翻轉,然後再對 subviews 左右翻轉一次。經過兩次翻轉,這些 subviews 就會從右到左排列,而且原有的程式碼幾乎不需要改動。這招或許看起來很 tricky,但它真的很有用,用得好可以節省非常多的時間。

if ([UIView userInterfaceLayoutDirectionForSemanticContentAttribute:view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft) {
    scrollView.transform = CGAffineTransformMakeScale(-1, 1);
    for (UIView *subview in scrollView.subviews) {
        subview.transform = CGAffineTransformMakeScale(-1, 1);
    }
}

以上就是我們在支援 RTL 時用到的所有方式,希望對大家有幫助。

本站搬家了

$
0
0

為了更高的彈性以及更好的資料自主性,我把部落格搬到 GitHub Page,這邊將不再維護。

想看最新文章的朋友,請移駕到 https://chiahsien.github.io/,它也有提供 RSS 訂閱服務喔。

Viewing all 20 articles
Browse latest View live