20200817のSwiftに関する記事は13件です。

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

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

【初心者向け】「return」がイマイチ分からない2年前の自分に向けての記事(if-elseの話も少し)

この記事を読んで習得できること

・プログラミングを初めてまだ「return」の使い方が分からない方々
・3年前の私

経緯

大学院でプログラムを書いていた3年前のボク。
大学の研究で実験の解析で必要だったため、必死で書いていたのをなんとなく覚えている。

ひとまず、自分の行いたい解析プログラムは動くようになり、
喜びながらそのプログラムを使って解析を行っていた。

ひと段落してから、プログラムを見てみると、
「結構ソース汚いな…」と思い、修正しようと試みた。

しかし、ソースのスパゲッティ感*が半端なく、
なかなか修正が出来ずに、前回と同じようなソースなのに、
一から書き直したのを覚えている。

3年前のソースで何が起こっていたのか

おおよそのイメージだが、こんな感じ。

sample.swift
func 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.swift
func 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.swift
func 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.swift
func 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.swift
func say() {
    num = 5
    doubleNum = doubleNum(num) // 10が返ってくる
}

// 2倍した数値を返してくれる関数
func doubleNum(num :Int) -> Int {
    return num * 2 //数値を返す
}

別のメソッドで計算して、処理結果を返してもらうってことも容易になる。

FizzBuzz使うとさらにお分りいただけるかもしれない。

FizzBuzz.swift
func 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をうまく使わないとメソッドが盛り盛りになってしまうので、もし使っていない方がいたら是非使って欲しい。
これを知らないで、よく大学卒業できたな、俺…

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

iOS13系からデフォルトになったpageSheetのdismiss風の動きをさせる

はじめに

iOS13系からUIViewControllerをpresentで遷移させると、modalPresentationStyleの.pageSheetがデフォルトの挙動となりました。
たまに見る、覆い被さる系の、アレですね。

closableVC4.gif

これの気持ち良さは良きだなと思った反面、
これやと遷移先のUIViewControllerは上まで到達せず、あくまで一時的な画面としてのレイアウトとなります。

今回やりたかったのは、
presentで全画面表示(iOS12系まで標準やったアレ)、かつdismissの動きはpageSheet風、
という状態を作りたかったわけで、
そうなるともう作るっきゃない!
となったので、実際にやってみましたと。

成果物

こんな感じになりました。
(グラデーションががびがびなのは見ないでください。)

closableVC3.gif

最近、とある事故があり、iPhoneが11になったことで、UINavigationControllerによくある閉じるボタンを押すのが辛くなっていた私でも、簡単に画面を閉じることができました!
UX向上だわこれ、って思いましたね、うんうん。

実装

タッチイベントを利用します。

didTouchesBegan
didTouchesMoved
didTouchesEnded

こいつらですね。
やることは、

  • 最初にタッチした位置から動いた距離分だけ画面を動かす
  • タッチが終わったときに
    • 閾値を超えていたらdismissする
    • 閾値以内やったら画面を元の状態に戻す

って感じ。

なんとなくイメージついたかと思います。

まずは、Storyboardで画面の準備。
Viewの構成としては、

  • 背景用のView (backgroundViewとする)
  • コンテンツ用のView (overViewとする)

の2つです。

スクリーンショット 2020-08-17 13.27.20.png

んで、
実際のコードは以下のようになります。

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どんどんでかくなるし、もうもはや左上に閉じるボタンとかをつけるだけでは厳しい世界に入っていっているので、工夫が必要になってくる場面も増えてくるかと思います。その内の一つの手法としては今回の記事は有用かと思うので、どんどん使いやすいアプリ目指してがんばりやしょう!

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

【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分後に自動更新が終了します。


参考:
自動購読課金について【iOS編】 | サイバーエージェント 公式エンジニアブログ

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

iOS Keyboard Extensionでキーボードを介さない操作を検知する

iOSの純正キーボードは超絶多機能です。ちょっと考えただけでもこんな機能がついています。

  • 入力中の文字を薄い青色(ダークモードでは黄色)でハイライトする。
  • 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、変換候補を変更する。
  • 点滅するカーソルをドラッグすることによるカーソルの移動を検知し、入力中の範囲から外に出られないようにする。
  • 入力範囲外をタップした場合入力中の文字を確定する。
  • 選択されているテキストを取得し、再変換する。
  • ペースト操作が行われた場合検知し、入力中の文字を確定する。 これは高度な機能です。実際私が日本語対応のkeyboard extensionを漁った限り、純正キーボードと同じ挙動を再現できているものはほとんどありませんでした。 iOS keyboard extension functions.png そこでこれらの機能をできる限り実現すべく、いろいろ努力した結果をまとめます。

