しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

【iOS】Smile-Lockでパスコードロック機能を実現する

Smile-Lockというライブラリを試してみました。
URL見るとリクルートライフスタイルで出しているライブラリなんですね。

github.com

インストール

CocoaPodsでインストールしました。

target 'MyApp' do
  use_frameworks!

  pod 'SmileLock'
end

使い方

下のようにWindowやViewに貼り付ける事で利用します。

import UIKit
import SmileLock

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let kPasswordDigit = 6
        let contentView = PasswordContainerView.createWithDigit(kPasswordDigit)
        if let window = view.window {
            let baseView = UIView(frame: window.bounds)
            baseView.addSubview(contentView)
            contentView.center = baseView.center
            window.addSubview(baseView)
        }
    }
}

実行すると下のような表示になります。

f:id:llcc:20161217214232p:plain

パスコードを入力した時の動作はプロトコルに記述します。
passwordInputComplete:input:は入力が完了した時に呼ばれます。

extension ViewController: PasswordInputCompleteProtocol {
    func passwordInputComplete(_ passwordContainerView: PasswordContainerView, input: String) {
        print(input)
    }
    
    func touchAuthenticationComplete(_ passwordContainerView: PasswordContainerView, success: Bool, error: NSError?) {
        
    }
}

パスコードが違う場合のシェイクはwrongPasswordメソッドを使います。
入力のクリアだけしたい場合はclearInputメソッドを使えば良さそうです。

extension ViewController: PasswordInputCompleteProtocol {
    func passwordInputComplete(_ passwordContainerView: PasswordContainerView, input: String) {
        passwordContainerView.wrongPassword()
    }
    
    func touchAuthenticationComplete(_ passwordContainerView: PasswordContainerView, success: Bool, error: NSError?) {
        
    }
}

それとPasswordContainerViewのプロパティーを触る事で少しだけデザインを変更できます。
ボタンの色を変えるtintColor、ハイライト時の色を変えるhighlightedColor、曇りガラス風デザインに変更できるisVibrancyEffectなどがあります。

f:id:llcc:20161217220112p:plain

タッチIDが使える場合は自動的にViewにタッチボタンが表示されます。

f:id:llcc:20161217220703p:plain

押すとタッチIDを求める表示になります。

f:id:llcc:20161217220830p:plain

指紋認証が完了するとdelegatetouchAuthenticationCompleteメソッドが呼ばれます。

extension ViewController: PasswordInputCompleteProtocol {
    func passwordInputComplete(_ passwordContainerView: PasswordContainerView, input: String) {
    }
    
    func touchAuthenticationComplete(_ passwordContainerView: PasswordContainerView, success: Bool, error: NSError?) {
        print(success)
    }
}

今回は触れないのですが、PasswordUIValidationというクラスを使ったアプローチもあるようです。

THPinViewControllerとの比較

THPinViewControllerより自由度が高いのが良さそうでした。
THPinViewControllerは良く使っているんですが、「パスコード正解時は自動的にdissmissされる為、確認ページへ遷移させるのが難しい」みたいな問題があってそれが解決するだけでも良さそうです。
それと指紋認証を標準で対応してくれているのもありがたいです。

llcc.hatenablog.com

FirebaseRemoteConfigでA/Bテストを試してみる

Firebaseを使う準備

まずは下サイトで新しいプロジェクトを作成します。

Firebase Console

f:id:llcc:20161215213452p:plain

プロジェクトを作成すると以下のような画面になるのでiOS アプリに Firebase を追加を選びます。

f:id:llcc:20161215213525p:plain

情報を入力してアプリを追加します。

f:id:llcc:20161215213609p:plain

次画面に移動するとGoogleService-Info.plistというファイルのダウンロードが開始します。
このファイルは後で使います。

f:id:llcc:20161215213643p:plain

次はXcodeでプロジェクトを作成します。
SingleViewApplicationを選択します。

f:id:llcc:20161215213858p:plain

次にFirebaseをインストールします。
Podsファイルを作成して以下のように修正して下さい。

platform :ios, ’10.0’

target 'MyApp' do
  use_frameworks!

  pod 'Firebase/Core'
  pod 'Firebase/RemoteConfig'
end

先程ダウンロードしたGoogleService-Info.plistをプロジェクトに追加します。

f:id:llcc:20161215214019p:plain

