- 投稿日:2020-08-07T23:47:45+09:00
NSImageのサイズを変更してPNGファイルを出力する
実装
- 検索するといくつも実装例が見つかりますが、最終的に下記を参考にしました。
- NSImage をリサイズする。
- Swiftのバージョンが古かったため、いくつかリファクタリングしています。
extension NSImage { func resizedImage(toSize newSize: NSSize) -> NSImage?{ if !self.isValid { return nil } guard let sourceBitmapRep = self.tiffRepresentation?.bitmap else { return nil } let bitmapRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(newSize.width), pixelsHigh: Int(newSize.height), bitsPerSample: sourceBitmapRep.bitsPerSample, samplesPerPixel: sourceBitmapRep.samplesPerPixel, hasAlpha: sourceBitmapRep.hasAlpha, isPlanar: sourceBitmapRep.isPlanar, colorSpaceName: sourceBitmapRep.colorSpaceName, bytesPerRow: sourceBitmapRep.bytesPerRow, bitsPerPixel: sourceBitmapRep.bitsPerPixel)! bitmapRep.size = newSize NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext.init(bitmapImageRep: bitmapRep) self.draw(in: NSRect(x: 0, y: 0, width: newSize.width, height: newSize.height), from: NSRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0) NSGraphicsContext.restoreGraphicsState() let newImage = NSImage(size: newSize) newImage.addRepresentation(bitmapRep) return newImage } }
- また実装を便利にするため、下記のextensionを定義しています。
extension NSBitmapImageRep { func imageWithFormat(for format: NSBitmapImageRep.FileType) -> Data? { return representation(using: format, properties: [:]) } } extension Data { var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) } } extension NSImage { var png : Data? { tiffRepresentation?.bitmap?.imageWithFormat(for: .png) } var jpeg: Data? { tiffRepresentation?.bitmap?.imageWithFormat(for: .jpeg) } var gif : Data? { tiffRepresentation?.bitmap?.imageWithFormat(for: .gif) } }
- 呼び出し例は下記のとおりです。
- 元の画像サイズの取得方法は下記を参考にしています。
guard let image = loadImage() else { return } guard let newWidth = image.tiffRepresentation?.bitmap?.pixelsWide, let newHeight = image.tiffRepresentation?.bitmap?.pixelsHigh else { return } // let newSize = NSSize(width: newWidth, height: newHeight) let newSize = NSSize(width: Double(newWidth) * 0.5, height: Double(newHeight) * 0.5) guard let resizedImage = image.resizedImage(toSize: newSize) else { return } let outputURL1 = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent("/images/created_image_1.png") let outputURL2 = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent("/images/created_image_2.png") if let imageData = image.png, let resizedImageData = resizedImage.png { do { print(imageData.count) print(resizedImageData.count) try imageData.write(to: outputURL1) try resizedImageData.write(to: outputURL2) print("PNG image saved") } catch { print(error) } }// MARK: - Helper Methods func loadImage() -> NSImage? { let desktopPath = (NSSearchPathForDirectoriesInDomains(.desktopDirectory, .userDomainMask, true) as [String]).first let filePath = desktopPath?.appending("/images/desktop 1.png") if let filePath = filePath { if let image = NSImage(contentsOfFile: filePath) { return image } } return nil }
- 出力結果は以下の通りです。
- 縦横のサイズがそれぞれ50%になっていることが確認できます。
解決していない問題
- ご存じの方、お教えいただけると助かります…。
NSImageを経由するとサイズが異なる
- representation(using:properties:)のpropertiesで何かしら設定しないといけない?
いくつかの情報が異なってしまう
- 解像度の値が異なる
- 解像度は
NSBitmapImageRep.size
に依存している?ColorSyncプロファイル
が異なる
- 投稿日:2020-08-07T23:47:45+09:00
NSImageのサイズを縮小してPNGファイルを出力する
実装
- 検索するといくつも実装例が見つかりますが、最終的に下記を参考にしました。
- NSImage をリサイズする。
- Swiftのバージョンが古かったため、いくつかリファクタリングしています。
extension NSImage { func pixelsSize() -> NSSize? { guard let pixelsWide = self.tiffRepresentation?.bitmap?.pixelsWide, let pixelsHigh = self.tiffRepresentation?.bitmap?.pixelsHigh else { return nil } return NSSize(width: pixelsWide, height: pixelsHigh) } func resizedImageScaseDown(to ratio: Double) -> NSImage? { if !(0.0 < ratio && ratio < 1.0) { return nil } if !self.isValid { return nil } guard let sourceBitmapRep = self.tiffRepresentation?.bitmap else { return nil } // 画像のサイズと実際の描画サイズ(ピクセル単位)の2つを使用する let newImageSize = NSSize(width: Double(self.size.width) * ratio, height: Double(self.size.height) * ratio) guard let pixelsSize = pixelsSize() else { return nil } let newPixelsSize = NSSize(width: Double(pixelsSize.width) * ratio, height: Double(pixelsSize.height) * ratio) let bitmapRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(newPixelsSize.width), pixelsHigh: Int(newPixelsSize.height), bitsPerSample: sourceBitmapRep.bitsPerSample, samplesPerPixel: sourceBitmapRep.samplesPerPixel, hasAlpha: sourceBitmapRep.hasAlpha, isPlanar: sourceBitmapRep.isPlanar, colorSpaceName: sourceBitmapRep.colorSpaceName, bytesPerRow: sourceBitmapRep.bytesPerRow, bitsPerPixel: sourceBitmapRep.bitsPerPixel)! bitmapRep.size = newImageSize NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext.init(bitmapImageRep: bitmapRep) self.draw(in: NSRect(x: 0, y: 0, width: newImageSize.width, height: newImageSize.height), from: NSRect.zero, operation: NSCompositingOperation.copy, fraction: 1.0) NSGraphicsContext.restoreGraphicsState() let newImage = NSImage(size: newImageSize) newImage.addRepresentation(bitmapRep) return newImage } }
- 下記3つの画像のサイズに関するパラメータがあります。
- 解像度に影響するので、比率を合わせるように実装しています。
パラメータ 例 NSImage.size (1680, 1050) NSBitmapImageRep.size (1680, 1050) NSBitmapImageRep.pixels (3360, 2100)
- また実装を便利にするため、下記のextensionを定義しています。
extension NSBitmapImageRep { func imageWithFormat(for format: NSBitmapImageRep.FileType) -> Data? { return representation(using: format, properties: [:]) } } extension Data { var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) } } extension NSImage { var png : Data? { tiffRepresentation?.bitmap?.imageWithFormat(for: .png) } var jpeg: Data? { tiffRepresentation?.bitmap?.imageWithFormat(for: .jpeg) } var gif : Data? { tiffRepresentation?.bitmap?.imageWithFormat(for: .gif) } }
- 呼び出し例は下記のとおりです。
guard let image = loadImage() else { return } // 50%のサイズにする guard let resizedImage = image.resizedImageScaseDown(to: 0.5) else { return } let outputURL1 = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent("/images/created_image_1.png") let outputURL2 = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.appendingPathComponent("/images/created_image_2.png") if let imageData = image.png, let resizedImageData = resizedImage.png { do { print(imageData.count) print(resizedImageData.count) try imageData.write(to: outputURL1) try resizedImageData.write(to: outputURL2) print("PNG image saved") } catch { print(error) } }// MARK: - Helper Methods func loadImage() -> NSImage? { let desktopPath = (NSSearchPathForDirectoriesInDomains(.desktopDirectory, .userDomainMask, true) as [String]).first let filePath = desktopPath?.appending("/images/desktop 1.png") if let filePath = filePath { if let image = NSImage(contentsOfFile: filePath) { return image } } return nil }
- 出力結果は以下の通りです。
- 縦横のサイズがそれぞれ50%になっていることが確認できます。
解決していない問題
- ご存じの方、お教えいただけると助かります…。
NSImageを経由するとサイズが異なる
- 読み込み元のファイルをそのまま出力しているつもりですが、ファイルサイズに微妙な差異が見られます。
- representation(using:properties:)のpropertiesで何かしら設定しないといけない?
ColorSyncプロファイルが異なる
- 投稿日:2020-08-07T23:40:29+09:00
Swiftでの複数ファイルの扱い
Swiftで、複数ファイルの扱いがどうなっているのか調べてみた。具体的には、Rubyで言う require みたいのがどうなっているのかよくわからなかったので、調べてみた。
Swift.org
Swiftの仕様等はこちらのサイトに集まっている。
複数ファイルの扱いは、まずはこちらにある。https://swift.org/getting-started/#using-the-package-manager
Swift package manager provides a convention-based system for building libraries and executables, and sharing code across different packages.
詳細は、こちら。
https://swift.org/package-manager実際に作ってみよう。以下のようにする。
% cd ~/dev
% mkdir PackageManager
% cd PackageManager
% swift package init
Creating library package: PackageManager
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/PackageManager/PackageManager.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/PackageManagerTests/
Creating Tests/PackageManagerTests/PackageManagerTests.swift
Creating Tests/PackageManagerTests/XCTestManifests.swiftビルドする際には、以下のようにする。
% swift build
コマンドライン実行するようなプログラムの場合は、
--type executable
をつける。% cd ~/dev
% mkdir Hello
% cd Hello
% swift package init --type executable
Creating executable package: Hello
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/Hello/main.swift
Creating Tests/
Creating Tests/LinuxMain.swift
Creating Tests/HelloTests/
Creating Tests/HelloTests/HelloTests.swift
Creating Tests/HelloTests/XCTestManifests.swift実行する際には、以下のようにする。
% swift run Hello
省略形で、以下のようにもできる。
% swift run
以下のようなファイルを作り、
Sources/Hello/Greeter.swiftfunc sayHello(name: String) { print("Hello, \(name)!") }
Sources/Hello/main.swift
を以下のようにする。Sources/Hello/main.swiftif CommandLine.arguments.count != 2 { print("Usage: hello NAME") } else { let name = CommandLine.arguments[1] sayHello(name: name) }すると、
main.swift
から、Greeter.swift
のsayHello
を呼び出せるようになる。% swift run Hello earth
[4/4] Linking Hello
Hello, earth!
% swift run Hello `whoami`
Hello, eto!main.swiftの冒頭には、
require
や#include
等の記述は無い。しかし、同じDirectoryにファイルがあるということで、同じPackageにあるとみなされ、呼び出される。これが、convention-based systemということ。convention-basedというのは、慣例にもとづくということ。単にファイルを置いただけで関連付けられるのは、楽で良さそうと思いつつ、恐いなという気持ちもある。さて、ここまではわかったが、わからなかったのは、インタープリタで実行する方法だ。実行してみると、
% cd ~/dev/Hello/Sources/Hello
% swift main.swift
main.swift:6:5: error: use of unresolved identifier 'sayHello'
sayHello(name: name)
^~~~~~~~となる。main.swiftからsayHelloを発見する方法が無いので、当然だ。main.swiftに
import Greeter
を追加しても、% swift main.swift
main.swift:3:8: error: no such module 'Greeter'
import Greeter
^となる。 import "Greeter.swift" や import "Greeter" も同様の結果になる。
どうやって読み込めばいいんだろう?
- 投稿日:2020-08-07T19:53:05+09:00
CALayerアニメーションで開始位置を変更した際に迷ったことを整理した
CoreAnimationを使って、吹き出しのアニメーションを作った際に
- アニメーションがViewの中心から始まってしまう
- Viewの表示位置がズレてしまう
という点について迷ってしまいましたので、自分なりに整理してみることにしました。
内容を整理するにあたって、日常生活の会話を題材に、以下の4つの点から開始するアニメーションを作成しました。
- 閃きのアニメーション (画像の下中央からのアニメーション)
- 吹き出しのアニメーション (画像の左右中央からのアニメーション)
- 汗マークのアニメーション (画像の上中央からのアニメーション)
アニメーション自体は、Viewのスケールを変えているだけになります。
動かした感じ
— 小岩井 (@WfODXAd0jmop1Ev) August 7, 2020アニメーションの開始位置を変更する
アニメーションの開始位置の指定は、CALayerのanchorPoint1を指定することで変更が可能です。
閃きのアニメーション
エナジードリンクを飲む前に、添える一言を思いついた瞬間を表すアニメーションです。
anchorPointで指定する際は、次のような座標をイメージすると対応を進め易いです。
電球のアニメーションでは、●の位置から始めたいので、座標はxが0.5、yが1.0になります。
デフォルトでは、この値が(0.5,0.5)になっているので、アニメーションがViewの中央から始まります。
電球のviewをlightBulbViewとすると
lightBulbView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)と指定することで、●の位置からアニメーションが開始されます。
吹き出しのアニメーション
エナジードリンクを飲む直前に添えた一言の吹き出しのアニメーションです。
吹き出しのアニメーションは、●の位置から開始したいので、xが1.0、yを0.5に指定します。
speechBubbleView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)汗マークのアニメーション
聞き返された後のアニメーションです。
吹き出しのアニメーションは、●の位置から開始したいので、xが0.5、yを0.0に指定します。
polkaDotView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)これで、Viewに追加した画像別に、アニメーションの開始位置を変えることができました。
Viewの表示位置を元に戻す
しかしこのままですと、配置したViewの表示される位置が変わってしまいました。
そこで、 anchorPointで移動させた分を、元に戻して意図した見た目の位置に戻しました。閃きのアニメーションで試した例let yMovement = lightBulbView.frame.size.height * lightBulbView.layer.anchorPoint.y lightBulbView.transform.ty = yMovement吹き出しのアニメーションで試した例let xMovement = speechBubbleView.frame.size.width * speechBubbleView.layer.anchorPoint.x speechBubbleView.transform.tx = xMovement
- 投稿日:2020-08-07T18:45:43+09:00
SwiftFormat(0.45.0以降)で想定外の整形結果になったので解決方法をまとめました
はじめに
私のチームではソースコードを綺麗な状態で保つために、OSSのSwiftFormatを利用してソースコードの自動整形(フォーマット)を行なっています。
今回、SwiftFormatを0.45.0以降にアップデートしたところ、今までは自動整形されていなかった箇所まで変更されてしまう事象が2つ発生したため、解決方法についてまとめました実行環境
環境 バージョン macOS Catalina 10.15.6 Xcode 11.6.0 SwiftFormat 0.45.0以降 発生した事象その1
次のように、修飾子の順番が変更されてしまう事象が発生しました。
- convenience required init?(hoge: Hoge) { + required convenience init?(hoge: Hoge) {- static public func hoge() { + public static func hoge() {原因
CHANGELOG.mdを確認したところ、バージョン0.45.0にて以下のオプション、ルール、構成オプションの名前が変更されていました。1
CHANGELOG.mdRenamed --empty option to --voidtype, which better describes its function Renamed specifiers rule to modifierOrder for consistency with SwiftLint Renamed --specifierorder configuration option to --modifierorderこのことが原因で、.swiftformatに設定していたspecifiersの無効指定が0.45.0以降では意味を成しておらず、現象その1が発生していました。
# disable --disable specifiers対応方法
CHANGELOG.mdの内容に従って、名前を変更することで無事解決できました
# disable - --disable specifiers + --disable modifierOrder発生した事象その2
次のように、guard-else文で改行を含む複数条件の場合、
else {
が改行されてしまう事象が発生しました。guard let foo = foo, - let bar = bar else { + let bar = bar + else { fatalError() }原因
CHANGELOG.mdを確認したところ、バージョン0.45.0にて以下のオプションが追加されていました。
CHANGELOG.mdAdded --guardelse option to control wrapping of else clauses in guard statements--guardelseオプションの説明を確認すると、defaultはautoとなっており、このことが原因で現象その2が発生していました。
elseOnSameLine
Place else, catch or while keyword in accordance with current style (same or next line).
Option Description --elseposition Placement of else/catch: "same-line" (default) or "next-line" --guardelse Guard else: "same-line", "next-line" or "auto" (default) 対応方法
--guardelseオプションに指定できる値は、same-line or next-line or autoとなっているため、今までのように何もしないという動作を選択することはできないようです。2
このため、今回は--guardelseオプションに現状のソースコードにより近い値を指定することで、(ほぼ)解決ということにしました。
(もっといい解決方法を知っている方いましたら、ぜひ教えてください )補足
類似問題として、--guardelseをnext-line or auto(default) にした場合に
else {
のインデントがずれる事象も観測されましたが、こちらの事象は障害のようでした。
すでに以下のIssueとして管理され、fixed in develop
ラベルが付与されている状態のため、次のリリース(たぶん、0.45.4だと思われる)で修正される予定のようです。最後に
今回取り上げた内容以外にも新しい機能が複数追加されているため、上記とは異なる問題が起きている方もいるかもしれません。
そのような場合は、まずCHANGELOG.mdの内容確認をオススメします。参考情報
ちゃんとCHANGELOG.mdを読んでいなかった自分も悪いと思いつつ、セマンティック バージョニングの考え方からするとマイナーバージョンのアップで互換性が失われるのはどうなんだろう? という気もしています。 ↩
--disableで無効化できないか試してみたのですが、上手くいきませんでした。ソースコードを自動整形(フォーマット)して綺麗に保つという趣旨からすると、何もしないという動作は許容したくないのかも?けど、無効化できるオプションもあるしなー ↩
- 投稿日:2020-08-07T17:18:36+09:00
ViewController.swiftのデフォルトコード【overrideとか、superとか】
overrideとか、superとか。
簡単にまとめます。??
前提
-親クラス(別名: スーパークラス)
-子クラス(別名: サブクラス)(英 super: 「超〜」以外に、リーダー、監督、管理人、なんて意味もある。)
override?
override
は、親クラスのメソッドに何か書き換えて使いたい時に使えます。継承して出来た子クラス側で、親クラスのメソッドを再定義するのが
override
です。
なので、「継承 =override」ではないです。?(英 override: 上書き)
super?
overrideされる前の親のメソッドを、明示的に呼びたい時に使います。
overrideを行った場合には
メソッド名が同じ
になるので、
そのままでは親クラスのメソッドを呼び出すことができません。super = "親クラスそのもの"を指す
(super.メソッド名
、使い方はこんな感じ。)class ViewController: UIViewController { <----「:」 で継承。 override func viewDidLoad() { super.viewDidLoad() // 新しく追加したい処理を書く } }「親クラス(super)のメソッドですよ」と、
わかりやすく(=明示的) に記述できる。
override func メソッド名
・super.メソッド名
。上記コードでのメソッド名はどちらも
viewDidLoad()
で区別がつかないですが、
super
を使うことで、明示的になります。overrideできないようにしたい場合?
final
を使う。-メソッドに使うと、overrideでエラー。(下記)
-親クラス名に使うと、継承自体できなくなる。class Paint { final func changeColor() { <----final print("色を変更します") } } class PaintChild: Paint { override func changeColor() { <----overrideでエラー print("色を緑に変更します") } }ViewController.swiftの、デフォルトコード
extends
で継承する言語もあると思いますが、Swiftは「:
」 で継承。class ViewController: UIViewController { <----「:」 で継承。 override func viewDidLoad() { super.viewDidLoad() // 新しく追加したい処理を書く } }親クラスの
viewDidLoad
メソッドを呼び出し、そこに追加処理を書く。
という感じ。おしまい。
参考サイト
- 投稿日:2020-08-07T16:51:35+09:00
Apple Store スクリーンショットサイズ まとめ
- 投稿日:2020-08-07T16:29:50+09:00
iOS14からエンジニアがすべきIDFA対応
はじめに
9月リリースが噂されているiOS14から、
iOS13まで端末単位で設定していたIDFAの取得許可(追跡型広告制限)
が端末兼アプリ単位の取得許可に変更され、
各アプリにて、通知や位置情報許可のように、ユーザに許可を取る必要がでてきます。IDFAとは?iOS13と比較した詳しい挙動の違いは?は
こちらの素晴らしい記事にまとまっていたので割愛させていただきます。
https://qiita.com/yofuru/items/213b88b85553631204e4重要なのはiOS14対応していないアプリはIDFAを取得できなくなることです。
本記事では実際にエンジニアがiOS14リリースまでに対応する必要がある実作業をまとめます。
※本記事はbeta4時点の情報です、beta版のスクリーンショットはNDA締結により掲載しておりません
1.frameworkの追加
IDFAの使用用途は、主に広告やトラッキングのためと考えられるため既に導入済みであることが多いかと思いますが、
AdSupport.framework
そして今回追加された
AppTrackingTransparency.framework
を追加します
公式リファレンス: https://developer.apple.com/documentation/apptrackingtransparency2.info.plistへ説明の追加
Privacy - Tracking Usage Description
をKeyとしてValueにユーザにAlertを出した時に表示される文言を追加します。ここでの文言は非常に重要になってくると考えられます。
ATTrackingManager.requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです
通知や位置や写真の時と一緒ですねAlertのタイトルはApple側が表示するもので、日本語だとbeta4時点でこう表示されてました
「"app"が他社が所有するAppやWebサイトを横断してあなたを追跡する許可を求めます」???
一般ユーザはなんぞや???
って感じだと思う
誰も許可したくならない…info.plistの記述はAlertのメッセージ部分に表示されます。
こちらを許可していただければ、関連性の高い広告を配信することができるなど
許可するメリットを伝えることが重要かと思います。また、info.plistの文言はAppleの審査の目が厳しい箇所でもあるので
強制させるような文言や、用途が何も伝わらない文言はやめましょう。3.Alertを表示するコードの追加
必要なタイミングで許可情報を取得し、Alertを表示するコードを追加しましょう
ViewController.swiftimport UIKit import AdSupport import AppTrackingTransparency class ViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) switch ATTrackingManager.trackingAuthorizationStatus { case .authorized: print("Allow Tracking") //IDFA取得 print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)") case .denied: print("?お断り") case .restricted: print("?制限") case .notDetermined: //Alert表示 ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in switch status { case .authorized: print("?") case .denied, .restricted, .notDetermined: print("?") @unknown default: fatalError() } }) @unknown default: fatalError() } } }先述のように、ATTrackingManager.requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです。そのため、表示するタイミングが非常に重要になってきます。
例えばリワード広告を出した後に、「より貴方が好む広告を表示するために情報を利用します」など…
自分も検討中です?なお、これまでIDFAを取得できるか利用していた
ASIdentifierManager の isAdvertisingTrackingEnabled
はiOS14からDeprecatedになります
https://developer.apple.com/documentation/adsupport/asidentifiermanager注意点
冒頭で説明した通り、iOS14からは、端末兼アプリ単位の追跡許可設定になります。
端末単位で拒否されていると、requestTrackingAuthorizationしていなくても、trackingAuthorizationStatusがdeniedで返ってきます、Alertを表示することさえ許されません、無慈悲?端末の設定は
設定 > プライバシー > トラッキング > Appからのトラッキングを許可(ON/OFF)
から設定できます
各アプリの設定もここからできますまた、iOS13までIDFAの設定画面にあった項目は
設定 > プライバシー > Appleの広告 > パーソナライズされた広告(ON/OFF)
に変更されています試したところ、パーソナライズされた広告がOFFの時でもtrackingAuthorizationStatusが.authorizedで返ってきてIDFAが取れていました
説明を読むとこちらは、App StoreやApple News、株価のターゲティングが無効になるそうですおわりに
この他に開発者が意識するべきことは、利用しているSDKがIDFAを利用しているかを把握し
各社に対応を伺うことかと思います。参考にAjust様がものすごく丁重に対応すべき項目をまとめてくださっていたので共有します。
Ajust: iOS14の変更に向けての準備と対策また、いくつかの広告会社さんからSDKのアップデート予定を告知していただきました?♂️
ユーザさんにメリットを伝えつつサービスの目的も達成しうる対応をしていきましょう?
- 投稿日:2020-08-07T16:29:50+09:00
iOS14でエンジニアがすべきIDFA対応
はじめに
9月リリースが噂されているiOS14から、
iOS13まで端末単位で設定していたIDFAの取得許可(追跡型広告制限)
が端末兼アプリ単位の取得許可に変更され、
各アプリにて、通知や位置情報許可のように、ユーザに許可を取る必要がでてきます。IDFAとは?iOS13と比較した詳しい挙動の違いは?は
こちらの素晴らしい記事にまとまっていたので割愛させていただきます。
https://qiita.com/yofuru/items/213b88b85553631204e4重要なのはiOS14対応していないアプリはIDFAを取得できなくなることです。
本記事では実際にエンジニアがiOS14リリースまでに対応する必要がある実作業をまとめます。
※本記事はbeta4時点の情報です、beta版のスクリーンショットはNDA締結により掲載しておりません
1.frameworkの追加
IDFAの使用用途は、主に広告やトラッキングのためと考えられるため既に導入済みであることが多いかと思いますが、
AdSupport.framework
を追加します。
そしてiOS14から追加された、
AppTrackingTransparency.framework
を追加します。公式リファレンス: https://developer.apple.com/documentation/apptrackingtransparency
2.info.plistへ説明の追加
Privacy - Tracking Usage Description
をKeyとしてValueにユーザにAlertを出した時に表示される文言を追加します。ここでの文言は非常に重要になってくると考えられます。
ATTrackingManager.requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです
通知や位置や写真の時と一緒ですね。AlertのタイトルはApple側が表示するもので、日本語だとbeta4時点でこう表示されてました。
「"app"が他社が所有するAppやWebサイトを横断してあなたを追跡する許可を求めます」???
一般ユーザはなんぞや???
って感じだと思う。
誰も許可したくならない…info.plistの記述はAlertのメッセージ部分に表示されます。
こちらを許可していただければ、関連性の高い広告を配信することができるなど
許可するメリットを伝えることが重要かと思います。また、info.plistの文言はAppleの審査の目が厳しい箇所でもあるので
強制させるような文言や、用途が何も伝わらない文言はやめましょう。3.Alertを表示するコードの追加
必要なタイミングで許可情報を取得し、Alertを表示するコードを追加しましょう。
ViewController.swiftimport UIKit import AdSupport import AppTrackingTransparency class ViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) switch ATTrackingManager.trackingAuthorizationStatus { case .authorized: print("Allow Tracking") print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)") case .denied: print("?拒否") case .restricted: print("?制限") case .notDetermined: showRequestTrackingAuthorizationAlert() @unknown default: fatalError() } } ///Alert表示 private func showRequestTrackingAuthorizationAlert() { ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in switch status { case .authorized: print("?") //IDFA取得 print("IDFA: \(ASIdentifierManager.shared().advertisingIdentifier)") case .denied, .restricted, .notDetermined: print("?") @unknown default: fatalError() } }) } }先述のように、requestTrackingAuthorizationを利用してAlertを表示できるのは1度きりです。そのため、表示するタイミングが非常に重要になってきます。
例えばリワード広告を出した後に、「より貴方が好む広告を表示するために情報を利用します」など…
自分も検討中です?なお、これまでIDFAを取得できるか利用していた
ASIdentifierManager の isAdvertisingTrackingEnabled
はiOS14からDeprecatedになります。
https://developer.apple.com/documentation/adsupport/asidentifiermanager注意点
冒頭で説明した通り、iOS14からは、端末兼アプリ単位の追跡許可設定になります。
端末単位で拒否されていると、requestTrackingAuthorizationしていなくても、trackingAuthorizationStatusがdeniedで返ってきます、Alertを表示することさえ許されません、無慈悲?端末の設定は
設定 > プライバシー > トラッキング > Appからのトラッキングを許可(ON/OFF)
から設定できます。
各アプリの設定もここからできます。また、iOS13までIDFAの設定画面にあった項目は
設定 > プライバシー > Appleの広告 > パーソナライズされた広告(ON/OFF)
に変更されています。試したところ、パーソナライズされた広告がOFFの時でもtrackingAuthorizationStatusが.authorizedで返ってきてIDFAが取れていました
説明を読むとこちらは、App StoreやApple News、株価のターゲティングが無効になるそうです。おわりに
この他に開発者が意識するべきことは、利用しているSDKがIDFAを利用しているかを把握し
各社に対応を伺うことです。参考にAdjust様がものすごく丁重に対応すべき項目をまとめてくださっていたので共有します。
Adjust: iOS14の変更に向けての準備と対策また、いくつかの広告会社さんからSDKのアップデート予定を告知していただきました?♂️
ユーザさんにメリットを伝えつつセキュリティを担保しつつ
サービスの目的も達成しうる対応をしていきましょう?
- 投稿日:2020-08-07T16:28:29+09:00
This bundle is invalid. The value for key CFBundleShortVersionString [1.0.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.0.0]. Please find more information about CFBundleShortVersionString [エラー対処]
iOS Version とBild 番号について
VersionはAppStoreで表示されるバージョン番号
Buildはアプリの内部的なバージョン番号これで、アップデートの確認をする
(Version, Build)番号について自分は、(1.0.0 , 1.0.0) -> (1.0.0 , 1.0.1) -> (1.0.0 , 1.0.2) -> (1.0.0 , 1.0.3)
とおこなってきたが、
こんな感じで怒られちゃった。
自分ができた解決策は、
(Version , Build) : (1.0.1 , 1)
にしたら、うまく行くことができた。Build 番号を1.0.3とかではなく、1つの数字で書くらしい
[参照]
- 投稿日:2020-08-07T13:02:13+09:00
[チュートリアル]カウントアップアプリを作ってSwiftを触ってみよう
今回のチュートリアルで想定している人
- スマホアプリを作ってみたい!
- Swiftをやってみたかった!
- 1人で始める自信がない・・・
- 何か新しい言語に触れてみたい!
みんなウェルカム
今回のチュートリアルで学べること
- ボタンをタップしてカウントアップ
- if文
- メソッド
- StoryBoardの基本的な使い方
- 画面遷移 v1
- 通知機能 v2
ソースコードはこちらから
https://github.com/techiro/CountUpAppForBeginners
早速Xcodeを開いて新規プロジェクトを作成
Xcodeを起動
→Create New Xcode Project
→Single View App
を指定して次へ。Product Name とOrganization Nameは、任意の名前を指定。
Languageは
Swift
、User Interface はStoryboard
を指定して次へ。
ボタンとラベルをStoryBoardに置いてみる
まず画面を作成していきます。
Main.StoryBoard
を開くとiPhoneの画面がポツンと表示されています。ここにボタンやラベルを置いていきます。
UIパーツの配置方法
Main.StoryBoard
を選択- 右上の+ボタンをクリック
- UIパーツ一覧が表示される
- 1つのLabelと2つのボタンを画面に配置
ボタンに画像を反映
Main.storyboard
を開く- ボタンを選択
画面右のAttribute Inspectorをクリック
Attribute Inspectorについて
UIのいろんな設定ができる
ボタンの例
- ボタンの色
- ボタンの大きさ
- ボタンの画像
などを変更できる。
画像をプロジェクトに追加する(Attribute Inspectorを触ってみる)
今回使用した+ボタンと-ボタン画像はiconfinderの画像を使用しました。画像はなんでもOK!
追記
+ーボタンがダウンロードできないエラーが生じているので、好きなアイコンをダウンロードしてください!
画像をダウンロードしたらプロジェクトの
Assets.xcassets
にドラッグ&ドロップポイント
Assets.xcassets
フォルダはアプリ内にある画像を保存して簡単に呼び出せる場所、
積極的に使っていきましょう!ボタンのimageを変更
ボタンを選択
→Attribute Inspector
→Image
左のボタンと右のボタンにーボタン、+ボタンをセット
シミュレーターで確認
画面左上再生ボタンを押すとシミュレーターが起動します。
ショートカットコマンドは⌘ + R
ここまで完成したら、次はUIパーツとコードを紐づける。
UIパーツとコードを紐づける
エディタをカスタムする
Assistantエディタをクリック
三 ボタンをクリックすると出てきます。
Assistantエディタを出現させた状態で、UIパーツのラベルをクリック
コントロールを押しながらコードのほうにドラッグ&ドロップUIパーツから青い線が伸びるので
override func viewDidLoad
の上で離す。このようにポップアップが出現します。
ここでこのラベルの名前などが設定できます。
Nameを任意の文字列に変更(今回はcountLabel)
Connectボタンを押すと
プログラムコード
とUIパーツ
が紐付けられ,
文字を変更したりボタンを押された時の動きなどをプログラムすることができます。
ボタンの設定方法
ボタンは
viewDidLoad
の下に紐付けます。注意点
ポップアップの1番上の項目が
Action
になっているか確認してください。Action
になっていないと「ボタンを押された時」という条件を作ることができません。ボタンの名前
+ボタン→countUpButton
-ボタン→countDownButton
コードの確認
import UIKit class ViewController: UIViewController { @IBOutlet weak var countLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func countUpButton(_ sender: Any) { } @IBAction func countDounButton(_ sender: Any) { } }
IBOutlet
はそのパーツに値を入れたり画面上に表示するためのものです。@IBAction
はそのパーツが押された時に呼ばれるメソッドになっています。そのためカウントアップ・ダウンをする時に処理を追加すればカウントアップアプリが実現できます。これら2つのメソッドはUIとコードの綱渡し的な存在です。
以上でUIの説明は終わり
UIパーツとコードの繋がりを確認する
ViewController.swift
を選択とりあえずUIパーツとコードをつなげたので一度つながりを確認してみましょう
viewが読み込まれる際に呼ばれる関数viewDidLoad()内にUIパーツのcountLabelの文字を出力してみます。
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(countLabel.text) }Labelと出力されたはずです。
ここでUIパーツだったLabelの文字列を
countLabel.text
とプログラムを書くことで出力したり取得することができることが確認できました。
カウントアップ・ダウンをする
やりたいこと(コメント)
@IBAction func countUpButton(_ sender: Any) { //+ボタンを押すとラベルの文字をカウントアップ //10以上になったら文字の色を緑に変更 } @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン //0より小さくなったら文字の色を赤色に変更 }カウントするために変数を準備
class ViewController: UIViewController { //数字を格納する場所 var count = 0 }var→変数
count→変数名countの型はInt型
なぜcountの型がInt型?
Swiftの特徴として型推論というものがある。代入するものの型が決まっていると代入先の型も同じになるような設計。
0
はInt型つまりcount
もInt型
count = 0
勝手に型を推論してくれる。型推論を使用せずに型を宣言する時は,
var count: Int = 0
とします。型を調べる方法
print(type(of: count))
でもいいがかなり便利なので使ってみてください!
カウントアップ・ダウンの完成
@IBAction func countUpButton(_ sender: Any) { //+ボタンを押すとラベルの文字をカウントアップ count = count + 1 //count += 1でも可 countLabel.text = String(count) //10以上になったら文字の色を緑に変更 } @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン count = count - 1 //count -= 1でも可 countLabel.text = String(count) }
- ボタンを押された時にcount ±1をする。
String(count)
でInt型であったcountをString型に変換countLabel.text
= countの値実行 ⌘ + R
無事できたら次はif文を使って機能を付け加えましょう!
ChangeTextColorメソッドを定義
メソッドとif分について触ってみる。
メソッドとは関数とも呼ばれプログラムを書いていく上では必須です。
//ラベルの色を変更するメソッドを定義する func changeTextColor() { //カウントにあわせて文字の色を変更 }
今回はラベルの色を変更するChangeTextColorメソッドを定義してみます。
if文を書いて条件分岐
- countの値が10以上だったら文字の色を緑に
- countの値が0より小さかったら文字の色を赤に
- その他の値だったら文字の色を青に
これを実装するためにはif文を使います。
//ラベルの色を変更するメソッドを定義する func changeTextColor() { if count >= 10 { countLabel.textColor = UIColor.green }else if count < 0 { countLabel.textColor = UIColor.red }else { countLabel.textColor = UIColor.blue } }色を変更するコードの完成
@IBAction func countUpButton(_ sender: Any) { //+ボタンを押すとラベルの文字をカウントアップ count = count + 1 countLabel.text = String(count) //カウントにあわせて文字の色を変更 changeTextColor() } @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン count = count - 1 countLabel.text = String(count) //カウントにあわせて文字の色を変更 changeTextColor() } //ラベルの色を変更するメソッドを定義する func changeTextColor() { if count >= 10 { countLabel.textColor = UIColor.green }else if count < 0 { countLabel.textColor = UIColor.red }else { countLabel.textColor = UIColor.blue } }
以上でカウントアップアプリのチュートリアルは終わりです。お疲れ様でした!
もっと深掘りしたい方
次はこのアプリをベースとして、アプリに動きをつける画面遷移と通知を送る方法を実装していきます。
- アプリに画面遷移を追加する方法
- カウントアップアプリを改造して,通知をn秒後に通知を出してみる (n>0)
最後に
最後までご覧いただきありがとうございます。
Twitterで主にSwiftについてのツイートをしているのでのぞいてみてください!今回はSwiftの言語についてあまり詳しく説明しませんでしたが、
ここでもっと詳しく記事を書いているのでSwiftに興味が湧いた方は参考にしてみてください!
- 投稿日:2020-08-07T13:02:13+09:00
[Swift]カウントアップアプリを作ってSwiftを触ってみよう
今回のチュートリアルで想定している人
- スマホアプリを作ってみたい!
- Swiftをやってみたかった!
- 1人で始める自信がない・・・
- 何か新しい言語に触れてみたい!
みんなウェルカム
今回のチュートリアルで学べること
- ボタンをタップしてカウントアップ
- if文
- メソッド
- StoryBoardの基本的な使い方
- 画面遷移 v1
- 通知機能 v2
ソースコードはこちらから
https://github.com/techiro/CountUpAppForBeginners
早速Xcodeを開いて新規プロジェクトを作成
Xcodeを起動
→Create New Xcode Project
→Single View App
を指定して次へ。Product Name とOrganization Nameは、任意の名前を指定。
Languageは
Swift
、User Interface はStoryboard
を指定して次へ。
ボタンとラベルをStoryBoardに置いてみる
まず画面を作成していきます。
Main.StoryBoard
を開くとiPhoneの画面がポツンと表示されています。ここにボタンやラベルを置いていきます。
UIパーツの配置方法
Main.StoryBoard
を選択- 右上の+ボタンをクリック
- UIパーツ一覧が表示される
- 1つのLabelと2つのボタンを画面に配置
ボタンに画像を反映
Main.storyboard
を開く- ボタンを選択
画面右のAttribute Inspectorをクリック
Attribute Inspectorについて
UIのいろんな設定ができる
ボタンの例
- ボタンの色
- ボタンの大きさ
- ボタンの画像
などを変更できる。
画像をプロジェクトに追加する(Attribute Inspectorを触ってみる)
今回使用した+ボタンと-ボタン画像はiconfinderの画像を使用しました。画像はなんでもOK!
追記
+ーボタンがダウンロードできないエラーが生じているので、好きなアイコンをダウンロードしてください!
画像をダウンロードしたらプロジェクトの
Assets.xcassets
にドラッグ&ドロップポイント
Assets.xcassets
フォルダはアプリ内にある画像を保存して簡単に呼び出せる場所、
積極的に使っていきましょう!ボタンのimageを変更
ボタンを選択
→Attribute Inspector
→Image
左のボタンと右のボタンにーボタン、+ボタンをセット
シミュレーターで確認
画面左上再生ボタンを押すとシミュレーターが起動します。
ショートカットコマンドは⌘ + R
ここまで完成したら、次はUIパーツとコードを紐づける。
UIパーツとコードを紐づける
エディタをカスタムする
Assistantエディタをクリック
三 ボタンをクリックすると出てきます。
Assistantエディタを出現させた状態で、UIパーツのラベルをクリック
コントロールを押しながらコードのほうにドラッグ&ドロップUIパーツから青い線が伸びるので
override func viewDidLoad
の上で離す。このようにポップアップが出現します。
ここでこのラベルの名前などが設定できます。
Nameを任意の文字列に変更(今回はcountLabel)
Connectボタンを押すと
プログラムコード
とUIパーツ
が紐付けられ,
文字を変更したりボタンを押された時の動きなどをプログラムすることができます。
ボタンの設定方法
ボタンは
viewDidLoad
の下に紐付けます。注意点
ポップアップの1番上の項目が
Action
になっているか確認してください。Action
になっていないと「ボタンを押された時」という条件を作ることができません。ボタンの名前
+ボタン→countUpButton
-ボタン→countDownButton
コードの確認
import UIKit class ViewController: UIViewController { @IBOutlet weak var countLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func countUpButton(_ sender: Any) { } @IBAction func countDounButton(_ sender: Any) { } }
IBOutlet
はそのパーツに値を入れたり画面上に表示するためのものです。@IBAction
はそのパーツが押された時に呼ばれるメソッドになっています。そのためカウントアップ・ダウンをする時に処理を追加すればカウントアップアプリが実現できます。これら2つのメソッドはUIとコードの綱渡し的な存在です。
以上でUIの説明は終わり
UIパーツとコードの繋がりを確認する
ViewController.swift
を選択とりあえずUIパーツとコードをつなげたので一度つながりを確認してみましょう
viewが読み込まれる際に呼ばれる関数viewDidLoad()内にUIパーツのcountLabelの文字を出力してみます。
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. print(countLabel.text) }Labelと出力されたはずです。
ここでUIパーツだったLabelの文字列を
countLabel.text
とプログラムを書くことで出力したり取得することができることが確認できました。
カウントアップ・ダウンをする
やりたいこと(コメント)
@IBAction func countUpButton(_ sender: Any) { //+ボタンを押すとラベルの文字をカウントアップ //10以上になったら文字の色を緑に変更 } @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン //0より小さくなったら文字の色を赤色に変更 }カウントするために変数を準備
class ViewController: UIViewController { //数字を格納する場所 var count = 0 }var→変数
count→変数名countの型はInt型
なぜcountの型がInt型?
Swiftの特徴として型推論というものがある。代入するものの型が決まっていると代入先の型も同じになるような設計。
0
はInt型つまりcount
もInt型
count = 0
勝手に型を推論してくれる。型推論を使用せずに型を宣言する時は,
var count: Int = 0
とします。型を調べる方法
print(type(of: count))
でもいいがかなり便利なので使ってみてください!
カウントアップ・ダウンの完成
@IBAction func countUpButton(_ sender: Any) { //+ボタンを押すとラベルの文字をカウントアップ count = count + 1 //count += 1でも可 countLabel.text = String(count) //10以上になったら文字の色を緑に変更 } @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン count = count - 1 //count -= 1でも可 countLabel.text = String(count) }
- ボタンを押された時にcount ±1をする。
String(count)
でInt型であったcountをString型に変換countLabel.text
= countの値実行 ⌘ + R
無事できたら次はif文を使って機能を付け加えましょう!
ChangeTextColorメソッドを定義
メソッドとif分について触ってみる。
メソッドとは関数とも呼ばれプログラムを書いていく上では必須です。
//ラベルの色を変更するメソッドを定義する func changeTextColor() { //カウントにあわせて文字の色を変更 }
今回はラベルの色を変更するChangeTextColorメソッドを定義してみます。
if文を書いて条件分岐
- countの値が10以上だったら文字の色を緑に
- countの値が0より小さかったら文字の色を赤に
- その他の値だったら文字の色を青に
これを実装するためにはif文を使います。
//ラベルの色を変更するメソッドを定義する func changeTextColor() { if count >= 10 { countLabel.textColor = UIColor.green }else if count < 0 { countLabel.textColor = UIColor.red }else { countLabel.textColor = UIColor.blue } }色を変更するコードの完成
@IBAction func countUpButton(_ sender: Any) { //+ボタンを押すとラベルの文字をカウントアップ count = count + 1 countLabel.text = String(count) //カウントにあわせて文字の色を変更 changeTextColor() } @IBAction func countDounButton(_ sender: Any) { //-ボタンを押すとラベルの文字をカウントダウン count = count - 1 countLabel.text = String(count) //カウントにあわせて文字の色を変更 changeTextColor() } //ラベルの色を変更するメソッドを定義する func changeTextColor() { if count >= 10 { countLabel.textColor = UIColor.green }else if count < 0 { countLabel.textColor = UIColor.red }else { countLabel.textColor = UIColor.blue } }
以上でカウントアップアプリのチュートリアルは終わりです。お疲れ様でした!
もっと深掘りしたい方
次はこのアプリをベースとして、アプリに動きをつける画面遷移と通知を送る方法を実装していきます。
- アプリに画面遷移を追加する方法
- カウントアップアプリを改造して,通知をn秒後に通知を出してみる (n>0)
最後に
最後までご覧いただきありがとうございます。
Twitterで主にSwiftについてのツイートをしているのでのぞいてみてください!今回はSwiftの言語についてあまり詳しく説明しませんでしたが、
ここでもっと詳しく記事を書いているのでSwiftに興味が湧いた方は参考にしてみてください!
- 投稿日:2020-08-07T12:46:51+09:00
iOSアプリ開発者のための「iOS14 IDFAオプトイン問題 」
iOS14ではIDFAを取得する場合は、ユーザから「明示的な同意」を得ることが必須となりました。
WWDC2020でこの件が発表された当時、広告界隈では「IDFAの死か!?」などと言われたりして大きな話題となっていました。あれから1ヶ月以上が経過し、現在の主な計測プロバイダー(MMP)の動向について整理したいと思います。
まずはこの問題の主要キーワード
- IDFA
- SKAdNetwork
- AppTrackingTransparency
を簡単に見ていきます。
IDFA
IDFA (Identifier for Advertisers) は、Appleがユーザーの端末にランダムに割り当てるデバイスIDです。
Adjustより各広告プラットフォーマー(Google広告やYahoo広告など)やモバイル計測プロバイダー(AdjustやAppsFlyerなど)は、基本的にこのIDFAを使ってアプリユーザーを追跡しています。
このIDは、大抵は広告SDKや計測系SDK側で取得されていたりするので、あまり自分で実装して取得するケースは少ないかも知れません。
ちなみにAndroidの場合はGPS ADID(Google Play Services ID for Android)があります。
SKAdNetwork
StoreKit Frameworkのクラス。
このクラス自体はiOS11.3から存在していて「どの広告からのインストールか?」などを判断するために使用したりします。
(主にアフィリエイト用途)これが今回のiOS14で「どのアプリの広告から来たか?」等のより細かい粒度の情報が追える様に更新されました。
(それでも計測プロバイダーからは実用的な効果測定のためには役不足と言われている)Appleとしては、今後の広告効果計測にはこのクラスを使った方法を推していくのだと思いますが、
これに従うかどうかでは、各社計測プロバイダーでも方向性が分かれているようです。AppTrackingTransparency
今回新しく追加されたフレームワーク。
これまでは「AdSupport framework」のASIdentifierManagerからIDFAを直接取得していました。iOS14ではこのフレームワークを使いユーザから許諾を得てから、IDFAを取得する必要があります。
(最終的にASIdentifierManagerからIDFAを取ってくることは変わりない)iOS 14、iPadOS 14、tvOS 14では、AppTrackingTransparencyフレームワークを通じてユーザーの許可を得ない限り、デベロッパがユーザーを追跡したり、ユーザーのデバイスの広告識別子にアクセスしたりすることはできません。
ユーザーのプライバシーとデータの使用より | Apple DeveloperiOS14でのIDFA取得の流れ
1. 許諾メッセージを作成
Info.plistに「Privacy-Tracking Usage Description」(NSUserTrackingUsageDescription)を追加。
値にIDFAのオプトインを求めるアラート用のメッセージを指定します。2. ユーザーへのオプトイン提示
AppTrackingTransparency frameworkの
ATTrackingManager.requestTrackingAuthorization(completionHandler:)を実行すると、初回のみ許諾を求めるアラートがユーザに提示され、
そのコールバックで以下のお馴染みの結果ステータスが返ってきます。authorized denied notDetermined restrictedちなみに設定アプリ上でiOS13までの「追跡型広告を制限」をONにしているユーザー(いわゆるLATオンのユーザ)にはアラートは表示されません。
また、iOS14ではオプトイン許諾を求める事に自体の可否を設定する事も可能になり、当然これが拒否されている場合でもアラートは出ません。3. IDFAの取得
許諾を得た後は、従来通りASIdentifierManagerからIDFAを取得するという流れになると思います。
guard ASIdentifierManager.shared().isAdvertisingTrackingEnabled else { return } let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString今のアプリをアップデートしないで放っておくとどうなる?
iOS14では、ATTrackingManagerを使って許諾を得ていないユーザから、直接IDFAを取得してもその値はゼロとなってしまいます。
// IDFAの値 00000000-0000-0000-0000-000000000000つまり、現在使用しているアドネットワークの広告SDK(imobileやnendなど)も影響を受けます。
(SDK側で取得済みIDFAをキャッシュしていれば暫くは使用できるかも?)また、今言われている可能性として
「広告プロモーションを実施する側のアプリ」
→ プロモーションを実施した際に正しい効果測定、分析が出来なくなる可能性
すでにプロモーションを実施中の場合は今までのデータとの解離がおこる可能性があります。
また広告屋さんからは「広告配信の選択肢が減った」という話も出てくるかも知れません。
(特定のユーザを狙ったリターゲティング広告配信などが今後出来なくなったりする)「広告を掲載する側のアプリ」
→ 収益が減る
カジュアルゲーム系やメディア系などの広告収益モデルのアプリでは、アプリ内で配信している広告の効果が追えない事で、価値が下がり、CPM(1000回表示して稼げる金額)が低下・・・という現象が起こり、その結果収益が減る可能性があります。
これらの点については、モバイル計測プロバイダー各社がどの技術を採用するか、または別のソリューションをリリースしてくるか?によって大きく左右される事になると思います。
どちらにしてもアプリのアップデートは必須となってきそうです。
主な各計測プロバイダー(MMP)動向について
Adjust
ブログで対応方針などの情報を発信しています。
- Appleのユーザープライバシーにおける変更点について
- Mobile measurement in iOS 14: 4 possible scenarios
- iOS14の広告エコシステムの将来像
- iOS14と互換性がある粒度の細かいアトリビューション:アトリビューションハッシュ
- iOS14の変更に向けての準備と対策 | Adjust
Adjustの考えとしては
- 現時点のSKAdNetworkの仕様では役不足
- 取り急ぎフィンガープリント技術で対応
- アトリビューションハッシュを使った新たな仕組みを導入してく最後の「アトリビューションハッシュを使った仕組み」とは
デバイス上でIDFAとIDFA(一意のベンダーID)を使用してハッシュ値を生成。
このハッシュ値をサーバ側で許諾を得て取得しているIDFAと照合すると言うもの。ポイントとしては、前提として広告掲載側アプリでのIDFAは取得が必要な点と
ローカルでのみのIDFA使用(オプトインなし)がAppleから承認されるか?がキモとなる。オプトインなしで追跡可能な方法として、こちらで触れられているが。。。
You may track users without obtaining user permission through the AppTrackingTransparency framework if it is for one of the following purposes:
User Privacy and Data Use - App Store - Apple DeveloperAdjustでは、現在Apple側と協議中とのことです。
AppsFlyer
- The App Store Economy in a Privacy-Centric World | AppsFlyer
- AppsFlyer's Aggregated Attribution Solution - iOS14 | AppsFlyer
こちらの考えとしては
- より具体的な内容はiOS14のベータ版の過程で進化していくのでは?
- とにかくアトリビューション(どの広告効果なのか)は大事
- IDFAが取れない場合でも計測出来るソリューションを準備中このソリューションと言うのはIDFA、SKAdnetwork、フィンガープリント技術の組み合わせで対応していく様です。
(ここで出てくる「Aggregated Attribution」と言うのは具体的にはよく分かりませんでした。ざっくりとした大まかな測定の意味?)Singular
IDFA Alternative: SKAdNetwork launch by Singular
こちらは上記とは対照的に
- Apple公式のSKAdNetworkを使った仕組みで行く様です。(フィンガープリントなど他の技術も併用しつつ)
こちらはN3TWORKへのインタビュー。今後の展望について。
Prepare for iOS 14: Insights from N3TWORKここでは今後の展望について、フォローチャート形式で解説しています。
iOS attribution: visualizing the crossroads for mobile measurementまとめ
「IDFAオプトイン問題」は開発者には関係ある話なのか?
一見、この「IDFAオプトイン問題」は、そこまでアプリ開発者やエンジニアにとっては関係の無い話、それ程大きな問題では無い様に思えます。
ただし、広告プロモーションを行っていたり、アプリ内広告を行っている場合は、IDFAを取るにしても止めるにしても何かしらの対応が必須となり、今後も関係がある話となりそうです。
またIDFAを今後も積極的に取得する選択をとった場合、
「どの様にしてオプトイン率をあげていくか?」や「どの様にユーザーへ説明するのか?」などを
継続的にテストする為、アップデートしていくことになりそうです。
- 投稿日:2020-08-07T03:25:42+09:00
ARKitでDepthDataの深度情報を取得する方法
はじめに
この記事ではARKitを使ってフロントカメラからDepthDataを取得し、1pixelずつの深度情報を取得する方法を備忘録的にまとめます。
もしかしたらフロントカメラ以外にも使えるかもと思い、タイトルは少し広い内容をカバーできるようにしました。いろんな事情でiPhone11で顔認識をした時に深度情報がfloatで欲しいと思い、いろいろ調べたんですが、一つにまとまった記事が存在しなかったので、深夜テンションで記事を作ろうと思いました。
(筆者はSwiftを勉強しはじめて数ヶ月のド素人なので、コードや文章の書き方が拙いかもしれませんがお許しください...)
(改善点や技術的補足がある人は大歓迎です)対象となる人
- ARKitで深度情報が欲しくなった人
- (TrueDepth搭載のiPhoneのフロントカメラを使用する人)
- ポインタという概念がちょこっとだけ分かる人
筆者の環境
- Xcode11.6
- Swift
- iPhone11
- iOS13.5
要約
- CVPixelBufferをコネコネすればできる
- UnsafeMutableRawPointerからUnsafeMutablePointerに変換する
- UnsafeBufferPointerに変換してからArrayに変換する
- 欲しいピクセルの深度を1次元配列から取得すれば深度情報が得られる
コード概要
とりあえずコードを初めに見せてから要点を解説していきます。
分からない人はsessionメソッドの部分に中身を丸コピしてもいいかもしれません。ViewController.swiftfunc session(_ session: ARSession, didUpdate frame: ARFrame) { // nilチェック guard let depthData = frame.capturedDepthData else { return } let depthMap = depthData.depthDataMap // depthMapのCPU配置(?) CVPixelBufferLockBaseAddress(depthMap, .readOnly) let base = CVPixelBufferGetBaseAddress(depthMap) // 先頭ポインタの取得 let width = CVPixelBufferGetWidth(depthMap) // 横幅の取得 let height = CVPixelBufferGetHeight(depthMap) // 縦幅の取得 // UnsafeMutableRawPointer -> UnsafeMutablePointer<Float32> let bindPtr = base?.bindMemory(to: Float32.self, capacity: width * height) // UnsafeMutablePointer -> UnsafeBufferPointer<Float32> let bufPtr = UnsafeBufferPointer(start: bindPtr, count: width * height) // UnsafeBufferPointer<Float32> -> Array<Float32> let depthArray = Array(bufPtr) // depthMapのCPU解放(?) CVPixelBufferUnlockBaseAddress(depthMap, .readOnly) let fixedArray = depthArray.map({ $0.isNaN ? 0 : $0 }) print(fixedArray[width*200+400]) //(400,200)に対応する深度値 }では解説に入っていきます。
CVPixelBufferについて
ARKitから深度情報を取得しようとした人はおそらく、ARFrameのメンバ変数であるcapturedDepthDataに目が付き、その中にあるdepthDataMapまではたどり着いたと思います。
しかしこのdepthDataMapのクラスはCVPixelBufferで、それからどうやって値を取得するか分かりにくいかと思います。調べたところによると、CVPixelBufferはGPUのメモリ上にデータが置かれているため、そう簡単に値を取得することができないそうです。
いろいろな処理をするためにはCPUが管理できるメモリに配置される必要があります。CVPixelBufferはCIImageに変換でき、そこからUIImage等に変換できるため、DepthMapの描画や、グレースケール画像としての画素値を取得することができたとは思います。
しかし、depthDataMap(AVDepthData)のリファレンスによると
A depth map describes at each pixel the distance to an object, in meters.
(デプスマップは、各ピクセルでのオブジェクトまでの距離をメートル単位で表します。)とあるので、何かしらの方法を使えばメートル単位での深度情報が取得できることがわかります。距離が取れるのであれば、グレースケール画像の画素値では情報が削られてしまうのであまり好ましい方法ではありません。
そこで、CVPixelBufferをCPUで扱う関数が登場します。
CVPixelBufferLockBaseAddress
CVPixelBufferLockBaseAddress関数は、CVPixelBufferの値をGPUからCPUが扱えるメモリへ移行してくれるのです。これにより、GPUによりプログラムで扱えなかったデータが扱えるようになります。
(この感覚だと思っていますが確証はありません。そもそもGPU管理だと扱えないのかどうかすら怪しいので詳しい人がいたら教えて欲しいです)そして、一連の作業が終了したあとはCVPixelBufferUnlockBaseAddressでGPUに値を返してあげます。
(この行為に意味があるかは分かってないです)CPUで扱えるようになることで、CVPixelBufferGetBaseAddressを使えば先頭ポインタを取得することができます。
...ポインタ?
Swiftのポインタについて
Swiftでもポインタは避けて通れません。むしろObjective-cの名残も含め、C言語と親和性の高い作りになっていることが実感できました...
本題に戻ります。
UnsafeMutableRawPointer
CVPixelBufferGetAddressから取得できるのは、UnsafeMutableRawPointerという型のポインタです。
この型は、「変更ができない型なしのポインタ」という意味です。constでvoid的な感じです。変更できないのは参照する値が変更できないということだと認識しています。このUnsafeMutableRawPointerから深度情報を取得するためには、まずデータ型の情報を与えてあげなければなりません。それがUnsafeMutablePointerになります。
UnsafeMutablePointer<T>
UnsafeMutableRawPointerと見間違えそうですが、Rawがあるかどうかがポイントです。
UnsafeMutablePointerは型があるポインタです。型を指定するので、深度情報がより取得できそうです。深度情報はFloat32で保存されています。のでFloat32の型になるよう指定します。
(このことはCVPixelBufferGetPixelFormatTypeを使って調べることができます)UnsafeMutableRawPointerからUnsafeMutablePointerに変換するにはbindMemoryを使いました。(すいません仕様はよく分かってません)
引数capacity
は要素数だと思い、縦幅x横幅を指定しました。実はこの状態からも値は取得できるのですが、配列にしておいた方が何かと都合が良さそうだと思い、Arrayを目指します。
UnsafeBufferPointer<T>
UnsafeBufferPointerはいうなれば配列ポインタです。これを経由することで配列に変換することができます。
UnsafeBufferPointerへの変換はinitする形でOKです。UnsafeBufferPointerはcountをメンバ変数として持っているので、initでまた要素数を指定してあげる必要があります。(無駄を感じるのでもう少し簡単にできそうですが...)
最後に、Array()を使うことで、最終的にCVPixelBufferからArrayに変換することができます。お疲れ様です。
DepthDataMapの値を読み取る
無事配列にすることができたのですが、1次元配列なので、欲しいピクセルの深度値を取得するためには多少の計算をする必要があります。
index = width * y + xxとyは欲しい座標で、widthは画像の横幅、indexが配列で指定すべきインデックスとなります。
配列は左上から横に値を取得していったような形式になると思います。depthDataMapにおける無効値はNaNで表現されます。上記のプログラムでは無効値を0に置き換え、すべて有効な数字として扱えるようにしています。必須ではないので書かなくても大丈夫です。
そして!重要なことが1つ!
これはフロントカメラに言えることなのですが、取得した深度画像は、本来の向きから90度左を向いた状態で取得することになります。なので、カメラの解像度が480x640の縦長の画像だとしたら、Arrayで取得できるのは、640x480の横長の画像として取得することになります。
画像の中心の深度値を取得したい場合は、(240,320)ではなく、(320,240)を取得する必要があることにご注意ください。
(分かる人はそもそもの向きを修正することができると思いますが...)まとめ
- DepthDataから深度値は取れる!
- CVPixelBufferめんどい
- ポインタしんどい
- フロントカメラの向きがヤバイ
最後に
今回は深度情報を1次元配列にしましたが、おそらく2次元配列にする方法はあると思います。
すいませんそこまでやる方法が思いつかなかったので、何かやり方があれば教えてください?♂️ちなみに、iOS14からAVDepthDataの代わりにARDepthDataというものが増えるそうです。
でもARDepthDataのメンバ変数であるdepthDataMapはCVPixelBufferクラスなので、結局この記事が参考になるかもしれません。ここまで読んでいただきありがとうございました。