20191129のSwiftに関する記事は12件です。

[はじめてのiOSアプリ]xcodeで地図アプリを作成(その5)

はじめに

iOSアプリを作ってみたいけど
何から始めて良いのかわからない

とりあえず、
「やってみました」記事を参考に
地図アプリを真似てみようと思う

という記事の5回目です。

今回は、アプリケーションアイコンを設定します。

アイコン画像作成

  1. センスのある人はカッコよく、センスのない人はそれなりの画像を作成
    • はい「それなりの画像」ができました(png形式)
      shinobee-sky-276px.png
    • 【なぜ?】
      • Simulatorに表示されるアプリケーションアイコンが、あまりにもカッコ悪いから変えたい(笑)
        SimulatorIPhone.png
  2. いろいろなサイズを用意
    • Xcode のファイルツリーから [Assets.xcassets]-[AppIcon]を選択して表示される画面に書かれているサイズの画像を用意する
      ApplicationIcon.png
    • 作成した
      IconFiles.png
    • 【なぜ?】
      • Xcodeの[AppIcon]画面には、いくつものサイズのアイコンを配置することができるから
      • 全部必要なのかわからないけど、できるだけ作成
      • ひとつ作って、プレビューアプリのツール機能を使えば簡単にサイズ違いを作成できるよ
      • サイズがわかりやすいようなファイル名を使用
      • なお、最大サイズ 1024x1024 は用意しなかった
  3. ファイルをxcodeの画面にドラッグ&ドロップ
    ApplicationIconSet.png
    • 【なぜ?】
      • これでxcodeに登録される
      • ファイル名からサイズがわかるようにしたのは、我ながらGoodでした
      • なお、サイズ間違いだと警告表示される
  4. テスト実行
    • Xcode 左上の矢印アイコンをクリック
    • アプリを終了して、アプリアイコンを確認
      SimulatorIPhoneChanged.png

今回の到達点

  • アプリケーションアイコンが「それなり」になった

連載

  1. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その1)
  2. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その2)
  3. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その3)
  4. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その4)
  5. [はじめてのiOSアプリ]xcodeで地図アプリを作成(その5)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIViewをコードで書くかxibで書くかの話(状況別)[Swift]

Storyboardをどこまでどう使うかはクックパッドさんの1Storyboard = 1VCの記事などでも話に上がってますが
もっと細かいところで

UIViewを設置する際
こんな感じでコードで書くか

view.addSubview(hogeView)

だったりxibファイル作って
StoryboardでView設置して紐付けるか

どういう基準で選ぶの?と思っていたのですが状況別でそれとなく理解したのでまとめておきます

Viewが中身しか変化しない場合

xibで作って設置
->中身の変化だけVCでかく

GirlsView.swift
import 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.swift
import 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とは関係ないけど合わせてやっとくと良さそうなので、、、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 = self

Google Sign inがバージョン5.0.0になったことで色々仕様が変わったようですね。
以下引用
公式ドキュメント
https://developers.google.com/identity/sign-in/ios/release
github
https://github.com/EddyVerbruggen/nativescript-plugin-firebase/issues/1370

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift]StackViewとhiddenを使って画面を作成したこと

概要

StackViewを作ってその中のLabelをhiddenでトルツメをするという作業を行ったのでメモしたいと思います。

やりたいこと

StackView、トルツメを使った名前入力画面の作成

↓適当に必要な要素を配置した画面を作成。
image.png

それぞれの要素ごとにやりたいこと

①画面のタイトル(概要)ラベル
・名前入力画面の概要として表示位置は固定で置きたい

②名前を入力するTextField
・ここで名前を入力させる
・名前入力に不備があった場合はエラー(④⑤)を表示させたい

③次の画面に遷移するボタン
・名前入力に不備があった場合は遷移させずにエラーを出したい
・名前入力に不備がない場合は次の画面に遷移させたい

④⑤名前入力で不備があった場合に出現するエラーラベル
・通常時はエラーラベルは隠しておきたい
・エラーラベル非表示は②TextFieldは①タイトルラベルの下におきたい
・③ボタンを押した時に不備があった場合、②の上にラベルを出現させたい

今回StackViewで実現させたい動作

・名前入力に不備があった場合はエラー(④⑤)を表示させたい
・通常時はエラーラベルは隠しておきたい
・エラーラベル非表示は②TextFieldは①タイトルラベルの下におきたい
・③ボタンを押した時に不備があった場合、②の上にラベルを出現させたい

