- 投稿日:2019-05-23T23:14:20+09:00
iOS・Safari・Androidブラウザでplaceholderの位置がずれる件
事象
chromeやIEでは、「半角数字3桁」と書かれているplaceholderは
ちゃんと真ん中に位置しているのに、、safariとiOS・Androidブラウザで見ると・・・
ん・・・、 真ん中より上になってる。。。。
解決方法
簡単です。一行で解決できます。
input::-webkit-input-placeholder { padding-top: 0.2em; }注意しないといけないのは、
padding-topの値をSafari/iOSとAndroidブラウザをみながら
調整をしなければならないことです。Safari/iOSが真ん中になっても、
Androidブラウザではずれていることがあるので、
細かく調整してみてください。
- 投稿日:2019-05-23T18:04:18+09:00
sudo gem install cocoapodsでパーミッションのエラー
TLDR
sudo gem install -n /usr/local/bin cocoapodsエラー内容
sudo gem install cocoapods
を実行したところERROR: While executing gem ... (Gem::FilePermissionError) You don't have write permissions for the /usr/bin directory.のエラーがでた
解決策
gem install
には-n, --bindir DIR Directory where executables are locatedと
-n
でどこにコマンドを置くか指定できるので指定する
お好きなようにパスの通っているところにおけばおksudo gem install -n /usr/local/bin cocoapods
- 投稿日:2019-05-23T18:02:55+09:00
【徹底解説】UIScrollViewクラス その1
UIScrollViewクラスとは
UIScrollView(以下,SV)は,画面に触れた指などの動きを追跡することで,画面サイズよりもはみ出たコンテンツを表示させるためのUI部品です。 UITableViewやUITextViewも画面からはみ出た範囲をスクロールさせることができますが,その理由は,SVがこれらのスーパークラスであるためです。
環境
- XCode Version 10.2.1
- Swift 5.0
第1章 とりあえずスクロールさせる
ここではスクロールするコンテンツをstoryboardを用いて作製してみます。実践には程遠いですが何事も基本が大切なので基礎固めしていきましょう。でも本当に固まってはいけません。
SVの配置
ナビゲータエリアからMain.storyboardを選択し,Objectライブラリから「Scroll View」を選びます。それを画面(View)上にドロップします。ドロップしたSVを画面全体に広げます。もちろん必ずしも画面全体に広げる必要はありません。用途に応じてサイズを変更させてください。ここでは練習のため画面全体に広げているだけです。
コンテンツビューの配置
実際にスクロールさせる部品であるコンテンツビュー(contents view)を配置します。コンテンツビューにはUIViewを用いることが多いと思います。Objectライブラリから「View」を選択し,SV上にドロップします。以後分かりやすいようにViewの名前を「contentsView」に変更します。
大きさと位置を決めましょう。右上にある「Show the Size inspector」にある「width」と「height」にぞれぞれ「800」と「1200」を設定します。この
contentsView
を上下左右に青い点線が出るところに配置します。もし上手くいかなければ,「Show the Size inspector」の「X」と「Y」にそれぞれ「-193」と「-152」を入力してください(iPhoneXRを選択していること)。もちろんこれらも用途に合わせて自由に設定してください。コンテンツビューだけではスクロールしているかどうかの判別が付かないのでUILabelを配置させましょう。Objectライブラリから「label」を選び,
contentsView
上にドロップさせ,上下左右に青い点線が出るところに配置させます。コンテンツビューのレイアウト
まだ各UI部品の位置関係についても何も制約をかけていませんでしたので制約をかけていきます。View Controllerを選択してある状態で画面右下の「Resolve Auto Layout issues」を押します。その中にある「All Views in Container」欄の「Add Missing Constraints」を押します。すると下図のように制約がかかると思います。
お疲れさまです。ここまできたら一旦ビルドしてみて下さい。ビルド後,画面をマウスなどで動かして見てください。おそらく動かないはずです。衝撃的ですよね。ここまで説明しておいて動かないなんて。SV上にはそれより大きな
contentsView
を配置したのでスクロールできても良さそうですが何故かできせん。Appleが悪いわけではありません。制約のかけ方が間違っているからです。下図の青色の点線で囲んだところをよく見てください。例えばSVのbottomの位置をみると
contentsView
から152下がった位置で制約がかかっています。上下左右ともSVとcontentsView
の位置はガチガチに制約がかかっている状態です。この状態ではスクロールはできません。それではスクロールができるように制約をかけ直しましょう。まず上の青い点線で囲んだ「Constraints」を消します。次に,contentsViewを選択し,画面右下にある「Add New Constraints」を押し,4つとも「0」を入力します。そして最後に「Add 4 Constraints」を押し,ビルドしてください。今度はスクロールできるはずです。
スクロールできた理由が大切です。SVの上下左右の位置と
contentsView
の上下左右の位置を一致させたためです。これによって,スクロールできる範囲は,左方向に関してはcontentsView
の左端がSVの左端に一致するまでとなりました。その他の位置も同様です。第2章 コードでやってみる
SVの配置のみstoryboard上で行い,上記で行ったコンテンツビューの配置などはコードでやってみましょう。
Outlet接続
SV以外のUIは全て削除し,SVをscrollViewという名前でOutlet接続させます。
コンテンツビューの作製
コンテンツビューとその中身を作っていきましょう。以下がコード全体になります。
ViewController.swiftimport UIKit class ViewController: UIViewController { @IBOutlet weak var scrollView: UIScrollView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. configureSV() } func createLabel(contentsView: UIView) -> UILabel { // labelを作る let label = UILabel() // labelの座標をcontentsViewの中心にする let labelX = contentsView.center.x let labelY = contentsView.center.y label.frame = CGRect(x: labelX, y: labelY, width: 95, height: 50) label.text = "Label" return label } func createContentsView() -> UIView { // contentsViewを作る let contentsView = UIView() contentsView.frame = CGRect(x: 0, y: 0, width: 800, height: 1200) // contentsViewにlabelを配置させる let label = createLabel(contentsView: contentsView) contentsView.addSubview(label) return contentsView } func configureSV() { // scrollViewにcontentsViewを配置させる let subView = createContentsView() scrollView.addSubview(subView) // scrollViewにcontentsViewのサイズを教える scrollView.contentSize = subView.frame.size } }それでは細かく見ていきましょう。
func createLabel(contentsView: UIView) -> UILabel
ラベルを生成するメソッドです。
let label = UILabel() // labelの座標をcontentsViewの中心にする let labelX = contentsView.center.x let labelY = contentsView.center.y label.frame = CGRect(x: labelX, y: labelY, width: 95, height: 50) label.text = "Label" return label引数である
contentsView
の中心にlabel
が配置するように設定しています。return
でそのlabel
を返します。
func createContentsView() -> UIView
コンテンツビューを生成するメソッドです。
let contentsView = UIView() contentsView.frame = CGRect(x: 0, y: 0, width: 800, height: 1200) // contentsViewにlabelを配置させる let label = createLabel(contentsView: contentsView) contentsView.addSubview(label) return contentsView
func configureSV()
SVの設定を行うメソッドです。
// scrollViewにcontentsViewを配置させる let subView = createContentsView() scrollView.addSubview(subView) // scrollViewにcontentsViewのサイズを教える scrollView.contentSize = subView.frame.sizeスクロールさせるためにはSVにコンテンツビューの大きさを教える必要があります。SVの
contentSize
プロパティーにその大きさを設定することでSVにコンテンツビューの大きさを教えることができます。これでビルドすると第1章と同様にスクロールさせることができると思います。ただ現状では,初期の「label」の位置が右下付近によっています。上下左右の中央にずらしたいですが,初期位置に関しては別の章で説明します。
第3章 横スクロールさせる
第1,2章だけではあまり面白みがありません。SVを使用する時は,横か縦方向にスクロールさせることが多いと思います。この章では横スクロールに焦点を当て,もうすこし実践的にしましょう。
スクロールで色が変わる画面を作る
次のようにスクロールさせることでページごとに色が変わるのアプリケーションを作りましょう。
スクロールビューのみstoryboardを使用して,それ以外はコードで実装していきます。任意ですが,スクロールビューの制約はSafeAreaに対して行います。全体のコードです。
ViewController.swiftimport UIKit class ViewController: UIViewController { let numberOfPages = 5 let colors: [UIColor] = [.yellow, .gray, .red, .blue, .brown] @IBOutlet weak var scrollView: UIScrollView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. configureSV() } func createPages(page: Int) -> UIView { let pageView = UIView() let pageSize = scrollView.frame.size let positionX = pageSize.width * CGFloat(page) let position = CGPoint(x: positionX, y: 0) pageView.frame = CGRect(origin: position, size: pageSize) pageView.backgroundColor = colors[page] return pageView } func createContentsView() -> UIView { let contentsView = UIView() let contentsWidth = scrollView.frame.width * CGFloat(numberOfPages) let contentsHeight = scrollView.frame.height contentsView.frame = CGRect(x: 0, y: 0, width: contentsWidth, height: contentsHeight) for i in 0 ..< numberOfPages { let pageView = createPages(page: i) contentsView.addSubview(pageView) } return contentsView } func configureSV() { let contentsView = createContentsView() scrollView.addSubview(contentsView) scrollView.contentSize = contentsView.frame.size } }細かく見ていきましょう。
インスタンスプロパティー
numberOfPages
:ページ数
colors
:色の設定
func createPages(page: Int) -> UIView
ページを生成するメソッドです。
let pageView = UIView() let pageSize = scrollView.frame.size let positionX = pageSize.width * CGFloat(page) let position = CGPoint(x: positionX, y: 0) pageView.frame = CGRect(origin: position, size: pageSize) pageView.backgroundColor = colors[page] return pageViewページの幅は
scrollView
と同じにしています。引数page
は現在のページの番号です。ページがスクロールした分だけページの位置がずれ,その値をpositionX
に格納します。pageの背景色はcolors
に格納されている色を順に取り出して
backgroundColor
で設定します。
func configureSV()
前章と同じでSVの設定を行います。
さて,これでビルドすると以下のようにスクロールができると思います。
スクロールビューの設定
ここではスクロールビューにどんな設定ができるかを見てみましょう。すべて
Bool
型で設定します。
インスタンスプロパティー名 true
の時の内容デフォルト値 isScrollEnabled
スクロール可能 true
isDirectionalLockEnabled
最初にスクロールさせた方向(水平か垂直)のみスクロール可能 false
isPagingEnabled
1ページずつスクロールする false
scrollsToTop
ステータスバーをタップすると一番上までスクロールする true
bounces
スクロールがコンテンツの一番端に到達すると跳ね返る(バウンドする) true
alwaysBounceVertical
コンテンツがスクロールビューよりも小さい場合でも常に垂直方向のバウンドを許可する。ただし bounces=true
であることも必要false
alwaysBounceHorizontal
コンテンツがスクロールビューよりも小さい場合でも常に水平方向のバウンドを許可する。ただし bounces=true
であることも必要false
例えば,ページ単位でスクロールするには以下のように設定します。
let scrollView = UIScrollView() scrollView.isPagingEnabled = true記事が長くなってきたのでここで一旦区切ります。第4章以降は別記事で説明します。お疲れ様でした。
続編
【徹底解説】UIScrollViewクラス その2(執筆中)
参考文献
詳細! Swift iPhoneアプリ開発入門ノート 著者:大重 美幸 出版社: ソーテック社
ご意見等あれば連絡ください。
- 投稿日:2019-05-23T17:56:30+09:00
Animations in iOS: Tab Bar concepts
We see tab bars every day.
They guide users inside the app, allowing them to quickly switch between different tabs. Apple provides a nice into in their Human Interface Guidelines:
Tab bars are translucent, may have a background tint, maintain the same height in all screen orientations, and are hidden when a keyboard is displayed. A tab bar may contain any number of tabs, but the number of visible tabs varies based on the device size and orientation. If some tabs can’t be displayed due to limited horizontal space, the final visible tab becomes a More tab, which reveals the additional tabs in a list on a separate screen
But who said mobile navigation should be boring?Let’s explore interesting animations inside the tab bars. I focused first on the ones made for iOS platform. And mentioned several useful guides to implement animated tab bars in Swift ?
Some of the animations are from real apps. While others — are just nice design concepts, made for inspiration.
Would you use some of them for your apps?
WeChat Tab Bar Redesign by Adrian Reznicek for PLATFORM
Animated tab bar concept by Cadabra Studio
Camera app tab bar [iPhone X edition] concept by Oleg Frolov for Magic Unicorn
Yoga App Menu Concept by Dannniel
Create a New Document Tab Bar concept by Hoang Nguyen
Fluid Tab Bar Interaction concept by Oleg Frolov
Animated Tab Bar Icons — Interface concept by Andrew McKay
Animated Tab Bar Icons — Interface concept by Andrew McKay
Tab bar icons by Dimest
Tab bar active animation by Aaron Iker, based on Valentin Tsymbaluk concept
Animated Tab Bar Icons — Interface concept and Swift implementation by Ramotion
Tab bar interaction concept by Boyang Zhang
Animated Tab Bar concept by Mauricio Bucardo
Tab bar interaction concept by Kaiseir
If you want to practice with designing or developing tab bars, these guides will help you out:
Human Interface Guidelines on Tab Bars, where you’ll learn best practices in a quick 4 min read.
Guide on “Starting an iOS Tab Bar App with UITabBarViewController”.
29 implemented examples .
Swift UI module library for adding animation to iOS tab bar.
Hope you enjoyed this small inspirational peace. You can also check 30 beautiful examples of animations for iOS and Everything you need to know about Loading Animations
Happy designing and developing!
- 投稿日:2019-05-23T15:26:02+09:00
Realmで配列を扱う!?
はい!タイトル詐欺です!Realmで配列を扱うことはできません!
じゃあどうするのかというと、「List」という型を使ってデータに紐付けをしていくというのが今回の内容です。
まずRealmとは
簡単にいうとデータベースです
Realm公式:https://realm.io/jp/
RealmSwiftリファレンス:https://realm.io/docs/swift/latest/api/index.htmlSwiftでRealmを扱うには「RealmSwift」というライブラリを使用します。
RealmSwiftで扱える「型」
詳しくはリファレンスを見ていただきたいんですが、主に扱える型としては
・String
・Int
・Bool
・Date
・List
などになります今回はこの「List」に焦点を当てていきます
Listについて
公式リファレンスの説明をgoogleさんに翻訳してもらいました
「List 1対多の関係を定義するために使用されるRealmのコンテナタイプです。」
よくわかんないですね
Swift的にわかりやすく説明すると
["task": "夏休みの宿題", "ticket": ["英語", "算数", "社会"]]ちょっと違うかもしれないですけどこんな感じですかね?
言葉で説明すると
「'夏休みの宿題' というタスクが '英語','算数','社会' のチケットを持っている」
という感じでしょうか?余計わかりにくなった?とりあえずこれをRealmで書いてみます
モデルの定義
それでは早速Realmの定義を書いていきましょう!
インストールなどについてはわかりやすい記事がたくさんあるのでご自身で調べてみてください
例https://qiita.com/leegun/items/70414223b2339b6052eeまずはクラスを作っていきます
今回は先ほど例に出したタスクとチケットのモデルを定義していきますimport RealmSiwf class Task: Object { @objc dynamic var taskTitle: String = "" } class Ticket: Object { @objc dynamic var ticketTitle: String = "" }Realmでモデルを定義する際には
@objc dynamic
と記載しますタスクとチケットのモデルが定義できました!
ただこれだとタスクとチケットが紐付いていません・・・
Listを使って1対多の関係を提示してあげましょう
import RealmSiwf class Task: Object { @objc dynamic var taskTitle: String = "" //Listの定義 let tickets = List<Ticket>() } class Ticket: Object { @objc dynamic var ticketTitle: String = "" }Listを定義する際は
List<Element>
としてあげる必要がありますこれで定義は完了しました!
では実際にデータを追加したり参照したりしてみましょう
データモデルの追加
Realmのインスタンスを取得し、そこにデータを挿入していきます
do { //インスタンスの取得 let realm = try Realm() let dictionary: [String: Any] = ["taskTitle": "夏休みの宿題", "tickets": [["ticketTitle": "算数"], ["ticketTitle": "英語"], ["ticketTitle": "社会"]] ] let task = Task(value: dictionary) //Taskモデルのインスタンスの作成 //書き込み処理 try! realm.write { realm.add(task) print(task) } } catch { print(error) }プリント結果
Task { taskTitle = 夏休みの宿題; tickets = RLMArray<Cat> <0x1c0306150> ( [0] Ticket { ticketTitle = 算数; }, [1] Ticket { ticketTitle = 英語; }, [2] Ticket { ticketTitle = 社会; } ); }Taskのインスタンスにタスク名とチケットを挿入して、実際のデータに書き込んでいます。
データに書き込んだり、またそのデータを削除したりなどの処理は
realm.write{}
の中にコーディングする必要があるので気をつけてください!データモデルの取得(Results)
追加したデータモデルを取得してみましょう
Realmデータベースに保存されている特定のモデルクラスのオブジェクトを全て取得するにはRealmクラスのobjects(_:)を使用します。
do { //インスタンスの取得 let realm = try Realm() //オブジェクトの取得 let results = realm.objects(Task.self) print(results) } catch { print(error) }プリント結果
Results<Task> <0x104423670> ( [0] Task { taskTitle = 夏休みの宿題; tickets = RLMArray<Ticket> <0x1c41193e0> ( [0] Ticket { ticketTitle = 算数; }, [1] Ticket { ticketTitle = 英語; }, [2] Ticket { ticketTitle = 社会; } ); }List側の削除と追加
まずはチケットを追加する処理を書いてみましょう
通常Realmでデータを追加するときは"add"を使用するのですが、List型の場合"append"で追加します。
do { //インスタンスの取得 let realm = try Realm() let results = realm.objects(Task.self) let ticket = Ticket(value: ["ticketTitle": "国語"]) //Ticketモデルのインスタンスの作成 print("追加前",results) // 追加処理 try! realm.write { for task in results { task.tickets.append(ticket) } } print("追加後",results) } catch { print(error) }プリント結果
追加前 Results<Task> <0x7ff8c650f470> ( [0] Task { taskTitle = 夏休みの宿題; tickets = List<Ticket> <0x600003d84360> ( [0] Ticket { ticketTitle = 算数; }, [1] Ticket { ticketTitle = 英語; }, [2] Ticket { ticketTitle = 社会; } ); } ) 追加後 Results<Task> <0x7ff8c650f470> ( [0] Task { taskTitle = 夏休みの宿題; tickets = List<Ticket> <0x600003d84a20> ( [0] Ticket { ticketTitle = 算数; }, [1] Ticket { ticketTitle = 英語; }, [2] Ticket { ticketTitle = 社会; }, [3] Ticket { ticketTitle = 国語; } ); } )では次に削除してみましょう!
Listには「removeLast()」や「removeFirst()」などのメソッドがあり最初のデータや最後のデータなどを指定して削除できます
今回は「remove(at:Int)」を使用して特定の値を削除してみたいと思いますdo { //インスタンスの取得 let realm = try Realm() let results = realm.objects(Task.self) print("削除前",results) // 削除処理 try! realm.write { for task in results { for (index, ticket) in task.tickets.enumerated() { if ticket.ticketTitle == "国語" { task.tickets.remove(at: index) } } } } print("削除後",results) } catch { print(error) }プリント結果
削除前 Results<Task> <0x7fc26af135e0> ( [0] Task { taskTitle = 夏休みの宿題; tickets = List<Ticket> <0x600003d5b9f0> ( [0] Ticket { ticketTitle = 算数; }, [1] Ticket { ticketTitle = 英語; }, [2] Ticket { ticketTitle = 社会; }, [3] Ticket { ticketTitle = 国語; } ); } ) 削除後 Results<Task> <0x7fc26af135e0> ( [0] Task { taskTitle = 夏休みの宿題; tickets = List<Ticket> <0x600003d51950> ( [0] Ticket { ticketTitle = 算数; }, [1] Ticket { ticketTitle = 英語; }, [2] Ticket { ticketTitle = 社会; } ); } )ちょっとネストがきつくなってしまいましたがご愛嬌ということで
おまけ!
タイトルで配列云々と言っていので、上記で作成したデータを配列に戻してみます
do { //インスタンスの取得 let realm = try Realm() let results = realm.objects(Task.self) var taskArray = ["taskTitle":"", "tickets":[]] as [String : Any] var ticketArray: Array<String> = [] for task in results { taskArray["taskTitle"] = task["taskTitle"] for ticket in task["tickets"] as! List<Ticket> { ticketArray.append(ticket.ticketTitle) } } taskArray["tickets"] = ticketArray print(taskArray) } catch { print(error) }プリント結果
["taskTitle": 夏休みの宿題],"tickets": ["算数", "英語", "社会"]]こんな感じでしょうか?
最後に
Realmは直感的にデータをいじれるので使っていて楽しいです
自分もまだまだ理解できていないことがたくさんあるので習得に励んでいこうと思います!皆さんも良いRealmライフを!!
追記
@t_naganoさんからのご指摘!
Realmからデータを取り出すとき、「map」を使用してあげるとより簡潔に取り出せます!do { //インスタンスの取得 let realm = try Realm() let results = realm.objects(Task.self) var taskArray = ["taskTitle":"", "tickets":[]] as [String : Any] var ticketArray = [String]() for task in results { taskArray["taskTitle"] = task["taskTitle"] // for ticket in task["tickets"] as! List<Ticket> { // ticketArray.append(ticket.ticketTitle) // } ticketArray = task.tickets.map { $0.ticketTitle } // mapを使って取り出す } taskArray["tickets"] = ticketArray print(taskArray) } catch { print(error) }
- 投稿日:2019-05-23T13:30:58+09:00
Xcode 9.4.1やXcode 10.1でiOS 12.3やiOS 12.4 betaをデバッグする方法
この方法は非公式です
Xcode 9.4.1でiOS 12.1のデバッグを行いたいときに
/Xcode_10.1.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/12.1
を
/Xcode_9.4.1.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/
にコピーしてデバイスを接続することで
/Users/UserName/Library/Developer/Xcode/iOS DeviceSupport
に 12.1 (16B91) のファイルがデバイスから吸い上げられデバッグが可能になります。
この方法は多くの方がご存知なのではないかと思います。2019/05/23段階でXcode 10.3がリリースされていない
Appleさん、どうしたんでしょうか。
もうiOS 12.4 beta 2がリリースされているというのにXcodeのバージョンは10.2.1が最新版のままです。
Xcode 10.3やXcode 10.4 betaがリリースされないため DeviceSupport をコピーしようにもできず
iOS 12.3や12.4 betaでデバッグができません。対処法
- Xcode 10.2.1をダウンロード
- /Xcode_10.2.1.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/12.2 をコピー
- /Xcode_10.1.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/ に貼り付けて 12.3 (16F156) や 12.4 (16G5027i) にリネーム
- Xcodeを再起動してデバイスを接続
以上でXcode 9.4.1や10.1でiOS 12.3や12.4 beta 2のデバッグが可能です。
この方法はあくまでも非公式であり何らかの問題が発生する可能性も無くはないため、実施は自己責任でお願いします。
Appleさん、早いとこXcodeの更新お願いしますよ…。
- 投稿日:2019-05-23T10:34:09+09:00
「Firebase + stripe + iOS」でクレジットカード決済機能を作る
Firebase Firestoreでデータを管理しているiOSアプリに決済機能をつけたいと思い、stripeを使うことにしました。そこで得た「Firebase + stripe」だけで決済機能を作る方法を紹介したいと思います。
ゴールは以下のような感じ。
手順
手順は以下のようになります。
- stripe上に顧客(customer)を作成する
- 決済時にcustomerIdを使ってワンタイムトークンをリクエストする
- ワンタイムトークンを使ってカード情報を取得
- customerIdとカード情報を使って決済リクエストする
省略して書くと、
- 顧客の作成
- ワンタイムトークン発行
- カード情報の取得
- 決済
となります。まず、これら一連の流れに必要なAPI作成の話をした後に、iOSから行うこれらの手順を1つずつ紹介していきます。
FunctionsでAPIを作成する
公式のはじめに: 最初の関数を作成してデプロイするを参考にして、Cloud Functionsのプロジェクトを作ります。今回はTypeScriptで作りました。
そして
index.ts
にて3つのAPIを作ります。
- customerを作ってcustomerIdをフロントに返すAPI
- ワンタイムトークンを発行するAPI
- 決済するAPI
index.tsconst stripe = require('stripe')(functions.config().stripe.token); // MARK: - stripeのcustomerを作ってcustomerIdを返す exports.createStripeCustomer = functions.https.onCall(async (data, context) => { const email = data.email; const customer = await stripe.customers.create({email: email}); const customerId = customer.id; return { customerId: customerId } }); // MARK: - Stripeのワンタイムトークンを発行する exports.createStripeEphemeralKeys = functions.https.onCall((data, context) => { const customerId = data.customerId; const stripe_version = data.stripe_version; return stripe.ephemeralKeys .create({ customer: customerId, stripe_version: stripe_version }) }); // MARK: - Stripeの決済する exports.createStripeCharge = functions.https.onCall((data, context) => { const customer = data.customerId; const source = data.sourceId; const amount = data.amount; return stripe.charges.create({ customer: customer, source: source, amount: amount, currency: "jpy", }) });なぜわざわざstripeの処理を全てFunctionsの中に書くのか?
実は、stripeのシークレットキーをiOSアプリの中に書いちゃえば、これらのAPIをCloud Functionsに書く必要はなく、直接Stripeと通信すればいいのですが、それは、セキュリティ的に推奨されないので、stripeのシークレットキーをFirebase Cloud Functionsの環境変数として置いてdeployしています。
ちなみに環境変数の設定方法は、環境の構成に書いてあって、今回の場合は、
firebase functions:config:set stripe.token="<ここにシークレットキー>"とコマンドで打って設定しています。
functions.https.onCallトリガーを使った理由
Firebase Cloud FunctionsにはFirestoreへの書き込みをトリガーに発火するfunctionも書けますが、他の処理とのトランザクション処理をフロントでやっちゃいたかったので、アプリから関数を呼び出すを参考に、Cloud FunctionsのiOS SDKを使って直接APIとして呼び出すことにしました。
deployに成功すると、以下のようにFirebaseの管理画面上で確認できます。上の4つの関数は書き込みトリガーなのに対して今回作ったStripe用の関数はHTTPリクエストなのが特徴的ですね。(もちろん場合によって書き込みトリガーでも良いと思います)
iOS側の実装
iOS側の実装方法を説明していきます。
顧客の作成
今回は、最初のユーザー作成時にStripeの顧客も作成するようにしました。FirebaseのAuthでユーザーを作成し、そこで得たemailアドレスを使って先ほどのAPIを叩きcustomerIdを取得、最後に細かいプロフィール情報は、FireStoreのUsersというcollectionに格納、その中のパラメータの1つとしてcustomerIdを作って入れるという手順です。
以下のメソッドでは、先ほど作った
createStripeCustomer
にemailを渡してstripeのcustomerIdを作っています。import FirebaseFunctions lazy var functions = Functions.functions() func createCustomerId(email: String, completion: ((String?, Error?) -> Void)?){ let data: [String: Any] = [ "email": email ] functions.httpsCallable("createStripeCustomer") .call(data) { result, error in if let error = error { completion(nil, error) } else if let data = result?.data as? [String: Any], let customerId = data["customerId"] as? String { completion(customerId, nil) } } }ワンタイムトークン発行
Stripeでは決済する際にワンタイムトークンが必要です。customerIdは既にFirestoreのUserの中に入っているので、それを使ってワンタイムトークンを発行します。
ここではStripeの公式ドキュメントUsing iOS Standard UI Components
を見ながら実装していきます。まず、stripe-ios SDKをcocoapods等でプロジェクトにいれておきます。
STPCustomerEphemeralKeyProviderに準拠したStripeProviderを作成し、ここで必須メソッドとなるcreateCustomerKeyの中で、先ほど作ったfunctionsのAPIを叩くようにします。この中に書いておくと、必要なタイミングで勝手に呼ばれて叩いてくれます。
import Stripe import FirebaseFunctions class StripeProvider: NSObject, STPCustomerEphemeralKeyProvider { lazy var functions = Functions.functions() let customerId: String init(customerId: String){ self.customerId = customerId } func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) { let data: [String: Any] = [ "customerId": customerId, "stripe_version": apiVersion ] functions .httpsCallable("createStripeEphemeralKeys") .call(data) { result, error in if let error = error { completion(nil, error) } else if let data = result?.data as? [String: Any] { completion(data, nil) } } } }決済をしたいViewControllerにて、ボタンの押下をトリガーに以下のように実装します。
ViewControllerimport Stripe private var paymentContext: STPPaymentContext? @IBAction func stripeButtonTapped(_ sender: Any) { let customerId = "firestoreから取得" let customerContext = STPCustomerContext(keyProvider: StripeProvider(customerId: customerId)) paymentContext = STPPaymentContext(customerContext: customerContext) paymentContext!.delegate = self paymentContext!.hostViewController = self paymentContext!.paymentAmount = 5000 paymentContext!.presentPaymentOptionsViewController() }すると、SDKに用意されたクレジットカード追加のUIが出てきます。これ、驚くべきことにSDKにデフォルトで用意されてるUIなんですよ!いい感じですよね。
stripeのドキュメントにも用意されているテストで使えるcard一覧で使える番号でテストしてみましょう。
カード情報の取得
一度カードを登録すると、stripeのサーバーに登録され、そのカードが次から選択できます。カードを選択すると、STPPaymentContextDelegateのpaymentContextDidChangeメソッドが呼び出され、カード番号の下4桁、カード会社の画像が得られるので、UIでフィードバックできます。
extension ViewController: STPPaymentContextDelegate { func paymentContextDidChange(_ paymentContext: STPPaymentContext) { cardNameLabel.text = paymentContext.selectedPaymentOption?.label cardImageView.image = paymentContext.selectedPaymentOption?.image } func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) { // 省略 } func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) { // 省略 } }決済
最後に決済です。
paymentContextをViewControllerのグローバル変数として保持しておいて以下のように使います。
paymentContext?.requestPayment()これをボタンのアクションに埋めます。
@IBAction func payButtonTapped(_ sender: Any) { paymentContext?.requestPayment() }すると決済確認のリクエストが走るので、この結果をまたdelegateで受けます。
ここではまだ決済が完了しません。
paymentResult
が作られ、stripeId
が受け取れるので、これとcustomerIdをさらにfunctionsのAPIに渡して、実際の決済はAPIの方でやってもらいます。lazy var functions = Functions.functions() extension ViewController: STPPaymentContextDelegate { func paymentContextDidChange(_ paymentContext: STPPaymentContext) { // 省略 } // paymentContext?.requestPayment()が押されたら呼ばれる func paymentContext(_ paymentContext: STPPaymentContext, didCreatePaymentResult paymentResult: STPPaymentResult, completion: @escaping STPErrorBlock) { let sourceId = paymentResult.source.stripeID let paymentAmount = paymentContext.paymentAmount self.functions.httpsCallable("createStripeCharge") .call(data) { result, error in if let error = error { completion(error) } else { completion(nil) } } } // 上のcompletionをトリガーに呼ばれる func paymentContext(_ paymentContext: STPPaymentContext, didFinishWith status: STPPaymentStatus, error: Error?) { switch status { case .error: self.showErrorDialog(error!) // 独自 case .success: self.showOKDialog(title: "決済に成功しました") // 独自 case .userCancellation: break @unknown default: break } } }これが一連の流れです。
決済が成功すると、以下のようにstripeの管理画面上で確認ができます。
以上。需要があればサンプルコードをGitHubに公開します。
参考記事
- 投稿日:2019-05-23T10:18:39+09:00
ReleaseビルドでMKMapViewDelegateのメソッドが呼び出されなくなった話
はじめに
これは、ReleaseビルドしたiOSアプリで
MKMapViewDelegate
の下記のようなメソッドが呼び出されないことがある現象に遭遇し、腑には落ちないけどなんとか解消したというメモです。func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?状況
環境
- Xcode 10.2
- Swift 5
- iOS 11 or 12
(Xcode 10.1 + Swift 4.2 のころに、この問題は発生していませんでした)
実装
アプリ内に
MKMapView
を表示するコントローラクラスが複数存在し、それらを抽象化した親クラスBaseMapViewController
がMKMapViewDelegate
を宣言していました。(宣言のみで実装は無し)class BaseMapViewController: UIViewController, MKMapViewDelegate { @IBOutlet weak var mapView: MKMapView! // MKMapViewDelegateの実装はなし }子クラス
FirstMapViewController
,SecondMapViewController
,ThirdMapViewController
... が実際にそれぞれの delegate メソッドを実装していましたが、ReleaseビルドにおいてFirstMapViewController
のrendererFor
は呼び出されるのに、SecondMapViewController
のrendererFor
は呼び出されないという不思議現象。(Debugビルドでは想定通りSecondMapViewController
のメソッドも呼び出されていました)class FirstMapViewController: BaseMapViewController { func showOverlay() { mapView.addOverlay(overlay, .aboveRoads) } func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { // 呼び出される? return FirstRenderer() } } class SecondMapViewController: BaseMapViewController { func showOverlay() { mapView.addOverlay(overlay, .aboveRoads) } func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { // 呼び出されない❌☠️ return SecondRenderer() } } class ThirdMapViewController: BaseMapViewController { func showAnnotation() { mapView.addAnnotation(annotation) } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? // 呼び出される? return ThirdView() } } class FourthMapViewController: BaseMapViewController { func showAnnotation() { mapView.addAnnotation(annotation) } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? // 呼び出されない❌☠️ return FourthView() } }*もちろん実際の実装はもっと複雑で、このコードだとおそらくちゃんと呼び出されると思うのですが、雰囲気が伝わればということでご了承ください。(なにしろ、実装を変更していないのに、突如呼び出されなくなってしまったので)
回避策
BaseMapViewController
でrendererFor
とviewFor
の空実装を定義し、それぞれの子クラスでそれらの delegate メソッドを override すると、リリースビルドでも正しく delegate メソッドが呼ばれるようになりました。class BaseMapViewController: UIViewController, MKMapViewDelegate { @IBOutlet weak var mapView: MKMapView! func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { // 空実装 return MKOverlayRenderer() } func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? // 空実装 return nil } } class FirstMapViewController: BaseMapViewController { override func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { // 呼び出される? return FirstRenderer() } } class SecondMapViewController: BaseMapViewController { override func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { // 呼び出された!!⭕️? return SecondRenderer() } } class ThirdMapViewController: BaseMapViewController { override func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? // 呼び出される? return ThirdView() } } class FourthMapViewController: BaseMapViewController { override func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? // 呼び出された!!⭕️? return FourthView() } }