2015年3月16日月曜日

WatchKit 向けの UIImage Animation を簡単に実装するためのライブラリ ParaMangar を作りました

いよいよApple Watch発売日まで1ヶ月ということで皆様精力的にWatch向けのアプリを作成されているのではないかと思いますが、現状のWatchKitで誰もが一度はマジギレ不満に思う点がアニメーションです。

こちらのブログにある通り、WatchKit向けのアニメーションを作成するのは現状極めて面倒と言わざるを得ません。

http://d.hatena.ne.jp/shu223/20150214/1423901142

そこでiOS側でUIViewを今までどおりレンダリングして、その結果をファイルにしたりUIImageにしてWatchKitに渡せばいいじゃない!というライブラリを書いてみたので公開いたします。

https://github.com/akisute/ParaMangar

このParaMangarを使うとこんな感じでアニメーションが作れます。

// Basic example
let duration = 1.0
self.animator = ParaMangar.renderViewForDuration(self.targetView, duration: duration, frameInterval: 2).toFile("Sample1", completion: {path in
self.animator = nil
println("Completed: \(path)")
})
// More complex example
// Render UIView animations!
self.animator = ParaMangar.renderViewForDuration(self.targetView, duration: duration, block: {
UIView.animateWithDuration(duration/2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 2.0, options: UIViewAnimationOptions.allZeros, animations: { () -> Void in
self.animatingView.transform = CGAffineTransformMakeScale(2.0, 2.0)
}, completion: nil)
UIView.animateWithDuration(duration/2, delay: duration/2, usingSpringWithDamping: 0.8, initialSpringVelocity: 2.0, options: UIViewAnimationOptions.allZeros, animations: { () -> Void in
self.animatingView.transform = CGAffineTransformIdentity
}, completion: nil)
}).toImage(duration, completion: { image in
self.animator = nil
self.imageView.image = image
})
view raw example.swift hosted with ❤ by GitHub

ライセンスはMITです。
あまりテスト出来ていないのでバグだらけかもしれませんが、ご意見issueなどgithubでお寄せいただければ幸いでございます!!

2015年2月2日月曜日

YouTube などのフルスクリーンで再生される UIWebView の動画をプログラムから終了させる方法

UIWebViewでyoutube.comやvine.coなどのサイトの動画を開いた場合、動画がフルスクリーンの専用動画プレイヤーViewControllerで再生されることがあります。このフルスクリーン動画を閉じる方法です。

方法1: 安全な方法

JavaScriptのFullscreen APIを使って安全に閉じることができます。iOS 6以上で動作確認済みです。
- (void)exitFullScreenVideo
{
// Works in iOS, not sure in OS X
[self.webView stringByEvaluatingJavaScriptFromString:@"Array.prototype.forEach.call(document.getElementsByTagName('video'),function(v){v.webkitExitFullscreen();});"];
// Didn't work in iOS, works in OS X
[self.webView stringByEvaluatingJavaScriptFromString:@"if(document.webkitFullscreenElement){document.webkitExitFullscreen();}"];
}

Fullscreen APIについては以下の資料が詳しいです。


方法2: animated, completionの制御もしたい場合

ここからがお待ちかねの黒魔法になります。
先ほどのJavaScriptを使った方法ではフルスクリーン動画を閉じる際のアニメーションを制御できません(必ずアニメーションが発生します)。またフルスクリーン動画を閉じ終わったタイミングのcompletion handlerが存在しません(JavaScriptでハンドルしようにも、videoタグのonendedイベントは通常の再生終了時にもイベントが飛ぶ上にアニメーション終了時ではなく再生終了時にイベントが飛ぶため使いづらい)。

通常はこれらが問題になることはまずありませんし、万一あったとしても仕様のほうを変えるほうが適切ですが、何らかの止むに止まれぬ理由によりなんとかしなければならなくなる場合が稀によくあります。

