20201115のiOSに関する記事は9件です。

iOS14からIDFA取得がオプトイン形式になる理由と今後予想される動き

iOS14でも様々な機能で仕様変更がありましたね。
デフォルトブラウザを選択できるようになったので、Chromeに変更してみました。

そんなある日、ふとあることに気がつきます。
開発しているアプリでWebViewが開かないじゃないですか...!?

調査をしたところ、LSApplicationQueriesSchemesにURLスキームを追加していないのが原因でした。
そうしないとUIApplicationcanOpenURLは常にfalseを返す仕様になっていました。

you must declare the URL schemes you pass to this method by adding the LSApplicationQueriesSchemes key to your app's Info.plist file. This method always returns false for undeclared schemes, whether or not an appropriate app is installed.

引用:canOpenURL(_:) | Apple Developer Documentation

iOS14からIDFA取得をオプトイン形式に...!?

上記の調査の過程で、iOS14からのIDFA取得が世間で騒がれていることを知りました。
騒がれている理由を調べると、iOS14からIDFAの取得をオプトイン形式に変更する必要があるからでした。

With iOS 14, iPadOS 14, and tvOS 14, you will need to receive the user’s permission through the AppTrackingTransparency framework to track them or access their device’s advertising identifier.

User Privacy and Data Use - App Store - Apple Developer より引用

ただ私はIDFAが何なのかも全くわかっていませんし、なぜ騒がれているのかもわかりませんでした。
そこでまずは全容を把握するために、基本的なところから地道に調査しました。

そもそもIDFAってなに?

Identifier for AdvertisersIDFA)は、iOS端末にランダムに割り当てる識別子とのこと。
広告主はIDFAを使って広告のエンゲージメントやアプリ内の行動を計測することできるようです。

The Identifier for Advertisers (IDFA) is a random device identifier assigned by Apple to a user’s device. Advertisers use this to track data so they can deliver customized advertising. The IDFA is used for tracking and identifying a user (without revealing personal information).

引用: What is an IDFA? Find out what an IDFA is here | Adjust | Adjust

この説明を読んで気になったのは、IDFAを使って広告からのインストールなどを計測する仕組みです。
WWDC20での説明がわかりやすかったので簡単にまとめると、

  • 広告タップ時に、広告のキャンペーンIDとタップしたユーザを識別できるIDFAを広告ネットワークに送信
  • アプリインストールして起動した時に、同じIDを広告ネットワークに送信
  • 送信された情報を広告ネットワークで突き合わせて、どの広告からインストールされたかを把握する

Screen Shot 2020-11-15 at 20.44.01.png

引用:Build trust through better privacy - WWDC 2020 - Videos - Apple Developer
※ 動画の27分あたりからユーザトラッキングに関する説明が始まります。

IDFA取得のこれまでとこれから

ここまでの調査で、IDFAを使用することでユーザの行動を計測できることがわかりました。
では、IDFA取得の仕様変更がなぜ世間で騒がれているのでしょうか?
まずはIDFA取得のこれまでの仕様とこれからの仕様について調べてみました。

iOS13以前の仕様

iOS13までは、ユーザの同意を得なくてもデフォルトでIDFAを取得できていました
ユーザがIDFAの取得を拒否したい場合は設定から変更できる、いわゆるオプトアウト形式だったようですね。

iOS14以降の仕様

iOS14からは、ユーザの同意を得ないとIDFAを取得できなくなるようです。
IDFAを取得する前に、以下のようなプロンプトを表示して、ユーザに同意してもらう必要があるようです。
iOS14で新しく提供されるAppTrackingTransparency frameworkを使えば実装できるようです。
もしユーザが拒否した場合、IDFAはすべて 0 になるという仕様になっていました。

Screen Shot 2020-11-15 at 21.58.29.png

Screen Shot 2020-11-15 at 22.04.45.png

引用:Build trust through better privacy - WWDC 2020 - Videos - Apple Developer
※ 動画の27分あたりからユーザトラッキングに関する説明が始まります。

オプトイン形式に変わることの影響

ではオプトイン形式に変わることで、どういう影響があるのでしょうか?

だいたい予想ができますが、IDFAの取得を拒否するユーザは増えそうですね。
自分がユーザでも許可するメリットがわからないので、拒否してしまいそうです。

拒否するユーザが増えると、IDFAを使用した広告効果の計測が難しくなります。
さらにターゲティング広告の配信などもできなくなるため、広告収益の低下にも繋がるようです。

参考:iOS 14、ターゲティング型広告への影響は? 対応方法と今後の広告業界について【WWDC 2020】 – fluct magazine

ここまで調査して、やっとことの重大さがわかってきました。
広告業界にとって、この変更による影響はかなり大きいように思えますね。

なぜオプトイン形式にする必要があるの?

でもどうしてAppleは世間が騒ぐような変更を行うのでしょうか?
調査してわかったことですが、AppleのCMにもヒントはあったようです。

Apple Privacy CM

引用:プライバシー。これがiPhone。 ― 共有すべきでないこと - YouTube

Appleのプライバシー機能の強化戦略

Appleは最近プライバシー機能の強化に力を入れているようです。
よくよく調べてみるとiOS14では、プライバシー関連で複数の機能に変更がありました。(下記は一部です)

  • 写真:端末内の写真にアクセスする際に一部の写真のみ読み書きができるパーミッションが増えた
  • 位置情報:アプリに渡す位置情報を大幅に曖昧にするプライバシー保護機能が増えた
  • 連絡先:アプリが連絡先リストの全体ではなく、特定の必要な連絡先のみへのアクセスに限定されるようになる

Screen Shot 2020-11-15 at 22.22.33.png

引用:Build trust through better privacy - WWDC 2020 - Videos - Apple Developer
※ 動画の4分30秒あたりから写真・位置情報・連絡先に関する説明が始まります。

IDFAの取得許可もプライバシーを守るため

IDFAをオプトイン形式にしたのも、プライバシーを守るためのようです。
データブローカーがIDFAと個人情報を突き合わせてプロフィールを作成することもできてしまうらしいです。
そういったことからユーザを守るために、IDFAの取得をオプトイン形式にしているのですね。納得。

注目が集まり始めるSKAdNetwork

Appleの主張に納得できる一方で、IDFAの取得が拒否されたら計測できないのではという疑問も湧きました。
IDFAの取得が拒否されることは仕方がないと受け入れて開発しなければならないのでしょうか?

WWDC20で機能改善が報告された

