20190411のiOSに関する記事は10件です。

FirebaseとiAd.frameworkの連携についてわかったことメモ

やりたいこと

BigQuery内でSearch Adsのコンバージョンを集計させたい

FirebaseのApple Search Adsに関するドキュメント

Firebaseの集計データはBigQueryのデータをいい感じに集計して表示してくれている。Google広告と連携させていれば、どの広告をタップしてインストールしてくれたのかということがわかる。しかしAppleのSearch Adsに関しては、iOSアプリ側から何かしら入れてあげないと集計できていないことがわかった。

具体的に送られるイベントや、何が送られるのかはこちらを参照する
https://support.google.com/firebase/answer/6317518?hl=ja

Apple Search Adsに関して引用すると

Apple Search Ads
Apple Search Ads のクリックによってアプリがインストールされた場合、firebase_campaign イベントが上記のパラメータとともに記録され、キャンペーン関連のイベントについては次のデータが表示されます。

参照元 = Apple
メディア = search
キャンペーン = <iad キャンペーン名>
Apple Search Ads をトラッキングするには、アプリ用の Xcodeプロジェクトファイルに iAd フレーム ワークを追加する必要があります。

このようにAppleに関しても集計できそうということがわかる
iAd.frameworkを入れればFirebaseがいい感じに集計してくれそうということがわかる

BigQueryの調べ方

具体的にBigQueryに何が送られてくるのかは、こちらを参照する
https://support.google.com/firebase/answer/7029846?hl=ja&ref_topic=7029512

traffic_sourceを調べる具体例

traffic_sourceとは ユーザーを最初に獲得したトラフィック ソースの名前。このフィールドは当日テーブルでは使用されません。

叩くクエリ例

SELECT traffic_source.source,  COUNT(*) 
FROM `{テーブル名}` 
GROUP BY  traffic_source.source
LIMIT 100

下記のような感じで、データの結果が帰ってくる。この例では、テーブルを検索し、どの流入元が一番多いのか調べるために、数を数えた
おそらくSearch Adsで広告を打った場合、traffic_source.sourceAppleと入ってきてくれるはず

スクリーンショット 2019-03-04 17.41.43.png

スクリーンショット 2019-03-05 17.28.35.png

iAd.framework 導入について

iAd.frameworkのドキュメントには下記のようにフレームワークを入れると書いてある

iAd framework
The iAd framework is bundled with Xcode. Follow these steps to add the iAd framework to the Xcode project file for your app:
1. Go to your target view and select General.
2. Scroll down to a section called Linked Frameworks and Libraries and click the plus (+) icon.
3. In the dropdown menu, search for iAd.
4. Select iAd.framework and click the Add button.

AppleのiAd.frameworkのドキュメント

ドキュメントに従いtargetの Generalを選択し、Linked Frameworks and Librariesの項目の下の方のプラスボタンを押して,iAd.frameworkを導入する

問題点

iAd.frameworkを導入するだけで、本当にFirebaseが集計してくれるのか不安、Search Adsに課金しない限り調べる方法はないのか探していた

改善策

Xcode デバックコンソールでログを見る

image.png
image.png

ドキュメントリンク

https://firebase.google.com/docs/analytics/ios/events?hl=ja

Firebase Debug Viewで送信されている
イベントを確認する

image.png

気になるログを発見した

iAd.frameworkを導入するとログが変わっていることがわかった

導入前

xxxxApp[00000:000000]  Ad framework is not linked. Search Ad Attribution Reporter is disabled.
xxxxApp[00000:000000]  No data to upload. Upload task will not be scheduled
xxxxApp[00000:000000]  Analytics enabled

導入後

xxxxApp[00000:000000]  Scheduling Search Ad Report timer
xxxxApp[00000:000000]  Search Ad campaign report alarm scheduled to fire in approx. (s):

アプリ流入比率変えた.032.jpeg

結果

無事にFirebaseで送られてBigQuery内で確認することができた

アプリ流入比率変えた.034.jpeg

(おまけ)iAd.framework 導入によってアプリ内に、端末がどのキャンペーン経由でインストールされたのか取得する

Firebaseの連携とは全く関係ないのだが一応、必要な人もいるかも知れないので共有
iAd.frameworkを導入すると利用できるようになる関数ADClient.shared().requestAttributionDetailsを用いて,
辞書 attributionDetails の中に広告の情報が入っている。ここでとりだして、各自データを集計しているサーバーにおくればいいのではないかと思う

詳細に関してはAppleのiAd.frameworkのドキュメントに書いてある

import iAd
import UIKit

class TestViewController: UIViewController{

override func viewDidLoad() {
        super.viewDidLoad()
    ADClient.shared().requestAttributionDetails({ (attributionDetails, error) in

            if error == nil {
                for (type, adDictionary) in attributionDetails! {
                    var attribution = adDictionary as? Dictionary<AnyHashable, Any>;
                    let params = [
                        "appID": "self.appData.appID",
                        "iadAdgroupId": attribution?["iad-adgroup-id"] as? String as Any,
                        "iadAdgroupName": attribution?["iad-adgroup-name"] as? String as Any,
                        "iadAttribution": attribution?["iad-attribution"] as? String  as Any,
                        "iadCampaignId": attribution?["iad-campaign-id"] as? String as Any,
                        "iadCampaignName": attribution?["iad-campaign-name"] as? String as Any,
                        "iadClickDate": attribution?["iad-click-date"] as? String as Any,
                        "iadConversionDate": attribution?["iad-conversion-date"] as? String as Any,
                        "iadCreativeId": attribution?["iad-creative-id"] as? String as Any,
                        "iadCreativeName": attribution?["iad-creative-name"] as? String as Any,
                        "iadKeyword": attribution?["iad-keyword"] as? String as Any,
                        "iadLineitemId": attribution?["iad-lineitem-id"] as? String as Any,
                        "iadLineitemName": attribution?["iad-lineitem-name"] as? String as Any,
                        "iadOrgName": attribution?["iad-org-name"] as? String as Any

                    ]
                    print("////////////////////")
                    print(params)
                }
            }
        })
  }

}

