20200807のSwiftに関する記事は14件です。

NSImageのサイズを変更してPNGファイルを出力する

実装

  • 検索するといくつも実装例が見つかりますが、最終的に下記を参考にしました。
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 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%になっていることが確認できます。

image

image

解決していない問題

  • ご存じの方、お教えいただけると助かります…。

NSImageを経由するとサイズが異なる

image

いくつかの情報が異なってしまう

  • 解像度の値が異なる
    • 解像度はNSBitmapImageRep.sizeに依存している?
  • ColorSyncプロファイルが異なる

image

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

NSImageのサイズを縮小してPNGファイルを出力する

実装

  • 検索するといくつも実装例が見つかりますが、最終的に下記を参考にしました。
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
    }
}
パラメータ
NSImage.size (1680, 1050)
NSBitmapImageRep.size (1680, 1050)
NSBitmapImageRep.pixels (3360, 2100)
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%になっていることが確認できます。

-w628

解決していない問題

  • ご存じの方、お教えいただけると助かります…。

NSImageを経由するとサイズが異なる

  • 読み込み元のファイルをそのまま出力しているつもりですが、ファイルサイズに微妙な差異が見られます。
  • representation(using:properties:)のpropertiesで何かしら設定しないといけない?

image

ColorSyncプロファイルが異なる

image

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

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.swift
func sayHello(name: String) {
    print("Hello, \(name)!")
}

Sources/Hello/main.swiftを以下のようにする。

Sources/Hello/main.swift
if CommandLine.arguments.count != 2 {
    print("Usage: hello NAME")
} else {
    let name = CommandLine.arguments[1]
    sayHello(name: name)
}

すると、main.swiftから、Greeter.swiftsayHelloを呼び出せるようになる。

% 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" も同様の結果になる。
どうやって読み込めばいいんだろう?

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

CALayerアニメーションで開始位置を変更した際に迷ったことを整理した

CoreAnimationを使って、吹き出しのアニメーションを作った際に

  • アニメーションがViewの中心から始まってしまう
  • Viewの表示位置がズレてしまう

という点について迷ってしまいましたので、自分なりに整理してみることにしました。

内容を整理するにあたって、日常生活の会話を題材に、以下の4つの点から開始するアニメーションを作成しました。

  • 閃きのアニメーション (画像の下中央からのアニメーション)
  • 吹き出しのアニメーション (画像の左右中央からのアニメーション)
  • 汗マークのアニメーション (画像の上中央からのアニメーション)

アニメーション自体は、Viewのスケールを変えているだけになります。

動かした感じ

アニメーションの開始位置を変更する

アニメーションの開始位置の指定は、CALayerのanchorPoint1を指定することで変更が可能です。

閃きのアニメーション

エナジードリンクを飲む前に、添える一言を思いついた瞬間を表すアニメーションです。

anchorPointで指定する際は、次のような座標をイメージすると対応を進め易いです。
電球.png

電球のアニメーションでは、の位置から始めたいので、座標はxが0.5、yが1.0になります。

デフォルトでは、この値が(0.5,0.5)になっているので、アニメーションがViewの中央から始まります。

電球のviewをlightBulbViewとすると

lightBulbView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)

と指定することで、の位置からアニメーションが開始されます。

吹き出しのアニメーション

エナジードリンクを飲む直前に添えた一言の吹き出しのアニメーションです。

吹き出し.png

吹き出しのアニメーションは、の位置から開始したいので、xが1.0、yを0.5に指定します。

speechBubbleView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)

汗マークのアニメーション

聞き返された後のアニメーションです。

汗マーク.png

吹き出しのアニメーションは、の位置から開始したいので、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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftFormat(0.45.0以降)で想定外の整形結果になったので解決方法をまとめました

はじめに

私のチームではソースコードを綺麗な状態で保つために、OSSのSwiftFormatを利用してソースコードの自動整形(フォーマット)を行なっています。
今回、SwiftFormat0.45.0以降にアップデートしたところ、今までは自動整形されていなかった箇所まで変更されてしまう事象が2つ発生したため、解決方法についてまとめました :thumbsup:

実行環境

環境 バージョン
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.md
Renamed --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の内容に従って、名前を変更することで無事解決できました :thumbsup:

  # 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.md
