Define Macros in Xcode

很久沒用objc...快忘光...

define.h
// 測試站
//#define APP_FOR_ISOBAR
// 客戶內部測試站
//#define APP_FOR_CLIENT_TEST
// 正式站
#define APP_FOR_RELEASE

#ifdef APP_FOR_ISOBAR
  #define BASE_URL            (@"http://4demo.isobar.com.tw/API/")
#endif

#ifdef APP_FOR_CLIENT_TEST
  // fixed: Xcode "macro redefined" warning
  #undef BASE_URL
  #define BASE_URL          (@"http://192.168.1.1/demo/API/")
#endif

#ifdef APP_FOR_RELEASE
  #undef BASE_URL
  #define BASE_URL        (@"https://demo.isobar.com.tw/api/")
#endif

// through Xcode(simulators and devices)
#if DEBUG
// ...
// distribute app to apple store
#else
// ...
#endif

iBeacon background monitoring in iOS9

先確定可以從背景取得 location 資訊

首先開啟 Xcode background modes 設定

確認 Info.plist

<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>location</string>

Location Services 取得權限為 "Always"
locationManager!.requestAlwaysAuthorization()

locationManager = CLLocationManager()
locationManager!.requestAlwaysAuthorization()
locationManager!.desiredAccuracy = kCLLocationAccuracyBest
locationManager!.allowsBackgroundLocationUpdates = true
locationManager!.pausesLocationUpdatesAutomatically = false
locationManager!.delegate = self

偵測 iBeacon

當 App 存在前景或背景,可以使用 startRangingBeaconsInRegion 偵測 Beacon

偵測到後會觸發下列函式:

func locationManager(manager: CLLocationManager, didRangeBeacons beacons: [CLBeacon], inRegion region: CLBeaconRegion)

當 App 不執行的狀態下,可以使用 startMonitoringForRegion 偵測 Beacon

偵測到後會觸發下列函式:

func locationManager(manager: CLLocationManager, didDetermineState state: CLRegionState, forRegion region: CLRegion)
func locationManager(manager: CLLocationManager, didEnterRegion region: CLRegion)
func locationManager(manager: CLLocationManager, didExitRegion region: CLRegion)

在 App 不執行的狀態下,此時可以發送本地推播?
或是進行 http request/response?
甚至連接藍芽裝置嗎?
答案是可以

如果要發送本地推播,記得要註冊

