MILLEN BOX

音楽好きの組み込みソフトエンジニアによるプログラミング(主にiOSアプリ開発)の勉強の記録

アニメーションしているViewを手軽に録画してムービーとして保存出来るクラスRecViewAnimationを作成・公開しました

お疲れ様です。

タイトルの通りアニメーションしているViewを録画してムービーとして保存出来るクラス、RecViewAnimationを作成しました。

以下にて公開しております。

▶︎GitHub - anthrgrnwrld/tryRecordingViewAnimation

「アニメーションしているView」ってのは、アニメーションしているViewそのものではなくて、そうゆうViewを内包している親Viewのことです。そいつに映ってるものを録画出来るというわけ。

前回、前々々回の更新はこいつの作成の中で調べた内容なんですよ。

anthrgrnwrld.hatenablog.com

anthrgrnwrld.hatenablog.com

作成した経緯

自作アプリでスクリーンに映っているアニメーションを録画する機能があります。
録画方法はAppleが標準で提供してくれているReplayKitを使用しているのですが、iOS11になってから、極端にその録画に失敗するようになってしましました。
何とか使えるようにならないか検討したのですがどうにもならず、それなら自分で録画用クラスを作成してしまおう!となった訳です。

使用方法

使い方はとてもシンプルにしたつもりです。
このプロジェクト内にある"RecordingViewAnimation.swift”を使用したいプロジェクトにまず放り込みます。 そして使いたいところで以下のような感じでレコーダーを作成してあげます。
let recorder = RecViewAnimation.shared

