20190327のiOSに関する記事は5件です。

タブレットとWeb両立プロジェクト開発(一)Node.jsをインストールします

タブレットプロジェクトというのは、例えば、iPhoneやiPadなどの端末上で稼働しているアプリを指します。Webプロジェクトというのは、Webブラウザーで見ているウェブサイトを指します。
タブレットアプリの特徴は、随時にバックエンドのサーバに繋がりをしなくて、ネットワークがない場合はタブレットが独立に動いておいて、ネットワーク例えばWi-Fiが繋がった場合にはサーバに接続してタブレットのデータをサーバに同期しようとすることがあります。
ウェブサイトのほうも、頻繁にサーバへ接続することを下げる期待を持っていることがあります。
こういうことを実現するために、JavaScriptをメインにしているプロジェクトを作りたいです。
本章では、JavaScriptプロジェクトを作成する前の準備の一つとして、Node.jsをインストルします。

npmインストル

npmとは、Node.jsに作られたライブラリやパッケージを管理する為のパッケージ管理ツールです。
npmをインストルする前に、Nodebrewをインストルすることが必要です。
Nodebrewは、Node.jsのバージョン管理ツールにあたります。

#nodebrewをインストル
brew install nodebrew
#Node.jsとnpmのインストール
nodebrew install-binary latest

もし、以下のエラーが出たら、下記のコマンドを実行して、ディレクトリを作成して、npmインストルを再度行います。
curl: (23) Failed writing body (0 != 941)
download failed: https://nodejs.org/dist/v7.10.0/node-v7.10.0-darwin-x64.tar.gz

mkdir -p ~/.nodebrew/src
nodebrew install-binary latest
#以下が表示
Fetching: https://nodejs.org/dist/v11.12.0/node-v11.12.0-darwin-x64.tar.gz
######################################################################## 100.0%
Installed successfully

上記のコマンドで最新版がインストールされます。
Node.jsを有効にします。

#バージョンの一覧を確認
nodebrew list
#バージョンの一覧が表示
v11.12.0
current: none
#current: noneとなっているため、必要なバージョンを有効化する。
nodebrew use v11.12.0
#バージョンの一覧を再確認
nodebrew list
#以下が表示
v11.12.0
current: v11.12.0

nodebrewが使いやすいように環境パスを通します。
インストルした結果を確認します。

echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
source ~/.bash_profile
#node.jsがインストル済かどうかを確認
node -v
#以下が表示
v11.12.0
#npmがインストル済かどうかを確認
npm -v
#以下が表示
6.7.0

上記ような各バージョーンが表示されたらNode.jsのインストールが終わりします。

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

iOSでWebブラウザを表示したとき、ダブルクリックで画面がズームしないようにする方法

はじめに

iOSのWebブラウザ上でダブルタップをすると勝手にズームする現象を発見。
解消する方法を調べました。

試したこと

  1. フォントサイズを「16px」以上にする
  2. トリガーからダブルクリックをクリックに変換する
  3. イベントハンドラのtouchendを利用し、ダブルクリックを検知したらクリックに変換する
    →「3」の方法で解決しました!

1. フォントサイズを 「16px」 以上にする

画面がズームする理由を調べてみたところ、iOSではWebブラウザを表示した際、input要素に指定しているフォントサイズが16pxを下回ると、フォーカス時にズームされるという仕様があることがわかりました。
https://uxcellence.com/2014/fix-ios-input-zoom

テキストボックスに文字を入力するときズームになるケースがこれに該当しますね。
iOSの仕様どおり、フォントサイズを16px以上に設定するとズームしなくなるケースが多いようです。

なお、上記の仕様があることから、以下のように強制的にズームを拒否してはいけません。

NG例
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">

今回はダブルクリックによるズームを止めたいので、別の方法を調べました。

2. トリガーからダブルクリックをクリックに変換する

この実装で、ズームはしなくなります。
・・・・が、非活性のボタンでも問答無用で強制クリックする効力を持っているので大変危険です。

document.addEventListener('touchend', function (event) {
  event.preventDefault();
  $(event.target).trigger('click');
}, false);

3. イベントハンドラのtouchendを利用し、ダブルクリックを検知したらクリックに変換する

最終的には、ダブルクリックによる検知を自作して解決しました ^ ^

var isClicked = false;

document.addEventListener('touchend', function (event) {
  if (isClicked) {
    // double click
    if (event.cancelable) {
      event.preventDefault();
    }
    isClicked = false;
  } else {
    // single click
    isClicked = true;
    setTimeout( function() {
      isClicked = false;
    }, 350);
  }
}, false);
  • イベントハンドラのdblclickでダブルクリックを検知することができますが、ダブルクリックの途中でテキストが選択されてしまうことがあるため、今回はtouchendからダブルクリックを検知するように実装しています。
  • mousedownの方がイベントの発火が遅いため良いかと思い実装しましたが、挙動が安定しませんでした。
if (event.cancelable) {
  event.preventDefault();
}
  • 上記のように、イベントがキャンセル可能かを判定してからイベントを中止しないと以下のエラーが出力されるので注意が必要です。

[Intervention]Ignored attempt to cancel a touchend event with cancelable=false, for example because scrolling is in progress and cannot be interrupted.

参考
http://js.studio-kingdom.com/javascript/event/cancelable
https://lab.syncer.jp/Web/JavaScript/Snippet/14/
https://qiita.com/ikemai/items/d0f9ada6c8d75cceab67

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

iOSで正しく歩数を取得する