参考文献

iAd frameworkをいれるだけでいいということに気がついた記事
https://groups.google.com/forum/#!topic/firebase-talk/dUxH5VfHG2k

http://www.nikola-breznjak.com/blog/ios/create-native-ios-app-can-read-search-ads-attribution-api-information/

https://stackoverflow.com/questions/50818600/ios-swift-search-ad-api-requestattributiondetails-just-returns-nil

https://forums.developer.apple.com/thread/66161

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

iOSアプリケーションにおける、Launch Screenの役割

iOS向けアプリケーションにおける、Launch Screenの役割

iOS向けアプリケーションを開発していると、Launch Screenを作る機会があるはずです。

僕もいくつか作ってきました。

Launch ScreenについてGoogle検索をしていると、Appleが意図した使い方ではない行為を推奨しようとしている記事をいくつか見つけてしまったので、Apple Human Interface Guidelinesにおける記述を合わせて、なるべくAppleの意図に沿う形で、Launch Screenの役割について説明していこうと思います。

1. Launch Screenとは

iOS向けアプリケーションという文脈で「Launch Screen」とは、旧来型の呼び名で言う、「スプラッシュスクリーンのようなもの」という説明で大筋を説明することができます。

スプラッシュスクリーンをご存じない方のために、説明させていただきますと、スプラッシュスクリーンとはアプリケーションが起動中の間の画面表示全般を指します。

起動中に表示される画面全般ですね。

2. Appleの定義するLaunch Screenとは

AppleはiOS向けアプリケーションに一貫したデザイン性・ユーザー体験を持たせるために、 Apple Human Interface Guidelinesというドキュメントを整備しています。

その中に、Launch Screenについての定義がありましたので、ここに転記します。
(https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/launch-screen/)

(原文版)
1. It’s solely intended to enhance the perception of your app as quick to launch and immediately ready for use.
2. The launch screen isn’t an opportunity for artistic expression.
3. Design a launch screen that’s nearly identical to the first screen of your app.
4. Don’t advertise. The launch screen isn’t a branding opportunity.

(邦訳版)
1. アプリの起動をすばやく開始し、すぐに使用できるようにすることを目的としています。
2. ローンチスクリーンは芸術的表現の機会ではありません。
3. アプリの最初の画面とほぼ同じ起動画面を設計します。
4. 宣伝しないでください。起動画面はブランディングの機会ではありません。

細かい説明

1つ1つ、細かく説明させていただきたいと思います。

a. すぐに使用できるようにデザインしなければならない

Launch Screenを安直に「スプラッシュスクリーン」と捉えてしまうと、よく陥りがちなのが

「とりあえずアプリのロゴをデザインする」

という解決策だと思います。

実際このパターンでやっているサービスは結構あります。

が、この行為はApple的には推奨ではありません。

iOS端末をお使いの方ならわかると思いますが、iOS端末の標準アプリ(App Store、iTunes Store、設定、メッセージ、メール、Map・・・etc)を開くたびにAppleのロゴが表示されることはありません。

b. 芸術的表現のためのものではない

これは、 a. すぐに使用できるようにデザインしなければならない の言い換えと捉えることもできるのですが、そもそもAppleはLaunch Screenの目的を

「アプリの起動をすばやく開始し、すぐに使用できるようにすること

としているため、Launch Screenでユーザーが待つ必要のある芸術性のある表現を行うことを してはならないと明言しています 。(禁止しています)

c. アプリの最初の画面とほぼ同じ画面にしよう

Launch Screenをアプリの最初の画面とほぼ同じ画面にすることにより、アプリが高速に動作しているようにユーザーに思わせることができます。

公式以外のアプリだと、Facebookや、Google Map、Slack、Whats Appなんかがとてもうまくできてますね。

d. ブランディングのために使ってはならない

無計画にロゴを出したりサウンドを出したりしてはいけません。Appleが明確に禁止しています。

これも a. すぐに使用できるようにデザインしなければならない の言い換えです。

AppleはHuman Interface Guidelinesで下記のように述べています

Don’t include logos or other branding elements unless they’re a static part of your app’s first screen.

邦訳 : ロゴや他のブランド要素がアプリの最初の画面の静的な部分でない限り、それらを含めないでください。

Launch Screenで安易にロゴを出すことによってブランディングを行うことは避けるべきです。

こぼれ話: そもそもなんでスプラッシュスクリーンは嫌われたのか

ユーザーインターフェース、ユーザーエクスペリエンスの権威とも言える、ヤコブ・ニールセンという方が、この問に対して一定の答えを提供しています。

https://u-site.jp/alertbox/interaction-cost-definition

彼は、ユーザビリティの究極の目標を下記のように述べています。

