2015年11月26日 星期四

Swift Optionals

在swift中使用Optional來處理值缺失的狀況,如果看見一個variable是Optional類型,只有兩種可能,要嘛有值,要嘛就是nil。

宣告方式

var coffee:String?

等於下面這種宣告方式

var coffee: Optional<String>

由此可知 ? 其實是syntactic sugar。

看看Optional的原始宣告

public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)
    /// Construct a `nil` instance.
    public init()
    /// Construct a non-`nil` instance that stores `some`.
    public init(_ some: Wrapped)
    /// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
    @warn_unused_result
    public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
    /// Returns `nil` if `self` is nil, `f(self!)` otherwise.
    @warn_unused_result
    public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?
    /// Create an instance initialized with `nil`.
    public init(nilLiteral: ())
}

如果沒有賦值的話,Optional的預設值是nil。在Objective-C中,nil是一個0指標,在swift中nil是一個確定的值,任何Optioanl都可以設為nil。關於瞭解空值,可以參考悟空

其他關於Optional chaining以及一些更深度的剖析,可以參考喵神的文章或是玉令天下的Blog

另外有一種情況是如果Optional呼叫的function需要傳入Optional本身,像

class ViewController: UIViewController {

    let finishedMessage = "Network call has finished"
    let messageLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()

        someNetworkCall { [weak self] in
            self?.finished(self?.finishedMessage)
        }
    }

    func finished(message: String) {
        messageLabel.text = message
    }
}

這一段是編譯不過的,因為self?.finishedMessage是Optional chaining,而Optional chaining回傳的一定是Optional值。那麼應該怎麼解呢?

可以直接使用!,因為當self?為nil時不會繼續呼叫function,所以一旦呼叫了function就代表有值。

someNetworkCall { [weak self] in
            self?.finished(self!.finishedMessage)
}

詳情可以參考http://blog.xebia.com/swift-optional-chaining-and-method-argument-evaluation/

2015年11月4日 星期三

隱藏無內容的Self Sizing Cell

基本上看完优化UITableViewCell高度计算的那些事就可以大概瞭解從iOS7以後計算TableViewCell高度需要注意的事。

假設cell裡只有一個addressLabel,而且我們使用iOS8的self sizing,想讓無內容的cell高度變為0,也就是不顯示,可以把top跟bottom constraint的constant設為0。先把xib或storyboard中上下的constraint拉出IBOutlet到 .m 檔中,在cellForRowAtIndexPath的時候根據內容是否為空決定constant大小。

Constraint

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *simpleTableIdentifier = @"ListTableViewCell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier forIndexPath:indexPath];
    if (cell.addressLabel.text.length == 0) {
        cell.addressLabelTopConstraint.constant = 0;
        cell.addressLabelBottomConstraint.constant = 0;
    }
    else {
        cell.addressLabelTopConstraint.constant = 15;
        cell.addressLabelBottomConstraint.constant = 15;
    }
    [cell layoutIfNeeded];
    return cell;
}

addressLabel如果沒有內容,因為Label約束與上下邊緣一致 (constant為0),所以Label高度會被自動計算為0,且不會佔任何空間。

2015年10月30日 星期五

在iOS App裡加入Fan page與App Store link

Screenshot

在App裡面,我們經常會需要連結到自己的粉絲團,或是App Store頁面請使用者評分。
接下來講解怎麼實做。

連到粉絲團

下面這一段code的思考模式是這樣,我們會先用canOpenURL去檢查使用者是否安裝了Facebook app,
若有則用原生的facebook app開啟粉絲團頁面,若無則使用safari去開啟行動網頁。
如果不知道自己的粉絲團id可以使用 http://findmyfbid.com/ 查詢。

NSURL *facebookAppLink = [NSURL URLWithString:@"fb://profile/yourid"];
NSURL *facebookURL = [NSURL URLWithString:@"https://m.facebook.com/yourid"];

if ([[UIApplication sharedApplication] canOpenURL:facebookAppLink]) {
    [[UIApplication sharedApplication] openURL:facebookAppLink];
} else {
    [[UIApplication sharedApplication] openURL:facebookURL];
}

在iOS9你很可能會遇到這樣的錯誤

CanOpen[2255:1002610] -canOpenURL: failed for URL:
 "fb://" - error: "(null)"

這裡要特別注意的是,在iOS9之後必需要在info.plist裡面設置LSApplicationQueriesSchemes,
這樣canOpenURL才會回傳YES,沒有設定他只會回傳NO。所以要到info.plist裡面加入
LSApplicationQueriesSchemes,新增一個fb item,類別是String,這樣就可以了。

scheme

圖片裡有很多item是因為我有整合facebook登入,如果要打開粉絲團頁面其實只需要fb那個就可以。

App Store頁面

網路上有很多人採用openURL的方式來打開App Store連結,如下所示。
但我個人覺得這個方式打開的速度有點慢,因為它會先打開safari,然後再跳轉到原生App Store裡面,
大概會lag幾秒,使用者體驗不是很優。

NSString *iTunesLink = @"itms://itunes.apple.com/us/app/apple-store/id375380948?mt=8";
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:iTunesLink]];

其實在iOS6之後就有新增一個SKStoreProductViewController類別可以在App內打開App Store頁面,
不用跳出自己的App就可以讓使用者評分了,這樣感覺是不是好很多了?

首先引入StoreKit,並讓自己的ViewController遵從SKStoreProductViewControllerDelegate。

@import StoreKit;

@interface ViewController : UIViewController<SKStoreProductViewControllerDelegate>
@end

打開頁面

- (void)openAppStorePage
{
    NSString *cAppleID = @"your app id";
    if ([SKStoreProductViewController class]) {
        SKStoreProductViewController *storeViewController = [[SKStoreProductViewController alloc] init];
        storeViewController.delegate = self;
        NSDictionary *dict = [NSDictionary dictionaryWithObject:cAppleID forKey: SKStoreProductParameterITunesItemIdentifier];
        [storeViewController loadProductWithParameters:dict completionBlock:^(BOOL result, NSError * _Nullable error) {
            if (result) {
                [self.navigationController presentViewController:storeViewController animated:YES completion:nil];
            }
        }];
    }

按下關閉時要dismiss目前顯示的頁面。

- (void)productViewControllerDidFinish:(SKStoreProductViewController *)viewController {
    [viewController dismissViewControllerAnimated: YES completion: nil];
}

2015年10月20日 星期二

整合Parse與Facebook登入

其實已經有人整理好一篇文章了,照著步驟做應該沒有什麼問題。不過呢,我們至少要知道為什麼要這樣做。Apple在iOS9預設HTTP必須走TLS1.2,也就是HTTPS的形式。不過看起來目前FB的Server還沒有支援,所以必須設置例外讓連線可以接通。關於ATS以及詳細的資訊可以參考iOS9网络适配_ATS:改用更安全的HTTPS

另外,如果要對UI做本地化(Localization),記得要新增一個ParseUI.strings而不是使用原本的Localizable.strings。

客制化按鈕顏色的話,要先去除backgroundImage再設定backgroundColor。

[self.logInView.logInButton setBackgroundImage:nil forState:UIControlStateNormal];
self.logInView.logInButton.backgroundColor = [UIColor orangeColor];