入力中の文字のハイライト

いきなり残念なお知らせですが、これは今は断念するのが正解です。
この機能の実現にはsetMarkedText()というメソッドが利用できます。が、これを実現すると他の無数の機能が死にます。
詳しくはこちらを参照してください:UITextDocumentProxyのsetMarkedTextを(まだ)使ってはいけない。 - Qiita

それ以外

  • カーソル移動検知(→カーソル移動制限など)
  • ペースト検知
  • 範囲外タップ検知
  • 選択検知
  • 選択解除検知
  • カット検知

などがどうにかできました。
大体、documentContextBeforeInputdocumentContextAfterInput、それにselectedTextをゴリ押しで取得していくとどうにかなりました。UIInputViewControllertextWillChangetextDidChangeは、引数の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.swift
class 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.swift
func registerSomethingWillChange(left:String, center:String, right:String){
    self.tempTextData = (left:left, center:center, right:right)
}

変化が起こった場合、変化前と変化後の状態を比較することで状況を判断します。このロジックは愚直に実装しました。私の知る限りこういう諸々の動作を検知するための機構はUIKitでは提供されていません。

registerSomethingDidChange.swift
func 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の純正キーボードでもちょっと怪しい挙動を発見しました。
入力中にカーソルを真ん中あたりまで移動し、その上で入力範囲外をタップするとカーソルの後の部分が全て消えて確定扱いになります。一方入力中にカーソルを真ん中あたりまで移動し、さらに文字を入力、または消去する操作を行ってから入力範囲外をタップすると単に確定扱いになります。
bug.gif
あまり自然な挙動とは思えないので、バグの可能性が高いと思います。きっとApple純正キーボードの開発者も相当苦労しているんでしょうね。

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

ダークモード対応の罠

去年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で色を入れるのは大丈夫なの?

大丈夫です
ライトモード時はライト、ダークモード時はダークの色を入れてくれます(最終的に)
試しにアセットカタログのファイルで以下の色を用意して
スクリーンショット 2020-08-17 14.39.24.png

  • UITraitCollection.current.userInterfaceStyle

の要素をログで追って確認し、ライトモードでバッググラウンド時に入ってみて検証してみました

dark

Optional([1.0, 1.0, 1.0, 1.0])

light

Optional([0.0, 0.0, 0.0, 1.0])

2回呼ばれますが、
ライトモードだとダーク→ライトの順番に呼ばれるので最終的にライトの色が入ります
同様にダークモードでも ライト→ダークと呼ばれダークの色が入るので大丈夫です

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

APIデータを、JSON形式で取得。【JSON解析】

 JSONとは?

JavaScript Object Notation ?

  • データフォーマット (データ形式) の一つ。
  • キーのペアで構成。(=辞書型)
  • データの記述量が少ないので、読み込みが速い。
  • JavaScriptとの親和性が高い。
  • JavaScriptに限らず、データのやりとりに広く使われる。

picture_pc_5726e4146e6ea68b6e70cc6e7b9361de.png

キーは常に文字列ですが、
値にはString, Int, Bool, 配列, nullなども使えます。

 データフォーマット (データ形式)

現在の主流のデータ形式は、XML, JSON, CSVの3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。

データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ

 マークアップ言語

タグで囲む(マークをつける)ことで構造を表現する言語。

<Title>This is Title</Title>

HTMLXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。

 JSON と API

JSONは、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。

4043253-friday-halloween-jason-movie_113258.png

 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

個人でも使える!おすすめAPI一覧


 Webリクエストをしてみる。?

SWAPI(The Star Wars API)

https://swapi.dev/api/people/1のデータを
JSON形式で取得してみたいと思います。

unnamed.jpg

 URLSessionを使って、HTTP通信する。

『HTTP』とは?

Webリクエストを作成して、JSONデータを取得します。

  1. urlに取得したいapiのリクエストURLを設定する
  2. URLSessionを使ってtaskを作る
  3. JSONデータを取得
  4. task.resume()でタスクを開始する

 コード

  • Servicesフォルダ > PersonApi.swift