理想的には、ユーザーがサイトに行ったら、探している答えがすぐ目の前にあるといい。それはつまりインタラクションコストがゼロになるということで、ユーザビリティという領域の究極の目標はそこにあるからである。

インタラクションコストを彼は下記のように定義しています。

インタラクションコストとは、ユーザーが目標を達成するため、サイトとインタラクトするのに必要な、精神的・肉体的な努力の総計である。

上記の前提の元、スプラッシュスクリーン説明したものがこちらです。

アプリ起動後、最初に現れるのはスプラッシュ画面である:

この時点でインタラクションコストに含まれるのは、フラッシュ画面が消え、このアプリの最初の操作可能な画面のためのスペースができるのを数秒待つことである

つまり、スプラッシュスクリーンはうまくデザインしないと

存在するだけでインタラクションコストを発生させる

ものだと彼は説明しています。

この考え方に納得できる人は多いのではないでしょうか。

参考

https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/launch-screen/

https://u-site.jp/alertbox/interaction-cost-definition

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

UIColorで使っているRGBの値を取得する

概要

UIColor(例えば、UIColor.red)で使われているRGB、alphaの値を取得したい

前提

  • Xcode 10.1
  • Swift 4.2

tl;dr

  • CGColor から取ってくる。
(lldb) po UIColor.yellow.cgColor.alpha
1.0
(lldb) po UIColor.yellow.cgColor.components
▿ Optional<Array<CGFloat>>
  ▿ some : 4 elements
    - 0 : 1.0
    - 1 : 1.0
    - 2 : 0.0
    - 3 : 1.0
print(UIColor.red.cgColor.components![0]) // Red 1.0

extend!

  • UIColorのextensionとして定義して便利に使う
import UIKit

extension UIColor {
    var redColor: CGFloat? {
        return self.cgColor.components?[0]
    }

    var greenColor: CGFloat? {
        return self.cgColor.components?[1]
    }

    var blueColor: CGFloat? {
        return self.cgColor.components?[2]
    }

    var alpha: CGFloat {
        return self.cgColor.alpha
    }
}

print(UIColor.red.redColor) // Optional(1.0)
print(UIColor.red.blueColor) // Optional(0.0)
print(UIColor.red.greenColor) // Optional(0.0)
print(UIColor.red.alpha) // 1.0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swiftで特定の複数の型のみを許容するArray

1つの型を許す配列はもちろん初心者でもすぐ出来ると思います。
では例えば、 IntDouble のみを許す数値の配列を用意したい場合、どうすればいいでしょう。
手段と長短をまとめました。

手段

Enum を利用

enum EnumNumber {
    case int(Int)
    case double(Double)
}

var enumNumbers: [EnumNumber] = [.int(10), .double(2.0)]

Protocol を利用

protocol ProtocolNumber {}
extension Int: ProtocolNumber {}
extension Double: ProtocolNumber {}

var protocolNumbers: [ProtocolNumber] = [Int(10), Double(2.0)]

長所と短所

switch で値を取り出す

Protocol を使用した場合だと、全パターンを書いたとしても default 行を書かなくてはならないので、少し安全ではないコードになってしまうかも。

// Enum
switch enumNumbers.first! {
case .int(let value):       print(value)
case .double(let value):    print(value)
}

// Protocol
switch protocolNumbers.first! {
case let value as Int:      print(value)
case let value as Double:   print(value)
default:                    print("exception")
}

型が分かっている物の値を取得

Protocol だとキャストするだけで値が取れる( optional もしくは forced unwrap ですが )のに対し、
Enum だとおそらく if case 文を書かないといけないのではと思います。となるとインデントが深くなってしまいます。
インデントが深くなるのを回避するために パターン2 のように computed property を生やす手がありますが、もしこうするならば型の数だけ用意しなければなりません。

// Enum パターン1
if case .int(let value) = enumNumbers[0] {
    let enumInt = value
}

// Enum パターン2
extension EnumNumber {
    var integerValue: Int? {
        if case .int(let value) = enumNumbers[0] {
            return value
        }
        return nil
    }
}
let enumInt = enumNumbers[0].integerValue

// Protocol
let protocolInt = enumNumbers[0] as? Int

生成する時

大きな違いは、 Protocol の場合は Int か Double か分かる必要が無い という点です。
Enum の場合は Int か Double か把握した上で Enum を生成 する必要があります。
明らかに Enum のほうが面倒くさくなると思います。

let unknownValue: Any = Int(123)

// Enum パターン1
// このパターンだと、unknownValue が Double の場合、追加されない
if let intValue = unknownValue as? Int {
    enumNumbers.append(.int(intValue))
}

// Enum パターン2
extension EnumNumber {
    init?(_ value: Any) {
        switch value {
        case let value as Int:
            self = .int(value)
        case let value as Double:
            self = .double(value)
        default:
            return nil
        }
    }
}
if let value = EnumNumber(unknownValue) {
    enumNumbers.append(value)
}

// Protocol
if let value = unknownValue as? ProtocolNumber {
    protocolNumbers.append(value)
}

ネストしたい

Swift の設計上 Protocol はネストできません。

class Sample {

    // OK
    enum EnumNumber {}

    // error: protocol 'ProtocolNumber' cannot be nested inside another declaration
    protocol ProtocolNumber {}
}

まとめ

主観で ○ × つけました。

