20200329のSwiftに関する記事は17件です。

[iOS]Tesseract OCRで認識させた日本語の情報を取得する

※ これは 2018/4/8,16 に個人ブログへ投稿した内容をまとめて転記したものです。

Google が出資しているという文字認識 API の iOS 用ラッパー、 Tesseract OCR iOS を使って文字認識をさせてみた備忘録です。
環境: macOS HighSierra, MacBook Pro 2016, XCode9.3

準備

本体のリポジトリはここ。ただし、 CocoaPods から読ませるので、直接 Clone しない。
CocoaPods が入ってない場合は、まず CocoaPods をインストール
終わったら下記の記事を参考にインストールしていく。
Xcode 7.0 + Swift2 でTesseract-OCR-iOSを使う(追記あり) - 今日も微速転進

動かしてみる

テストコードは公式 Wiki の方を使用した。シミュレータで動かす分にはこちらのほうがシンプルで良い。
上の記事でも言っているとおり、 BridgingHeader はいらないので惑わされないように1
Tesseract OCR iOS の中身は Tesseract3.03 らしく、最新2の学習データ (tessdata) は使えない。
Github で公開されている 3.04 か、 Wiki からそれ以前のものを選んで使う。

英語版 (eng.tessdata) は 3.04 そのままでも動くが、日本語版 (jpn.tessdata) は 3.04 を使うとエラーが出る。

read_params_file: parameter not found: allow_blob_division

これを解決するために、下記に従って学習データ内のパラメータを変更するのだが、このときビルドに使うソースは3.04を使うこと
4.00 だと加工後の tessdata を読み込んでくれない。

それから、 ./configure にパラメータをつけると私の環境では失敗してしまったので、パラメータをなくしたら上手くいった。
これはこの記事を読みながら事前に

brew link icu4c --force

しておいたからかも。

./autogen.sh
./configure
make
sudo make install
make training
sudo make training-install

なお、一度 4.00 をインストールしてしまったときは、 make したディレクトリで

sudo make uninstall
sudo make training-uninstall

したらアンインストールできた…気がする。

日本語を認識させてみる

日本語を認識させて結果の情報を取得してみました。

ソース

Swift 初心者なので、 API リファレンスを見ながら、 Xcode に言われるがまま書いたソースはこちら

結果

入力
20180416230659.png

出力
20180416234126.png

あえてちょっと斜めにしてみましたが、いい感じに認識していますね!

技術的なメモ

認識結果とその表示

let box = block.boundingBox(atImageOf: actualImageRect.size)

Tesseract では元画像のサイズを 1×1 としたときの相対位置で対象 (文字) の座標を認識しているようです。
なので、デバイスの画面に収まるように画像を自動レイアウトした場合は、レイアウト後のサイズを取得し、それを atImageOf に指定して変換してもらう必要があります。
レイアウト後の画像の実サイズを取得する Extension、UIImageView.ContentClippingRect は下記より。
How to find an aspect fit image’s size inside an image view

Extensionって最高ですね…!

文字のまとまり

let blocks = tesseract.recognizedBlocks(by: G8PageIteratorLevel.symbol)

G8PageIteratorLevel で認識結果のまとまりの大きさを指定できます。
意味深な定数名が付いていますが、もちろん日本語の形態素解析をして云々なんてしていないので、一通り試してみました。

block: ブロック。段落を複数認識したら、そのまとまりということかな?
20180416234131.png

paragraph: 段落
20180416234231.png

textline: 行
20180416234249.png

word: 語。やはり英語前提でスペース区切りで認識されていそう。
20180416234301.png

symbol: 文字
20180416234316.png


  1. リンクが通らなくて1時間くらい悩んだ。 

  2. 2018/4/8 時点で master は 4.00 

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

AlertViewを表示する(ダイアログ的なやつ)

ボタンを押した時に下から出てくるやつを表示するコード

スクリーンショット 2020-03-29 18.42.23.png

スクリーンショット 2020-03-29 18.42.31.png

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    private func popUp() {
        let alertController = UIAlertController(title: "確認", message: "本当に実行しますか", preferredStyle: .actionSheet)

        let yesAction = UIAlertAction(title: "はい", style: .default, handler: nil)
        alertController.addAction(yesAction)

        let noAction = UIAlertAction(title: "いいえ", style: .default, handler: nil)
        alertController.addAction(noAction)

        let cancelAction = UIAlertAction(title: "キャンセル", style: .cancel, handler: nil)
        alertController.addAction(cancelAction)

        present(alertController, animated: true, completion: nil)
    }

    @IBAction func doButtonTap(_ sender: Any) {
        popUp()
    }
}

豆知識

comd + shift + l(エル)
でlibraryを表示できる。

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

改行コードを含む文字列から改行コードを取り除いて一行の文字列を取得する

タイトルどおりです
忘れがちなのでメモ

let a = "hogehoge\fugafuga"

このような文字列をもつ定数があったとしてこれを二行から改行コード(\n)を取り除いて一行の文字列に変換します

// 改行コードを取り除いて、取り除いた後にくっつけて一行にする
a.dfdfa.components(separatedBy: .newlines).joined()

参考

https://qiita.com/Yokemura/items/0beac5be04118c4a9774

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

UserDefaultsのキーを管理

同じKey名を設定したつもりでもtypoしてしまっていてKeyに期待結果が入ってないなんてことはあるので
enumで管理するにはどうすればいいかまとめておこうと思いましたが当然すでにまとめられていたのでその記事のリンクを貼って終わりにします

http://glassonion.hatenablog.com/entry/2016/12/07/140529

これみて自分もとりあえず必要な分だけ作りました

enum UserDefaultsSettings: String {
    case hogehoge

    func set(value: Any) {
        UserDefaults.standard.set(value, forKey: rawValue)
    }

    func object() -> Any? {
        return UserDefaults.standard.object(forKey: rawValue)
    }

    // 該当のKeyに対応するvalueがnilじゃないか
    func hasValue() -> Bool {
        return self.object() != nil
    }
}

あとはUserDefaultsを使って色々なところで値を保存したりしていると使用する場所が散らばって値の変化を追いづらくなるみたいなのでその辺もあとで調べながらまとめていきたい
DataStoreみたいなところで実際のset,object等のUserDefautlsの処理をまとめておけばいい気もするけど

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

Result型を使ってエラーをスマートにハンドリングする方法(Swift)

はじめに

2020/03/29現在、Swiftではスローするエラーの型を指定できません。
そのため、スローするエラーの型が決まっていたとしても、すべてのエラーをキャッチする必要があります。

MyError というエラーの型のみを返す myFunc(isSuccess:) メソッドを例に考えてみます。
(この例では MyError.bar は返ることがないのですが、返る可能性があると考えてください)

