ぴよログ

↓に移転したのでこっちは更新されません、多分。

AFHTTPSessionManagerでHTTPBodyを設定する

AFNetworkingのAFHTTPSessionManagerを使っていてBodyに素のXMLを入れてPOSTしたかったのだが、そのやり方がなかなか見当たらずソースコードを読んで解決したのでメモしておく。

ざっくり言うとAFHTTPSessionManagerでのPOSTはこんな感じに行う。

let manager = AFHTTPSessionManager()
manager.POST(url, parameters: params, success: { (task, response) -> Void in
    // success
}) { (task, error) -> Void in
    // failure
}

POSTするパラメータがあれば、上のメソッドparametersのところにdictionaryとかで渡せばいいはずだが(確か)、今回やりたいのはBodyにXMLのテキストを入れるということだった。

まず気軽にparametersにXML文字列を直接渡してみようなどとやってみたら失敗した。

let manager = AFHTTPSessionManager()
let xmlstr = "<?xml ..."
manager.POST(url, parameters: xmlstr, success: { (task, response) -> Void in
    // success
}) { (task, error) -> Void in
    // failure
}

これでできたリクエストを見てみるとBodyにはこんなのが入っていた。

(null)=%3C%3Fxml%20...

どうやら名前が空で値がURLエンコード済文字列のパラメータをBodyとして設定してしまっているらしい。

ソースコードを読むとそれがデフォルトの動作だということがわかった。

// AFURLRequestSerialization.m
switch (self.queryStringSerializationStyle) {
    case AFHTTPRequestQueryStringDefaultStyle:
        query = AFQueryStringFromParametersWithEncoding(parameters, self.stringEncoding);
        break;
}

じゃあデフォルトじゃない動作は、というと上のコードの近くにあった。

// AFURLRequestSerialization.m
if (self.queryStringSerialization) {
    NSError *serializationError;
    query = self.queryStringSerialization(request, parameters, &serializationError);

    if (serializationError) {
        if (error) {
            *error = serializationError;
        }

        return nil;
    }
}

self.queryStringSerializationはBlockで、このブロックを設定してparameterのシリアライズをカスタマイズできるよ、ということのようだった。

つまり文字列をそのままBodyに入れたい場合は、

let manager = AFHTTPSessionManager()
let xmlstr = "<?xml ..."

manager.requestSerializer.setQueryStringSerializationWithBlock { (request, params, error) -> String! in
    return params as! String
}

manager.POST(url, parameters: xmlstr, success: { (task, response) -> Void in
    // success
}) { (task, error) -> Void in
    // failure
}

manager.requestSerializer.setQueryStringSerializationWithBlockでparameterをそのまま文字列と返してあげればよかった。

これで解決できた。良かった。

Webエンジニアの教科書

Webエンジニアの教科書

NSTextFieldで入力を検出する

iOS開発からCocoaにも手を付け始めた人間によくあることだろうが、UIKitとAppKitでお作法が同じだったり微妙に違ったりすることでちょくちょくつまずくことがある。

今日はテキストフィールドでユーザーが入力を行うたびに入力値のチェックをしてOKボタンの有効/無効を切り替えるという処理を書こうとしてつまづいた。

UITextFieldの場合

UITextFieldの場合はStoryboardからCtrl+ドラッグでActionを作るときにEditing Changedイベントを選んであげればOK。

NSTextFieldの場合

Storyboardを使って同じようなことをやろうとしてもiOSのようにコントロールに発生したイベントに対応したActionを作ることができない。ここまで書いて気がついたけどAppKitはイベント型(?)じゃないということなんだね。

ではどうすればいいかといえば、Delegateが存在するのでそれを実装してあげれば良い。DelegateiOS開発でも毎回使うだろうからiOS Developerなら大丈夫のはず。

class ViewController: NSViewController, NSTextFieldDelegate {
    @IBOutlet weak var textField: NSTextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do view setup here.
        textField.delegate = self
    }

    override func controlTextDidChange(obj: NSNotification) {
        doSomething()
    }
}

