- 投稿日:2019-04-11T22:12:35+09:00
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=jaApple 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=7029512traffic_sourceを調べる具体例
traffic_sourceとは ユーザーを最初に獲得したトラフィック ソースの名前。このフィールドは当日テーブルでは使用されません。叩くクエリ例
SELECT traffic_source.source, COUNT(*) FROM `{テーブル名}` GROUP BY traffic_source.source LIMIT 100下記のような感じで、データの結果が帰ってくる。この例では、テーブルを検索し、どの流入元が一番多いのか調べるために、数を数えた
おそらくSearch Adsで広告を打った場合、traffic_source.sourceにAppleと入ってきてくれるはず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.ドキュメントに従いtargetの Generalを選択し、Linked Frameworks and Librariesの項目の下の方のプラスボタンを押して,
iAd.frameworkを導入する問題点
iAd.frameworkを導入するだけで、本当にFirebaseが集計してくれるのか不安、Search Adsに課金しない限り調べる方法はないのか探していた改善策
Xcode デバックコンソールでログを見る
ドキュメントリンク
https://firebase.google.com/docs/analytics/ios/events?hl=ja
Firebase Debug Viewで送信されている イベントを確認する
気になるログを発見した
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):結果
無事にFirebaseで送られてBigQuery内で確認することができた
(おまけ)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
- 投稿日:2019-04-11T20:26:01+09:00
Swiftのイニシャライザはややこしい
久しぶりにSwiftのイニシャライザを復習したので、そのときに思ったことや新たに知ったことを、つれづれなるままに書いていきます。つれづれなるままなので「〜思います」系の表現があります。
環境はSwift5.0です。2つのクラスに継承関係があるとき、サブクラスのinitでは、親のinitを呼ぶ前に自分のところで定義したものを初期化するのはなぜか
class A1 { var name: String init() { print("super") name = "unknown" } } class B1: A1 { var age: Int override init() { print("sub") super.init() //エラー Property 'self.age' not initialized at super.init call age = 0 } }上の例ではB1のageが初期化されていないとエラーが出ます。初めてこの仕様を見たときは不思議な感じがしました。親を固めて(初期化して)から子を固めるべき、そう感じたからです。
しかし、上のコードの使い方が許されると、次の例ではサブクラスが固まってないのにサブクラスのsomeFunction()が呼び出されてしまうので、こういうのを防ぐ理由もあるかなと気が付きました。class A1 { var name: String init() { print("super") name = "unknown" someFunction() } func someFunction() { print("super someFunction") } } class B1: A1 { var age: Int override init() { print("sub") super.init() age = 0 } override func someFunction() { print("sub someFunction") super.someFunction() } }2つのクラスに継承関係があるとき、親のコンビニエンスinitと同じ引数のinitをサブクラスで指定initとして定義するときの挙動
今回いろいろ調べていてわかったことがあります。誤解を恐れずに書くと
2つのクラスに継承関係があって親が指定initとコンビニエンスinitを持つときに、サブクラスで親と同じ指定initを定義しないで、サブクラスでその指定initが自動生成されると同時に親クラスのコンビニエンスinitも自動生成される条件が整った状況で、親クラスのコンビニエンスinitと同じ引数のinitをサブクラスで指定initとして定義すると、例えばoverrideをつけるように催促するエラーが出ることもなく、コンパイルが通る
です。class A2 { var name: String init(name: String) { print("super 1") self.name = name } convenience init() { print("super 2") self.init(name: "unknown") } } class B2: A2 { init() { print("sub") super.init(name: "unknown") } } var b2 = B2() //出力 //sub //super 12つのクラスに継承関係があって親の指定initが2つあるとき、サブクラスでその片方をコンビニエンスinitとして定義するときの挙動
わかったことの2つめは
2つのクラスに継承関係があって親クラスが指定initを2つ持つときに、その片方と同じ引数のinitをサブクラスでコンビニエンスinitとして定義すると、もう片方の指定initは自動生成されて、サブクラスで定義したコンビニエンスinitはoverrideをつけるように催促され、overrideをつけるとそのコンビニエンスinitからself.init()で自動生成された指定initが呼び出せる
です。class A2 { var name: String init(name: String) { print("super 1") self.name = name } init() { print("super 2") self.name = "unknown" } } class B2: A2 { convenience override init() { print("sub") self.init(name: "unknown") } } var b2 = B2() //出力 //sub //super 1おわりに
*** で詳しく書いてあります。などの情報があれば、よろしくおねがいします。
- 投稿日:2019-04-11T19:44:48+09:00
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.0print(UIColor.red.cgColor.components![0]) // Red 1.0extend!
- 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
- 投稿日:2019-04-11T19:31:03+09:00
Swiftで特定の複数の型のみを許容するArray
1つの型を許す配列はもちろん初心者でもすぐ出来ると思います。
では例えば、IntとDoubleのみを許す数値の配列を用意したい場合、どうすればいいでしょう。
手段と長短をまとめました。手段
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を使うべきですね笑追記: 折衷案
@herara_ofnir3 さんにコメントをいただきました!
上記の表で見ても良いとこ取りできてます。enum EnumNumber { case int(Int) case double(Double) } protocol EnumNumberConvertible { var enumNumber: EnumNumber { get } } extension Int : EnumNumberConvertible { var enumNumber: EnumNumber { return .int(self) } } extension Double : EnumNumberConvertible { var enumNumber: EnumNumber { return .double(self) } } // 配列を用意 var numbers: [EnumNumberConvertible] = [10, 0.2] // 1. switch で値取り出し for number in numbers { switch number.enumNumber { case .int(let value): print(value) case .double(let value): print(value) } } // 2. 型が分かっている物の値を取得 let value = numbers[0] as? Int // 3. 生成する時 let unknownValue: Any = Int(123) if let value = unknownValue as? EnumNumberConvertible { numbers.append(value) }参考
環境
- Swift 5.0
他にもなにかありましたら教えてください!
- 投稿日:2019-04-11T18:53:29+09:00
AutoLayout+StackViewで自動可変行テーブルビューを実装する
従来、UITableViewCellの内容に応じて行の高さを算出して可変にする実装はかなりめんどくさかった。
AutoLayoutとStackViewで自動的に可変になるように実装してみた。
AutoLayoutはSnapKitを使った。
- Xcode 10.1
- Swift 4.2
サンプルコードは以下に配置
https://github.com/yumemi-ajike/AutoFlexibleRowHeightUITableViewCellの内部レイアウト
パターン
UITableViewCellのサブクラスとしてTableViewCellを追加し、
イメージが縦に配置されるVerticalTableViewCellと左横に配置されるHorizontalTableViewCellをそれぞれTableViewCellを共通の親としてサブクラス化した。
イメージの縦配置 イメージの横配置 テキストのみ1行 テキストのみ複数行 テキスト1つのみ 組み合わせはもっとたくさんあるけどサンプルコードをビルドしたら確認可能なので割愛する。
イメージの縦配置
VerticalTableViewCellクラスで実装している。ビュー構造
UITableViewCell.contentViewに以下のビュー構造で追加していて、
それぞれのUIImageViewやUILabelのisHiddenを変更することでレイアウト内容を変えている。
- UIStackView
- UIImageView
- UILabel
- UILabel
- UILabel
- UIView(下部separator)
表示順
addArrangedSubviewした順番に表示される。TableViewCell.swiftfinal 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で項目間のマージンを均一にしている。
セル内側のマージンは以下で定義してstackViewのedgesにinsetする形でセットしている。TableViewCell.swiftlet insets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)イメージサイズ
ImageViewの表示サイズの制約は16:9で表示するようにし、
非表示時の対応としてpriorityを.defaultLowにセットしている。
これがないとAutoLayoutの警告が出るので注意。TableViewCell.swiftthumbnailImageView.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.swiftfinal 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.swiftimageBaseView.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.swiftfunc 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.swiftでTableViewCell.Itemの配列を作ってパターンを網羅するようにしている。TableViewController.swiftprivate 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以前で発生する制約の不整合とかもあるのでその辺の解決にちょっと時間かかった。
- 投稿日:2019-04-11T13:34:14+09:00
[Swift4] バックグラウンドでオーディオ再生する
iOSアプリでオーディオ再生を実装する際に、アプリがバックグラウンドになっても再生し続けるようにする設定手順です。
必要な設定は2つです。
- Capabilities設定で、Background Modes - Audio をONにする。
- AVAudioSession のカテゴリをPlaybackに設定する。
Capabilities設定からBackground Modesを設定する
プロジェクトファイルを選択肢、「TARGETS」からアプリ名をクリックし、「Capabilities」を選択します。
この中の、 「Background Modes」をON に変更します。
さらに、「Modes」の中から、「Audio. AirPlay. and Picture in Picture」のチェックボックスをONにします。以下の画面は、これらの設定をした後の画面です。
AVAudioSession のカテゴリをPlaybackに設定する
続いて、AVAudioSessionの設定を行います。
AVAudioSessionは、アプリでオーディオをどのように扱うかを宣言するためのオブジェクトです。
例えば、他のアプリで音声再生をしているときの挙動や、バックグラウンドでの音声再生の挙動などを扱うことができます。
AVAudioSession - Apple Developer DocumentAppDelegateにて、AVAudioSessionのCategory設定を変更してやります。
AppDelegate.swiftfunc 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 }以上でオーディオ再生時をアプリがバックグラウンドに移動しても継続できるようになります。
- 投稿日:2019-04-11T09:53:43+09:00
URLSessionを使ってGET通信【Swift】
URLSession
例として郵便番号から住所を取得するとします。
今回利用させて頂くAPI:http://zipcloud.ibsnet.co.jp/doc/api
※プロダクションでは
!せずにguardなどで安全に処理してくださいvar component = URLComponents(string: "http://zipcloud.ibsnet.co.jp/api/search")! component.queryItems = [URLQueryItem(name: "zipcode", value: "150-0011")] URLSession.shared.dataTask(with: component.url!) { (data, response, error) in let json = String(data: data!, encoding: .utf8)! print(json) }.resume()console{ "message": null, "results": [ { "address1": "東京都", "address2": "渋谷区", "address3": "東", "kana1": "トウキョウト", "kana2": "シブヤク", "kana3": "ヒガシ", "prefcode": "13", "zipcode": "1500011" } ], "status": 200 }APIから取得したJSONは
Codableという機能を使って型に変換することができます。URLSessionでもシンプルに実装できたと思います。
他のFrameworkを使う前にURLSessionの使い方を理解しておくのがおすすめです。
- 投稿日:2019-04-11T07:17:27+09:00
UIレイヤーまでユニットテストを入れているOSSアプリ/フレームワーク/ライブラリポインタ集
はじめに
UIレイヤーまでユニットテストをがっつりやっているOSSアプリ/フレームワーク/ライブラリです。
商用レベルなiOSオープンソースアプリ集と一部被りますが列挙してみました。
非UIなOSSフレームワーク/ライブラリについてはもはやテストコードがないと相手にすらされないので入れてません。
Wire iOS
検証方法:スナップショット
TwitterKit
検証方法:プロパティを検証
FireFox
検証方法:無し
Artsy
検証方法:?
Kickstarter
検証方法:MVVM、スナップショット
SpreadsheetView
検証方法:?