PersonApi.swift
class 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 例外を投げる

【Swift入門】guardの使い方をマスターしよう!

 URLSessionとは?

関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。

 URLSessionTaskとは?

URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。

【Swift】URLSessionまとめ

  • Utilityフォルダ > Constants.swiftファイルに、 リンクをまとめた。
Constants.swift
let URL_BASE = "https://swapi.dev/api/"
let PERSON_URL = URL_BASE + "people/1/"
  • Controllerフォルダ > SelectPersonVC.swift=ViewController
SelectPersonVC.swift
import UIKit

class SelectPersonVC: UIViewController {

    var personApi = PersonApi() // クラスをインスタンス化

    override func viewDidLoad() {
        super.viewDidLoad()
        personApi.getRandomPersonUrlSession() // メソッド呼び出し
    }

}

 結果

dataresponseが、無事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~は、サーバー側のエラー。

スクリーンショット 2020-08-17 17.04.35.png

 JSON解析 ?

【Swift入門】SwiftでJSONを扱ってみよう!

サーバから送られてきたJSON文字列を、クライアントで使用するには、
解析が必要。

  • パース(parse)とも言います。
  • 最初に、import Foundationの宣言が必要。
  • JSONSerialization.jsonObjectを使用する。

 JSONSerialization.jsonObject?

JSONSerializationは、
Apple標準フレームワークのFoundationに含まれている、便利なライブラリ。

let json = JSONSerialization.jsonObject(with: data, options[])

Serializationとは...ざっくりですが、

オブジェクトの状態をStreamの状態に変換すること。
(1バイトずつ読み書きできる、データ構造)

この機能を使うと、簡単にインスタンスを外部記憶装置などに保存し、
インスタンスの情報を永続化することができる。

 throw

一般式.swift
func メソッド名(引数) throws -> 戻り値 {
    // エラーを投げる可能性のある処理
}

jsonObjectOption+クリックすると、Declaration(宣言)が表示されるのですが、

// jsonObjectメソッドの、Declarationが表示されます。

class func jsonObject(
  with data: Data, options opt: JSONSerialization.ReadingOptions = []
) throws -> Any
// Anyは、クラス・構造体・列挙型、「すべての型のインスタンス」を指します。

throwsとあるので、jsonObjectメソッドは、エラーを投げる可能性があります。

そこで、エラーを受け取る必要があります。

 do-catchとtryで、エラーを受け取る

一般式.swift
do {     // エラーを投げる可能性のある処理、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.swift
func 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.swift
struct 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.swift
private 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, ...) でもOK

 privateの全体コード

自動補完もしてくれないから、とても面倒。

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データを、print。 

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データのnameheightを取得。
Luke Skywalker、意外と背が小さい。

実行結果.
Luke Skywalker
172  

おしまい


JSONでAPIデータ取得するの大変だなあ。

SwiftyJSONについては、のちに追記予定。

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

APIデータを、JSON形式で取得。【URLSession】

 JSONとは?

JavaScript Object Notation ?

  • データフォーマット (データ形式) の一つ。
  • キーのペアで構成。(=辞書型)
  • データの記述量が少ないので、読み込みが速い。
  • JavaScriptとの親和性が高い。
  • JavaScriptに限らず、データのやりとりに広く使われる。

picture_pc_5726e4146e6ea68b6e70cc6e7b9361de.png

キーは常に文字列ですが、
値にはString, Int, Bool, 配列, nullなども使えます。

 データフォーマット (データ形式)

現在の主流のデータ形式は、XML, JSON, CSVの3つ。
JSONが一番軽く、使い勝手がよく、多く使われる。

データフォーマットとは?XML、JSON、CSVの違いや特徴についてわかりやすく解説するよ

 マークアップ言語

タグで囲む(マークをつける)ことで構造を表現する言語。

<Title>This is Title</Title>

HTMLXMLは、どちらもマークアップ言語。(Markup Language)
違いは、上記サイト参照。

 JSON と API

JSONは、
APIを介して送信されるデータを構造化するための、最も一般的なデータ形式。

4043253-friday-halloween-jason-movie_113258.png

 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

個人でも使える!おすすめAPI一覧


 Webリクエストをしてみる。?

SWAPI(The Star Wars API)

https://swapi.dev/api/people/1のデータを
JSON形式で取得してみたいと思います。

unnamed.jpg

 URLSessionを使って、HTTP通信する。

