- 投稿日:2019-09-09T22:47:47+09:00
プロパティまとめ(Swift)
プロパティとは
プロパティとは、型に紐づいた値のことです。
例えば、TVは色や機種、製造会社、画素数など多数の情報を持っています。この場合型はTV、それに関連するその他の情報をプロパティと言えます。
このようにクラスや構造体、列挙型に紐づく値のことをプロパティと呼びます。基本的な定義
//Carが構造体名 struct Car { //再代入可 var プロパティ名: プロパティの型 = 式 //再代入不可 let プロパティ名: プロパティの型 = 式 }上記が基本的なプロパティの定義文になります。
このコードはCarという構造体の内部でプロパティが定義されている形となっているため、これらのプロパティはCar型のプロパティとなります。そしてvarで定義した値が再代入可能な値、letで代入した値が再代入不可なものになります。サンプルコード
struct Something { var variable = 123 let constant = 456 } //インスタンス化 let something = Something() let a = something.variable let b = something.constant //実行 print(a) print(b) //結果 123 456上記のサンプルコードを見ると構造体Something内で定義した変数と定数を別の定数に代入する際に、インスタンス化したsomethingに対して、「.」の後にプロパティ名を指定しています。つまり構造体内で定義したプロパティを使用する場合、インスタンス化した構造体に対して、プロパティ名を「.」の後に指定する必要があるということです。
結果として、代入されたaとbをprintで実行することで結果が出力されました。プロパティの種類
紐づく対象による分類
プロパティはその値がどこに紐づいているのかにより、2つに分類されます。
それを以下で紹介します。インスタンスプロパティ
インスタンスプロパティとは先ほどのサンプルコードで定義したプロパティのことを指します。
通常単にプロパティと呼ぶときは、インスタンスプロパティのことを指します。
このインスタンスプロパティは型のインスタンスに紐づくため、インスタンスごとに異なる値を格納できます。そのため下記のコードを見ると、最初にインスタンス化した構造体と2回目にインスタンス化した構造体とでは、2つ目のインスタンスに値を再代入しているため、最終的な結果が異なります。これが先ほど言った異なる値を格納できるインスタンスプロパティの特性です。//構造体定義 struct Greeting { プロパティ定義 var to = "Taro Yamada" } //構造体をインスタンス化1 let greeting1 = Greeting() //構造体をインスタンス化2 let greeting2 = Greeting() greeting2.to = "Hanako Yamada" let to1 = greeting.to let to2 = greeting2.to print(to1) Print(to2) //結果 Taro Yamada Hanako Yamadaスタティックプロパティ
スタティックプロパティは先ほどのインスタンスプロパティとは異なり、インスタンスではなく型そのものに紐づいているプロパティになります。
このプロパティはインスタンスに紐づいているわけではないため、各インスタンスから変更を加えることも出来ずどのインスタンスでも統一された値となります。そのため、下記コードを見ると別々で定義したインスタンスの実行結果でもスタティックインスタンスに関しては同一になります。
ちなみにこのスタティックプロパティの定義方法は構造体でのプロパティ定義時に文頭にstaicをつけることです。
※注意点としてはインスタンスプロパティの場合宣言さえすれば、インスタンス化後代入のタイミングがあるため、初期値を設定する必要はないが、スタティックプロパティはインスタンス化後代入が不可能なので宣言時に初期値を代入しないとコンパイルエラーになります。//構造体定義 struct Greeting { //スタティックプロパティ定義 static let signature = "Sent form iPhone" //インスタンスプロパティ定義 var to = "Kankurou Nekoyashiki" var body = "Hello!" } //関数定義 func print(greeting: Greeting) { print("to: \(greeting.to)") print("to: \(greeting.body)") print("signature: \(Greeting.signature)") } let greeting1 = Greeting() var greeting2 = Greeting() greeting2.to = "Shinji Hamada" greeting2.body = "Yeah!" print(greeting: greeting1) print("------------------") print(greeting: greeting2) //実行結果 to: Kankurou Nekoyashiki to: Hello! signature: Sent form iPhone ------------------ to: Shinji Hamada to: Yeah! signature: Sent form iPhone値を保持するかしないかの分類
ストアドプロパティ
プロパティは値を保持するストアドプロパティと、値を保持しないコンピューテッドプロパティに分類されます。
このプロパティは先ほどのサンプルコードでも出てきた値を保持することが可能なプロパティのことを指します。そのためインスタンスプロパティとスタティックプロパティの双方ともに、このストアドプロパティに分類されることがあります。
下記のように値を保持可能はプロパティは全てこのストアドに分類されます。struct SomeStruct { var vaiable = 123 //再代入可能 let constant = 456 //再代入不可 static var staticVariable = 789 //再代入可、型自身に紐づく static let staticVariable = 890 //再代入不可、型自身に紐づく }プロパティオブザーバ
プロパティオブザーバとはストアドプロパティに使える機能です。これはストアドプロパティの値の変更を監視し、変更前と変更後に処理を実行する際に使えます。
以下がプロパティオブザーバの定義文になります。下記は構造体内で定義したプロパティを表しており、いつものプロパティ定義文の後に中括弧を記述することでプロパティオブザーバが定義可能になります。var プロパティ名 = 初期値 { willSet { プロパティ変更前に実行する文 変更後の値には定数newValueとしてアクセスできます。 } didSet{ プロパティ変更後に実行する文 } }サンプルコード
//構造体定義 struct Greeting { //プロパティオブザーバ var to = "Yosuke Ishikawa" { //変更前の処理 willSet { print("willSet: (to: \(self.to), newValue: \(newValue))") } //変更後の処理 didSet { print("didSet: (to: \(self.to))") } } } //インスタンス化 var greeting = Greeting() //実行 greeting.to = "Yusei Nishiyama" //結果 willSet: (to: Yosuke Ishikawa, newValue: Yusei Nishiyama) didSet : (to: Yusei Nishiyama)上記のようにwillSetの内部で変更前の処理、didSetの内部で変更後の処理を記述することで、実装できます。
レイジーストアドプロパティ
レイジーストアドプロパティとは、アクセスされるまで初期化を遅延させるストアドプロパティのことです。定義方法は文頭に「lazy」と記述することで、記述順序としては「static lazy var」と言う流れになります。
※注意点としては、lazyは再代入不可な「let」での定義では使用できないので注意してください。lazy var インスタンスプロパティ名: プロパティの型 = 式 static lazy var インスタンスプロパティ名: プロパティの型 = 式サンプルコード
struct SomeStruct { var value: Int = { print("valueの値を生成します") return1 }() lazy var lazyValue: Int = { print("lazyValueの値を生成します") return 2 }() } //実行 var someStruct = someStruct() print("SomeStructをインスタンス化しました") print("valueの値は\(someStruct.value)です") print("lazyValueの値は\(someStruct.lazyValue)です") //結果 valueの値は生成します SomeStructをインスタンス化しました valueの値は1です lazyValueの値を生成します lazyValueの値は2です上記のコードのポイントは、初期化処理が行われた際にprint()関数が呼ばれるように仕込んだプロパティを定義している点です。そのため、プロパティの初期化時にprintでログが出力されるようになります。
その結果valueに対しては「valueの値を生成します」と言う文字列がインスタンス化した時に出力され、lazyValueに対しては「lazyValueの値を生成します」が値を呼び出された時に出力されています。これは通常インスタンス化した際に行われる初期化が、lazyValueの場合値を呼び出されてた時に初めて初期化したため、起こった現象になります。
以上がレイジーストアドプロパティの特性になります。コンピューテッドプロパティ
コンピューテッドプロパティは、プロパティ自身では値を保存せず、すでに存在しているストアドプロパティなどから計算して値を返すプロパティです。このプロパティはアクセスするたびに計算し直すため、計算元の値との整合性が常に保たれているという性質があります。
このプロパティの定義には、varに続けてプロパティと型名を指定し、{}内にgetキーワードでゲッタを、setキーワードでセッタを指定します。ゲッタはプロパティの値を返す処理、セッタはプロパティの値を更新する処理です。var プロパティ名: 型名 { get { return文によって値を返す処理 } set { 値を更新処理 プロパティに代入された値には定数newValueとしてアクセスできる } }文量が多いですが上記のコードで一つのプロパティということになります。
それでは、以下でゲッタとセッタについて解説していきます。ゲッタ
ゲッタは、他のストアドプロパティなどから値を取得して、コンピューテッドプロパティの値として返す処理です。値の返却にはreturn文を使用します。
下記のコードを見るとストアドプロパティの初期値をコンピューテッドプロパティがgetで参照し、自らの値として結果に返しています。
これがゲッタの使用方法です。struct Greeting { //ストアドプロパティ var to = "Haru Suzumiya" //コンピューテッドプロパティ var body: String { get { return "Hello, \(to)!" } } } //インスタンス化 let greeting = Greeting() //実行 greeting.body //結果 Hello, Haru Suzumiya!セッタ
セッタは、プロパティに代入された値を使用して、他のストアドプロパティなどを更新する処理です。
コンピューテッドプロパティに代入された値は暗黙的に宣言されたnewValueという定数を通じてセッタ内で扱われ、結果的にセッタで返された値が対象のストアドプロパティに代入されます。下記のコードを見ると、最初の実行結果は初期値のまま実行しています。そのため、摂氏温度はそのまま出力され、華氏温度はgetで計算され、その返り値が出力されています。
次の実行結果はストアドプロパティを変更しているため、摂氏温度は変更された値が出力され、華氏温度は変更された値をgetで再計算された値が返り値として出力されています。
最後の実行結果はコンピューテッドプロパティが代入されたため、setでその値をnewValueとして計算し、摂氏温度に代入しています。そしてその値が摂氏温度として出力され、その値を再度getで計算して華氏温度として返り値を返しています。その結果最初にコンピューテッドプロパティに代入された値と、再計算して返した値が等しくなり整合性が取れる結果となりました。
stuct Temperature { //摂氏温度 var celsius: Double = 0.0 //華氏温度 var fahrenheit: Double { get { return (0.9 / 5.0) * celsius + 32.0 } set { celsius = (5.0 / 9.0) * (newValue - 32.0) } } } //初期値で出力 var temperature = Temperature() temperature.celsius //0 temperature.fahrenheit //32 //ストアドプロパティを更新して、出力 temperature.celsius = 20 temperature.celsius //20 temperature.fahrenheit //68 //コンピューテッドプロパティを更新して出力 temperature.fahreheit = 32 temperature.celsius //0 temperature.fahrenheit //32セッタの省略
コンピューテッドプロパティを使用する上でゲッタの定義は必須ですが、セッタの定義は任意です。そのため、セッタが存在しない場合は、getキーワードと{}を省略してゲッタが記述可能になります。struct Greeting { var to = "Taro Yamada" var body: String { return "Hello, \(to)!" } }上記のように直接ストアドプロパティを参照して、値を返すことでゲッタの役割を果たします。
最後に
今回はプロパティについてざっとまとめてみました。
自己学習用なのでみにくい点あると思うので、その点ご了承ください!
最後まで読んでくれた方ありがとうございました!!!!!!!!!!!
- 投稿日:2019-09-09T19:46:13+09:00
RxとMVVMの勉強ガイド(自分がやってきたこと)
この記事はRxをちょっと手を出したけどどう勉強すればいいかわからない人が、なんとなくコードを書けるようになるための記事です
やってるところでは当たり前になってるRxですが、まだまだ敷居が高くて手が出せないという方も多いのでは無いでしょうか?
そんな方たちに向けて、自分はこんなふうに勉強してどうにかコード書けるようになったよ!というのをまとめたいと思います
必ずしも上から順番にしっかり理解しなければいけないわけではないので、よくわからないものがあっても、とりあえず次に進んでください
記事中に簡単に説明を書きますが、だいぶ自分が直感的に理解したもので間違ってる(正しくない)可能性があります
参考記事を載せるので、正確な説明はそちらを見てくださいObservable(とその派生)
Rxを使っていると色々なプロパティと出会います。それぞれの性質や用途を把握しましょう。
Observable
すべての基本。こいつを購読(subscribe)するとonNext、onError、onCompletedの3つのイベントが流れてくる。これを使ってもいいが、用途別にいろいろな派生が存在する。他の派生が使えないときに使うイメージ。
参考:
https://qiita.com/k5n/items/17f845a75cce6b737d1e#observable
https://qiita.com/_ha1f/items/e16ddc6017c4ad807c3c#observable%E5%85%A5%E9%96%80(Publish|Behaviour)Subject
外から値を詰め込んでイベントを流すことができるObservable。元のObservableは生成時に指定した値でしかイベントが発生しない。
PublishとBehaviourの違いはキャッシュを持つかどうか。BehaviourSubjectは最後に流された値をキャッシュとして持っていて、購読した際にその値がすぐに流れてくる。PublishSubjectは購読後に発生したイベントのみ流れてくる。
任意のタイミングで変更される値をイベントとして扱いたいときに使うイメージ。ViewModelのプロパティはこれかRelayかDriverが多い気がする。
Subjectには他にも種類があるが自分は使わないので良く知らない。。。参考:
https://qiita.com/k5n/items/17f845a75cce6b737d1e#publishsubject
https://qiita.com/k5n/items/17f845a75cce6b737d1e#behaviorsubject
https://qiita.com/yyokii/items/81dc182dc4a6f1b9fd1f#subject(Publish|Behaviour)Relay
onNextのみ発生する(onErrorとonCompletedが発生しない)Subject。ある条件下で常に生き続けるストリームを作るために使える。
PublishとBehaviourの違いはSubjectと同じくキャッシュを持つかどうか。購読時の動作はSubjectと同じ。BehaviourRelayはvalueというプロパティで現在のキャッシュ値を参照できる。
ViewやViewModelのプロパティとしてよく使う。参考:
https://qiita.com/yyokii/items/81dc182dc4a6f1b9fd1f#relay
https://qiita.com/morishin/items/fbbb71d6b49f89d33b98#relay-%E3%81%A8%E3%81%AF
https://egg-is-world.com/2018/08/11/rxswift-behaviorrelay-publishrelay/
https://tech.mercari.com/entry/2017/12/04/103247Driver
メインスレッドで通知され、Hot変換され、onErrorを通知しないObservable。いきなりHotとか出てきたけど、UI部品の更新で使うとおぼえておけばOK。
ViewModelでViewの状態を表すプロパティとして使われる。それ以外にも上の性質を満たしたいときには使える。参考:
https://qiita.com/k5n/items/44ef2ab400f47fb66731
https://qiita.com/summer-hues/items/e8786cd75c292cdc3ec0
https://qiita.com/inamiy/items/d6fa90d0401fa0e83852Single
一度のみ値(onSuccess)かエラー(onError)が流れるObservable。
API通信など、1回で完結する処理に使える。参考:
https://qiita.com/yyokii/items/81dc182dc4a6f1b9fd1f#single
https://qiita.com/monoqlo/items/7bcec98432389b3b8909
https://qiita.com/shoheiyokoyama/items/40b9a2e9f8ae747c477a#singleCompletable
onCompletedかonErrorのみが流れるObservable。
返り値を使わないAPIなど、完了したことだけが知りたい場合に使える。参考:
https://qiita.com/yyokii/items/81dc182dc4a6f1b9fd1f#completable
https://qiita.com/monoqlo/items/7bcec98432389b3b8909
https://qiita.com/shoheiyokoyama/items/40b9a2e9f8ae747c477a#completableObservableの購読/購読解除
Observableを購読することで、イベントが流れるようになります。購読時にイベントが流れてきたときの処理を書きます。
基本的にはObservableに対してsubscribe()
を使います。派生によって流れてくるイベントが違うので、それに合わせてイベントが流れてきたときの処理を記述します。
bind(to:)
を使うと、あるObservableを別のObservableに接続させることができます。ViewControllerのサブビューで発生したイベントを、ViewControllerを通してViewModelに通知したい場合などに使用できます。
購読したいのがDriverの場合は、drive()
を使用します。
ずっと購読しているとメモリリークしてしまうので、必要がなくなったら購読を解除します。
それぞれのObservableに対してdispose()
を呼ぶことで購読を解除できます。
ただ、これは非常に面倒なので、DisposeBagを使用してまとめて購読を解除することもできます。画面が消えるときにその画面で購読したものを一斉に解除する、といった使い方をします。参考:
https://qiita.com/_ha1f/items/e16ddc6017c4ad807c3c#subscribe%E8%B3%BC%E8%AA%AD
https://qiita.com/_ha1f/items/e16ddc6017c4ad807c3c#dispose%E8%B3%BC%E8%AA%AD%E8%A7%A3%E9%99%A4
https://qiita.com/k5n/items/17f845a75cce6b737d1e#observableObservableの生成
様々な生成関数があります。
と言っても個人的にはcreate()
くらいしか使わないですが、知っておくといざというときにきっと役に立ちます。参考:
https://qiita.com/_ha1f/items/43b28792d27dbee7133d
https://qiita.com/moaible/items/de94c574b25ea4f0ef17イベントの加工と合成
イベントで流れていた値を加工して使いたい場合があります。例えばViewからViewModelに対してIndexPathが流れてきて、それを元にデータを取得する場合など。
その際に使用できるオペレータと呼ばれるものがたくさん用意されています。(オペレータ自体はとても広い概念で、例えばObservableの生成関数もオペレータの一種です)
加工するものとしてはfilter、map、flatMap、skipなどがあります。
また合成ではmerge、zip、concat、combineLatestなどがあります。
とにかくたくさんありますが、それぞれどのような動きをするのかを把握しましょう。参考:
https://qiita.com/_ha1f/items/db72471d0c9e82fab13d#_reference-dfb07cedc63c1f933f92
https://qiita.com/k5n/items/17f845a75cce6b737d1e#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E5%8A%A0%E5%B7%A5
https://qiita.com/k5n/items/17f845a75cce6b737d1e#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E5%90%88%E6%88%90
https://qiita.com/atizawa/items/92dfa5f49546b4711957
https://qiita.com/k5n/items/e80ab6bff4bbb170122dHotとCold
Rxをやっているとそのうちこの話題にたどり着きます。
ColdなObservableはsubscribeされて初めてイベントが流れ出し、subscribeされる度に別々のストリームが生成されます。
対してHotなObservableはsubscribeされてなくてもイベントが流れ、一つのストリームを複数のObserverがsubscribeできます。
最初はなんのこっちゃですが、ここを理解してないと効率の悪いコードを書いてしまったり、期待する結果にならなかったりするので、うまくいかないときは戻ってきて確認しましょう。参考:
https://qiita.com/morishin/items/99c0493de58079b722fe#cold--hot-
https://qiita.com/_ha1f/items/2d0fc50505ce3a1fcdab
https://qiita.com/toRisouP/items/f6088963037bfda658d3
https://qiita.com/kazu0620/items/bde4a65e82a10bd33f88サンプルを読む
ここまで来ればRxについてはなんとなく読めるはずなので、サンプルを読み漁って実際にどんな使われたかをしているか見てみましょう。
まずはRxSwiftについてるサンプルを読んでみるのが良いと思います。解説記事もあるので読みやすいかと思います。参考:
https://qiita.com/kzykbys/items/3bbb4fa24d30917afd05
https://qiita.com/yuzushioh/items/634a96c6fd69de4acdd0
https://qiita.com/fumiyasac@github/items/90d1ebaa0cd8c4558d96
https://qiita.com/kumapo/items/6207d74be19cedcf4b72
などなど便利なライブラリなど
RxViewController
UIViewControllerにrxを生やして、ライフサイクルのイベントをRx風に扱えるようにしてくれます。
NSObject-Rx
NSObjectのextensionでDisposeBagを追加することで、各クラスで毎回DisposeBagを書かなくて済むようにしてくれます。
Unio
MVVMのためのフレームワークです。MVの入出力を非常にわかりやすくしてくれますし、フレームワークを導入することで、個人による記述のブレも小さくすることができます。
おわりに
Rxの記事でコードが少しも出てこないというのはなかなか挑戦的だと思いましたが、勉強トピックのカタログというイメージで書きました
だいぶ粗い内容だし、リンクだらけですが、参考になれば幸いです
- 投稿日:2019-09-09T18:48:44+09:00
だらだら転職日記 (1) SwiftとDartは似ているか?
転職のための勉強日記です. 世の中を簡単にをモットーに頑張っていきたいと思います.概要
FlutterとかSwiftUIとか楽しそうなフレームワーク1が出てきました. SwiftUIをとりあえず勉強しようと思ったのですが現状ベータでプレビューが上手く表示されない2などの不具合に見舞われたので正式リリースまでは言語の方を勉強しようと思います. Swiftは出始めに少し触ったことがあるのですが, Dartは初めてです. そのためSwiftを基本としつつ, 対応するような機能がDartにあるのかという形式で書きたいと思います.3 両者が似ていれば勉強も楽というものです.
バージョン
以下のバージョンでの比較です.
Swift Dart 5.0.1 2.4.1 比較
機能 Swift Dart 型推論 ✅ ✅ 定数 let const/final クラス ✅ ✅ 継承 ✅ ✅ 継承 ✅ ✅ 列挙型 ✅ ✅ 無名関数 ✅ ✅ ジェネリックス型 ✅ ✅ ジェネリックス型関数 ✅ ✅ ジェネリックス型制約 ✅ ✅ プロトコル/インタフェース ✅ ✅ プロトコル拡張/ミックスイン ✅ ✅ オプション型 ✅ ❌ 構造体 ✅ ❌ 多重継承 ❌ ✅ 不透明型 ❌ ✅ 変数
SwiftもDartも静的型付言語ですが基本的型推論が効きます.
var str = "I'm Swift!"var str = "I'm Dart!";定数
定数はletとconstの違いがあります.
let name = "BrainVader"const name = "BrainVader";変数補完
文字列中に変数を埋め込める地味に助かる構文です. Rustには無かったりして辛いですが, SwiftとDartにはあります.
let name = "BrainVader" let greeting = "Hello \(name)!"const name = "BrainVader"; final greeting = "Hello ${name}!";型注釈と明示型
基本的にどちらの言語も型推論で済ますことができると思うのですが, 型を明示することもできます.
let name: String = "BrainVader"const string NAME = "BrainVader";for
基本的な制御構文は変わりませんが, Swiftには単純なforループはありません.
var count = 0 for _ in 0..<100 { count += 1; }var count = 0; for(var i = 0; i < 100; i++) { count++; } print("count ${count}");let GAFA = ["Google", "Amazon", "Facebook", "Apple"] print("GAFA stands for the following companies...") for name in names { print("\(name)!") }const GAFA = [0, 1, 2]; for (var x in collection) { print(x); // 0 1 2 }while
var count = 0 let end = 10 while !(count == end) { count += 1; }var count = 0; const END= 10; while (count != END) { count++; } print('Total ${count}');if
ifはほぼ同じです.
let LANGUAGE = "Swift" if LANGUAGE == "Dart" { print("Dart"); } else { print("Swift"); }const LANGUAGE = "Dart"; if ( LANGUAGE == "Dart") { print("Dart"); } else { print("Swift"); }switch文
基本的なswitch文も小さな違いがあるだけです.
let language = "Swift" switch language { case "Swift": print("This is Swift") default: print("This is other language") }const language = "Dart"; switch (language) { case "Swift": print("This is Swift"); break; default: print("This is other language"); break; }関数
関数は返り値を後ろに置くか前に置くかが違います.
func greet(name: String) -> String { return "Hello, \(name)!" } let message = greet(name: "Dart")String greet(String name) { return "Hello, ${name}!"; } final message = greet("Swift"); print(message);Swiftでは呼び出し時に引数ラベル4を指定するのがデフォルトですが, 名前付きパラメータで同じことができます.
String greet({String name}) { return "Hello, ${name}!"; } var message = greet(name: "Swift"); print(message);Swiftでも引数ラベルを省略したい場合は以下のようになります.
func greet(_ name: String) -> String { return "Hello, \(name)!" }クロージャ/無名関数
厳密には違うものとは思いますが使われ方はほぼ同じと思います.
let nums = [1, 2, 3, 4]; let multiplyBy2 = {(num) -> Int in num * 2} let result = nums.map(multiplyBy2); print(nums); print(result);const nums = [1, 2, 3, 4]; var multiplyBy2 = (num) => num * 2; var result = nums.map(multiplyBy2).toList(); print(nums); print(result);この比較だとSwiftの記法が少し不自然に見えます.5 しかしこれは末尾クロージャという記法を使うとより自然に見えます.
let nums = [1, 2, 3, 4] let result = nums .filter {(num: Int) -> Bool in num % 2 == 0 } .map {(num: Int) -> Int in num * 2} print(nums); print(result);クロージャでも型推論が効きますので型は省略できます. 更にコマンドライン引数のようにドルマーク(?)と番号でアクセスできます. ここまでやるとSwiftの方が読みやすい気もします.
let nums = [1, 2, 3, 4] let result = nums.filter {$0 % 2 == 0}.map {$0 * 2} print(nums); print(result);列挙型
列挙型も基本は同じです.
enum Language { case Swift case Dart case Python case Rust } let myLanguage = Language.Swift let yourLanguage = {print($0)} switch myLanguage { case .Swift: yourLanguage("Swift") case .Dart: yourLanguage("Dart") case .Python: yourLanguage("Python") case .Rust: yourLanguage("Rust") default: print("No Language") }// Define in the global scope enum Language { Swift, Dart, Python, Rust } const myLanguage = Language.Dart; final yourLanguage = (language) => {print(language)}; switch (myLanguage) { case Language.Swift: yourLanguage("Swift"); break; case Language.Dart: yourLanguage("Dart"); break; case Language.Python: yourLanguage("Python"); break; case Language.Rust: yourLanguage("Rust"); break; default: print("No Language"); break; }Swiftの列挙型には関連値(associated value)が指定できます.
enum MouseState { case Start(Int, Int) case Move(Int, Int, Int, Int) case End(Int, Int) } let dragging = MouseState.Move(0, 0, 100, 100) switch dragging { case MouseState.Start(let x, let y): print("Start at (\(x), \(y))") case MouseState.Move(let x_0, let y_0, let x, let y): print("Dragging from (\(x_0), \(y_0)) to (\(x), \(y))") case MouseState.End(let x, let y): print("End at (\(x), \(y))") }構造体
Dartには無いようですがSwiftでは構造体の利用が基本なようなので書いておきます.
As a general guideline, prefer structures because they’re easier to reason about, and use classes when they’re appropriate or necessary.5
C言語の構造体とは違いメソッドを持てます.
struct Neko { let age: Int; let kind: String; let sex: String; func say() -> String { return "nyao!" } } func say(neko: Neko) -> () { print(neko.say()); } let neko = Neko(age: 5, kind: "Calico", sex: "Male"); say(neko: neko);クラス
クラスは他の言語と書き方は同じです. クラス・メソッド, インスタンス・メソッド, コンストラクタなどが定義できます.
class Neko { let age: Int; let kind: String; let sex: String; static func clone(from neko: Neko) -> Neko { return Neko(age: neko.age, kind: neko.kind, sex: neko.sex) } static func info(about neko: Neko) -> String { return "age: \(neko.age), kind: \(neko.kind), sex: \(neko.sex)"; } init(age: Int, kind: String, sex: String) { self.age = age self.kind = kind self.sex = sex } func getOld() -> Neko { return Neko(age: self.age + 1, kind: self.kind, sex: self.sex) } }class Neko { final num age; final String kind; final String sex; static Neko clone({Neko neko}) { return Neko(neko.age, neko.kind, neko.sex); } static String info({Neko neko}) { return "age: ${neko.age}, kind: ${neko.kind}, sex: ${neko.sex}"; } Neko(this.age, this.kind, this.sex); }プロトコル/インターフェース/Mixins
いわゆる実装を持たない振る舞いだけを定義したもののことです.
protocol Animal { var age: Int { get }; var kind: String { get }; var sex: String { get }; func getOld() -> Animal; } protocol Clone { func clone() -> Any; } protocol Debug { func info() -> String; } class Neko: Animal, Clone, Debug { // define class body here! }Dartで静的メソッドを継承させることができないのとmixinを使った方が良いのではということでmixinにしてみた. こちらの方がインターフェースに近い気がする.6
mixin Animal { num age; String kind; String sex; Animal getOld(); } mixin Clone<T> { T clone(); } mixin Debug<T> { String info(); } class Neko with Animal, Clone, Debug { // define class body here! }結論
SwiftとDartは似ている7注釈
- 投稿日:2019-09-09T11:46:39+09:00
[iOS] UIWebViewがいよいよヤバいらしい
2019年9月初めぐらいから、TestFlightにビルドをアップロードした後、以下の自動メールを受信しました。
Dear Developer,
We identified one or more issues with a recent delivery for your app, "XXXXX" X.X.X (XXXX). Your delivery was successful, but you may wish to correct the following issues in your next delivery:
ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs . See https://developer.apple.com/documentation/uikit/uiwebview for more information.
After you've corrected the issues, you can use Xcode or Application Loader to upload a new binary to App Store Connect.
Best regards,
The App Store Team
Apple will stop accepting submissions of apps that use UIWebView APIs .
「AppleはUIWebView APIを使用するアプリの提出を受け付けなくなります。」いよいよ、UIWebViewを使っているとリジェクトされる日は間近なようです。
2019/9/9 追記:
「デッドラインがハッキリしない」というやりとり
https://forums.developer.apple.com/thread/122114「現時点での早急な対応は不要」とのご見解
https://qiita.com/YutoMizutani/items/2b5185c84c31edfdc2d4
- 投稿日:2019-09-09T11:46:39+09:00
[iOS] UIWebViewがいよいよヤバいらしい("ITMS-90809: Deprecated API Usage"メールが届いた件)
2019年9月初めぐらいから、TestFlightにビルドをアップロードした後、以下の自動メールを受信しました。
Dear Developer,
We identified one or more issues with a recent delivery for your app, "XXXXX" X.X.X (XXXX). Your delivery was successful, but you may wish to correct the following issues in your next delivery:
ITMS-90809: Deprecated API Usage - Apple will stop accepting submissions of apps that use UIWebView APIs . See https://developer.apple.com/documentation/uikit/uiwebview for more information.
After you've corrected the issues, you can use Xcode or Application Loader to upload a new binary to App Store Connect.
Best regards,
The App Store Team
Apple will stop accepting submissions of apps that use UIWebView APIs .
「AppleはUIWebView APIを使用するアプリの提出を受け付けなくなります。」いよいよ、UIWebViewを使っているとリジェクトされる日は間近なようです。
2019/9/9 追記:
「デッドラインがハッキリしない」というやりとり
https://forums.developer.apple.com/thread/122114「現時点での早急な対応は不要」とのご見解
https://qiita.com/YutoMizutani/items/2b5185c84c31edfdc2d42019/9/10 追記:
Ionicチームのブログ
https://ionicframework.com/blog/understanding-itms-90809-uiwebview-api-deprecation/以下、上記のブログより抜粋しGoogle翻訳したもの
- (iOSアプリをリリースできなくなりますか?)現在は、いいえ。これは単なる警告であり、エラーではないためです。警告によってApp Storeが拒否されることはありません。
- Appleに、UIWebView APIを含むアプリの提出の受け付けを停止する正式な日付をAppleに求めましたが、まだ返事はありません。 詳細がわかり次第、このブログを更新します。
- 投稿日:2019-09-09T09:44:23+09:00
【iOSDC Japan 2019 参加報告ブログ】ありがとう、ありがとう、ありがとう
iOSDC Japan 2019に参加してきました。
今年はコアスタッフ、そしてスピーカーとして参加し
色々と感じたことや思っていたことなどを書きたいと思います。コアスタッフとして
エコバッグ作成
今年はエコバックの作成を担当してました。
とは言ってもデザイナーの方はいらっしゃるので
僕は素材や色などを決めるだけなのですが。ただ、エコバッグは制作などに時間がかかるため
結構早い段階で発注をしなければいけず
結構ギリギリの時期に担当になったので
ちょっと焦りましたw今年のバッグはやはりダークモード取り入れられたことが一番印象深かったです。
バックの素材や種類を決めた時に
黒と白の2色を選べることができ
そこからデザイナーさんにデザインの候補を出してもらいました。その際に出てきたのが
黒地のバッグに青いデザイン
後々のSlackで
僕の心はずっとダークモードでした
とやや中二病的な発言をしてしまうほど気に入ってしまい
スタッフの皆様のご意見を伺いつつ
ダークモードの採用が決まりました。ここから今年のiOSDCはダークモード対応しようという機運が高まり
色々なものにダークモードが採用されるようになりました。ただ本当にダークモード気に入ってもらえるのかなという不安は
ずっと抱いていましたが
当日、ダークモードばかりなくなっていると裏で聞き「よしっ」
と内心思ってましたw
※
数に限りがあったのでもらえなかった方は本当にごめんなさい。
そして受付の方には「ダークモードがもうないぞ」という騒動を起こしてしまい本当申し訳なく思っています。個人の感想としては
皆様に気に入っていただけのではないかと自負しており
やって良かったなーという印象です。ドーナツ決死隊
朝に出すドーナツを用意する係です。
昨年は急遽ドーナツ購入が決まったのですが
好評だったので今年は事前に導入することが決まりました。「決死隊」ってなんだ?
と思われるかもしれませんが
これは時間の問題でiOSDCは9:30開場に対して
ミスドの開店時間は9:00なので
9:00に馬場のミスドに商品を取りにいき
そこから開場に向かって15分くらいで用意を完了させる。というミッションがありました。
なんとか間に合うように
取りに行く前にドーナツ以外の用意を済ませ
有志のスタッフの方と現地で合流して一緒に荷物を運んで頂き(300個/日)
なんとか間に合ったかな(?)と思っています。運用に関しては色々と課題がありましたが
当初の予想を裏切るスピードで売り切れたので
良かったなーと思っています。ただ、
朝ごはんだったので惣菜系のドーナツも入れていたのですが
その売れ行きがいまいちだったことはちょっと意外でした。何人かの方にご意見お伺いしましたが
ドーナツは甘いもの
のようですね。※
気がつかなかったという意見もあったので
それは私の配慮不足なだけかもしれません。
なんかご意見あったら欲しいです??ルーキーズLT司会
これは自分でやりたいと言ってやりました。
実は私自身が去年登壇者として参加して
それが私のiOSDCの原点だったので
個人的に思い入れのある企画でした。ルーキーズLT練習会に参加して
LTをやる仲間ができ
本番はすごい緊張しましたが
多くの方から感想を頂き
すごい嬉しかったのが今でも思い出されます。とは言っても実際大したことはしていないんですがw
当日の感想としては
これはルーキーではないw
です。
「iOSDCで初めて登壇する」なので
登壇自体は経験のある方もいらっしゃったのかもしれませんが
聞きやすく、スライドも見やすく、本当すごいなーって思いました。特に、LT経験者としてタイムマネジメントをしっかりされているのが印象深かったです。
やったことがある方ならわかると思いますが
5分で思っている以上に短いですよね。内容を考えるときにも
「何を話そうか」
よりも
「何を話さないか」
を考える時間のほうが多いくらいで
時間管理にとても苦労します。そんな中
僕の記憶では途中で終わる人もおらず
皆様きっちりまとめられていて事前準備しっかりしているなーと感じました。スピーカーとして
今回はレギュラートークに選んで頂き
30分枠で参加させて頂きました。内容に関してはスライドや補足資料をご参照頂けましたら幸いです。
スライド
https://speakerdeck.com/shiz/swift-clean-code補足資料
https://qiita.com/shiz/items/849e483338bd568cb6ab
https://qiita.com/shiz/items/5755a35887bcb7897464
https://qiita.com/shiz/items/7b3bda7c2d84c5a83c0f
https://qiita.com/shiz/items/79c7b39f94f32e548df3
https://qiita.com/shiz/items/da71d547b59c757cca94まず
お聞きに来て頂きました皆様
本当にありがとうございました!!直前にBitriseさんのランチセッションがあったので
なんか人は多いなーという印象だったのですが
最終的にこんな感じになるとは。。。本番は本当にドキドキしていましたが
やるだけのことはやったかなと思っていたので
「あとはどうにでもなれ!」ぐらいの感覚で話すことができたので
そこは良かったのかなーと思っています。※
「甘い!」とかありましたら叱ってくださいm(_ _)mただ、心残りなのがQ&Aです。
発表が終わったと同時に
やり切った感が出てしまい頭が真っ白になっていましたw質問していただいた方の質問を復唱することはしっかりやろうと思ったまでは良いのですが
そのあとは何も出てこず。。。そんな中、皆様温かい目で見守っていただいて
皆様の優しさにちょっと感動してしまいました。
(質問に答えろよ、とそのあと自分にすぐツッコミを入れましたが)とにかく
本当にありがとうございました!もしよろしければ、フィードバックで感想などを頂けますと嬉しいです!
また、何か質問などあれば、QiitaでもTwitterでもなんでも良いので聞いてください!
わかりづらかった点や間違っている箇所などのご指摘もぜひ教えてください??♂️最後に
もう完全に自己満なのですが
今回満員で部屋に入れなかった人用に「大入シール」というものがあり
自分の発表が終わったあとにこっそり拝借してこんな写真撮っちゃいましたw最後に
色々と書きましたが、
やっぱり参加して良かったという印象しかありません。こんな年齢になっても
子供の頃のように純粋に楽しめる機会はあまりないと思いますし
毎年一緒に楽しめる仲間が増えていくことはとても嬉しいです。参加していただいた皆様、本当にありがとうございました!
スタッフとして体力的にしんどい時間帯もありましたが皆様の笑顔がパワーになりました。トークを聞きに来てくださった皆様、最後まで温かく見守って頂きありがとうございました!
そして私に関わってくださった皆様、楽しい時間をありがとうございました!
見かけた際はまたお話しましょう!まだお話したことがない皆様、
勉強会などで今年の名札はずっと下げていると思うので
「なんかあのアイコン見かけたことがある」とか思ったらぜひ気軽に声をおかけください?そしてスタッフの皆様、
僕はなんかうろちょろしていただけで何の役に立っていたかはあまりわかっていませんがw
一緒にこの素敵なイベントを作っていくことができて幸せでした。
ただただ、ありがとうございました!これで僕のiOSDC Japan 2019を一旦終了とさせて頂きます!
- 投稿日:2019-09-09T09:19:31+09:00
Macアプリ初心者:Realmの導入(CocoaPods利用)
今回はMacアプリでDBを使いたくてRealmについて調べてみました。
以前に作成したプロジェクトに組み込んでみます。完成イメージ
環境
- macOS Mojave:10.14.6
- Xcode:10.3
- CocoaPods:1.7.5
Realm インストール
Podfile の作成
$ pod init $ ls NSTableViewSample NSTableViewSample.xcodeproj PodfileRealm インストール
Podfile に
pod 'RealmSwift'
を追加$ vi Podfile
# Uncomment the next line to define a global platform for your project # platform :osx, '10.14' target 'NSTableViewSample' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for NSTableViewSample pod 'RealmSwift' endRealm インストール
$ pod install Analyzing dependencies Downloading dependencies Installing Realm (3.17.3) Installing RealmSwift (3.17.3) Generating Pods project Integrating client project Sending stats Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed. [!] Automatically assigning platform `osx` with version `10.14` on target `NSTableViewSample` because no platform was specified. Please specify a platform for this target in your Podfile. See `https://guides.cocoapods.org/syntax/podfile.html#platform`.Realm を使う
pod install
で作成された workspace を開く$ open NSTableViewSample.xcworkspace株式情報保存用のクラス(StockInfo)を作成
Object クラスを継承して作成します。
StockInfo.swiftimport Cocoa import RealmSwift class StockInfo: Object { @objc dynamic var name = "" @objc dynamic var price = -1 @objc dynamic var status = StockStatus.flat.rawValue @objc dynamic var createdAt = Date() } extension StockInfo { /// 状態をEnum形式で返却 var stockStatus: StockStatus { get { return StockStatus(rawValue: self.status) ?? StockStatus.flat } set { self.status = newValue.rawValue } } /// StockInfo オブジェクトを作成 class func create(realm: Realm, name: String, price: Int, status: StockStatus) -> StockInfo { let stockInfo = realm.create(StockInfo.self) stockInfo.name = name stockInfo.price = price stockInfo.stockStatus = status return stockInfo } /// 企業名で部分一致検索(空の場合は全件取得) class func objects(searchValue: String = "") -> Results<StockInfo> { var result = try! Realm().objects(StockInfo.self) let searchValue = searchValue.trimmingCharacters(in: .whitespaces) if searchValue != "" { result = result.filter("name CONTAINS %@", searchValue) } return result.sorted(byKeyPath: "createdAt") } /// デバッグ用 class func debugInitData() { let realm = try! Realm() try! realm.write { // データクリア realm.deleteAll() // サンプルデータ作成 let _ = StockInfo.create(realm: realm, name: "○○株式会社", price: 1000, status: .up) let _ = StockInfo.create(realm: realm, name: "○×水産", price: 12345, status: .down) let _ = StockInfo.create(realm: realm, name: "株式会社□○", price: 345, status: .flat) let _ = StockInfo.create(realm: realm, name: "×△ホールディングス", price: 321, status: .up) let _ = StockInfo.create(realm: realm, name: "ABC BANK", price: 20, status: .down) let _ = StockInfo.create(realm: realm, name: "▼○重工", price: 98000, status: .up) // サンプル用に1,000 件データを作成 for i in 1...1_000 { let status = StockStatus.init(rawValue: Int.random(in: 0..<3))! let price = Int.random(in: -1..<10_000_000) let _ = StockInfo.create(realm: realm, name: "○△×株式会社 \(i)", price: price, status: status) } } } }株価情報の状態を表すEnum(StockStatus)を作成
StockStatus.swiftimport Cocoa enum StockStatus: Int { case flat = 0 case up case down /// 状態ごとのImageオブジェクト var image: NSImage { get { switch self { case .flat: return NSImage(named: NSImage.Name("flat"))! case .up: return NSImage(named: NSImage.Name("up"))! case .down: return NSImage(named: NSImage.Name("down"))! } } } /// 状態ごとのカラーオブジェクト var color: NSColor { get { switch self { case .flat: return .gray case .up: return .green case .down: return .red } } } }StockInfo クラスを利用して株価情報を表示
細かい説明は省きますが、リファクタリングして多少見やすくなっているはずです。
ViewController.swiftimport Cocoa class ViewController: NSViewController { // MARK: - IBOutlet @IBOutlet private weak var tableView: NSTableView! { didSet { self.tableView.dataSource = self self.tableView.delegate = self self.tableView.headerView = nil self.tableView.rowHeight = 88 } } @IBOutlet private weak var countTextField: NSTextField! // MARK: - private variables private var stockValues = StockInfo.objects() // MARK: - override func override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // デバッグ用にデータ作成 StockInfo.debugInitData() // 株価情報をViewに反映 self.updateStockValuesCount() } override var representedObject: Any? { didSet { // Update the view, if already loaded. } } // MARK: - private func private func updateStockValuesCount() { let formatter = NumberFormatter() formatter.numberStyle = .decimal let commaString = formatter.string(from: NSNumber(value: self.stockValues.count))! self.countTextField.stringValue = "\(commaString)件" } // MARK: - actions @IBAction func actionSearchField(_ sender: NSSearchField) { self.stockValues = StockInfo.objects(searchValue: sender.stringValue) self.tableView.reloadData() self.updateStockValuesCount() } } extension ViewController: NSTableViewDataSource, NSTableViewDelegate { // MARK: - NSTableViewDataSource func numberOfRows(in tableView: NSTableView) -> Int { return self.stockValues.count } // MARK: - NSTableViewDelegate func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "MyView"), owner: self) as! CustomNSTableCellView // Set the stringValue of the cell's text field to the nameArray value at row let stock = self.stockValues[row] result.image = stock.stockStatus.image result.name = stock.name result.color = stock.stockStatus.color result.price = stock.price // Return the result return result } }
- 投稿日:2019-09-09T03:15:44+09:00
iOSで課金のレシートをローカルで判定する方法
はじめに
最近 iOSでアプリ内課金で自動更新のサプスクリプションをの実装を行いました。その時に、Web でどうやってレシートの検証を行うかを検索しました。
• レシート検証 プログラミングガイド(PDF)
• レシート検証 プログラミングガイド(Web)
• In App Purchaseのレシートをローカルで検証できるようになった話(Qiita)
• iOSの月額課金レシート検証をサーバーサイドで行うときのTipsまとめ(Qiita)
• iOS In-App Purchase実装で必ず知っておきたい隠れた罠(Qiita)
• Apple App Store Receipt Validation with Swift and Go
• Local Receipt Validation for iOS in Swift From Start to Finish
• Validating in-app purchases in your iOS app
• レシートのverifyとSandbox
• 自動購読課金について【iOS編】いろいろありますが、ほとんどがサーバーでのレシート確認方法です。ローカルで検証する方法は本家のプログラミングガイドでもさらっと書かれているのみで、正直これだけ見て実装するのは骨が折れそうです。
今回の記事はレシートをローカルで検証する知見を紹介したいと思います。サーバーを介して検証する方法に関しての記述はしないつもりですし、うるさく言われているセキュリティの脆弱化については一部許容する方針とします。また、StoreKit の使い方やチップスについては触れません。
レシートの検証
では、レシートの検証
全体の流れ
- Appleのルート証明書(.cer)を取得する
- アプリがレシートを取得する
- アプリのレシートが Apple によって署名されているか確認する
- レシートから購入情報の列を取得する
- 購入情報の有効期限が切れていないか、キャンセルされていないか確認する
- 有効な購入情報があった場合、当該機能またはコンテンツをアンロックする
Appleのルート証明書(.cer)を取得する
以下のサイトから apple のルート証明書をダウンロードします。
https://www.apple.com/certificateauthority/と言われてもどれをダウンロードしていいかわからないので、これを使いました。
Apple Inc. Root Certificate
https://www.apple.com/appleca/AppleIncRootCertificate.cerこれをアプリに組み込みますが、リソースとして読み込む場合は、さすがにジェイルブレークなどでリソースを入れ替えた偽物をつかまされないように、ハッシュ値を確認しましょう。
$ shasum -a 256 AppleIncRootCertificate.cer b0b1730ecbc7ff4505142c49f1295e6eda6bcaed7e2c68c5be91b5a11001f024 AppleIncRootCertificate.cerこれを base64 でエンコードすると
sLFzDsvH/0UFFCxJ8Slebtpryu1+LGjFvpG1oRAB8CQ=
なので、こんな感じでAppleのルート証明書のバイナリを用意します。こんなバレバレの名前だとコードをクラックされるかもしれませんが、そこは横に置いておく事とします。var appleIncRootCertificate: Data { if let url = Bundle.main.url(forResource: "AppleIncRootCertificate", withExtension: "cer"), let data = try? Data(contentsOf: url) { // make sure the certificate is not fake one let sha256 = Data(base64Encoded: "sLFzDsvH/0UFFCxJ8Slebtpryu1+LGjFvpG1oRAB8CQ=") if data.sha256 == sha256 { return data } } fatalError("error: failed to read the certificate.") }レシートのデータは、ASN.1 のフォーマットという事ですが、多くのアプリはこれを信頼の置ける外部サーバーを経由し、 Apple に投げてそのレスポンスを処理してるようですが、ここではローカルで検証したいと思います。
ここで、アップルはコードのクラックを警戒してか、自分で
ASN1C
を使うか、openssl
を static にリンクしてなんとかしろというのですが、openssl
はともかくASN1C
から始めるにはあまりにもハードルが高いので、良さげなオープンソースのコードを探します。ASN1Decoder
https://github.com/filom/ASN1Decoder
How to use for AppStore receipt parse
と説明もありますが、レシートがAppleによって署名されているかどうかは書かれていません。import ASN1Decoder if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL, FileManager.default.fileExists(atPath: appStoreReceiptURL.path) { do { let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped) let pkcs7 = try PKCS7(data: receiptData) if let receiptInfo = pkcs7.receipt() { print(receiptInfo.originalApplicationVersion) } } catch { print(error) } }次にレシートから署名を全て取り出してその公開鍵を調べ、Apple のルート証明書の公開鍵と一つでも一致するか確認します。あとは、Bundle ID がレシート一致するかなどを調べます。
let appleX509cert = try X509Certificate(data: self.appleIncRootCertificate) guard let appleKey = appleX509cert.publicKey?.key else { fatalError("x509 public key not found.") } // check if one of these certificates is signed by apple let signedByApple: Bool = { print("certificates:") for certificate in pkcs7.certificates { if let signedKey = certificate.publicKey?.key { print(signedKey as NSData) if signedKey == appleKey { return true } } } return false }() guard signedByApple else { fatalError("the receipt is not signed by apple.") } guard let receipt = pkcs7.receipt() else { fatalError("receipt not found") } guard receipt.bundleIdentifier == Bundle.main.bundleIdentifier else { fatalError("bundle identifier do not match with certificate.") // 他必要な内容を確認そして、
ASN1Decoder
は購入情報のデコードもしてくれるので、以下のように購入情報を一つ一つ有効期限やキャンセルの有無を確認します。if let inAppPurchases = receipt.inAppPurchases { for purchase in inAppPurchases { guard let productIdentifier = purchase.productId else { continue } print("product identifier:", productIdentifier) print("purchase date:", purchase.purchaseDate ?? "n/a") print("original purchase date:", purchase.originalPurchaseDate ?? "n/a") print("cancellation date:", purchase.cancellationDate ?? "n/a") print("expires date:", purchase.expiresDate ?? "n/a") print("current date:", now) // 有効なレシートがないか確認 } }購入情報の確認方法自体は記事の対象外とさせていただきます。サーバーを介してAppleから戻ってくる JSON とは多少の違いがあるようですが、詳細は未確認です。
未解決事項
レシート検証 プログラミングガイドには、こんな記述があります。
Compute the Hash of the GUID
In macOS, use the method described in Get the GUID in macOS to fetch the computer’s GUID.
In iOS, use the value returned by the identifierForVendor property of UIDevice as the computer’s GUID.
To compute the hash, first concatenate the GUID value with the opaque value (the attribute of type 4) and the bundle identifier. Use the raw bytes from the receipt without performing any UTF-8 string interpretation or normalization. Then compute the SHA-1 hash of this concatenated series of bytes.
...snip...
5) Compute the hash of the GUID as described in Compute the Hash of the GUID.
If the result does not match the hash in the receipt, validation fails.しかし、レシート内には GUID が存在しないような気がします。
ASN1Decoder
に漏れがあるのか、こちらの見落としなのか、とにかく GUID が確認できないように思えます。環境
執筆時の環境に関しては以下の通りです。
Xcode Version 10.3 (10G8) Apple Swift version 5.0.1 (swiftlang-1001.0.82.4 clang-1001.0.46.5) Target: x86_64-apple-darwin18.7.0