Apple がまもなく導入する App Tracking Transparency(ATT)ポリシーでは、他の企業のアプリやウェブサイトの一部の情報を広告目的で使う場合、その許可を得ることが必須となります。これには、すでにユーザーの同意を得ている場合も含まれます。iOS エコシステムのデベロッパーや広告主はまだ適応する方法を模索している状況なので、今日は Google がどのようにコミュニティの準備をサポートしているかについてお知らせします。
Apple が ATT を変更することにより、広告がどの程度コンバージョン(アプリのインストールや販売)を促進しているかを示す重要な指標の一部が見えなくなります。これは、広告主による広告インプレッションの価値評価や入札に影響します。そのため、Apple の ATT ポリシーが適用されると、アプリの発行元は、iOS での Google 広告の収益に重大な影響が発生する可能性があります。iOS の収益化率を向上するには、Google Mobile Ads SDK のバージョン 7.64 にアップグレードし、SKAdNetwork サポートなどの新機能を利用することをお勧めします。アプリの発行元が準備できることの詳細は、こちらをご覧ください。
Google は、iOS 14 で広告主がキャンペーンの結果を正確に測定できるように、業界と連携して、SKAdNetwork の改善に関するフィードバックを Apple に提供しています。改善が行われるまでの間は、最新バージョンの Google Analytics for Firebase にアップグレードし、SKAdNetwork サポートなどの新機能を利用することをお勧めします。また、すべての iOS のアプリ キャンペーンのパフォーマンスや成果を細かく監視し、必要に応じて目標を達成できるように予算や入札を調整することをお勧めします。アプリの広告主が準備できることの詳細は、こちらをご覧ください。また、一連のガイドは Learn with Google 教育シリーズに掲載されています。
広告主がウェブベースのコンバージョン目標に向けてディスプレイ、動画などのキャンペーンをしている場合、Apple の ATT ポリシーが適用される際に実績が変動する可能性があります。この期間には、推定コンバージョンを拡張してより多くの iOS 14 トラフィックに対応できるようにする予定です。
Apple のポリシーが適用されると、現在広告目的で ATT に該当する(IDFA などの)情報を利用しているいくつかの Google 製 iOS アプリで、その情報を利用できなくなります。そのため、Apple のガイドに従い、これらのアプリには ATT プロンプトは表示しません。私たちは、App Store のすべての Google 製アプリについて、Apple のガイドを理解してそれに準拠する作業を懸命に進めています。新機能やバグの修正などで Google 製 iOS アプリがアップデートされると、アプリの掲載情報ページで App のプライバシーに関する詳細情報が新しくなるのを確認できます。
Google は、常にユーザーとプライバシーを最優先しています。透明性、選択肢、制御は、ユーザーに対する私たちの献身の根底であり、それは広告でも同様です。Google は、プライバシーと選択肢が確かに尊重され、広告によってサポートされる幅広いコンテンツにアクセスでき、活発でオープンなアプリのエコシステムをこれからも守り続けます。集計ソリューションやオンデバイス ソリューションなどのプライバシー保護技術に注力し続けているのはそのためです。現在、エコシステム パートナーとともにウェブで開発しているプライバシー サンドボックスもその 1 つです。
// Before Dart 2 Widget build(BuildContext context) { return new Container( height: 56.0, padding: const EdgeInsets.symmetric(horizontal: 8.0), decoration: new BoxDecoration(color: Colors.blue[500]), child: new Row( ... ), ); } // After Dart 2 Widget build(BuildContext context) => Container( height: 56.0, padding: EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration(color: Colors.blue[500]), child: Row( ... ), );
guard let deepLink = URL(string: "https://mydomain.com/page?param=value") else { return } let components = DynamicLinkComponents(link: deepLink, domain: domain) let iOSParams = DynamicLinkIOSParameters(bundleID: bundleID) iOSParams.minimumAppVersion = minVersion components.iOSParameters = iOSParams // ダイナミック リンクを生成する let link = components.url // またはショート リンクを生成する components.shorten { (shortURL, warnings, error) in if let error = error { print(error.localizedDescription) return } // TODO: shortURL を使用する }
String deepLink = "https://mydomain.com/page?param=value"; DynamicLink.Builder builder = FirebaseDynamicLinks.getInstance() .createDynamicLink() .setDynamicLinkDomain(domain) .setAndroidParameters(new DynamicLink.AndroidParameters.Builder() .setMinimumVersion(minVersion) .build()) .setLink(deepLink); // ダイナミック リンクを生成する DynamicLink link = builder.buildDynamicLink(); // またはショート リンクを生成する builder.buildShortDynamicLink() .addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(ShortDynamicLink shortDynamicLink) { // shortDynamicLink を使用する } });
compile "com.google.firebase:firebase-dynamic-links:11.0.0"
FirebaseDynamicLinks.getInstance().getDynamicLink(getIntent()) .addOnSuccessListener( new OnSuccessListener() { @Override public void onSuccess(PendingDynamicLinkData data) { if (data == null || data.getLink() == null) { // FDL は特にない。何もする必要がない return; } Intent launchIntent = data.getUpdateAppIntent(MainActivity.this); if (launchIntent != null) { startActivity(launchIntent); // アップグレード フローを起動する } Uri deepLink = dynamicLink.getLink(); String myAppItemId = deepLink.getQueryParameter("myAppItemId"); // TODO: myAppItemId のコンテンツを表示する } });
connectToFCM()
content-available
application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
application(_:didFailToRegisterForRemoteNotificationsWithError:)
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Oh no! Failed to register for remote notifications with error \(error)") }
didRegister...
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { var readableToken: String = "" for i in 0..<deviceToken.count { readableToken += String(format: "%02.2hhx", deviceToken[i] as CVarArg) } print("Received an APNs device token: \(readableToken)") }
application.registerForRemoteNotifications()
UNUserNotificationCenter.current().getNotificationSettings { (settings) in print("Alert setting is \(settings.alertSetting == UNNotificationSetting.enabled ? "enabled" : "disabled")") print("Sound setting is \(settings.soundSetting == UNNotificationSetting.enabled ? "enabled" : "disabled")") }
let authOptions : UNAuthorizationOptions = [.alert, .badge, .sound] UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { (granted, error) in if (error != nil) { print("I received the following error: \(error)") } else if (granted) { print ("Authorization was granted!") } else { print ("Authorization was not granted. :(") } }
curl 7.47.1 (x86_64-apple-darwin15.6.0) libcurl/7.47.1 OpenSSL/1.0.2f zlib/1.2.5 nghttp2/1.8.0 Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets
.p12
openssl pkcs12 -in MyApp_APNS_Certificate.p12 -out myapp-push-cert.pem -nodes -clcerts
ab8293ad24537c838539ba23457183bfed334193518edf258385266422013ac0d
> curl --http2 --cert ./myapp-push-cert.pem \ -H "apns-topic: com.example.yourapp.bundleID" \ -d '{"aps":{"alert":"Hello from APNs!","sound":"default"}}' \ https://api.development.push.apple.com/3/device/ab8293ad24537c838539ba23457183bfed334193518edf258385266422013ac0d
--cert
.pem
apns-topic
userNotificationCenter(_:willPresent:withCompletionHandler:)
completionHandler([.alert])
firInstanceIDTokenRefresh
func application(_ application: UIApplication, didFinishLaunchingWithOptions // ... printFCMToken() // This will be nil the first time, but it will give you a value on most subsequent runs NotificationCenter.default.addObserver(self, selector: #selector(tokenRefreshNotification), name: NSNotification.Name.firInstanceIDTokenRefresh, object: nil) application.registerForRemoteNotifications() //... } func printFCMToken() { if let token = FIRInstanceID.instanceID().token() { print("Your FCM token is \(token)") } else { print("You don't yet have an FCM token.") } } func tokenRefreshNotification(_ notification: NSNotification?) { if let updatedToken = FIRInstanceID.instanceID().token() { printFCMToken() // Do other work here like sending the FCM token to your server } else { print("We don't have an FCM token yet") } }
> curl --header "Content-Type: application/json" \ --header "Authorization: key=AU...the rest of your server key...s38txvmxME-W1N4" \ https://fcm.googleapis.com/fcm/send \ -d '{"notification": {"body": "Hello from curl via FCM!", "sound": "default"}, "priority": "high", "to": "gJHcrfzW2Y:APA91...the rest of your FCM token...-JgS70Jm"}'
{"multicast_id":86655058283942579,"success":1,"failure":0,"canonical_ids":0,"results":[{"message_id":"0:1486683492595106961%9e7ad9838bdea651f9"}]}
InvalidRegistration
priority
high
"content-available":
"content_available": true
"content-available"
FEATURE_NFC_HOST_CARD_EMULATION
SELECT user_dim.app_info.app_instance_id, user_dim.device_info.device_category, user_dim.device_info.user_default_language, user_dim.device_info.platform_version, user_dim.device_info.device_model, user_dim.geo_info.country, user_dim.geo_info.city, user_dim.app_info.app_version, user_dim.app_info.app_store, user_dim.app_info.app_platform FROM [firebase-analytics-sample-data:ios_dataset.app_events_20160601]
user_dim.app_info.app_instance_id
UNION ALL
SELECT user_dim.geo_info.country as country, EXACT_COUNT_DISTINCT( user_dim.app_info.app_instance_id ) as users FROM [firebase-analytics-sample-data:android_dataset.app_events_20160601], [firebase-analytics-sample-data:ios_dataset.app_events_20160601] GROUP BY country ORDER BY users DESC
user_properties
SELECT user_dim.user_properties.value.value.string_value as language_code, EXACT_COUNT_DISTINCT(user_dim.app_info.app_instance_id) as users, FROM [firebase-analytics-sample-data:android_dataset.app_events_20160601], [firebase-analytics-sample-data:ios_dataset.app_events_20160601] WHERE user_dim.user_properties.key = "language" GROUP BY language_code ORDER BY users DESC
SELECT event_dim.name, COUNT(event_dim.name) as event_count FROM [firebase-analytics-sample-data:android_dataset.app_events_20160601] GROUP BY event_dim.name ORDER BY event_count DESC
SELECT event_dim.params.value.int_value as virtual_currency_amt, COUNT(*) as num_times_spent FROM [firebase-analytics-sample-data:android_dataset.app_events_20160601] WHERE event_dim.name = "spend_virtual_currency" AND event_dim.params.key = "value" GROUP BY 1 ORDER BY num_times_spent DESC
SELECT user_dim.geo_info.city, COUNT(user_dim.geo_info.city) as city_count FROM TABLE_DATE_RANGE([firebase-analytics-sample-data:android_dataset.app_events_], DATE_ADD('2016-06-07', -7, 'DAY'), CURRENT_TIMESTAMP()), TABLE_DATE_RANGE([firebase-analytics-sample-data:ios_dataset.app_events_], DATE_ADD('2016-06-07', -7, 'DAY'), CURRENT_TIMESTAMP()) GROUP BY user_dim.geo_info.city ORDER BY city_count DESC
SELECT user_dim.app_info.app_platform as appPlatform, user_dim.device_info.device_category as deviceType, COUNT(user_dim.device_info.device_category) AS device_type_count FROM TABLE_DATE_RANGE([firebase-analytics-sample-data:android_dataset.app_events_], DATE_ADD('2016-06-07', -7, 'DAY'), CURRENT_TIMESTAMP()), TABLE_DATE_RANGE([firebase-analytics-sample-data:ios_dataset.app_events_], DATE_ADD('2016-06-07', -7, 'DAY'), CURRENT_TIMESTAMP()) GROUP BY 1,2 ORDER BY device_type_count DESC
user_dim.user_id
SELECT STRFTIME_UTC_USEC(eventTime,"%Y%m%d") as date, appPlatform, eventName, COUNT(*) totalEvents, EXACT_COUNT_DISTINCT(IF(userId IS NOT NULL, userId, fullVisitorid)) as users FROM ( SELECT fullVisitorid, openTimestamp, FORMAT_UTC_USEC(openTimestamp) firstOpenedTime, userIdSet, MAX(userIdSet) OVER(PARTITION BY fullVisitorid) userId, appPlatform, eventTimestamp, FORMAT_UTC_USEC(eventTimestamp) as eventTime, eventName FROM FLATTEN( ( SELECT user_dim.app_info.app_instance_id as fullVisitorid, user_dim.first_open_timestamp_micros as openTimestamp, user_dim.user_properties.value.value.string_value, IF(user_dim.user_properties.key = 'user_id',user_dim.user_properties.value.value.string_value, null) as userIdSet, user_dim.app_info.app_platform as appPlatform, event_dim.timestamp_micros as eventTimestamp, event_dim.name AS eventName, event_dim.params.key, event_dim.params.value.string_value FROM TABLE_DATE_RANGE([firebase-analytics-sample-data:android_dataset.app_events_], DATE_ADD('2016-06-07', -7, 'DAY'), CURRENT_TIMESTAMP()), TABLE_DATE_RANGE([firebase-analytics-sample-data:ios_dataset.app_events_], DATE_ADD('2016-06-07', -7, 'DAY'), CURRENT_TIMESTAMP()) ), user_dim.user_properties) ) GROUP BY date, appPlatform, eventName
pod update
userNotificationCenter:willPresentNotification: withCompletionHandler
application:didReceiveRemoteNotification: completionHandler
NSCalendarsUsageDescription
NSBluetoothPeripheralUsageDescription
plist
NSContactsUsageDescription
NSUserDefaults
application:didReceiveRemoteNotification
UIApplicationDelegate
UNUserNotificationCenterDelegate
brew install watchman npm install -g react-native-cli
react-native init GroceryApp # or whatever you want
atom GroceryApp # if you’re into Atom
react-native run-ios
Cmd+R
npm
npm install firebase --save
index.ios.js
import * as firebase from 'firebase';
// Initialize Firebase const firebaseConfig = { apiKey: "<your-api-key>", authDomain: "<your-auth-domain>", databaseURL: "<your-database-url>", storageBucket: "<your-storage-bucket>",, }; const firebaseApp = firebase.initializeApp(firebaseConfig);
const
React.createClass()
class GroceryApp extends Component { render() { return ( <View style="{styles.container}"> </View> ); } }
StyleSheet
var styles = StyleSheet.create({ container: { backgroundColor: '#f2f2f2', flex: 1, }, });
<View style="{styles.container}"> I’m a container lol! </View>
styles.js
module.exports
require()
const styles = require('./styles.js')
components
GroceryApp
'use strict'; import React, {Component} from 'react'; import ReactNative from 'react-native'; const styles = require('../styles.js') const constants = styles.constants; const { StyleSheet, Text, View, TouchableHighlight} = ReactNative; class ActionButton extends Component { render() { return ( <View style={styles.action}> <TouchableHighlight underlayColor={constants.actionColor} onPress={this.props.onPress}> <Text style={styles.actionText}>{this.props.title}</Text> </TouchableHighlight> </View> ); } } module.exports = ActionButton;
import React, {Component} from 'react'; import ReactNative from 'react-native'; const styles = require('../styles.js') const { View, TouchableHighlight, Text } = ReactNative; class ListItem extends Component { render() { return ( <TouchableHighlight onPress={this.props.onPress}> <View style={styles.li}> <Text style={styles.liText}>{this.props.item.title}</Text> </View> </TouchableHighlight> ); } } module.exports = ListItem;
'use strict'; import React, {Component} from 'react'; import ReactNative from 'react-native'; const styles = require('../styles.js') const { StyleSheet, Text, View} = ReactNative; class StatusBar extends Component { render() { return ( <View> <View style={styles.statusbar}/> <View style={styles.navbar}> <Text style={styles.navbarTitle}>{this.props.title}</Text> </View> </View> ); } } module.exports = StatusBar;
import React, {Component} from 'react'; import ReactNative from 'react-native'; import * as firebase from 'firebase'; const StatusBar = require('./components/StatusBar'); const ActionButton = require('./components/ActionButton'); const ListItem = require('./components/ListItem'); const styles = require('./styles.js');
_renderItem(item) { return ( <ListItem item="{item}" onpress="{()" ==""> {}} /> ); } render() { return ( <View style="{styles.container}"> <StatusBar title="Grocery List"> <ListView datasource="{this.state.dataSource}" renderrow="{this._renderItem.bind(this)}" style="{styles.listview}/"> <ActionButton title="Add" onpress="{()" ==""> {}} /> </View> ); }
render()
_renderItem()
constructor(props) { super(props); this.state = { dataSource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }) }; }
state
ListView.DataSource
ListView
componentDidMount()
componentDidMount() { this.setState({ dataSource: this.state.dataSource.cloneWithRows([{ title: 'Pizza' }]) }) }
setState()
this.itemsRef = firebaseApp.database().ref();
listenForItems(itemsRef) { itemsRef.on('value', (snap) => { // get children as an array var items = []; snap.forEach((child) => { items.push({ title: child.val().title, _key: child.key }); }); this.setState({ dataSource: this.state.dataSource.cloneWithRows(items) }); }); }
DataSnapshot
forEach(child)
.forEach
.key()
_key
dataSource.cloneWithRows(items)
cloneWithRows()
DataSource
componentDidMount() { this.listenForItems(this.itemsRef); }
ActionButton
AlertIOS
_addItem() { AlertIOS.prompt( 'Add New Item', null, [ { text: 'Add', onPress: (text) => { this.itemsRef.push({ title: text }) } }, ], 'plain-text' ); }
text
style
onPress
plain-text
secure-text
.push()
/items
<ActionButton title="Add" onpress="{this._addItem.bind(this)}"> </ActionButton>
_renderItem(item)
_renderItem(item) { const onPress = () => { AlertIOS.prompt( 'Complete', null, [ {text: 'Complete', onPress: (text) => this.itemsRef.child(item._key).remove()}, {text: 'Cancel', onPress: (text) => console.log('Cancel')} ], 'default' ); }; return ( <ListItem item="{item}" onpress="{onPress}"> ); }
.child(key)
item
.remove()
ListItem