let notificationType:UIUserNotificationType = [UIUserNotificationType.Sound, UIUserNotificationType.Alert]
let notificationSettings = UIUserNotificationSettings(forTypes: notificationType, categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(notificationSettings)

App Transport Security Policy in iOS9

iOS9 預設強制所有的網路傳輸都要走 https 不然會噴 error

App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.

有支援 https 但不符合 TLS v1.2 也會出錯

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

可以用下列方式檢測 URL 是否符合 ATS 的規範
nscurl -v --ats-diagnostics https://apple.com
如果真的沒辦法支援,可以直接在 Info.plist 關掉 ATS

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    <dict>

Certificates, Identifiers, Devices & Provisioning Profiles

首先大家要知道,開發 apple 的程式一定要先有蘋果開發者帳戶,
沒有這個帳戶下面都不用講。
進入Member後,操作畫面如圖


右側會有四塊區域,分別如下

Certificates(憑證)

可以想像成這個憑證代表開發者自己,蘋果是藉由這個憑證來認定你是否可以開發或是發佈這個 App。

憑證要如何製作呢?
製作憑證需要上傳開發者自己的 CSR(Cert Signing Request),CSR 可以從 keychain 工具產生,
而 XCode 6 之後會自動產生 Developer/Production 憑證。

如何備份私鑰

Identifiers

ipa 裡指的就是 entitlements,也就是這個 APP 可以使用哪些蘋果的服務,像是
播播
票卷
群組應用程式
...
等功能

Devices(UDID)

每隻手機獨一無二的編號,用來分辦手機。

Profiles(Provisioning Profiles)中文又稱描述檔

開發者最常搞不清楚描述檔與憑證有什麼不同,又兩者有什麼關係,
被搞得一個頭兩個大。
描述檔主要包含四大部份,

  • App ID
  • Entitlements
  • UUID List
  • 憑證

先看看下面這張圖,


image error
簡單瞭解一下 ipa 如何安裝到 iOS 裝置上,
App 編繹完後,會用開發者自己的憑證(私鑰)打包產生 ipa,
使用者下載安裝這個 ipa 的時候,
ipa 裡的 Bundle ID 需要符合描述檔裡的 App ID,
ipa 裡的 Entitlements 需要符合描述檔裡的 Entitlements,
裝置的 UDID 需要列在描述檔 UUID List 裡頭,
ipa 的這組憑證(私鑰)需與描述檔裡的憑證(公鑰)配對,
配對失敗的話,蘋果認定你不是這個 APP 的開發者,而不進行安裝,
如果通過上述測試,就可以成功安裝到 iOS 裝置上。

到底有幾種描述檔?

開發用的描述檔(Development)

  • 開發階段使用,需要有開發者憑證,裝置需加入 UUID List 才可以安裝。

Ad-Hoc測試用的描述檔(Ad-Hoc)

  • Ad-Hoc 測試用,通常拿來測推播,需要有 Production 憑證,裝置需加入 UUID List 才可以安裝。

企業用的描述檔(Enterprise)

  • 基本上同 Ad-Hoc 描述檔,但沒有 UUID 數目的限製。

商店上架用的描述檔(Apple Store)

  • 只供上傳 Apple Store 審核用,沒有 UUID List。

事實上 ipa 通過 Apple 審核後,會重新打包,發佈到 Apple Store,
重新打包??
是的,Apple 會用自家專屬的憑證及描述檔重新打包,之後這個 ipa 就像有
萬能鑰匙一般可以安裝到任意 iOS 裝置上沒有任何限制。

這裡提供兩個指令方便查詢描述檔,如下:
checking provisioning profile
security cms -D -i Payload/XXX.app/embedded.mobileprovision

checking entitlements
codesign -d --entitlements - Payload/XXX.app

Itunes Connect Status Pending Contract

因為沒即時繳保護費,造成 iOS developer membership 過期,
後來雖然馬上重新 renew 但所有的 App 已下架,
renew 後 Free App 的狀態都呈現 1.4 Pending Contract
google 後有人提到把價格設為Free可以解決這個問題,
iTunes Connect -> 我的 App -> 價格 -> 設定成 Free -> Save
但是後台一直出現無法儲存的錯誤,最終我的解法
先將價格設為Tier,儲存成功後,再調為Free就一切正常。

提外話,好孩子還是應該要早早去 renew 呀...

If your Apple Developer Program membership expires, your apps will no longer be available for download and you will not be able to submit new apps or updates. You will lose access to pre-release software, Certificates, Identifiers & Profiles, and Technical Support Incidents. However, your apps will still function for users who have already installed or downloaded them, and you will still have access to iTunes Connect and free development resources.

If your Apple Developer Enterprise Program membership expires, your apps will no longer be available for download and will no longer function for those who have already installed or downloaded them. You will still have access to free development resources.

ref

Realm Memo

Realm 0.91.5

Creating Realm Models

定義各個屬性欄位為 NSString/NSInteger/BOOL...
groups 是特有的 RLMArray<Group> 陣列

接著看狀況設定下面幾個 methods
primaryKey: 這是最基本的,通常會設 primary key
indexedProperties: 設定 index,可以指定多個 property
defaultPropertyValues: property 預設值

另外在 Group 裡,建立 inverse relationship to Person.groups
linkingPerson: 這在計算群組裡有多少人的時候很好用

@interface Person : RLMObject

@property NSString *sno;
@property NSString  *name;
@property NSInteger gender;
@property NSDate    *birthday;
@property NSString  *phone;
@property NSString  *address;
@property NSString  *phoneForHome;
@property NSString  *phoneForOffice;
@property NSString  *email;
@property NSString  *reference;
@property NSString  *company;
@property NSString  *economy;
@property BOOL      maritalStatus;
@property NSString  *children;
@property NSString  *total;
@property BOOL      otherFilter;
@property NSString  *otherFilterType;
@property NSString  *note;
@property RLMArray<Group> *groups;

@end

@implementation Person

+ (NSString *)primaryKey {
  return @"sno";
}

+ (NSArray *)indexedProperties {
  return @[@"name"];
}

+ (NSDictionary *)defaultPropertyValues {
  return @{
           @"sno":[NSString stringWithFormat:@"%f",[[NSDate date] timeIntervalSince1970]],
           @"gender":@0,
           @"birthday":[NSDate dateWithTimeIntervalSince1970:0],
           @"address":@"",
           @"phoneForHome":@"",
           @"phoneForOffice":@"",
           @"email":@"",
           @"reference":@"",
           @"company":@"",
           @"economy":@"",
           @"maritalStatus":@NO,
           @"children":@"",
           @"total":@"",
           @"otherFilter":@NO,
           @"otherFilterType":@"",
           @"note":@""
           };
}

@end

@interface Group : RLMObject

@property NSString *name;
@property (readonly) NSArray *linkingPerson;

@end
RLM_ARRAY_TYPE(Group)

@implementation Group

+ (NSString *)primaryKey {
  return @"name";
}

// Define "linkingPerson" as the inverse relationship to Person.groups
- (NSArray *)linkingPerson {
  return [self linkingObjectsOfClass:@"Person" forProperty:@"groups"];
}

@end

新增,有設 primaryKey 才可以用 createOrUpdateInDefaultRealmWith...

RLMRealm *realm = [RLMRealm defaultRealm];
Group *group = [Group new];
group.name = groupName;
[realm beginWriteTransaction];
[Group createOrUpdateInDefaultRealmWithObject:group];
[realm commitWriteTransaction];

刪除

[realm beginWriteTransaction];
[realm deleteObject:[_arrDataSource[indexPath.section] objectAtIndex:indexPath.row]];
[realm commitWriteTransaction];

刪除所有 objects

[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];

Block

[realm transactionWithBlock:^{
    [realm deleteObject:[MyCustomRealmObject objectForPrimaryKey:@"1234567890"]];
}];

新增多筆資料

// creating _sortedArray
[_sortedArray addObject:@{
                 @"name":[self contactName:contact],
                 @"phone":[self contactPhonesFirstLabel:contact],
                 @"phoneForHome":[self contactPhones:contact],
                 @"email":[self contactEmails:contact],
                 @"company":contact.company.length > 1 ? contact.company : @"",
                 @"note":contact.note.length > 1 ? contact.note : @""
                 }];
[realm beginWriteTransaction];
for (NSDictionary *dic in _sortedArray) {
  [Person createInDefaultRealmWithObject:dic];
}
[realm commitWriteTransaction];

Query all objects

RLMResults *person = [Person allObjects];

查詢 name="groupA"

[Group objectsWhere:@"name=%@",@"groupA"];

Checking first object

[Group objectsWhere:@"name=%@",@"groupA"].firstObject;

不分大小寫

[Person objectsWhere:@"name contains[c] %@",searchText];

搜詢 Person.groups 裡有 currentGroup 的所有人

[[Person allObjects] objectsWhere:@"ANY groups = %@", _currentGroup];

currentGroup 裡有多少人(要設定 linkingPerson)

[[_currentGroup.linkingPerson valueForKeyPath:@"name"] count];

Notifications

realm 有更新的時候,送出通知,用來更新 reload tableView 很好用。

@property (nonatomic, strong) RLMNotificationToken *notification;
__weak typeof(self) weakSelf = self;
_notification = [RLMRealm.defaultRealm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
    //...
  [weakSelf.tableView reloadData];
}];