最後にAppDelegate.swiftへFirebase初期化処理を追加します。
これでFirebaseを使えるようになりました。

import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        FIRApp.configure()
        
        return true
    }
}

FirebaseRemoteConfigを使ってみる

続けてFirebaseRemoteConfigを使ってみます。
Firebaseの管理画面に戻り、左メニューのRemote Configを選択します。

f:id:llcc:20161215214415p:plain

そのページで最初のパラメータを追加ボタンを押してキーと値を追加します。

f:id:llcc:20161215214524p:plain

最後に変更を公開ボタンを押して追加したパラメータを有効化します。

f:id:llcc:20161215214834p:plain

設定したパラメータの取得方法は以下の通りです。

import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let remoteConfig = FIRRemoteConfig.remoteConfig()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        FIRApp.configure()
        remoteConfig.fetch { [weak self] status, error in
            print(status.rawValue)
            print(error)
            self?.remoteConfig.activateFetched() // これでパラメータを取得できる
            print(self?.remoteConfig["test_key"].stringValue) // → "test_value"
        }
        
        return true
    }
}

FirebaseRemoteConfigでA/Bテストをする

最後にFirebaseRemoteConfigを使ったA/Bテストの方法を見ていきます。
まず管理画面上の条件を追加ボタンから条件を追加します。

f:id:llcc:20161215223617p:plain

条件はUser in random percentileにして、好きな割合を入力します。

f:id:llcc:20161215223714p:plain

保存すると、2種類の値を設定できるようになります。
これらは先程設定した割合に応じて出しわけられます。

f:id:llcc:20161215223823p:plain

余談ですが、条件は推定地域などを選ぶ事ができます。
国に応じて出し分けをしたい場合などに有効そうです。

f:id:llcc:20161215223914p:plain

最後にFirebaseAnalyticsの設定を行います。
サイドメニューのAnalyticsタブのユーザープロパティーを選択して下さい。

f:id:llcc:20161215224125p:plain

新しいユーザーのプロパティーボタンからプロパティーをセットします。

f:id:llcc:20161215224240p:plain

次はXcode上でユーザーのプロパティーをセットします。
FIRAnalytics.setUserPropertyStringを使ってプロパティーのセットを行って下さい。

import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    let remoteConfig = FIRRemoteConfig.remoteConfig()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        FIRApp.configure()
        remoteConfig.fetch { [weak self] status, error in
            self?.remoteConfig.activateFetched()
            FIRAnalytics.setUserPropertyString(self?.remoteConfig["test_key"].stringValue, forName: "my_property")
        }
        
        return true
    }
}

これで実装は完了です。
Analytics上でプロパティー毎の数字を見る事ができるので、これを使って値の出し分けの成果を見ます。

f:id:llcc:20161215224559p:plain

その他

FirebaseRemoteConfigはとてもおもしろかったのですが、コホート分析でプロパティーによるフィルターが使えないのが少し痛かったです。
アクティブ率をKPIにする事が多いので、ここでA/Bテストを多用したかったです。

f:id:llcc:20161215224837p:plain

この辺りはGoogleAnalyticsのカスタムディメンションを使ったほうがうまくいくかもしれません。
GoogleAnalyticsだと下のように見ることができます。

f:id:llcc:20161221103050p:plain

参考URL

Firebase Remote Config  |  Firebase

R.swiftでNSLocalizedStringも取れるようになっていたので試してみる

R.swiftがいつの間にかNSLocalizedStringにも対応されていたので動かしてみました。

R.swiftとは

R.swiftとはStoryboardや画像などの取得を便利にするライブラリです。
Storyboardの取得はUIStoryboard(name: "Main", bundle: nil)のように文字列で指定しますが、R.swiftを使うとR.storyboard.mainと変数として取得する事ができます。
以前記事を書いたので、宜しければ併せてご参照下さい。

R.swiftを使ってStoryboard名や画像名のTypoを0にする - Qiita

準備

CocoaPodsを使ってインストールします。

platform :ios, ’10.0’

target 'MyApp' do
  use_frameworks!

  pod “R.swift”
end

次にBuild PhasesでRun scriptを追加して"$PODS_ROOT/R.swift/rswift" "$SRCROOT"と入力します。

f:id:llcc:20161214234041p:plain

この状態ビルドすればR.generated.swiftが生成されるので、プロジェクトに追加します。