Added --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オプションに現状のソースコードにより近い値を指定することで、(ほぼ)解決ということにしました。
(もっといい解決方法を知っている方いましたら、ぜひ教えてください :raised_hand:

補足

類似問題として、--guardelsenext-line or auto(default) にした場合に else {のインデントがずれる事象も観測されましたが、こちらの事象は障害のようでした。
すでに以下のIssueとして管理され、fixed in develop ラベルが付与されている状態のため、次のリリース(たぶん、0.45.4だと思われる)で修正される予定のようです。

最後に

今回取り上げた内容以外にも新しい機能が複数追加されているため、上記とは異なる問題が起きている方もいるかもしれません。
そのような場合は、まずCHANGELOG.mdの内容確認をオススメします。

参考情報


  1. ちゃんとCHANGELOG.mdを読んでいなかった自分も悪いと思いつつ、セマンティック バージョニングの考え方からするとマイナーバージョンのアップで互換性が失われるのはどうなんだろう?:thinking: という気もしています。  

  2. --disableで無効化できないか試してみたのですが、上手くいきませんでした。ソースコードを自動整形(フォーマット)して綺麗に保つという趣旨からすると、何もしないという動作は許容したくないのかも?けど、無効化できるオプションもあるしなー:thinking:  

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

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メソッドを呼び出し、そこに追加処理を書く。
という感じ。

おしまい。

 参考サイト

[Swift入門] overrideの意味と使い方
メソッドのオーバーライド

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

Apple Store スクリーンショットサイズ まとめ

Appプレビューとスクリーンショット

iPhone 6.5 インチディスプレイ

1242 * 2688 or 2688 * 1242

iPhone 5.5 インチディスプレイ

1242 * 2208 or 2208 * 1242

iPad Pro 第3世代 12.9 インチディスプレイ

2048 * 2732 or 2732 * 2048

iPad Pro 第2世代 12.9 インチディスプレイ

2048 * 2732 or 2732 * 2048

プレビュー -> ツール -> サイズの変更を行うのがおすすめ

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

iOS14からエンジニアがすべきIDFA対応

はじめに

9月リリースが噂されているiOS14から、
iOS13まで端末単位で設定していたIDFAの取得許可(追跡型広告制限)
iOS13.jpg

が端末兼アプリ単位の取得許可に変更され、
各アプリにて、通知や位置情報許可のように、ユーザに許可を取る必要がでてきます。

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/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.swift
import 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のアップデート予定を告知していただきました?‍♂️

ユーザさんにメリットを伝えつつサービスの目的も達成しうる対応をしていきましょう?

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

iOS14でエンジニアがすべきIDFA対応

はじめに

9月リリースが噂されているiOS14から、
iOS13まで端末単位で設定していたIDFAの取得許可(追跡型広告制限)
iOS13.jpg

が端末兼アプリ単位の取得許可に変更され、
各アプリにて、通知や位置情報許可のように、ユーザに許可を取る必要がでてきます。

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.swift
import 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のアップデート予定を告知していただきました?‍♂️

ユーザさんにメリットを伝えつつセキュリティを担保しつつ
サービスの目的も達成しうる対応をしていきましょう?

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

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)
とおこなってきたが、

今回
Distribution failed with errors.png

こんな感じで怒られちゃった。

自分ができた解決策は、
(Version , Build) : (1.0.1 , 1)
にしたら、うまく行くことができた。

Build 番号を1.0.3とかではなく、1つの数字で書くらしい

[参照]

https://www.it-swarm.dev/ja/ios/app-store%E3%81%AE%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E6%99%82%E3%81%AB%E3%81%A9%E3%81%AEios%E3%82%A2%E3%83%97%E3%83%AA%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E3%83%93%E3%83%AB%E3%83%89%E7%95%AA%E5%8F%B7%E3%82%92%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%AA%E3%83%A1%E3%83%B3%E3%83%88%E3%81%99%E3%82%8B%E5%BF%85%E8%A6%81%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%81%8B%EF%BC%9F/1043794503/

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

[チュートリアル]カウントアップアプリを作ってSwiftを触ってみよう

今回のチュートリアルで想定している人

  • スマホアプリを作ってみたい!
  • Swiftをやってみたかった!
  • 1人で始める自信がない・・・
  • 何か新しい言語に触れてみたい!

みんなウェルカム:grinning:


今回のチュートリアルで学べること

  • ボタンをタップしてカウントアップ
  • if文
  • メソッド
  • StoryBoardの基本的な使い方
  • 画面遷移 v1
  • 通知機能 v2

ソースコードはこちらから

https://github.com/techiro/CountUpAppForBeginners


早速Xcodeを開いて新規プロジェクトを作成

Xcodeを起動 → Create New Xcode ProjectSingle View App を指定して次へ。

Product Name とOrganization Nameは、任意の名前を指定。

Languageは Swift、User Interface は Storyboard を指定して次へ。


ボタンとラベルをStoryBoardに置いてみる

まず画面を作成していきます。

Main.StoryBoardを開くとiPhoneの画面がポツンと表示されています。

ここにボタンやラベルを置いていきます。

スクリーンショット 2020-08-04 19.16.30.png


UIパーツの配置方法

  1. Main.StoryBoardを選択
  2. 右上の+ボタンをクリック
  3. UIパーツ一覧が表示される
  4. 1つのLabelと2つのボタンを画面に配置

スクリーンショット_2020-08-07_9_09_43.png

ボタンに画像を反映

  1. Main.storyboardを開く
  2. ボタンを選択

画面右のAttribute Inspectorをクリック

スクリーンショット_2020_08_07_13_24.png

Attribute Inspectorについて

UIのいろんな設定ができる

ボタンの例

  • ボタンの色
  • ボタンの大きさ
  • ボタンの画像

などを変更できる。

画像をプロジェクトに追加する(Attribute Inspectorを触ってみる)

今回使用した+ボタンと-ボタン画像はiconfinderの画像を使用しました。画像はなんでもOK!

追記

+ーボタンがダウンロードできないエラーが生じているので、好きなアイコンをダウンロードしてください!

画像をダウンロードしたらプロジェクトのAssets.xcassetsにドラッグ&ドロップ

全画面_2020_08_04_19_38.png

ポイント

Assets.xcassetsフォルダはアプリ内にある画像を保存して簡単に呼び出せる場所、
積極的に使っていきましょう!

ボタンのimageを変更

ボタンを選択Attribute InspectorImage

スクリーンショット 2020-08-04 19.45.47.png

左のボタンと右のボタンにーボタン、+ボタンをセット


シミュレーターで確認

スクリーンショット_2020-08-04_19_47_57.png

画面左上再生ボタンを押すとシミュレーターが起動します。
ショートカットコマンドは⌘ + R

ここまで完成したら、次はUIパーツとコードを紐づける。


UIパーツとコードを紐づける

エディタをカスタムする

Assistantエディタをクリック

ボタンをクリックすると出てきます。

スクリーンショット_2020-08-04_20_13_59.png

Assistantエディタを出現させた状態で、UIパーツのラベルをクリック
コントロールを押しながらコードのほうにドラッグ&ドロップ

スクリーンショット_2020_08_04_20_48.png

UIパーツから青い線が伸びるのでoverride func viewDidLoadの上で離す。

スクリーンショット 2020-08-04 20.51.19.png

このようにポップアップが出現します。

ここでこのラベルの名前などが設定できます。

Nameを任意の文字列に変更(今回はcountLabel)

Connectボタンを押すとプログラムコードUIパーツが紐付けられ,
文字を変更したりボタンを押された時の動きなどをプログラムすることができます。


ボタンの設定方法

ボタンはviewDidLoadの下に紐付けます。

注意点

ポップアップの1番上の項目がActionになっているか確認してください。Actionになっていないと「ボタンを押された時」という条件を作ることができません。

スクリーンショット_2020-08-04_20_53_01.png

ボタンの名前

+ボタン→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))でもいいが