enum MyError: Error {
    case foo(_ message: String)
    case bar(_ message: String)
}

func myFunc(isSuccess: Bool) throws -> String {
    if isSuccess {
        return "Success!!"
    } else {
        throw MyError.foo("Foo")
    }
}

myFunc(isSuccess:) メソッドを呼び出します。
エラーの型を MyError だと決めていますが、一番下に } catch { を書いてすべてのエラーをキャッチする必要があります。

before
do {
    let result = try myFunc(isSuccess: false)
    // `result` を使う処理
} catch MyError.foo(let message) {
    // エラーハンドリング
} catch MyError.bar(let message) {
    // エラーハンドリング
} catch {
    // !!!: 絶対に入らないけど必要
}

もし一番下の } catch { を書いていないと、以下のビルドエラーが発生します。

Errors thrown from here are not handled because the enclosing catch is not exhaustive
ここからスローされたエラーは、囲んでいるキャッチが完全ではないため処理されません

Switch文で default: をできる限り書かないようにするのと同様、絶対に入らないとわかっているエラーに対してハンドリングしたくありません。
絶対に入らないことを明確にするため、 fatalError(_:) を使っている人も多いと思います。

} catch {
    fatalError("Unexpected error: \(error).")
}

いずれにせよスマートではないように感じます。
Twitterでスマートな方法を教えていただいたので紹介します。

環境

  • OS:macOS Mojave 10.14.6
  • Swift:5.1.3
  • Xcode:11.3.1 (11C504)

結論

先に結論を述べます。
Result.init(catching:)Result.mapError(_:) を使って実現します。

after
let result = Result { try myFunc(isSuccess: false) }
    .mapError { $0 as! MyError }

switch result {
case .success(let string):
    // `string` を使う処理
case .failure(let error):
    switch error {
    case .foo(let message):
        // エラーハンドリング
    case .bar(let message):
        // エラーハンドリング
    }
}

こうすることで、 MyError 型のみをハンドリングできるようになりました。
エラーを列挙型で定義している場合、Switch文が使えるので、エラーのハンドリング漏れも防げます。

解説

おそらくSwiftに慣れていても、上記のような書き方を知らない人は多いと思います。
少なくとも私は知りませんでした。

1つずつ解説します。

Result.init(catching:)

まずは1行目です。

let result = Result { try myFunc(isSuccess: false) }

こちらは Result 型のイニシャライザを、トレイリングクロージャを使って呼び出しています。

Result 型には Result.init(catching:) という、スローするクロージャを引数に持つイニシャライザがあります。
クロージャの処理が成功した場合は .success を返し、クロージャの戻り値がAssociated Valueに代入されます。
失敗した場合は .failure を返し、エラーがAssociated Valueに代入されます。
つまり、今回は myFunc(isSuccess:)String を返すため、Result<String, Error> という型が生成されます。

Result.mapError(_:)

次に2行目です。

    .mapError { $0 as! MyError }

こちらは失敗時のエラーの型をマップ(変換)するメソッドです。

Result.init(catching:) で生成した型は Result<String, Error> です。
mapError(_:)Error 型を MyError 型に変換し、 Result<String, MyError> を生成します。

ここまでできたら、あとは通常の Result 型と同じように使えます。

switch result {
case .success(let string):
    // `string` を使う処理
case .failure(let error):
    switch error {
    case .foo(let message):
        // エラーハンドリング
    case .bar(let message):
        // エラーハンドリング
    }
}

おまけ:なぜエラーの型を指定できないか考える

個人的な意見ですが、例えば throws の後ろにエラーの型を記述し、スローするエラーの型を指定できると使いやすいと思いました。