録画開始したい時には
recorder.startRecording(view: [対象のView], fpsSetting: [フレームレート(1秒間に何コマ表示させるか)]
をコールします。
そして録画停止したい時には
recorder.stopRecording()
をコールします。
このstartRecordingのコールで写真ライブラリへの保存まで行います。
(今後オプションでライブラリへの保存はせず、ファイルパスを渡すようなUpdateもアリかもしれません。)

録画保存の完了を通知して欲しい場合もあるかと思います。
その場合にはrecViewDidFinishedToSaveDelegateを使用して下さい。
その際にはUIViewControllerへのRecViewAnimationDelegateの追加、そしてrecorder.delegate = self の記載を忘れないで下さい。

因みにRecViewAnimationはSingletonクラスなので、どこでRecViewAnimation.shared作っても内容は保持されてますので安心です。
(もちろん停止したら消去するようにしてます。)

また、途中でViewのサイズが変更されてしまった際(画面がRotateしちゃった時とかね)、強制的に録画をStopする仕様にしてます。
このようなケースで録画終了してしまった場合にはDelegateメソッド recViewDidFinishedWithoutCallToStop で通知します。

あ〜最後に、このクラスはPhotoライブラリーへファイルを追加する為、Info.plistにPrivacy - Photo Library Additions Usage Descriptionの記載も必要になります。気をつけて!

以上になります。

そんなに多機能ではありませんが、使ってもらえると嬉しいです。

因みに...

きっかけとなったアプリはiOS11のアップデートによって現象が発生しなくなりました。。。
なんだかな〜。

他で使えるアプリを考えているところです。

UIView(withアニメーション)からUIImageへの変換する時の注意点

お疲れ様です。

UIViewからUIImageへの変換って割とよく行う処理かと思います。
今まで私は以下みたいなUIViewのExtensionペタッと貼り付けて、任意のタイミングで使用していました。

fileprivate extension UIView {
    
    /**
   対象のViewをキャプチャしUIImageで保存
   
   - returns : キャプチャ画像 (UIImage)
   */
    fileprivate func getCaptureImageFromView() -> UIImage? {
        
        let contextSize = self.frame.size
        
        UIGraphicsBeginImageContextWithOptions(contextSize, false, 0.0)       //コンテキストを作成
        self.layer.render(in: UIGraphicsGetCurrentContext()!)              //画像にしたいViewのコンテキストを取得
        
        let captureImage = UIGraphicsGetImageFromCurrentImageContext()        //取得したコンテキストをUIImageに変換
        UIGraphicsEndImageContext()                                         //コンテキストを閉じる
        
        return captureImage
        
    }
    
    
}

上記を機嫌よく使っていたのですが、以下のような問題が発生しました。

アニメーションが入ったViewをキャプチャしたとき思てたんと違う

今回、アニメーション付きのViewを内部で保持している親Viewをキャプチャーしようと思いました。
アニメーションしているViewと瞬間の表示位置も正しく、です。

しかし結果は…
どのタイミングでキャプチャーしてもアニメーションViewがずっとおんなじ場所じゃん!!
なんで?ねぇ何で??

コードを見直してみました。
そしてあることに気がつきました。

self.layer.render(in: UIGraphicsGetCurrentContext()!)

ん??このコンテキストを取得している部分、対象ViewのCALayerに対してレンダリングしてるな…。
一方、今回のアニメーションは animate(withDuration:animations:completion:) で行なっていました。
この方法ってViewの座標位置を示すポイント値は変化せずにViewがアニメーションしているんですよね。
しかしコンテキストを取得した対象は対象ViewのCALayer…。
一つの仮説が生まれました。

viewが保持している座標位置に従ってコンテキストを取得している!

上記が正しいのかどうかちゃんと調査してませんが、多分そんなに遠い憶測ではないと思っています。

それはそうと、うーん、これは困りました…。
今回はどうしても animate(withDuration:animations:completion:) でアニメーションしているViewを任意のタイミングでキャプチャしたかったんです。
(試してませんが、アニメーションをCABasicAnimationで行なった場合にはちゃんとアニメーションしている位置に従ってキャプチャできると思われます。
がしかし、そこでView側にて制限を加えたくなかったのです。)

他の人の書いたUIView→UIImageのコードを眺めるじーっと眺めてみることにしました。
その時間、約一時間くらい。
解決策がないか探って見ました。

すると、、、
おんなじ様に見えていたコードが、よく見ると違うものが混じっていることに気づいてきました。
そして解答に行き着いたのです。 drawHierarchy(in:afterScreenUpdates:) に。

どうやらiOS7から追加されたメソッドの模様。
リファレンスには以下の記載があります。

Renders a snapshot of the complete view hierarchy as visible onscreen into the current context.

なんかいけそうな感じがする!

引数inには指定したViewのRectを入れるみたい。
この時、view.frame の場合には正味そのViewのみのキャプチャを行い、view.boundsの場合には、内包されているviewも対象になるみたい。今回はview.boundsがいいですね。

afterScreenUpdatesは対象のViewに変化があったかどうかを確認可能にするか否か(ややこしや)を指定するものみたいです。
今回はfalseとしました。

上記を考慮し、以下の様にUpdateしました。

fileprivate extension UIView {
    
    /**
   対象のViewをキャプチャしUIImageで保存
   
   - returns : キャプチャ画像 (UIImage)
   */
    fileprivate func getCaptureImageFromView() -> UIImage? {
        
        let contextSize = self.frame.size
        
        UIGraphicsBeginImageContextWithOptions(contextSize, false, 0.0)       //コンテキストを作成
        
        self.drawHierarchy(in: self.bounds, afterScreenUpdates: false)
        
        let captureImage = UIGraphicsGetImageFromCurrentImageContext()        //取得したコンテキストをUIImageに変換
        UIGraphicsEndImageContext()                                         //コンテキストを閉じる
        
        return captureImage
        
    }
    
    
}

これでanimate(withDuration:animations:completion:) で作成したアニメーションも任意の表示位置のキャプチャが出来る様になりましたとさ。

​ SceneKitについて興味が湧いてきたので少し調べてみた

​ あけましておめでとうございます。
今年もよろしくお願いいたします。

急なのですが今更ながらSceneKitに興味が湧いてきました。
なぜ今更?Unityの方がいいんじゃないの??って声が聞こえてくる。。。

きっかけはコレ
出来上がったものは何かすごそうなのに、コードを見ると、、「ん?何となくわかる!」となり詳しく知りたくなったからです。

lepetit-prince.net

SceneKitについて学べれば、その後にはARKitみたいな面白そうなものもあるし!
今日はとりあえず調べたことを箇条書きにして自分メモを残してみます。

SceneKitメモ

  • SCNView(frame: CGRect)でScene用のViewを作成できる
  • SCNViewは通常のUIViewと共存可能
  • SCNViewにはsceneというプロパティー(SCNScene型)がおり、この子が起点となって3Dの物体を配置される
  • sceneは初期化(SCNScene())してあげないと何故か使えない。理由はよくわからない。
  • SCNViewをself.viewに追加する場合には、通常通りaddSubviewすればOK
  • scene内に置ける3D物体をNodeといいSCNNode(geometry: SCNGeometry?)、若しくはSCNNode(mdlObject: MDLObject)で初期化できる
  • というかsceneにはデフォルトでrootNodeとやらがぶら下げっている
  • SCNNodeはCNNode(geometry: SCNGeometry?)での初期化でOKだと思われる
  • SCNNode(mdlObject: MDLObject)での初期化方法についてはよくわからない。mdlObjectって多分3Dオブジェクトのことだけどあんまりよくわかってない(気にしない)
  • SCNGeometryには以下の12の種類がある。

    • 無限平面(SCNFloor)
    • 立方体(SCNBox)
    • カプセル型(SCNCapsule)
    • 円錐(SCNCone)
    • 円柱(SCNCylinder)
    • 平面(SCNPlane)
    • 三角錐(SCNPyramid)
    • 球(SCNSphere)
    • ドーナツ型(SCNTorus)
    • チューブ型(SCNTube)
    • テキスト(SCNText)
    • パスからの図形(SCNShape)
  • 作成したSCNNodeはrootNodeに対しaddChildNodeすることで追加して見える化できる

  • 作成したSCNNodeは表示する座標(3次元)を指定したり大きさを変更したり形を変形させたりすることができる
  • ここまでではまだ立体のオブジェクトが存在するだけで立体的には見えない。立体的に見せるには、カメラと光源の定義が必要である
  • scene.autoenablesDefaultLightingをtrueにすると光源についてよろしくやってくれる
  • scene.allowsCameraControlをtrueにすると画面を指でグリグリすることでカメラの位置を変えれたり、表示の大きさを変えれたりすることが出来る
  • カメラと光源もノードとして扱われる為、バシッと位置とかを指定することが出来る。(けど詳細は今は知らない。)
  • [おまけ]daeという拡張子の3DのドキュメントファイルからSCNSceneを直接作成する方法もある模様(SCNScene(url: URL, options: ))
    (参考)
    qiita.com

また以下のページからはじまるまとめがかなりわかりやすいと思われます。まだ全然読めてないけど。。。

appleengine.hatenablog.com

今日は以上。

CADisplayLinkで定期的に処理を実行してみる (swift4)

ちょっと定期的に実行させたい処理があったんです。
そこで今回はCADisplayLinkを使ってみようと思います。

今までも勿論そうゆうのをしている部分はあって、scheduledTimerWithTimeIntervalとかを使ってました。
1分毎とか10秒毎とか、最も短くでも1秒毎とかの定期処理なんで、そこまでタイミングにシビアではないケースでしたが。

しかし今回は1/60秒くらいで実行させたい処理で、これだけ間隔の短い定期処理を行うのにNSTimer使うのってどうなん??というばっくりした不安がありました。(結局そんな気にすることはないって話もあるかもですが…)

そんな中、「それならCADisplayLinkを使ってみるといいよ〜」と諸先輩から教えて頂きました。
これがなかなか使いやすい。
また、画面のリフレッシュレートに同期しているということで、タイミングに関する信頼性がかなり増しております(自分的に)。

CADisplayLinkの使い方

使い方は非常に簡単。
タイマーを開始したいタイミングで以下の処理を実行し、CADisplayLink設定を行うだけ。

     // CADisplayLink設定
        let displayLink = CADisplayLink(target: self, selector: #selector(update(_:)))   //#selector部分については後述
        displayLink.preferredFramesPerSecond = 20  // FPS設定  //この場合は1秒間に20回
        displayLink.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)    //forModeについては後述

selector部分も単純に定期的に行いたいメソッドを書くだけ。
今回はprintするだけにしてます。
Swift4だとRunTimeの絡みがあるので@objcの記載を忘れずに。

 @objc func update(_ displayLink: CADisplayLink) {

        // timeOffsetに現在時刻の秒数を設定
        print("\(#function) is called! \(count)\n");
        
        count += 1

    }

displayLink.addの引数のforModeはRunLoopMode型の構造体です。
RunLoopModeについてはあんまり調べれてませんが、リファレンスを見る限り以下のような種類で設定可能な模様です。
またいじりながら実地研修したいと思います。

static let commonModes: RunLoopMode
Objects added to a run loop using this value as the mode are monitored by all run loop modes that have been declared as a member of the set of “common" modes; see the description of CFRunLoopAddCommonMode(_:_:) for details.
static let defaultRunLoopMode: RunLoopMode
The mode to deal with input sources other than NSConnection objects.
static let eventTrackingRunLoopMode: RunLoopMode
A run loop should be set to this mode when tracking events modally, such as a mouse-dragging loop.
static let modalPanelRunLoopMode: RunLoopMode
A run loop should be set to this mode when waiting for input from a modal panel, such as NSSavePanel or NSOpenPanel.
static let UITrackingRunLoopMode: RunLoopMode
The mode set while tracking in controls takes place. You can use this mode to add timers that fire during tracking.

CADisplayLink、なかなか楽しかったです。

CADisplayLink - Core Animation | Apple Developer Documentation

tableView(_:didSelectRowAt:) が呼ばれない場合の原因について調べてみた

私、今まで作成したアプリではあんまりTableViewって使ったことないですが、最近久しぶりに触る機会がありました。

ableViewを使用する場合、ViewControllerにUITableViewDataSourceとUITableViewDelegateを追加して、numberOfRowsInSectionやcellForRowAtなどのデリゲードメソッドを追加しますよね。
ここまでは大丈夫。

そこで、「あ、そうそう。Cellタップしたら「〜」をできるようにしなきゃ」と思い、tableView(_:didSelectRowAt:)を追加しました。
呼ばれるかどうか一応確かめるかーってので、とりあえず中の処理に print("\(#function) is called!") か何かか書いて呼び出し確認をしてみました。

そしたら、、、呼ばれてない。。。何で!?

ということで原因を調べて見ました。

原因1: UITableViewDelegateを追加していない

かなり初歩的ですが、UITableViewDelegateをUIViewControllerに追加していない場合は勿論tableView(_:didSelectRowAt:)は呼ばれません。というかビルドも通らないよね。
私の場合、前述の通りこれについては実行済みでした。

原因2: Selectionが「No Selection」

StoryboardでTable Viewを選択し、Attributes Inspector→Table View→Selectionが「No Selection」になっていないかを確認して下さい。
No Selectionだと、Cellの選択が出来ないのでそもそもtableView(_:didSelectRowAt:)が呼ばれる訳がありません。
「No Selection」を「Single Selection」に変更しましょう。

f:id:anthrgrnwrld:20171026201039j:plain

ネットを調べた限り、これが原因で問題が発生しているケースが多そうでした。
しかし私の問題はこれでは解決しませんでした。

原因3: sampleTableView.delegate = self

UITableViewDelegateを追加した場合には、当然誰に処理を委任するのか指定してあげないと行けないですよね。
この場合だと通常、sampleTableView.delegate = self でOKかと思います。
これもかなり初歩的は部分です。
当然、やってま…..ん??

うそ?やってない!?

まさかね。。。これとは。。。
かなり初歩的ですが、初歩的すぎて逆に見つけられないという。
(言い訳)

当たり前のことから見直さないとだめですね。。。

Swift4 + Xcode9環境にてUIImageViewの画像が表示されなくなっって一瞬戸惑った話

先日初心者の人にiOSアプリの作成方法を軽くレクチャーすることがあったんですよ。

手始めにStoryboardにてUILabelを貼り付けてテキストの内容を変更したり、
それをコードと関連付けしてプログラムでテキスト内容を変化させたり、
UIButtonを追加して、ボタンを押すことで内容を変化させるようなアプリを作ったりして、簡単な説明をしました。

そして、「画像も表示出来るんだよ~」ってことで、StoryBoardにてUIImageViewを追加し、お手軽にAttribute InspectorでXcodeに放り込んだjpgファイルを指定してSimulatorで実行しました。

そしたら...指定した画像が出ないんです!!!
レクチャーしてる時にこれは焦りました。。。

結構時間かけて、あーでもないこーでもないしたんですが、結局Google先生に問いただしました。
調べてみると、画像ファイルにてTargetのチェックボタンを追加するフェーズが追加されているんですね。。。。

こんな感じでボックスにチェックを追加することを忘れないようにしてください。
f:id:anthrgrnwrld:20171021071049j:plain

今までも音声ファイルなんかを入れたいときに同じ罠にはまってましたが、
今まで幾度となく追加してきたUIImageViewで同じことが起こってしまうとは。。。
これは忘れるよ。。。

皆様もご注意ください。。。

- [UIApplication delegate] must be used from main thread only

前回に引き続きswift4への対応について書きます。

既存アプリに対するマイグレーションを経た一連の対応後、下記のようなエラーが出てきました。

UI API called from background thread: 
-[UIApplication delegate] must be used from main thread only

わっかんないけど、またランタイムとかその辺のこと?とか疑いながらググりましたがすぐに出てきましたね。↓

starhoshi.hatenablog.com

私の環境の場合、Firebaseは入っておらず、AdMob環境のみでしたが、バッチリこれに当たっておりました。

上記エラーが出た際には、一先ず使用しているライブラリのアップデートをおすすめします!