できた。

Cocoaアプリケーション(OSXアプリ)はじめました

実験的な意味も含めてCocoaアプリの開発を始めた。完成したものはMac App Storeに並べてもらうべくiOSと同じように申請して審査してもらっている。

そもそもMac App Storeに並んでいるアプリはなんと呼ぶのが正解なのかすらよくわかっていないが、Cocoaアプリということにしておく。

iOSの開発は5年ぐらいやっているけれど、Cocoaアプリの開発は全くの初めてだったから調べながら進めている。今は単機能のSingle Windowアプリを3つ作った程度で、ドキュメントベースのアプリケーションのような複雑なことはしていないが、Cocoaアプリ開発についてのコメントというか感想を書いてみようと思う。ただの感想文ですよ。

情報が少ない、見つけにくい

まず言えるのはこれだと思う。iOS開発の情報はネットの世界に溢れている。日本語でも大量の情報があるから大抵のことは調べればできる。

その反面、Cocoaアプリケーションの情報はなかなか見つからない。日本語で書かれたそこそこ新しい情報がたまにヒットするが、大抵はAppleのドキュメントページが検索のトップに来る。そしてその情報も数年前に書かれたものだったりする(必ずしも情報が古いというわけではなく、SDKが変わっていないというだけだったりするが)。

検索自体もしづらい。例えばAVFoundation OSX Swift ○○とか調べようとするとiOSの検索結果がずらりと出てくる。iOSとは独立してはっきりしたネーミングがあればよかったのにと思う。

最近気がついたのは、Cocoaというキーワードを入れておくとなかなか良さそうな検索結果を得られる気がする。

お作法が割と違う

UI周りがまあまあ違う。iOSのように画面サイズを制限されない分、自由にビューを配置したり複数のウィンドウを使ったり色々なことができる。

例えばiOSではUIImageを使っているところをCocoaではNSImageを使うことになるんだけど、このクラスのインターフェースが微妙に異なるので同じように使えるもんだと思っているといちいち躓く。

で、ドキュメントを探したり例を探そうとすると最初の話「情報が見つからない」に行き着いて時間がかかる。

慣れるまでは同じことを何度も調べる可能性があるので見つけたページはEvernoteのWebClipすることにしている。この機能は最近全く使っていなかったけれど久しぶりに重宝しそうだ。

iOSの経験はかなり活きる

お作法が微妙に異なるとは言え、SDKの思想はiOSとほぼ変わらないからiOS経験者は比較的すんなり入ることができる。

例えば、縦横の表を表現するときにはNSTableViewを使うが、iOSの経験のない人がいきなりこのViewを使おうとしたら混乱するんじゃないかと思う。予想です。

iOSUITableViewもこのNSTableViewdelegatedataSourceを用いてコンテンツや挙動を制御するわけだけど、行と列がある分NSTableViewのほうが複雑と言っていいと思う。先に複雑じゃないUITableViewを触っていれば、比較的すんなり入れるんじゃないかな。

Cocoaアプリケーション開発でもStoryBoardやSegueのようなUI開発の仕組みがあるし、Cocoapodsなども活用できるからiOS経験者にはハードル低いんじゃないかな。みんなやったほうがいいよ。

おわり

これからは備忘録的にちょくちょく書いていく予定。

ちなみに最初に作ったのはiPhoneiPadのAppStoreで使えるPreview動画を自動生成するアプリ。ネーミングやデザインなどはお察しだけどなかなか便利なのができたよ。

Movie Resize for App Previews
カテゴリ: ユーティリティ, グラフィック&デザイン

MAC OS X COCOAプログラミング 第4版

MAC OS X COCOAプログラミング 第4版

Single WindowなOSX Appで全てのWindowが閉じたときの挙動でリジェクト

MacのアプリケーションはWindowを赤ボタンとか⌘Wで閉じてもアプリケーション自体が終了しないことが多いが、その場合メインウィンドウを再表示できるようにしておかないとMac App Storeに申請した際にリジェクトを食らってしまう(食らった)。