Enum Protocol
switch で値を取り出す ×
型が分かっている物の値を取得 ×
生成する時 ×
ネストしたい ×

まあこんな色々書きましたが、基本的には Protocol を使うべきですね笑

参考

環境

  • Swift 5.0

他にもなにかありましたら教えてください!

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

AutoLayout+StackViewで自動可変行テーブルビューを実装する

従来、UITableViewCellの内容に応じて行の高さを算出して可変にする実装はかなりめんどくさかった。
AutoLayoutとStackViewで自動的に可変になるように実装してみた。
AutoLayoutはSnapKitを使った。

  • Xcode 10.1
  • Swift 4.2

scroll.gif

サンプルコードは以下に配置
https://github.com/yumemi-ajike/AutoFlexibleRowHeight

UITableViewCellの内部レイアウト

パターン

UITableViewCell のサブクラスとして TableViewCell を追加し、
イメージが縦に配置される VerticalTableViewCell と左横に配置される HorizontalTableViewCell をそれぞれ TableViewCell を共通の親としてサブクラス化した。

イメージの縦配置 イメージの横配置 テキストのみ1行 テキストのみ複数行 テキスト1つのみ
vertical_cell.png horizontal_cell.png vertical_singletext_cell.png vertical_text_cell.png vertical_textonly_cell.png

組み合わせはもっとたくさんあるけどサンプルコードをビルドしたら確認可能なので割愛する。

イメージの縦配置

VerticalTableViewCell クラスで実装している。

ビュー構造

UITableViewCell.contentView に以下のビュー構造で追加していて、
それぞれの UIImageViewUILabelisHidden を変更することでレイアウト内容を変えている。

  • UIStackView
    • UIImageView
    • UILabel
    • UILabel
    • UILabel
  • UIView(下部separator)

表示順

addArrangedSubview した順番に表示される。

TableViewCell.swift
final class VerticalTableViewCell: TableViewCell {
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        contentView.addSubview(stackView)
        stackView.addArrangedSubview(thumbnailImageView)
        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(dateLabel)
        stackView.addArrangedSubview(descriptionLabel)

        stackView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview().inset(insets)
        }
        thumbnailImageView.snp.makeConstraints { (make) in
            make.width.equalTo(stackView.snp.width)
            // 16 : 9 and hidden priority
            make.height.equalTo(stackView.snp.width).multipliedBy(0.5625).priority(UILayoutPriority.defaultLow.rawValue)
        }
    }
    ...
}

stackView.axis = .vertical で縦方向に整列するようにし、 stackView.distribution = .equalSpacing で項目間のマージンを均一にしている。
セル内側のマージンは以下で定義して stackViewedgesinset する形でセットしている。

TableViewCell.swift
let insets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)

イメージサイズ

ImageViewの表示サイズの制約は16:9で表示するようにし、
非表示時の対応としてpriorityを .defaultLow にセットしている。
これがないとAutoLayoutの警告が出るので注意。

TableViewCell.swift
        thumbnailImageView.snp.makeConstraints { (make) in
            make.width.equalTo(stackView.snp.width)
            // 16 : 9 and hidden priority
            make.height.equalTo(stackView.snp.width).multipliedBy(0.5625).priority(UILayoutPriority.defaultLow.rawValue)
        }

イメージの横配置

HorizontalTableViewCell クラスで実装している。

ビュー構造

UITableViewCell.contentView に以下のビュー構造で追加。

  • UIStackView
    • UIView
      • UIImageView
    • UIStackView
      • UILabel
      • UILabel
      • UILabel
  • UIView(下部separator)

イメージの右横にテキスト行を配置する必要があるため、 UIStackView の入れ子構造とすることで実現している。
また UIImageView がひしゃげてしまったり伸びてしまったりすることを避けるため、 imageBaseView を追加することで回避している。

TableViewCell.swift
final class HorizontalTableViewCell: TableViewCell {
    private let imageBaseView = UIView()
    ...
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        let horizontalStackView = UIStackView()
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        horizontalStackView.axis = .horizontal
        horizontalStackView.alignment = .top
        horizontalStackView.distribution = .fill
        horizontalStackView.spacing = 8
        contentView.addSubview(horizontalStackView)

        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(descriptionLabel)
        stackView.addArrangedSubview(dateLabel)

        horizontalStackView.addArrangedSubview(imageBaseView)
        imageBaseView.addSubview(thumbnailImageView)
        horizontalStackView.addArrangedSubview(stackView)

        horizontalStackView.snp.makeConstraints { (make) in
            make.edges.equalToSuperview().inset(insets)
        }
        ...
    }
}

親のStackViewは horizontalStackView.axis = .horizontal で横方向に整列するようにし、
horizontalStackView.alignment = .top で上揃えになるようにしている。

イメージサイズ

ImageViewの表示サイズは固定で64 x 64になるように制約を設けている。

TableViewCell.swift
        imageBaseView.snp.makeConstraints { (make) in
            make.width.equalTo(64)
            make.height.greaterThanOrEqualTo(imageBaseView.snp.width).priority(UILayoutPriority.defaultLow.rawValue)
        }
        thumbnailImageView.snp.makeConstraints { (make) in
            make.size.equalTo(64)
            make.center.equalToSuperview()
        }

文字や画像のセット

各ビューに文字や画像などの情報を入れ、表示されないビューの isHidden を変更して表示を切り替えている。

