- 投稿日:2020-02-24T22:21:26+09:00
SwiftUI チュートリアルを噛み砕いていく
筆者はSwift未経験かつ、SwiftUIから入る変わり者です。
始めて自学自習で未経験の言語を習得していく人と、将来の自分にむけて記事を残していきます。(以前途中までやったけど完全に忘れた)・SwiftUIが始めて
・Xcodeも始めて
・他の言語の学習経験あり(ある程度高級)という前提で始めますので、そもそもプログラミングが何もわからない人はチュートリアルをやる前に、他の言語を勉強したほうが無難です。
以下のページと同じ手順を踏みますが、英語が得意な人は自分で学習してもいいと思います
チュートリアル自体は
・SwiftUI Essentials (3)
・Drawing and Animation(2)
・App Design and Layout(2)
・Framework Integration(3)の4つのセクションかつ(n)個の編があります。
さらに編の中にも演習が入っており、結果としてそこそこ多い分量となってます。
編の最後には必ず理解度を試す簡単な問題がありますが、ぶっちゃけ役に立たない。やっていきましょう。
バージョンとか環境はその都度書いていきます。
- 投稿日:2020-02-24T21:51:42+09:00
【iOS】はじめてのPyto
はじめに
iOSのPython環境で最も有名なのはPythonistaだと思います。個人的にPythonistaは、
- 更新が2年以上無い(2020/2/24現在)
- 対応ライブラリが少ない(Pandasが使えない)
という点が引っかかり、購入を見送っていました。ある日、日経ソフトウェアに「Pyto」というアプリが紹介されており、検索してみるとPythonistaと比較して、以下の点が魅力的だったので購入しました。
- 更新頻度が高い
- Pandasに加え、sklearn、skimage、さらにはOpenCVも使える
- Pythonistaと同様にiOS用のGUIアプリの作成が可能(iOS13以降)
この記事ではPytoの使い方や基本的な機能について説明します。
環境
iPad pro11 64GB (iOS13.3.1)
Pyto(11.4)使い方
起動画面・ファイル選択画面
初回はアプリを起動すると、ファイルを選択する画面になります。任意の場所のファイルを開くか、新規作成を行います。また、この画面の「…」のボタン(図のピンクの矢印で示したボタン)がアプリやエディタのメニュー・設定になります。
メニュー・設定
メニューは上図のようになっています。ここでエディタ・外観の設定を行ったり、対話モードを使ったり、PyPiで新たにライブラリを入れることが可能です(追加可能パッケージはpure python限定)。
対話モード(REPL)
対話モードです。コードは下のテキストボックスに入力します。対話モードでもmatplotlibのグラフ表示が可能です。Run module (python -m)
.py
ファイルを実行できます。上の画面ではpipを実行していますが、pip show
でパッケージの情報を表示することはできませんでした。また、インストール済みのパッケージを確認するためにpip list
を実行しましたが、後述するPyPiで後からインストールしたパッケージしか表示されませんでした。PyPi
PyPiを使ってインストールされているライブラリのバージョンを確認したり、新たにライブラリを追加することができます。ただし、追加できるのはpure pythonのものに限るそうです。seabornはインストールできましたが、tensorflow、pyaudioはインストールできませんでした。
上の図ではインストールしたseabornを使ってpairplotを実行しています。Pytoでmatplotlibやseabornを使う際は、plt.clf()
を最後に記述しないと複数回実行時にグラフが重なってしまいます。サンプルコード
OpenCV, sklearn、GUIモジュール等を使ったサンプルコードが入っています。Loaded modules
読み込んだことのあるモジュールが表示されます。そのモジュールのソースコードも表示できます。何に使うのかはよくわかりません。
Settings
エディタの設定画面です。ソフトタブ(スペースタブ)に対応しています。また、エディタの外観も数種類から選んだり、カスタマイズすることが可能です。
エディタ画面
エディタ画面です。右下に歯車マークがありますが、これはアプリの設定ではありません。Siriに編集中のファイルを登録したり、カレントディレクトリを変更したりできます。左下の…はエディタアクションで、ファイルに対して何か実行できるそうです。その隣のブックマークアイコンはPytoのドキュメントです。Pytoのパッケージ(GUIツールなど)の使い方が書いてあります。このドキュメントはインターネット上に公開されており、こちらから確認できます。
上部のボタンは左の4つの四角形から順に「ファイル選択画面に戻る」、「検索・置換」、「デバッグ(pdb)」、「実行」です。
また、画面右下の「V」のようなボタンを押すと、外部キーボード使用時に入力を終了できます。これにより、split viewでブラウザと同時に使用したときにカーソルをブラウザに移すのが容易になります。
add-to-siri, arguments, current directory
add-to-siri, arguments, current directoryを設定できます。argumentsではプログラム実行時に渡す引数を設定できます。
current directoryではカレントディレクトリを変更することができます。変更すると警告が出ますが、カレントディレクトリ変更後も問題なく実行できます。ここで選択したディレクトリには編集権限がつき、ファイルの参照・変更が可能となります。ローカルのファイルを読み込めない時はここでフォルダ選択すると読み込めるようになるかもしれません。Editor actions
Editor actionsです。デフォルトで2to3とblackが用意されています。ドキュメント
ドキュメントです。公開されているこちらと同じものです。右上の「V」のようなボタンでスプリットビューになります。補完
補完が可能です。タップで選択するか、Tabキーで候補から選んでEnterで確定します。フォルダロック
初めて使用するフォルダは初期状態では参照・変更できないようになっています。この場合は右下の鍵マークを押し、現在のディレクトリを選択することで参照・変更が可能となります。キーボード
ソフトウェアキーボードはiOSのデフォルトです。PytoではTabキー以外の拡張キーは用意されていません。そのためiPhoneでプログラムを書くのは少し厳しいです。
外部キーボード(Bluetoothキーボード等)は難なく使えます。コメントアウト、逆インデント、実行等のショートカットキーも使用でき、そこそこ快適です。しかし、複数行を選択した状態で一括コメントアウトを行うことはできませんでした。
おまけ
OpenCVにはdnnモジュールが含まれているので、試しにMobilenetを動かしてみました。ソースコードはhttp://asukiaaa.blogspot.com/2018/03/opencvdnnpythonmobilenet.html からお借りし、Pyto用に少し書き直しました。カメラはiPadの外カメラです。
動作は問題無く、フレームレートもカクツキを感じないレベルでした。正直、iOSアプリの上でディープラーニング(の予測)が動くとは思っていなかったので、驚きました。
その他
- ユニバーサルアプリ(iPhone, iPad両対応)である
- デバッガ(pdb)が使える
- csv、画像の読み込みはローカルでも問題なかった
- matplotlibのグラフ重なりは
plt.clf()
で解決できる(前述)- PytoのGUIモジュールはpythonistaとほぼ同じ書き方ができる?
- syntax highlightが少しおかしい(「おまけ」の18行目、20行目など)
- 折り返された行は新しい行扱いになる(自動でインデントされなかったり、行の途中なのにコメントアウトできてしまう。バグ?)
- 閉じ括弧「)」を自動で付けてくれるが、自分で書くと2重になってしまう(vscodeみたいに括弧の外に出してくれない)
- pythonistaほどGUIに強くない(GUI作成ツールでパーツ配置できない)
GUIアプリ作成を重視するならPythonista、処理を重視するならPytoといった感じでしょうか。PythonistaにあってPytoに無いライブラリもあるので、ドキュメント等で内蔵ライブラリの比較を行うと良いと思います。
Pytoはこれからも発展していきそうなので、とても期待できるアプリだと感じました。
リンク
Pyto公式サイト:https://pyto.app/
Pytoドキュメント:https://pyto.readthedocs.io/en/latest/
- 投稿日:2020-02-24T21:46:18+09:00
Xcodeプロジェクトで Swift Package Manager を使っているとBitriseでfastlaneのテスト実行が失敗する問題に対処
事象
Xcodeプロジェクトを Swift Package Manager を使ってライブラリ管理するように変更したところ、Bitriseで以下のようなエラーが出るようになりました。
xcodebuild: error: Could not resolve package dependencies:
An unknown error occurred. '/Users/vagrant/Library/Developer/Xcode/DerivedData/Timetodo-cdnlkhtafdnmclauocshwxxtmoag/SourcePackages/repositories/XCGLogger-9081359c' exists and is not an empty directory (-4)
timeout after 600 seconds解決方法
Fastfileに以下を追加してライブラリの依存解決すると成功するようになりました。
Dir.chdir("../") do sh("xcodebuild", "-resolvePackageDependencies") end参考
https://errors.wtf/xcodebuild-error-could-not-resolve-package-dependencies/
環境
Xcode 11.3.1
Swift 5.1.3
- 投稿日:2020-02-24T21:39:15+09:00
Instance method 'request(:didFailWithError:)' nearly matches optional requirement 'request(:didFailWithError:)' of protocol 'SKRequestDelegate' に対処する
事象
SKProductsRequestDelegate
の継承元であるSKRequestDelegate
に定義されている以下のメソッドをSwiftで実装してエラーハンドリングしようとしたところ、warningが出てメソッドが呼ばれない事象に遭遇しました。func request(_ request: SKRequest, didFailWithError error: Error) { }Instance method 'request(:didFailWithError:)' nearly matches optional requirement 'request(:didFailWithError:)' of protocol 'SKRequestDelegate'
解決方法
以下のように
@objc
でメソッド名を指定し、errorの型をNSError
に変更することでメソッドが呼ばれるようになりました。@objc(request:didFailWithError:) func request(_ request: SKRequest, didFailWithError error: NSError) { }未解決
上記のように変更してもwarningは消えませんでした...
参考
環境
Xcode 11.3.1
Swift 5.1.3
- 投稿日:2020-02-24T18:34:12+09:00
【Swift クイズ】そのクロージャ、本当に [weak self] 必要ですか?
この記事は何?
クロージャにおいて
[weak self]
は本当に必要なのか?
実際のところケースバイケースですが、今回はDispatchQueue.main
を例にしてクイズ・解説をしたいと思います。
ということで、早速クイズです。環境
- Xcode Version 11.3.1 (11C504)
クイズ
第一問
次のコードに
[weak self]
は必要ですか?DispatchQueue.main.async { [weak self] in guard let `self` = self else { return } // self 使った処理 }第二問
次のコードに
[weak self]
は必要ですか?UIView.animate(withDuration: 10) { [weak self] in guard let `self` = self else { return } // self 使った処理 }いかがでしょうか?
特段の理由なく[weak self]
を記述しているのであれば、引き続きクイズ回答・解説をご覧ください。クイズ回答
第一問
A.
[weak self]
は必須ではない。処理の内容によっては[weak self]
が妥当な場合もある。第二問
A.
[weak self]
は不要。解説
一般的な話として、循環参照を回避する目的で
[weak self]
を指定する場合がありますが、
上記の処理は、クロージャ実行後に循環参照が解決されるので[weak self]
は必須ではありません。かんたんな実験によって、このことを確認することができます。
class Detail1ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() DispatchQueue.main.async { [object = SomeObject()] in object.doSomething() } // あるいはこういうコード // UIView.animate(withDuration: 30) { [object = SomeObject()] in // object.doSomething() // } } }ここで、
SomeObject
には動作確認のための実装をしておきます。class SomeObject { init() { log() } func doSomething() { log() } deinit { log() } private func log(_ function: String = #function) { print("???\t\(Self.self).\(function)") } }そうすることで、実行時には次のコンソール出力を確認することができます。
??? SomeObject.init() ??? SomeObject.doSomething() ??? SomeObject.deinit
doSomething()
の実行後にdeinit
が呼ばれることが確認できると思います。SomeObject を強参照しているのはクロージャだけなので、
これは、クロージャの実行後にクロージャ自体がメモリから開放されたことを意味しています。つまり、
DispatchQueue
やUIView.animate
は[weak self]
せずとも、循環参照によるメモリリークは発生しないことを意味しています。
なので、循環参照の回避を目的とした weak キャプチャは不要です。ただし、非同期処理の実行時にオブジェクトが開放されていても良い場合や、
開放されていることが妥当な場合は、[weak self]
を指定することが好ましいと思います。たとえば、
DispatchQueue...asyncAfter
が実行される前に、ナビゲーションコントローラから該当のビューコントローラがポップされた場合を考えてみます。
この場合は、次のコードのように[weak self]
や[weak view = view]
のような指定を行うことで、無意味な処理をスキップすることができます。class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 強参照によるキャプチャを行った場合、 // クロージャの実行が完了するまでビューコントローラが開放されることはありません。 // ビューコントローラをポップしたあとはビューを操作しても意味が無いので weak キャプチャが妥当です。 DispatchQueue.main.asyncAfter(deadline: .now() + 10) { [weak self] in self.view?.backgroundColor = .orange } } }DispatchQueue のように、非同期処理の実行後にクロージャが破棄される処理では、次のことに気をつけましょう。
- weak キャプチャせずとも循環参照は発生しない
- 必要に応じて weak キャプチャを使うことで無意味な処理をスキップできる
一方で
UIView.animate
については、単純にキャプチャリストは不要です。
クロージャ実行後、クロージャを破棄する点では DispatchQueue と同じですが、実はクロージャを同期処理として実行するので、weak キャプチャは不要です。また、
UIView.AnimationOptions.repeat
などを指定してもクロージャの実行は1回きりです。かんたんな実験によって、この事実の動作確認ができます。
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // `animations` で指定したクロージャは即実行されるので、weak キャプチャは無意味。 // // `completion` で指定したクロージャは、`animations` で指定したアニメーションが停止したタイミングで実行される // 例えば、次のいずれかの処理を行うと `completion` がコールされる。 // - ビューコントローラをナビゲーションコントローラからポップする // - view.layer.removeAllAnimations() をコールする // // `animations`, `completion` ともに実行は一度きりで、 // 実行後にクロージャは開放される UIView.animate( withDuration: 10, delay: 0, options: .repeat, animations: { self.view?.backgroundColor = .orange }, completion: { [o = SomeObject()] _ in o.doSomething() } ) } deinit { print("???\t\(Self.self).\(#function)") } }このように
[weak self]
の無いコードを書いても、ナビゲーションコントローラから該当のビューコントローラをポップすることで、ビューコントローラのdeinit
が呼ばれてメモリリークしないことが確認できます。まとめ
非同期処理のコールバックハンドラとしてクロージャを扱う場合でも、クロージャの実行後に、そのクロージャがメモリから開放されれば、循環参照が解決されたことになるのでメモリリークは発生しません。
そして、
DispatchQueue
は上記に該当するので、[weak self]
がなくてもメモリリークしません。[weak self]
の有無は、循環参照が引き起こすメモリリーク問題とは切り離して、処理のスキップが必要かどうかで判断しましょう。
- 投稿日:2020-02-24T18:22:37+09:00
SocialHub を支える技術 - iOS Android 共通ライブラリ編 -
SocialHub とは?
まずはアプリの紹介をさせてください。SocialHub とは、先日筆者がリリースした iOS 向けマルチ SNS クライアントアプリです。大きな特徴として挙げられるのが、複数の SNS を同時に見て投稿できる ことで、現在対応している SNS の種類は Twitter, Mastodon, Slack, Tumblr になります。
SocialHub - マルチ SNS クライアント - | App Storeマルチプラットフォーム
SocialHub は現在 iOS のアプリしかリリースしていませんが、Android のアプリも見据えたプロジェクトで、複数の SNS を統合して扱う都合上複雑なロジックから逃げられないため、できればワンコードでロジックを記述したい気持ちがありました。昨今流行りのマルチプラットフォームなフレームワークを用いての実装を、当初 (2018年初旬) 考えており、当時のマルチプラットフォームというと、Xamarin や React Native が選択肢としてあったのですが、自分がその分野に明るくなかった事や、一番の問題として、そのライブラリで本当に自分の目指した UI/UX が実現できるか自信がなかったことです。
また、技術選定中に Flutter も注目を浴びましたが、こちらについてもサンプルプロジェクトを作って動作を検証したのですが、パフォーマンスが気になる事や、情報の少なさから、自分の求める UI が作成できるか自信がなかったので採用を見送りました。
共有ライブラリという選択
そこで注目したのが、iOS/Android で共有ライブラリを作ることのできるフレームワークです。共有ライブラリを用いることで、共通のロジックは全て単一の言語で記述し、クライアントアプリ側はネイティブで UI を記述することによって、リッチな体験を担保します。以下に選択肢として考えたものについて記述します。(理解が浅い部分があったらコメントくださると助かります)
Embeddinator-4000
共有ライブラリという方針を決めてから、一番初めに見つけたのが Embeddinator-4000 です。これは Xamarin のサブプロジェクトの一つで C# で書かれたコードを iOS/Android 向けの共有ライブラリとして扱うことができるようにするコンパイラ? (適切な表現かどうか不明) です。細かい内容については、以下の自分のブログ記事を御覧ください。.NET のコードを他言語に変換? Embeddinator-4000 を使ってみた。| urushi blog
動作確認までしたのですが、正直 iOS 向けにバイナリをビルドする際に、C# のジェネリクスが使えない事がかなりの痛手で、ほぼまともに動作するライブラリを作成することができませんでした。理由としては iOS 向けに mono のランタイムをアプリに同梱することができないためで、一朝一夕でなんとかなる問題ではありませんでした。また、開発もあまり活発的ではなく、上記の問題の解決が期待できないので諦めざるを得ない状況でした。
J2ObjC
次に発見したのが J2ObjC です。Google が主に開発を進めているプロジェクトで、Java のコードを Objective-C に変換してくれるコンパイラ になります。既に公開から八年以上経っているプロジェクトですが、まだ開発が行われており、Google 社内でも恐らく使われているようなので、安心感があります (?) J2ObjC についても、ブログ記事を書いたので、詳しくは以下を参照してください。Java のコードを Objective-C に? J2ObjC を試してみた。 | urushi blog
SocialHub はこの J2ObjC を採用しました。 理由としては、
- 自分に Java に対しての言語理解があること
- プロジェクトがメンテされていること
- パフォーマンスの劣化がほぼ無いこと
が挙げられます。J2ObjC はコンパイル前後の言語 (要するに Java と Objective-C) の知識をある程度必要とするので、問題に対応する際に、Java が分からないでは対処が難しいケースが存在します。
ということで SocialHub で作成したコアライブラリがこちら。
(ライブラリ名も SocialHub) J2ObjC でここまでコンパイルしている例って Google 外ではあまりないんじゃないかと思う。(そもそも J2ObjC なんて誰も使ってねえよ、っていう声は聞かないことにします) コンパイルは GitHub Actions に代行させており、大体所要時間は 15 分程。Java を Objective-C に変換した後、マルチアーキテクチャ向けにバイナリを作成し、Cocoapod のレポジトリを作成するところまで実行します。
補足として、J2ObjC のサブプロジェクトでは、既にメンテナンスされていないものも存在し、ややエコシステムとして不便な部分もありました。J2ObjC はコンパイルする際に make (or bazel) を使うケースが多いのですが、自分はめんどくさがりなので、J2ObjC の Gradle Plugin を用いてコンパイルを行っていました。Plugin を使うと Gradle で記述されている依存関係についても自動的にコンパイルターゲット加え、かつ import での参照関係も確認してくれるため、余計なコンパイルをしないで済みます。しかし Gradle Plugin は既にメンテされなくなっており、Java11 では動作しない問題を抱えていました。そのため Fork して修正しました。(結局手間がかかっている)
正直、J2ObjC での初期の実装は茨の道でしたが、その道を通り抜けると、それなりに快適な開発環境を整えることができました。ということで、SocialHub では Java のライブラリを使用して iOS アプリを作成しています。
SocialHub の権利表記画面頑張りすぎた。。。こんながんばらんでもええだろ、誰も見ねえよ。。。 pic.twitter.com/Q93wySWfRt
— うるし (@U_Akihir0) November 10, 2019アプリ内の権利表記にはコアライブラリ内で使用した Java のライブラリも記載しています。Twitter4J をはじめとした各種 SNS クライアントライブラリを使っています。
Kotlin/Native
一応、共有ライブラリということなので、忘れてはいけないもう一つの選択肢である
Kotlin/Native について記載します。こちらは Kotlin で書かれたコードを LLVM エコシステムに混ぜてしまおうという、実にシンプルな目的のプロジェクトです。ここで厄介なのが、Kotlin で書かれたコードといっても、その内容に制約がかかることです。Kotlin といえば一般的に Java の資産がそのまま流用できるイメージを持たれていると思いますが、Kotlin/Native においては JVM 外での Kotlin の使用になるので java, javax 以下のパッケージのライブラリには使用の制限がかかりますし、既存の Kotlin ライブラリもマルチプラットフォーム用に記述されていないと使用できません。
利点としては、プラットフォーム API 等を用いたより高度な抽象化ロジックを記述することができる事にあります。Kotlin/Native では iOS の UIKit を扱うためのライブラリ等が提供されているため、共有ライブラリのレベルで実現できる範囲が広いのが特徴です。SocialHub ではあまりその恩恵を受けられないので、J2ObjC を選択しました。将来的に Java のライブラリが困難なく使用できるレベルになってきたら使用するか検討したいと考えています。(そんな日が来るのかあんま詳しくない)
まとめ
- マルチプラットフォームを選択する場合、本当に実現したい事が何かに立ち戻る
SocialHub の場合ユーザー体験について妥協することがどうしても嫌だったので、UI やパフォーマンスに対してフレームワークを理由に妥協することがどうしてもできなかった → 共通ライブラリという選択
無論、何を意識するかで全てが変わるので、実装スピードやスキルセットに合わせて Flutter や ReactNative を使用することは全然アリです。というか、かなり自分は異端だと感じています。
- J2ObjC 使ってる人いないけどいいぞ
共通ライブラリをまとめると以下のような印象。
フレームワーク 言語 導入難度 知名度 実用性 Embeddinator-4000 C# 難しい ほぼ無名 ✗ J2ObjC Java 難しい ほぼ無名 ○ Kotlin/Native Kotlin 普通 有名 △ J2ObjC のすごいところは、Java の標準ライブラリをほぼ Objective-C で再実装されている点で、思った以上に普通に Java がコンパイルできることです。でも Java なんだよなぁ。。。
- 投稿日:2020-02-24T16:19:50+09:00
Flutter WidgetのKey指定が必要な理由と仕組みについて解説 - 前編
はじめに
Flutterの Widget でたまに指定する
Key
について、何となく必要性は理解できるものの、完全に理解するためにまとめることにしました。今回は前編として、主に
Key
とは何かと、Key
が必要になる理由について解説します。
後編では、Key
の種類一覧やKey
指定位置について解説予定です。
Key
とは?簡単に言うと、ElementからWidgetを識別するためのIDです (RenderObjectもWidgetを参照します…)。
Key
は意図的に指定しないとデフォルトではnull
です。必要となるシーンが限定される感じですが、よく紹介されているのは以下ですね。
ただ、必要性が分かるようで分からない感じで、何となく使えている感じがします。
- ToDoアプリのようなStateをもったWidget郡のソート, 追加, 削除
- Listのスクロール位置の保存
本記事を最後まで読んで頂くと理解できると思いますが、Widgetツリーの中でノード (あるWidget) を他の場所に移動させる場合など、Widgetツリーと対になるElementツリー側から特定のWidgetを識別する必要がある場合にのみKeyを利用します。
何故
Key
が必要になるのか?サンプルアプリを用いて、
Key
の必要性を理解していきます。タイルを入れ替える簡単なアプリ
FABをクリックする度に2つのタイル (ランダムな色で塗り潰た四角) の位置を入れ替えるだけの簡易アプリです。
ソースコードは以下です。
StatelessWidget
を用いてアプリ起動時にランダムな色のタイルを配置し、ボタンが押される度にListの順番を入れ替えるだけです。ソースコードimport 'package:flutter/material.dart'; import 'dart:math'; class StatelessColorfulTile extends StatelessWidget { final Color _color = UniqueColorGenerator.getColor(); @override Widget build(BuildContext context) { return Container( color: _color, child: Padding(padding: EdgeInsets.all(100.0))); } } class UniqueColorGenerator { static List _colorOptions = [ Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.orange, Colors.indigo, Colors.amber, Colors.black, ]; static Random _random = new Random(); static Color getColor() { if (_colorOptions.length > 0) return _colorOptions.removeAt(_random.nextInt(_colorOptions.length)); else return Color.fromARGB(_random.nextInt(256), _random.nextInt(256), _random.nextInt(256), _random.nextInt(256)); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ StatelessColorfulTile(), StatelessColorfulTile(), ]; } void _swapTiles() { setState(() { _tiles.insert(1, _tiles.removeAt(0)); }); } @override Widget build(BuildContext context) { return Scaffold( body: Row(children: _tiles), floatingActionButton: FloatingActionButton( child: Icon(Icons.sentiment_satisfied), onPressed: _swapTiles), ); } }StatelessWidgetの動作
StatelessWidget
の場合、作成されるStatelessElement
はState
を保有しないため、Widgetは描画毎に必ずrebuild (Widget build(BuildContext context)) が実行されます。つまり、FABを押す度にsetState
をトリガーにして、2つのタイルが再構成 (rebuild) されて、再描画され、結果的にスワップしたように見えます。また、Flutterは各Widgetをツリー構成で管理しており、
Widget
/Element
/RenderObject
の3つのツリーが構築されます。ざっくり、WidgetがUIパーツの構成/仕様、ElementがそのWidgetの状態管理やRenderObjectとの仲介、RenderObjectElementが描画管理という役割分担で、それぞれのツリーは相互参照の関係になります。今回のサンプルでのツリー構成を以下に示します。
ただし、説明簡略化のためにRenderObjectElementは割愛します。
rebuild時の動きは以下のpackages/flutter/lib/src/widgets/framework.dartのコードを追ってみると分かります。
仕組みを追いかけるなら、サンプルを作成してそれをデバッガでステップ実行しながら動作確認する手法がお勧めです。
Element.updateChild
→StatelessElement.update
→Element.rebuild
→ComponentElement.performRebuild
→StatelessElement.build
→StatelessColorfulTile.build
という順番でコールされます。StatelessElement.updateclass StatelessElement extends ComponentElement { /// Creates an element that uses the given widget as its configuration. StatelessElement(StatelessWidget widget) : super(widget); @override StatelessWidget get widget => super.widget; @override Widget build() => widget.build(this); @override void update(StatelessWidget newWidget) { super.update(newWidget); assert(widget == newWidget); _dirty = true; rebuild(); // ← ここでrebuildがコールされる } }上記サンプルをStatefulWidgetで作ってみると?
今度は
StatelessWidget
からStatefulWidget
に置き換えて動かしてみます。StatefulWidgetに置き換えclass StatefulColorfulTile extends StatefulWidget { StatefulColorfulTile({Key key}) : super(key: key); @override ColorfulTileState createState() => ColorfulTileState(); } class ColorfulTileState extends State<StatefulColorfulTile> { Color _color; @override void initState() { super.initState(); _color = UniqueColorGenerator.getColor(); } @override Widget build(BuildContext context) { return Container( color: _color, child: Padding( padding: EdgeInsets.all(100.0), )); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), StatefulColorfulTile(), // 変更!! StatefulColorfulTile(), // 変更!! ]; }何故でしょう?その理由こそKeyが必要な理由です。
(個人的には、フレームワークの作り上の都合な産物な気もしますが…)StatefulWidget (Key指定なし) の動作
StatefulWidget
とStatelessWidget
の最大の違いはState
の有無です。
StatefulWidget
はsetState
をトリガーにした描画更新処理でElementのrebuildを実施するかどうかの判定に、参照する新旧のWidgetのKey
とRuntimeType
(クラス名のこと) を参照しています。ソースコードでは、Element.updateChildとWidget.canUpdateが該当部分になります。
Element.updateChild@protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { assert(() { if (newWidget != null && newWidget.key is GlobalKey) { final GlobalKey key = newWidget.key; key._debugReserveFor(this); } return true; }()); if (newWidget == null) { if (child != null) deactivateChild(child); return null; } if (child != null) { if (child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } if (Widget.canUpdate(child.widget, newWidget)) { // Key,RuntimeTypeが同じならWidgetの参照だけ更新して終了 if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); assert(child.widget == newWidget); assert(() { child.owner._debugElementWasRebuilt(child); return true; }()); return child; } deactivateChild(child); assert(child._parent == null); } return inflateWidget(newWidget, newSlot); }Widget.canUpdate@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; @protected Element createElement(); @override String toStringShort() { return key == null ? '$runtimeType' : '$runtimeType-$key'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } // 新旧でRuntimeTypeとKeyが同じかどうかチェック static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }Key指定しない場合は新旧Widgetどちらもnull、RuntimeTypeは同じクラス名となり、rebuildは実施されず、ElementからWidgetへの参照だけの更新となります。その後のRenderObjectもElementがrebuildされていないため、描画処理が更新されません。
この時点でおかしな状態になっていることが分かります。
Key追加による解決
先ほどの問題を解決するために、StatefulColorfulTileクラスの引数のKeyに
UniqueKey()
を追加します。Key追加class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), //StatefulColorfulTile(), //StatefulColorfulTile(), StatefulColorfulTile(key: UniqueKey()), // 変更!! StatefulColorfulTile(key: UniqueKey()), // 変更!! ]; }StatefulWidget (Key指定あり) の動作
Keyを指定した場合は、
Element.updateChild
内の判定で新旧WidgetのKeyが異なるため、rebuild処理が走ります。ただし、rebuildといってもStatelessWidget
とは異なり、Elementツリーの同一階層内で同じKeyのElementを探し、もし存在すればそのElementと位置を入れ替えることで処理を軽量化しています。図示すると以下のような感じになります。
参考文献
- 投稿日:2020-02-24T16:19:50+09:00
Flutter WidgetにKeyが必要な理由, 仕組み, 使い方について解説 - 前編
はじめに
Flutterの Widget でたまに指定する
Key
について、何となく必要性は理解できるものの、完全に理解するためにまとめることにしました。今回は前編として、主に
Key
とは何かと、Key
が必要になる理由について解説します。
後編では、Key
の種類一覧やKey
指定位置について解説予定です。
Key
とは?簡単に言うと、ElementからWidgetを識別するためのIDです (RenderObjectもWidgetを参照します…)。
Key
は意図的に指定しないとデフォルトではnull
です。必要となるシーンが限定される感じですが、よく紹介されているのは以下ですね。
ただ、必要性が分かるようで分からない感じで、何となく使えている感じがします。
- ToDoアプリのようなStateをもったWidget郡のソート, 追加, 削除
- Listのスクロール位置の保存
本記事を最後まで読んで頂くと理解できると思いますが、Widgetツリーの中でノード (あるWidget) を他の場所に移動させる場合など、Widgetツリーと対になるElementツリー側から特定のWidgetを識別する必要がある場合にのみKeyを利用します。
Key
が必要な理由についてサンプルアプリを用いて、
Key
の必要性を理解していきます。タイルを入れ替える簡単なアプリ
FABをクリックする度に2つのタイル (ランダムな色で塗り潰た四角) の位置を入れ替えるだけの簡易アプリです。
ソースコードは以下です。
StatelessWidget
を用いてアプリ起動時にランダムな色のタイルを配置し、ボタンが押される度にListの順番を入れ替えるだけです。ソースコードimport 'package:flutter/material.dart'; import 'dart:math'; class StatelessColorfulTile extends StatelessWidget { final Color _color = UniqueColorGenerator.getColor(); @override Widget build(BuildContext context) { return Container( color: _color, child: Padding(padding: EdgeInsets.all(100.0))); } } class UniqueColorGenerator { static List _colorOptions = [ Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.purple, Colors.orange, Colors.indigo, Colors.amber, Colors.black, ]; static Random _random = new Random(); static Color getColor() { if (_colorOptions.length > 0) return _colorOptions.removeAt(_random.nextInt(_colorOptions.length)); else return Color.fromARGB(_random.nextInt(256), _random.nextInt(256), _random.nextInt(256), _random.nextInt(256)); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ StatelessColorfulTile(), StatelessColorfulTile(), ]; } void _swapTiles() { setState(() { _tiles.insert(1, _tiles.removeAt(0)); }); } @override Widget build(BuildContext context) { return Scaffold( body: Row(children: _tiles), floatingActionButton: FloatingActionButton( child: Icon(Icons.sentiment_satisfied), onPressed: _swapTiles), ); } }StatelessWidgetの動作
StatelessWidget
の場合、作成されるStatelessElement
はState
を保有しないため、Widgetは描画毎に必ずrebuild (Widget build(BuildContext context)) が実行されます。つまり、FABを押す度にsetState
をトリガーにして、2つのタイルが再構成 (rebuild) されて、再描画され、結果的にスワップしたように見えます。また、Flutterは各Widgetをツリー構成で管理しており、
Widget
/Element
/RenderObject
の3つのツリーが構築されます。ざっくり、WidgetがUIパーツの構成/仕様、ElementがそのWidgetの状態管理やRenderObjectとの仲介、RenderObjectElementが描画管理という役割分担で、それぞれのツリーは相互参照の関係になります。今回のサンプルでのツリー構成を以下に示します。
ただし、説明簡略化のためにRenderObjectElementは割愛します。
rebuild時の動きは以下のpackages/flutter/lib/src/widgets/framework.dartのコードを追ってみると分かります。
仕組みを追いかけるなら、サンプルを作成してそれをデバッガでステップ実行しながら動作確認する手法がお勧めです。
Element.updateChild
→StatelessElement.update
→Element.rebuild
→ComponentElement.performRebuild
→StatelessElement.build
→StatelessColorfulTile.build
という順番でコールされます。StatelessElement.updateclass StatelessElement extends ComponentElement { /// Creates an element that uses the given widget as its configuration. StatelessElement(StatelessWidget widget) : super(widget); @override StatelessWidget get widget => super.widget; @override Widget build() => widget.build(this); @override void update(StatelessWidget newWidget) { super.update(newWidget); assert(widget == newWidget); _dirty = true; rebuild(); // ← ここでrebuildがコールされる } }StatefulWidgetだとどうなるか?
今度は
StatelessWidget
からStatefulWidget
に置き換えて動かしてみます。StatefulWidgetに置き換えclass StatefulColorfulTile extends StatefulWidget { StatefulColorfulTile({Key key}) : super(key: key); @override ColorfulTileState createState() => ColorfulTileState(); } class ColorfulTileState extends State<StatefulColorfulTile> { Color _color; @override void initState() { super.initState(); _color = UniqueColorGenerator.getColor(); } @override Widget build(BuildContext context) { return Container( color: _color, child: Padding( padding: EdgeInsets.all(100.0), )); } } class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), StatefulColorfulTile(), // 変更!! StatefulColorfulTile(), // 変更!! ]; }何故でしょう?その理由こそKeyが必要な理由です。
(個人的には、フレームワークの作り上の都合な産物な気もしますが…)StatefulWidget (Key指定なし) の動作
StatefulWidget
とStatelessWidget
の最大の違いはState
の有無です。
StatefulWidget
はsetState
をトリガーにした描画更新処理でElementのrebuildを実施するかどうかの判定に、参照する新旧のWidgetのKey
とRuntimeType
(クラス名のこと) を参照しています。ソースコードでは、Element.updateChildとWidget.canUpdateが該当部分になります。
Element.updateChild@protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { assert(() { if (newWidget != null && newWidget.key is GlobalKey) { final GlobalKey key = newWidget.key; key._debugReserveFor(this); } return true; }()); if (newWidget == null) { if (child != null) deactivateChild(child); return null; } if (child != null) { if (child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } if (Widget.canUpdate(child.widget, newWidget)) { // Key,RuntimeTypeが同じならWidgetの参照だけ更新して終了 if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); assert(child.widget == newWidget); assert(() { child.owner._debugElementWasRebuilt(child); return true; }()); return child; } deactivateChild(child); assert(child._parent == null); } return inflateWidget(newWidget, newSlot); }Widget.canUpdate@immutable abstract class Widget extends DiagnosticableTree { const Widget({ this.key }); final Key key; @protected Element createElement(); @override String toStringShort() { return key == null ? '$runtimeType' : '$runtimeType-$key'; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense; } // 新旧でRuntimeTypeとKeyが同じかどうかチェック static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; } }Key指定しない場合は新旧Widgetどちらもnull、RuntimeTypeは同じクラス名となり、rebuildは実施されず、ElementからWidgetへの参照だけの更新となります。その後のRenderObjectもElementがrebuildされていないため、描画処理が更新されません。
この時点でおかしな状態になっていることが分かります。
Keyを指定して解決
先ほどの問題を解決するために、StatefulColorfulTileクラスの引数のKeyに
UniqueKey()
を追加します。Key追加class SampleAppPageState extends State<SampleAppPage> { List<Widget> _tiles; @override void initState() { super.initState(); _tiles = [ //StatelessColorfulTile(), //StatelessColorfulTile(), //StatefulColorfulTile(), //StatefulColorfulTile(), StatefulColorfulTile(key: UniqueKey()), // 変更!! StatefulColorfulTile(key: UniqueKey()), // 変更!! ]; }StatefulWidget (Key指定あり) の動作
Keyを指定した場合は、
Element.updateChild
内の判定で新旧WidgetのKeyが異なるため、rebuild処理が走ります。ただし、rebuildといってもStatelessWidget
とは異なり、Elementツリーの同一階層内で同じKeyのElementを探し、もし存在すればそのElementと位置を入れ替えることで処理を軽量化しています。図示すると以下のような感じになります。
参考文献
- 投稿日:2020-02-24T15:25:33+09:00
signal SIGABRT の解決
signal SIGABRTにあたってしまったお。
signal abort: シグナルが途切れた、つまり、referenceできないよーってなんとなく言われているはず。
エラーコードとエラーコードググった感じから、SwifterライブラリのターゲットバージョンがiOSのバージョンとズレてるのかとかの感覚で、実際、podfileでSwifterライブラリのターゲットバージョンを変更した後だったから、これは、適切なバージョン調べないとダメか?と小1時間くらいswifterのドキュメントとか見てやったけど、
解決法は、”cleanしてbuild”しただけ。
参考: http://www.project-unknown.jp/entry/2015/03/28/133347バグの解決は、
可能性を列挙->簡単な選択肢から潰す。
これをキッモに銘じよう。
- 投稿日:2020-02-24T13:19:45+09:00
【2020年デザイン】NeumorphismというUIデザインを知っていますか?
Neumorphismというデザインを知っていますか?
このようになデザインのことを言います。
- 要素に明暗2色のドロップシャドウを付ける
- 明るいシャドウ(光)は左上に、暗いシャドウは右下に落とす
- 要素の色は要素を配置している背景色と同じにする
- 凸型(convex)と凹型(concave)がある
- 凹型はインナーシャドウを使い、明暗を逆にする
【衝撃】2020年のUIデザインのトレンド!?『Neumorphism』とは?時代が進むにつれ、シンプルになっていく。。
- 投稿日:2020-02-24T12:14:09+09:00
Firebaseでお手軽サーバレスアプリ(Realtime Database+CloudFunctions+Cloud Messaging+Android(kotlin)+iOS(swift))その3
はじめに
今回はiOSからFirebaseのRealtime Databaseに接続しデータの登録と参照を行ってみます。
必要なのは、MacとXCodeです。
今回はMacBook Air (Retina, 13-inch, 2018)、XCode Version 11.3.1 (11C504)を使っています。XCodeでアプリのベースを作成
- Create new project を実行
- iOSのSingle View Appを選択
- Product nameを適当につけます
- 言語はSwift、User InterFaceはStoryBoardを選択
- 実行してスケルトンが動作することを確認します
Firebaseの準備
iOSアプリにFireBaseを追加するで、iOSアプリのバンドルIDを入力します
バンドIDはプロジェクトを選択しGeneralのタブで参照できます
設定ファイルをダウンロードせよと出るのでFireBaseのページで指示されるとおりに操作します
plistを追加できたら「次へ」を押します
cocoapodsが必要なので、入れてない場合はインストールします
terminalでプロジェクトのフォルダを開きます
terminalでの手順はFireBaseのページの通です
- pod init
- Podfilesの編集
- pod install
xcodeを一旦閉じて「.xcworkspace」で開きます
AppDelegateにFirebaseへ接続するコードを追加します
AppleDelegate.swiftimport UIKit import Firebase @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. FirebaseApp.configure() return true } // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } }
- ここまで来たらビルドして実行して、firebaseのページでアクセスできたか確認します
- firebaseがアプリに正常に追加されました と表示されればO K
iOSアプリにUIを追加
- Main.stoyboardにTextViewを追加します
- Constraintsは上左右は0、下は200
- 下のスペースにButtonを追加します
- Constrainsは上下左右5に設定します
DBへの書き込みを追加
- ViewControllerにボタンを押した際のイベントハンドラーを追加
- DBに書き込むコードを書きます
ViewController.swiftimport UIKit import FirebaseDatabase class ViewController: UIViewController { var databaseRef: DatabaseReference! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. databaseRef = Database.database().reference().child("/log") } @IBAction func WriteToDB(_ sender: Any) { view.endEditing(true) let MyTime:Int = Int(NSDate().timeIntervalSince1970*1000) let messageData = ["name": "user2(ios)", "message": "だれ?","timestamp":String(MyTime)] databaseRef.childByAutoId().setValue(messageData) } }
- FirebaseDatabaseが解決できないとエラーになると思います。PodsのprofileにFirebaseDatabaseを追加します。
- Podsに追加しらPod installをします
profile# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'otameshi-ios' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for otameshi-ios pod 'Firebase/Analytics' pod 'Firebase/Database' end
- ビルドして動作させます。ボタンを押してDBにデータが書き込まれれば成功です
データの取得と表示
- データの更新前にTextViewを更新します
ViewContoller.swiftimport UIKit import FirebaseDatabase class ViewController: UIViewController { var databaseRef: DatabaseReference! @IBOutlet weak var TextView: UITextView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. databaseRef = Database.database().reference().child("/log") databaseRef.observe(.childAdded, with: { snapshot in if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"], let time = obj["timestamp"] { let currentText = self.TextView.text self.TextView.text = (currentText ?? "") + "\n \(time) \(name) : \(message)" } }) } @IBAction func WriteToDB(_ sender: Any) { self.view.endEditing(true) let MyTime:Int = Int(NSDate().timeIntervalSince1970*1000) let messageData = ["name": "user2(ios)", "message": "だれ?","timestamp":String(MyTime)] self.databaseRef.childByAutoId().setValue(messageData) } }TextViewをオートスクロールに
- 一番下まで行った時に勝手にスクロールするようにします
ViewContoller.swiftimport UIKit import FirebaseDatabase class ViewController: UIViewController { var databaseRef: DatabaseReference! @IBOutlet weak var TextView: UITextView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. databaseRef = Database.database().reference().child("/log") databaseRef.observe(.childAdded, with: { snapshot in if let obj = snapshot.value as? [String : AnyObject], let name = obj["name"] as? String, let message = obj["message"], let time = obj["timestamp"] { let currentText = self.TextView.text self.TextView.isScrollEnabled = false self.TextView.text = (currentText ?? "") + "\n \(time) \(name) : \(message)" self.scrollToBottom() } }) } @IBAction func WriteToDB(_ sender: Any) { self.view.endEditing(true) let MyTime:Int = Int(NSDate().timeIntervalSince1970*1000) let messageData = ["name": "user2(ios)", "message": "だれ?","timestamp":String(MyTime)] self.databaseRef.childByAutoId().setValue(messageData) } func scrollToBottom() { TextView.selectedRange = NSRange(location: TextView.text.count, length: 0) TextView.isScrollEnabled = true let scrollY = TextView.contentSize.height - TextView.bounds.height let scrollPoint = CGPoint(x: 0, y: scrollY > 0 ? scrollY : 0) TextView.setContentOffset(scrollPoint, animated: true) } }まとめ
これでAndroidとiOSのアプリが揃いました。
それぞれでボタンを押すと同時に反映されることが確認できると思います。このシリーズについて
その2:Androidのクライアントアプリからデータの登録と表示まで
その3:iOSのクライアントアプリからデータの登録と表示まで
[その4:準備中]
[その5:準備中]
- 投稿日:2020-02-24T09:06:47+09:00
お絵描きアプリで良い感じの線を引く今時の方法を調べた
はじめに
昨年、自作のお絵かきゲームアプリをリリースしました。
アナリティクスを見ると、残念ながら当初私が考えていたようなゲーム用途としては全く手応えなしだったんですが、
デッサン練習用途としては、少しだけ需要が感じられる状態でございます。
そこで、使っていただいているユーザーの方からapple pencilで線が途切れるとの情報を寄せていただいたので、この機会に、線の引き方について再度調べてみました。これまで線の引き方については、
KikurageChan様の記事
iOS標準機能の良いお絵描きアプリを目指して・・・
hollymoto様の外部記事
https://anthrgrnwrld.hatenablog.com/entry/2016/07/14/230929を参考にさせていただき、UIBezierPathを使いタッチ開始/終了の中点を取ってベジェ曲線を描いていたのですが、(紹介されている中では手軽なやり方)
傾けても使えるようになっているapple pencilでは、タッチを拾えないときがあると分かりました。合体タッチ
調査の結果、Swift 9.0からは合体タッチ(coalescedTouches)と呼ばれる機能が存在し、この機能を使えばapple pencilの傾きを検知し、かつ、滑らかに線が引けることが分かりました。
従来の方法ではタッチイベントのストロークごとに始点と終点が取れるので、間に直線を引くか、中点を自前で計算して、ベジェ曲線を引いていました。
合体タッチではストロークごとのタッチを配列に持ち、複数の制御点を使って線を引ける仕組みであるようです。合体タッチを使うと、特別なことをしなくても複数の制御点が手に入るので、滑らかな線が引けそうですね。
ただ、iOSデバイスのタッチスキャン性能に影響されます。またやはり、指よりもapple pencilの方が滑らかです。
一応、iPhone 5sで指で描くことを試してみても、そこまで角張った線にはならなかったので、
iOS9.0以上が使えるデバイスであれば、採用を検討できるのではないかと思います。サンプルコード
private func drawStroke(context: CGContext, touch: UITouch) { let previousLocation = touch.previousLocation(in: self) let location = touch.location(in: self) drawColor.setStroke() context.setLineWidth(lineWidth) context.setLineCap(.round) context.move(to: CGPoint(x: previousLocation.x, y: previousLocation.y)) context.addLine(to: CGPoint(x: location.x, y: location.y)) context.strokePath() } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } guard let canvas = self.imageView.image else { return } UIGraphicsBeginImageContextWithOptions(canvas.size, false, 0.0) guard let context = UIGraphicsGetCurrentContext() else { return } let imgCanvasRect = CGRect(x: 0, y: 0, width: canvas.size.width, height: canvas.size.height) tmpImage?.draw(in: imgCanvasRect) var touches = [UITouch]() //合体タッチ取得 if let coalescedTouches = event?.coalescedTouches(for: touch) { touches = coalescedTouches } else { touches.append(touch) } for touch in touches { drawStroke(context: context, touch: touch) } //合体タッチで描いた画像の取得 tmpImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return tmpImage }本来、タッチイベントはUIImageView上で取得したかったのですが、
iOS13.0からのデフォルトであるSwiftUIを雛形にするプロジェクトでは
AppDelegateからViewControllerを起動したとき、
なぜかUIImageView上でのtouchesMovedイベントが発生しない状態になりました。。
UIView上でならば、上記で動くことを確認しました。
しかし毎回大きく変わるなぁ・・・。予測タッチ
正直、自分ではドキュメントを見ても仕組みがわからないのですが、
iOS9.0以降、予測タッチという機能が使えるようです。
タッチに対する画面レンダリングの遅延をカバーするため、
デバイスに蓄積された予測データに基づき、まだタッチされていない箇所に、あらかじめ線を引いているらしいです。
描く速度が早すぎて描画できないという事態を防ぐ手段と理解しました。予測タッチを採用する場合、タッチのレンダリングが正しく行われた場合は
予測分の描画を破棄する必要が出てきます。
次のサンプルコードでは、合体タッチの線 + 予測タッチの線を画像として保存しておき、
タッチイベントが完了した時は合体タッチの線のみの画像で上書きすることで予測の破棄を行っています。override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } guard let canvas = self.imageView.image else { return } UIGraphicsBeginImageContextWithOptions(canvas.size, false, 0.0) guard let context = UIGraphicsGetCurrentContext() else { return } let imgCanvasRect = CGRect(x: 0, y: 0, width: canvas.size.width, height: canvas.size.height) tmpImage?.draw(in: imgCanvasRect) var touches = [UITouch]() if let coalescedTouches = event?.coalescedTouches(for: touch) { touches = coalescedTouches } else { touches.append(touch) } for touch in touches { drawStroke(context: context, touch: touch) } //合体タッチで描いた画像の取得 tmpImage = UIGraphicsGetImageFromCurrentImageContext() //予測タッチ取得 if let predictedTouches = event?.predictedTouches(for: touch) { for touch in predictedTouches { drawStroke(context: context, touch: touch) } } //合体タッチ + 予測タッチで描いた画像の取得 self.imageView.image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { //タッチイベント完了時、合体タッチのみの画像で上書きする self.imageView.image = self.tmpImage } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { //タッチイベントキャンセル時、合体タッチのみの画像で上書きする self.imageView.image = self.tmpImage }サンプルコード・解説ともにほとんど
https://www.raywenderlich.com/121834/apple-pencil-tutorial
の翻訳なので、さらに詳しく知りたい方はこちらをご確認ください。UIBezierPathも使ってみる
合体タッチ・予測タッチとも、制御点同士の間の線を直線で描いています。
制御点が多く取れる場合は良いですが、少ないと角ばる可能性が出てきます。
そこで、制御点同士の間の線をcontext.addLineの代わりに、UIBezierPathで描いてみるのを試してみました。コードは後述のGitHubにあります。
動かしてiPhone5s上で幾つも丸を描きましたが、私の絵心がないせいか、違いはほとんど分かりませんでした。
ただ理屈上はわずかでも滑らかになっているはずで、今後自分のアプリでも使ってみたいと思います。PencilKitだ…と?
ここまで書いて何ですが、iOS13.0からはPencilKitというものが導入されていて、
iOSメモ帳で使えるペンのパレットツールが、簡単に実装できるようです。niwasawa様の記事
たった3行のコードで PencilKit を導入して Apple Pencil 対応これから作るアプリとかこれで良さそう。。
まだ自分で使っていないので分からないのですが、もしカスタイマイズが難しいのであれば
本記事の内容も場面によっては役に立てるでしょうか。おわりに(宣伝)
お絵かきに興味のある方、宜しければ冒頭のアプリ、日々のデッサン練習に使ってみてください。
絵が上手くなって第二の鬼滅の刃描いちゃってください!
私としても、まだもう少し使い勝手を向上させて行きたいと思います。記事内のサンプルコードをGitHubにあげました。
参考にして頂けましたら幸いです。
- 投稿日:2020-02-24T06:26:22+09:00
【Swift、Objective-c】画面遷移とメモリ使用について
Swift、Objective-cでの画面遷移をする際の遷移方法、メモリ使用についてまとめました。
画面遷移する方法
ViewController間での画面遷移は、storyboard ID、Segue Identifierを使用する方法と
dismissViewControllerAnimatedを使用する方法があります。Objective-c//storyboard IDを使用した画面遷移 UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:@"storyboard ID"]; [self presentViewController:vc animated:NO completion:nil]; //Segue Identifierを使用した画面遷移 [self performSegueWithIdentifier:@"Segue Identifier" sender:self]; //dismissViewControllerAnimatedを使用した画面遷移 [self dismissViewControllerAnimated:NO completion:nil];Swift//storyboard IDを使用した画面遷移 let secondViewController = self.storyboard?.instantiateViewController(withIdentifier: "storyboard ID") as! nextViewController self.present(secondViewController, animated: true, completion: nil) //Segue Identifierを使用した画面遷移 self.performSegue(withIdentifier: "Segue Identifier", sender: self) //dismissViewControllerAnimatedを使用した画面遷移 self.dismiss(animated: false, completion: nil)画面遷移する場合のメモリ使用量
2つのViewController間を画面遷移した場合、メモリ使用量の変化は以下のようになります。
dismissViewControllerAnimatedで画面遷移をした場合のメモリ使用量
画面遷移した時に、メモリが解放されます。
storyboard ID、Segue Identifierで画面遷移をした場合のメモリ使用量
画面遷移をした時に新しく画面を作成していくため、画面遷移してもメモリが解放されず、
メモリが溜まっていきます。
dismissViewControllerAnimated以外での画面遷移では新しく画面を作成する。
そのため、storyboard ID、Segue Identifierのみで画面遷移を続けた場合、メモリが解放されず溜まっていき、
結果として動作が重くなりアプリが強制終了してしまう。
そのため、新しい画面への画面遷移以外は、dismissViewControllerAnimatedで戻った方が良い。dismissViewControllerAnimatedで遷移した場合の問題点
メモリ解放のため、dismissViewControllerAnimatedを使用した画面遷移を使用していきたいが、dismissViewControllerAnimatedは、現在表示されているViewControllerを削除することで画面遷移するため、戻った後のViewControllerは更新されない。
そのため、TableViewの画面情報を再表示したい場合、追加の処理を行う必要がある。
NSNotificationを使用したTableViewの更新
解決策として事前にmainViewControllerでNSNotificationの通知要求を登録しておき、遷移後のViewControllerから画面遷移する時に、通知を送ることで画面遷移後にTableViewを更新することができます。
Objective-c mainViewController- (void)viewDidLoad { [super viewDidLoad]; //通知要求の登録をする [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hoge:) name:@"reload" object:nil]; } //通知を受信した時に実行する -(void)hoge:(NSNotification *)notification { if([[notification name]isEqual:@"reload"]) { //TableViewの更新処理 [tableView reloadData]; } }Objective-c nextViewController//画面遷移後に通知を送信する [self dismissViewControllerAnimated:NO completion:^{ [[NSNotificationCenter defaultCenter]postNotificationName:@"reload" object:self]; }];Swift mainViewControlleroverride func viewDidLoad() { super.viewDidLoad() //通知要求の登録をする let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(hoge), name: Notification.Name(rawValue:"reload"), object: nil) } //通知を受信した時に実行する @objc func hoge(notification: Notification?) { if notification!.name.rawValue == "reload" { tableView.reloadData() } }Swift nextViewController//画面遷移後に通知を送信する self.dismiss(animated: false, completion: { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reload"), object: nil) })mainViewControllerまで一気に戻る場合
複数画面遷移している状態から一気にmainViewControllerまで戻る場合以下のようにすれば良い。
Objective-c[[UIApplication sharedApplication].windows[0].rootViewController dismissViewControllerAnimated:NO completion:nil];Swiftlet window = UIApplication.shared.windows[0].rootViewController window?.dismiss(animated: false, completion: nil)参考
presentViewController:animated:completion:
dismissViewControllerAnimated:completion:
https://tutorialmore.com/questions-2605800.htm