⌥ + クリックでポップアップが出現する
スクリーンショット_2020_08_07_13_51.png

かなり便利なので使ってみてください!


カウントアップ・ダウンの完成

@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に興味が湧いた方は参考にしてみてください!

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

[Swift]カウントアップアプリを作ってSwiftを触ってみよう

今回のチュートリアルで想定している人

  • スマホアプリを作ってみたい!
  • Swiftをやってみたかった!
  • 1人で始める自信がない・・・
  • 何か新しい言語に触れてみたい!

みんなウェルカム:grinning:


今回のチュートリアルで学べること

  • ボタンをタップしてカウントアップ
  • if文
  • メソッド
  • StoryBoardの基本的な使い方
  • 画面遷移 v1
  • 通知機能 v2

ソースコードはこちらから

https://github.com/techiro/CountUpAppForBeginners


早速Xcodeを開いて新規プロジェクトを作成

Xcodeを起動 → Create New Xcode ProjectSingle View App を指定して次へ。

Product Name とOrganization Nameは、任意の名前を指定。

Languageは Swift、User Interface は Storyboard を指定して次へ。


ボタンとラベルをStoryBoardに置いてみる

まず画面を作成していきます。

Main.StoryBoardを開くとiPhoneの画面がポツンと表示されています。

ここにボタンやラベルを置いていきます。

スクリーンショット 2020-08-04 19.16.30.png


UIパーツの配置方法

  1. Main.StoryBoardを選択
  2. 右上の+ボタンをクリック
  3. UIパーツ一覧が表示される
  4. 1つのLabelと2つのボタンを画面に配置

スクリーンショット_2020-08-07_9_09_43.png

