User Notifications frameworkでローカル通知を送ってみる
iOS10から使えるようになったUser Notifications frameworkを試してみました。
通知の許可を得る
まずはXcodeにUser Notifications frameworkを追加します。
許可を得る処理は以下の通りです。
引数で使いたいオプションを指定します。
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound], completionHandler: { result, error in })
これはiOS9以前での以下の書き方に相当します。
let settings = UIUserNotificationSettings(types: [.badge, .alert, .sound], categories: nil) UIApplication.shared.registerUserNotificationSettings(settings)
通知を作成する
通知の作成は以下の通りです。
今回は10秒後に起動する通知を作成しました。
let content = UNMutableNotificationContent() content.title = "たいとる" content.subtitle = "さぶたいとる" content.body = "ほんぶん" content.badge = NSNumber(value: 1) content.sound = UNNotificationSound.default() let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false) let request = UNNotificationRequest(identifier: "Identifier", content: content, trigger: trigger) let center = UNUserNotificationCenter.current() center.add(request)
上ではTimeIntervalを使ってTriggerを作りましたが、DateComponentを使ってTriggerを作る事もできます。
let component = DateComponents(calendar: Calendar.current, year: 2017, month: 3, day: 27, hour: 23, minute: 30) let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: false)
UNUserNotificationCenterにはdelegate
import UserNotifications @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let content = UNMutableNotificationContent() content.title = "たいとる" let component = DateComponents(calendar: Calendar.current, year: 2017, month: 3, day: 27, hour: 23, minute: 30) let trigger = UNCalendarNotificationTrigger(dateMatching: component, repeats: false) let request = UNNotificationRequest(identifier: "Identifier", content: content, trigger: trigger) let center = UNUserNotificationCenter.current() center.delegate = self center.add(request) return true } } extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { // バックグラウンドで来た通知をタップしてアプリ起動したら呼ばれる } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { // アプリがフォアグラウンドの時に通知が来たら呼ばれる } }
フォアグラウンドで通知が来た時、下のようにcompletionHandlerを実行すれば通知を表示する事ができます。
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .badge, .sound]) }
通知のキャンセルはremoveAllPendingNotificationRequestsメソッドを使います。
removePendingNotificationRequestsメソッドでIDを指定したキャンセルも可能です。
let center = UNUserNotificationCenter.current() center.removeAllPendingNotificationRequests() center.removePendingNotificationRequests(withIdentifiers: ["Identifier"])
GameplayKitの「Entities and Components」を試してみる
GameplayKitの「Entities and Components」を試してみました。
Entities and Componentsとは
Entities and Componentsとは、コンポーネント指向プログラミングをサポートするものです。
コンポーネント指向プログラミングとは、そのオブジェクトの性質を継承などではcomponentの追加を使って表すものです。
例えばタップ時にジャンプするオブジェクトにはジャンプコンポーネントを追加を、ランダムに移動するオブジェクトにはランダム移動コンポーネントの追加をすると言った具合です。
そうすることで、簡単に1つのオブジェクトが複数の性質を持たせる事ができます。
Entities and Componentsの実装
早速実装してみます。
今回はNodeを2つ作って、片方は横移動+タップで回転、もう片方は縦移動+タップで回転するようにします。
まずは画面にNodeを2つ表示します。
import SpriteKit import GameplayKit class GameScene: SKScene { let node1 = SKSpriteNode(color: UIColor(red: 0.1, green: 0.8, blue: 0.8, alpha: 1), size: CGSize(width: 100, height: 100)) let node2 = SKSpriteNode(color: UIColor(red: 0.8, green: 0.8, blue: 0.1, alpha: 1), size: CGSize(width: 100, height: 100)) override func didMove(to view: SKView) { node1.position.x = -100 node2.position.x = 100 addChild(node1) addChild(node2) } }
次にnode1は右方向、node2は上方向に動くようにします。
まずは上方向に移動する性質を表すHorizontalComponentクラスと横方向に移動する性質を表すVerticalComponentクラスを作ります。
class HorizontalComponent: GKComponent { let node: SKNode init(node: SKNode) { self.node = node super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func update(deltaTime seconds: TimeInterval) { node.position.x += 1 } } class VerticalComponent: GKComponent { let node: SKNode init(node: SKNode) { self.node = node super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func update(deltaTime seconds: TimeInterval) { node.position.y += 1 } }
続けて今作ったクラスをnode1、node2に適用します。
import SpriteKit import GameplayKit class GameScene: SKScene { let horizontalComponentSystem = GKComponentSystem(componentClass: HorizontalComponent.self) let verticalComponentSystem = GKComponentSystem(componentClass: VerticalComponent.self) var lastTime: TimeInterval = 0 let node1 = SKSpriteNode(color: UIColor(red: 0.1, green: 0.8, blue: 0.8, alpha: 1), size: CGSize(width: 100, height: 100)) let node2 = SKSpriteNode(color: UIColor(red: 0.8, green: 0.8, blue: 0.1, alpha: 1), size: CGSize(width: 100, height: 100)) let entity1 = GKEntity() let entity2 = GKEntity() override func didMove(to view: SKView) { node1.position.x = -100 node2.position.x = 100 addChild(node1) addChild(node2) let horizontalComponent = HorizontalComponent(node: node1) entity1.addComponent(horizontalComponent) horizontalComponentSystem.addComponent(horizontalComponent) let verticalComponent = VerticalComponent(node: node2) entity2.addComponent(verticalComponent) verticalComponentSystem.addComponent(verticalComponent) } override func update(_ currentTime: TimeInterval) { if lastTime == 0 { lastTime = currentTime } let deltaTime = currentTime - lastTime horizontalComponentSystem.update(deltaTime: deltaTime) verticalComponentSystem.update(deltaTime: deltaTime) lastTime = currentTime } }
これでnode1が右方向、node2が上方向に移動するようになりました。
entity1とentity2は下のupdateメソッドのように、片方のコンポーネントから別のコンポーネントのメソッドを呼び出す時に便利です。
class HorizontalComponent: GKComponent { let node: SKNode init(node: SKNode) { self.node = node super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func update(deltaTime seconds: TimeInterval) { node.position.x += 1 entity?.components.flatMap { $0 as? VerticalComponent }.forEach { $0.update(deltaTime: seconds) } } }
最後に、それぞれのnodeに「画面タップで回転」という性質を追加します。
「画面タップで回転」という性質を表すRotateComponentクラスを下のように作成します。
class RotateComponent: GKComponent { let node: SKNode init(node: SKNode) { self.node = node super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func rotate() { node.run(SKAction.rotate(byAngle: 2, duration: 1)) } }
今作ったcomponentをnode1とnode2に反映します。
import SpriteKit import GameplayKit class GameScene: SKScene { let horizontalComponentSystem = GKComponentSystem(componentClass: HorizontalComponent.self) let verticalComponentSystem = GKComponentSystem(componentClass: VerticalComponent.self) let rotateComponentSystem = GKComponentSystem(componentClass: RotateComponent.self) var lastTime: TimeInterval = 0 let node1 = SKSpriteNode(color: UIColor(red: 0.1, green: 0.8, blue: 0.8, alpha: 1), size: CGSize(width: 100, height: 100)) let node2 = SKSpriteNode(color: UIColor(red: 0.8, green: 0.8, blue: 0.1, alpha: 1), size: CGSize(width: 100, height: 100)) let entity1 = GKEntity() let entity2 = GKEntity() override func didMove(to view: SKView) { node1.position.x = -100 node2.position.x = 100 addChild(node1) addChild(node2) let horizontalComponent = HorizontalComponent(node: node1) entity1.addComponent(horizontalComponent) horizontalComponentSystem.addComponent(horizontalComponent) let verticalComponent = VerticalComponent(node: node2) entity2.addComponent(verticalComponent) verticalComponentSystem.addComponent(verticalComponent) // ここから今回追加 let rotateComponent1 = RotateComponent(node: node1) let rotateComponent2 = RotateComponent(node: node2) entity1.addComponent(rotateComponent1) entity2.addComponent(rotateComponent2) rotateComponentSystem.addComponent(rotateComponent1) rotateComponentSystem.addComponent(rotateComponent2) } override func update(_ currentTime: TimeInterval) { if lastTime == 0 { lastTime = currentTime } let deltaTime = currentTime - lastTime horizontalComponentSystem.update(deltaTime: deltaTime) verticalComponentSystem.update(deltaTime: deltaTime) lastTime = currentTime } // ここから今回追加 override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { rotateComponentSystem.components.flatMap { $0 as? RotateComponent }.forEach { $0.rotate() } } }
これで画面タップでnodeが回転するようになりました。
Storyboardの多言語対応
Storyboardの多言語対応を試してみました。
まずはプロジェクトに言語設定を追加します。
プロジェクト設定で言語を選択して下さい。
下のような画面になるので、多言語対応するStoryboardを選択します。
選択したStoryboardには、以下のように.stringsファイルが作られます。
次はStoryboardにラベルを配置します。
次は日本語用の.stringsファイルに先程配置したラベルの日本語表示を追加します。
上画像のBOM-ZI-JF1
はラベルのObject-IDになります。
下画像の右下の箇所で確認する事ができます。
言語設定が英語の状態でアプリを起動すると下のようになります。
言語設定を日本語にしたら、以下のように切り替わってくれました。
UISegmentControlやUIButtonは下の方法で対応できました。
"BOM-ZI-JF1.text" = "テストラベル!!"; "2ee-nm-Qy8.segmentTitles[0]" = "セグメント1"; "2ee-nm-Qy8.segmentTitles[1]" = "セグメント2"; "w32-UA-4Oj.normalTitle" = "ボタン";
UIImageViewだけはStoryboardで対応する方法が見つからなかったため、コード上で対応する必要がありそうです。
SDWebImageライクなライブラリ、Kingfisherを使ってみる
Kingfisherというライブラリを使ってみました。
こちらはSDWebImageにインスパイアされて作ったもので、画像のダウンロードが簡単にできるものになっています。
インストール
いつものようにCocoapodsを使います。
target 'MyApp' do use_frameworks! pod 'Kingfisher' end
使い方
以下のように画像の読み込みを行う事ができます。
import UIKit import Kingfisher class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let v = UIImageView(frame: CGRect(x: 10, y: 10, width: 100, height: 100)) v.kf.setImage(with: URL(string: "https://cdn-ak.f.st-hatena.com/images/fotolife/l/llcc/20151012/20151012161841.png")) view.addSubview(v) } }
実行すると以下のように画像が表示されます。
setImageにはプレースホルダー画像、完了後のコールバックなどを渡す事ができます。
import UIKit import Kingfisher class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let v = UIImageView(frame: CGRect(x: 10, y: 10, width: 100, height: 100)) v.kf.setImage(with: URL(string: "https://cdn-ak.f.st-hatena.com/images/fotolife/l/llcc/20151012/20151012161841.png"), placeholder: #imageLiteral(resourceName: "placeholder"), options: nil, progressBlock: { receivedSize, totalSize in }, completionHandler: { image, error, cacheType, imageURL in }) view.addSubview(v) } }
オプションではキャッシュ方法やScaleなどを設定できます。
形式は、以下enumの配列になります。
public enum KingfisherOptionsInfoItem { case targetCache(ImageCache) case downloader(ImageDownloader) case transition(ImageTransition) case downloadPriority(Float) case forceRefresh case forceTransition case cacheMemoryOnly case onlyFromCache case backgroundDecode case callbackDispatchQueue(DispatchQueue?) case scaleFactor(CGFloat) case preloadAllGIFData case requestModifier(ImageDownloadRequestModifier) case processor(ImageProcessor) case cacheSerializer(CacheSerializer) case keepCurrentImageWhileLoading }
setImageにはUIButtonにも追加されています。
こちらは引数にUIControlStateを渡す必要があります。
import UIKit import Kingfisher class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let b = UIButton(frame: CGRect(x: 10, y: 10, width: 100, height: 100)) b.kf.setImage(with: URL(string: "https://cdn-ak.f.st-hatena.com/images/fotolife/l/llcc/20151012/20151012161841.png"), for: .normal, placeholder: #imageLiteral(resourceName: "placeholder"), options: nil, progressBlock: { receivedSize, totalSize in }, completionHandler: { image, error, cacheType, imageURL in }) view.addSubview(b) } }
【fastlane】1プロジェクトに複数アプリの入っている場合のメタデータ管理
1つのプロジェクトで複数アプリをリリースしている場合のメタデータ管理方法です。
複数アプリが全く同じメタデータならいいのですが、アイコンやスクリーンショットを別々にする場合少し工夫をする必要があります。
fastlaneの設定方法はこちらをご参照下さい。
複数アプリ対応
まずは1アプリに対応します。
プロジェクトのルートで以下コマンドを実行します。
fastlane init
アプリのメタデータが以下のような構成でダウンロードされます。
2つ目以降のアプリは以下のように別フォルダを指定してダウンロードします。
fastlane deliver download_screenshots --app_identifier com.example.other --screenshots_path fastlane/screenshots_other_app fastlane deliver download_metadata --app_identifier com.example.other --metadata_path fastlane/metadata_other_app
これで別アプリのメタデータがフォルダに保存されます。
別アプリへのメタデータのアップロードは以下のようにオプション付ける事で実現できます。
fastlane deliver --app_identifier com.example.other --screenshots_path fastlane/screenshots_other_app --metadata_path fastlane/metadata_other_app
上コマンドはFastfileに登録しておけば簡単に実行できるようになります。
fastlane update_metadata_other_app
platform :ios do lane :update_metadata_other_app do deliver( app_identifier: 'com.example', screenshots_path: 'fastlane/screenshots_other_app', metadata_path: 'fastlane/metadata_other_app', ) end end
InAppPurchase用ライブラリ、RMStoreを使ってみる
RMStoreというiPhoneのアプリ内課金を楽にしてくれるライブラリを使ってみました。
インストール
インストールはcocoapodsを使います。
pod 'RMStore'
使い方
商品情報は以下のように取得します。
引数にproductIdを渡せばSKProductの配列が返ってきます。
let productIds = ["com.example.item"] RMStore.default().requestProducts(productIds, success: { products, invalidIdentifiers in self?.products = products as? [SKProduct] }, failure: { error in })
購入処理はaddPaymentメソッドを使います。
レシートチェックなどは引数で渡されるSKPaymentTransactionクラスのインスタンスを使います。
RMStore.default().addPayment("com.example.item", success: { transaction in if transaction?.transactionState != .purchased { return } }, failure: { transaction, error in })
リストア処理は以下の通りです。
購入履歴が引数として渡されるので、それを使って購入したかの判定をします。
RMStore.default().restoreTransactions(onSuccess: { [weak self] transactions in if let transactions = (transactions as? [SKPaymentTransaction]), transactions.map({ $0.payment.productIdentifier }).contains("com.example.com") { print("商品情報がありました") } else { print("商品情報が見つかりませんでした") } }, failure: { error in })
今回は試していないのですが、コンテンツダウンロードも対応しているようです。
【iOS】iTunesConnectのSalesデータ取得プログラムを動かす【新方式に対応】
iTunesConnectの売上やダウンロード数などのデータを取得するプログラムに関する話です。
今まではiTunesConnectのSalesデータをAutoIngestion Toolというものを使って取得できました。
しかしこのツールは非推奨になり、2016年12月で停止してしました。
AppleからはReporterというツールを使うよう通知があったので今回はそれを試してみました。
ダウンロード
ダウンロードは下ページのSetup
にあるダウンロードリンクから行います。
ダウンロードすると、Reporter.jar
とReporter.properties
の2つのファイルが入っています。
Reporter.jar
が実際にデータを取得するプログラムで、Reporter.properties
はユーザー情報を書くファイルです。
Salesデータの取得方法
まずは認証情報を記入します。
Reporter.properties
を開くと以下のようになっているので、userIDとpasswordを埋めます。
userID = password = Mode=Robot.xml SalesUrl=https://reportingitc-reporter.apple.com/reportservice/sales/v1 FinanceUrl=https://reportingitc-reporter.apple.com/reportservice/finance/v1
入力が終わったらReporter.jarを使ってSalesデータを取得します。
java -jar Reporter.jar p=Reporter.properties Sales.getReport [vendor id], Sales, Summary, Daily, 20161201
vendor idは以下コマンドで取得する事ができます。
java -jar Reporter.jar p=[properties file] Sales.getVendors
コマンドは以下のようなXML返します。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Output> <Message>Successfully downloaded xxxxxxx.txt.gz</Message> </Output>
xxxxxxx.txt.gzファイルには各アプリのダウンロード数などのデータが入っています。
形式は以下のように1行1アプリ、各要素タブ区切りになります。
Provider Provider Country SKU Developer Title Version Product Type Identifier Units Developer Proceeds Begin Date End Date Customer Currency Country Code Currency of Proceeds Apple Identifier Customer Price Promo Code Parent Identifier Subscription Period Category CMB Device Supported Platforms Proceeds Reason Preserved Pricing Client APPLE US com.example developername appname 1.0 0 0 0 12/16/2016 12/16/2016 JPY JP JPY 123456789 0 Business iPhone iOS