TableViewCell.swift
    func configure(with item: Item) {
        thumbnailImageView.kf.setImage(with: item.imageUrl)
        isThumbnailHidden = item.imageUrl == nil
        titleLabel.text = item.title
        titleLabel.isHidden = item.title == nil
        dateLabel.text = item.date
        dateLabel.isHidden = item.date == nil
        descriptionLabel.text = item.description
        descriptionLabel.isHidden = item.description == nil
    }

動作確認

TableViewController.swiftTableViewCell.Item の配列を作ってパターンを網羅するようにしている。

TableViewController.swift
    private let items = [TableViewCell.Item(title: "Short title",
                                            date: nil,
                                            description: nil,
                                            imageUrl: nil),
                         TableViewCell.Item(title: "Short title",
                                            date: "April 7th, 2019",
                                            description: nil,
                                            imageUrl: nil),
                         ...
                         省略
                         ...
                         TableViewCell.Item(title: "Long title: A view that displays one or more lines of read-only text, often used in conjunction with controls to describe their intended purpose.",
                                            date: "April 7th, 2019",
                                            description: "The appearance of labels is configurable, and they can display attributed strings, allowing you to customize the appearance of substrings within a label. You can add labels to your interface programmatically or by using Interface Builder.",
                                            imageUrl: URL(string: "https://via.placeholder.com/320x180.png?text=Sample+Image")),
                         TableViewCell.Item(title: "Long title: A view that displays one or more lines of read-only text, often used in conjunction with controls to describe their intended purpose.",
                                            date: "April 7th, 2019",
                                            description: "The appearance of labels is configurable, and they can display attributed strings, allowing you to customize the appearance of substrings within a label. You can add labels to your interface programmatically or by using Interface Builder.",
                                            imageUrl: URL(string: "https://via.placeholder.com/320x180.png?text=Sample+Image"))]

感想

かなりシンプルになったし、コード量が激減するので嬉しい!
行高計算のめんどくささからも解放されて喜ばしいけど、AutoLayoutの制約エラーを解決していくのがメイン作業になってこれはこれで慣れが必要。。。
UIStackViewを入れ子構造にしたときにiOS10以前で発生する制約の不整合とかもあるのでその辺の解決にちょっと時間かかった。

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

Adobe XDでiOSアプリのスプラッシュ画像を一括書き出しする

Adobe XDとは

スクリーンショット 2019-04-11 15.34.57.png

Adobe XDはAdobeからリリースされているプロトタイピングツールです。
Adobeアカウントを作成(無料)すれば誰でも使うことができます。(無料!!!)
登録はこちらから。

以前はSkecthを使用してワイヤフレームの作成などをしていましたが、買い切り型からサブスクリプション型に移行されてしまい、出費も気になっていたところ、試しに使ってみたらすごく使いやすかったので、こっちに乗り換えました。

デザイナーでなくても直感的にワイヤフレームを作成できるので、エンジニアにもおすすめです。
自分は職種上プロジェクトマネジャーになりますが、お客さんの要望からXDでワイヤフレームを作成し、要件に落とし込んでからデザイナーへXDのファイルをそのまま共有しているので、要件定義からデザイン作成の工程がスムーズに出来ます。実装指示書や設計書への落とし込みも楽チンです。

今回やること

XDはプロトタイピングツールとして非常に優秀ですが、ちょっとした画像の作成なども簡単に出来てしまうので、今回は用意するのが以外に面倒なアプリのスプラッシュ画像(起動画像)の作成をXDでやってみたいと思います。

最近ではStory boardでスプラッシュ画像を設定するプロジェクトもありますが、まだImages.xcassetsでデバイス毎に画像を設定しているプロジェクトもあるかと思いますので、今回は後者向けの内容の記事になります。

必要なスプラッシュ画像

デバイス サイズ 倍率
iPhone Xs Max 1242 x 2688px 3x
iPhone Xr 828 x 1792px 2x
iPhone X/iPhone Xs 1125 x 2436px 3x
iPhone 6/7/8 Plus 1242 x 2208px 3x
iPhone 6/7/8 750 x 1334px 2x
iPhone 5/SE 640 x 1136px 2x
iPhone 4/4S 640 x 960px 2x

プロジェクトの作成

まずはXDをインストールして、XDアプリを起動し、新規プロジェクトを作成します。
適当に上メニューのiPhone X/XSを選択します。
スクリーンショット 2019-04-11 15.59.20.png

新規プロジェクトが作成され、iPhone X/XSと書かれたアートボードが表示されたと思います。
スクリーンショット 2019-04-11 16.00.40.png

スプラッシュを作成

作成されたアートボードに適当にオブジェクトを配置して画面を作成していきます。(適当に配置しました)
スクリーンショット 2019-04-11 16.08.01.png

デバイスサイズ毎にアートボードを作成

iPhone X/XSのスプラッシュ画像が出来たので、他のディスプレイサイズのアートボードも作成して、スプラッシュ画像を作成します。
スクリーンショット 2019-04-11 16.15.30.png

残念ながら、iPhone 4/4S用のスクリーンはXDには用意されていないので、適用なアートボードを追加して、サイズを変更します。
スクリーンショット 2019-04-11 16.18.42.png

アートボードの名前はダブルクリックで変更可能です。

追加したアートボードにも同様にオブジェクトを配置していきます。
コピペなどでもいけますが、シンボル化などを行うとより効率的に配置することが出来ます。(今回は説明を割愛します。)
オブジェクトのサイズなどはデバイス毎に見やすいように調整してください。
スクリーンショット 2019-04-11 16.24.37.png