WWDC20を最後までみてみると、どうやらそうでもなさそうです。
SKAdNetworkという機能がiOS14でアップデートされました。
まだ仕組みを完全に理解できていないのですが、WWDC20の説明を箇条書きにすると、

  • 広告のタップ時にStoreKitフレームワークを呼び出し、キャンペーンIDを渡す
  • キャンペーンIDがApp Storeクライアントによって保存される
  • アプリダウンロードも記録される
  • アプリ起動時にStoreKitにアプリが正常に起動したことを知らせる
  • StoreKitは、その情報をApp Storeクライアントに知らせる
  • 送信元アプリと宛先アプリの一意の組み合わせでユーザが特定されないよう、変換データをAppleに送信
  • Appleで正常に変換されたかどうかを検査する
  • 検査に合格したらデバイスに通知され、変換データを広告ネットワークに送信する
  • このやりとりの間が終始、暗号化されていてるため、データの完全性を担保している

Screen Shot 2020-11-15 at 22.52.35.png

引用:Build trust through better privacy - WWDC 2020 - Videos - Apple Developer
※ 動画の27分あたりからユーザトラッキングに関する説明が始まります。

プライバシーが守られているから同意の必要がないSKAdNewwork

上記の説明から、ユーザが特定できないような仕組みになっていることがわかります。
公式ドキュメントにもユーザ固有のデータは含まれていないということが書かれています。

The Apple-signed notification includes the campaign ID but doesn’t include user- or device-specific data.

引用:SKAdNetwork | Apple Developer Documentation

これらのやりとりは終始暗号化されて行われるので、プライバシーが守ることができています。
よって、SKAdNetworkでユーザの同意は必要ないとWWDC20の最後あたりで明言されていました。

Screen Shot 2020-11-15 at 23.31.58.png

引用:Build trust through better privacy - WWDC 2020 - Videos - Apple Developer
※ 動画の27分あたりからユーザトラッキングに関する説明が始まります。

予想される今後の流れ

WWDC20の内容を見る限り、今後はSKAdNetworkの使用が推奨されていくような気がします。
WWDC20の発表からSKAdNetworkに関する記事が増えているそうです。
AppsFlyerのとある記事では、IDFAは廃止される方向に進むのではと予想されていました。

Following these important improvements, Apple introduced the AppTrackingTransparency (ATT) framework, which will ultimately eliminate the IDFA.

引用:Does privacy have to come at the expense of UX? | AppsFlyer

Appleのこれまでのプライバシー機能の強化戦略をみると、あながち間違ってないのかな〜と思ったり。

まとめ

これまでの調査を、以下の3行でまとめてみました。

  • iOS14からIDFAの取得がオプトイン形式に変わる
  • オプトイン形式に変わったのは、ユーザのプライバシーを守るため
  • 今後はSkAdNetworkが主流になっていくかも???
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初めてのiOS開発

今度会社の業務としてiOSアプリの開発を行うので事前に勉強しておこうと思いまして、今回の記事を書きました。自分の備忘録としてささっと作成したのでわかりにくいところが多々あるかと思います。そのうちちゃんと書こうと思います。

xcodeのインストール

Xcodeとは

XcodeはMac、iPhone、iPad、Apple Watch、Apple TV向けのアプリを開発できる環境を提供してくれるIDE(統合開発環境)です。
Xcodeを利用することで画面やコードの作成、デバック、App Storeへのアプリの提出ができるようになります。
SwiftやObjective-Cを使用して開発できます。

インストール方法

App Storeからxcodeをインストールするか、下記のURLから直接xcodeのインストーラをダウンロードする方法があります。
https://developer.apple.com/download/more/
僕の場合,App Storeからインストールしようとするとxcodeの容量が大きいのかインストールできませんでした。
なので、xcodeのインストーラをダウンロードしました。
最初はそれでもインストールできなかったのですが、SSDストレージを40GBほど開けたところでインストーラを起動できました。インストーラは11GB、xcodeのアプリは16GBの容量を使いました。なぜこんなに容量食うんだ。。。
ということで、xcodeのアプリをインストールできたので早速使っていきましょう!

xcodeの使用方法

Xcodeを起動すると次の画面になります。

①iOSアプリを開発する場合は[Create a new Xcode project]をクリックします。
②次の画面が表示されるので[iOS]タブの[App]を選択して、[Next]をクリックします。

③[Next]をクリックすると次の画面が表示されるので、作成するプロジェクト情報を入力します。

フィールド 説明
Project Name プロジェクトの名前を入力
Team Xcodeに登録済みの[AppleID]を選択
Organization Name 会社名 or 組織名を入力
Organization Identifier アプリが所属するグループ(組織や個人)を識別する。
識別として利用されるのは、ドメイン表記を逆にした記述。
メールアドレスを逆に入力する方法もある。(その場合は@を.にする必要がある)
Bundle Identifier 入力不可(世界中のアプリの中で一意の値になる)
Interface 開発環境を指定する。
SwiftUI or Storyboardを選択する。
Language アプリを作るプログラミング言語の選択をする。
Swift or Objective-Cを選択する。
Use Core Data 高度な開発で必要になるデータベースやテストコードの指定をする。
Include Unit Tests 高度な開発で必要になるデータベースやテストコードの指定をする。
Include UI Tests 高度な開発で必要になるデータベースやテストコードの指定をする。

プロジェクトの情報を入力して[Next]をクリックします。これでプロジェクトが作成され、iPhoneアプリの開発が始められます。

実機(iPhone)でのテスト

①MacとiPhoneをLightningケーブルで接続する。
②画面左上の実行停止アイコンの右側にあるスキーマメニューをクリックして、接続してあるiPhoneを選択する。(アプリの転送先の指定)
スクリーンショット 2020-11-15 22.50.01.png

AppleIDをXcodeに登録する

※ Xcodeに登録しているAppleIDとiPhoneに登録しているAppleIDが同じであればアプリを転送できる。
③アプリケーションメニューから[xcode]>[Preferences...]を選択する。
④[Accounts]タブを選択し、[]をクリックする。
⑤ポップアップが表示されるのでAppleIDを選択し、[Continue]をクリックする。
AppleIDPasswordを入力して[Next]をクリックする。(teamの名前は後で設定するので覚えておく)

アプリのプロジェクトにTeamを設定

⑦[project navigator]を選択してプロジェクトを選択する。
⑧[TARGETS]でアプリのプロジェクトを選択する。
⑨メニューの[Signing&Capabilities]を選択する。
⑩Teamの部分を先ほど設定したteamの名前にする。
teamの設定が完了するとiPhoneほんんタイの検証に必要なファイルがインターネットからダウンロードされます
 ※ ファイルにはiPhoneでのアプリの動きや結果をXcodeで解析するためのプログラムなどが含まれている。ダウンロードが終わると実機転送ができるようになるため、完了するまで待つ。
iPhone側の設定
[設定]>[一般]>[プロファイルとデバイス管理]>[デベロッパappのアカウント]をタップ>[信頼]をタップする。
Macで[▶︎]アイコン(build and run)をクリックすると接続しているiPhoneへアプリが転送され、実機でアプリの動作を確認することができる。