remove notification

[RLMRealm.defaultRealm removeNotification:_notification];

Transparent Modal View Controller

iOS8 下很簡單,在 Storyboard 中將 Modal View Controller 設定為
Transition Style: Cover Vertical
Presentation: Over Current Context

iOS7 用上述的方式疊上去後,底下的 vc 會自動被移除,解法是用 UIViewControllerTransitioningDelegate
自行實作轉場動態,這裡是拿 pop 及 popping 來處理轉場動態。
先安裝 pod 'pop'
參考 popping 下的 PresentingAnimator.h/DismissingAnimator.h

#import "PresentingAnimator.h"
#import "DismissingAnimator.h"

#pragma mark - Button Actions
- (IBAction)showingAlert:(id)sender
{
    AlertForSkipImportAddressBookViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:@"AlertForSkipImportAddressBookViewControllerID"];
    vc.delegate = self;
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
      [self presentViewController:vc animated:YES completion:nil];  
    }
    else {
    /*Fixed transparent vc is not working in iOS7*/
      vc.transitioningDelegate = self;
      vc.modalPresentationStyle = UIModalPresentationCustom;
      [self presentViewController:vc animated:YES completion:nil];
    }
}

#pragma mark - UIViewControllerTransitioningDelegate
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
  return [PresentingAnimator new];
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
  return [DismissingAnimator new];
}

#pragma mark - AlertForSkipImportAddressBookView Delegate
- (void)clickAlertButtonAtIndex:(NSInteger)buttonIndex
{
  switch (buttonIndex) {
    case AlertButtonIndexCancel:
      break;
      
    case AlertButtonIndexSkip:
      break;

    default:
      break;
  }
}

BlocksKit Memo

列出幾個常用到的地方

UIAlertView

#import <UIControl+BlocksKit.h>

UIAlertView *alertView = [[UIAlertView alloc] bk_initWithTitle:@"系統訊息" message:@"有新版本可供下載,請即刻更新!!"];
[alertView bk_addButtonWithTitle:@"升級" handler:^{
    //...
}];
[alertView show];

UIButton