『HTTP』とは?

Webリクエストを作成して、JSONデータを取得します。

  1. urlに取得したいapiのリクエストURLを設定する
  2. URLSessionを使ってtaskを作る
  3. JSONデータを取得
  4. task.resume()でタスクを開始する

 コード

  • Servicesフォルダ > PersonApi.swift
PersonApi.swift
class 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 例外を投げる

【Swift入門】guardの使い方をマスターしよう!

 URLSessionとは?

関連するネットワーク上のデータ転送処理群(=URLSessionTask)をまとめるクラス。
1つのSessionで繰り返しURLSessionTaskの作成が可能。

 URLSessionTaskとは?

URLで特定されるリソースを実際に取得し、アプリにデータを返却したり、
リモートサーバからファイルのダウンロードやアップロードを行う。

【Swift】URLSessionまとめ

  • Utilityフォルダ > Constants.swiftファイルに、 リンクをまとめた。
Constants.swift
let URL_BASE = "https://swapi.dev/api/"
let PERSON_URL = URL_BASE + "people/1/"
  • Controllerフォルダ > SelectPersonVC.swift=ViewController
SelectPersonVC.swift
import UIKit

class SelectPersonVC: UIViewController {

    var personApi = PersonApi() // クラスをインスタンス化

    override func viewDidLoad() {
        super.viewDidLoad()
        personApi.getRandomPersonUrlSession() // メソッド呼び出し
    }

}

 結果

DataResponseが、無事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~は、サーバー側のエラー。

スクリーンショット 2020-08-17 17.04.35.png

 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

追記予定。

おしまい。

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

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.com

Dark, Night, Aubergine からデザイナーさんと相談して選ぶ or カスタマイズすれば、簡単にダークモード対応が完了します。

備考

Android エンジニアさんは こちら をどうぞ。
iOS と同様に対応できるので、本記事をご参考いただければ幸いです。

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

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)

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

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 Ichiro

 returnを使った関数

一方的にただ呼び出す関数も便利ですが、(上記2つのコード??)

関数の中でいろいろな処理をさせて、その「結果」を貰いたいときがあります。
要するに、呼び出しに対する「返事」が欲しいときです。

 with a return value?

swift6_04_01.jpg

 with parameters and a return value?

swift6_04_07.jpg

戻り値を持つ関数
書き方は、こんな感じ。

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を埋め込み。

おしまい。

 参考サイト

[Swift初心者向け] function(メソッド)の使い方

Swiftの関数、引数、戻り値の基本的な書き方と使い方

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

[2020年版]tabBarControllerの設定方法

tabBarControllerとは?

image.png
tabBarControllerとは、よくiPhoneで見かける液晶下部にあるボタンのことを指します。
このBarの働きは複数画面と接続し、UITabBar(以下、tabBar)に並べられたボタンで画面の切り替えを行う部品であります。

tabBarの実装方法

Xcode右上のプラスボタン(+)をクリックしてtabBarControllerを選択
すると以下画像のように3つの画面が出てくるのでそのまま配置。
image.png

tabBarのテキスト変更方法

以下の画像のようにテキストを変更したいtabBarをフォーカスして、右サイドバーのTitleのテキストを編集。
image.png
すると、左の液晶のtabBar左側のボタンを継承して右上の液晶と連携していることが分かる。(今回は'テキスト編集'と編集)
image.png

tabBarアイコン変更方法

アイコンも同様に変更したいtabBarをフォーカスして、右サイドバーで編集。なお、アイコンの場合はImageを変更すればアイコンを変更できる。
image.png
ちなみに、よく見かけるアイコンはAppleがすでに用意してくれています。
image.png

実装確認

ここまで編集をおこないシュミレーター を起動するとアイコンとテキストの両方の編集が成功しているとおもいます。
これだけの設定で画面遷移を実装できる点、Xcodeは素晴らしいですよね!しかも割と直感的に操作ができる!

最後に

今回はtabBarControllerに関してアウトプットを行いました。
久々の更新となりましたが今後も継続していきますので応援宜しくお願いします!

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

Loggingについて調べてみた(Swift)

この記事を書くに至った経緯

AppleStoreに個人開発アプリを初リリースするにあたり、printをどう処理すべきか、またリリース後のデバグ処理を効果的に行うためのログどう取るべきかわからなかったため、Loggingについて調べ、備忘録用に内容をまとめてみました。