f:id:llcc:20161214234158p:plain

これでRという構造体を通してStoryboardなどを取得できるようになりました。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        R.storyboard.main.instantiateInitialViewController()
    }
}

NSLocalizedStringを取得してみる

MyString.stringsを追加して、以下のようにテキストを記入しました。

"MyText" = "Hello!";

ビルドするとR.generated.swiftに先程の文字が追加されていました。

print(R.string.myString.myText()) // → Hello!

MyString2.stringsファイルを作って、"myText2" = "Hello world!";を追加したら以下のようになりました。

print(R.string.myString.myText()) // → Hello!
print(R.string.myString2.myText2()) // → Hello world!

複数言語対応も試してみます。
MyString.stringsを英語用と日本語用を作って、英語用には"MyText" = "Hello!";、日本語用には"MyText" = "こんにちは";を追加します。

f:id:llcc:20161214235227p:plain

この状態で起動すると、シミュレータの言語設定に合わせた文字を出力してくれます。

print(R.string.myString.myText()) // → Hello! or こんにちは

【Swift】WKWebViewのスクロール速度を変更する

WKWebViewのスクロール速度を変更する方法を調べてみました。 まずはViewControllerにwebViewを貼り付けます。

import UIKit
import WebKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let webView = WKWebView(frame: view.bounds)
        webView.load(URLRequest(url: URL(string: "https://google.com")!))
        view.addSubview(webView)
    }
}

起動するとSafariなどと同様のスクロール速度です。

f:id:llcc:20161213230103g:plain

webViewのscrollViewのdecelerationRateにUIScrollViewDecelerationRateNormalをセットします。

import UIKit
import WebKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let webView = WKWebView(frame: view.bounds)
        webView.load(URLRequest(url: URL(string: "https://google.com")!))
        webView.scrollView.decelerationRate = UIScrollViewDecelerationRateNormal // 追加
        view.addSubview(webView)
    }
}

これで起動すると、スクロールがかなり早くなります。

f:id:llcc:20161213230527g:plain

スクロール速度はUIScrollViewDecelerationRateFastとUIScrollViewDecelerationRateNormalが用意されています。

f:id:llcc:20161213230129p:plain

decelerationRateはCGFloatなので直接値を指定する事もできます。
ちなみに0.1だと指を話した瞬間に止まるような動きになります。

webView.scrollView.decelerationRate = 0.1

それぞれの値は下のようになっていました。

print(webView.scrollView.decelerationRate) // 0.9893243312
print(UIScrollViewDecelerationRateNormal) // 0.998
print(UIScrollViewDecelerationRateFast) // 0.99

参考URL

UIWebViewのスクロールを早くしたい! - Qiita

SKStoreProductViewControllerでアプリから移動せずにStore画面に行く

StoreKitのSKStoreProductViewControllerを使ってアプリ内でアプリのページに移動してみました。

まずはStoreKitを追加します。

f:id:llcc:20161212235732p:plain

SKStoreProductViewControllerはUIViewController同様にpresentをして表示します。
Storeの情報はloadProductメソッドで読み込みます。

import UIKit
import StoreKit

class ViewController: UIViewController {
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let c = SKStoreProductViewController()
        c.delegate = self
        present(c, animated: true, completion: {
            let params = [SKStoreProductParameterITunesItemIdentifier: "1099462086"]
            c.loadProduct(withParameters: params, completionBlock: { _ in
                
            })
        })
    }
}

extension ViewController: SKStoreProductViewControllerDelegate {
    func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
        // キャンセルを押した時に呼ばれる
    }
}

複数のGKGoalを使って複雑なAIを作成する

昨日調べた事の続きです。
この記事では、GKGoalを使って特定のオブジェクトに向かって移動するAgentを作成しました。
今回は「特定のオブジェクトに向かって移動」「別のオブジェクトから逃げる」の複数を指定した場合を試してみます。

GKGoalとGKAgentで目的に向かって動くオブジェクトを作ってみる - しめ鯖日記

昨日同様に、Gameプロジェクトを作成します。

f:id:llcc:20161211185859p:plain