まとめ

まだ概要しかわかってませんがまた近々開発部分のアウトプットをしたいと思っています。
またこの記事も少しわかりにくいところがあるかと思いますので引き続きブラッシュアップしていきます。
何かあればぜひコメントよろしくお願いします。

・参考にした書籍
たった2日でマスターできるiPhoneアプリ開発集中講座

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

swift実践入門output Chapter5  後編

制御構文

条件分岐、繰り返し、遅延実行、パターンマッチ
プログラムの実行フローを制御する構文。制御フローの制御には条件分岐や繰り返しなどがあり、それを組み合わせ実行し自在に操る

for-in文 シーケンスの要素の列挙

chapter5.swift
let array = [1,2,3]
for element in array {
    print(element)
}

Dictionary

chapter5.swift
let dictionary = ["a": 1, "b": 2]
for (key,value) in dictionary {
    print("Key: \(key), vlue: \(value)")
}

while文 継続条件による繰り返

chapter5.swift
var f = 1
while f < 4 {
    print(f)
    f += 1
}

repeat while文

while文は実行前に条件式を評価するため、場合によっては一度も実行されない
そこで条件式の成否にかかわらず、必ず1回以上繰り返しを実行したい場合は、repear whieを使用

chapter5.swift
var t = 1
repeat {
    print(t)
    t += 3
}
while t < 1

実行中の中断

break文は繰り返し文全体を終了させます。

chapter5.swift
var containsTwo = false
let garray = [1,2,3]

for element in garray {
    if element == 2 {
        containsTwo = true
        break
    }
    print("element:\(element)")
}
print("containsTwo:\(containsTwo)")

continue文は現在の処理のみを中断し、後続の処理を継続する

chapter5.swift
var odds = [Int]()
let rarray = [1,2,3,4,5,6]

for element in rarray {
    if element % 2 == 1 {
        odds.append(element)
        continue
    }
    print("even:\(element)")
}
print("odds:\(odds)")
}

遅延実行 のためのdefer文

defer文  スコープ退出時の処理

chapter5.swift
var count = 0

func someFUNTIONS() -> Int{
    defer {
        count += 1
    }
    return count
}
print(someFUNTIONS())
print(someFUNTIONS())
print(count)

パターンマッチ 値の構造や性質による評価

swiftには値の持つ構造や性質を表現するパターンという概念がある
パターン待ちとは、値が特定のパターンに合致するかの検査をすること

式パターン  ~ = 演算子による評価

chapter5.swift
let integer = 6

switch integer {
case 6:
    print("match: 6")

case 5...10:
    print("match: 5...10")

default:
    print("default")
}

バリューバインディング 値の代入を伴う評価

chapter5.swift
let values = 3

switch values {
case let matchValue :
    print(matchValue)
}

オプショナルパターン Optional型の値の有無の評価

chapter5.swift
let values = 3
let optionalD = Optional(4)
switch optionalD{
case let f?:
    print(f)

default:
    print("nil")
}

}

列挙型ケースパターン ケースとの一致の評価

chapter5.swift
enum Hemisphere {
    case northern
    case southern
}

let hemisphere = Hemisphere.southern
//let hemisphere = Optional(Hemisphere.southern)
switch hemisphere {
case .northern:
    print("match: .northern")
case .southern:
    print("match: .southern")
//case nil:
//    print("nil")
}

enum Color {
    case rgb(Int,Int,Int)
    case cmyk(Int,Int,Int,Int)
}

let color = Color.rgb(100, 200, 230)
switch color {
case .rgb(let r, let h, let j):
print(".rgb: (\(r), \(h), \(j))")
case .cmyk(let k,let y, let u, let o):
print(" .cmyk: (\(k),\(y),\(u),\(o))")

}

is演算子による型キャスティングパターン 型の判定による評価

chapter5.swift
let any: Any = "fafafa"
switch any {
case is String:
    print("match: String")
case is Int:
    print("match: Int")
default:
    print("default")
}

}

as演算子による型キャスティングパターン 型のキャストによる評価

chapter5.swift
let anys : Any = "1"
switch anys {
case let string as String:
    print("match: string(\(string))")
case let int as Int:
    print("match: int(\(int))")
default:
    print("default")
}

パターンマッチが使える場所if 文

chapter5.swift
let fff = 9
if case 1...10 = fff {
    print("yes")
}

//guard 文
func someFunc() {
    let value = 9
    guard case 1...10 = value else {
        return
    }
    print("yes")
}
someFunc()

//for in文
let harray = [1,2,3,4,5,6]
for case 2...5 in harray {
    print("2以上5以下")
}

while 文

