Checking iOS Device Type

通常我們會用 UI_USER_INTERFACE_IDIOM 來做檢查

(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

但如果 xCode Project Devices 沒有選 iPad 或是 Universal 這招就會失效,
只好改用判斷 model 的方式來處理。

([[[UIDevice currentDevice] model] rangeOfString:@"iPhone"].location == NSNotFound)

Parsing parameters from URL scheme in iOS

// ie: http://todolist?tk=tokenString&did=123-321-111-222-333&account=0DD3223&id=3547
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
  if ([[url scheme] isEqualToString:@"todolist"]) {
    NSString *query = [url query];
    if (query.length > 0) {
      NSArray *components = [query componentsSeparatedByString:@"&"];
      NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
      for (NSString *component in components) {
        NSArray *subcomponents = [component componentsSeparatedByString:@"="];
        [parameters setObject:[[subcomponents objectAtIndex:1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]
                       forKey:[[subcomponents objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
      }
      return YES;
    }
  }
  return NO;
}

Reference

Share data between two apps using App Groups in iOS8

iOS8 有提供一個新的功能 App Groups,可以讓兩個獨立的 app 互相共享資料

首先新增一個共用的 App Group Name

Enable App Group Service

Enable App Group in xCode

接下來用 NSUserDefaults initWithSuiteName 初始化,就可以進行簡單的共享資料。

//var myShareDefaults = NSUserDefaults(suiteName: "group.com.xxx.AppGroupsDemo")
NSUserDefaults *myShareDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.xxx.AppGroupsDemo"];

完整的範例在此

Handling Global iOS Uncaught Exception

xCode debug 的時候常常會丟出一些不是人看的 exception 資訊,往往找到問題點時已花費不少時間,這裡提供兩個小技巧解決這個問題。

NSSetUncaughtExceptionHandler

void uncaughtExceptionHandler(NSException *exception) {
    NSLog(@"CRASH: %@", exception);
    NSLog(@"Stack Trace: %@", [exception callStackSymbols]);
    // Internal error reporting
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{   
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
    // ...
}

在 xCode 裡可以直接加入 exception breakpoint 更方便。
command + 7 -> Add Exception Breakpoint

Creating Custom iOS User Interfaces

Make your app stand out

When Should You Build Custom UI?

  • Where will this be used?
  • Dose it need to support multiple states?
  • Can this be shared across serveral apps?
  • What APIs can be leveraged?

UIAppearance

  • UIImageRenderingMode

Dynamic Type

Accessibility

Localization

Spring Animations

Where might you use them?

  • 不想用傳統線性動態
  • 想符合原生 UIKit 的動態
  • 接近 iOS 原生動態
  • 較為自然的動態
  • 用於任意動態的屬性(alpha...)而不是只有位置
// presents
[UIView animateWithDuration:kTRANSITION_DURATION
        delay:0
        usingSpringWithDamping:0.75
        initialSpringVelocity:10
        options:0
        animations:^{ ... }];
// dismiss
[UIView animateWithDuration:kTRANSITION_DURATION - 0.1
        delay:0
        usingSpringWithDamping:1
        initialSpringVelocity:0
        options:0
        animations:^{ ... }];

Rendering in iOS

Rendering Pipeline

  1. Application - commit transaction
  2. Render server
  3. GPU
  4. Display

Commit Transaction

  • Set up and perform view layout
  • View and string drawing
  • Addition Core Animation work
  • Package up layer and send them to render server

Fast, Static Blur

drawViewHierarchyInRect:afterScreenUpdates:

UIVisualEffectView

New API for creating visual effects
Two effect types

  • Live blur
  • Vibrancy(通常用於文字,提升可讀性,不受背景模糊效果影響)

UIBlurEffect - Three styles

  • Dark
  • Light
  • ExtraLight

UIVibrancyEffect

儘可能避免

  • alpha 與 blur 混用
  • mask
  • groups
    • Animation groups
    • Opacity group

Rendering cost

  1. Standard view(效能最好)
  2. Standard view with static blur
  3. Blur effect
  4. Blur effect with vibrancy(效能最差)

CAShapeLayer

Path

bezierPath = [UIBezierPath bezierPath];
[bezierPath moveToPoint:CGPointMake(0,220)];
[bezierPath addCurveToPoint:CGPointMake(160, 115)
            controlPoint:CGPointMake(0,220)
            controlPoint:CGPointMake(15,100)];
...
[shapeLayer setPath:bezierPath.CGPath];

Line

  • lineCap(線頭的樣式:平/圓弧...)
  • lineDashPattern(虛線的樣式)
  • lineDashPhase(虛線的樣式)

Stroke

  • strokeColor(線的顏色)
  • strokeBegin(從頭算起幾%開始填色)
  • strokeEnd(從尾算起)

CAShapeLayer 是用 CPU 運算,再丟給 render server
當有複雜圖形時,將會花費大量的 CPU time。

+ (Class)layerClass {
  return [CAShapeLayer class];
}

- (void)awakeFromNib {
  CAShapeLayer *layer = (CAShapeLayer *)self.layer;
  CGPathRef path = CGPathCreateWithRect(self.bounds, NULL);
  [layer setPath:path];
  [layer setLineWidth:6];
  ...

}

Dynamic Core Animation Behaviors

CAAction Protocol

- (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict {
  ...
}

CALayerDelegate method

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
  if ([event isEqualToString:@"opacity"]) {
    return [[APPLOpacityAction alloc] init];
  }
  return [super actionForLayer:layer forKey:event];
}

Summary

  • Spring animations
  • UIVisualEffectView
  • CAShapeLayer
  • Dynamic Core Animation behaviors

Intermediate Swift

Intermediate Swift - Session 403

Options

String to Integer
Optional Type
var optionalNumber:Int?
// default initialized to nil
optionalNumber = 6

Non-Optional Types can't be nil
var myString:String = nil
// error

Optional Return Types

func findIndexOfString(string:String, array:String[]) -> Int? {
    for (index, value) in enumerate(array) {
        if value == string {
            return index
        }
    }
    return nil
}

Optional Binding

Test and unwrap at the same time

var neighbors = ["Alex", "Anna", "Madison", "Dave"]
let index = findIndexOfString("Anna", neighbors)
//if let indexValue = index { // index is of type Int, not nil?, indexValue is of type Int
if let indexValue = findIndexOfString("Anna", neighbors) { // index is of type Int, not nil?, indexValue is of type Int
 println("Hello, \(neighbors[indexValue])")
}
else {
    println("Must've moved away")
}

Optional Chaining

if let addressNumber = paul.residence?.address?.buildingNumber?.toInt() {
addToDatabase("Paul", addressNumber)
}

  • Use if let optional binding to test and unwrap at the same time
  • Option chaining (?) is a concise way to work with chained optionals

Memory Management

Automatic Reference Counting
Weak References are optional values
Binding the optional produces a strong reference

if let tenant = apt.tenant {
    tenant.buzzIn()
}
apt.tenant?.buzzIn()

Unowned References

Strong, Weak, and Unowned References
strong references are default

Initialization

Every value must be initialized before it is used.

class RaceCar:Car {
    var hasTurbo:Bool
    init(color:Color, turbo:Bool) {
        hasTurbo = turbo
        super.init(color:color)
    }
    // 多個 initializers
 convenience init(color:Color) {
        self.init(color:color, turbo:true)
    }

    convenience init() {
        self.init(color:Color(gray:0.4))
    }
}
class FormulaOne:RaceCar {
    let minimumWeight = 642

    init(Color:Color) {
        self.init(color:color, turbo:false)
    }
}

Lazy Properties

@lazy var multiplayerManager = MultiplayerManager()

Deinitialization

class FileHandle {
    let fileDescriptor:FileDescriptor
    init(path:String) {
        fileDescriptior = openFile(path)
    }
    deinit {
        closeFile(fileDescriptor)
    }
}

Initialize all values before you use them
set all stored properties first, then call super.init

Designated initializers only delegate up
Convenience initializers only delegate across

Deinitializers are there... if youe need them

Closures

var clients = ["Pestov", "Buenaventura", "Sreeram", "Babbage"]
clients.sort({(a:String, b:String) -> Bool in
  return a < b
})
println(clients)

Type Inference 簡化成

struct Array<String> {
  func sort(order:(String, String) -> Bool)
}

clients.sort({a, b in a < b})
Implicit Arguments

clients.sort({ $0 < $1 })

Trailing Closures

client.sort { $0 < $1 }

Functional Programming
println(words.filter { $0.hasSuffix("gry") })
// angry
// hungry
println(words.filter { $0.hasSuffix("gry") }
  .map { $0.uppercaseString })
// ANGRY
// HUNGRY
Function Values - Closures
numbers.map {
//  sum += $0
  println($0)
}
// 可以簡化成
numbers.map(println)

var indexes = NSMutableIndexSet()
numbers.map {
  indexes.addIndex($0)
}
// 可以簡化成
numbers.map(indexes.addIndex)
Capture Lists
class TempNotifier {
    var onChange:(Int) -> Void = {}
    var currentTemp = 72

    init() {
        onChange = {[unowned self] temp in
            self.currentTemp = temp
        }
    }
}
Pattern Matching
Validating a Property List
func stateFromPlist(list:Dictionary<String, AnyObject>) -> State?
// 正常狀況
stateFromPlist("name": "California"
                             "population", 38_040_000,
                             "abbr": "CA")
// 當傳入錯誤的型別
// ie: population 應該要傳入數字,卻傳入字串,需要回傳 nil
stateFromPlist("name": "California"
                            "population", "hella peeps",
                            "abbr": "CA")

可以用這樣的方式檢查

func stateFromPlist(list:Dictionary<String, AnyObject>) -> State? {
    var name:NSString?
    switch list["name"] {
        case .Some(let listName as NSString):
            name = listName
        // ...
     default:
            name = nil
    }
    return name
}

有更好的方法

func stateFromPlist(list:Dictionary<String, AnyObject>) -> State? {
    switch (list["name"], list["population"], list["abbr"]) {
        case (.Some(let listName as NSString),
                    .Some(let pop as NSNumber),
                    .Some(let abbr as NSString))
        where abbr.length ==2:
            // 用 where 檢查 abbr 長度是否為2
         return State(name:listName, population:pop, abbr:abbr)
        default:
            return nil
    }
}

總結

  • Optionals
  • Memory management
  • Initialization
  • Closures
  • Pattern matching

PonyDebugger

官網上寫這樣裝

curl -sk https://cloud.github.com/downloads/square/PonyDebugger/bootstrap-ponyd.py | python - --ponyd-symlink=/usr/local/bin/ponyd ~/Library/PonyDebugger

但實際在 OSX 10.9.4 上安裝卻會有問題,稍微改一下

curl -s https://cloud.github.com/downloads/square/PonyDebugger/bootstrap-ponyd.py | python - --ponyd-symlink=/usr/local/bin/ponyd ~/Library/PonyDebugger
source ~/Library/PonyDebugger/bin/activate
pip install -U -e git+https://github.com/square/PonyDebugger.git#egg=ponydebugger --allow-external pybonjour --allow-unverified pybonjour
ponyd update-devtools

接下來直接從 CocoaPods 安裝 PonyDebugger

程式中加上:

#import <PonyDebugger/PonyDebugger.h>

PDDebugger *debugger = [PDDebugger defaultInstance];
[debugger connectToURL:[NSURL URLWithString:@"ws://localhost:9000/device"]];
[debugger enableNetworkTrafficDebugging];
[debugger forwardAllNetworkTraffic];

ponyd 跑起來後,就可以用 browser 開啟 http://localhost:9000/ 進行除錯。
./ponyd serve --listen-interface=127.0.0.1

UITableViewHeaderFooterView Crash Bug in iOS8

iOS8 下存取 UITableViewHeaderFooterView 在某些情況下會 crash...
只好先用下面的方式修正..

if([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
  UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *) view;
  ...
}