MILLEN BOX

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

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環境のみでしたが、バッチリこれに当たっておりました。

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

​ #selector がswift4では使えない? → 使えます

​ みなさんiOS11対応してますか?
私はやっとチョビチョビと始めましたよ。

今回はiOS10で動かしていたswift3ソースを初めて開き、自動変換した時にぶつかった疑問をメモしておきます。
Xcode9を起動してswift3以前のソースを開くとXcodeマイグレーションしてくれます。

そうすると今まで一部にしかついていなかった@objc記述子が#selectorで呼ばれている全てのメソッドの頭につきます。
これでおしまいかと思いきや、ビルドするとこんな警告が出るのです。

The use of Swift 3 @objc inference in Swift 4 mode is deprecated. 
Please address deprecated @objc inference warnings, test your code 
with “Use of deprecated Swift 3 @objc inference” logging enabled, 
and then disable inference by changing the "Swift 3 @objc Inference" 
build setting to "Default" for the "bluerer" target.

Google翻訳さんの助けを借りて自分なるに翻訳しますと、

Swift4にてSwift3の@objc推論を使用することは非推奨です。
推奨されない@objc推論の警告に対処してください。
対処方法としては、「非推奨のSwift 3 @objc推論の使用」ロギングを有効にし、
ターゲットの "Swift 3 @objc Inference"ビルド設定を "Default"に変更して推論を無効にします。

@objcを使ったやり方はswift4では非推奨です、と書いているように見えます。

うーん、、、どういうこと?対処方法はあるみたいだけど?

swift4では@objcを使ったselector呼び出しを使うのが非推奨なの?

上記について、正確に理解しているわけではないのですが、非推奨になったわけではないようです。
Build settingsのSwift 3 @objc inferenceの設定をOnからDefaultへ変更することで対処可能ですが、これがAppleが推奨する形になったと考えて良さそうです。
プロジェクト→Build settingsに進んで、そこの検索窓に「Swift 3 @objc inference」と打てばすぐに出てきます。

f:id:anthrgrnwrld:20171003082420p:plain  この方法についてある方に
「非推奨な使い方にも関わらず、設定で警告が消えてしまうことに違和感を感じる」
と伝えたところ、

@objc自体が非推奨になったわけじゃないみたい。
Swift3のときはXcodeが自動的に@objcをつけてくれてたけど、
Swift4からは明示的にかかないといけなくなったという感じかな。

とコメント頂きストンと落ち着きました。
ありがとうございました!
皆さまのご参考にもなれば幸いです!


​ <参考ページ>

dev.classmethod.jp

https://help.apple.com/xcode/mac/current/#/deve838b19a1?sub=devded6c2001

リリースアプリのアップデート

iOSアプリの開発を始めて2年半経ちました。そして5本のアプリをリリースすることができました。

始めた当初はとにかくリリースすることが精一杯。
それに反して溢れてくるアイデアとの折り合いをつけるのがとても大変でした。
なので出したら出しっぱなし。メンテナンスなんてしてませんでした。と言うかできませんでした。
新しいものを作りたい、けどコード書くのは遅い。そしたら出したもののケアは「取り敢えず後回し」になって。そして次第にそのことを忘れちゃって。

このようなループから最近抜け出しつつあります。
出したものについてある程度のケアをする (=アップデートをする) よう心がけています。

なぜ抜け出しせたのか? 順を追って説明していきましょう。

事実1 直近リリースした2つのアプリを気に入っている

一つ目としては、直近リリースした二つのアプリ、 ポーン - プロフェッショナルな名言をカウントしようぼかすん - 背景ぼかし & 消したい部分を簡単に隠せる への思い入れが強いことが関連しています。
かわいい子達の手入れを出来るだけしてあげたい。そんな思いが生まれます。

事実2 新規でアプリを起こすのに大変エネルギーがいるようになった

二つ目としては、先に述べた二つのアプリのお陰でリリースしようと思うハードルが自分の中で上がってしまったことが関連しています。
それまでは気にしていなかった部分についてもケアするようになり、アプリ一つを新規で起こすことが大変エネルギーのいる作業と感じるようになってしまっているのです。

事実3 新しいことは試したい

新規でアプリを起こすのは大変だけど、新しいアイデアや新しく覚えたことは試したいのです。

じゃあリリース済みのアプリをアップデートすることで新しいことを試せばいいんじゃない?

そう。新規でアプリで作るにはエネルギーはいるものの既存のアプリに新機能を追加するのであれば、新規で起こすのに比べれば全然負担は楽。
尚且つ、可愛いアプリ達がよりいいものになるので一石二鳥。

と言うわけで最近はそれ以前のものも含めて、楽な気持ちでアップデート出来るようになってきました! これは良いことだなぁと個人的には感じています。

ポーン - プロフェッショナルな名言をカウントしよう は名言メモ機能(以下の画像や動画の保存)の追加、ぼかすん - 背景ぼかし & 消したい部分を簡単に隠せる はぼかしエフェクト(モザイクとクリスタル)のリリース時に比べ追加しています。 もし良かったら試してみて下さい!

f:id:anthrgrnwrld:20170915183250j:plain f:id:anthrgrnwrld:20170915183352p:plain

swiftでの配列の初期化方法について

本日は配列の宣言についてメモ書きしておきます。 勘違いからミスってしまって解決するまでの流れを書いていきます。

以下みたいな感じでfor文で配列imageArrayにボコボコ値(UIImage型)を入れていきたい場合を考えます。

for i in 0 ..< numberOfPicture {  
    imageArray.append(UIImage(named:"\(i).png")!)  
}  

この場合、上記処理の前に配列imageArrayについて宣言をしておかなければなりません。
僕、初め以下のように宣言しちゃっていたんですね。

var imageArray: [UIImage] = [UIImage()]

この誤った宣言で処理を進めるとどうなるのか。

例えばpngファイルが1.pngから5.pngの5枚あった場合を考えましょう。つまりnumberOfPicture = 5の場合です。
期待する処理A後の要素数 imageArray.count は勿論5です。 しかしながら誤った宣言で処理Aを実行した後の imageArray.count は6になってしまうのです。 期待値としては1.pngはimageArray[0]に入っていてほしいし、5.pngはimageArray[4]に入っていて欲しいですが、1.pngはimageArray[1]、5.pngはimageArray[5]に入っています。

なぜ期待した値より1増えたメンバー数となるのか。

誤った宣言部をもう一度見てイコール以降の右辺部に注目して下さい。
[UIImage()]となっています。
この場合、配列の0番目には空っぽのUIImage型の何かが入ってしまうのです。
その配列に処理Aで書いたような形でappendを使用してメンバーを追加した場合はどうなるか?もうお分りと思います。
appendは既存の配列の最後にメンバーを追加するものなので、1.pngはimageArray[1]に追加されるし、5.pngはimageArray[5]に追加されてしまうのです。

解決策は?

誤った宣言部を以下のように直して下さい。

var imageArray: [UIImage] = []

これで期待通りの動作をするはずです。

思ったこと

実は空っぽの配列の宣言をする時、いつもどのように書こうか迷っていたんですよね。 今回スッキリできてよかったです。