- 投稿日:2019-11-29T23:57:15+09:00
[はじめてのiOSアプリ]xcodeで地図アプリを作成(その5)
はじめに
iOSアプリを作ってみたいけど
何から始めて良いのかわからないとりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思うという記事の5回目です。
今回は、アプリケーションアイコンを設定します。
アイコン画像作成
- センスのある人はカッコよく、センスのない人はそれなりの画像を作成
- いろいろなサイズを用意
- ファイルをxcodeの画面にドラッグ&ドロップ
- 【なぜ?】
- これでxcodeに登録される
- ファイル名からサイズがわかるようにしたのは、我ながらGoodでした
- なお、サイズ間違いだと警告表示される
- テスト実行
今回の到達点
- アプリケーションアイコンが「それなり」になった
連載
- 投稿日:2019-11-29T23:35:15+09:00
UIViewをコードで書くかxibで書くかの話(状況別)[Swift]
Storyboardをどこまでどう使うかはクックパッドさんの1Storyboard = 1VCの記事などでも話に上がってますが
もっと細かいところでUIViewを設置する際
こんな感じでコードで書くかview.addSubview(hogeView)だったりxibファイル作って
StoryboardでView設置して紐付けるかどういう基準で選ぶの?と思っていたのですが状況別でそれとなく理解したのでまとめておきます
Viewが中身しか変化しない場合
xibで作って設置
->中身の変化だけVCでかくGirlsView.swiftimport UIKit class GirlsView: UIView { //プロパティは省略 //storyboardで設置した時 required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder)! loadNib() } override func awakeFromNib() { super.awakeFromNib() //viewのautolayout取得前//VCの制約決定前 } override func layoutSubviews() { super.layoutSubviews() //viewのautolayout取得前後//VCの制約決定前 } func loadNib() { //こっちでも if let view = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? UIView { view.frame = self.bounds self.addSubview(view) } //こっちでもViewのUI表示してくれる //String(describing: type(of: self)) = "GirlsView" let view = Bundle.main.loadNibNamed("GirlsView", owner: self, options: nil)?.first as! UIView view.frame = self.bounds self.addSubview(view) } //ぐぐるとこのパターンも結構あった//こちらだとうまく表示できず private func configure() { let nib = UINib(nibName: "GirlsView", bundle: nil) //nibは取得できたが、各要素の取得がダメになるらしい //Viewの各要素はfile's ownerに紐付けないとダメ? //File's OwnerのみにGirlsViewを紐付けた状態で繋ぐ->各要素が勝手に親のUIViewではなくFile's Ownerに紐づく guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else { return } addSubview(view) } }Viewの数を動的に変化させたい場合
Tinderの模擬アプリをYoutube用に作成した時
がこの場合に当たります
Viewの数だけVCで設置するというのは
必要なViewの数が変化した時に対応できないので
雛形だけxibで作っておいて、設置や中身の設定はVCでやるというのが良さそうですGirlsView.swiftimport UIKit class GirlsView: UIView { //プロパティは省略 //コードで設置した時 override init(frame: CGRect) { super.init(frame: frame) loadNib() } override func awakeFromNib() { super.awakeFromNib() //viewのautolayout取得前//VCの制約決定前 } override func layoutSubviews() { super.layoutSubviews() //viewのautolayout取得前後//VCの制約決定前 } func loadNib() { //こっちでも if let view = Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)?.first as? UIView { view.frame = self.bounds self.addSubview(view) } //こっちでもViewのUI表示してくれる //String(describing: type(of: self)) = "GirlsView" let view = Bundle.main.loadNibNamed("GirlsView", owner: self, options: nil)?.first as! UIView view.frame = self.bounds self.addSubview(view) } //ぐぐるとこのパターンも結構あった//こちらだとうまく表示できず private func configure() { let nib = UINib(nibName: "GirlsView", bundle: nil) //nibは取得できたが、各要素の取得がダメになるらしい //Viewの各要素はfile's ownerに紐付けないとダメ? //File's OwnerのみにGirlsViewを紐付けた状態で繋ぐ->各要素が勝手に親のUIViewではなくFile's Ownerに紐づく guard let view = nib.instantiate(withOwner: self, options: nil).first as? UIView else { return } addSubview(view) } }ポイント
- xibでcustomViewを生成->コードで配置の場合
->
loadNibNamed
でxibを指定->フレームを指定->addSubViewをcustomView内でもやる
このやり方しかうまくViewを表示できませんでした。
->File's Owner
との兼ね合いでこういう結果になったんだと思います。
またわかったら追記します。
- 2つの
init
,awakeFromNib
,layoutSubviews
の違い->
override init(frame: CGRect)
: コードでcustomView設置したとき呼ばれる
->required init?(coder aDecoder: NSCoder)
: storyboardでcustomView設置したとき呼ばれる
->awakeFromNib
: storyboardでcustomView設置したとき、required init
の後に呼ばれる->View要素のframeはAutoLayout適用前の値
->layoutSubviews
: storyboardでcustomView設置したとき(なのかはあとで調べる)、required init
,awakeFromNib
の後に呼ばれる->View要素のframeはAutoLayout適用後の値最後に
いろんなやり方があると思いますので他の意見や反論あればどしどしお待ちしてます
参考
・init(frame:), init(coder:), awakeFromNib, prepareForInterfaceBuilder が呼ばれる条件をまとめてみた
・[Swift, iOS] awakeFromNibでView要素のframeサイズがおかしい - TERAKOYA
・IB/Storyboard使わない派のlayoutSubviewsによるレイアウト調整 - Qiita・UIViewControllerのライフサイクル - Qiita
->Viewとは関係ないけど合わせてやっとくと良さそうなので、、、
- 投稿日:2019-11-29T21:51:45+09:00
SwiftUI Textの使い方
SwiftUI Textの基本的な使い方メモ
// Hello Worldを表示する Text("Hello World") // 行数の制限もできる Text("Hello World") .lineLimit(5) // 行数を無制限にしたい場合はnil Text("Hello World") .lineLimit(nil) // 長い文章の場合はtruncationModeを利用して一部省略できる Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") .truncationMode(.middle) // fontの設定 Text("Hello World") .font(.largeTitle) // 背景などの色の設定 Text("Hello World") .foregroundColor(Color.white) .background(Color.gray) // 文字の間隔を設定 Text("Hello World") .kerning(10) // 複数行のalignmentを設定 Text("Hello World") .multilineTextAlignment(.center)
- 投稿日:2019-11-29T21:26:13+09:00
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'presentingViewController must be set.' のエラー解決方法【GoogleSignIn for iOS】
SwiftでFirebaseを使ってGoogleログインするときにでたエラーの解決方法です。
原因
出たエラーはこちら
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'presentingViewController must be set.'ボタンをクリックしたときグーグル認証されるというもの。
ボタンをタップしたときに上記のエラーが起こる。googleLogin.py@IBAction func loginGoogleButton(_ sender: Any) { GIDSignIn.sharedInstance()?.signIn() }解決方法
これを
GIDSignIn.sharedInstance()?.uidelegate = selfこう変える
GIDSignIn.sharedInstance()?.presentingViewController = selfGoogle Sign inがバージョン5.0.0になったことで色々仕様が変わったようですね。
以下引用
公式ドキュメント
https://developers.google.com/identity/sign-in/ios/release
github
https://github.com/EddyVerbruggen/nativescript-plugin-firebase/issues/1370
- 投稿日:2019-11-29T20:22:53+09:00
[Swift]StackViewとhiddenを使って画面を作成したこと
概要
StackViewを作ってその中のLabelをhiddenでトルツメをするという作業を行ったのでメモしたいと思います。
やりたいこと
StackView、トルツメを使った名前入力画面の作成
それぞれの要素ごとにやりたいこと
①画面のタイトル(概要)ラベル
・名前入力画面の概要として表示位置は固定で置きたい②名前を入力するTextField
・ここで名前を入力させる
・名前入力に不備があった場合はエラー(④⑤)を表示させたい③次の画面に遷移するボタン
・名前入力に不備があった場合は遷移させずにエラーを出したい
・名前入力に不備がない場合は次の画面に遷移させたい④⑤名前入力で不備があった場合に出現するエラーラベル
・通常時はエラーラベルは隠しておきたい
・エラーラベル非表示は②TextFieldは①タイトルラベルの下におきたい
・③ボタンを押した時に不備があった場合、②の上にラベルを出現させたい今回StackViewで実現させたい動作
・名前入力に不備があった場合はエラー(④⑤)を表示させたい ・通常時はエラーラベルは隠しておきたい ・エラーラベル非表示は②TextFieldは①タイトルラベルの下におきたい ・③ボタンを押した時に不備があった場合、②の上にラベルを出現させたい上記をStackViewで実現させていきます。
Storyboard上の手順
1.二つのエラーラベルを選択状態にする(commandを押しながらクリック)
5.2~3と同様の手順を再度行う(Embed In → StackView)
↓StackViewとTextFieldが更にStackViewでまとまる
ここまでStoryboard上のみの作業は完了です。
ViewControllerでの手順
class ViewController: UIViewController { // 1.エラーラベルのStackViewを接続(errorStackView) @IBOutlet weak var errorStackView: UIStackView! override func viewDidLoad() { super.viewDidLoad() // 2.画面表示時にerrorStackViewをhiddenで非表示にさせる errorStackView.isHidden = true } @IBAction func tapButton(_ sender: Any) { // 2.ボタンタップ時にエラーが出る仮定として出現させる errorStackView.isHidden = false } }完成!
シュミレーターを起動するとちゃんとerrorStackViewが非表示されてTextFieldがトルツメされている!
↓ボタンをタップするとerrorStackViewが出現!
まとめ
今回は
- 投稿日:2019-11-29T17:44:29+09:00
【swift】var geometry: [MKShape & MKGeoJSONObject] という定義を見てビックリした
小ネタですが、1時間ぐらい調べてしまったので、書いときます。
あなたはわかりますか?
AppleのMapkitにGeoJSONを処理するクラスがあって、それを仕事でゴニョゴニョしています。
そんな中、こんなプロパティを見つけました。var geometry: [MKShape & MKGeoJSONObject] { get }これを最初見たとき、「え? 型2つあるやん!」(誤解)と焦りました。
答え
正解は、MKGeoJSONObjectはprotocolで、型はMKShapeだけです。
MKShapeはClassなので、MKGeoJSONObjectに従うClass=MKShapeという指定でした。プロトコルコンポジションというカッコいい名前があるみたいです。
プロトコルコンポジションするときは、だいたい下記みたいに、カンマで区切るのが多いので、
今回みたいに&でつなぐパターンもあるのか〜と新発見しました。class hoge: protocolA, protocolB,…これをプロパティの型に指定して書いてやると、
var any: [hoge & protocolA & protocolB]こんな感じになるわけですね。
「,」区切りのときも継承クラスとの区別がわかりづらいな〜と思っていましたが、
「&」区切りだと完全に型とプロトコルが同列に並んでるように見えて、僕にはわかりづらく感じます。
慣れなんですかね?
- 投稿日:2019-11-29T17:29:30+09:00
【Swift】Swiftの"?"と"!",はじめからていねいに (1/2)
はじめに
この記事は明治大学/明治大学大学院 Advent Calendar 2019の7日目の記事です.
お久しぶりです. 昨年度までプログラミング演習を中心にTAを担当していました河野です.卒業後はiOSアプリエンジニアにジョブチェンジして日々修行しています.今回の投稿では,Swiftの1大テーマである"?"と"!"について一度整理しようと思い,このテーマにしました.既存の解説記事とはやや切り口が異なりますが,個人的にこのまとめ方で理解するのもわかりやすいのでは,と試行錯誤してまとめてみたので,よろしくお願いします.
型の後ろの"?"と"!"
Swiftでは変数を宣言する際に型の後ろに"?"・"!"をつけることがあります.これは変数をオプショナル型として宣言する場合につけるものです.Swiftでは変数にnilを代入することができません.nilの代入を許容するには,変数宣言するときにその変数をオプショナル型変数として宣言する必要があります.Swiftのオプショナル型変数にはオプショナル型(Optional Value)と暗黙的アンラップ型(Implicitly Unwrapped Optional)の2種類があります.
var hoge: Int //Int型(nilを許容しない) var fuga: Int? //オプショナルInt型(nilを許容する) var piyo: Int! //暗黙的アンラップInt型(nilを許容する)オプショナル型変数と非オプショナル型変数は同じInt型であっても異なるデータ型という扱いになります.
var hoge: Int = 10 //Int型(nilを許容しない) var fuga: Int? = 10 //オプショナルInt型(nilを許容する) var piyo: Int! = 10 //暗黙的アンラップInt型(nilを許容する) print(hoge) //10 print(fuga) //Optional(10) print(piyo) //Optional(10)したがって,これらを二項演算すると型が異なる変数同士を演算することになるためエラーになります.
var hoge: Int = 10//Int型(nilを許容しない) var fuga: Int? = 10//オプショナルInt型(nilを許容する) print(hoge + fuga) //---- 以下出力されるエラー ----// error: value of optional type 'Int?' must be unwrapped to a value of type 'Int' print(hoge + fuga)このとき,この演算を実行するためにはオプショナル型変数に対して,オプショナル変数に格納されている値を取り出すアンラップ (unwrap)という操作が必要になります.
オプショナル型変数のうち暗黙的アンラップ型は演算が実行されるタイミングで自動的に(暗黙的に)アンラップの操作を行います.
var hoge: Int = 10//Int型(nilを許容しない) var piyo: Int! = 10//暗黙的アンラップInt型(nilを許容する) print(piyo)//Optional(10) print(hoge + piyo)//20 print(piyo == 10)//trueしかし,piyo = nilの場合にアンラップを行なった場合,nilと(nilを許容しない)非オプショナル型を二項演算することになるためエラーが発生します.したがって,nilの可能性があるからという理由で闇雲に変数を暗黙的オプショナル型として宣言することは危険です.
var hoge: Int = 10//Int型(nilを許容しない) var piyo: Int! = nil//オプショナルInt型(nilを許容する) print(piyo)//nil print(hoge + piyo)//エラー print(piyo == 10)//エラー名前の後ろの"?"と"!"
Swiftのコードに出てくる"?"と"!"のうち,変数やメソッドなどの名前の後ろについてくる"?"と"!"は前述のアンラップに関わる操作を行うための記号です.名前の後ろに"?"や"!"をつける処理としてForced Unwrapping (強制的アンラップ)とOptional Chaining (オプショナルチェイニング)をここではまとめます.
<オプショナル型変数>! ← Forced Unwrapping (強制的アンラップ) <クラス>.<オプショナル型のプロパティ>?.<プロパティ他> ← Optional Chaining (オプショナルチェイニング)Forced Unwrapping (強制的アンラップ)
Forced Unwrapping (強制的アンラップ)とはオプショナル型変数の中にどんな値が入っていてもアンラップをするという方法です.以下の記法でオプショナル型変数を非オプショナル型変数に変換します.
<オプショナル型変数>!例
var fuga: Int? = 10//オプショナルInt型(nilを許容する) print(fuga)//Optional(10) print(fuga!)//10しかし,fugaにnilが入っている場合にもアンラップを強行しようとするため,その場合にはエラーになります.
var fuga: Int? = nil//オプショナルInt型(nilを許容する) print(fuga!)//エラーしたがって,Forced Unwrappingによるアンラップを行う場合には,その変数に値が必ず格納されていることが保証されていることを確認して,行う必要があります.(公式ドキュメントにもこの"!"について以下のように記述がありました)
The exclamation mark effectively says, “I know that this optional definitely has a value; please use it.”
Optional Chaining (オプショナルチェイニング)
Optional Chaining (オプショナルチェイニング)とはクラスが持つオプショナル型のプロパティやメソッドを安全に呼び出す方法です.以下の例(公式ドキュメントを引用)をみてください.
class Person { var residence: Residence? } class Residence { var numberOfRooms = 1 }このようにPersonクラスを定義した上でPersonクラスのインスタンスjohnのresidence.numberOfRoomsを取得することを考えます.以下の手順でnumberOfRoomsを取得しようとすると,エラーが発生します.
let john = Person()//この時点で特段イニシャライザの処理をしていないのでjohn.residenceはnil let roomCount = john.residence!.numberOfRooms//john.residence!のアンラップ失敗でnumberOfRoomsが取得できずエラー print(roomCount)//エラー上のケースではオプショナル型のオブジェクトがnilを含む可能性のある状況下でForced Unwrappingをしたためにエラーが発生してしまいました.このような場面において,Optional Chainingを活用すれば,より安全にresidence.numberOfRoomsを取得することができます.
Optional Chainingでは以下の記法でプロパティやメソッドを参照します.
<クラス>.<オプショナル型のプロパティ/メソッド>?.<プロパティ/メソッド>...Optional Chainingは,Forced Unwrappingとは異なり,この<オプショナル型のプロパティ/メソッド>がnilだった時点で,それ以降のプロパティやメソッドを参照せずにnilを返します.
let john = Person() if john.residence?.numberOfRooms != nil { let roomCount = john.residence?.numberOfRooms ?? 0 print(roomCount) } else { print("住所なし")//こちらが出力される }したがって,強制的なアンラップを行わずに安全にオプショナル型のオブジェクトを扱うことができます.上の例でも存在しないかもしれないjohn.residence.numberOfRoomsを直でアクセスせずに,residenceがnilかそうでないかによって後続の処理を変えることができます.
さらに,前述したようなオプショナル型の変数がnilであるかそうでないかによって後続の処理を分岐させるためのより工夫された仕組みがあります.次回はそれらを中心についてまとめていこうと思います.(長くなってしまったので)
次回予告
安全なアンラップ
- if let
- guard let
asの後ろの"?"と"!"
まとめ
参考
- 投稿日:2019-11-29T13:11:20+09:00
【Swift】画面遷移時の値・処理の受け渡し方法(俯瞰用)
先日投稿した画面遷移で書ききれなかったところについて、続きを書き加えます。
先日の記事と同様に備忘録の意味合いが強めです。先日の記事はこちら:【Swift】画面遷移の方法(俯瞰用)
動作環境
- macOS : Mojave 10.14.6
- Swift : 5.0.1
- iOS : 13.1.3
- Xcode : 11.0
前回のポイント - 代表的な画面遷移の方法
画面遷移の実装方法は主に以下の三つ。
A. InterfaceBuilder(ボタンと画面をSegueで接続)
B. InterfaceBuilder(Segue Identifier) + コーディング
C. InterfaceBuilder(Storyboard ID) + コーディング今回書く内容
- 遷移先画面への値渡しの方法
- 遷移先の結果を遷移元に反映させる方法
遷移先画面への値渡しの方法
基本的な考え方
基本的に遷移先への値の渡し方は以下の手順
(A_ViewController -> B_ViewController に遷移することを考えます)遷移先のB_ViewControllerの実装
1. 遷移先に渡したい値を格納する変数を用意する遷移元のA_ViewControllerの実装
2. 遷移先のViewControllerを取得
3. 1で用意した遷移先の変数に値を渡す
4. 画面遷移実行上記はSegueにIDをつけてperformSegueメソッドを使用する場合でも、StoryboardIDをつけpresentメソッドを呼び出すような場合でも同様です。
Bパターン【InterfaceBuilder(Segue Identifier) + コーディング】での実装
画像のように遷移元A_ViewControllerでテキストフィールドに値を入力して、
遷移先のB_ViewControllerで表示するような場合を考えます。まず画像右上の赤枠部分のStoryboard Segue -> IdentifierにsegueのIDを付与します。
今回はtoBViewControllerとしました。遷移先のB_ViewControllerの実装
B_ViewControllerimport UIKit class B_ViewController: UIViewController { @IBOutlet weak var outputLabel: UILabel! // 1. 遷移先に渡したい値を格納する変数を用意する var outputValue : String? override func viewDidLoad() { super.viewDidLoad() outputLabel.text = outputValue } }遷移元のA_ViewControllerの実装
A_ViewControllerimport UIKit class A_ViewController: UIViewController { @IBOutlet weak var inputField: UITextField! override func viewDidLoad() { super.viewDidLoad() } // segueが動作することをViewControllerに通知するメソッド override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // segueのIDを確認して特定のsegueのときのみ動作させる if segue.identifier == "toBViewController" { // 2. 遷移先のViewControllerを取得 let next = segue.destination as? B_ViewController // 3. 1で用意した遷移先の変数に値を渡す next?.outputValue = self.inputField.text } } @IBAction func tapTransitionButton(_ sender: Any) { // 4. 画面遷移実行 performSegue(withIdentifier: "toBViewController", sender: nil) } }遷移元のA_ViewControllerではsegueのidentifierをみて、処理を行うか決めています。
prepareメソッドはsegueが動作するたびに呼ばれるので、
(今回は遷移先は一つなので必要ありませんが)遷移先が増えた時に適切な処理をするために必要です。C. InterfaceBuilder(Storyboard ID) + コーディング
このパターンでは、segueはつながずviewControllerにIDを付与することで遷移するパターンですが、
基本パターンの1-4の手順は同一です。まず画像のように遷移先であるB_ViewControllerにIDを付与します。
遷移先のB_ViewControllerの実装
(Bパターンと同一のため割愛)
遷移元のA_ViewControllerの実装
A_ViewControllerimport UIKit class A_ViewController: UIViewController { @IBOutlet weak var inputField: UITextField! override func viewDidLoad() { super.viewDidLoad() } @IBAction func tapTransitionButton(_ sender: Any) { let storyboard = self.storyboard! // 2. 遷移先のViewControllerを取得 let next = storyboard.instantiateViewController(withIdentifier: "B_ViewController") as! B_ViewController // 3. 1で用意した遷移先の変数に値を渡す next.outputValue = self.inputField.text // 4. 画面遷移実行 self.present(next, animated: true) } }結果
遷移先に入力したテキスト「test」が反映されています。
遷移先の結果を遷移元に反映させる方法
今度は遷移先の処理結果を遷移元に戻った時に反映させる処理をします。
delegateパターンやAppDelegateパターンなどがあるとのことですが、
今回は「prepareメソッドでクロージャで処理ごと渡す」パターンで実装をしてみます。画面イメージ
①ボタンをタップして遷移先へ
②値を入力後、ボタンをタップして遷移元へ戻る
③ラベルの値が変わっているはず実装手順
上記画面のように遷移のパターンとしてはBパターンのsegueにIDを付与し、prepareメソッド呼び出し時に値を渡す処理とします。
この時、遷移先に「値」を渡すのではなく、「遷移先の値を使って遷移元の値を更新してね」という処理ごとクロージャで渡します。遷移元のA_ViewControllerの実装
A_ViewControllerimport UIKit class A_ViewController: UIViewController { @IBOutlet weak var outputLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() } @IBAction func tapTrasitionButton(_ sender: Any) { // segueを使って画面遷移 performSegue(withIdentifier: "toBViewController", sender: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // segueのIDを確認して特定のsegueのときのみ動作させる if segue.identifier == "toBViewController" { // 遷移先のViewControllerを取得 let next = segue.destination as? B_ViewController // 遷移先のプロパティに処理ごと渡す next?.resultHandler = { text in // 引数を使ってoutputLabelの値を更新する処理 self.outputLabel.text = text } } } }遷移先のB_ViewControllerの実装
B_ViewControllerclass B_ViewController: UIViewController { @IBOutlet weak var inputField: UITextField! // 遷移元から処理を受け取るクロージャのプロパティを用意 var resultHandler: ((String) -> Void)? override func viewDidLoad() { super.viewDidLoad() } @IBAction func tapBackButton(_ sender: Any) { // nilチェック guard let text = self.inputField.text else { return } // 用意したクロージャに関数がセットされているか確認する if let handler = self.resultHandler { // 入力値を引数として渡された処理の実行 handler(text) } // 遷移元へ戻る self.dismiss(animated: true, completion: nil) } }通常、遷移先の画面から、遷移元の画面の要素にアクセスしたい時はdelegateパターンを使う場合が多いようですが、簡単な処理ならこちらで書くのも良いかなと思いました。
結果
元々のテキスト「Label」から遷移先で入力したテキスト「test」が反映されています。
(少し分かりづらいですが)まとめ
遷移先画面への値渡しの方法する場合の考え方は以下
- 遷移先に渡したい値を格納する変数を用意する
- 遷移先のViewControllerを取得
- 1で用意した遷移先の変数に値を渡す
- 画面遷移実行
遷移先の結果を遷移元に反映させる方法
- クロージャで処理ごと反映する処理ごと渡す
- 今回の方法以外にDelegateパターンや、AppDelegateを経由するパターンもあり
参考
- 投稿日:2019-11-29T11:52:11+09:00
[iOS] アプリが終了している状態でもヘルスケアデータの更新を検知できる
はじめに
iOSでは、iOSや各アプリから"ヘルスケア"にいろいろなヘルスケアデータが書き込まれます。
iOSアプリはヘルスケアデータをHealthkit経由で読み書きが可能で、またデータの更新を監視することができます。そして更新の監視はアプリが終了している状態でも有効にすることができる、というのがこの記事の主旨です。
アプリの状態 ヘルスケアデータの更新検知 フォアグラウンドで実行されている 可能 実行されているがフォアグラウンドにない 可能 アプリが終了している 可能 ←ここ ↓のサンプルでは、アプリが終了している状態でヘルスケアへのデータ書き込みを検知して、アプリからローカル通知を送信してます。
※最初左右にスワイプしてるのはヘルスケア以外にアプリ起動してないアピールです
サンプル 実装方法
- Healthkitで任意の種類のヘルスケアデータに対して
HKObserverQuery
を実行するenableBackgroundDelivery(for:frequency:withCompletion:)
を実行するlet objectType = HKSampleType.quantityType(forIdentifier: .bodyMass)! let query = HKObserverQuery(sampleType: objectType, predicate: nil, updateHandler: { query, completionHandler, error in // 更新検知 }) HKHealthStore().execute(query) // バックグランドでのヘルスケアデータの更新検知を有効にする HKHealthStore().enableBackgroundDelivery(for: objectType, frequency: .immediate, withCompletion: { success, error in })
- 検知できる更新タイミングは
enableBackgroundDelivery(for:frequency:withCompletion:)
の引数として渡す HKUpdateFrequency で指定した期間に最大1回
- 指定できる期間は
immediate
hourly
daily
weekly
、ただし特定のヘルスケアデータ(歩数など更新頻度が非常に高いものなど)はhourly
以上の頻度でしか検知できない詳しくは公式ドキュメント参照。
サンプル
以下、体重データの更新を検知してローカル通知を送るサンプルです。
サンプルの動作環境:
Xcode11.2.1
iOS13.2
プロジェクト設定
- ヘルスケアからデータの読み出しを許可するため、Info.plistに
Privacy - Health Share Usage Description
をセットする- project -> target -> Signing&Capabilities で以下を設定する
サンプルコード
AppDelegate.swiftimport UIKit import UserNotifications import HealthKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // プッシュ通知を有効にする UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge], completionHandler: { _, _ in DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } }) // ヘルスケアから体重データの読み込みを許可 let objectType = HKSampleType.quantityType(forIdentifier: .bodyMass)! HKHealthStore().requestAuthorization(toShare: nil, read: Set([objectType]), completion: { success, error in }) // ヘルスケアデータの更新を検知するクエリ let query = HKObserverQuery(sampleType: objectType, predicate: nil, updateHandler: { query, completionHandler, error in if error != nil { return } // ヘルスケアデータの更新を検知したらローカル通知を送る let content = UNMutableNotificationContent() content.body = "体重データが更新されました" let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) let request = UNNotificationRequest(identifier: "detection-test", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) completionHandler() }) HKHealthStore().execute(query) // バックグランドでのヘルスケアデータの更新検知を有効にする HKHealthStore().enableBackgroundDelivery(for: objectType, frequency: .immediate, withCompletion: { success, error in }) return true } }参考URL
- 投稿日:2019-11-29T11:27:41+09:00
MPMediaLibraryへアクセス許可を促す(不許可されても)
オークファンAdvent Calendar 12日です。
弊社に19新卒で入社して半年強が経ちました。
業務でPHPを使っており、プライベートでモバイルアプリ開発しています。
自作のアプリは審査の時でリジェクトされた問題点を書きます。環境
Xcode 11.2
Swift 4.2
IOS 13.2問題点
Info.plistで Privacy- Media Library Usage Descriptionを設定すればアクセス許可画面は出ますけど
その画面が一回しか出ないのでもしユーザーが許可なしを選択したら、アプリ内でボタン押しても反応がなくなってしまった。
解決策
UIAlertControllerで選択画面を作り、設定画面まで飛ばせる。
authorizationStatus 説明 .notDetermined 未選択 .denied 不許可 .authorized 許可 音楽選択ボタンを押す時
@IBAction func pickMusic(_ sender: Any) { let status = MPMediaLibrary.authorizationStatus() //MPMediaLibraryにアクセスできない、選択画面を表示 if status == .denied { self.displayPermissionViewController() } else { let picker = MPMediaPickerController(mediaTypes: MPMediaType.music) picker.delegate = self self.present(picker, animated: true, completion: nil) } }選択画面で設定しますを押す時
func displayPermissionViewController() { let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle: UIAlertController.Style.alert) alert.popoverPresentationController?.sourceView = self.view let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil) alert.addAction(cancelAction) let okAction = UIAlertAction(title: "設定します", style: .default) { _ in // セッティング画面に行く if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } alert.addAction(okAction) present(alert, animated: true, completion: nil) }全体コード
import UIKit import MediaPlayer class ViewController: UIViewController, MPMediaPickerControllerDelegate{ override func viewDidLoad() { super.viewDidLoad() } @IBAction func pickMusic(_ sender: Any) { let status = MPMediaLibrary.authorizationStatus() //MPMediaLibraryにアクセスできない、選択画面を表示 if status == .denied { self.displayPermissionViewController() } else { let picker = MPMediaPickerController(mediaTypes: MPMediaType.music) picker.delegate = self self.present(picker, animated: true, completion: nil) } } func displayPermissionViewController() { let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle: UIAlertController.Style.alert) alert.popoverPresentationController?.sourceView = self.view let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil) alert.addAction(cancelAction) let okAction = UIAlertAction(title: "設定します", style: .default) { _ in // セッティング画面に行く if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } alert.addAction(okAction) present(alert, animated: true, completion: nil) } func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) { dismiss(animated: true, completion: nil) } func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) { let musicPlayer = MPMusicPlayerController.applicationMusicPlayer musicPlayer.setQueue(with: mediaItemCollection) musicPlayer.play() dismiss(animated: true, completion: nil) } }
- 投稿日:2019-11-29T11:04:18+09:00
MPMediaLibraryへアクセス許可を促す(不許可されても)
環境
Xcode 11.2
Swift 4.2
IOS 13.2問題点
Info.plistで Privacy- Media Library Usage Descriptionを設定すればアクセス許可画面は出ますけど
その画面が一回しか出ないのでもしユーザーが許可なしを選択したら、アプリ内でボタン押しても反応がなくなってしまった。
自作のアプリは審査の時でこの問題でリジェクトされたことがあります。
解決策
UIAlertControllerで選択画面を作り、設定画面まで飛ばせる。
authorizationStatus 説明 .notDetermined 未選択 .denied 不許可 .authorized 許可 音楽選択ボタンを押す時
@IBAction func pickMusic(_ sender: Any) { let status = MPMediaLibrary.authorizationStatus() //MPMediaLibraryにアクセスできない、選択画面を表示 if status == .denied { self.displayPermissionViewController() } else { let picker = MPMediaPickerController(mediaTypes: MPMediaType.music) picker.delegate = self self.present(picker, animated: true, completion: nil) } }選択画面で設定しますを押す時
func displayPermissionViewController() { let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle: UIAlertController.Style.alert) alert.popoverPresentationController?.sourceView = self.view let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil) alert.addAction(cancelAction) let okAction = UIAlertAction(title: "設定します", style: .default) { _ in // セッティング画面に行く if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } alert.addAction(okAction) present(alert, animated: true, completion: nil) }全体コード
import UIKit import MediaPlayer class ViewController: UIViewController, MPMediaPickerControllerDelegate{ override func viewDidLoad() { super.viewDidLoad() } @IBAction func pickMusic(_ sender: Any) { let status = MPMediaLibrary.authorizationStatus() //MPMediaLibraryにアクセスできない、選択画面を表示 if status == .denied { self.displayPermissionViewController() } else { let picker = MPMediaPickerController(mediaTypes: MPMediaType.music) picker.delegate = self self.present(picker, animated: true, completion: nil) } } func displayPermissionViewController() { let alert = UIAlertController(title: "メディアライブラリのアクセス許可を設定しますか?", message: "音楽選択するため", preferredStyle: UIAlertController.Style.alert) alert.popoverPresentationController?.sourceView = self.view let cancelAction = UIAlertAction(title: "キャンセル", style: .default, handler: nil) alert.addAction(cancelAction) let okAction = UIAlertAction(title: "設定します", style: .default) { _ in // セッティング画面に行く if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url, options: [:], completionHandler: nil) } } alert.addAction(okAction) present(alert, animated: true, completion: nil) } func mediaPickerDidCancel(_ mediaPicker: MPMediaPickerController) { dismiss(animated: true, completion: nil) } func mediaPicker(_ mediaPicker: MPMediaPickerController, didPickMediaItems mediaItemCollection: MPMediaItemCollection) { let musicPlayer = MPMusicPlayerController.applicationMusicPlayer musicPlayer.setQueue(with: mediaItemCollection) musicPlayer.play() dismiss(animated: true, completion: nil) } }
- 投稿日:2019-11-29T00:29:16+09:00
iOS13のModalにつまずいた
はじめに
iOS13からはUIViewControllerのmodalPresentationStyleのデフォルト値がfullScreenからautomaticに変更されました。
これによってModal表示した際に下記のようになります。
今まではModalで表示する場合は閉じるボタンを置くなどして画面を閉じるために何か用意しないといけませんでしたが、iOS13からは下にスワイプするだけで閉じることができます。(やったね!!)
しかし、すべてのModal画面をこの表示にすると割と困ります...
対応
私が担当していたアプリでは
ログイン画面->メニュー画面->各種メニュー
のような遷移をしていたのですが、この遷移がすべてModalだったためログイン後ずっと画面が少し下がった状態で表示されてしまいました。(そもそも遷移がおかしい気がしますが...)
本来なら画面ごとにfullScreenとautomaticを切り替えて対応したかったのですが、fullScreenからautomaticを表示するとレイアウトが崩れたりと対応に苦労したので、今回はModal表示はすべてfullScreenにすることにしました。
対応としては下記になります。
- SegueでModal遷移している部分でfullScreen設定
- コードでModal遷移している部分でfullScreen設定
Segue設定
下記のようにStoryboardのSegueをfullScreenに設定していきます。
画面数が少ないと簡単なのですが、画面が30以上あったり複数のStoryboardがあると見落としも増えてきます。
segueがModalに設定されているとStoryboardに下記のような記述があるのですが
<segue destination="T3g-Ug-Mo4" kind="presentation" identifier="s1" id="aZj-Sv-0pL"/>下記のようにプロジェクト内を検索してもStoryboard内のものはひっかかってくれません
色々考えて私はSublime Textでgrepすることにしました。
こんな感じで検索結果を表示してくれます。
/プロジェクトパス/Base.lproj/Main.storyboard: 31 <connections> 32 <action selector="send2:" destination="BYZ-38-t0r" eventType="touchUpInside" id="WP3-or-zu5"/> 33: <segue destination="T3g-Ug-Mo4" kind="presentation" identifier="s2" modalPresentationStyle="fullScreen" id="lmW-TY-aBi"/> 34 </connections> 35 </button> .. 53 <connections> 54 <outlet property="searchbar" destination="iDr-NE-pOY" id="L8F-EI-0u9"/> 55: <segue destination="T3g-Ug-Mo4" kind="presentation" identifier="s1" id="aZj-Sv-0pL"/> 56 </connections> 57 </viewController> 3 matches in 1 file原始的な気がしますが、これで地道に潰していきました。
コードで設定
プロジェクト内を
present(
で検索し地道に下記のように設定していきました。let vc = (storyboard?.instantiateViewController(withIdentifier: "second"))! vc.modalPresentationStyle = .fullScreen // ここ追加 present(vc, animated: true)これで完了!!...と思いきやまだあります。
func show(_ vc: UIViewController, sender: Any?)show(_:sender:)こんなんあったのか...(恥ずかしながら今回初めて知りました)
プロジェクト内を
show(
で検索し地道にModal遷移しているところを調査し下記のように設定していきました。let vc = (storyboard?.instantiateViewController(withIdentifier: "second"))! vc.modalPresentationStyle = .fullScreen // ここ追加 show(vc, sender: nil)その他Automatic表示の場合の処理
画面を下スワイプで閉じないようにしたい!とか画面を閉じる前にアラートを出したい!とかあると思いますがその辺もちゃんと制御できるようです。
iOS13からのUIModalPresentationStyle.pageSheetでユーザにプルダウンで閉じられないようにする方法
さいごに
今回はすべてのModalをfullScreenにすることで対応しましたが、後々はautomaticで対応していきたい...
大抵の場合は、そもそもModalで表示してることがおかしいんじゃないかなとも思いました。(下記参考)
参考