__weak __typeof__(self) weakSelf = self;
if ([btnForRemove bk_hasEventHandlersForControlEvents:UIControlEventTouchUpInside]) {
  [btnForRemove bk_removeEventHandlersForControlEvents:UIControlEventTouchUpInside];
}
[btnForRemove bk_addEventHandler:^(id sender) {
  __strong __typeof__(weakSelf) self = weakSelf;
  AlertForMyCollectsViewController *vc =  [self.storyboard instantiateViewControllerWithIdentifier:@"AlertForMyCollectsViewControllerID"];
  vc.delegate = self;
  if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
    [self presentViewController:vc animated:YES completion:nil];
  }
  else {
    // Fixed transparent vc is not working in iOS7
    vc.transitioningDelegate = self;
    vc.modalPresentationStyle = UIModalPresentationCustom;
    [self presentViewController:vc animated:YES completion:nil];
  }
} forControlEvents:UIControlEventTouchUpInside];

UIWebView

[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:_item.itemUrl]]];
[self.webView bk_setShouldStartLoadBlock:^BOOL(UIWebView *webView, NSURLRequest *request, UIWebViewNavigationType navigationType) {
  NSString *buf = [[request URL] relativePath];
  if ([buf rangeOfString:@"water-video.html"].location != NSNotFound) {
  //...
  }
  return YES;
}];

Git Memo

在 git commit 後,發現打錯字,修正最後一次 commit。
git commit --amend

在 git commit 後,想回到某個特定的版本,
預設為 soft 只會抹掉 commit,修改的內容及檔案還是保留,
hard 則是全部回覆。
git reset
git reset --hard

上面的情境,git reset 後又反悔,該怎麼辦?

git rebase -i
進入 vim
1 pick 175a270 Prepare for 2.42
2 pick afd9e00 Prepare for 2.42(20150618).
3 pick 110d487 Fixed Bug-893.
改成
1 pick 110d487 Fixed Bug-893.
2 pick 175a270 Prepare for 2.42
3 fixup afd9e00 Prepare for 2.42(20150618).

fixup: 合併 175a270 與 110d480 且不保留 175a270 commit message.
squash: 同上,唯一不同是保留 commit message.

實作 iPhone 聯絡人的顯示方式

實作 iPhone 聯絡人的顯示方式,中文是照筆畫及英文排序。

20150612 更新:
排序的時候呼叫 localizedCompare: 即可,像是簡體中文下是用拼音排序,繁體中文下是用筆劃排序。

NSArray *sortedArray = [aa sortedArrayUsingSelector:@selector(localizedCompare:)];

20150419 更新:
其實很簡單只要排序的時候指定語系,即可以照筆畫排序。

NSArray *arr = @[
                 @"大魚兒",
                 @"Jason",
                 @"肥貓貓",
                 @"Service",
                 @"辦公室",
                 @"Superman",
                 @"Bob",
                 @"二哥"];

// 指定 locale
NSLocale *strokeSortingLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh_TW"];
arr = [arr sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
  return [obj1 compare:obj2 options:0 range:NSMakeRange(0, [obj1 length]) locale:strokeSortingLocale];
}];

接下來要將相同筆畫數的字視為同一個群組,再處理這個問題前,
會需要知道到底要分多少群組呢? 當然你可以自訂 A/B/C...Z ,
或者是用 UILocalizedIndexedCollation 來取得當下語系的群組,
以 zh_TW 為例會有 1畫/2畫/3畫...A/B/C/...Z/# 這幾個,
再來利用 sectionForObject 將字串放到對應的 section 裡。

// 取得群組: 1畫/2畫/3畫...A/B/C/...Z/#
//NSLog(@"sectionTitles=%@",[[UILocalizedIndexedCollation currentCollation] sectionTitles]);
// 總共幾個群組
NSInteger sectionTitlesCount = [[[UILocalizedIndexedCollation currentCollation] sectionTitles] count];
// 建立群組陣列
NSMutableArray *mutableSections = [[NSMutableArray alloc] initWithCapacity:sectionTitlesCount];
for (NSUInteger idx = 0; idx < sectionTitlesCount; idx++) {
  [mutableSections addObject:[NSMutableArray array]];
}
// 將每個物件放到對應的 section
for (id object in objects) {
  NSInteger sectionNumber = [[UILocalizedIndexedCollation currentCollation] sectionForObject:object collationStringSelector:@selector(description)];
  [[mutableSections objectAtIndex:sectionNumber] addObject:object];
}

實際發佈到手機測試的時候,雖然手機設定為台灣,卻一直抓到
英語系的群組(A/B/...Z),google 後發現這裡用到 currentCollation
需要在 Info.plist 指定 Localized resources can be mixed = YES
才解決這個問題。

code

ref1
ref2