この記事でわかること

  1. リリース時にPrintは消すべきかどうか 
  2. PrintをLoggingを使った方法に置き換えることのメリット
  3. WWDC2016で紹介されているLoggingについてのまとめ
  4. その他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データは以下の様に、と表示される。
Screen Shot 2020-08-17 at 2.43.38 PM.png

よって、個人情報保護の観点からも、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 Swift

3.WWDC2016で紹介されているLoggingについてのまとめ

参考:WWDC2016: Unified Logging and Activity Tracing

背景:

・Loggingは2014年に導入された。
・その後、AppleがActivity Tracingを導入、FaultとErrorの概念が形成された。

導入目的

・UserModeとkernel modeで使用できる効率的なLogメカニズムを作ること。

  1. Compress data: データを圧縮していため、パフォーマンスの面からいってもLow コスト。
  2. Deferring work and data collection :実際にLogを表示する時まで遅らせる処理をできるだけ減らし、Observer effectを防ぐ
  3. Managing log message lifecycle:わざわざコマンドを入力しなくてもいい様に、できるだけデバグに必要な情報をLoggingで集められるようにする。

*以下の画像は、WWDC2016 Unified Logging and Activity Tracingで使用のスライドのスクショです。

Log Fileのフォーマット

image.png
・ログはバイナリーデータで保存されているため、ログを確認するにはツール使用する必要がある。
・ログアーカイブ常に、ログデータは1ファイルでまとめられるため、バグレポートとして、Emailに添付して送れる。

サブシステムとカテゴリー

image.png
・ログメッセージ:サブシステムとカテゴリーに分類される。
・ツールでログメッセージを見る時、フィルターや検索をかけることができる。
・サブシステム、カテゴリーの数に限りはないため、好きなだけ作成できる。

ログ種類と保存先

image.png
種類:
ベーシックレベル:
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の保存される。

プライバシー

image.png
・Dynamic String/Collections/ArrayはPrivateデータとされている。
・Static String/ScalersはPublicデータととされている。
これにより、個人が特定できる情報のログ化を防いでいる。

FaultとErrorについて

image.png
Error:
特定のアプリケーション・ライブラリーで検知された問題を示す。なにかエラーが見つかれば、メモリーバッファーから、その処理に関連するすべてのログメッセージをDiskに書き込む。

Fault:
Errorより広い範囲(システムレベル)で発生する問題を示す。なにかエラーが見つかれば、メモリーバッファーから、その処理とそのactivityに関連するすべての処理に関するすべてのログメッセージをDiskに書き込む。

ログ保存の仕組み

image.png
・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まとめ

image.png

OS_log_createの使用例

image.png

os_log_t log = os_log_create("サブシステム名", "カテゴリー名")

シングルトンを作成し、サブシステム、カテゴリーをつけることで、コンソールでログを確認時にカテゴリー分けやフォルターをかけられるようになる。

OS_LOG_DEFAULT:
ログのカテゴリー分けやフィルター性能が落ちるが、カテゴリーやフィルターを気にしない場合は、このコマンドでOK

Build-in Type Formatters

image.png
image.png

各パラメータのプライバシー設定

image.png
・パラメータごとにプライバシー設定できる。
・Dynamic String/Collections/ArrayはPrivateデータとされている。
・Static String/ScalersはPublicデータととされている。
これにより、個人が特定できる情報のログ化を防いでいる。

WW2016以前のログの取り方との比較

image.png
image.png

Activity API

image.png
image.png

コンソールでできること

image.png
・ライブてログを確認できる。
・ログアーカイブを開いて、アクティビティーをメインにログを追える。
・フィルター、検索できる。
・デバイスからのログを見れる。

コマンドラインツールでできること

image.png
・コンソールと同じ機能
・Stream live log messageを見れる
・付与したメッセージ付きの Stream live logをみることができる。
・ログファイル、アーカイブを表示できる。

ベストプラクティス

image.png
・デバグに必要な情報だけメッセージに含むようにする。
・Stringへの変換作業はツールで行う。変換すればするほど遅くなる。
・関数の中にos_log* APIを関数に含めないこと。
・APIをラップする必要がある場合、関数ではなく、マクロ内ですること。
・コレクションの必要なものだけのログをとること。(dictionaries, array)
・ループの中でコードを使わないこと。

いつos_log APisを使うべきか?

image.png
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-instruments

osLogを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

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