上記をStackViewで実現させていきます。

Storyboard上の手順
1.二つのエラーラベルを選択状態にする(commandを押しながらクリック)
image.png

2.Embed Inをクリック
image.png

3.StackViewをクリック
image.png

↓クリックすると、選択状態にしていた要素がまとまる
image.png

4.StackViewとTextFieldを選択状態にする
image.png

5.2~3と同様の手順を再度行う(Embed In → StackView)

↓StackViewとTextFieldが更にStackViewでまとまる
image.png

6.一番上のStackViewに制約をつける
image.png

image.png

7.StackView内の要素毎のスペースを決める
image.png

ここまで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がトルツメされている!
image.png

↓ボタンをタップするとerrorStackViewが出現!

image.png

まとめ

今回は

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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]

こんな感じになるわけですね。
「,」区切りのときも継承クラスとの区別がわかりづらいな〜と思っていましたが、
「&」区切りだと完全に型とプロトコルが同列に並んでるように見えて、僕にはわかりづらく感じます。
慣れなんですかね?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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の後ろの"?"と"!"

  • まとめ

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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としました。

image.png

遷移先のB_ViewControllerの実装

B_ViewController
import 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_ViewController
import 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を付与します。

image

遷移先のB_ViewControllerの実装

(Bパターンと同一のため割愛)

遷移元のA_ViewControllerの実装

A_ViewController
import 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」が反映されています。

practiceSegue

遷移先の結果を遷移元に反映させる方法

今度は遷移先の処理結果を遷移元に戻った時に反映させる処理をします。
delegateパターンやAppDelegateパターンなどがあるとのことですが、
今回は「prepareメソッドでクロージャで処理ごと渡す」パターンで実装をしてみます。

画面イメージ

①ボタンをタップして遷移先へ
②値を入力後、ボタンをタップして遷移元へ戻る
③ラベルの値が変わっているはず

image.png

実装手順

上記画面のように遷移のパターンとしてはBパターンのsegueにIDを付与し、prepareメソッド呼び出し時に値を渡す処理とします。
この時、遷移先に「値」を渡すのではなく、「遷移先の値を使って遷移元の値を更新してね」という処理ごとクロージャで渡します。

遷移元のA_ViewControllerの実装

A_ViewController
import 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_ViewController
class 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」が反映されています。
(少し分かりづらいですが)

PracticeSegue2.gif

まとめ

  • 遷移先画面への値渡しの方法する場合の考え方は以下

    1. 遷移先に渡したい値を格納する変数を用意する
    2. 遷移先のViewControllerを取得
    3. 1で用意した遷移先の変数に値を渡す
    4. 画面遷移実行
  • 遷移先の結果を遷移元に反映させる方法

    • クロージャで処理ごと反映する処理ごと渡す
    • 今回の方法以外にDelegateパターンや、AppDelegateを経由するパターンもあり

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS] アプリが終了している状態でもヘルスケアデータの更新を検知できる

はじめに

iOSでは、iOSや各アプリから"ヘルスケア"にいろいろなヘルスケアデータが書き込まれます。
iOSアプリはヘルスケアデータをHealthkit経由で読み書きが可能で、またデータの更新を監視することができます。

そして更新の監視はアプリが終了している状態でも有効にすることができる、というのがこの記事の主旨です。

アプリの状態 ヘルスケアデータの更新検知
フォアグラウンドで実行されている 可能
実行されているがフォアグラウンドにない 可能
アプリが終了している 可能 ←ここ

↓のサンプルでは、アプリが終了している状態でヘルスケアへのデータ書き込みを検知して、アプリからローカル通知を送信してます。
※最初左右にスワイプしてるのはヘルスケア以外にアプリ起動してないアピールです

サンプル
sample.gif

実装方法

  1. Healthkitで任意の種類のヘルスケアデータに対して HKObserverQuery を実行する
  2. 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

プロジェクト設定

  1. ヘルスケアからデータの読み出しを許可するため、Info.plistに Privacy - Health Share Usage Description をセットする
  2. project -> target -> Signing&Capabilities で以下を設定する
    • HealthKit を有効にする
    • PushNotifications を有効にする(このサンプルでローカル通知を送るため) スクリーンショット 2019-12-05 0.23.03.png