GameScene.swiftを以下のように置き換えます。
昨日の記事と違い、GKBehaviorに複数のGKGoalを与えています。
こうする事でagent3にagent2から遠ざかりつつagent1へ移動する動きをさせる事ができます。

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    let agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
    let agent1 = GKAgent2D()
    let agent2 = GKAgent2D()
    let agent3 = GKAgent2D()
    let node1 = SKSpriteNode(color: UIColor.brown, size: CGSize(width: 10, height: 10))
    let node2 = SKSpriteNode(color: UIColor.green, size: CGSize(width: 10, height: 10))
    let node3 = SKSpriteNode(color: UIColor.red, size: CGSize(width: 10, height: 10))
    var prevTime: TimeInterval = 0
    
    override func didMove(to view: SKView) {
        agent1.position = vector_float2(x: 100, y: 100)
        agent2.position = vector_float2(x: 300, y: 100)
        node1.position = CGPoint(x: Double(agent1.position.x), y: Double(agent1.position.y))
        node2.position = CGPoint(x: Double(agent2.position.x), y: Double(agent2.position.y))
        
        agent3.position = vector_float2(x: 300, y: 500)
        agent3.maxAcceleration = 10
        agent3.maxSpeed = 10
        agent3.delegate = self
        agent3.behavior = GKBehavior(goals: [
            GKGoal(toSeekAgent: agent1),
            GKGoal(toFleeAgent: agent2)
            ], andWeights: [100, 10]
        )
        agentSystem.addComponent(agent3)
        
        addChild(node1)
        addChild(node2)
        addChild(node3)
    }
    
    override func update(_ currentTime: TimeInterval) {
        let deltaTime = prevTime == 0 ? 0 : currentTime - prevTime
        prevTime = currentTime
        agentSystem.update(deltaTime: deltaTime)
    }
}

extension GameScene: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        if let agent = agent as? GKAgent2D {
            node3.position = CGPoint(x: Double(agent.position.x), y: Double(agent.position.y))
            print(agent.position)
        }
    }
}

この状態で起動するとagent2を避けつつagent1に向かう動きをします。
下図の右上がagent3、右下がagent2、左下はagent1を表しています。

f:id:llcc:20161211190512p:plain

もしここでandWeightsを両方100にした場合は少し動きが変わってきます。

agent3.behavior = GKBehavior(goals: [
    GKGoal(toSeekAgent: agent1),
    GKGoal(toFleeAgent: agent2)
    ], andWeights: [100, 100]
)

agent1に近づく動きとagent2から遠ざかる動きが重なり、下図のように真っ直ぐ左側に移動する事になります。

f:id:llcc:20161211190810p:plain

GKGoalとGKAgentで目的に向かって動くオブジェクトを作ってみる

GameplayKitの「Agents, Goals, and Behaviors」という機能を試してみました。

GameplayKit Programming Guide: Agents, Goals, and Behaviors

これは特定のオブジェクトに向かって移動したり、特定のオブジェクトから逃げるようなものを作れる機能です。
早速実装をしてみます。

まずはXcodeでゲームプロジェクトを作成します。

f:id:llcc:20161210231421p:plain

GameScene.swiftを以下のように置き換えます。
こちらはagent2がagent1に向かって移動するサンプルです。

import SpriteKit
import GameplayKit

class GameScene: SKScene {
    var agentSystem = GKComponentSystem(componentClass: GKAgent2D.self)
    var agent1 = GKAgent2D()
    var agent2 = GKAgent2D()
    var prevTime: TimeInterval = 0
    
    override func didMove(to view: SKView) {
        agent1.position = vector_float2(x: 100, y: 100)
        
        agent2.position = vector_float2(x: 300, y: 500)
        agent2.delegate = self
        agent2.behavior = GKBehavior(goal: GKGoal(toSeekAgent: agent1), weight: 100)
        agentSystem.addComponent(agent2)
    }
    
    override func update(_ currentTime: TimeInterval) {
        let deltaTime = prevTime == 0 ? 0 : currentTime - prevTime
        prevTime = currentTime
        agentSystem.update(deltaTime: deltaTime)
    }
}

extension GameScene: GKAgentDelegate {
    func agentDidUpdate(_ agent: GKAgent) {
        print((agent as? GKAgent2D)?.position)
    }
}

実際に起動すると以下のようなログが表示されます。
agent2を表示しているのですが、agent1(x: 100, y: 100)に向かって移動している事が分かるります。

f:id:llcc:20161210232853p:plain

agent1(x: 100, y: 100)の座標に移動したあとは、バネのような動きをします。

f:id:llcc:20161210233457p:plain