1つめの対応方法として、Dockアイコンをクリックしたときにメインウィンドウを再表示するコードをAppDelegateに書く。

    func applicationShouldHandleReopen(sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
        if flag == false && sender.windows.count > 0 {
            sender.windows[0].makeKeyAndOrderFront(self)
        }
        return true
    }

もしくは全てのウィンドウが閉じたときアプリケーションを終了させてしまうというのもOKらしい。これは推測だけど、申請したのがシングルウィンドウなアプリケーションだったからこの対処方法でもいいよと言ってくれたんじゃないかと思う。ドキュメントベースの場合はウィンドウが閉じることって頻繁にあるだろうから多分だめなんじゃないかな。

その場合はAppDelegateにこんなコードを書く。

    func applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication) -> Bool {
        return true
    }

今回は単機能のアプリだったので後者で対応することにしてみた。

MAC OS X COCOAプログラミング 第4版

MAC OS X COCOAプログラミング 第4版

【追記あり】デフォルトアプリケーションがXcode-betaになってしまったのを戻した

XCodeのベータ版を入れたらxcodeprojなどを開くときのデフォルトアプリケーションがベータ版のほうになってしまった。いやいや君普段はそんなに使わないから。

僕はターミナルでそのプロジェクトのGitディレクトリに移動して、そこからopen hogehoge.xcodeprojとかやることが多いのでデフォルトアプリケーションは重要なのだ。

デフォルトアプリケーションの変更はFinderの情報を見るあたりからできるというのは知っていたが、ついでに*.hなど、基本XCodeで開くよねとなっているファイルたちのデフォルトも変えたかったからそれだと物足りなかった。全部のファイルタイプに同じことするの面倒なので。

そこで強引な方法で解決することにした。

  1. XCode-beta.appを削除
  2. ゴミ箱から戻す

これでOK!

追記

だめだったー。ベータを起動したら元に戻ってました。なんとかならんのこれ。

UIWebView内リンクでの遷移をNavigationでやるやつ、Swiftバージョン

iOSでUIWebView内のリンクをタップしたらNavigationで画面遷移させる実験 - PILOG

↑こんなん書いたけどこのときはObjective-Cで書いてたのでSwiftで書き換えてみました。オプショナルが全然慣れませんがこれから勉強します。

import UIKit
import TKRSegueOptions

class ViewController: UIViewController, UIWebViewDelegate {
    
    let HOST = "http://localhost:4000"

    @IBOutlet weak var webView: UIWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        webView.delegate = self
        var urlstring = HOST + "/"
        if let path = self.segueOptions?["path"] as? String {
            urlstring = HOST + path
        }
        let url = NSURL(string: urlstring)
        let req = NSURLRequest(URL: url!)
        webView.loadRequest(req)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        
        if(request.URL?.scheme == "callback"){
            println(request.URL?.absoluteString)
            let urlstring = request.URL?.absoluteString
            let href = urlstring?.substringFromIndex(advance(urlstring!.startIndex, 11))
            performSegueWithIdentifier("next", options: ["path": href!])
        }
        return true
    }
}

詳解 Swift

詳解 Swift

iOSでUIWebView内のリンクをタップしたらNavigationで画面遷移させる実験

しばらくiOSから離れていてSwiftはほとんど触ったことないのでとりあえずObjective-Cでやりました。このあとSwiftで実装し直そうと思ってるけれど、一旦エントリにします。

いつだったかDHHが既存のWebサイトを活かしてアプリ作るよーみたいなことを言ってたのを覚えていたので、今回は「いや、実際どうやってやるんだろうな」というのを考えてみたわけです。

何をやりたいか

こういうページ遷移がある普通のWebサイトがあるとして、

このページをiOSのUIWebViewで表示するんだけど、リンクをタップしたときに新しいViewControllerを作ってNavigationで遷移するということをやってみます。普通にやるとリンクをタップしたら同一WebView内画面遷移するところを、あたかもiOSのネイティブのように動かしてみようということね。