サンプルコード

AppDelegate.swift
import 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MPMediaLibraryへアクセス許可を促す(不許可されても)

オークファンAdvent Calendar 12日です。

弊社に19新卒で入社して半年強が経ちました。
業務でPHPを使っており、プライベートでモバイルアプリ開発しています。
自作のアプリは審査の時でリジェクトされた問題点を書きます。

環境

Xcode 11.2
Swift 4.2
IOS 13.2

問題点

Info.plistで Privacy- Media Library Usage Descriptionを設定すればアクセス許可画面は出ますけど
その画面が一回しか出ないのでもしユーザーが許可なしを選択したら、アプリ内でボタン押しても反応がなくなってしまった。
IMG_0008.PNG

解決策

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)
        }
    }

IMG_0009.PNG

選択画面で設定しますを押す時

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)
    }

IMG_0010.PNG

全体コード

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)
    }
}


作った目覚ましアプリはこちらです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MPMediaLibraryへアクセス許可を促す(不許可されても)

環境

Xcode 11.2
Swift 4.2
IOS 13.2

問題点

Info.plistで Privacy- Media Library Usage Descriptionを設定すればアクセス許可画面は出ますけど
その画面が一回しか出ないのでもしユーザーが許可なしを選択したら、アプリ内でボタン押しても反応がなくなってしまった。
自作のアプリは審査の時でこの問題でリジェクトされたことがあります。
IMG_0008.PNG

解決策

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)
        }
    }

IMG_0009.PNG

選択画面で設定しますを押す時

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)
    }

IMG_0010.PNG

全体コード

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)
    }
}


作った目覚ましアプリはこちらです

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS13のModalにつまずいた

はじめに

iOS13からはUIViewControllerのmodalPresentationStyleのデフォルト値がfullScreenからautomaticに変更されました。

これによってModal表示した際に下記のようになります。

modal

今まではModalで表示する場合は閉じるボタンを置くなどして画面を閉じるために何か用意しないといけませんでしたが、iOS13からは下にスワイプするだけで閉じることができます。(やったね!!)

しかし、すべてのModal画面をこの表示にすると割と困ります...

対応

私が担当していたアプリでは

ログイン画面->メニュー画面->各種メニュー

のような遷移をしていたのですが、この遷移がすべてModalだったためログイン後ずっと画面が少し下がった状態で表示されてしまいました。(そもそも遷移がおかしい気がしますが...)

本来なら画面ごとにfullScreenとautomaticを切り替えて対応したかったのですが、fullScreenからautomaticを表示するとレイアウトが崩れたりと対応に苦労したので、今回はModal表示はすべてfullScreenにすることにしました。

対応としては下記になります。

  • SegueでModal遷移している部分でfullScreen設定
  • コードでModal遷移している部分でfullScreen設定

Segue設定

下記のようにStoryboardのSegueをfullScreenに設定していきます。

segue

画面数が少ないと簡単なのですが、画面が30以上あったり複数のStoryboardがあると見落としも増えてきます。

segueがModalに設定されているとStoryboardに下記のような記述があるのですが

<segue destination="T3g-Ug-Mo4" kind="presentation" identifier="s1" id="aZj-Sv-0pL"/>

下記のようにプロジェクト内を検索してもStoryboard内のものはひっかかってくれません:disappointed_relieved:

search

色々考えて私は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:)こんなんあったのか...(恥ずかしながら今回初めて知りました:flushed:)

プロジェクト内をshow(で検索し地道にModal遷移しているところを調査し下記のように設定していきました。

let vc = (storyboard?.instantiateViewController(withIdentifier: "second"))!
vc.modalPresentationStyle = .fullScreen // ここ追加
show(vc, sender: nil)

その他Automatic表示の場合の処理

画面を下スワイプで閉じないようにしたい!とか画面を閉じる前にアラートを出したい!とかあると思いますがその辺もちゃんと制御できるようです。

iOS13からのUIModalPresentationStyle.pageSheetでユーザにプルダウンで閉じられないようにする方法

さいごに

今回はすべてのModalをfullScreenにすることで対応しましたが、後々はautomaticで対応していきたい...

大抵の場合は、そもそもModalで表示してることがおかしいんじゃないかなとも思いました。(下記参考)

iOS ヒューマンインターフェースの原則

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む