ボタンに画像を反映

  1. Main.storyboardを開く
  2. ボタンを選択

画面右のAttribute Inspectorをクリック

スクリーンショット_2020_08_07_13_24.png

Attribute Inspectorについて

UIのいろんな設定ができる

ボタンの例

  • ボタンの色
  • ボタンの大きさ
  • ボタンの画像

などを変更できる。

画像をプロジェクトに追加する(Attribute Inspectorを触ってみる)

今回使用した+ボタンと-ボタン画像はiconfinderの画像を使用しました。画像はなんでもOK!

追記

+ーボタンがダウンロードできないエラーが生じているので、好きなアイコンをダウンロードしてください!

画像をダウンロードしたらプロジェクトのAssets.xcassetsにドラッグ&ドロップ

全画面_2020_08_04_19_38.png

ポイント

Assets.xcassetsフォルダはアプリ内にある画像を保存して簡単に呼び出せる場所、
積極的に使っていきましょう!

ボタンのimageを変更

ボタンを選択Attribute InspectorImage

スクリーンショット 2020-08-04 19.45.47.png

左のボタンと右のボタンにーボタン、+ボタンをセット


シミュレーターで確認

スクリーンショット_2020-08-04_19_47_57.png

画面左上再生ボタンを押すとシミュレーターが起動します。
ショートカットコマンドは⌘ + R

ここまで完成したら、次はUIパーツとコードを紐づける。


UIパーツとコードを紐づける

エディタをカスタムする

Assistantエディタをクリック

ボタンをクリックすると出てきます。

スクリーンショット_2020-08-04_20_13_59.png

Assistantエディタを出現させた状態で、UIパーツのラベルをクリック
コントロールを押しながらコードのほうにドラッグ&ドロップ

スクリーンショット_2020_08_04_20_48.png

UIパーツから青い線が伸びるのでoverride func viewDidLoadの上で離す。

スクリーンショット 2020-08-04 20.51.19.png

このようにポップアップが出現します。

ここでこのラベルの名前などが設定できます。

Nameを任意の文字列に変更(今回はcountLabel)

Connectボタンを押すとプログラムコードUIパーツが紐付けられ,
文字を変更したりボタンを押された時の動きなどをプログラムすることができます。


ボタンの設定方法

ボタンはviewDidLoadの下に紐付けます。

注意点

ポップアップの1番上の項目がActionになっているか確認してください。Actionになっていないと「ボタンを押された時」という条件を作ることができません。

スクリーンショット_2020-08-04_20_53_01.png

ボタンの名前

+ボタン→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))でもいいが

⌥ + クリックでポップアップが出現する
スクリーンショット_2020_08_07_13_51.png

かなり便利なので使ってみてください!


カウントアップ・ダウンの完成

@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に興味が湧いた方は参考にしてみてください!

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

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

SKAdNetwork

StoreKit Frameworkのクラス。
このクラス自体はiOS11.3から存在していて「どの広告からのインストールか?」などを判断するために使用したりします。
(主にアフィリエイト用途)

これが今回のiOS14で「どのアプリの広告から来たか?」等のより細かい粒度の情報が追える様に更新されました。
(それでも計測プロバイダーからは実用的な効果測定のためには役不足と言われている)

Appleとしては、今後の広告効果計測にはこのクラスを使った方法を推していくのだと思いますが、
これに従うかどうかでは、各社計測プロバイダーでも方向性が分かれているようです。

AppTrackingTransparency

AppTrackingTransparency

今回新しく追加されたフレームワーク。
これまでは「AdSupport framework」のASIdentifierManagerからIDFAを直接取得していました。

iOS14ではこのフレームワークを使いユーザから許諾を得てから、IDFAを取得する必要があります。
(最終的にASIdentifierManagerからIDFAを取ってくることは変わりない)

iOS 14、iPadOS 14、tvOS 14では、AppTrackingTransparencyフレームワークを通じてユーザーの許可を得ない限り、デベロッパがユーザーを追跡したり、ユーザーのデバイスの広告識別子にアクセスしたりすることはできません。
ユーザーのプライバシーとデータの使用より | Apple Developer

iOS14での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

ブログで対応方針などの情報を発信しています。

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 Developer

Adjustでは、現在Apple側と協議中とのことです。

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を今後も積極的に取得する選択をとった場合、
「どの様にしてオプトイン率をあげていくか?」「どの様にユーザーへ説明するのか?」などを
継続的にテストする為、アップデートしていくことになりそうです。


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

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.swift
func 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 + x

xと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クラスなので、結局この記事が参考になるかもしれません。

ここまで読んでいただきありがとうございました。

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