結論からいうとそれは可能です。↓のアニGIFを見てもらえばリンクを押したときにナビゲーションで次のビューがスタックしてくるのがわかると思います。

こうすることでWebにコンテンツを自由にデプロイしつつ、ユーザー体験はネイティブに近づけられるんじゃないかと。

こんなことはすでに昔からみんながやってたりしたかもしれないけど、最近は端末も速くなっているし、個人的にはWebView+ナビゲーションってあまり見たことがない気がするのでその部分の実験ということで見てもらえると良いです。

Webページの準備

まずはHTML側のソースです。ここで見るべきは、<a>タグについてるcallback-to-iosクラスぐらい。

<!DOCTYPE html>
<html>
  <body>
    <p>1ページ目</p>
    <a href="/secondpage.html" class="callback-to-ios">2ページ目ヘ</a>
  </body>
</html>

続いてJavascriptです。こっちが肝ね。

$ ->
  reportBackToIOS = (href)->
    iframe = $('<iframe />').attr('src', "callback://" + href)
    $('body').append(iframe)
    iframe.remove()
    iframe = null

  $('.callback-to-ios').click ->
    reportBackToIOS($(@).attr("href"))
    false

まずリンクのクリック時にreportBackToIOSを呼び出した上で、falseを返してリンクを辿らないようにしてあります。

reportBackToIOSは名前の通りiOSにリンクがタップされたことを知らせる役割を担っています。iOSのUIWebViewではWebView内でURLの読み込みが発生する直前にコールバックを通るんですが、ここではそれを利用しています。

具体的には、iOSに渡したい情報をURL(の一部)として持ったiframeを作って一旦HTMLのbodyに追加し、すぐに削除していますね。iframeがbodyに追加されたとき、srcアトリビュートで持っているURL(ここでは、'callback://' + 遷移先のURL)をiframe内で開こうとするため、WebViewのコールバックが呼ばれることになるわけです。

iOS側の対応

ViewControllerに適当にUIWebViewを置いてあり、ViewControllerViewControllerの遷移をするSegueがnextという名前で定義されているとします(実際にはViewControllerからViewControllerへのSegueは定義できないので、隠しボタンからViewControllerへのSegueです。)。


#define HOST @"http://localhost:4000"

- (void)viewDidLoad {
    [super viewDidLoad];

    self.webView.delegate = self;
    NSString* urlstring = NULL;
    NSString* path = self.segueOptions.stringValue;
    if(path) {
        urlstring = [NSString stringWithFormat:@"%@%@", HOST, path];
    } else {
        urlstring = [NSString stringWithFormat:@"%@%@", HOST, @"/"];
    }

    NSURL* url = [NSURL URLWithString:urlstring];
    NSURLRequest* urlRequest = [NSURLRequest requestWithURL:url];
    [self.webView loadRequest:urlRequest];
}

肝心のコールバックはこんな感じで実装しておきます。

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    if ([[[request URL] scheme] isEqualToString:@"callback"]) {
        NSString* href = [[[request URL] absoluteString] substringFromIndex:11];
        [self performSegueWithIdentifier:@"next" options:href];
        return NO;
    }
    return YES;
}

shouldStartLoadWithRequestは新しいリクエストが来た時に通るコールバックで、何もしないときはYESを返すことで読み込みをスタートさせることができるわけですが、今回はcallback://で始まるURLの場合だけはnextという名前のSegueを実行してNOを返すことにします。NOを返せばURLの読み込みは行われないので、callback://****みたいなURLを処理しちゃってエラー、ということも起こりません。

これで最初に紹介したGIFアニメのようなページ遷移ができるようになりました。便利なような、使いどころ難しいようなそんな感じですが、ネイティブっぽく動かすという目的はなんとなく果たせていますね。

参考リンク

reportBackToIOSのところはStackOverflowのこの質問を参考にしました。

ios - How can I reliably detect a link click in UIWebView? - Stack Overflow