20190327のSwiftに関する記事は7件です。

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

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

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 で無事にビルドできました。

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

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 を作ってて、そこでほぼほぼ同じ実装になる VariableLazyVariable があって、それぞれ手書きで作るの大変だなと思ってメタプログラミングで頑張ろうと思い、いきなりこの「型名の変数作りたいんだけどどうすればいいんだっけ」の壁にぶつかりました。ちなみに最終的にできたのはこちらのファイルです。

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

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 install

nginxの設定

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 reload

APIを実行する

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を用いた場合では、下記のような実装になります。

StoreKitSample
func 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_Sample
import 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

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

【swift】カスタムビューをxibで作成して各ViewControllerで使い回す

概要

StoryboardでViewControllerのViewの一部が他のViewControllerでも同じレイアウトで表示している場合があります。その場合ViewをコピーしてViewControllerに貼り付けて利用したりしますが、それだとそのViewに変更を加えると複数利用しているため他のViewも変更しなくてはなりません。
そう行った場合、使い回せるViewをカスタムビューとしてxibで作成して利用することで、そのxibのみを変更すれば複数のViewでその変更が反映されるようになります。

実装方法

まずは、カスタムビューをxibで作成します。

SampleView_xib.png
続いて、ViewControllerで、xibのカスタムビューをインスタンス化します。

ViewController.swift
class 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? UIView
ViewController.swift
    weak 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 = "あああ"
    }

■ビルド実行

Simulator Screen Shot - iPhone XS - 2019-03-26 at 14.41.56.png

カスタムビューを管理するクラスを生成する

ViewControllerでxibのViewをインスタンス化して使用するのは上記の方法ですが、多くの場合カスタムビューを管理するクラスを作成してxibとセットで利用したいと考えます。

その方法としては、withOwnerをカスタムビューを管理するクラスにします。

(SampleViewOwnerはSampleView.xibを管理するクラス)

SampleViewOwner.swift
class 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.swift
class 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クラスの方で行うことが出来ます。

■ビルド結果

Simulator Screen Shot - iPhone XS - 2019-03-26 at 14.58.38.png

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

NavigationControllerのカスタマイズ

extension UINavigationController {

  public func pushViewController(viewController: UIViewController, animated: Bool, completion: (() -> Void)?) {
    CATransaction.begin()
    CATransaction.setCompletionBlock(completion)
    pushViewController(viewController, animated: animated)
    CATransaction.commit()
  }

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

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)
    }

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