そこで画面上にUIWebView経由で表示されている動画プレイヤーViewControllerをビュー階層をたどって見つけ出し無理矢理dismissViewController:completion:で消すという方法を取ります。iOS 6以上にて動作確認済みです。ただしiOS 7以下の場合とiOS 8の場合で全く構造が異なり、将来にわたって動作するか非常に怪しいです。
/*
WARNING: PRIVATE API CALLS
WARNING: FORCEFUL MEMORY MANAGEMENT
highly recommend to avoid using this code
*/
- (void)exitFullScreenVideo:(BOOL)animated completion:(void (^)(void))completion
{
/*
iOS 6~7の場合
本ViewControllerの上にMPInlineVideoFullscreenViewControllerがfullscreen modalで表示されるような挙動になるので
それを通常通り閉じてやればよい
iOS 8の場合
新しいUIWindowが生成され、生成されたUIWindowのrootViewController.presentedViewControllerがAVFullScreenViewControllerになる
まずAVFullScreenViewControllerを閉じて、それからUIWindowのrootViewControllerをnilにセットしてhiddenにすることでUIWindowを消す
*/
if ([[[UIDevice currentDevice] systemVersion] doubleValue] >= 8.0) {
BOOL isFullScreenVideoContent = NO;
for (UIWindow *window in [UIApplication sharedApplication].windows) {
if ([window.rootViewController.presentedViewController isKindOfClass:NSClassFromString(@"AVFullScreenViewController")]) {
[window.rootViewController dismissViewControllerAnimated:animated completion:^{
window.rootViewController = nil;
window.hidden = YES;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[window performSelector:NSSelectorFromString(@"release")];
#pragma clang diagnostic pop
completion();
}];
isFullScreenVideoContent = YES;
break;
}
}
if (!isFullScreenVideoContent) {
completion();
}
} else {
if ([self.visibleViewController isKindOfClass:NSClassFromString(@"MPInlineVideoFullscreenViewController")]) {
[self dismissViewControllerAnimated:animated completion:completion];
} else {
completion();
}
}
}

Private APIの名前がバリバリ含まれるコードになっておりますので審査に出すアプリには使用しないことを強くおすすめさせていただきます。

2015年1月15日木曜日

ReactiveCocoa を Swift から使ってみた(2) KVO編

前回の記事から引き続き、ReactiveCocoaを触ったりしています。FRPの概念に慣れてくると通常のプログラミングスタイルでは得られない知見に遭遇出来てなかなか面白いです。

今回はまずSwiftでReactiveCocoaを学ぶときに参考にするドキュメントについてご紹介したいと思います。といってもSwiftに特化したドキュメントはほとんど存在しないため、Objective-CでReactiveCocoaを学ぶときのドキュメントを見て学ぶしか無いのが現状です。

私は個人的にはサンプルコードを直接触るほうが性に合っているようなので、以下のオープンソースのReactiveCocoaで作られたアプリのコードを見ています。
https://github.com/AshFurrow/C-41
https://github.com/jspahrsummers/GroceryList
GroceryListのほうがより本格的にFRPっぽい書き方をしているのでオススメです。実際にコードを動かしたい場合はC-41のほうが比較的簡単に動かしやすいと思います(それでも使われているライブラリが古いためビルドを通すのが大変だったりしますが・・・)

SwiftでReactiveCocoaを使っている例としてはCarthageが良いと思います。ただしMacのコマンドラインアプリなのでUIまわりがらみのサンプルとしては余り参考になりませんでした。
https://github.com/Carthage/Carthage

例えばReactiveCocoaを使う上でどうしても欠かせないのがRACObserveを使った特定プロパティに対するKVOのシグナル化です。これにより特定のプロパティが変化した時にシグナルを受け取ることができるようになります。通常Objective-CでReactiveCocoaを使う場合は標準のRACObserveというマクロを使用すれば良いのですが、Swiftではマクロが使用できないため他の方法を採用する必要があります。

調べてみたところ、幸いにしてRACObserveマクロは単なるNSObjectのrac_valuesForKeyPathメソッドのラッパですので、以下のようにしてrac_valuesForKeyPathを使用すれば解決できます。
// Objective-C
@interface Document: NSObject
@property (nonatomic, copy) NSArray *notes;
@property (nonatomic, readonly) RACSignal *notesUpdatedSignal;
@end
@implementation Document
- (RACSignal *)notesUpdatedSignal
{
// In Objective-C, just simply use RACObserve
return RACObserve(self, notes);
}
@end
view raw Sample.m hosted with ❤ by GitHub
// Swift
// 1. Do not forget to inherit NSObject (or add @objc attribute)
public class Document: NSObject {
// 2. Make sure to add 'dynamic' modifier
public dynamic var notes: [Note]= []
// 3. observer should always be 'self'
public var notesUpdatedSignal: RACSignal {
return self.rac_valuesForKeyPath("notes", observer: self)
}
}
view raw Sample.swift hosted with ❤ by GitHub


ここで注意することとして、KVO対象となるプロパティにはdynamic修飾子を付ける必要があります。Objective-Cにもdynamic修飾子はありましたが、Swiftのdynamic修飾子はObjective-Cのものとは意味が異なりdynamic修飾子を付けたプロパティについてObjective-Cと同様に動的プロパティアクセスを行うようにする(具体的にはobjc_msgSendする?)という意味があります。詳細については以下の記事を参照してみてください。
http://stackoverflow.com/questions/24092285/is-key-value-observation-kvo-available-in-swift#comment39273366_24092370
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html