画像を書き出す

スプラッシュの作成が終わったら、画像を書き出します。
メニューからファイル > 書き出し > すべてのアートボードを選択
スクリーンショット 2019-04-11 16.26.17.png

スクリーンショット 2019-04-11 16.28.30.png

保存先は適当に選択してください。
書き出し先にiOSを選択し、デザイン倍率は1xを選択してください。
設定が終わったらすべてのアードボードを書き出しを選択してください。

スクリーンショット 2019-04-11 16.30.27.png
指定したフォルダにすべてのアートボードの1x~3xサイズの画像が書き出されました。
あとはXcodeに設定するなりなんなりすればOKです!

おわりに

本来のXDの使い方とは異なりますが、こんなことも簡単に出来てしまうくらいXDは使いやすいです。
こういう形で管理できれば新しいデバイスが登場した際や、デザインを変更したいときに書き出す手間などが少なくできるのでいいのではないかなと思いました。

XDはプロトタイプの作成にとても優れているので、興味を持った方はぜひ使ってみていただければと思います。
職場でパワーポイントなどで一所懸命ワイヤーを作ってる人を見かけたらXDを勧めていますw

参考資料

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

[Swift4] バックグラウンドでオーディオ再生する

iOSアプリでオーディオ再生を実装する際に、アプリがバックグラウンドになっても再生し続けるようにする設定手順です。

必要な設定は2つです。

  1. Capabilities設定で、Background Modes - Audio をONにする。
  2. AVAudioSession のカテゴリをPlaybackに設定する。

Capabilities設定からBackground Modesを設定する

プロジェクトファイルを選択肢、「TARGETS」からアプリ名をクリックし、「Capabilities」を選択します。

この中の、 「Background Modes」をON に変更します。
さらに、「Modes」の中から、「Audio. AirPlay. and Picture in Picture」のチェックボックスをONにします。

以下の画面は、これらの設定をした後の画面です。

スクリーンショット 2019-04-09 16.45.14.png

AVAudioSession のカテゴリをPlaybackに設定する

続いて、AVAudioSessionの設定を行います。
AVAudioSessionは、アプリでオーディオをどのように扱うかを宣言するためのオブジェクトです。
例えば、他のアプリで音声再生をしているときの挙動や、バックグラウンドでの音声再生の挙動などを扱うことができます。
AVAudioSession - Apple Developer Document

AppDelegateにて、AVAudioSessionのCategory設定を変更してやります。

AppDelegate.swift
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        /// AVAudioSessionCategory設定
        let session = AVAudioSession.sharedInstance()
        do {
            // CategoryをPlaybackにする
            try session.setCategory(.playback, mode: .default)
        } catch  {
            // 予期しない場合
            fatalError("Category設定失敗")
        }

        // session有効化
        do {
            try session.setActive(true)
        } catch {
            // 予期しない場合
            fatalError("Session有効化失敗")
        }

        return true
    }

以上でオーディオ再生時をアプリがバックグラウンドに移動しても継続できるようになります。

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

UIレイヤーまでユニットテストを入れているOSSアプリ/フレームワーク/ライブラリポインタ集 

はじめに

UIレイヤーまでユニットテストをがっつりやっているOSSアプリ/フレームワーク/ライブラリです。

商用レベルなiOSオープンソースアプリ集と一部被りますが列挙してみました。

非UIなOSSフレームワーク/ライブラリについてはもはやテストコードがないと相手にすらされないので入れてません。 

Wire iOS

検証方法:スナップショット

TwitterKit

検証方法:プロパティを検証

FireFox

検証方法:無し

Artsy

検証方法:?

Kickstarter

検証方法:MVVM、スナップショット

SpreadsheetView

検証方法:?

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

dockerでiOSの疑似脱獄

iOSでは、制約に縛られることがかなり多いわけですが、dockerを使って、iOSを乗っ取れるかもしれない、などと夢想しています。

たまたまiPhoneのマイク、スピーカーを共有する必要のあるプロジェクトにかかわっていまして、アイデアの一つとして、可能性を追いかけたく思っています。

皆様のご意見等いただければ幸いです。:smile:

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

UIGraphicsBeginImageContextを脱魔術させる

指定色で画像を返せるようにしたいなぁ...。
「ググろう!...スタックオーバーフローにそれっぽいの書いてあるわ!」
「これ使えるっぽいからコピペしよ!」
「使えたわ!」

こんな経験ありませんか? 僕はあります。

ということで今回は、そのコピペコード/おまじないコードで何をやっているかを明らかにする。
「魔術コード1を脱魔術させる」のをテーマとして記事を書いていきます。

基本的には UIGrahics~ 周りのAPIが理解を妨げる原因となっているのを感じるので、その辺り突っ込んで「なぜ?」と考えていきます。

1.魔術コード

今回はこのコードをベースに議論していきたいと思います。
こちらは、色とサイズからUIImageを初期化する独自定義のイニシャライザです。

こちらの記事を参考にしました。
https://stackoverflow.com/questions/26542035/create-uiimage-with-solid-color-in-swift/39604716

image-from-color.swift
extension UIImage {