この記事は、以下の資料をqiita用の記事にしたものです。
https://speakerdeck.com/satoshin/getting-step-count-on-ios

はじめに

iOSで歩数を取得する方法には以下の二つの方法があります。

  • CMPedometerから取得する方法
  • HealthKitから取得する方法

HealthKitの方では、取得方法を間違えると実際のデータと大きく異なるので注意してください。

CMPedometerから取得する

let pedometer = CMPedometer()
 pedometer.queryPedometerData(from: start, to: end) { (data, error) in
    print(data?.numberOfSteps)
}
  • start ~ end までの間で端末で検知した歩数を取得
    • AppleWatchの情報取得はWatchAppで実装する必要あり
  • 取得できるのは過去7日分

HealthKitから取得する

:warning: HKSampleQuery で取得する

HealthKitでは、いつ、何時に何歩歩いたかと言う情報をHealthStoreに保持しています。
HealthStoreからデータを取得する方法の一つにHKSampleQueryを利用できます。
取得したデータの合計値を計算すれば良いかと考えられますが、この方法はやってはいけないダメな方法です。

なぜなら、HealthStoreにはiPhoneApple Watchが自動記録した歩数データが入っているため、
素直に足し合わせてしまうと、二つの歩数計データを足し合わせてしまうことになります。

iPhoneだけでテストしてしまうと、一見正しい情報に見えてしまうため注意です。

間違った取得方法-HKSampleQueryで取得した合計値を表示する
let store = HKHealthStore()
let type = HKSampleType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForSamples(withStart: start, end: end)
let query = HKSampleQuery(sampleType: type, 
                          predicate: predicate, 
                          limit: HKObjectQueryNoLimit, 
                          sortDescriptors: nil) { (query, result, error) in
    let result = result as! [HKQuantitySample]
    let sum = result(0) { $0 + $1.quantity.doubleValue(for: .count()) }
    print(sum)
}
store.execute(query)

:o: HKStatisticsQuery / HKStatisticsCollectionQuery で取得する

多くの場合において、一つ一つの歩数を取得したいのではなく、データの特徴を知りたいはずです。
この場合は HKStatisticsQuery / HKStatisticsCollectionQuery を利用しましょう。

以下のサンプルでは、HKStatisticsQueryを利用していますが、
実際には update のハンドリングができ、時間範囲にDateComponentが使える、
HKStatisticsCollectionQueryを利用することが多いです。

HKStatisticsQueryで歩数を取得する
let store = HKHealthStore()
let type = HKSampleType.quantityType(forIdentifier: .stepCount)!
let predicate = HKQuery.predicateForSamples(withStart: start, end: end) 
let query = HKStatisticsQuery(quantityType: type, 
                              quantitySamplePredicate: predicate, 
                              options: .cumulativeSum) { (query, statistics, error) in
     print(statistics?.sumQuantity())
}
store.execute(query)

不正対策

HealthKitから取得する歩数は、時間がかぶっていなければThirdpartyやHealthAppから手動で登録した値も加算されます。
これでは、歩いた歩数がポイントになるアプリなどでは、歩いていないのに多くのポイントを入手することが可能になってしまいます。

HealthStoreに保存された各データには、登録したアプリのBundle Identifierのデータも一緒に書き込まれています。
よってこれでフィルタリングすれば、iPhone、AppleWatchの自動歩数記録機能から保存されたデータのみを抽出することができます。

自動歩数記録機能で記録されたデータとHealthAppのBundle Identifierは以下です。

  • 自動歩数記録機能: com.apple.health.[UUID] (UUID部分は端末によって変わります)
  • HealthApp: com.apple.Health

よって、hasPrefix("com.apple.health")で判定してあげればOKです。

自動記録された歩数だけを取得する
let store = HKHealthStore()
let type = HKSampleType.quantityType(forIdentifier: .stepCount)!
let datePredicate = HKQuery.predicateForSamples(withStart: start, end: end)
let query = HKStatisticsQuery(quantityType: type, 
                              quantitySamplePredicate: datePredicate, 
                              options: .separateBySource)
    { (query, data, error) in
    if let sources = data?.sources?.filter({ $0.bundleIdentifier.hasPrefix("com.apple.health") }) {
        let sourcesPredicate = HKQuery.predicateForObjects(from: Set(sources))
        let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [datePredicate, sourcesPredicate])
        let query = HKStatisticsQuery(quantityType: type, 
                                      quantitySamplePredicate: predicate, 
                                      options: .cumulativeSum) 
        { (query, statistics, error) in
             print(statistics?.sumQuantity())
        }
       store.execute(query)
    }
}
store.execute(query)

サンプルコード

https://github.com/sato-shin/ios-steps

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Xcodeの自動アップデート設定を無効にする方法

AppStore.png

App StoreからダウンロードしたXcodeは自動アップデートの設定が有効になっています。

チームで開発をしている状況では、知らない間にXcodeのバージョンが揃わなくなり、
特定の人しかビルドできない...などといったトラブルの元になります。

SS123.png

トラブルを避けるためにはDeveloperサイトの
Downloads > See more downloadsからダウンロードしてください。
こちらからダウンロードしたXcodeには自動アップデートの設定が付いていません。
more.png

自動アップデート無効化

  1. Xcodeのアイコンを右クリックして「パッケージの内容を表示」を選択。
    package_no.png

  2. Contentsの下にある「_MASReceipt」を削除。
    before.png

  3. これでOKです。
    after.png

それでは新しいXcodeをお楽しみください!!

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