- 投稿日:2020-08-17T23:52:39+09:00
UIFeedbackGenerator を使おう
概要
UIFeedbackGenerator は UI にフィードバックを追加するための、フィードバックジェネレーターの抽象クラスで、フィードバックの種類別に下記の3つの具象クラスが存在し、それぞれ iOS10 以降で使用することが可能です。
基本的には、UIFeedbackGenerator を自身でインスタンス化することは禁止されていて、上記のサブラクスをインスタンス化してフィードバックをトリガーします。また、システムの設定やアプリケーションの状態、バッテリー残量など特定の要因によって Haptics が再生されないことがあります。下記に例を載せておきます。
- デバイスが Taptic Engine を搭載していない場合
- アプリがバックグラウンド状態の場合
- システムの Haptics 設定が無効な場合
Feedback の種類
UIImpactFeedbackGenerator
最も基本的なフィードバックで、ユーザがボタンをタップしたりした時などに使用されます。強度別にスタイルが用意されており、iOS13 からはトリガーのタイミングで強度(itensity)を指定できるようになりました。詳しくはこちらをご覧ください。
public enum FeedbackStyle : Int { case light case medium case heavy @available(iOS 13.0, *) case soft @available(iOS 13.0, *) case rigid }UISelectionFeedbackGenerator
スライダーでの値の変更など、連続したフィードバックをしたい時に使用します。
UINotificationFeedbackGenerator
イベントの結果などによって、成功・警告・失敗など種類別にフィードバックを行いたい時に使用します。詳しくはこちらを参照してください。
public enum FeedbackType : Int { case success case warning case error }実際に使ってみる
3種類のフィードバックタイプをそれぞれ個別にインスタンス化して使用するのもいいですが、今回は全てのフィードバックを試してみたかったので下記のようなクラスを作成しました。
import UIKit enum FeedbackGeneratorType { case impact(style: UIImpactFeedbackGenerator.FeedbackStyle) case notification case selection @available(iOS 13.0, *) case impactWithIntensity(intensity: CGFloat) } final class FeedbackGenerator { private var feedbackGenerator: UIFeedbackGenerator? private let type: FeedbackGeneratorType init(type: FeedbackGeneratorType) { self.type = type } // Please call this method a few seconds before triggering the feedback. func prepare() { switch type { case .impact(let style): feedbackGenerator = UIImpactFeedbackGenerator(style: style) case .notification: feedbackGenerator = UINotificationFeedbackGenerator() case .selection: feedbackGenerator = UISelectionFeedbackGenerator() case .impactWithIntensity: feedbackGenerator = UIImpactFeedbackGenerator() } feedbackGenerator?.prepare() } func releaseFeedbackEngine() { feedbackGenerator = nil } // MARK: - Excute Haptics methods. func excuteImpactFeedback(intensity: CGFloat? = nil) { let optionalIntensity = intensity guard let impactFeedbackGenerator = feedbackGenerator as? UIImpactFeedbackGenerator else { return } if case .impactWithIntensity(let intensity) = type, #available(iOS 13.0, *) { if let specificIntensity = optionalIntensity { impactFeedbackGenerator.impactOccurred(intensity: specificIntensity) } else { impactFeedbackGenerator.impactOccurred(intensity: intensity) } } else { impactFeedbackGenerator.impactOccurred() } releaseFeedbackEngine() } func excuteNotificationFeedback(notificationType: UINotificationFeedbackGenerator.FeedbackType) { guard let notificationFeedbackGenerator = feedbackGenerator as? UINotificationFeedbackGenerator else { return } notificationFeedbackGenerator.notificationOccurred(notificationType) releaseFeedbackEngine() } func excuteSelectionFeedback() { guard let selectionFeedbackGenerator = feedbackGenerator as? UISelectionFeedbackGenerator else { return } selectionFeedbackGenerator.selectionChanged() feedbackGenerator?.prepare() } }注目すべき点は2つあり、prepare() の呼び出しと、feedbackGenerator インスタンスの解放タイミングです。prepare() は Taptic Engine と呼ばれるフィードバックを再生するための振動モーターを準備中にするために呼び出します。これにより、フィードバックを再生する際にレイテンシをなくすことができます。2つ目の feedbackGenerator のインスタンス解放タイミングは、UIFeedbackGenerator がインスタンス化されると Taptic Engine が待機状態になり電力を消費するため、なるべくフィードバック毎にインスタンス を解放することが重要になってきます。また、UISelectionFeedbackGenerator に関しては連続してフィードバックを再生するのでインスタンス は解放せず、再生直後に prepare() を呼び出し、Taptic Engine を準備中にしていますので、任意のタイミングで feedbackGenerator を解放する必要があります。
使い方
UIButton の TouchDown イベントで prepare() して buttonTouchUpInside イベントでフィードバックを再生するサンプルです。
class ViewController: UIViewController { let feedbackGenerator1 = FeedbackGenerator(type: .impactWithIntensity(intensity: 10000)) @IBAction func buttonTouchDown(_ sender: Any) { feedbackGenerator1.prepare() } @IBAction func buttonTouchUpInside(_ sender: Any) { feedbackGenerator1.excuteImpactFeedback() } }
- 投稿日:2020-08-17T23:49:07+09:00
【初心者向け】「return」がイマイチ分からない2年前の自分に向けての記事(if-elseの話も少し)
この記事を読んで習得できること
・プログラミングを初めてまだ「return」の使い方が分からない方々
・3年前の私経緯
大学院でプログラムを書いていた3年前のボク。
大学の研究で実験の解析で必要だったため、必死で書いていたのをなんとなく覚えている。ひとまず、自分の行いたい解析プログラムは動くようになり、
喜びながらそのプログラムを使って解析を行っていた。ひと段落してから、プログラムを見てみると、
「結構ソース汚いな…」と思い、修正しようと試みた。しかし、ソースのスパゲッティ感*が半端なく、
なかなか修正が出来ずに、前回と同じようなソースなのに、
一から書き直したのを覚えている。3年前のソースで何が起こっていたのか
おおよそのイメージだが、こんな感じ。
sample.swiftfunc yabaiCode() { if 条件1 { 処理1(15行以上) 処理A(10行以上) 処理4(10行以上) } else if 条件2 { 処理2(15行以上) 処理A`(10行以上) 処理5(10行以上) } if 条件3 { 処理3(15行以上) 処理A``(10行以上) 処理6(10行以上) } else { 処理7(20行以上) } }何が言いたいかって、条件分岐の中の処理がとんでもなく長いのだ。
しかし、処理を分けたいのだが、分け方がイマイチ分からない。
どうやら、メソッドで分割していけば、もっと簡単に処理ができるとのことだ。でも、メソッドで分けることが出来なかった。
メソッド間での、値の受け渡しが分からなかったのだ…んで、結局分からないまま、このままのコードを残して卒業してしまったわけだが(後輩ちゃんごめん)、
今になって(流石に)return文がある程度分かった自分が、過去の自分にreturnを教えるために、この記事を書いたってわけです。returnの主な使い方は2つ
1.メソッドを終了させる
sample.swiftfunc return1() { num = 5 // 好きな数値を入れる if num == 4 { print("4です") return } else if num == 5 { print("5です") return // このメソッドはここで終了するはず // これ以降は処理されない } if num == 6 { print("6です") return } else { print("分からない") return } }このreturnは、無駄な処理を省くことができる。
途中で答えが出たら、その時点で処理を終了させればいいし、
また、returnがあるおかげで、if-else
を使わなくてよくなる。if-elseは、処理が多くなる上に、ifとelseが同時でいることが前提になるため、
1メソッドあたりの処理が多くなることがある。
読みにくくなったりもする。
職場によっては、「if-else
禁止!」なんてところもあるだろう。
(自分の部署はそうでした)どんだけif-elseがないとコードが読みやすくなるか。
FizzBuzz文を参考にしてみたいと思う。まずは、if-else文を使用したもの。
FizzBuzz.swiftfunc fizzBuzz() { for i in 1...30 { judgeFizzBuzz(num: i) } } func judgeFizzBuzz(num : Int) { if num % 15 == 0 { print("FizzBuzz") } else if num % 3 == 0 { print("Fizz") } else if num % 5 == 0 { print("Buzz") } else { print(num) } }めっちゃ悪いわけではないが、もし仮に、7の倍数の時の処理を入れるなんて時は、
気をつけないと、全ての処理がぶっ壊れてしまう。
(return使うときももちろん気をつける必要があるが)では、return文を使ってみる。
FizzBuzz.swiftfunc fizzBuzz() { for i in 1...30 { judgeFizzBuzz(num: i) } } func judgeFizzBuzz(num : Int) { if num % 15 == 0 { print("FizzBuzz") return } if num % 3 == 0 { print("Fizz") return } if num % 5 == 0 { print("Buzz") return } print(num) }if文がパーツ化されるので、実に見やすい。本当に素晴らしい。
何もなければnumがprintされるってことも一目瞭然だ。2.値を返してくれる
ここでは、数値を返してくれるメソッドを使用する
sample.swiftfunc say() { num = 5 doubleNum = doubleNum(num) // 10が返ってくる } // 2倍した数値を返してくれる関数 func doubleNum(num :Int) -> Int { return num * 2 //数値を返す }別のメソッドで計算して、処理結果を返してもらうってことも容易になる。
FizzBuzz使うとさらにお分りいただけるかもしれない。
FizzBuzz.swiftfunc fizzBuzz() { for i in 1...30 { print(judgeFizzBuzz(num: i)) } } func judgeFizzBuzz(num : Int) -> String { if num % 15 == 0 { return "FizzBuzz" } if num % 3 == 0 { return "Fizz" } if num % 5 == 0 { return "Buzz" } return String(num) }1で書いたものよりも、さらに見やすくなったと思う。
judgeFizzBuzz
がStringを返すようになったということで、全体としてのコード量も減った(print)まとめ
returnをうまく使わないとメソッドが盛り盛りになってしまうので、もし使っていない方がいたら是非使って欲しい。
これを知らないで、よく大学卒業できたな、俺…
- 投稿日:2020-08-17T19:15:59+09:00
iOS13系からデフォルトになったpageSheetのdismiss風の動きをさせる
はじめに
iOS13系からUIViewControllerをpresentで遷移させると、modalPresentationStyleの.pageSheetがデフォルトの挙動となりました。
たまに見る、覆い被さる系の、アレですね。これの気持ち良さは良きだなと思った反面、
これやと遷移先のUIViewControllerは上まで到達せず、あくまで一時的な画面としてのレイアウトとなります。今回やりたかったのは、
presentで全画面表示(iOS12系まで標準やったアレ)、かつdismissの動きはpageSheet風、
という状態を作りたかったわけで、
そうなるともう作るっきゃない!
となったので、実際にやってみましたと。成果物
こんな感じになりました。
(グラデーションががびがびなのは見ないでください。)最近、とある事故があり、iPhoneが11になったことで、UINavigationControllerによくある閉じるボタンを押すのが辛くなっていた私でも、簡単に画面を閉じることができました!
UX向上だわこれ、って思いましたね、うんうん。実装
タッチイベントを利用します。
didTouchesBegan didTouchesMoved didTouchesEndedこいつらですね。
やることは、
- 最初にタッチした位置から動いた距離分だけ画面を動かす
- タッチが終わったときに
- 閾値を超えていたらdismissする
- 閾値以内やったら画面を元の状態に戻す
って感じ。
なんとなくイメージついたかと思います。
まずは、Storyboardで画面の準備。
Viewの構成としては、
- 背景用のView (backgroundViewとする)
- コンテンツ用のView (overViewとする)
の2つです。
んで、
実際のコードは以下のようになります。final class ViewControllerForCloseableVC: UIViewController { @IBOutlet private weak var overView: UIView! @IBOutlet private weak var backgroundView: UIView! @IBOutlet private weak var overViewTopConstraint: NSLayoutConstraint! @IBOutlet private weak var overViewBottomConstraint: NSLayoutConstraint! private var position: CGPoint = .zero // 最初の触れた位置 private var isDismiss: Bool = false override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard !self.isDismiss, let touch = touches.first else { return } let position = touch.location(in: self.view) self.position = position } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard !self.isDismiss, let touch = touches.first else { return } let movedPosition = touch.location(in: self.view) // 差分計算 let diff: CGFloat = { let diff: CGFloat diff = movedPosition.y - self.position.y return diff > 0 ? diff : 0 }() // 差分の分だけoverViewをずらす self.overViewTopConstraint.constant = diff self.overViewBottomConstraint.constant = -diff self.view.layoutIfNeeded() } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { guard !self.isDismiss else { return } // dismissの挙動をさせるかどうか let needDismiss: Bool = self.overViewTopConstraint.constant > 100 // 閾値は一旦100にしておく if needDismiss { self.isDismiss = true // overViewを画面外へ self.overViewTopConstraint.constant = self.view.bounds.height self.overViewBottomConstraint.constant = -self.view.bounds.height UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: { self.view.layoutIfNeeded() }, completion: { _ in self.dismiss(animated: false, completion: nil) }) } else { // overViewを元の位置に self.overViewTopConstraint.constant = 0 self.overViewBottomConstraint.constant = 0 UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: { self.view.layoutIfNeeded() }, completion: nil) } } }なお、今回はViewControllerにUIView乗っけてるだけなのでアレですが、
実際のアプリではもっといろんなものが乗っているかと思います。
ときにはタッチイベントがそっちにとられるなんてこともあるだろうなので、そういう場合は例えば一番上に乗っかってるViewを以下のようなカスタムViewにして、delegate伝いで実装するのがいいかなと思います。(様々な場合があると思うので、状況に応じて対応内容は変わるかと思います。)protocol TouchableViewDelegate: AnyObject { func didTouchesBegan(position: CGPoint) func didTouchesMoved(position: CGPoint) func didTouchesEnded() // touchesEndedに関しては位置を必要としないので、positionは渡していない } final class TouchableView: UIView { weak var delegate: TouchableViewDelegate? override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) guard let touch = touches.first else { return } let position = touch.location(in: self) self.delegate?.didTouchesBegan(position: position) } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesMoved(touches, with: event) guard let touch = touches.first else { return } let position = touch.location(in: self) self.delegate?.didTouchesMoved(position: position) } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesEnded(touches, with: event) self.delegate?.didTouchesEnded() } }で、これやとoverViewの制約が反映された後での位置になるので、差分計算のところでそれを考慮します。
let diff: CGFloat = { let diff: CGFloat diff = movedPosition.y - self.position.y + self.overViewTopConstraint.constant return diff > 0 ? diff : 0 }()さて、ここまでの内容で一応タイトル通りのdismiss風の動きにはなるわけですが、
成果物を見てもらったら、なんか上記の2点だけではなし得ない動きしてね?ってなる。
ここからはプラスアルファの部分ですが、指が動いた分だけ画面が移動する、ってだけでは少し味気なく、ちょっと非連続な遷移アニメーションになってしまうなと思い、今回は、
- 背景の透明度の調整
- コンテンツ用のViewの丸み・透明度の調整
の2点を施すことで、より気持ちよく、かつ連続的な画面遷移になるようにします。
このあたりを実装した最終形態が、以下のようになります。final class ViewControllerForCloseableVC: UIViewController { @IBOutlet private weak var overView: UIView! @IBOutlet private weak var backgroundView: UIView! @IBOutlet private weak var overViewTopConstraint: NSLayoutConstraint! @IBOutlet private weak var overViewBottomConstraint: NSLayoutConstraint! private var position: CGPoint = .zero // 最初の触れた位置 private var isDismiss: Bool = false override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard !self.isDismiss, let touch = touches.first else { return } let position = touch.location(in: self.view) self.position = position } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard !self.isDismiss, let touch = touches.first else { return } let movedPosition = touch.location(in: self.view) // 差分計算 let diff: CGFloat = { let diff: CGFloat diff = movedPosition.y - self.position.y return diff > 0 ? diff : 0 }() // 差分の分だけoverViewをずらす self.overViewTopConstraint.constant = diff self.overViewBottomConstraint.constant = -diff // 透明度や丸みの計算(Easingに関してはこちらに。https://qiita.com/haguhoms/items/abc5635e8fa95719cb12) let max = self.view.bounds.height let radius = Easing.easeIn.quad.getProgress(elapsed: TimeInterval(diff), duration: 100, startValue: 0, endValue: 20) let alpha = Easing.easeInOut.quad.getProgress(elapsed: TimeInterval(diff), duration: TimeInterval(max), startValue: 1, endValue: 0) let backgroundAlpha = Easing.easeInOut.quad.getProgress(elapsed: TimeInterval(diff), duration: TimeInterval(max), startValue: 0.8, endValue: 0) // 透明度や丸みの調整 self.overView.cornerRadius = radius self.overView.alpha = alpha self.backgroundView.alpha = backgroundAlpha self.view.layoutIfNeeded() } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { guard !self.isDismiss else { return } // dismissの挙動をさせるかどうか let needDismiss: Bool = self.overViewTopConstraint.constant > 100 if needDismiss { self.isDismiss = true // overViewを画面外へ self.overViewTopConstraint.constant = self.view.bounds.height self.overViewBottomConstraint.constant = -self.view.bounds.height UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: { self.overView.cornerRadius = 30.0 self.overView.alpha = 0.0 self.backgroundView.alpha = 0.0 self.view.layoutIfNeeded() }, completion: { _ in // もろもろアニメーションさせた後、animatedをfalseでdismissさせる self.dismiss(animated: false, completion: nil) }) } else { // overViewを元の位置に self.overViewTopConstraint.constant = 0 self.overViewBottomConstraint.constant = 0 UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: { self.overView.cornerRadius = 0.0 self.overView.alpha = 1.0 self.backgroundView.alpha = 0.8 self.view.layoutIfNeeded() }, completion: nil) } } }(Easingに関してはこちらに。https://qiita.com/haguhoms/items/abc5635e8fa95719cb12)
これで、完成です。
まとめ
タッチイベントを利用して遷移アニメーションを作ったわけですが、なんかちょっとゴリゴリ感は否めない。
まぁそんなぎこちなさも人生のスパイスだよねってことで終わりです。余談
iPhoneどんどんでかくなるし、もうもはや左上に閉じるボタンとかをつけるだけでは厳しい世界に入っていっているので、工夫が必要になってくる場面も増えてくるかと思います。その内の一つの手法としては今回の記事は有用かと思うので、どんどん使いやすいアプリ目指してがんばりやしょう!
- 投稿日:2020-08-17T17:39:58+09:00
【iOSアプリ内課金のテスト】サブスクリプション自動更新時間の進み方
サブスクリプションの課金テスト時に、毎回調べてしまうのでメモ&Tipsです。
定期購読更新の時間は早回しで進む
Sandbox環境下では、サブスクリプション更新期間の時間は早回しで進みます。
表でまとめると以下になります。
更新期間の比較
本番 Sandbox 7日 3分 1ヶ月 5分 2ヶ月 10分 3ヶ月 15分 6ヶ月 30分 1年 60分 自動更新は6回まで
- Sandbox環境では6回しか自動更新されない
例えば、1ヶ月更新のプランだと、半年で自動更新が終了します。
テスト環境での時間を計算すると
5分(1ヶ月)×6回更新=30分
で自動更新が切れるので
プラン購入から30分後に、ユーザが更新継続しなかったパターンをテストする事が出来ます。
なお、本番環境では設定アプリから「購読の停止」がいつでも行えますが
Sandbox環境ではその設定が無く、手動で停止する事が出来ません。(自動更新切れを待つしかない)
無料トライアルを設定している場合
定期購読プランに「最初の1週間は無料!」みたいなお試しオファーを設定している場合は、その分を加味して計算します。
例)1週間の初回無料トライアル付き、1ヶ月プランのケース
3分+(5分×6回更新)=33分
33分後に自動更新が終了します。
- 投稿日:2020-08-17T17:05:31+09:00
iOS Keyboard Extensionでキーボードを介さない操作を検知する
iOSの純正キーボードは超絶多機能です。ちょっと考えただけでもこんな機能がついています。
- 入力中の文字を薄い青色(ダークモードでは黄色)でハイライトする。
- 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、変換候補を変更する。
- 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、入力中の範囲から外に出られないようにする。
- 入力範囲外をタップした場合入力中の文字を確定する。
- 選択されているテキストを取得し、再変換する。
- ペースト操作が行われた場合検知し、入力中の文字を確定する。 これは高度な機能です。実際私が日本語対応のkeyboard extensionを漁った限り、純正キーボードと同じ挙動を再現できているものはほとんどありませんでした。
そこでこれらの機能をできる限り実現すべく、いろいろ努力した結果をまとめます。
入力中の文字のハイライト
いきなり残念なお知らせですが、これは今は断念するのが正解です。
この機能の実現にはsetMarkedText()
というメソッドが利用できます。が、これを実現すると他の無数の機能が死にます。
詳しくはこちらを参照してください:UITextDocumentProxyのsetMarkedTextを(まだ)使ってはいけない。 - Qiitaそれ以外
- カーソル移動検知(→カーソル移動制限など)
- ペースト検知
- 範囲外タップ検知
- 選択検知
- 選択解除検知
- カット検知
などがどうにかできました。
大体、documentContextBeforeInput
とdocumentContextAfterInput
、それにselectedText
をゴリ押しで取得していくとどうにかなりました。UIInputViewController
のtextWillChange
とtextDidChange
は、引数のtextInput
は使えませんが、少なくとも私の環境では一応呼ばれるのでこれを利用します。
documentContextBeforeInput
は日本語入力ではカーソルの左側の文字列(改行の後まで)、documentContextAfterInput
は右側の文字列(改行の前まで)を取得します。選択部分が存在する場合には両端がそれぞれカーソルとみなされる挙動のようです。したがってテキスト全体はdocumentContextBeforeInput+selectedText+documentContextAfterInput
で得られます。改行周りの悪夢
改行を考慮すると、取得される文字列が悪夢のように複雑になります。考慮しないのが一番ですが、一応こういう挙動をします。
//left/center/rightとして得られる情報は以下の通り |はカーソル位置。二つある場合は選択範囲 --------------------- abc|def ->abc/nil/def --------------------- abc|def|ghi ->abc/def/ghi --------------------- abc| ->abc/nil/nil --------------------- abc| ->abc/nil/nil --------------------- abc| ->abc/nil/empty def --------------------- abc |def ->\n /nil/def --------------------- a|bc d|ef ->a/bc \n d/ef ---------------------実装
まず、
ViewController
側でなんらかの変化が起こる前に現在の状態を登録、起こった後に変化後の状態を登録します。KeyboardViewController.swiftclass KeyboardViewController: UIInputViewController { override func textWillChange(_ textInput: UITextInput?) { // The app is about to change the document's contents. Perform any preparation here. super.textWillChange(textInput) let left = self.textDocumentProxy.documentContextBeforeInput ?? "" let center = self.textDocumentProxy.selectedText ?? "" let right = self.textDocumentProxy.documentContextAfterInput ?? "" registerSomethingWillChange(left: left, center: center, right: right) } override func textDidChange(_ textInput: UITextInput?) { // The app has just changed the document's contents, the document context has been updated. super.textDidChange(textInput) let left = self.textDocumentProxy.documentContextBeforeInput ?? "" let center = self.textDocumentProxy.selectedText ?? "" let right = self.textDocumentProxy.documentContextAfterInput ?? "" registerSomethingDidChange(left: left, center: center, right: right) } }で、適当なところに次の二つの関数を書いておきます。まず変化前の状況は保存します。
registerSomethingWillChange.swiftfunc registerSomethingWillChange(left:String, center:String, right:String){ self.tempTextData = (left:left, center:center, right:right) }変化が起こった場合、変化前と変化後の状態を比較することで状況を判断します。このロジックは愚直に実装しました。私の知る限りこういう諸々の動作を検知するための機構はUIKitでは提供されていません。
registerSomethingDidChange.swiftfunc registerSomethingDidChange(left:String, center:String, right:String){ //leftは変化後のtextDocumentProxy.documentContextBeforeInput //centerは変化後のtextDocumentProxy.selectedText //rightは変化後のtextDocumentProxy.documentContextAfterInput let b_left = self.tempTextData.left //変化前のleft let b_center = self.tempTextData.center //変化前のcenter let b_right = self.tempTextData.right //変化前のafter let isWholeTextChanged = !((left+center+right) == (b_left + b_center + b_right)) //全体が変化しているか? let wasSelected = !(b_center == "") //選択されていたか? let isSelected = !(center == "") //選択されているか? //全体としてテキストが変化せず、選択範囲が存在している場合→新たに選択した、または選択範囲を変更した if !isWholeTextChanged && isSelected{ //なんらかの操作をする。例えば再変換したい場合はcenterの値を用いて変換候補を表示する。 return } //全体としてテキストが変化せず、選択範囲が無くなっている場合→選択を解除した if !isWholeTextChanged && wasSelected && !isSelected{ //なんらかの操作をする。例えば再変換の候補の表示を消す。 return } //全体としてテキストが変化せず、選択範囲は前後ともになく、左側(右側でも良い)の文字列が変わっていた場合→カーソルを移動した if !isWholeTextChanged && !wasSelected && !isSelected && b_left != left{ //カーソルの移動を処理する。例えば移動範囲が入力中の範囲を超えていた場合はadjustTextPositionなどを用いてカーソルを補正する。 return } //それ以外の状況で全体のテキストに変化がなければ、検出の必要はおそらくない。 if !isWholeTextChanged{ //なんらかの操作 return } //全体としてテキストが変化しており、左は改行コードになっており、かつ前のwholeText(=left+center+right)と後の選択範囲が一致する場合→行全体が選択された if isWholeTextChanged && left == "\n" && b_left + b_center + b_right == center{ //行全体の選択を検知する。 return } //全体としてテキストが変化しており、前の左は改行コードで、かつ前のcenterと後のwholeTextが一致する場合→行全体の選択が解除された if isWholeTextChanged && b_left == "\n" && b_center == left + center + right{ //行全体の選択解除を検知する。 return } //全体としてテキストが変化しており、左右の文字列を合わせたものが不変である場合→ユーザが選択部分をカットした。 if isWholeTextChanged && b_left + b_right == left + right{ //カットを検知する。 return } //全体としてテキストが変化しており、右側の文字列が不変であった場合→ペーストが疑われる。 if isWholeTextChanged && b_right == right{ //もしクリップボードに文字列がコピーされており、かつ、前の左側文字列にその文字列を加えた文字列が後の左側の文字列に一致した場合→確実にペーストである。 if let pastedText = UIPasteboard.general.string, pastedText == left.suffix(pastedText.count){ //なんらかの操作 return } } //上記のどれにも引っかからず、なおかつテキスト全体が変更された場合→範囲外タップ。 if isWholeTextChanged{ //範囲外タップを検出し、例えば確定する。 return } }まとめ
お読みいただいた通りで、Keyboard Extension周りはかなり気合が求められます。頑張りましょう。
余談
こんな記事を書いていたら、iOSの純正キーボードでもちょっと怪しい挙動を発見しました。
入力中にカーソルを真ん中あたりまで移動し、その上で入力範囲外をタップするとカーソルの後の部分が全て消えて確定扱いになります。一方入力中にカーソルを真ん中あたりまで移動し、さらに文字を入力、または消去する操作を行ってから入力範囲外をタップすると単に確定扱いになります。
あまり自然な挙動とは思えないので、バグの可能性が高いと思います。きっとApple純正キーボードの開発者も相当苦労しているんでしょうね。
- 投稿日:2020-08-17T14:56:20+09:00
ダークモード対応の罠
去年iOS 13の新機能で出たダークモードを自社アプリに対応したのですが、思わぬ挙動でバグが出てしまいそのTips共有です
traitCollectionDidChangeの罠
ライト⇄ダークの色切り替え時に特定の処理を行いたい場合は
func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
を使って切り替え時の検知ができます単純にライト/ダーク時に専用の配色を入れる(例 CGColorで色を入れる必要がある)場合は問題ないですが、それ以外の何か別の設定を入れる(例 ダークモードの時だけ〇〇する)みたいなケースだと問題がおきます
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if UITraitCollection.current.userInterfaceStyle == .dark { // ダーク時だけしたい処理 } }コントロールセンターのライト→ダークに切り替え時に
traitCollectionDidChange
が呼ばれるのですが、traitCollectionDidChange
がバックグラウンドに入った瞬間に2回呼ばれます(ライト⇄ダーク切り替えなしに)しかも、ただ2回呼ばれるのではなくバッググラウンドに入った時に、今端末がライト/ダークどっちの設定が入っているかを見れる
UITraitCollection.current.userInterfaceStyle
がライトとダークそれぞれの状態で来ます本来ダークモードの設定してる時だけしたい処理がバックグラウンド時にライトとダークそれぞれ来ることになり、端末設定がライトモードの時でもダークモードの時にしたい処理をしてしまう期待外れなことが起きてしまいます?
この挙動がよくわからず・・・
ちなみに他のライフサイクルでの検知できるメソッドだとどうか?に関しては同様の問題が起きてました・・例
- viewWillLayoutSubviews
- viewDidLayoutSubviews
など
回避策
個人的にあんまり納得いく修正方法ではありませんが、バッググラウンド時は特定の処理までしないようにしました
バックグラウンド上で設定を変えてもフォアグラウンドに戻った時呼ばれるので問題ないですoverride func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if UIApplication.shared.applicationState == .background { return } if UITraitCollection.current.userInterfaceStyle == .dark { // ダークの時のみしたい処理 } }なぜ今回の問題になった挙動をするかいまいちわかってないのでうーんっていう気持ちで修正できました()
おまけ
traitCollectionDidChange
で色を入れるのは大丈夫なの?大丈夫です
ライトモード時はライト、ダークモード時はダークの色を入れてくれます(最終的に)
試しにアセットカタログのファイルで以下の色を用意して
- 色
UITraitCollection.current.userInterfaceStyle
の要素をログで追って確認し、ライトモードでバッググラウンド時に入ってみて検証してみました
dark Optional([1.0, 1.0, 1.0, 1.0]) light Optional([0.0, 0.0, 0.0, 1.0])2回呼ばれますが、
ライトモードだとダーク→ライト
の順番に呼ばれるので最終的にライトの色が入ります
同様にダークモードでもライト→ダーク
と呼ばれダークの色が入るので大丈夫です
- 投稿日:2020-08-17T13:15:07+09:00
APIデータを、JSON形式で取得。【JSON解析】
JSONとは?
JavaScript Object Notation ?
データフォーマット (データ形式)
の一つ。- キーと値のペアで構成。(=辞書型)
- データの記述量が少ないので、読み込みが速い。
- JavaScriptとの親和性が高い。
- JavaScriptに限らず、データのやりとりに広く使われる。
キーは常に文字列ですが、
値にはString
,Int
,Bool
,配列
,null
なども使えます。データフォーマット (データ形式)
現在の主流のデータ形式は、
XML
,JSON
,CSV
の3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ
マークアップ言語
タグで囲む(マークをつける)ことで構造を表現する言語。
<Title>This is Title</Title>HTMLとXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。JSON と API
JSON
は、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。APIとは?
Application Programming Interface?
ソフトウェア同士を繋ぐのが
API
。インターフェイスとは「接点」。
「何か」と「何か」を繋ぐものです。APIで、できること。
認証機能、チャット機能など、色々な機能を共有できます。
APIには、「どうやって使えば良いの?」が示されています。
(ルール・説明書・決まり事)例えるなら...
オランダ・清と、日本とを繋ぐのが、
長崎の出島
です。
外部デバイスとパソコンを繋ぐのが、USB
です。
ソフトウェア同士を繋ぐのがAPI
です。?「APIを公開する」=
外部とやりとりする窓口(=API)を作り、
外部アプリとコミュニケーションや連携ができる状態にする。=「長崎に出島を設置する」
=「PCにUSBを挿し込む」API 例
- オンライン決済
Stripe
- 顔認識AI
Microsoft Face API
- Google Maps
Google Maps JavaScript API
- 商品の在庫管理や注文レポートの取得などを行う
Amazon MWS API
Webリクエストをしてみる。?
https://swapi.dev/api/people/1
のデータを
JSON形式で取得してみたいと思います。URLSessionを使って、HTTP通信する。
Webリクエストを作成して、JSONデータを取得します。
url
に取得したいapiのリクエストURLを設定するURLSession
を使ってtask
を作る- JSONデータを取得
task.resume()
でタスクを開始するコード
Services
フォルダ >PersonApi.swift
PersonApi.swiftclass PersonApi { func getRandomPersonUrlSession() { guard let url = URL(string: PERSON_URL) else { return } let task = URLSession.shared.dataTask(with: url) { (data, response, error) in print("Data = \(data)") print("Response = \(response)") } task.resume() } }Guard
guard文は、
条件を満たさない場合の処理を記述する構文です。
- return メソッド内の処理を、終了
- break 繰り返し処理を、終了
- continue 処理をスキップ
- throw 例外を投げる
URLSessionとは?
関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。URLSessionTaskとは?
URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。
Utility
フォルダ >Constants.swift
ファイルに、 リンクをまとめた。Constants.swiftlet URL_BASE = "https://swapi.dev/api/" let PERSON_URL = URL_BASE + "people/1/"
Controller
フォルダ >SelectPersonVC.swift
=ViewControllerSelectPersonVC.swiftimport UIKit class SelectPersonVC: UIViewController { var personApi = PersonApi() // クラスをインスタンス化 override func viewDidLoad() { super.viewDidLoad() personApi.getRandomPersonUrlSession() // メソッド呼び出し } }結果
data
とresponse
が、無事printされた。
URLSession
を使って、指定URLに対してWebリクエストできた。サーバから送られてきたJSON文字列を、クライアントで使用するには、
解析
が必要らしいので、後ほど行う。console.Data = Optional(637 bytes) Response = Optional(<NSHTTPURLResponse: 0x600000f223c0> { URL: https://swapi.dev/api/people/1/ } { Status Code: 200, Headers { // <----------- "Status Code"? "Content-Length" = ( 0 ); "Content-Type" = ( "application/json" ); Date = ( "Mon, 17 Aug 2020 02:15:29 GMT" ); // 以下省略ちなみに
Status Code
?
400~は、自分側のエラー。
500~は、サーバー側のエラー。JSON解析 ?
サーバから送られてきたJSON文字列を、クライアントで使用するには、
解析
が必要。
- パース(parse)とも言います。
- 最初に、
import Foundation
の宣言が必要。JSONSerialization.jsonObject
を使用する。JSONSerialization.jsonObject?
JSONSerializationは、
Apple標準フレームワークのFoundationに含まれている、便利なライブラリ。let json = JSONSerialization.jsonObject(with: data, options[])Serializationとは...ざっくりですが、
オブジェクトの状態をStreamの状態に変換すること。
(1バイトずつ読み書きできる、データ構造)この機能を使うと、簡単にインスタンスを外部記憶装置などに保存し、
インスタンスの情報を永続化することができる。throw
一般式.swiftfunc メソッド名(引数) throws -> 戻り値 { // エラーを投げる可能性のある処理 }
jsonObject
をOption+クリックすると、Declaration(宣言)が表示されるのですが、// jsonObjectメソッドの、Declarationが表示されます。 class func jsonObject( with data: Data, options opt: JSONSerialization.ReadingOptions = [] ) throws -> Any // Anyは、クラス・構造体・列挙型、「すべての型のインスタンス」を指します。
throws
とあるので、jsonObjectメソッドは、エラーを投げる可能性があります。そこで、エラーを受け取る必要があります。
do-catchとtryで、エラーを受け取る
一般式.swiftdo { // エラーを投げる可能性のある処理、do{}。 try // メソッド呼び出し } catch { // エラーが発生した場合の処理、catch{} }do { let json = try JSONSerialization.jsonObject(with: data, options: []) } catch { debugPrint(error.localizedDescription) return } // localizedDescriptionは、エラーの概要を表示する。
- localizedDescription エラーの概要
- localizedFailureReason エラーが発生した理由
- localizedRecoverySuggestion 復旧方法(NextAction)
- localizedRecoveryOptions AlertViewに表示するボタンの名前
localizedとは?
Localizationとは、アプリを各国の言語に合わせること。
localizedDescription
とかを使うと、
フレームワーク側でローカライズしてくれるので、他言語対応の手間が省ける。キャストとは?
変数の型を、別の型に変換すること。
guard let jsonAny = json as? [String: Any] else { return }【Swift入門 文法編】型キャスト(as, as!, as?)をマスターしよう
JSON解析、おわり。
先ほどの
task
に色々追記していきます。let task = URLSession.shared.dataTask(with: url) { (data, responce, error) in // ここに色々追記します。 }error
errorがnilじゃなければ、エラーメッセージを表示して、
returnで処理を終了。guard error == nil else { debugPrint(error.debugDescription) return } // Guardは前述dataの、アンラップ
data
パラメータは、リクエストに失敗するとnil
となるらしいので、オプショナル型。
JSONSerialization.jsonObject
で使用したいので、アンラップします。guard let data = data else { return }全体のコード
personApi.swiftfunc getRandomPersonUrlSession() { guard let url = URL(string: PERSON_URL) else { return } let task = URLSession.shared.dataTask(with: url) { (data, responce, error) in guard error == nil else { debugPrint(error.debugDescription) return } guard let data = data else { return } do { let json = try JSONSerialization.jsonObject(with: data, options: []) guard let jsonAny = json as? [String: Any] else { return } ---------> print(json) } catch { debugPrint(error.localizedDescription) return } } task.resume() }
print(json)
の実行結果は以下の通り。SWAPI(The Star Wars API)の
https://swapi.dev/api/people/1
のデータを、
JSON形式で取得できた。"Luke Skywalker"の誕生日とか、目の色とか、身長、髪の色とか。
実行結果.{ "birth_year" = 19BBY; created = "2014-12-09T13:50:51.644000Z"; edited = "2014-12-20T21:17:56.891000Z"; "eye_color" = blue; films = ( "http://swapi.dev/api/films/1/", "http://swapi.dev/api/films/2/", "http://swapi.dev/api/films/3/", "http://swapi.dev/api/films/6/" ); gender = male; "hair_color" = blond; height = 172; homeworld = "http://swapi.dev/api/planets/1/"; mass = 77; name = "Luke Skywalker"; "skin_color" = fair; species = ( ); starships = ( "http://swapi.dev/api/starships/12/", "http://swapi.dev/api/starships/22/" ); url = "http://swapi.dev/api/people/1/"; vehicles = ( "http://swapi.dev/api/vehicles/14/", "http://swapi.dev/api/vehicles/30/" ); }構造体を作る。?
JSON解析の続きです。
さて、Webリクエストにより取得したJSONデータですが、このままでは使えません。JSONのメンバの値は様々な型を持つため、
パースするにはどのメンバがどの型を持つかを、一つ一つ指定しなければいけません。「名前: 値」をメンバと呼びます。
JSONを、
構造体
に変換し、表示項目の宣言をします。Struct[構造体]とは?
ひとことで簡単に言えば、継承のできないクラス。PersonModel.swiftstruct Person { // "Person"構造体 let name : String let height : String let mass : String let hair : String let birthYear : String let gender : String let homeWorldUrl : String let filmUrls : String let vehicleUrls : [String] <------- // 配列 let starshipUrls : [String] <------- // 配列 }private内で、こんなことします。?
let とあるキーの値 = jsonAny["キー"]personApi.swiftprivate func parsePersonManual(json: [String: Any]) { let name = json["name"] as? String ?? "" let height = json["height"] as? String ?? "" let mass = json["mass"] as? String ?? "" let hair = json["hair_color"] as? String ?? "" // 以下、省略。 } // "キー"は、https://swapi.dev/api/people/1のデータと同名。 // "hair_color"は、仮に"hair"の場合、APIデータ取得不可。private
アクセス修飾子の一つです。
- 別ファイルからのアクセスはNG。
- クラス単位のスコープではない。
『??』とは
「nilガード」です。
bがnil
だったら、aに空文字列を代入してくれるという構文です。let a = b ?? ""初期化
同じくprivateメソッド内にて、初期化します。
return
するので、-> Person
もお忘れなく。let person = Person(name: name, height: height, mass: mass, //以下、省略。) return person // return person = Person(name: name, ...) でもOKprivateの全体コード
自動補完もしてくれないから、とても面倒。
JSONのための外部ライブラリ『SwiftyJSON』ってのが
よく使われるらしいので、後日学びます。private func parsePersonManual(json: [String: Any]) -> Person { let name = json["name"] as? String ?? "" let height = json["height"] as? String ?? "" let mass = json["mass"] as? String ?? "" let hair = json["hair_color"] as? String ?? "" let birthYear = json["birth_year"] as? String ?? "" let gender = json["gender"] as? String ?? "" let homeWorldUrl = json["homeworld"] as? String ?? "" let filmUrls = json["films"] as? String ?? "" let vehicleUrls = json["vehicles"] as? [String] ?? [String]() let starshipUrls = json["starships"] as? [String] ?? [String]() let person = Person(name: name, height: height, mass: mass, hair: hair, birthYear: birthYear, gender: gender, homeWorldUrl: homeWorldUrl, filmUrls: filmUrls, vehicleUrls: vehicleUrls, starshipUrls: starshipUrls) return person }個別のJSONデータを、print?
private
メソッドを呼び出して、インスタンス作って、
個別のJSONデータを、do { let json = try JSONSerialization.jsonObject(with: data, options: []) guard let jsonAny = json as? [String: Any] else { return } ------> let person = self.parsePersonManual(json: jsonAny) print(person.name) print(person.height) } catch { debugPrint(error.localizedDescription) return }JSONデータの
name
とheight
を取得。
Luke Skywalker、意外と背が小さい。実行結果.Luke Skywalker 172おしまい
JSONでAPIデータ取得するの大変だなあ。
SwiftyJSONについては、のちに追記予定。
- 投稿日:2020-08-17T13:15:07+09:00
APIデータを、JSON形式で取得。【URLSession】
JSONとは?
JavaScript Object Notation ?
データフォーマット (データ形式)
の一つ。- キーと値のペアで構成。(=辞書型)
- データの記述量が少ないので、読み込みが速い。
- JavaScriptとの親和性が高い。
- JavaScriptに限らず、データのやりとりに広く使われる。
キーは常に文字列ですが、
値にはString
,Int
,Bool
,配列
,null
なども使えます。データフォーマット (データ形式)
現在の主流のデータ形式は、
XML
,JSON
,CSV
の3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ
マークアップ言語
タグで囲む(マークをつける)ことで構造を表現する言語。
<Title>This is Title</Title>HTMLとXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。JSON と API
JSON
は、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。APIとは?
Application Programming Interface?
ソフトウェア同士を繋ぐのが
API
。インターフェイスとは「接点」。
「何か」と「何か」を繋ぐものです。APIで、できること。
認証機能、チャット機能など、色々な機能を共有できます。
APIには、「どうやって使えば良いの?」が示されています。
(ルール・説明書・決まり事)例えるなら...
オランダ・清と、日本とを繋ぐのが、
長崎の出島
です。
外部デバイスとパソコンを繋ぐのが、USB
です。
ソフトウェア同士を繋ぐのがAPI
です。?「APIを公開する」=
外部とやりとりする窓口(=API)を作り、
外部アプリとコミュニケーションや連携ができる状態にする。=「長崎に出島を設置する」
=「PCにUSBを挿し込む」API 例
- オンライン決済
Stripe
- 顔認識AI
Microsoft Face API
- Google Maps
Google Maps JavaScript API
- 商品の在庫管理や注文レポートの取得などを行う
Amazon MWS API
Webリクエストをしてみる。?
https://swapi.dev/api/people/1
のデータを
JSON形式で取得してみたいと思います。URLSessionを使って、HTTP通信する。
Webリクエストを作成して、JSONデータを取得します。
url
に取得したいapiのリクエストURLを設定するURLSession
を使ってtask
を作る- JSONデータを取得
task.resume()
でタスクを開始するコード
Services
フォルダ >PersonApi.swift
PersonApi.swiftclass PersonApi { func getRandomPersonUrlSession() { guard let url = URL(string: PERSON_URL) else { return } let task = URLSession.shared.dataTask(with: url) { (data, response, error) in print("Data = \(data)") print("Response = \(response)") } task.resume() } }Guard
guard文は、
条件を満たさない場合の処理を記述する構文です。
- return メソッド内の処理を、終了
- break 繰り返し処理を、終了
- continue 処理をスキップ
- throw 例外を投げる
URLSessionとは?
関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。URLSessionTaskとは?
URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。
Utility
フォルダ >Constants.swift
ファイルに、 リンクをまとめた。Constants.swiftlet URL_BASE = "https://swapi.dev/api/" let PERSON_URL = URL_BASE + "people/1/"
Controller
フォルダ >SelectPersonVC.swift
=ViewControllerSelectPersonVC.swiftimport UIKit class SelectPersonVC: UIViewController { var personApi = PersonApi() // クラスをインスタンス化 override func viewDidLoad() { super.viewDidLoad() personApi.getRandomPersonUrlSession() // メソッド呼び出し } }結果
Data
とResponse
が、無事printされた。
JSON形式でAPIデータを取得できた。console.Data = Optional(637 bytes) Response = Optional(<NSHTTPURLResponse: 0x600000f223c0> { URL: https://swapi.dev/api/people/1/ } { Status Code: 200, Headers { // <----------- "Status Code" "Content-Length" = ( 0 ); "Content-Type" = ( "application/json" ); Date = ( "Mon, 17 Aug 2020 02:15:29 GMT" ); // 以下省略ちなみに
Status Code
400~は、自分側のエラー。
500~は、サーバー側のエラー。JSON解析
サーバから送られてきたJSON文字列を
クライアントで使用するには、解析
が必要。
- パース(parse)とも言います。
- 最初に、
import Foundation
の宣言が必要。JSONSerialization.jsonObject
を使用する。先ほどの
task
に色々追記していきます。let task = URLSession.shared.dataTask(with: url) { (data, responce, error) in // ここに色々追記します。 }error
errorがnilじゃなければ、エラーメッセージを表示して、
returnで処理を終了。guard error == nil else { debugPrint(error.debugDescription) return } // Guardは前述Sample
追記予定。
おしまい。
- 投稿日:2020-08-17T13:10:56+09:00
Google Maps もダークモード対応しようね
スタイルを適用する
isDarkMode
のようなパラメータがあるわけではなく、自分で用意したスタイルを適用する形になります。
背景は黒、路線は青、文字は白などを個々に指定する必要があります。指定方法としては JSON ファイルで指定する方法 と String で直接指定する方法 がありますが本記事では前者について紹介します。
実装方法は大きく変わりませんが、可読性や Android との共通化の観点から JSON 形式で指定することをお勧めします。JSON で指定する
style.json
などの JSON ファイルをローカルに用意し、Map のmapStyle
に適用します。// Map のインスタンス let mapView = GMSMapView() // ローカルファイルの URL を取得 if let styleURL = Bundle.main.url(forResource: "style", withExtension: "json") { // スタイルを適用 mapView.mapStyle = try GMSMapStyle(contentsOfFileURL: styleURL) }パラメータ
下記 3 つの要素を 1 セットとして、スタイル指定を配列で羅列していく形になります。
- featureType - 地理的な要素 (国境、施設、道 etc.)
- elementType - 地図上の要素 (線、文字)
- stylers - スタイル (色、明度、表示 / 非表示 etc.)
例えば 一般道の名前を白くする という指定は以下のようになります。
style.json[ { "featureType": "road.local", "elementType": "labels", "stylers": [ { "color": "#FFFFFF" } ] } ]各パラメータの詳細はこちらから。
https://developers.google.com/maps/documentation/ios-sdk/style-referenceテンプレートあります
パラメータを 1 から全て指定する事は根気がいる作業です。
実際の現場でデザイナーさんに細かく指定してもらう事や確認してもらう事は困難でしょう。というわけで、スタイルを生成するサイトが公式であります。
https://mapstyle.withgoogle.comDark, Night, Aubergine からデザイナーさんと相談して選ぶ or カスタマイズすれば、簡単にダークモード対応が完了します。
備考
Android エンジニアさんは こちら をどうぞ。
iOS と同様に対応できるので、本記事をご参考いただければ幸いです。
- 投稿日:2020-08-17T10:08:54+09:00
delegateの設定、dismiss 備忘録
遷移前の変数、配列を遷移後のコードでも使用したい場合、delegateまたはシングルトンを使うといい
delegateの使い方
//遷移前コード let vc = R.storyboard.storyboard.menuView()! vc.delegate = self self.present(vc, animated: true, completion: nil)//遷移後コード var delegate : ViewController? //自分自身を閉じる(破棄する) self.dismiss(animated: true, completion: { //クロージャー //self.delegate?.遷移前の実行したい関数 }) let vc = R.storyboard.main.main()! self.present(vc, animated: true, completion: nil)
- 投稿日:2020-08-17T09:33:00+09:00
SwiftのFunctionを学んだ。【returnとか、paramとか】
Functionの初歩。
func(関数)を学んだので、簡単におさらい。
Functionを何故使うのか。
amazonのような買い物アプリにて、
ユーザーの買い物カゴの合計金額を計算する箇所が5つあるとき、
5箇所で同じコードを書くのは無駄。
コード量が増えて、プログラム自体が複雑で読みにくくなる。
処理が1回しかないものでも、functionには利点あり。
functionにしてマトめる
と、コード量がかなり多いとき読みやすい。function名
を適切につけて、判別しやすい。パラメータと引数の違い
意思疎通にはそれほど困らないけど・・・
「引数 == パラメータ」ではない
- パラメータ (仮引数)は、関数に受け渡されるものの宣言
- 引数は、関数に渡した実際の値
// Funtion with parameters func declare(name: String) { print(name) } declare(name: "Shimura") // 呼び出し declare(name: "Ken") // 呼び出し「志村」と「けん」が引数で、nameがパラメータ。
パラメータと引数の違い因みに、「ひきすう」と読みます。
関数3パターン、おさらい。
with no parameters?
func declareName() { print("MyName") } declareName() // 呼び出しwith parameters?
func declare(name: String) { print(name) } declare(name: "Shimura") // 呼び出し declare(name: "Ken") // 呼び出しwith a return value?
func 一日の秒数() -> Int { return 24 * 60 * 60 } let 秒数 = 一日の秒数() // 呼び出し & 代入 (=インスタンス化) print("一日は\(秒数)秒!") // 一日は86400秒!with parameters and a return value?
func createFullName(firstName: String, lastName: String) -> String { return firstName + " " + lastName } //let fullName = createFullName(firstName: String, lastName: String) let fullName = createFullName(firstName: "Suzuki", lastName: "Ichiro") // 呼び出し & 代入 (=インスタンス化) print(fullName) // Suzuki Ichiroreturnを使った関数
一方的にただ呼び出す関数も便利ですが、(上記2つのコード??)
関数の中でいろいろな処理をさせて、その「結果」を貰いたいときがあります。
要するに、呼び出しに対する「返事」が欲しいときです。with a return value?
with parameters and a return value?
戻り値を持つ関数。
書き方は、こんな感じ。func 関数の名前() -> 戻り値の型 { // 実行する処理 return 戻り値 }具体例。
func 一日の秒数() -> Int { return 24 * 60 * 60 }でも、上記コードだけでは実行されない。
「関数の呼び出し」
を、変数or定数に代入
。(=インスタンス化)let seconds = 一日の秒数() // インスタンス化 print("一日は\(seconds)秒!") // 一日は86400秒!
「
\()
」の中に変数を入れると、その内容が埋め込まれます。Swiftでは『戻り値』のデータ型を指定する 必要があります。
『->
データ型』で指定。これがないとエラー。{}内でreturnが実行されると 関数内の処理は終了なので、
関数{}の中の、一番最後に書く。func 一日の秒数() -> Int { // 今回はInt型。(Integer: 整数) return 24 * 60 * 60 print("Hello World") // エラー。 Code after 'return' will never be executed } let seconds= 一日の秒数() print("一日は\(seconds)秒!") // 『\()』を使って、変数secondsを埋め込み。おしまい。
参考サイト
- 投稿日:2020-08-17T09:18:42+09:00
[2020年版]tabBarControllerの設定方法
tabBarControllerとは?
tabBarControllerとは、よくiPhoneで見かける液晶下部にあるボタンのことを指します。
このBarの働きは複数画面と接続し、UITabBar(以下、tabBar)に並べられたボタンで画面の切り替えを行う部品であります。tabBarの実装方法
Xcode右上のプラスボタン(+)をクリックして
tabBarControllerを選択
すると以下画像のように3つの画面が出てくるのでそのまま配置。
tabBarのテキスト変更方法
以下の画像のようにテキストを変更したいtabBarをフォーカスして、右サイドバーのTitleのテキストを編集。
すると、左の液晶のtabBar左側のボタンを継承して右上の液晶と連携していることが分かる。(今回は'テキスト編集'と編集)
tabBarアイコン変更方法
アイコンも同様に変更したいtabBarをフォーカスして、右サイドバーで編集。なお、アイコンの場合はImageを変更すればアイコンを変更できる。
ちなみに、よく見かけるアイコンはAppleがすでに用意してくれています。
実装確認
ここまで編集をおこないシュミレーター を起動するとアイコンとテキストの両方の編集が成功しているとおもいます。
これだけの設定で画面遷移を実装できる点、Xcodeは素晴らしいですよね!しかも割と直感的に操作ができる!最後に
今回はtabBarControllerに関してアウトプットを行いました。
久々の更新となりましたが今後も継続していきますので応援宜しくお願いします!
- 投稿日:2020-08-17T01:03:57+09:00
Loggingについて調べてみた(Swift)
この記事を書くに至った経緯
AppleStoreに個人開発アプリを初リリースするにあたり、printをどう処理すべきか、またリリース後のデバグ処理を効果的に行うためのログどう取るべきかわからなかったため、Loggingについて調べ、備忘録用に内容をまとめてみました。
この記事でわかること
- リリース時にPrintは消すべきかどうか
- PrintをLoggingを使った方法に置き換えることのメリット
- WWDC2016で紹介されているLoggingについてのまとめ
- その他Logging関連の役立つサイトを紹介
1. リリース時にPrintは残すべきか消すべきか?
結論:
print()は削除し、代わりに必要に応じてLoggingを使用すべきだと判断しました。
理由:
1.個人情報保護
なんらかの理由でプライバシーに関わる情報をPrint文で出力している場合、個人情報流出に繋がる可能性がある。2.パフォーマンスに影響する
print()処理ではアウトプット時に, binary dataをStringに変換しているので、処理が重くなる。なお、以下のようにrelease build時にprint()がアウトプットされないようにすることはできるが、1、2を考慮すると必要なければ消した方が良いと考える。
func print(_ object: Any) { #if DEBUG Swift.print(object) #endif }2. Printをloggingに置き換えることのメリット
Apple documentation(1)記載のメリット:
Logメッセージを使用することで、アプリRUNの時のログを連続データとして確認することができ、特に以下の場合に有効としている。・アプリにデバッガーをつけることができない(例:ユーザーのマシン上で問題の診断を行う場合)
・エラーが処理の途中で起きており、デバッガーでは捉えることが難しい
・あるタスクがいつ開始し、終了したのかを知りたい場合iOS開発者Antoine v.d. SwiftLeがavanderleeの記事(2)で説明してるメリット:
・パフォーマンコストが低い
・デバイスより後からでもLogアーカイブを回収できるプラバシー保護の観点からもメリット:
WWDC2020でも紹介されているようにLogメッセージの形式をプライバシーとパブリックで切り替えることができ、さらにデフォルトの設定で、
・Dynamic String/Collections/ArrayはPrivateデータ
・Static String/Scalers/objects はPublicデータ
と扱っているため、デッバーガーを付けずに、デバイスで記録されたログをコンソールで確認すると、Privateデータは以下の様に、と表示される。
よって、個人情報保護の観点からも、osLogを使用するメリットがあると考える。
補足:
プライバシーレベルは、%{public}@、%{private}@ syntaxをつける必要あったため、付け忘れる恐れがあったが、iOS14からは以下の様に、以下の様にprivacyをenumで設定することができる。Logger.viewCycle.debug("User \(username, privacy: .private) logged in")まとめ:
1.負荷が少ない
2.バグの特定がしやすくなる
3.個人情報の保護に有効な機能を備えている
4.後からログを回収できる参考
1. AppleDocumentation Logging
2. OSLog and Unified logging as recommended by Apple
3. WWDC2020: Explore logging in Swift3.WWDC2016で紹介されているLoggingについてのまとめ
参考:WWDC2016: Unified Logging and Activity Tracing
背景:
・Loggingは2014年に導入された。
・その後、AppleがActivity Tracingを導入、FaultとErrorの概念が形成された。導入目的
・UserModeとkernel modeで使用できる効率的なLogメカニズムを作ること。
- Compress data: データを圧縮していため、パフォーマンスの面からいってもLow コスト。
- Deferring work and data collection :実際にLogを表示する時まで遅らせる処理をできるだけ減らし、Observer effectを防ぐ
- Managing log message lifecycle:わざわざコマンドを入力しなくてもいい様に、できるだけデバグに必要な情報をLoggingで集められるようにする。
*以下の画像は、WWDC2016 Unified Logging and Activity Tracingで使用のスライドのスクショです。
Log Fileのフォーマット
・ログはバイナリーデータで保存されているため、ログを確認するにはツール使用する必要がある。
・ログアーカイブ常に、ログデータは1ファイルでまとめられるため、バグレポートとして、Emailに添付して送れる。サブシステムとカテゴリー
・ログメッセージ:サブシステムとカテゴリーに分類される。
・ツールでログメッセージを見る時、フィルターや検索をかけることができる。
・サブシステム、カテゴリーの数に限りはないため、好きなだけ作成できる。ログ種類と保存先
種類:
ベーシックレベル:
1.Default
2.Info
3.Debug
スペシャルレベル:
1.Fault
2.Error保存先:
・Defaultは常にEnable
・Logging systemには、ログメッセージ用のin-memory circular buffersがあり、そこにinfoデータは一時保存される。
・FaultとErrorのみDiskに保存される。
・メモリーにFault/Errorの場合のみ残して、残りはメッセージについては、最後のVersionのみDiskの保存される。プライバシー
・Dynamic String/Collections/ArrayはPrivateデータとされている。
・Static String/ScalersはPublicデータととされている。
これにより、個人が特定できる情報のログ化を防いでいる。FaultとErrorについて
Error:
特定のアプリケーション・ライブラリーで検知された問題を示す。なにかエラーが見つかれば、メモリーバッファーから、その処理に関連するすべてのログメッセージをDiskに書き込む。Fault:
Errorより広い範囲(システムレベル)で発生する問題を示す。なにかエラーが見つかれば、メモリーバッファーから、その処理とそのactivityに関連するすべての処理に関するすべてのログメッセージをDiskに書き込む。ログ保存の仕組み
・FaultとErrorはBasuc log dataとは別のログファイルにキャプチャーされ優先的にメモリが確保される。
・通常、non observer effectによる影響を受けないよう設計されているが、live log streamの場合、IPCをすべてのCallに対して行うため、影響をもろに受けるようになる。コンソールを使ったログアーカイブの確認操作デモ
参考:WWDC2016: Unified Logging and Activity Tracingの16:00-25:00をご確認ください。
osLog APIsまとめ
OS_log_createの使用例
os_log_t log = os_log_create("サブシステム名", "カテゴリー名")シングルトンを作成し、サブシステム、カテゴリーをつけることで、コンソールでログを確認時にカテゴリー分けやフォルターをかけられるようになる。
OS_LOG_DEFAULT:
ログのカテゴリー分けやフィルター性能が落ちるが、カテゴリーやフィルターを気にしない場合は、このコマンドでOKBuild-in Type Formatters
各パラメータのプライバシー設定
・パラメータごとにプライバシー設定できる。
・Dynamic String/Collections/ArrayはPrivateデータとされている。
・Static String/ScalersはPublicデータととされている。
これにより、個人が特定できる情報のログ化を防いでいる。WW2016以前のログの取り方との比較
Activity API
コンソールでできること
・ライブてログを確認できる。
・ログアーカイブを開いて、アクティビティーをメインにログを追える。
・フィルター、検索できる。
・デバイスからのログを見れる。コマンドラインツールでできること
・コンソールと同じ機能
・Stream live log messageを見れる
・付与したメッセージ付きの Stream live logをみることができる。
・ログファイル、アーカイブを表示できる。ベストプラクティス
・デバグに必要な情報だけメッセージに含むようにする。
・Stringへの変換作業はツールで行う。変換すればするほど遅くなる。
・関数の中にos_log* APIを関数に含めないこと。
・APIをラップする必要がある場合、関数ではなく、マクロ内ですること。
・コレクションの必要なものだけのログをとること。(dictionaries, array)
・ループの中でコードを使わないこと。いつos_log APisを使うべきか?
os_log : 問題をデバグするのにクリティカルな情報のログ取得したい時に使用する。例:数時間前の情報を必要とする時。
os_log_info: errorやfault中にキャプチャーされた追加情報を取得するとき。
os_log_debug: 開発など、多くのdebugをする必要があるとき。
os_log_error: app: でキャプチャーした追加情報
os_log_falult: systemに関する追加情報が必要な時。4. その他Logging理解に役立つ情報
Loggingを使用してパフォーマンスを測る方法:
https://developer.apple.com/videos/play/wwdc2018/405/Unified loggingをコンソールで使用する練習用のチュートリアル:
https://www.raywenderlich.com/605079-migrating-to-unified-logging-console-and-instrumentsosLogをprintやNSlogの代替として使用することへの考察:
https://www.avanderlee.com/debugging/oslog-unified-logging/iOS14から導入される最新のunified logging APIs:
https://developer.apple.com/videos/play/wwdc2020/10168/iOS 14's New Logger API vs. OSLog
https://medium.com/better-programming/ios-14s-new-logger-api-vs-oslog-ef88bb2ec237