    public convenience init?(color: UIColor, size: CGSize) {
        UIGraphicsBeginImageContext(size)

        let rect = CGRect(origin: CGPoint.zero, size: size)
        let context = UIGraphicsGetCurrentContext()
        context?.setFillColor(color.cgColor)
        context?.fill(rect)

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        guard let cgImage = image?.cgImage else { return nil }
        self.init(cgImage: cgImage)
    }

}

2.UIGraphicsBeginImageContext?

UIGraphicsBeginImageContext(size)

まずこのコードが初見では「?」です。

SwiftのAPIは、だいたい雰囲気で分かるので知らなくても調べればすぐ使えるのですが、これに関しては「画像のコンテキストを開始する?んんん?」と思いました。

APIリファレンスを見てみましょう。

Creates a bitmap-based graphics context and makes it the current context

「ビットマップベースのグラフィックコンテキストを作成し、それをカレントのコンテキストに設定します。」と書いてあります。

...まずグラフィックコンテキストって何?

3.Graphic Context?

これに関しては、Appleが出している公式ドキュメントが存在します。(アーカイブかつ、古いバージョンですがこれが分かりやすかった。

Graphics Context Basicsを読んでみたので、軽く紹介と感想を入れてきますね!(結構がっつり端折ってます!

ざっくり何するの?

Graphics Context オブジェクトの主な役割は、描画環境の現在の状態に関する情報を管理することです。

描画環境の状態に関する情報を管理してくれるやつなのか... :thinking:

draw(_:)周りでしてること

カレントコンテキストがViewにフォーカスしてる間、パス、画像、テキスト、またはその他の必要なコンテンツを描画できます。 現在の描画環境の属性を変更して、コンテンツに必要な外観を実現することもできます。

「確かに、図形描画とかをGraphics Context使って行ってる時は、 setFillColor(:) とかして色変えたりしてた...。」

draw(_:)を抜けた後

draw(_:)メソッドがreturnした後、 Cocoaは次のビューのために描画環境をリセットするプロセスを経ます。描画環境に加えた変更をリセットし、次のビューの座標変換とクリッピング領域を設定して、独立したまっさらの環境を作ります。このプロセスはアプリケーションの各更新サイクル中に繰り返されます。

「レイアウトサイクルの間、draw(_:)の直前に該当のViewに着目して、色々描画したりして、抜けたら次のViewのためのまっさらな環境を作る!こんな感じの動きをするのか!」

なるほど!!!!

...最初に出てきたカレントコンテキストって何?

4.UIGraphicsGetCurrentContext()?

let context = UIGraphicsGetCurrentContext()

このコード、StackOverFlowなどでコピペコードを量産している時によく見かけるのですが、何をしているのかよくわかっていませんでした。

リファレンスの概要にはこう書いてあります。

Returns the current graphics context

どうやら、カレントのグラフィックスコンテキストを返してくれると書いてあるので、今回知りたい情報のようです。

これだけだと意味がわからないので、より詳しく書いてあるDiscussionの欄も読んでみます。

現在のグラフィックスコンテキストはデフォルトではnilです。 draw(_:)メソッドを呼び出す前に、ビューオブジェクトは有効なコンテキストをスタックにプッシュし、それを現在のものにします。 ただし、描画にUIViewオブジェクトを使用していない場合は、UIGraphicsPushContext(_:)関数を使用して手動で有効なコンテキストをスタックにプッシュする必要があります。
この関数はあなたのアプリのどのスレッドからも呼び出すことができます。

draw(_:)の前に、描画環境のあれこれをstackにpushして、 UIGraphicsGetCurrentContext() では、設定された現在のコンテキストを取得できるようになっているようです。

...ちなみにスタックってなんのスタック?

5.NSGraphicsContext?

グラフィックコンテキストのスタックについてハッキリ明言された記述は探すのに苦労したのですが、AppKitの中のNSGraphicsContextにそれっぽい事が書いてあります。

Graphics contexts are maintained on a stack. You push a graphics context onto the stack by sending it a saveGraphicsState() message, and pop it off the stack by sending it a restoreGraphicsState() message. By sending restoreGraphicsState() to a graphics context object you remove it from the stack, and the next graphics context on the stack becomes the current graphics context.

「グラフィックコンテキストはスタック上で維持されて、メッセージを送る事でpushとpopが可能です!保存したくなったらsaveで、復元したくなったらrestoreしてね!restore場合は、次のグラフィックコンテキストがカレントになるよ!」的な事が書かれてます。グラフィックスコンテキスト用のスタック構造が存在し、そちらで管理されてるイメージを持っています。

話を元に戻して、ここまでで見て来た内容を元に全体を整理していきます。

6.UIGraphicsBeginImageContext!

ここで最初のAPIを概要を再掲します。

ビットマップベースのグラフィックコンテキストを作成し、それをカレントのコンテキストに設定します。

スタックの話が詳しく書いてあるのが UIGraphicsBeginImageContextWithOptionsなので、そちらをみると

The drawing environment is pushed onto the graphics context stack immediately.

このコンテキストは即時にスタックにpushされるようです。つまりはカレントのコンテキストに設定されます。

これらを加味して情報を付け足して、自分なりにアレンジするとこんな感じ。

UIGraphicsBeginImageContext は、ビットマップベースの描画環境の現在の状態に関する情報管理オブジェクトを作成し、即時にグラフィックコンテキスト用のスタックにプッシュされます。それはカレントのコンテキストに設定されているので、 UIGraphicsGetCurrentContext() で取得する事が出来ます。」

おお!!!分かるやん!!!

7.ここまでの振り返り

  • Graphics Contextオブジェクトの役割は、描画環境の現在の状態に関する情報を管理すること
  • Graphics Contextは、それ専用のスタック構造で管理されている
  • draw(_:)が呼ばれる前に、viewオブジェクトが有効なコンテキストをスタックにpushしている
  • draw(_:)がリターンされた後に、描画環境リセットのプロセスが実行される
  • UIGraphicsGetCurrentContext()は、スタックtopのコンテキストを取得する
  • UIGraphicsBeginImageContext()は、bitmap-basedのコンテキストを作成し、即時スタックにpushしている

8.残りを駆け抜けて理解しよう

ここまで雰囲気がつかめていれば、あとは簡単なはずです。

こちらは2個目のコラムで「カレントコンテキストがViewにフォーカスしてる間、パス、画像、テキスト、またはその他の必要なコンテンツを描画できます。 現在の描画環境の属性を変更して、コンテンツに必要な外観を実現することもできます。」
と説明した内容に当たり、以下のコードではカレントコンテキストに対して塗りつぶし色を設定しています。

context?.setFillColor(color.cgColor)

指定領域を、コンテキストに設定してある塗りつぶし色で塗りつぶしています。

context?.fill(rect)

UIGraphicsGetImageFromCurrentImageContextは、カレントのグラフィクスコンテキストがbitmap-basedの場合のみ、その情報を使って画像を返します。そうでなければnilを返します。

let image = UIGraphicsGetImageFromCurrentImageContext()

最後に、UIGraphicsBeginImageContext(_:) によって作成されたbitmap-basedのコンテキストをスタックからpopしてお掃除しています。

UIGraphicsEndImageContext()

この guard let ~ else の辺りは、class func として宣言していれば、 return image で済むのですが、 convenience init として定義したかったので、自身のイニシャライザを呼び出さないといけない都合の問題です。cgImageを経由しています。

guard let cgImage = image?.cgImage else { return nil }
self.init(cgImage: cgImage)

9.脱魔術コード

ここまでで書き連ねた情報をコメントして書き加えながら、最初のコードを表示すると以下のようになります。

image-from-color.swift
extension UIImage {

    public convenience init?(color: UIColor, size: CGSize) {
        // ビットマップのグラフィックコンテキストを作成し、即時stackにpushされCurrentContextに設定する。
        UIGraphicsBeginImageContext(size)

        // 描画領域の指定
        let rect = CGRect(origin: CGPoint.zero, size: size)

        // stackのtopにいるグラフィックコンテキスト取得(直前で作成したビットマップのgraphicContext)
        let context = UIGraphicsGetCurrentContext()

        // グラフィックコンテキストにfillColorの設定をしている。
        context?.setFillColor(color.cgColor)

        // 指定領域をグラフィックコンテキストの色(直上で記述した)で満たす
        context?.fill(rect)

        // Currentのグラフィックコンテキストから画像を作成する(bitmap-basedの場合)
        let image = UIGraphicsGetImageFromCurrentImageContext()

        // UIGraphicsBeginImageContextで作成したコンテキストをスタックからpop
        UIGraphicsEndImageContext()

        // 初期化処理
        guard let cgImage = image?.cgImage else { return nil }
        self.init(cgImage: cgImage)
    }

}

どうでしょうか?雰囲気だけでも掴めたでしょうか?
自分も「CoreGraphics完全に理解した()2ぐらいの理解度になれました。やったね!

今回は、「コピペコードを理解ベースで進めていく=脱魔術」というテーマで取り組みましたが、みなさんが魔法使いを卒業できたとしたら筆者も嬉しい限りです!

10.UIGraphicsImageRenderer

ここまで長々と UIGraphicsBeginImageContext の脱魔術化がどうこうの話をしましたが、iOS10からは使う必要はありません。

クラメソ記事が詳しく解説しているのですが、以下のように書けます。

image-from-color-renderer.swift
extension UIImage {

    public convenience init?(color: UIColor, size: CGSize) {
        let renderer = UIGraphicsImageRenderer(size: size)

        guard let cgImage = renderer.image(actions: { rendererContext in
            rendererContext.cgContext.setFillColor(color.cgColor)
            rendererContext.fill(CGRect(origin: CGPoint.zero, size: size))
        }).cgImage else {
            return nil
        }

        self.init(cgImage: cgImage)
    }

}

ワオ!便利!!!

明示的にコンテキストのpushとpopをさせたり、カレントコンテキスト取得のコードを書く必要がないですね!!!

直近2世代カバーをAppleが強く推奨しているのでiOS9対応しているアプリはあまりないとは思いますが、画像を作成したい場合は可能な限りこちらのAPIを利用するのが良さそうです!

11.分からなかった事

saveGState()では、GraphicsContextごとにグラフィック状態のスタックを持っていると書いてあるのですが、GraphicsContextを管理するスタックもあるので、そちらの関係が良く分からなかったです。

この辺を詳しく調べていくと、iOSやCocoaのCoreGraphicsの世界へ足を踏み入れて行くのでしょうね。

分かり次第、追記か書き直しか別記事かで対応します!
お付き合いありがとうございました!!


  1. ここでは原理が分からない事象を魔法/魔術~と呼んでいます。 

  2. https://ja.wikipedia.org/wiki/ダニング=クルーガー効果 

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