func myFunc(isSuccess: Bool) throws MyError -> String {

なぜこれが実装されないか考えたのですが、おそらく普通に実装しても様々なエラーが発生し得るからだと思いました。
自分では MyError 型しかスローしていないつもりでも、実装ミスで他のエラーがスローされる可能性は0ではありません。
実際、今回紹介した方法も as! MyError として強制キャストすることで実現しています。

おわりに

これでエラーをスマートにハンドリングできるようになったと思います!
GitHubにサンプルのplaygroundを上げたので、よかったら実行してみてください。
https://github.com/uhooi/SwiftErrorResultSample/blob/master/ErrorResult.playground/Pages/Sample.xcplaygroundpage/Contents.swift

他にもいい方法がありましたら、コメントなどで教えていただけると嬉しいです。

参考リンク

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

UIViewの背景にグラデーションを設定する

前回作ったチュートリアルのFirstVC,SecondVC,ThirdVCの各画面の背景にグラデーションを設定してみます

実装

グラデーションされたCAGradientLayerを返すメソッドを実装

import UIKit

extension CAGradientLayer {
    static func layerForView() -> CAGradientLayer {
        let gradientLayer = CAGradientLayer()
        gradientLayer.colors =  [UIColor.red.cgColor,
                                 UIColor.blue.cgColor]
        // 設定した各グラデーションカラーのstopポイントを決めている?
        // これを設定しないとおそらく設定したカラーが画面全体に均一の割合で表示される
        gradientLayer.locations = [0.0, 0.6]
        // startPointとendPointを決めることでグラデーションをかける方向を決めている?
        gradientLayer.startPoint = CGPoint(x: 0, y: 0)
        gradientLayer.endPoint = CGPoint(x: 0, y: 1)
        return gradientLayer
    }
}

前回作ったチュートリアルの全3画面にグラデーションをかける

import UIKit

class TutorialContainerViewController: UIViewController {
    enum Page:Int {
        case first
        case second
        case third
    }

    // MARK: - IBOutlet
    @IBOutlet weak var nextButton: UIButton!

    // MARK: - internal
    var didSelectButtonToHome: (() -> Void)?

    // MARK: - private
    var currentPage = Page.first
    private lazy var pageViewController: UIPageViewController = {
        // このViewControllerが持つpageViewControllerを取得
        let pageVC = children.first as! UIPageViewController
        return pageVC
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        pageViewController.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    @IBAction func tappedButton(_ sender: UIButton) {
        if currentPage == .first {
            pageViewController.setViewControllers([getSecond()], direction: .forward, animated: true, completion: nil)
            currentPage = .second
            nextButton.isEnabled = false
            return
        }

        if currentPage == .third {
            didSelectButtonToHome?()
        }
    }
}

extension TutorialContainerViewController {
    private func getFirst() -> FirstViewController {
        let vc: FirstViewController = storyboard!.instantiateViewController(identifier: "FirstViewController")
        let gradientLayer = CAGradientLayer.layerForView()
        // gradientLayerは生成されただけだとまだフレームサイズが決まってなくてx:0, y:0 ,height:0 ,width: 0 のままなのでinsertSublayerしても表示されないためフレームを決めてあげる必要がある
        gradientLayer.frame = vc.view.frame
        // layerのsublayersno0番目にgradientLayerを差し込む
        vc.view.layer.insertSublayer(gradientLayer, at: 0) ← NEW
        return vc
    }

    private func getSecond() -> SecondViewController {
        let gradientLayer =  CAGradientLayer.layerForView()
        let vc: SecondViewController = storyboard!.instantiateViewController(identifier: "SecondViewController")
        gradientLayer.frame = vc.view.frame
        vc.view.layer.insertSublayer(gradientLayer, at: 0) ← NEW
        // SecondViewControllerでボタンがタップされた通知が飛んでくる
        // {}の中身はその通知が飛んできた時に呼ばれる内容だから、getSecond()が呼ばれても{}の中身は呼ばれない
        vc.selectedButtonToThird = {
            self.nextButton.isEnabled = true
            self.pageViewController.setViewControllers([self.getThird()], direction: .forward, animated: true, completion: nil)
        }
        return vc
    }

    private func getThird() -> ThirdViewController {
        let vc: ThirdViewController = storyboard!.instantiateViewController(identifier: "ThirdViewController")
        let gradientLayer =  CAGradientLayer.layerForView()
        gradientLayer.frame = vc.view.frame
        vc.view.layer.insertSublayer(gradientLayer, at: 0) ← NEW
        return vc
    }
}


gradientLayerのフレームを決めないで表示されなかったり、insertSublayerではなくてaddSublayerしてLabel等のオブジェクトが見えなくなるのはありがちな気がする
*insertSublayerについてはよくわかってなかったけど、下の画像をみてなんとなく理解した.UIViewは必ずCALayerを持っていて、CALayerはその下にさらにsublayerを持つことができる。UIButtonやUILabelといったオブジェクトはaddSubされるものなのでレイヤー的にはSublayerの下にあるみたい。だからaddSublayerすることで背景色が変わったように見えるんだと思う

image.png
参考: https://thoughtbot.com/blog/building-ios-interfaces-custom-button

シュミレーターで確認すると無事グラデーションがかかって表示された

1画面目 2画面目 3画面目
Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-29 at 16.33.42.png Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-29 at 16.33.44.png Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-29 at 16.33.48.png

 改善

毎回CAGradientLayerインスタンスを生成するコードを書いて、毎回gradientLayerのフレーム決めるコード書いて、毎回addSublayerするコードを書くのは面倒なので
表示する各画面をcontentsVCsみたいなUIViewControllerの配列にあらかじめ入れておいてforEachで回して設定するやり方もできると思います

contentsVCs.forEach {
    let gradientLayer = CAGradientLayer.layerForView()
    gradientLayer.frame = $0.view.frame
    $0.view.layer.insertSublayer(gradientLayer, at: 0)
}

ただこのやり方も最初のやり方もそうですが、親であるTutorialContainerViewControllerがこねくりましたり、TutorialContainerViewController内で生成された各VCのプロパティに設定したりとTutorialContainerViewController内でViewのグラデーションの設定を行なっています
でも実際はViewの描画はView自体の責務であり、ViewControllerは知る必要がありません
なので、こういう時はカスタムViewを作ってそこでグラデーションをかける。親であるTutorialContainerViewControllerではただそのViewを使うだけにした方がいいかと思うので修正します
まずは先ほど追加したコードの削除
新しくUIViewのカスタムクラスとしてGradationViewを作成

import UIKit

class GradationView: UIView {
    // オートレイアウトでレイアウトが決まってからgradientLayerのフレームの位置・サイズを決めないとgradation.frameの各値が0になるからlayoutSubview内で設定する必要がある
    override func layoutSubviews() {
        super.layoutSubviews()
        let gradientLayer = CAGradientLayer.layerForView()
        gradientLayer.frame = self.frame
        self.layer.insertSublayer(gradientLayer, at: 0)
    }
}

あとはこのカスタムViewのStoryboard上でFirstVC,SecondVC,ThirdVCのそれぞれのViewに設定すれば完了
スクリーンショット 2020-03-29 16.42.09.png
*SecondVC, ThirdVCにも同じように設定する

追記

storyboardContainerViewControllerで新しいViewを作ってその上にボタンをaddSubviewするように修正しました

参考

https://qiita.com/Gutenberg0x/items/ac737fb3305da3389bf9
https://thoughtbot.com/blog/building-ios-interfaces-custom-button

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

Swift MkMapViewで地図アプリ作成してみた(21)- 周辺検索した地点をセミモーダルビューに表示する

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

周辺検索した地点をセミモーダルビューに表示する

これを作ります

googleMapみたいなモーダルビューを作成します。
modal.png CustomFloatingPanelLayout2.png CustomFloatingPanelLayout3.png

FloatingPanelをインストールする

swift単体では実装できません。
簡単な方法を探した結果、CocoaPodsで'FloatingPanel'をインストールすることにしました。
ターミナルから下記を実行してインストールする。

pod 'FloatingPanel'

モーダルビューのデザインパターンを設定する

FloatingPanelLayoutを継承したクラスを作成し、モーダルビューのデザインパターンを設定する。
insetForで、フリックした時のモーダルビューの表示領域を設定する。

import Foundation
import FloatingPanel

class CustomFloatingPanelLayout: FloatingPanelLayout {
    // セミモーダルビューの初期位置
    var initialPosition: FloatingPanelPosition {
        return .half
    }

    var topInteractionBuffer: CGFloat {
        return 0.0
    }

    var bottomInteractionBuffer: CGFloat {
        return 0.0
    }

    // セミモーダルビューの各表示パターンの高さを決定するためのInset
    func insetFor(position: FloatingPanelPosition) -> CGFloat? {
        var ret: CGFloat!
        switch position {
        case .full:
            ret = nil//56.0
        case .half:
            ret = 262.0
        case .tip:
            ret = 75.0
        default:
            ret = nil
        }
        return ret
    }

    // セミモーダルビューの背景Viewの透明度
    func backdropAlphaFor(position: FloatingPanelPosition) -> CGFloat {
        return 0.0
    }
}

作成中ですので、記事を後日更新します。

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

ARKit 3.5のScene Reconstructionサンプルのコードを読む

ARKit 3.5とLiDAR搭載の新型iPad Proが出ましたね。これらを試せるAppleの公式サンプル「Visualizing and Interacting with a Reconstructed Scene」のソースコードを読んでみたメモです。

どんな感じのサンプルかはこちらのツイートの動画がよくわかります。

Scene Understandingの設定

ARView.Environment.SceneUnderstanding構造体のoptions(型はARView.Environment.SceneUnderstanding.Options)に.occlusion.physicsを追加することでオクルージョンと物理計算を有効化。

arView.environment.sceneUnderstanding.options = []

// Turn on occlusion from the scene reconstruction's mesh.
arView.environment.sceneUnderstanding.options.insert(.occlusion)

// Turn on physics for the scene reconstruction's mesh.
arView.environment.sceneUnderstanding.options.insert(.physics)

SceneUnderstandingオブジェクトはARViewenvironmentプロパティ(型はARView.Environment)が持つsceneUnderstandingプロパティにセットする。

https://developer.apple.com/documentation/realitykit/arview/environment/sceneunderstanding/options

デバッグ用にメッシュを可視化

公式サンプルではメッシュが深度に応じて色分けされた見事なビジュアライゼーションが実現されている。

どうやっているのかというと、ARViewdebugOptionsプロパティに.showSceneUnderstandingを追加するだけ。

arView.debugOptions.insert(.showSceneUnderstanding)

https://developer.apple.com/documentation/realitykit/arview/debugoptions/3521397-showsceneunderstanding

ちなみにARSCNViewではこれができない。

Scene Reconstructionの有効化

ARViewの自動設定をオフにし、ARWorldTrackingConfigurationsceneReconstructionプロパティに.meshWithClassificationを指定する。デフォルト以外に.none.mesh(メッシュのClassificationを行わない)がある。

arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.sceneReconstruction = .meshWithClassification

configuration.environmentTexturing = .automatic
arView.session.run(configuration)

https://developer.apple.com/documentation/arkit/arworldtrackingconfiguration/3521376-scenereconstruction

タップでClassification結果を可視化

Classificationというのはそのメッシュが壁なのか床なのかテーブルなのか、という分類を示す情報で、ARMeshClassificationというenumで定義されている。

public enum ARMeshClassification : Int {
   case none = 0
   case wall = 1
   case floor = 2
   case ceiling = 3
   case table = 4
   case seat = 5
   case window = 6
   case door = 7
}

https://developer.apple.com/documentation/arkit/armeshclassification

公式サンプルでは、画面(ARView)タップでraycastによる(.estimatedPlaneとの)当たり判定を行い、classificationの判定結果をテキスト表示している。

if let result = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .any).first {
   let resultAnchor = AnchorEntity(world: result.worldTransform)
   ...
}

ここらへんのコードが実はおもしろくて、ここまで簡単にScene Reconstructionやメッシュの可視化ができるのでClassificationの結果もARMeshAnchorのプロパティから取り出して終わりでしょ、と思いきや、そんなプロパティはなく、意外とめんどくさい処理をやっている。

https://developer.apple.com/documentation/arkit/armeshanchor

ARMeshAnchorからARMeshClassificationを得る

具体的にはARMeshAnchorgeometryプロパティに入っているARMeshGeometryオブジェクトのclassificationプロパティから取り出しているのだが、これがまだARMeshClassification型ではなくて、ARGeometrySource型なのである。

var vertices: ARGeometrySource

https://developer.apple.com/documentation/arkit/armeshgeometry

で、ARGeometrySourceというのが割とめんどくさいクラスで、そのデータをMTLBufferというMetalのバッファに保持している。

open class ARGeometrySource : NSObject, NSSecureCoding {


   /**
    A Metal buffer containing per-vector data for the source.
    */
   open var buffer: MTLBuffer { get }


   ...
}

https://developer.apple.com/documentation/arkit/argeometrysource

サンプルではこのバッファをGPUではなくCPUでほじくり、とりだした生の数値からARMeshClassificationを初期化している。

extension ARMeshGeometry {
    ...

    /// To get the mesh's classification, the sample app parses the classification's raw data and instantiates an
    /// `ARMeshClassification` object. For efficiency, ARKit stores classifications in a Metal buffer in `ARMeshGeometry`.
    func classificationOf(faceWithIndex index: Int) -> ARMeshClassification {
        guard let classification = classification else { return .none }
        assert(classification.format == MTLVertexFormat.uchar, "Expected one unsigned char (one byte) per classification")
        let classificationPointer = classification.buffer.contents().advanced(by: classification.offset + (classification.stride * index))
        let classificationValue = Int(classificationPointer.assumingMemoryBound(to: CUnsignedChar.self).pointee)
        return ARMeshClassification(rawValue: classificationValue) ?? .none
    }

タップ位置に近いメッシュのfaceを抽出

前項の処理で説明してないことがあって、classificationOf(faceWithIndex:)メソッドではfaceのindexを引数に渡し、そのfaceについてのclassifiation結果を取り出している。

メッシュの中にたくさんある中で、タップした位置に近いfaceを取り出してそのclassification結果を可視化しているわけだ。

その「タップした位置に近い(5cm以内)場所にあるfaceを取り出す」実装はこうなっている。

for anchor in meshAnchors {
    for index in 0..<anchor.geometry.faces.count {
        // Get the center of the face so that we can compare it to the given location.
        let geometricCenterOfFace = anchor.geometry.centerOf(faceWithIndex: index)

        // Convert the face's center to world coordinates.
        var centerLocalTransform = matrix_identity_float4x4
        centerLocalTransform.columns.3 = SIMD4<Float>(geometricCenterOfFace.0, geometricCenterOfFace.1, geometricCenterOfFace.2, 1)
        let centerWorldPosition = (anchor.transform * centerLocalTransform).position

        // We're interested in a classification that is sufficiently close to the given location––within 5 cm.
        let distanceToFace = distance(centerWorldPosition, location)
        if distanceToFace <= 0.05 {
            ...
        }
    }
}

この計算をするためには、ARMeshGeometryverticesプロパティとfacesプロパティを使用する必要があり、これまたARGeometrySource型。Metalバッファから数値を読み出すために、ARMeshGeometryのextensionとして次のようなメソッドが実装されている。

extension ARMeshGeometry {
    func vertex(at index: UInt32) -> (Float, Float, Float) {
        assert(vertices.format == MTLVertexFormat.float3, "Expected three floats (twelve bytes) per vertex.")
        let vertexPointer = vertices.buffer.contents().advanced(by: vertices.offset + (vertices.stride * Int(index)))
        let vertex = vertexPointer.assumingMemoryBound(to: (Float, Float, Float).self).pointee
        return vertex
    }

    ...

    func vertexIndicesOf(faceWithIndex faceIndex: Int) -> [UInt32] {
        assert(faces.bytesPerIndex == MemoryLayout<UInt32>.size, "Expected one UInt32 (four bytes) per vertex index")
        let vertexCountPerFace = faces.indexCountPerPrimitive
        let vertexIndicesPointer = faces.buffer.contents()
        var vertexIndices = [UInt32]()
        vertexIndices.reserveCapacity(vertexCountPerFace)
        for vertexOffset in 0..<vertexCountPerFace {
            let vertexIndexPointer = vertexIndicesPointer.advanced(by: (faceIndex * vertexCountPerFace + vertexOffset) * MemoryLayout<UInt32>.size)
            vertexIndices.append(vertexIndexPointer.assumingMemoryBound(to: UInt32.self).pointee)
        }
        return vertexIndices
    }

    func verticesOf(faceWithIndex index: Int) -> [(Float, Float, Float)] {
        let vertexIndices = vertexIndicesOf(faceWithIndex: index)
        let vertices = vertexIndices.map { vertex(at: $0) }
        return vertices
    }

    func centerOf(faceWithIndex index: Int) -> (Float, Float, Float) {
        let vertices = verticesOf(faceWithIndex: index)
        let sum = vertices.reduce((0, 0, 0)) { ($0.0 + $1.0, $0.1 + $1.1, $0.2 + $1.2) }
        let geometricCenter = (sum.0 / 3, sum.1 / 3, sum.2 / 3)
        return geometricCenter
    }
}

この実装はたぶんめちゃくちゃ多くの人がコピーして使うと思う。サンプルの鑑。

関連

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

[悲報]Xcode 11.4 でナビゲーションバーのタイトルの色が黒に固定される(*条件あり)

それはXcodeをアップデートした直後のことでした・・・
俺氏「ん?!ナビゲーションタイトルの文字が黒にしかならんやんけ?」
スクリーンショット 2020-03-29 2.11.31.png

発生条件

  • navigation controllerのBar Tintを変更している場合

上記場合に、Title Colorを変更しても、タイトルの色は黒固定になっちゃいます。

Bar Tintをデフォルトにしとけばタイトルカラーを変更できます。
スクリーンショット 2020-03-29 2.10.29.png

デフォルトカラー以外では変更できないから超不便?

解決法

  1. 多分Xcode11.4のバグ、アップデートを待ちましょう←
  2. Xcode11.3.1にダウングレードする(多分これがいい(小並感))
  3. おーばーふろーで無理やり変更してる人もいた

 

 それではみなさん楽しいバグライフを!?

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

【悲報】Xcode 11.4 でナビゲーションバーのタイトルの色が黒に固定される(*条件あり)

それはXcodeをアップデートした直後のことでした・・・
俺氏「ん?!ナビゲーションタイトルの文字が黒にしかならんやんけ?」
スクリーンショット 2020-03-29 2.11.31.png

発生条件

  • storyboardでnavigation controllerのBar Tintを変更している場合

上記場合に、Title Colorを変更しても、タイトルの色は黒固定になっちゃいます。

Bar Tintをデフォルトにしとけばタイトルカラーを変更できます。
スクリーンショット 2020-03-29 2.10.29.png

デフォルトカラー以外では変更できないから超不便?

解決法

  1. 多分Xcode11.4のバグ、アップデートを待ちましょう←
  2. Xcode11.3.1にダウングレードする(多分これがいい(小並感))
  3. おーばーふろーで無理やり変更してる人もいた

 

 それではみなさん楽しいバグライフを!?

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

Swift MkMapViewで地図アプリ作成してみた(20)- 受信精度の悪いGPSデータを間引く

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

受信精度の悪いGPSデータを間引く

GPSの受信鮮度

あまり該当するケースは存在しないが、GPS鮮度がtimeInterval秒過ぎていた場合、読み捨てる。

    if timeInterval <= Int(abs(locations.last!.timestamp.timeIntervalSinceNow)) {
        return
    }

水平誤差

水平誤差がaccuracyメートルを越えた場合、読み捨てる。

    if accuracy < Int(locations.last!.horizontalAccuracy) {
        return
    }

緯度経度が無効

水平誤差が負の値の場合、緯度経度が無効なので読み捨てる。

    if 0 > locations.last!.horizontalAccuracy {
        return
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】【まとめ】iOSアプリを作ろうとしている方がまず見るべきQiitaの記事をまとめてみました【Swift】

iOSアプリのためのまとめ記事が欲しかった

iOSエンジニアになって早8ヶ月。

最近ようやく慣れてきたものの、初めは実装するのにとっても時間がかかりました。

というのは、実装についてまとまったオールインワンの記事があまりなかったことがもしかしたら原因なのかもと思っています。(自分でもっと勉強しろって話かもしれないですが…)

なので、今回は、自身が開発をしていく上で参考にした記事をまとめてみたいと思います。

これからiOS開発をする方に参考になればと思っています。

参考になるかもしれないソースコード

この記事を作っていくにあたって、以下の記事を参考にして実装したものを載せています。

これを見ながら記事と共に勉強していただければ幸いです。

https://github.com/taichi6930/iOSAppBase/tree/master

早速見るべき記事

ライフサイクル

iOSアプリがどのように動いているか、といった感じです。
作る際にここがわかっていないと「??」となりまくってしまいます(自分がそうでした)。

アプリ作成前に一度読んでおくと良いと思います。

iOSアプリのライフサイクル
https://qiita.com/KenNagami/items/766d5f95940c76a8c3cd

UIViewControllerのライフサイクル
https://qiita.com/motokiee/items/0ca628b4cc74c8c5599d

AppDelegate,UIViewController,UIViewのライフサイクル/iOS/Swift
https://qiita.com/kayo311/items/4710c4ac02a191652a96

iosアプリ ViewControllerのライフサイクル
https://qiita.com/usutan/items/71760df10e8523166bab

ページ遷移について

ページ間での移動についてです。
ここも全然分からなかったので、最初に読んでおいた方がいいと思います。
(ページ遷移しないと、ただのワンページアプリになってしまうのでw)

同じ/異なるStoryboardでの画面遷移
https://qiita.com/kedarui/items/97b5cc1410d9c61933d5

Swiftのページ遷移【Navigation Controller】
https://qiita.com/ryu1_f/items/4a0e452e94c9ba609220

TableView

画像のようにcellが縦方向に続いていくViewです。

自身のソースコードではセルの生成とタップ時の処理を記載しました。
(例が少ないので足していく予定です…)

スクリーンショット 2020-03-29 13.27.16.png

参考になった方々の記事です↓

SwiftでTableViewを使ってみよう
https://qiita.com/pe-ta/items/cafa8e20029047993025

UITableViewの使い方 【Swift4.2 , Xcode10】
https://qiita.com/abouch/items/3617ce37c4dd86932365

UITableViewのデリゲートメソッドまとめ
https://qiita.com/kagemiku/items/22b74010365723c5c4fe

CollectionView

画像のようにcellが横方向に続いていくViewです。

自身のソースコードでは、セルの生成とタップ時の処理を記載しています。

TableViewと同様に作成すれば良かったので、まずはTableViewを作成してみてください!

スクリーンショット 2020-03-29 13.30.56.png

WebView

アプリ内でWebページが見れるViewです。

今回はWKWebViewを使用して作成しました。

cookie処理などは入れていないので、今後行っていければいいなと思っています。

スクリーンショット 2020-03-29 13.34.23.png

参考になった方々の記事です↓

WebKit View(WKWebView) を実装
https://qiita.com/MdRk/items/34912e7ba43568f15905

WKWebViewについてのまとめ
https://qiita.com/s_emoto/items/dc3d61626155f5cf83e7
(まだ実装出来ていないので今後していきたい…)

生体認証

端末によってはFaceIDやTouchIDでログインすることができるアプリがあります。

端末によってtouchIDだったりFaceIDだったり変化させています。

そこまで難しくなかったので実装してみました。

スクリーンショット 2020-03-29 13.50.56.png

参考になった方の記事↓

【iOS 11】LocalAuthenticationでFace IDとTouch IDの認証を実装する
https://qiita.com/MilanistaDev/items/b0cd432290d18f336766

Alert表示

画像を見れば明らかですがアラート表示についてです。

スクリーンショット 2020-03-29 13.51.14.png

参考になった方々の記事↓

【Swift】アラートを表示する(Alert/ActionSheet)
https://qiita.com/funacchi/items/b76e62eb82fc8d788da5

最後に

この記事は自分のために作ったみたいなところがあります。
今後もメモのような感じで残せたらと思います。

何か不足だったり指摘だったり、こんなの載せて欲しいというのがあったら是非気軽に言っていただきたいです!

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

SwiftのArrayとList(Realm)の比較と変換方法

はじめに

メモアプリに複数のタグを紐付ける機能を実装していた際に、
SwiftのArrayとListという型がそれぞれ別の役割であることに気づかなかったので、
それぞれの役割と変換方法について簡潔にまとめようと思います!

環境

  • xcode 11.3.1
  • swift 5.1.3

ArrayとListの違い

ArrayもListも複数要素を扱うという点では共通ですが
ArrayはSwiftで配列として使われる型です。
Listは「RealmSwift」というRealmを扱うためのライブラリで扱える型です。

List型とArray型を使い分ける

今回はViewControllerで List<Tag>を扱おうとしたところ、
「Listとなんて型は定義されていないよ!」
と怒られたので、 Array<Tag>に変更しました。

そうしたら今度は、Model→Realmに変換できず nilになって、
保存されないという事象が発生しました!
nilになってもエラーにはならずただDBに値が入らないので要注意

なので、
ロジックは Array<Tag>にキャスト
データ格納時は List<Tag>に変換するという実装で対応しました。

イメージ図
ListとArrayの変換.jpg

Array<=>Listの変換方法

Array→List

ArrayからListに変換するときは、Modelで List<Object>を指定しておき、
append(objectsIn:)を使えば ListとしてRealmに入れることできました。

Model
import Foundation
import RealmSwift

class Memo: RObject {
    @objc dynamic var title : String = ""
    @objc dynamic var content : String = ""
    @objc dynamic var createdDate : Date = NSDate() as Date
    @objc dynamic var updatedDate : Date = NSDate() as Date

    var tags = List<Tag>() // ここをList型にする
}

class Tag: Object {
    @objc dynamic var tagName = ""

    var memos: LinkingObjects<Memo> {
        return LinkingObjects(fromType: Memo.self, property: "tags")
    }
}

ViewController
import UIKit
import RealmSwift


class MemoTableViewController: UITableViewController {
    // 一部抜粋
            let memoModel = Memo()
            let realm = try! Realm()
            try! realm.write {
                memoModel.title = memo
                memoModel.content = memo
                memoModel.tags.removeAll()
                memoModel.tags.append(objectsIn: tags) // Listに追加                
    // 一部省略
            }
}

List→Array

ListをArrayに変化するときは Array()で変換してあげれば大丈夫です!
また、 Array型に入れるときは append(contentsOf:)を用いるようです。

ListToArray
let testList = List<Tag>() //List型
var testArray = Array<Any>() // Array型

testArray.append(contentsOf: Array(testList)) // Array()でListを変換

まとめ

 今回はListとArrayを変換しましたが、
RealmSwiftをimportし、使い回すという方法もありかもしれないです。

 Javaだと、配列もListも存在するので、Swiftも似た感じかと思い
ListがRealmSwiftの型ということに気づくのに時間かかってしまったので、
javaからSwift入る人は注意してください^^;

 ArrayでRealmに入れようとするとnilが入り、処理が落ちずに
保存に失敗するのも注意ポイントでした!

*実装ベースで気づいたことをまとめたので、もし間違いや指摘がある場合は
確認・修正するのでコメントでお知らせくださいmm

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

PageViewControllerを使ってチュートリアル画面を実装する

作ろうとしているもののイメージ

https://qiita.com/Motchy_1204/items/5d4c281d7cc1c3c26365

スワイプではなくてボタンをタップして別の画面を表示する

少し違うのはここでは色を切り替えているだけだけど
- 今回は画面を切り替える
- 2画面目では画面内のボタンをタップしてページを切り替える

今回の仕組み

各画面ごとのViewControllerを用意してボタンタップすることでそのViewControllerを切り替える
そのためViewControllerにPageViewControllerを持たせる必要があるのでViewControllerにViewControllerを持たせるためのContainerViewを持たせる
次に各画面用のViewControllerを用意して、PageViewControllerによって画面遷移を行う

ContainerViewとは

ViewControllerの中にViewControllerを配置することができるもの。
xibじゃなくてViewControllerを置きたい!という時に利用する。

PageViewControllerとは

スワイプで用意したViewControllerに画面遷移ができる
画面遷移時にアニメーションを付けられる

手順1: UIPageViewControlerをViewControllerに持たせる

StoryboardからViewControllerにUIPageViewControlerを持たせます

   流れ
    - ContainerViewとしてUIViewをViewControllerに持たせる
    - UIPageViewControllerを作る
    - ContainerViewとUIPageViewControllerを繋ぎEmbedを選択する
UIPageViewControllerを追加 Embedで繋ぐ 完成
スクリーンショット 2020-03-29 10.08.03.png スクリーンショット 2020-03-29 10.10.03.png スクリーンショット 2020-03-29 10.10.11.png

コードでこのPageViewControllerを取得していきます
childrenでそのViewControllerが持つViewControllerの配列を取得できるのでchildren.firstで今回ViewControllerがもつUIPageViewControllerを取得することができます

An array of view controllers that are children of the current view controller.
https://developer.apple.com/documentation/uikit/uiviewcontroller/1621452-children

スクリーンショット 2020-03-29 10.21.38.png

このViewControllerは各画面のコンテナになるので名前を変えて、それと今後どこかでこのViewControllerを取得する可能性もあるのでコードで取得できるようにしておきます

名前を変更 コード取得できるように設定
スクリーンショット 2020-03-29 10.56.42.png スクリーンショット 2020-03-29 10.56.33.png

チュートリアルで表示する画面の作成

各画面用のViewControllerをファイルとstoryboard両方に作成
TutorialCotainerViewControllerと同じように、storyboardで作成したViewControllerをコードで取得できるようにStoryboard IDを設定しておきます

FirstVCコード FirstVC Storyboard
スクリーンショット 2020-03-29 11.36.41.png スクリーンショット 2020-03-29 11.37.19.png
SecondVCコード SecondVC Storyboard
スクリーンショット 2020-03-29 11.39.29.png スクリーンショット 2020-03-29 11.37.24.png
ThirdVCコード ThirdVC Storyboard
スクリーンショット 2020-03-29 11.36.52.png スクリーンショット 2020-03-29 11.37.29.png

完成

完成したコードとstoryboardです

SecondVCの時はNextボタンを非活性にして「次へ」ボタンタップでThirdVCへ遷移するようにしています
遷移自体はTutorialContainerVCが担うので、SecondVCでボタンがタップされたらクロージャーでTutorialVCへ通知して遷移させるようにしています

| Storyboard | TutorialContainerVC コード | FirstVC コード | SecondVC コード | ThirdVC コード |
|-------|
| スクリーンショット 2020-03-29 11.41.31.png |

import UIKit

class TutorialContainerViewController: UIViewController {
    enum Page:Int {
        case first
        case second
        case third
    }

    // MARK: - IBOutlet
    @IBOutlet weak var nextButton: UIButton!

    // MARK: - private
    var currentPage = Page.first
    private lazy var pageViewController: UIPageViewController = {
        // このViewControllerが持つpageViewControllerを取得
        let pageVC = children.first as! UIPageViewController
        return pageVC
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        pageViewController.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    @IBAction func tappedButton(_ sender: UIButton) {
        if currentPage == .first {
            pageViewController.setViewControllers([getSecond()], direction: .forward, animated: true, completion: nil)
            currentPage = .second
            nextButton.isEnabled = false
            return
        }
    }
}

extension TutorialContainerViewController {
    private func getFirst() -> FirstViewController {
        return storyboard!.instantiateViewController(identifier: "FirstViewController")
    }

    private func getSecond() -> SecondViewController {
        let vc: SecondViewController = storyboard!.instantiateViewController(identifier: "SecondViewController")
        // SecondViewControllerでボタンがタップされた通知が飛んでくる
        // {}の中身はその通知が飛んできた時に呼ばれる内容だから、getSecond()が呼ばれても{}の中身は呼ばれない
        vc.selectedButtonToThird = {
            self.nextButton.isEnabled = true
            self.pageViewController.setViewControllers([self.getThird()], direction: .forward, animated: true, completion: nil)
        }
        return vc
    }

    private func getThird() -> ThirdViewController {
        return storyboard!.instantiateViewController(identifier: "ThirdViewController")
    }
}
import UIKit

class FirstViewController: UIViewController {

}
import UIKit

class SecondViewController: UIViewController {

    @IBOutlet weak var nextButtonToThird: UIButton!

    // クロージャーを設定
    var selectedButtonToThird: (() -> ())?

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    @IBAction func tappedToThird(_ sender: UIButton) {
        // ボタンがタップされたらクロージャーで通知がいくようにする
        selectedButtonToThird?()
    }
}
import UIKit

class ThirdViewController: UIViewController {

}

おまけ

ページのアニメーションを横にスクロールするようなアニメーションにする

デフォルトではページをめくるようなアニメーションになっています
これを横にスクロールするようなアニメーションにする場合にはstoryboardでPageViewcontrollerを選択して
Transition StyleScrollに変更することで実現できます

スクリーンショット 2020-03-29 11.58.23.png

RootViewControllerを切り替える

チュートリアル画面を実装するようなアプリの場合初回起動判定で
初回と判定されればアプリ起動時にチュートリアル画面を表示
初回でないと判定されればトップ画面をアプリ起動時に表示とすることが多いです

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = makeRootVC()
        window?.makeKeyAndVisible()
        return true
    }

    // 初回起動のところは自分で判定処理を入れてください
    func makeRootVC() -> UIViewController {
        if 初回起動 {
            let tutorialContainerVC = storyboard?.instantiateViewController(identifier: "TutorialContainerViewController")
            // TutorialContainerViewControllerでdidSelectButtonToTopが呼ばれたらここに通知がきて {} 内が実行されrootViewControllerが変わることで画面が切り替わる
            tutorialContainerVC.didSelectButtonToTop = {
                self.window?.rootViewController = トップ画面のVC
            }
            return tutorialContainerVC
        }
        // 初回起動ではない場合
        return トップ画面のVC
    }
}
import UIKit

class TutorialContainerViewController: UIViewController {
    enum Page:Int {
        case first
        case second
        case third
    }

    // MARK: - IBOutlet
    @IBOutlet weak var nextButton: UIButton!

    // MARK: - internal
    var didSelectButtonToHome: (() -> Void)? ← NEW

    // MARK: - private
    var currentPage = Page.first
    private lazy var pageViewController: UIPageViewController = {
        // このViewControllerが持つpageViewControllerを取得
        let pageVC = children.first as! UIPageViewController
        return pageVC
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        pageViewController.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    @IBAction func tappedButton(_ sender: UIButton) {
        if currentPage == .first {
            pageViewController.setViewControllers([getSecond()], direction: .forward, animated: true, completion: nil)
            currentPage = .second
            nextButton.isEnabled = false
            return
        }

        if currentPage == .third {
            didSelectButtonToHome?()  ← NEW
        }
    }
}

extension TutorialContainerViewController {
    private func getFirst() -> FirstViewController {
        return storyboard!.instantiateViewController(identifier: "FirstViewController")
    }

    private func getSecond() -> SecondViewController {
        let vc: SecondViewController = storyboard!.instantiateViewController(identifier: "SecondViewController")
        // SecondViewControllerでボタンがタップされた通知が飛んでくる
        // {}の中身はその通知が飛んできた時に呼ばれる内容だから、getSecond()が呼ばれても{}の中身は呼ばれない
        vc.selectedButtonToThird = {
            self.nextButton.isEnabled = true
            self.pageViewController.setViewControllers([self.getThird()], direction: .forward, animated: true, completion: nil)
        }
        return vc
    }

    private func getThird() -> ThirdViewController {
        return storyboard!.instantiateViewController(identifier: "ThirdViewController")
    }
}

ナビゲーションコントローラーを使っている場合

*StoryboardでTutorialContainerViewControllerもトップ画面VCもNavigationControllerと紐づいている、NavigationControllerを持っている場合

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.rootViewController = UINavigationController(rootViewController: makeRootVC)
        window?.makeKeyAndVisible()
        return true
    }

    // 初回起動のところは自分で判定処理を入れてください
    func makeRootVC() -> UIViewController {
        if 初回起動 {
            return storyboard?.instantiateViewController(identifier: "TutorialContainerViewController")
        }
        // 初回起動ではない場合
        return トップ画面のVC
    }
}
import UIKit

class TutorialContainerViewController: UIViewController {
    enum Page:Int {
        case first
        case second
        case third
    }

    // MARK: - IBOutlet
    @IBOutlet weak var nextButton: UIButton!

    // MARK: - private
    var currentPage = Page.first
    private lazy var pageViewController: UIPageViewController = {
        // このViewControllerが持つpageViewControllerを取得
        let pageVC = children.first as! UIPageViewController
        return pageVC
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        pageViewController.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    @IBAction func tappedButton(_ sender: UIButton) {
        if currentPage == .first {
            pageViewController.setViewControllers([getSecond()], direction: .forward, animated: true, completion: nil)
            currentPage = .second
            nextButton.isEnabled = false
            return
        }

        if currentPage == .third {
            // NavigationControllerによって管理されているViewControllersを引数に渡した特定のViewControllerに置き換える(リセットされる)
            navigationController?.setViewControllers([トップ画面VC], animated: true)
        }
    }
}

extension TutorialContainerViewController {
    private func getFirst() -> FirstViewController {
        return storyboard!.instantiateViewController(identifier: "FirstViewController")
    }

    private func getSecond() -> SecondViewController {
        let vc: SecondViewController = storyboard!.instantiateViewController(identifier: "SecondViewController")
        // SecondViewControllerでボタンがタップされた通知が飛んでくる
        // {}の中身はその通知が飛んできた時に呼ばれる内容だから、getSecond()が呼ばれても{}の中身は呼ばれない
        vc.selectedButtonToThird = {
            self.nextButton.isEnabled = true
            self.pageViewController.setViewControllers([self.getThird()], direction: .forward, animated: true, completion: nil)
        }
        return vc
    }

    private func getThird() -> ThirdViewController {
        return storyboard!.instantiateViewController(identifier: "ThirdViewController")
    }
}

間違いがあったらすみません

参考

https://qiita.com/Motchy_1204/items/5d4c281d7cc1c3c26365
https://qiita.com/ninoko1995/items/5498c1e821ba3e94bdd6
https://qiita.com/Kyoya1123/items/588206357cf9c6492047
https://qiita.com/koji-nishida/items/b901f47255ddcfbaa927

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

Swift MkMapViewで地図アプリ作成してみた(19)- UserDefaultsでデータをバックアップする

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

UserDefaultsでデータをバックアップする

UserDefaultsとは

ユーザーのデフォルトデータベースへのインターフェイスで、アプリを終了しても消えない永続的なデータを保存できる。
簡単なので、ユーザの設定データなどちょっとしたデータを保存するのに良い。

データをバックアップするクラスを作成する

データをバックアップするクラスを作成して、UserDefaultsの変数を宣言する。

import Foundation
import UIKit

class UserDataManager: NSObject {
    // UserDefaultsを宣言する
    let userDefaults = UserDefaults.standard

クラス初期化時に、UserDefaultsのデータを初期化する。
maxSpeedをUserDefaultsで管理するものとする。

// クラスの初期化
override init() {
    userDefaults.register(defaults: ["maxSpeed": 0.0])
}

UserDefaultsからデータを読み込む

UserDefaultsから変数speedにデータを読み込む。

// 保存したデータを読み込む
func roadData() {
    speed = userDefaults.object(forKey: "maxSpeed") as! Double
}

UserDefaultsにデータを書き込む

UserDefaultsのmaxSpeedにデータを書き込む。

// 保存したデータを読み込む
func saveData(_ speed: Double) {
    userDefaults.set(speed, forKey: "maxSpeed")
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift MkMapViewで地図アプリ作成してみた(18)- 位置情報から速度、平均速度、最高速度を求める

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

前提条件

(02)- 現在位置を取得するで取得した位置情報から、速度、平均速度、最高速度を求める。

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
    // この関数で受信した位置情報から、速度、平均速度、最高速度を求める。
    let lonStr = (locations.last?.coordinate.longitude.description)! // 経度
    let latStr = (locations.last?.coordinate.latitude.description)!  // 緯度
}

位置情報から速度、平均速度、最高速度を求める

速度を求める

locations.last!.speedで秒速が取得できる。
時速に変換したい場合は、以下の通りとなる。

秒速を時速に変換する
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
    // 秒速を少数第2位の時速に変換
    let speed: Double = floor((locations.last!.speed * 3.6)*100)/100}

平均速度を求める

速度の合計値を受信回数で除算するとで、平均速度を求めることができる。

平均速度を求める
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
    // 平均速度の更新
    self.avgSumSpeed += locations.last!.speed
    self.avgSumCount += 1
    let tmpAvgSpeed = floor(((self.avgSumSpeed / Double(self.avgSumCount)) * 3.6)*100)/100

最高速度を求める

単純に過去の最高速度を保存する。

最高速度を求める
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
    // 秒速を少数第2位の時速に変換
    let speed: Double = floor((locations.last!.speed * 3.6)*100)/100}
    // 最高速度
    if speed > dMaxSpeed {
        dMaxSpeed = speed
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift MkMapViewで地図アプリ作成してみた(17)- バックグラウンド・スリープ中でも位置情報を受信する

記事一覧

Swift MkMapViewで地図アプリ作成してみた(記事一覧)

バックグラウンド・スリープ中でも位置情報を受信する

CLLocationManagerのプロパティを設定する

allowsBackgroundLocationUpdatesプロパティをtrueにすれば、
バックグラウンドでGPSを受信することができる。

プロパティを設定する
locManager = CLLocationManager()
locManager.allowsBackgroundLocationUpdates = true

バックグラウンドにした時のデバイスの画面

正しく受信できれば、左上に位置情報を受信しているアイコンが表示される。
スクリーンショット 2020-03-29 9.31.12.png

位置情報を受信する関数

バックグラウンド・スリープ中も、下記の関数で位置情報を受信することができる。
位置情報の取得方法は、(02)- 現在位置を取得するを参照してください。

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {
    let lonStr = (locations.last?.coordinate.longitude.description)!
    let latStr = (locations.last?.coordinate.latitude.description)!
}

参考文書

Apple Documentation > allowsbackgroundlocationupdates

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