- 投稿日:2019-03-27T18:43:55+09:00
Firebase FirestoreのTimestamp型のdateへの変換
備忘録的に書く。
SwiftのFirestoreで使えるtimeStamp型をdateに変換する方法
How to convert Firebase Firestore Timestamp to Date (Swift)?let timeStamp: Timestamp let date: Date = timeStamp.dateValue()以上。
参考
https://stackoverflow.com/questions/51116381/convert-firebase-firestore-timestamp-to-date-swift
- 投稿日:2019-03-27T18:09:12+09:00
Xcode10.2でcarthage updateすると `Could not find any available simulators for iOS`
Xcode10.2にあげてSwift5でビルドしようとすると
Module compiled with Swift 4.2.1 cannot be imported by the Swift 5.0 compiler: /Users/......
といったメッセージが表示されました。もちろんSwift4.2でコンパイルしていたものをSwift5でインポートできないので
carthage updateでビルドし直します。
ただcarthage updateをしたところ
Could not find any available simulators for iOSというエラーがcarthageがSwift5に対応していないんなじゃないかなと思って
brew upgrade carthageを実行してver0.31から0.32へアップデートその後
carthage updateで無事にビルドできました。
- 投稿日:2019-03-27T13:54:58+09:00
Sourcery(Stencil)で自前の変数の作り方
本文
Sourcery でメタプログラミングやる時に、たまには自分で変数を作りたい時もあるかと思います。例えば「この型とこの型とこの型、内部実装が少しずつだけ違うのでメタプログラミングで実装したい、でもそもそも宣言も何もないからメタプログラミングで型名をまず変数宣言しないといけない」と言ったシチュエーションです。
残念ながら今の Sourcery のサンプルのほとんどは、
Equatableプロトコルの適合などのような、あとで extension で対応するものばかりで、最初から用意された変数(例えばtypes.implementing.AutoEquatableのようなものばかりです。自分で変数宣言どうすればいいかの情報が全くありませんでした。そして Sourcery が内部で使うスクリプトは Stencil ですが、こちらも公式ドキュメント含めてそう言った情報特に見つかりませんでした…
が、どうにか関連プロジェクトの StencilSwiftKit で情報見つかりました!
{% set variable "value" %}もしくは
{% set variable %}value{% endset %}です!
そして配列を入れたいときは、残念ながら直接配列を入れる方法は見つかりませんでしたが、文字列の演算子
|split:を使えば文字列から配列が作れるので、今回の場合はこのように作れました:{% set array "item1,item2,item3"|split:"," %}これで
[item1, item2, item3]の配列arrayが作られます。余談
実はなぜこれが欲しかったかというと、今まだリリースまで持っていけてないですが、Observer パターンだけに特化したライブラリー SteinsKit を作ってて、そこでほぼほぼ同じ実装になる
VariableとLazyVariableがあって、それぞれ手書きで作るの大変だなと思ってメタプログラミングで頑張ろうと思い、いきなりこの「型名の変数作りたいんだけどどうすればいいんだっけ」の壁にぶつかりました。ちなみに最終的にできたのはこちらのファイルです。
- 投稿日:2019-03-27T12:39:35+09:00
iOS アプリ内課金で、必要なことをやってくれるお手軽Frameworkを作った
年末から冬休みの宿題として、iOS向けプロダクトを作っていまして、個人プロダクトで初めてアプリ内課金を実装してみようと思い、勢いでオレオレFrameworkを作ってみた私です。
おはこんばんちわ。In-App Purchase
フリーミアムモデルのアプリケーションで、収益化する際は「広告を入れる」or「一部機能を有償で解放する」の二択になると思います。後者のような一部の機能をプレミアム、有料化して、販売することがアプリ内課金によって実現することができます。
In-App Purchaseの種類
アプリ内課金には、大きく分けて4つのタイプが用意されています。
消耗型
ソーシャルゲームなどで、ライフであったりスタージュエルのようないわゆる石などのお助けアイテムやゲーム内通貨を購入する際に使われるタイプです。
名前の通り、一度消費してしまったら無くなります。非消耗型
一度購入すれば、ずっと使い続けることができるタイプです。イメージとして、フリーミアムのカメラアプリで、追加課金するとフィルムっぽい写真が撮れるフィルターを使えるようになるような機能追加に使うことが多いでしょう。
購入後もずっと使えるアイテムになるので、実装する際にはリストア処理が必要になります。自動更新サブスクリプション
フューチャーフォン時代によくあった月額課金制のアレです。一定期間サービスを利用するのにプレミアム登録が必要だったりするときに使うタイプです。
更新のタイミングに処理を行う場合、サーバー側での継続確認処理なども発生するので、少々面倒な奴。非更新サブスクリプション
一定期間のみプレミアムサービスを利用する際の期間利用権利を購入する際に使うタイプです。
自動更新はされないので、ユーザーが都度更新の手続きを行う必要があります。個人開発のアプリでは、運用面からあまりサブスクリプションモデルを使うことは多くないと思います。
主にアプリ内課金で必要とするのは、非消耗型のタイプが多いのかなと考えてます。今回作ったプロダクトでも、非消耗型のタイプでのアプリ内課金を実装しました。YMTInAppPurchaseFramework,YMTInAppPurchaseAPI
今回、非消耗型タイプに特化したアプリ内課金の処理を担ってくれるFrameworkを作りました。
主に下記のことをやってくれます。
- AppStoreで販売中のアイテムの確認、取得
- 購入時のトランザクション、レシート検証
- 購入済みアイテムのリストア処理
基本的には、StoreKitの処理を自分なりに使いやすいようにラッパーしているようなFrameworkです。
また、レシート検証をサーバーサイドで行うためにnode.jsで書いた簡単なAPIも用意し、「YMTInAppPurchaseFramework」と「YMTInAppPurchaseAPI」を組み合わせて使う前提で作っています。
この記事では、作ったFrameworkの使い方を記載します。StoreKitが行なっているトランザクション処理などについては、公式のドキュメント等を参照していただけますと幸いです。YMTInAppPurchaseAPIのセットアップ
インストール
APIを稼働させたいサーバーの適当なディレクトリに本プロジェクトを落としてください。
$ cd ./hoge/hoge $ git clone https://github.com/MasamiYamate/YMTInAppPurchaseAPI.gitそのままですと、必要なモジュールが含まれていないので
npm installを実行してAPI実行に必要なモジュールをインストールします。$ cd YMTInAppPurchaseAPI $ npm installnginxの設定
APIを外に公開するためにnginxのリバースプロキシを利用します。
./etc/nginx/conf.d/にあるconfファイルに下記のようにAPIのロケーションを指定しましょう。
※1 すでにnginxがインストールしてサービスとして動いている前提です。
※2 また、素のnginxではhttps対応はしていませんので、別途Let's Encryptなどでhttps対応を行ってください。server { listen 443 ssl http2; server_name hogehoge; location /appleapi/ { proxy_pass http://localhost:3000; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } }設定後下記コマンドを実行し、nginxを再起動します。
$ sudo nginx -s reloadAPIを実行する
node index.jsでも動かすことができますが、セッションが切れると止まってしまうのでforeverなどのデーモン化ツールと組み合わせて常に待ち受けるようにします。$ forever start index.js※スクリプトが置いてあるディレクトリなどは適宜自分の環境に読み替えてください。
APIのエンドポイント
上記の例のまま設定すると下記のURLがエンドポイントになります。
フレームワークの初期化時に必要になるので控えておきます。Registration
https://【your-domain-name】/appleapi/regi
Restore
https://【your-domain-name】/appleapi/restore
YMTInAppPurchaseFrameworkの使い方
インストール
Cocoapodsに公開済みのため、pod installで組み込むことが可能です。
Podfileに下記のように追記します。Podfile.file# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'hogehoge' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for hogehoge pod 'YMTInAppPurchase' target 'hogehogeTests' do inherit! :search_paths # Pods for testing end target 'hogehogeUITests' do inherit! :search_paths # Pods for testing end end追記後、
pod installを実行します。
Frameworkのインストール作業は以上で完了です。利用方法
キー、検証APIのエンドポイント設定
アプリ側の実装前に下記の項目値を取得しておきましょう。
- YMTInAppPurchaseAPI Registration end point
- YMTInAppPurchaseAPI Restore end point
- App内課金共有シークレットキー
App内課金共有シークレットキーは、Appstore Connectより取得することができる16進数の文字列になります。
上記の3つの値を取得しましたら、AppdelegateのdidFinishLaunchingWithOptionsで、Frameworkの初期化を行います。
AppDelegate.swift// // AppDelegate.swift // YMTInAppPurchaseSampleApp // import UIKit // Framework import import YMTInAppPurchase @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // App内課金共有シークレットキーを登録します YMTInAppPurchase.shared.setAppShareKey("In-App Purchase Shared Secret Key") // YMTInAppPurchaseAPIのそれぞれのエンドポイントを設定します let registration = "registration url" let restore = "restore url" YMTInAppPurchase.shared.setValidationUrls(regist: registration, restore: restore) return true } }iTunes Storeの販売アイテムが有効か判別する
ここからは実際に販売アイテムを取り扱っていきます。
事前にAppStoreConnectから販売したいアイテムの情報などを登録する必要があります。
その際、プロダクトIDを独自で設定しますがこのIDを元に販売できるアイテムであるかということを判別することが求められます。
StoreKitを用いた場合では、下記のような実装になります。StoreKitSamplefunc productValidation (ids: [String]) { //販売するアイテムのIDの配列を渡す let productReq = SKProductsRequest(productIdentifiers: Set(ids)) //デリゲートの継承 productReq.delegate = self //リクエストの開始 productReq.start() } //リクエスト完了後コールされる func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { //有効なアイテムは、「SKProduct」オブジェクトの配列として返却 effectiveProducts = response.products //無効なアイテムは、プロダクトIDの文字列の配列として返却 invalidProductIds = response.invalidProductIdentifiers }YMTInAppPurchaseFrameworkでは、上記の実装をラッパーしたメソッドを用意しています。
ProductsIDの有効無効判定は、多少時間がかかるためアプリ起動時よりもアイテム販売ページの読み込み時に実行する方がよいと思います。YMTInAppPurchaseFramework_Sampleimport UIKit import YMTInAppPurchase class ViewController: UIViewController { //販売予定のプロダクトIDの配列 let productsIds = ["itemOne" , "itemTwo"] override func viewDidLoad() { super.viewDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) //取得済みのSKProductの件数が0の時に有効アイテムの判別を行う //アプリ起動後、一度でも判別を行い有効アイテムがある場合は実行する必要はない if Payment.shared.getProductsCnt() == 0 { Payment.shared.setProductIds(productsIds, callback: { //有効アイテムの取得後、アイテム購入画面に反映などの処理を行う }) } } }販売アイテムの情報を取得する
UITableViewなどに販売アイテムを表示して、該当するアイテムをタップすると購入プロセスを走らせるなどが一般的な課金アイテムの販売Viewになると思います。
その際、有効のアイテムを取得するには下記のメソッドを利用します。/// 販売アイテムの総数を取得します /// /// - Returns: Int YMTInAppPurchase.shared.getProductsCnt() /// index番号を元に特定アイテムのSKProductを取得します /// /// - Parameter idx: Int /// - Returns: SKProduct? YMTInAppPurchase.shared.getProduct(index) /// 全ての販売アイテムを取得します /// /// - Returns: [SKProduct] YMTInAppPurchase.shared.getProducts() /// 特定アイテムのローカライズ済みのアイテム名を取得します /// /// - Parameter product: SKProduct /// - Returns: String YMTInAppPurchase.shared.getProductLocalizedTitle(PRODUCT) /// 特定アイテムのローカライズ済みのアイテム説明文を取得します /// /// - Parameter product: SKProduct /// - Returns: String YMTInAppPurchase.shared.getProductLocalizedBody(PRODUCT) /// 特定アイテムのローカライズ済みのアイテム価格を取得します /// /// - Parameter product: SKProduct /// - Returns: String YMTInAppPurchase.shared.getProductLocalizedPrice(PRODUCT)決済処理、リストア処理を行う
実際にユーザーがアイテムを選び、アプリ内に用意してあるであろう購入ボタンをタップした時にリクエストするメソッドです。
決済処理、リストア処理共に完了後にコールバックが呼ばれます。その際、引数として決済に成功したアイテムのプロダクトIDが渡されますので、アプリ側はプロダクトIDを元に有料機能の有効化などの処理を行ってください。/// 決済処理を行う /// /// - Parameters: /// - product: SKProduct /// - callback: ((String?) -> Void)? YMTInAppPurchase.shared.startTransaction(product, callback: { productId in //プロダクトIDが含まれる場合は、有料機能の有効化処理を行い //nilの場合は、決済に失敗しているのでエラーアラートなどを出す if productId != nil { //有効化処理 }else{ //エラーアラートなど } }) /// リストア処理を行う /// /// - Parameter callback: ((String?) -> Void)? YMTInAppPurchase.shared.startRestore(callback: { productId in //プロダクトIDが含まれる場合は、有料機能の有効化処理を行い //nilの場合は、決済に失敗しているのでエラーアラートなどを出す if productId != nil { //有効化処理 }else{ //エラーアラートなど } })おわりに
トランザクションの検証部分などこれでいいのかという不安は抱えつつではありますが、今回Framework化に挑戦してみました。リリースしたアプリは、Appleの審査も通過しているので機能的には問題ないものになっていると思います。
まだまだ改善の余地は残されていると思いますので、少しづつ改良していきたいと思います。Github - YMTInAppPurchaseFramework
Github - YMTInAppPurchaseAPI
- 投稿日:2019-03-27T11:55:53+09:00
【swift】カスタムビューをxibで作成して各ViewControllerで使い回す
概要
StoryboardでViewControllerのViewの一部が他のViewControllerでも同じレイアウトで表示している場合があります。その場合ViewをコピーしてViewControllerに貼り付けて利用したりしますが、それだとそのViewに変更を加えると複数利用しているため他のViewも変更しなくてはなりません。
そう行った場合、使い回せるViewをカスタムビューとしてxibで作成して利用することで、そのxibのみを変更すれば複数のViewでその変更が反映されるようになります。実装方法
まずは、カスタムビューをxibで作成します。
続いて、ViewControllerで、xibのカスタムビューをインスタンス化します。ViewController.swiftclass ViewController: UIViewController { weak var sampleView: UIView! override func loadView() { super.loadView() sampleView = UINib(nibName: "SampleView", bundle: Bundle.main).instantiate(withOwner: self, options: nil).first as? UIView sampleView.backgroundColor = .yellow view.addSubview(sampleView) } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() sampleView.frame = CGRect.init(x: 100.0, y: 100.0, width: 100.0, height: 100.0) } }この時に、withOwnerでselfを指定しているので、xibで生成したlabelを@IBOutletで繋ぐことが出来ます。
UINib(nibName: "SampleView", bundle: Bundle.main).instantiate(withOwner: self, options: nil).first as? UIViewViewController.swiftweak var sampleView: UIView! @IBOutlet weak var label: UILabel! override func loadView() { super.loadView() sampleView = UINib(nibName: "SampleView", bundle: Bundle.main).instantiate(withOwner: self, options: nil).first as? UIView sampleView.backgroundColor = .yellow view.addSubview(sampleView) label.text = "あああ" }■ビルド実行
カスタムビューを管理するクラスを生成する
ViewControllerでxibのViewをインスタンス化して使用するのは上記の方法ですが、多くの場合カスタムビューを管理するクラスを作成してxibとセットで利用したいと考えます。
その方法としては、withOwnerをカスタムビューを管理するクラスにします。
(SampleViewOwnerはSampleView.xibを管理するクラス)
SampleViewOwner.swiftclass SampleViewOwner: NSObject { @IBOutlet weak var label: UILabel! var sampleView: UIView! override init() { super.init() sampleView = UINib(nibName: "SampleView", bundle: Bundle.main).instantiate(withOwner: self, options: nil).first as? UIView label.text = "いいい" } }ViewController.swiftclass ViewController: UIViewController { var sampleViewOwner: SampleViewOwner! override func loadView() { super.loadView() sampleViewOwner = SampleViewOwner() sampleViewOwner.sampleView.backgroundColor = .yellow view.addSubview(sampleViewOwner.sampleView) } override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() sampleViewOwner.sampleView.frame = CGRect.init(x: 100.0, y: 100.0, width: 100.0, height: 100.0) } }こうすることで各ViewControllerでownerクラスをインスタンス化して使用すれば、xib上のlabel等の変更は、管理するownerクラスの方で行うことが出来ます。
■ビルド結果
- 投稿日:2019-03-27T04:24:20+09:00
NavigationControllerのカスタマイズ
extension UINavigationController { public func pushViewController(viewController: UIViewController, animated: Bool, completion: (() -> Void)?) { CATransaction.begin() CATransaction.setCompletionBlock(completion) pushViewController(viewController, animated: animated) CATransaction.commit() } }
- 投稿日:2019-03-27T04:23:59+09:00
UIColor
/// UIColorの拡張 extension UIColor { /// 16進数での色指定 /// /// - Parameter rgb: 16進数での指定色 convenience init(rgb: Int32) { let r = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 let g = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 let b = CGFloat(rgb & 0x0000FF) / 255.0 self.init(red: r, green: g, blue: b, alpha: 1.0) } //----------------------------------------------------------------------------- /// 16進数での色指定(アルファチャンネル付) /// /// - Parameter rgba: 16進数での指定色 convenience init(rgba: Int64) { let r: CGFloat = CGFloat((rgba & 0xFF000000) >> 24) / 255.0 let g: CGFloat = CGFloat((rgba & 0x00FF0000) >> 16) / 255.0 let b: CGFloat = CGFloat((rgba & 0x0000FF00) >> 8) / 255.0 let a: CGFloat = CGFloat(rgba & 0x000000FF) / 255.0 self.init(red: r, green: g, blue: b, alpha: a) } }