chapter5.swift
var nextValue = Optional(1)
while case let value? = nextValue {
    print("value:\(value)")

    if value >= 8 {
        nextValue = nil
    } else {
        nextValue = value + 1
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swift実践入門output Chapter5

制御構文

条件分岐、繰り返し、遅延実行、パターンマッチ
プログラムの実行フローを制御する構文。制御フローの制御には条件分岐や繰り返しなどがあり、それを組み合わせ実行し自在に操る

if 文 条件の成否による分岐

chapter5.swift
let value = 5

if value <= 4 {
    print("valueは4以下")
}else {
    print("valueは5以上")
}

if let文 値の有無による分岐

オプショナル型とは変数にnilの代入を許容するデータ型で、反対に非オプショナル型はnilを代入できません。オプショナル型の変数にはデータ型の最後に「?」か「!」をつけます。

chapter5.swift
let optionalA = Optional(1)

if let a = optionalA {
    print("値は\(a)です")
}else {
    print("値は存在しません")
}

let optionalB = Optional("b")
let optionalC = Optional("c")

if let b = optionalB, let c = optionalC{
    print("値は\(b)\(c)ですよ")
}else {
    print("ない")
}

guardl文 条件不成立時に早期退出する分岐

guard文は、条件を満たさない場合の処理を記述する構文です。
条件を満たさない場合には、メソッドを終了したり、繰り返し処理を終了したり、スコープを抜けるための処理を書く必要がある。

guard文を使って条件を満たさない場合に処理を抜けるコード

chapter5.swift
//guard 条件式 else {
//  条件式がfalseの場合実行される文
//  guard文が記述されているスコープ文に退出する必要がある
//}

func someFunction() {
    let value = -1

    guard value >= 0 else {
        print("0未満の値")
        return
    }
    print(value)
}
someFunction()

guard文のスコープ外への退出の強制

chapter5.swift
func printIfPositive(_ a: Int) {
    guard a > 0 else {
        return
    }
    print(a)
}
printIfPositive(6)

guard文で宣言された変数や定数へのアクセス

guard let文とif letぶんの違いは
guard let文で宣言された変数や定数は guard let文以降で利用可能という点

chapter5.swift
func someFONCTION() {
    let a :Any = 3

    guard let int = a as? Int else {
        print("aはInt型ではありません")
        return
    }
    print("値はInt型の\(int)です")
}
someFONCTION()

func checkInt(num:Int) {
    guard num >= 0 else {
        print("マイナスです")
        return
    }
    print("プラスです")
}

checkInt(num:-10)
checkInt(num:10)

switch文 複数のパターンマッチによる分岐

chapter5.swift
let r = -1
switch  r {
case Int.min ..< 0:
    print("rは負の数")

case 1 ..< Int.max:
    print("rは正の数")
default:
    print("aは0")
}

ケースの網羅性のチェック

chapter5.swift
enum SomeEnum {
    case foo
    case bar
    case baz
}

let foo = SomeEnum.foo

switch foo {
case .foo:
    print(".foo")

case .bar:
    print(".bar")

case .baz:
    print(".baz")

}

whereキーワード  ケースにマッチする条件の追加

chapter5.swift
let optionalP: Int? = 12

switch optionalP {
case .some(let a) where a > 10:
    print("10より大きい値\(a)が存在します")
default:
    print("値が存在しない、もしくは、10以下")
}

let g = 1

switch g {
case 1:
    print("実行される")
    break
    print("実行されない")
default:
    break
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TableViewでのデータの読込・リフレッシュ・更新についての処理

はじめに

Qiitaが提供しているAPIを使用して、TableViewに記事を表示する際に、データの読み込みリフレッシュ更新について実装しましたので備忘録として投稿します。

概要

  1. QiitaAPIの初期取得
  2. 取得しているQiitaAPIのリフレッシュ
  3. TableView下部までスクロールした際にQiitaAPI追加取得

※上記にフォーカスをあたて説明になります。

動作環境

【Xcode】Version 12.0.1
【Swift】Version 5.3

実装コード

CustomTableView.swift
import UIKit

class CustomTableView: UITableView {

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    override init(frame:CGRect, style: UITableView.Style) {
        super.init(frame: frame, style: .plain)
        setup()
    }

    private func setup() {
        let refreshControl = UIRefreshControl()
        refreshControl.attributedTitle = NSAttributedString(string: "読み込み中")
        self.refreshControl = refreshControl
        // UITableViewが空のときの線(セパレーター)を消すために、空のViewを設定
        self.tableFooterView = UIView()
    }

    func addTargetToRefreshControl(_ target: Any?, action: Selector, event: UIControl.Event) {
        self.refreshControl?.addTarget(target, action: action, for: event)
    }

    func beginRefreshing() {
        guard let refreshControl = self.refreshControl else { return }
        refreshControl.beginRefreshing()
        refreshControl.sendActions(for: .valueChanged)
        self.contentOffset.y = -self.bounds.height
    }

    func endRefreshing() {
        guard let refreshControl = self.refreshControl else { return }
        refreshControl.endRefreshing()
    }

}
ViewController.swift
class ViewController: UIViewController {

    @IBOutlet private weak var tableView: CustomTableView!

    private var reloading: Bool = false
    private var page: Int = 20

    # ・・・省略・・・

    override func viewDidLoad() {
        super.viewDidLoad()
        # ・・・省略・・・
        tableView.addTargetToRefreshControl(self, action: #selector(self.refreshArticlesAction), event: .valueChanged)
        // refreshControlを呼び出し、ローディングする
        tableView.beginRefreshing()

        # ・・・省略 (Qiitaからデータ取得する処理) ・・・
        tableView.endRefreshing()
        tableView.reloadData()
    }

    @objc private func refreshArticlesAction() {
        // 取得数を初期値にする
        page = 20

        # ・・・省略 (Qiitaからデータ取得する処理) ・・・
        tableView.endRefreshing()
        tableView.reloadData()
    }

}

# ・・・省略・・・

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let height = scrollView.frame.size.height
        let contentYoffset = scrollView.contentOffset.y
        let distanceFromBottom = scrollView.contentSize.height - contentYoffset
        if distanceFromBottom < height {
            // 何回も呼ばれてしまうので、リロード中は変数:reloadingをtrueにしておく
            if !reloading && page < 100 {
                // 取得数を20追加にする
                if page <= 100 { page += 20 }
                // リロード処理のフラグ変更を「true」
                reloading.toggle()

                # ・・・省略 (Qiitaからデータ取得する処理) ・・・
                // リロード処理のフラグ変更を「false」
                reloading.toggle()
                tableView.reloadData()
            }
        }
    }
}

実装詳細

1. QiitaAPIの初期取得

output1.gif

  • viewDidLoadrefreshControl.beginRefreshing()を呼び出してローディング開始。
  • Qiitaからデータ取得。
  • refreshControl.endRefreshing()でローディング停止し、tableView.reloadData()で内容更新します。

2. 取得しているQiitaAPIのリフレッシュ

output3.gif

  • UIRefreshControlのインスタンスにaddTargetで指定しています。
CustomTableView.swift
func addTargetToRefreshControl(_ target: Any?, action: Selector, event: UIControl.Event) {
    self.refreshControl?.addTarget(target, action: action, for: event)
}
ViewController.swift
tableView.addTargetToRefreshControl(self, action: #selector(self.refreshArticlesAction), event: .valueChanged)
  • 下に引っ張る度に@objc private func refreshArticlesAction()が呼ばれるので、初期取得数 20 に変更し、Qiitaからデータ取得。(省略しておりますが、初期取得数はQiitaAPIの取得数になります)
  • refreshControl.endRefreshing()でローディング停止し、tableView.reloadData()で内容更新します。

3. TableView下部までスクロールした際にQiitaAPI追加取得

output2.gif

  • ScrollViewのデリゲートメソッドを使い、下までスクロールされたか検出します。
  • distanceFromBottom何回も呼ばれるのでBool型の変数reloadingを用意しました。
  • QiitaAPIの追加取得数は 20 ずつ増やすことにし、100 で上限としました。
  • tableView.reloadData()で内容更新します。
ViewController.swift
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let height = scrollView.frame.size.height
    let contentYoffset = scrollView.contentOffset.y
    let distanceFromBottom = scrollView.contentSize.height - contentYoffset
    if distanceFromBottom < height {
        // 何回も呼ばれてしまうので、リロード中は変数:reloadingをtrueにしておく
        if !reloading && page < 100 {
            // 取得数を20追加にする
            if page <= 100 { page += 20 }
            // リロード処理のフラグ変更を「true」
            reloading.toggle()

            # ・・・省略 (Qiitaからデータ取得する処理) ・・・
            // リロード処理のフラグ変更を「false」
            reloading.toggle()
            tableView.reloadData()
        }
    }
}

参考

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

iOSで動作する異常検知モデルを作った

背景

農業x深層学習のアプリケーションが作りたい!

という動機のもと、例えば作物の画像を入力してその健康状態を診断するようなアプリが作れるんじゃないかと考えてます。

病気を判定できるすごいモデルのせたアプリを作ったとして、ユーザが対象の作物の画像を正しく入力してくれるかどうかは、そのアプリの信頼性を担保する上で重要な問題になります。

例えば、上記の稲の病気を診断してくれるアプリを作ったとして、ユーザが雑草の画像を入力したとしてもそれっぽい結果を出力してしまえば、そのアプリの診断結果自体が疑わしいものになってしまいます。

IMG_0168.PNG

この問題に対処するため、メインとなるモデルの前段に入力画像の異常画像検知モデルを置いておけばよいのでは、と考えました。

IMG_0169.PNG

異常検知モデルにおいて正常と判定された画像のみをメインモデルに渡せば、信頼性の高い結果を出力することができそうです。

できたもの

3年前に購入した iPhoneXにアプリをインストールして、稲と雑草の画像を手元のラップトップに表示し、それを撮影してみました。
右上の円型のゲージに注目していただくと、稲と雑草をなんとなく識別している様子がわかると思います。

Videotogif.gif

以下で、どんなことをやったのかをつらつらと書いていきます。

メトリックラーニング

メトリックラーニングは、ある画像のペアが同一かどうかを判別するモデルを作成するために用いられる手法です。今回の要件にあたっては、入力した画像が学習させた正常画像と同じかどうかを判別するためにこの手法を利用しました。

以下の記事を参考にさせていただきました。
https://qiita.com/shinmura0/items/06d81c72601c7578c6d3

モデル

モデルの作成にはPytorchを使いました。

スマホに載せることを目標としているので、軽量なMobileNetV2を特徴抽出器として利用します。
MobileNetV2はtorchvisionにデフォルトで用意されています。

今回は画像サイズを128x128としました。featuresレイヤーの出力をいい感じに整形して、最終出力を512次元のベクトルにします。

from torchvision.models import MobileNetV2

class MobileNetFeatures(nn.Module):
    def __init__(self):
        super(MobileNetFeatures, self).__init__()
        self.head = MobileNetV2().features
        self.pool = nn.AvgPool2d(4, 4)
        self.flat = nn.Flatten()
        self.fc = nn.Linear(1280, 512)

    def forward(self, x):
        x = self.head(x)
        x = self.pool(x)
        x = self.flat(x)
        x = self.fc(x)
        return x

学習

データセット

学習データとして、正常画像と同時にランダムな異常画像を与える必要があります。
そこで、オープンデータセットであるCOCOデータセットから、正常画像と同じ数だけランダムに抽出し、これを異常画像の集合としました。

Loss関数

割と新しいLoss関数である Arcface を使いました。
Arcfaceの説明としては下記の記事がめちゃくちゃわかりやすかったです。
https://qiita.com/yu4u/items/078054dfb5592cbb80cc

また、以下のレポジトリではこういったメトリックラーニングの最新の論文実装がライブラリとして提供されているため、こちらを利用させていただきました。
https://github.com/KevinMusgrave/pytorch-metric-learning

異常度の測定

学習させたモデルの出力は512次元のベクトル(embedding)です。
入力した画像が異常かどうかを判別するには、正常画像から得られるembeddingとのコサイン類似度をとる必要があります。

そのため、学習フェーズではモデルの保存と同時にバリデーションデータのembeddingの平均ベクトルを保存しておくようにします。

そして推論時にはこれを読み込んで、入力した画像とのコサイン類似度をとることで、異常かどうかの判別を行うことができます。

train.py
    if save_interval > 0 and epoch_id % save_interval == 0:
        model.eval()
     # 正常画像と異常画像のコサイン類似度をそれぞれ測定する.
        positive_dist = []
        negative_dist = []
        for batch in valid_loader:
            images = batch[0].to(device)
            labels = batch[1].numpy().tolist()
            labels = [bool(i) for i in labels]
            with torch.no_grad():
                embeddings = model(images).cpu().numpy()

            positive_embeddings = embeddings[labels]
            negative_embeddings = embeddings[[not i for i in labels]]

            mean_embedding = np.mean(positive_embeddings, axis=0)
            for pe in positive_embeddings:
                cos_sim = np.dot(mean_embedding, pe) / (np.linalg.norm(mean_embedding, ord=2) * np.linalg.norm(pe, ord=2))
                positive_dist.append(cos_sim)
            for ne in negative_embeddings:
                cos_sim = np.dot(mean_embedding, ne) / (np.linalg.norm(mean_embedding, ord=2) * np.linalg.norm(ne, ord=2))
                negative_dist.append(cos_sim)
        mean_positive_dist = sum(positive_dist) / len(positive_dist)
        mean_negative_dist = sum(negative_dist) / len(negative_dist)

        print(f"epoch{epoch_id}: {mean_positive_dist} {mean_negative_dist}")
        model.train()

        # embeddingを保存する
        features_save_path = f"../saved_features/embedding.txt"
        np.savetxt(features_save_path, mean_embedding, delimiter=",")

スマホモデルへの変換

今回はiOSに載せることを想定し、coreMLを利用しました。

PytorchモデルからcoreMLへの変換のために、一度ONNX形式への変換を経由します。
(coremltoolsの最新版ではONNXを経由せずに変換できるようですが、今回は調査不足のため旧いやり方に従います。)

以下のスクリプトを参照ください。

注意としては、2020/11/14現在 Python3.8.2の環境ではProtocolBuffer関連のエラーが発生し、ONNX -> CoreML への変換が動作しませんでした。
これは3.7.7を利用することで解決できます。

あとは生成された .mlmodel を Swiftへ組み込めばOKです。

終わりに

プロジェクト全体は以下のリポジトリに置いてあります。
https://github.com/fltwtn/light_weight_annomaly_detection

実際にスマホで動作させてみて、MobileNetV2の速さを改めて実感しました。たぶん30fps以上は出てるんじゃないかな。。。
最近では精度も高く高速なモデルが次々にリリースされているので、今後もいろんなモデルをスマホモデルに変換して試してみようと思います。

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

【ゲームアプリとベジェ曲線:その③(完結)】パーツによる絵のバリエーション

はじめに

このシリーズでは iOS/Android ゲームアプリの描画手法として、ベジェ曲線の可能性を模索します。
第三回目は、素材をパーツ単位で作成することで、手軽に絵のバリエーションを増やす手法について探っていきます。

サンプルアプリのプロジェクトは GitHub にあります。
https://github.com/hakumai-iida/BezierSample
title_3rd
前回:【その②:ストローク(線)による表現の付け足し

パーツ分けのメリット

↓の画像は、【その①:環境準備とアンカーポイント(点)による形の変化】で作成したミクさんの顔です。
ss00
白目や目玉、口や鼻、顔や頭など、構成要素をパーツごとに作成しておき、アプリ上で組み合わせて描画することで、顔の絵を構成しています。

でも、なぜこんな面倒なことをするのでしょう?

アプリで処理する際、顔なら顔として1回の描画で済むように、データ上で1つにまとめておいたほうが無駄がないはずです。
絵をバラバラにしておくメリットとはなんでしょうか?

メリット1:パーツの差し替えによるバリエーション

例えば、下記のような素材があるとします。
ss01
これらの画像は全てアプリ上で描画されたものですが、先に説明したミクさんと同様、顔や体の要素がバラバラに作成されています。
※ミクさんの顔に追加されている陰影やタッチについては【その②:ストローク(線)による表現の付け足し】をご覧ください

そして、各パーツが共通のルールで作成されているため、目なら目、前髪なら前髪、手足なら手足として、パーツ間で互換性があります。
言い換えると、パーツどうしを差し替えられるということです。
ss02
一見するとこれらの絵は、一枚一枚が個別の素材に見えますが、基本素材のパーツをアプリ上でランダムに組み合わせて描画したものになります。

素材を部位毎に作成することで入れ替えが可能となり、『手軽に絵のバリエーションを生み出せる』のがパーツ分けの1つめのメリットです。

メリット2:パーツの変化によるアニメーション

ベジェ曲線として作成したデータは実行時に線を計算して描画するため、拡大/縮小&回転しても絵的に劣化しないという特性があります。
この強みのおかげで、パーツ単位で拡大/縮小&回転をフルに活用できます。

例えば、体、腕、脚などのパーツを横方向に大きくすることで、キャラクタの見た目を太らせることができます。
同様に、パーツを縦方向に大きくすることで背を高く、小さくすることで背を低く見せることもできます。

さらに腕や脚などのパーツを、関節を中心に回転させることでちょっとした動きをつけることもできます。
ss03
上記は、キャラクタの体型を変えつつ、手足の関節を曲げることで、素材的には1枚の絵をアニメーションさせています。
※体型や関節の動きについては【その①:環境準備とアンカーポイント(点)による形の変化】をご覧ください

各部位を個別に変化させることで、『手軽に絵を動かせる』のがパーツ分けの2つ目のメリットです。

パーツ分けにおける課題

素材をパーツ単位で分けて作る際、パーツ同士をどう馴染ませるかが課題になります。

いかにパーツをつなげるか?

例えば、腕のパーツを「上腕」と「前腕」に分けて作るとします。
ss04
仮に上記のようなパーツがあったとして、肘を曲げる動きをつけようとしましょう。
絵的になんの調整もせずに前腕だけを回転させてしまうと、肘の部分で線が綺麗につながらず、見た目に違和感がでてしまいます。
ss05
形状が一致しないパーツ同士をくっつける場合、いかに線をつなげて違和感のない形状にするかが課題となります。

いかにパーツの向きを判定するか?

パーツに対して体型的な操作をしようとした場合、拡大/縮小を適用する方向の判断が必要になってきます。
ss06
例えば上記画像において、①の下げた腕であればX方向に拡大することで腕を太く見せられますが、②の上げた腕をX方向に拡大すると腕が伸びて見えてしまいます。
ss07
パーツの大きさを変える際、どの向きに拡大/縮小するかの判定も課題となります。

パーツ分けに対するベジェ曲線的アプローチ

さて、ベジェ曲線における描画では、点(アンカーポイント)を計算しつつ形状を構成していきます。
この時、アンカーポイントに色々な情報を紐付けておくことで、状況に応じて形を変化させることが可能です。
※アンカーポイントについては【その①:環境準備とアンカーポイント(点)による形の変化】をご覧ください

上述の課題の解決には、このアンカーポイントが活躍します。
では、ベジェ曲線的なアプローチを見ていきましょう。

フックによるストロークの接続

サンプルデータでは、顔の輪郭は「顔の上部=生え際からこめかみ」と「顔の下部=こめかみからアゴ」の2つのパーツで構成されます。
ですが、データ上でストローク間の座標が微妙にずれていて、パーツを縦に並べてただけでは、こめかみのラインがきれいに繋がってくれません。
ss08
そこで、この2つのパーツのストロークの両端(アンカーポイント)に座標を共有するためのIDを設定し、パーツが描画される際にこめかみの所で線がフック(接続)されるようにします。
ss09
上記画像において、緑の四角がフック指定されたアンカーポイントとなります。
これらがIDによりフックされることで、きれいにこめかみが繋がりました。

パーツ間のストロークを接続するためのアプローチ、それがフックとなります。

ジョイントによる太さの維持

ここで、上腕と前腕のパーツに話しを戻しましょう。
ss04
これらのパーツもフックにより接続されるのですが、単純にアンカーポイントの座標が共有されただけでは、前腕のパーツが回転した場合に肘の太さが潰れてしまいます。
ss11
そこで、パーツ間で角度に差がでることが想定される場合(言い換えるとパーツのどちらかが独自に回転する可能性がある場合)、パーツの接続部分を円にみたててジョイント(関節)情報を設定します。

この時ジョイントはその直径により、二本のストローク間で保たれるべき「太さ」の役割を担います。
具体的には、パーツ間の角度の差分をジョイントが把握し、ストローク端を円上で回転させてからフックします。
ss12
パーツ間の太さを維持するためのアプローチ、それがジョイントとなります。

呼び出し位置によるパーツの伸縮

サンプルデータでは、「腹」パーツが描画の起点となり、「上半身」と「下半身」を呼び出します。
その後、上半身は「上腕」と「首」を呼び出し、上腕は「前腕」を、首は「頭」を呼び出します。
さらに、前腕が「手」を呼び出し、さらに、さらに、と言うように、体の中心に近いパーツが末端へ向けてパーツを呼び出すことで、キャラクタの絵が構成されます。
ss14
さて、パーツの呼び出しはとても簡単です。
適当な位置にアンカーポイントを置いて、呼び出すパーツの種別を設定するだけです。

そして、先に説明した通り、パーツ間のストロークはフック指定によって自動で繋がってくれるため、パーツの呼び出し位置を遠ざけることでパーツ間の見た目を伸ばしたり、近づけることで縮ませることが可能となります。

パーツの伸縮にはこの特性を利用します。

体型による変化のパラメータをアンカーポイントに設定することで、パーツの呼び出し位置を補正し、キャラクタの体を伸ばしたり縮めたりすることが可能となります。
※実際には、体型パラメータは呼び出し位置だけではなくストロークの座標=あらゆる形状に影響します. 詳しくは【その①:環境準備とアンカーポイント(点)による形の変化】をご覧ください

試しに、パーツの呼び出し位置を極端にしてみた画像が下記となります。
ss15
この例では、「頭」、「前腕」、「下腿」、「つま先」パーツが、親パーツからかなり離れた位置に呼び出された結果、キリンのような見た目になりました。

最終手段のカバー

さて、今までは、パーツ同士をいかに結合させるかを考えてきました。
ですが、「見た目の親和性=絵としてのなじみ具合」の観点から結合が厳しい場合もでてきます。

例えば、ミクさんは「絵的にノースリーブ」です。
一方で、他のキャラクタ達は「絵的に袖のあるシャツ」を着ています。

処理上は、ミクさんの上半身に他のキャラの上腕を接続しても何の問題もありませんが、肌の露出した肩口に袖を接続さてしまうと「見た目的に変な絵」になってしまいます。

段ボールやネギの着ぐるみの場合はさらに深刻で、そのままパーツを呼び出すと、首や上腕の付け根が絵的になじまず、違和感が顕著になります。
ss16
そこで、パーツ間の絵的な親和性が低そうな場合、見苦しい部分を誤魔化すためにカバー(粗隠し)を適用します。

例えば、段ボールを上半身として利用する時のカバーとして、「首や上腕を通す穴」のようなパーツを追加で作成します。
ss17
そして、段ボールが上腕を呼び出した際、カバーを対象パーツのジョイントに紐づけることで、違和感のある付け根部分を、文字通り覆い隠してしまいます。

この時、カバーは紐づけられたジョイントから情報を受け取り、対象パーツの太さと向きにあわせて拡大/縮小&回転して表示されます。
ss18
絵的にどうしようもない時の最終手段、それがカバーとなります。

ごまかしとの戦い

以上がパーツ分け描画に対する、ベジェ曲線的なアプローチとなります。

とはいえまだまだ完璧というわけではありません。
サンプルアプリで「ランダム」生成を試してみると、粗の目立つ画像がコレでもかという具合に出てきてしまいます。

データ的には問題ないけど、見た目的に厳しい部分。
これをどう回避していくのかは今後の課題でもあります。
ss19_1
例えば基本ストロークで描画された上記の絵は、細かな粗がいくつも目につきます。
・右耳のおさげの付け根が頭のラインをハミ出している
・右耳のおさげが前腕の前にきてしまっている
・両肩の接続が変(前述したミクさんのノースリーブに袖を接続した状態)
・右脇と上腕の接続が変(おそらくフックの設定漏れ)

こういった粗は、データで丁寧に修正していくのが本来なのですが、一括で抑え込むことができたら楽ですし保険にもなります。
例えばストロークの種類をブレ線にしてみたり鉛筆線にしてみると細かな描写が潰れるため、いくつかの粗が目立たなくなったりします。
ss19_2
絵のニュアンスが大きく変わるので功罪ではありますが、これもある意味、ベジェ曲線的なアプローチと言えるかもしれません。

データの作り方、パーツの構成、それを支えるシステム。
まだまだ試行錯誤の余地がありそうです。

ベジェ曲線の可能性

さて、ゲームアプリの描画におけるベジェ曲線の可能性について、3回にわたって探ってきました。

当初想定していた機能の実装&検証が終わった今、ベジェ曲線の可能性として1番に感じていることは、色々な素材をゴチャ混ぜにして、1つのニュアンス(解像度+ストローク+塗り)の絵として表示できる点です。

とくにベジェ曲線は、素材を再利用するアプローチが豊富なため、少ない素材でバリエーションを容易に生み出せます。
さらにこの強みは、素材の数に対して加速度的に増していきます。
※基本素材を100種類ぐらい用意できたら、そこから生成されるランダム画像のバリエーションは物凄いことになりそうです

キャラクタの見た目の変化を前面に押し出すようなジャンル等、ベジェ曲線は十分にゲーム描画で活用できると思います。

例えば「女の子を合成するゲーム」とか。
あるいは「女の子が着ぐるみに身を包んで闘って、負けたら爆ぜて素体に戻るゲーム」とか。

やばい、ドキドキしてきた!

では、このシリーズはこれにて完結となります。
お読みいただきありがとうございました。

その①:環境準備とアンカーポイント(点)による形の変化
その②:ストローク(線)による表現の付け足し

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

[Swift5]プロトコルを使ってModelからControllerへ値を返す方法

はじめに

今回の記事は前回投稿した[Swift5]ViewControllerからModelに通信を行って値を渡す方法の続編です。まだ見てない方は先にコチラ▼を見てからの方が良いかと思います。
https://qiita.com/nkekisasa222/items/dabe806c23d3890a009f

それでは本題に入りましょう!

プロトコルの作成

まずコードを記述します。

SampleModel.swift
//①ここでプロトコルの作成
protocol DoneCatchProtocol {

    //②Controllerに返したい値と型を指定(今回はString)
    func catchSampleData(sampleValueA: String, sampleValueB: String, sampleValueC: String)
}

class SampleModel {

    var sampleValueA: String?
    var sampleValueB: String?
    var sampleValueC: String?

    //③プロトコルのインスタンスを作成
    var doneCatchProtocol: DoneCatchProtocol?

    init(firstSampleValue: String, secondSampleValue: String, thirdSampleValue: String) {

        sampleValueA = firstSampleValue
        sampleValueB = secondSampleValue
        sampleValueC = thirdSampleValue
    }

    //④Model内で処理を行うのでメソッドを作成
    func processingSampleModel {

        //⑤ここで作成したプロトコルを呼び出して値を入れる
        self.doneCatchProtocol?.catchSampleData(sampleValueA: sampleValueA, sampleValueB: sampleValueB, sampleValueC: sampleValueC)
    }

流れをまとめます。

①Model側でプロトコルを作成します。
②返したい値のキー値と型を指定してあげる。
③プロトコルのインスタンスを作成。
④Model内で処理を行うのでメソッドを作成
⑤プロトコルを呼び出してControllerで呼び出したい値を指定する。

Controller側の記述

まずコードを記述します。

SampleViewController.swift
//①Controllerでプロトコルを扱えるように呼び出します。
class SampleViewController: UIViewController, DoneCatchProtocol {

    firstSampleValue  = "firstSampleValue"
    secondSampleValue = "secondSampleValue"
    thirdSampleValue  = "thirdSampleValue"

    //④SampleModelから返ってくる値を代入するプロパティ
    var valueA: String?
    var valueB: String?
    var valueC: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        startSampleModel()
    }

    func startSampleModel() {

      let sampleModel = SampleModel(firstSampleValue: firstSampleValue, secondSampleValue: secondSampleValue, thirdSampleValue: thirdSampleValue)

      //③SampleModelのプロトコルに委託とメソッドの呼び出し
      sampleModel.doneCatchProtocol = self
      sampleModel.processingSampleMode()
    }

    //②Controller側でプロトコルを呼び出すと自動生成されます。
    func catchSampleData(sampleValueA: String, sampleValueB: String, sampleValueC: String) {

        //⑤SampleModelから返ってきた値を代入する
        valueA = sampleValueA
        valueB = sampleValueB
        valueC = sampleValueC
    }
}

流れをまとめます。

①Controllerでプロトコルを扱えるように呼び出す。
②①でプロトコルを呼び出すとメソッドが自動生成されます。
③SampleModelのプロトコルに委託とメソッドの呼び出し
④SampleModelから返ってくる値を代入するプロパティを用意
⑤SampleModelから返ってきた値を代入する

これでModelで処理した値をControllerで使用できますね。

最後に

前回記事と今回の記事でControllerからModelに通信を行い、Modelで処理した内容をControllerへ返してそれをControllerで扱う方法について投稿しました。MVCモデルではよく扱われる開発方法なのでは?と思いますので、是非参考にしていただけたらと思います。

最後までご覧いただきありがとうございました!

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

【SwiftUI】Mapkitを使った位置情報の取得とピンの表示

はじめに

SwiftUIでMapkitとFirebaseを使用し、位置情報をデータベースに保存することを目的とする。
今回はSwiftUIで現在地を取得し、地図を表示するところまでを記載。
Firebaseの導入方法は前回記事を参考にしてください。

参考記事
【SwiftUI】CocoaPods導入手順とFirebaseの設定

開発環境

OSX 10.15.7 (Catalina)
Xcode 12.0.1
CocoaPods 1.10.0

Mapkitのインポートと地図表示まで

MapkitとCoreLocationをインポートする。
Alertは位置情報の取得が設定できなかった場合に通知を行うように設定。

ContentView.swift
import SwiftUI
import Firebase

// 以下の行を追加
import CoreLocation
import MapKit

struct ContentView: View {
    // 以下を追記
    @State var manager = CLLocationManager()
    @State var alert = false

    var body: some View {
        // 以下の行を追加
        // ContentViewに地図を表示
        mapView(manager: $manager, alert: $alert).alert(isPresented: $alert) {

            Alert(title: Text("Please Enable Location Access In Setting Panel!!!"))
        }
    }
}

// MKMapViewの設定

mapViewの作成

地図表示にはmakeUIView、updateUIViewの設定が必要です。

【SwiftUI】MapKitを導入して地図を表示する
を参考にすると、makeUIViewのところはcodeの部分はMKMapView(frame: .zero)を記載するだけで地図の表示は可能です。今回は位置情報を取得するため、showsUserLocationtrueに設定しています。

makeCoordinatorはカスタムインスタンスを作成し、makeUIView(context:)メソッドを呼び出す前にこのメソッドを呼び出します。makeCoordinatorはclassで設定したコーディネーターを提供します。

ContentView.swift
// MKMapViewの設定
struct mapView : UIViewRepresentable {
    typealias UIViewType = MKMapView

    @Binding var manager : CLLocationManager
    @Binding var alert : Bool

    let map = MKMapView()

    func makeCoordinator() -> mapView.Coordinator {

        return mapView.Coordinator(parent1: self)
    }

    func  makeUIView(context: UIViewRepresentableContext<mapView>) -> MKMapView {
        // Tokyo 35.6804° N, 139.7690° E
        let center = CLLocationCoordinate2D(latitude: 35.6804, longitude: 139.7690)
        let region = MKCoordinateRegion(center: center, latitudinalMeters: 10000, longitudinalMeters: 10000)
        map.region = region

        manager.delegate = context.coordinator
        manager.startUpdatingLocation()
        map.showsUserLocation = true
        manager.requestWhenInUseAuthorization()
        return map
    }

    func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<mapView>) {

    }

    class Coordinator: NSObject, CLLocationManagerDelegate {

        var parent : mapView

        init(parent1 : mapView) {

            parent = parent1
        }

        func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {

            if status == .denied{

                parent.alert.toggle()
                print("denied")
            }
        }

        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {

            let location = locations.last

            let point = MKPointAnnotation()

            let georeader = CLGeocoder()
            georeader.reverseGeocodeLocation(location!) { (places, err) in

                if err != nil {

                    print((err?.localizedDescription)!)
                    return
                }

                let place = places?.first?.locality
                point.title = place
                point.subtitle = "Current Place"
                point.coordinate = location!.coordinate
                self.parent.map.removeAnnotations(self.parent.map.annotations)
                self.parent.map.addAnnotation(point)

                let region = MKCoordinateRegion(center: location!.coordinate, latitudinalMeters: 10000, longitudinalMeters: 100000)
                print(region)
                self.parent.map.region = region

            }
        }
    }
}

locationManagerのところで、位置情報を取得し、地図上にピンを落とし、表示する設定にしています。simulatorを利用した場合はピンが落とされますが、SwiftUI画面のpreview画面では表示されません。simulatorではGPS情報は取得できませんでしたが、実機で確認したところ、現在地を表すピンが表示されました。

ここではCLGeocoderを呼び出しているため、逆ジオコーディングを行い、地名を表示することも可能です。他にもピンの表示の仕方もここを変更すれば実装が可能です。
ここは今後実装します。

Xcodeプロジェクトの'info.plist'に追加

Info_plist.png

Privacy - Location Usage Description
開発者に許可を与える。なぜ、ユーザーの位置情報を取得するかの理由を記載。

[Deprecated] Privacy - Location Usage Description (NSLocationUsageDescription) - Allows the developer to describe why the app wants to access the user location. NOTE: This key has been deprecated in iOS 8 (and greater). Use NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription instead.

Privacy - Location When In Use Usage Description
開発者に許可を与える。アプリ使用中にユーザーの位置情報を取得する理由を記載。

Privacy - Location When In Use Usage Description (NSLocationWhenInUseUsageDescription) - Allows the developer to describe why the app wants to access the user's location while it is running.

Info_plist.png

「Location When In Use Usage Description」のValueを編集し、文字列を入力すると、初回起動時の確認画面で、文字を表示することができます。
下記のsimulator画像参考。

プレビューで確認し、地図を表示

現在の位置情報が取得できます。
Simulatorを起動した場合は初回時に「位置情報の取得」許可が求められます。

ContentView_swift.png

初回起動画面
iPhone_11_–_14_0.png

位置情報取得とピンの表示
simulatorでGPS取得ができずになぜか、サンフランシスコが表示されている。
iPhone_11_–_14_0-2.png

デバックの際に[GEOAddressObject] [NSLocale currentLocale] failed for NSLocaleCountryCode@のエラーが表示されるのはsimulatorの「設定」→「一般」→「言語と地域」のところで「地域」に日本を登録すると、エラーは出なくなります。

以上です。

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