- 投稿日:2020-12-06T23:34:32+09:00
【Flutter】FlutterでCupertinoThemeDataを使用したiOSテーマスタイルの作成
この記事は「HTC Advent Calendar 2020」の6日目の記事です。
今回は、FlutterのWidgetsの1つである「Cupertino widegets」について書いていきます。FlutterでiOSライクなアプリを作ろうとした際に、
CupertinoAppウィジェットのCupertinothemeDataについて調べた内容になります。Cupertino ウィジェット
Flutterといえば、マテリアルデザインを想像する方が多いと思いますが、実はCupertinoというiOS風のウィジェットも提供しています。
読み方は「クパチーノ」で、Apple本社のあるクパチーノ由来の命名のようです。Flutterにおけるテーマ
簡単に説明すると、Flutterでアプリ全体を統一感のあるデザインにできる仕組みです。
この仕組みを使うことで、デザインを一箇所で統一的に管理できます。例えば、iOSのステータスバーの色やテキストの色が気に入らなければカスタマイズできます。また、ダークモード用のプロパティも用意されており、簡単に実装できるようになっています。iOSテーマスタイルの作成
Cupertinoウィジェットを使用するために、パッケージの導入を行います。
main.dartimport 'package:flutter/cupertino.dart';
CupertinoAppウィジェットを作成し、CupertinoThemeDataでテーマの指定を行います。
CupertinoAppウィジェットは、Material ComponentのMaterialAppウィジェットの代わりになるウィジェットです。MaterialAppウィジェットとほとんど同じプロパティを持ちます。main.dartimport 'package:flutter/cupertino.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return CupertinoApp( theme: CupertinoThemeData( barBackgroundColor: CupertinoColors.lightBackgroundGray, // ・・・省略 } }上記のコードの例では、
barBackgroundColorを変更することでナビゲーションバーの背景色をライトグレーに変更しています。
CupertinoThemeDataの主なプロパティは以下に記載しておきます。
barBackgroundColor: 上部のナビゲーションバーと下部のタブバーの背景色を変更できます。
brightness: アプリの全体の明るさを変更できます。
primaryColor: アプリの基本となる色を変更できます。
textTheme: Cupertinoウィジェットで使用されるテキストのスタイルを変更できます。画面上ではこのような色になります。
他にもCupertinoIconThemeDataを使用すれば、アイコンのテーマ変更も可能となります。
- 投稿日:2020-12-06T22:55:43+09:00
WKWebViewの完了ボタンを消した話
これのこと
WebView内で入力が必要になったときに自動的についてくるらしい、タップするとキーボードを閉じてくれるが、この手のボタンはカスタムで配置するので邪魔だったので消せるようにした
やり方は以下// // WKWebView+removeDone.swift // WebViewDone import Foundation import WebKit extension WKWebView { func removeDoneButton() { guard let target = scrollView.subviews.first(where: { String(describing: type(of: $0)).hasPrefix("WKContent") }), let superclass = target.superclass else { return } let noInputAccessoryViewClassName = "\(superclass)_NoInputAccessoryView" var newClass: AnyClass? = NSClassFromString(noInputAccessoryViewClassName) if newClass == nil, let targetClass = object_getClass(target), let classNameCString = noInputAccessoryViewClassName.cString(using: .ascii) { newClass = objc_allocateClassPair(targetClass, classNameCString, 0) if let newClass = newClass { objc_registerClassPair(newClass) } } guard let noInputAccessoryClass = newClass, let originalMethod = class_getInstanceMethod(NoInputAccessoryView.self, #selector(getter: NoInputAccessoryView.inputAccessoryView)) else { return } class_addMethod(noInputAccessoryClass.self, #selector(getter: NoInputAccessoryView.inputAccessoryView), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) object_setClass(target, noInputAccessoryClass) } } fileprivate final class NoInputAccessoryView: NSObject { @objc var inputAccessoryView: AnyObject? { return nil } }WKWebViewにinputAccessoryViewプロパティがあるがread-onlyなため、自分で作った
NoInputAccessoryViewにSwizzleする感じ
あとはWebViewから呼ぶだけwebView.removeDoneButton()消えた。あとはOSのアップデート等でこの辺が変更にならないことを祈るのみ。
- 投稿日:2020-12-06T21:41:56+09:00
iOSアプリで位置情報取得からのバックグラウンド処理をしたときの話
これは2014年頃、Swiftが発表される直前の話。
バックグラウンドになってもどうにか処理をつづけられないかという依頼があって、いろいろためしてみたメモ。
知っての通りiOSにはバックグラウンドに厳しい制約があり基本無理だがいくつか方法はあった
BackgroundFetch(Background App Refresh Tasks)をつかう
iOSが任意のタイミングでアプリの処理を30秒だけ実行してくれる。
iOS7からだったので却下したVoIPアプリになる
VoIPアプリではないので却下位置情報をバックグラウンドで取得
他アプリでも採用されていた方法だったのでこれをやってみたいまはBackground ProcessingやBackgroundNotificationの選択肢もあるが当時はなかったので除外
位置情報取得
CoreLocation.frameworkの追加、CapabilityからBackground Modesを追加してLocation updatesにチェックをいれておく
当時はObjective-cで書いていたが、今Objective-cを残してもしょうがないのでSwiftで記載するvar locationManager: CLLocationManager = { var locationManager = CLLocationManager() locationManager.allowsBackgroundLocationUpdates = true locationManager.desiredAccuracy = kCLLocationAccuracyKilometer locationManager.distanceFilter = 1 return locationManager } ()
allowsBackgroundLocationUpdatesをtrueにしておくとバックグラウンドで受信できるfunc startLocation() { locationManager.startMonitoringSignificantLocationChanges() // 大幅に位置が変更した時のみ位置情報を取得する }
startMonitoringSignificantLocationChangesを使うと位置情報が大きく動いたときだけupdateされるextension AppDelegate: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let status = UIApplication.shared.applicationState if status != .background { return } NSLog("test application update location") task.execute() } }位置情報の変更を検知したらバックグラウンドタスクを実行、今回はバックグラウンドで動いていることがわかりやすいようにタスクの実行をバックグラウンドでのみ行う。
class BackgroundTask { private var loopCount = 0 private var isExecuting = false private let PrefKey = "countValue" func execute() { if isExecuting { return } DispatchQueue.global().async { self.isExecuting = true self.loopCount = UserDefaults.standard.integer(forKey: self.PrefKey) while(true) { NSLog("test application background task %d", self.loopCount) sleep(1) self.loopCount = self.loopCount + 1 UserDefaults.standard.setValue(self.loopCount, forKey: self.PrefKey) } } } }検証用のバックグラウンドタスク、ただ1秒おきにログを表示するだけ
結果
検証はシミュレータのLocationからFree Driveをつかって行った
2020-12-06 21:28:29.373625+0900 LaunchLocationInBackground[2189:105006] test application enter foreground 2020-12-06 21:28:32.634231+0900 LaunchLocationInBackground[2189:105006] test application enter background 2020-12-06 21:28:38.703424+0900 LaunchLocationInBackground[2189:105006] test application update location 2020-12-06 21:28:38.710385+0900 LaunchLocationInBackground[2189:105264] test application background task 0 2020-12-06 21:28:39.759649+0900 LaunchLocationInBackground[2189:105264] test application background task 1 2020-12-06 21:28:40.821763+0900 LaunchLocationInBackground[2189:105264] test application background task 2 2020-12-06 21:28:41.858983+0900 LaunchLocationInBackground[2189:105264] test application background task 3 2020-12-06 21:28:42.959856+0900 LaunchLocationInBackground[2189:105264] test application background task 4 2020-12-06 21:28:43.967623+0900 LaunchLocationInBackground[2189:105264] test application background task 5 2020-12-06 21:28:45.003097+0900 LaunchLocationInBackground[2189:105264] test application background task 6 2020-12-06 21:28:46.037451+0900 LaunchLocationInBackground[2189:105264] test application background task 7 2020-12-06 21:28:47.138904+0900 LaunchLocationInBackground[2189:105264] test application background task 8 2020-12-06 21:28:48.235930+0900 LaunchLocationInBackground[2189:105264] test application background task 9 2020-12-06 21:28:49.258731+0900 LaunchLocationInBackground[2189:105264] test application background task 10 2020-12-06 21:29:32.255424+0900 LaunchLocationInBackground[2189:105006] test application update location 2020-12-06 21:29:32.255519+0900 LaunchLocationInBackground[2189:105264] test application background task 11 2020-12-06 21:29:33.323138+0900 LaunchLocationInBackground[2189:105264] test application background task 12 2020-12-06 21:29:34.425526+0900 LaunchLocationInBackground[2189:105264] test application background task 13 2020-12-06 21:29:35.434688+0900 LaunchLocationInBackground[2189:105264] test application background task 14 2020-12-06 21:29:36.526842+0900 LaunchLocationInBackground[2189:105264] test application background task 15 2020-12-06 21:29:37.535671+0900 LaunchLocationInBackground[2189:105264] test application background task 16 2020-12-06 21:29:38.622164+0900 LaunchLocationInBackground[2189:105264] test application background task 17 2020-12-06 21:29:39.658490+0900 LaunchLocationInBackground[2189:105264] test application background task 18 2020-12-06 21:29:39.698484+0900 LaunchLocationInBackground[2189:105006] test application update location 2020-12-06 21:29:40.672103+0900 LaunchLocationInBackground[2189:105264] test application background task 19 2020-12-06 21:29:41.681671+0900 LaunchLocationInBackground[2189:105264] test application background task 20 2020-12-06 21:29:42.747317+0900 LaunchLocationInBackground[2189:105264] test application background task 21 2020-12-06 21:29:43.818607+0900 LaunchLocationInBackground[2189:105264] test application background task 22 2020-12-06 21:29:44.830959+0900 LaunchLocationInBackground[2189:105264] test application background task 23 2020-12-06 21:29:45.855771+0900 LaunchLocationInBackground[2189:105264] test application background task 24 2020-12-06 21:29:46.954273+0900 LaunchLocationInBackground[2189:105264] test application background task 25 2020-12-06 21:29:48.055699+0900 LaunchLocationInBackground[2189:105264] test application background task 26 2020-12-06 21:29:49.065966+0900 LaunchLocationInBackground[2189:105264] test application background task 27 2020-12-06 21:29:50.097980+0900 LaunchLocationInBackground[2189:105264] test application background task 28
application enter backgroundでバックグラウンドに突入してから、application update locationでアプリが起動し、バックグラウンドタスクが実行されている。
また、バックグラウンド生存期間が終了したあとも位置情報の更新によって続きから実行されている。
昔試したときはバックグラウンドでの実行時間は3分あったきがするのだが短くなったのだろうか
- 投稿日:2020-12-06T20:35:48+09:00
写真とUIの意外な共通点
1年ぶりです、Classi Advent Calendar 2020 7日目は、デザイナーの@shio312が毎年のいつもの記事とはちょっと違った味でお送りします。
はじめに
去年度から今年度にかけて、他部署の方に
「デザイナーの頭の中身ってどうなってるの?」とか、
「特にUIのような画面を起こす人君たち(特に私)の頭はどんな構成なんじゃ?」
とよく訊かれました。と言うわけで、UIデザイナーの頭をかち割って中身を見せることはできないので、今年は自分はこんなふうにUIに紐づけて世界を見ていますよ、という実例を紹介しようと思います。
写真で。
「エッ?関係あんの?」
と思われるかもしれませんが、趣味として嗜む方も多い写真を用いてお話しする事で、少しでも画面デザイナーに共感と親近感を覚えていただけると嬉しいなと思い、ちょっとした例えでお話ししてみようと思います。ちなみに、デザイナー皆が皆そうではないことだけは事前にご理解の上、拝読くださいね。
4枚の写真を用いた実例紹介するよ
さてここに私自身が撮影した4枚の写真があります。
----A----
----B----
----C----
----D----
母と京都の南禅寺に行った際に撮った写真たちです。いや〜良い紅葉でした?
うん、写真を通じて友人に古都自慢したいな!
よし、今から記載の写真のどれが一番"秋の古都の魅力"が伝わるか考えたいと思います。
↓
とすると、1枚だけで秋の古都の魅力を伝えるという目的を達成するための最低要件は
以下3点が最低情報として写真に収まっていると分かりやすいと考えます要件
1. 画面に収まる建築物によって(おおよそ)直感でここは古い街だと言うことが分かること
2. 画面に収まり赤や黄色の紅葉が秋だと直感で分かること
3. 感情的に秋の終わりらしい古都の趣きが写真を通じて見る人に伝わることA〜Dの写真の考察
----A----
紅葉がいっぱいある、秋だなきれいだな!けれど、場所がわからんしどこだよ此処
----B----
なんか古そうな建物もあるし紅葉もあるな、写真としても画角も良いしカッコええんじゃないか?
ただ天気良すぎて視線がそっち行くな...古都らしい雰囲気はCより下か?
----C----
Bほど写真としてのカッコよさはないけど、奥に古い建物(南禅寺の三門)があるし紅葉もあるな?
----D----
紅葉の絨毯が綺麗だけど、古木の渋さを伝えたいわけなじゃいんだ、目的がちがうぞ!となります。
BとCが目的達成に近そうですね。
ここで写真A〜Dが用件をどれだけクリアしているか星取表を作ってみましょう。
必要用件 要件1 要件2 要件3 A × ◯ × B ◯ ◯ △ C ◯ ◯ ◯ D × ◯ × はい、こうなりました。
画としてはBの方が奇をてらった面白い画ですが、要件の達成度としてはCに軍配が上がりました。マジか〜(本音)
此処で思い出してみましょう。
Bの写真ですが、「ぱっと見イケてるサイト/アプリなんだけど、あともうちょい分かりやすいUIだったらな〜ファストビュー内にフォームとボタンがあったらな〜(使い勝手の要件が満たされていない)」
と同じ状態というわけです。Cの写真は素人丸出しかもしれません。正直写真としては品質が微妙です。
けれど、自分の目的における要件を達成しているのはCということになります。このあとも納得いく写真を求めて沢山撮りました。
先ほど結論づけた結果から、目的達成のための必要条件をクリアしながらも、更に画として目に入る情報の順番が(無理やり言い換えると情報設計ですね)より分かりやすい、そして品質の高い画を作るために私は写真のファインダーを絞っていきました。
目的が達成された品質の高い写真を見た友人との楽しい会話を想像しながら。と言った感じで私はUIを「使う人がどうやって何ができるか、そして結果笑顔が生まれるか」を考えて画面に落とし込んでいっています。
こんな感じでした、ジャンジャン。
写真や近代絵画などでも「何を伝えたがっているのか」を意識して見てみると、今まで思いもしなかった視点に気がつくかもしれません。
そんなの当たり前だよ〜と思う方が多いかもしれませんが、ぜひ改めて試してみてはいかがでしょうか。
機材で表現を広げるのも手ですが、同じ機材でどこまで伝え方の幅を広げるか工夫を凝らすのも中々楽しいもんです。モノや絵画、写真、空間においても存在している、人と人とのコミュニケーションを産み、つなぐインターフェース。
そう考えると、この仕事の可能性とそれに携わることができている自分が誇らしいなあと思えます。これを読んだ方の物づくりの楽しさの視野が、ほんのちょっと広がると私は嬉しいです。
それでは、良いお年を。
- 投稿日:2020-12-06T20:35:48+09:00
写真とUIデザインの意外な共通点
1年ぶりです、Classi Advent Calendar 2020 7日目は、デザイナーの@shio312が毎年のいつもの記事とはちょっと違った味でお送りします。
はじめに
去年度から今年度にかけて、他部署の方に
「デザイナーの頭の中身ってどうなってるの?」とか、
「特にUIのような画面を起こす人君たち(特に私)の頭はどんな構成なんじゃ?」
とよく訊かれました。と言うわけで、UIデザイナーの頭をかち割って中身を見せることはできないので、今年は自分はこんなふうにUIに紐づけて世界を見ていますよ、という実例を紹介しようと思います。
写真で。
「エッ?関係あんの?」
と思われるかもしれませんが、趣味として嗜む方も多い写真を例にお話しする事で、少しでも画面デザイナーに共感と親近感を覚えていただけると嬉しいなと思い、挑戦としてお話ししてみようと思います。ちなみに、UI関連に携わるデザイナー皆が皆そうではないことだけは事前にご理解の上、拝読くださいね。
4枚の写真を用いた実例紹介するよ
さてここに私自身が撮影した4枚の写真があります。
----A----
----B----
----C----
----D----
母と京都の南禅寺に行った際に撮った写真たちです。いや〜良い紅葉でした?
うん、写真を通じて友人に古都自慢したいな!
よし、今から記載の写真のどれが一番"秋の古都の魅力"が伝わるか考えたいと思います。
↓
とすると、1枚だけで秋の古都の魅力を伝えるという目的を達成するための最低要件は
以下3点が最低情報として写真に収まっていると分かりやすいと考えます要件
1. 画面に収まる建築物によって(おおよそ)直感でここは古い街だと言うことが分かること
2. 画面に収まり赤や黄色の紅葉が秋だと直感で分かること
3. 感情的に秋の終わりらしい古都の趣きが写真を通じて見る人に伝わることA〜Dの写真の考察
----A----
紅葉がいっぱいある、秋だなきれいだな!けれど、場所がわからんしどこだよ此処
----B----
なんか古そうな建物もあるし紅葉もあるな、写真としても画角も良いしカッコええんじゃないか?
ただ天気良すぎて視線がそっち行くな...古都らしい雰囲気はCより下か?
----C----
Bほど写真としてのカッコよさはないけど、奥に古い建物(南禅寺の三門)があるし紅葉もあるな?
----D----
紅葉の絨毯が綺麗だけど、古木の渋さを伝えたいわけなじゃいんだ、目的がちがうぞ!となります。
BとCが目的達成に近そうですね。
ここで写真A〜Dが要件をどれだけクリアしているか星取表を作ってみましょう。
必要用件 要件1 要件2 要件3 A × ◯ × B ◯ ◯ △ C ◯ ◯ ◯ D × ◯ × はい、こうなりました。
画としてはBの方が奇をてらった面白い画ですが、要件の達成度としてはCに軍配が上がりました。マジか〜(本音)
此処で思い出してみましょう。
Bの写真ですが、「ぱっと見イケてるサイト/アプリなんだけど、あともうちょい分かりやすいUIだったらな〜ファストビュー内にフォームとボタンがあったらな〜(使い勝手の要件が満たされていない)」
と同じ状態というわけです。Cの写真は素人丸出しかもしれません。正直写真としては品質が微妙です。
けれど、自分の目的における要件を達成しているのはCということになります。このあとも納得いく写真を求めて沢山撮りました。
先ほど結論づけた結果から、目的達成のための必要条件をクリアしながらも、更に画として目に入る情報の順番が(無理やり言い換えると情報設計ですね)より分かりやすい、そして品質の高い画を作るために私は写真のファインダーを絞っていきました。
目的が達成された品質の高い写真を見た友人との楽しい会話を想像しながら。と言った感じで私はUIを「使う人がどうやって何ができるか、そして結果笑顔が生まれるか」を考えて画面に落とし込んでいっています。
こんな感じでした、ジャンジャン。
写真や近代絵画などでも「何を伝えたがっているのか」を意識して見てみると、今まで思いもしなかった視点に気がつくかもしれません。
そんなの当たり前だよ〜と思う方が多いかもしれませんが、ぜひ改めて試してみてはいかがでしょうか。
機材で表現を広げるのも手ですが、同じ機材でどこまで伝え方の幅を広げるか工夫を凝らすのも中々楽しいもんです。モノや絵画、写真、空間においても存在している、人と人とのコミュニケーションを産み、つなぐインターフェース。
そう考えると、この仕事の可能性とそれに携わることができている自分が誇らしいなあと思えます。これを読んだ方の物づくりの楽しさの視野が、ほんのちょっと広がると私は嬉しいです。
それでは、良いお年を。
- 投稿日:2020-12-06T16:37:37+09:00
SwiftLintで追加・廃止されたルールまとめ(Swift 5.1.2→5.3.1版)
はじめに
本記事は Swift/Kotlin愛好会 Advent Calendar 2020 の7日目の記事です。
SwiftLintで追加・変更・廃止されたルールまとめ(Swift 4.2→5.1.2版) - Qiita から2020/12/06現在の最新版までの間に、追加・廃止されたルールを解説します。
前回の記事を読んでいなくても読み進められますが、併せて読むことをオススメします。比較方法
前回の記事や公式ドキュメント、リリースノートを参考に差分を抽出しました。
変更されたルールも書こうとしたのですが、リリースノートを見ると多かったので見送りました。
ルール 数 追加 13 廃止 0 ※ソースでなくドキュメントを確認して調査したため、実際の数と異なる可能性があります。
環境
- OS:macOS Big Sur 11.0.1
- Swift:5.3.1
- Xcode:12.2 (12B45b)
- SwiftLint:0.42.0-rc.1
0.42.0は正式リリースされていない追加されたルール
Comment Spacing
- 追加バージョン:0.42.0-rc.1
- デフォルト:有効
コメントはスラッシュの後ろにスペースを1つ以上空けるべきです。
https://realm.github.io/SwiftLint/comment_spacing.html// bad //Foo ///Foo // good // Foo // Foo /// FooComputed Accessors Order
- 追加バージョン:0.40.0
- デフォルト:有効
コンピューテッドプロパティのゲッターとセッターは、統一した順序にすべきです。
https://realm.github.io/SwiftLint/computed_accessors_order.html// bad class Foo { var foo: Int { set { _foo = newValue } get { 3 } } } // good class Foo { var foo: Int { get { 3 } set { _foo = newValue } } }Inclusive Language
- 追加バージョン:0.41.0
- デフォルト:有効
識別子は人種、性別、社会経済的地位に基づく差別を避けるために、包括的な言語を使うべきです。
https://realm.github.io/SwiftLint/inclusive_language.html// bad enum BlackList { case foo case bar } func updateWhiteList(add: String) {} init(master: String, slave: String) {} // good enum DenyList { case foo case bar } func updateAllowList(add: String) {} init(leader: String, follower: String) {} enum WalletItemType { case visa case mastercard // `0.42.0-rc.1` からデフォルトで許可された }Twitterが提供している一覧が参考になります。
We’re starting with a set of words we want to move away from using in favor of more inclusive language, such as: pic.twitter.com/6SMGd9celn
— Twitter Engineering (@TwitterEng) July 2, 2020Orphaned Doc Comment
- 追加バージョン:0.38.2
- デフォルト:有効
ドキュメンテーションコメントは宣言に付けるべきです。
https://realm.github.io/SwiftLint/orphaned_doc_comment.html// bad /// My great property // Not a doc string var myGreatProperty: String! // good /// My great property var myGreatProperty: String!Enum Case Associated Values Count
- 追加バージョン:0.38.1
- デフォルト:無効
列挙型のアソシエイテッドバリューは少なくすべきです。
https://realm.github.io/SwiftLint/enum_case_associated_values_count.html// bad enum Employee { case fullTime(name: String, retirement: Date, age: Int, designation: String, contactNumber: Int) case partTime(name: String, contractEndDate: Date, age: Int, designation: String, contactNumber: Int) } // good enum Employee { case fullTime(name: String, retirement: Date, designation: String, contactNumber: Int) case partTime(name: String, age: Int, contractEndDate: Date) }IBInspectable in Extension
- 追加バージョン:0.40.0
- デフォルト:無効
エクステンションで
@IBInspectableのプロパティを追加すべきではありません。
https://realm.github.io/SwiftLint/ibinspectable_in_extension.html// bad extension Foo { @IBInspectable private var x: Int } // good class Foo { @IBInspectable private var x: Int }Indentation Width
- 追加バージョン:0.38.2
- デフォルト:無効
1つのタブまたは設定したスペースの量でコードをインデントすべきです。
https://realm.github.io/SwiftLint/indentation_width.html// bad firstLine secondLine // good firstLine secondLineNon-private XCTest member
- 追加バージョン:0.42.0-rc.1
- デフォルト:無効
テストクラスのテストでないメンバーはすべて
privateにすべきです。
https://realm.github.io/SwiftLint/non_private_xctest_member.html// bad class TotoTests: XCTestCase { var foo: Bar? func helperFunction() {} } // good class TotoTests: XCTestCase { private var foo: Bar? private func helperFunction() {} }Optional Enum Case Match
- 追加バージョン:0.38.1
- デフォルト:無効
オプショナルの列挙型は
?を付けずにマッチすべきです。
https://realm.github.io/SwiftLint/optional_enum_case_matching.html// bad switch foo { case .bar?: break case .baz: break default: break } // good switch foo { case .bar: break case .baz: break default: break }Prefer Nimble
- 追加バージョン:0.41.0
- デフォルト:無効
XCTAssertよりNimbleのマッチャーを使うべきです。
https://realm.github.io/SwiftLint/prefer_nimble.html// bad XCTAssertEqual(foo, 1) // good expect(foo) == 1Prefer Self Type Over Type of Self
- 追加バージョン:0.38.1
- デフォルト:無効
type(of: self)よりSelfを使うべきです。
https://realm.github.io/SwiftLint/prefer_self_type_over_type_of_self.html// bad class Foo { func bar() { type(of: self).baz() } } // good class Foo { func bar() { Self.baz() } }Prefer Zero Over Explicit Init
- 追加バージョン:0.40.0
- デフォルト:無効
CGPoint(x: 0, y: 0)のようなパラメータが0のイニシャライザより.zeroを使うべきです。
https://realm.github.io/SwiftLint/prefer_zero_over_explicit_init.html// bad CGPoint(x: 0.0, y: 0.0) CGRect(x: 0, y: 0, width: 0, height: 0) CGSize(width: 0, height: 0) CGVector(dx: 0, dy: 0) // good .zero .zero .zero .zeroTest case accessibility
- 追加バージョン:0.41.0
- デフォルト:無効
テストケースにはテスト以外の
privateなメンバーのみ含まれるべきです。
https://realm.github.io/SwiftLint/test_case_accessibility.html// bad class FooTests: XCTestCase { var foo: String? func foobar() {} } // good class FooTests: XCTestCase { private var foo: String? private func foobar() {} }「Non-private XCTest member」との違いがまだわかっていません。
廃止されたルール
なし
おわりに
これでSwift 5.3.1でもSwiftLintを適切に使うことができます!
以上、 Swift/Kotlin愛好会 Advent Calendar 2020 の7日目の記事でした。
明日も @uhooi の記事です。参考リンク
- 投稿日:2020-12-06T12:25:58+09:00
App Store Small Business Programを申請してみた
App Store Small Business Programとは?
年間収益が100万ドル以下のデベロッパーに対して、有料AppおよびApp内課金への手数料が、30%から15%に引き下げられるプログラムです。
Apple Developer ニュースとアップデート
App Store Small Business Programを発表
App Store Small Business Programへの登録受付開始申請手順
App Store Connectにサインインします。
「Review the updated Paid Applications Schedule.」の警告が表示されている場合は「Agreements, Tax, and Banking」をクリックして同意を行います。
これは、今までにも有料アプリの規約が変更になった際に行っていた手続きです。上の警告が表示されなくなったら「Learn More」をクリックします。
App Store Small Business Programの概要説明ページが表示されます。
スクロールしていくと以下の画面にたどり着きます。
「Enroll」をクリックします。
冒頭の警告表示に対する確認項目と思われます。
「Yes, I have accepted.」チェックボックスをオンにします。
続いて、「他の関連する開発者アカウントを保持しているか」に関する確認項目があります。
いくつかの項目があります。以下のような背景に基づく確認項目のようです。
TechCrunch Japanより引用:
12月31日以降にプログラムが開始されると、参加デベロッパーは他のアカウントとの間でアプリを移せなくなる。
これはおそらく、開発者による「このアプリは儲かりすぎたため、早く他のアカウントに切り替えてしまおう」といった行為を防ぐためだ。
「2020年12月31日以降にアプリを移そうとした場合、または2020年12月31日以降に開始されたアプリの移転を受け入れた場合、プログラムに参加する資格がなくなります」と、アップルは記述している複数の開発者アカウントを管理している場合、それらを特定するようにアップルは求めている
私の場合は個人の開発者であり他に関連するアカウントはないため、全て「No」を選択しました。
チェックボックスをオンにして「Submit」をクリックします。
以上で手続きは完了のようです。
Apple Developerから「We’ve received your request to join the App Store Small Business Program.」というタイトルのメールが飛んできます。
We’ll review your details and let you know your enrollment status by December 30, 2020.
メール本文によると、2020年12月30日に審査結果が出るようです。
ご注意:
本記事執筆時点ではまだ審査が終わっていませんので、上記が正しい手順であることを証明できません。
申請の際は自己責任でお願いします。
- 投稿日:2020-12-06T12:15:55+09:00
Xcode 12でのビルドが2021年4月から必須になる件と、Xcode 11から12への移行事例
情報
2021年4月移行、アプリ審査の提出時にはXcode 12ビルドが必須になります。
https://developer.apple.com/jp/ios/submit/
2021年4月以降、App Storeに提出するiOS AppとiPadOS Appはすべて、Xcode 12およびiOS 14 SDKでビルドしなければなりません。
事例
私が現在担当しているアプリでも移行を実施しましたので、結果を共有します。
移行作業は例年に比べると軽微でした。移行環境
- Xcode 11.3 → 12.1
アプリ概要
- ファイナンス系
- 画面数:約120
- Swiftステップ数:約85,000
- 使用Framework
- WebKit
- StoreKit
- LocalAuthentication
- PassKit
- WatchConnectivity
- UserNotifications
ライブラリ
ライブラリ管理ツール(Carthage)への影響
- CarthageビルドがXcode 12で失敗する問題があります。公式リポジトリで回避方法が公開されていますので、それを使うことにしました。
- Xcode 12でビルドするためのシェルスクリプトが公開されているので、それを使うという回避方法で、ソースの修正等は必要ありませんし、Cartfileや導入ライブラリに対しての作業は特にありませんでした。
導入済みライブラリへの影響
11.3時代のバージョンそのままでも、特に問題はありませんでした。
- Firebase Analytics - 6.32.0
- Firebase Crashlytics - 6.32.0
- Lottie - 3.1.4
- KeychainAccess - 3.2.0
- RealmSwift - 3.18.0
ソースコードへの影響
Xcode 12のサジェスチョンに従って自動FIXした箇所が数カ所ありました。
以下のような、「再代入していないvar変数をletに変更」といった軽微なもので、コンパイルはさくっと通りました。- var foo = self.bar + let foo = self.bar
- 投稿日:2020-12-06T09:26:29+09:00
[WIP][iOS]テストケース作成
異常系テスト
典型的には通信処理(API呼び出し処理)の失敗時など。
MoyaのStubProviderを使うと良い。
同値分割
同値分割とは「入力を意味のあるグループ(同値クラス)に分け、各グループから代表値を選ぶ方法」。数値の入力値のほか、その他にも応用はできる。
画像サイズ
iPhoneの画像サイズは11種類くらいあるので、使う中で最小、最大、中くらいのサイズの端末で試すなどし、レイアウト崩れがないか試す事が考えられる。
OSバージョン
対象となるバージョンで最低一個ずつ試す(iOS12, 13, 14)など。
参考
WRブログ - テストケース作成のポイント
テストケースの作成
Qangaroo - 失敗しないテストケースの作り方と、効率よくテストを進める方法
- 投稿日:2020-12-06T02:33:58+09:00
スクロールでNavigationBarとTabBarを隠す機能を自作してみる
はじめに
この記事はand factory Advent Calendar 2020 の6日目の記事です。
昨日は@myoshitaさんのNavigation Component with Kotlin DSLでした!今回は、iOSでスクロール時にNavigationBarとTabBarを隠したり表示したりする機能を自作してみましたので
実装の仕方をまとめてみようと思います。環境
開発環境は以下の通りです。
Tool Version Xcode 12.2 Swift 5.3.1
Target iOS 13 or later
Interface Storyboard (UIKit) ※ 本投稿ではSwiftUI版の内容は含んでおりませんm(_ _)m
完成イメージ
スクロール方向に応じて、Barを隠したり表示したりします
dragging scrolling 実装の方針について
スクロールに応じてNavigationBarを隠したり表示したりする機能はいくつかOSSがあったのですが、
Barを隠した分TableViewをうまくリサイズできなかったりしたので
勉強も兼ねてスクラッチで実装してみようと思い、やってみました!※ OSSに関しては、自分が実装の仕方誤っていただけかもなので、本記事では特に扱いませんm(_ _)m
実装の流れ
大まかには以下のような実装になります。
スクロール開始時にスクロール位置を保持
↓
スクロール量に応じて、NavigationBarを上に、TabBarを下にそれぞれ移動させる
↓
ScrollView(サンプルではTableView)のTopとBottomのConstraintを更新するAutoLayoutの設定
特に意識せずにTableViewのAutoLayoutを設定するとSafeAreaに対して上下の制約を設定してしまいますが
今回は、Superviewに対して制約を設定します。設定した制約をソースコードに紐付けて、制約を制御することで
NavigationBarとTabBarが表示されている場合は、Barの高さ分だけ制約でスペースを開けて
NavigationBarとTabBarが隠されている場合は、画面の端までTableViewを広げられるようになります。実装
複数の画面で同じ実装をすることになりそうだったので、
HideBarManagerというクラスを作って、ViewControllerから必要なプロパティを渡して
スクロール量の計算や制約の更新をします(*・ω・*)初期処理
final class HideBarManager { /// スクロールビューのTopの制約 /// /// - Note: Topの制約は、SafeAreaInsets.TopではなくSuperview.Topに対して設定してください private weak var topConstraint: NSLayoutConstraint? /// スクロールビューのBottomの制約 /// /// - Note: Bottomの制約は、SafeAreaInsets.BottomではなくSuperview.Bottomに対して設定してください private weak var bottomConstraint: NSLayoutConstraint? /// スクロールビュー private weak var scrollView: UIScrollView! /// タブバー private weak var tabBar: UITabBar? /// ナビゲーションバー private weak var navigationBar: UINavigationBar? /// タブバーのフレーム private var tabBarDefaultFrame: CGRect! /// ナビゲーションバーのフレーム private var navigationBarDefaultFrame: CGRect! /// スクロール開始時の位置 private var scrollBeginningOffsetY: CGFloat? /// タブバーの移動量 private var tabBarMovingDistance: CGFloat = 0 /// ナビゲーションバーの移動量 private var navigationBarMovingDistance: CGFloat = 0 /// スクロール方向 private var scrollDirectionY = UIScrollView.ScrollDirectionY.none private init() {} static func build(topConstraint: NSLayoutConstraint?, bottomConstraint: NSLayoutConstraint?, scrollView: UIScrollView!, tabBar: UITabBar?, navigationBar: UINavigationBar?) -> HideBarManager { let hideBarManager = HideBarManager() hideBarManager.topConstraint = topConstraint hideBarManager.bottomConstraint = bottomConstraint hideBarManager.scrollView = scrollView hideBarManager.tabBar = tabBar hideBarManager.navigationBar = navigationBar return hideBarManager } }VC側
private lazy var hideBarManager = HideBarManager.build( topConstraint: self.tableViewTopConstraint, bottomConstraint: self.tableViewBottomConstraint, scrollView: self.tableView, tabBar: self.tabBarController?.tabBar, navigationBar: self.navigationController?.navigationBar )ライフサイクル処理
ViewControllerの各ライフサイクルのメソッドが呼び出されるタイミングで実行する処理をHideBarManagerに実装していきます。
extension HideBarManager { func viewDidLoad() { self.addObservers() } func viewDidLayoutSubviews() { // 初回のみ代入したいためnilチェックもする if let tabBar = self.tabBar { self.tabBarDefaultFrame = self.tabBarDefaultFrame ?? tabBar.frame } if let navigationBar = self.navigationBar { self.navigationBarDefaultFrame = self.navigationBarDefaultFrame ?? navigationBar.frame } self.updateConstraints() } func viewWillDisappear() { self.showBarImmediately() } } extension HideBarManager { private func addObservers() { // アプリ復帰時にナビゲーションバーを表示しておくために、非アクティブになる直前に表示しておく NotificationCenter.default.addObserver(self, selector: #selector(showBarImmediately), name: UIApplication.willResignActiveNotification, object: nil) } /// タブバーとナビゲーションバーを表示させる @objc private func showBarImmediately() { self.tabBar?.frame = self.tabBarDefaultFrame self.navigationBar?.frame = self.navigationBarDefaultFrame self.tabBarMovingDistance = 0 self.navigationBarMovingDistance = 0 self.updateConstraints() } /// TopとBottomの制約を更新する private func updateConstraints() { guard let superview = self.scrollView.superview else { return } if self.tabBar != nil { self.bottomConstraint?.constant = -superview.safeAreaInsets.bottom + self.tabBarMovingDistance } if self.navigationBar != nil { self.topConstraint?.constant = superview.safeAreaInsets.top + self.navigationBarMovingDistance } } }VC側
override func viewDidLoad() { super.viewDidLoad() self.hideBarManager.viewDidLoad() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() self.hideBarManager.viewDidLayoutSubviews() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.hideBarManager.viewWillDisappear() }UIScrollViewDelegateの処理
UIScrollViewDelegateの各メソッドが呼び出されるタイミングで実行する処理をHideBarManagerに実装していきます。
extension HideBarManager { func scrollViewShouldScrollToTop() { self.showBarImmediately() } /// ドラッグ開始 func scrollViewWillBeginDragging() { if self.scrollBeginningOffsetY == nil { self.scrollBeginningOffsetY = self.scrollView.contentOffset.y self.tabBarMovingDistance = 0 self.navigationBarMovingDistance = 0 } } /// ドラッグ終了 func scrollViewDidEndDragging(decelerate: Bool) { // 慣性によるスクロールがない場合は、スクロール開始位置とスクロール方向を初期化する if !decelerate { self.scrollBeginningOffsetY = nil self.scrollDirectionY = .none } } /// スクロール中 func scrollViewDidScroll() { guard self.scrollView.isDragging else { return } let scrollViewHeight = scrollView.frame.size.height let scrollContentSizeHeight = scrollView.contentSize.height let scrollOffset = scrollView.contentOffset.y let scrollBeginningOffset = self.scrollBeginningOffsetY ?? 0 // 一番上に到達した場合は、Barを表示する if scrollOffset <= 0 { self.showBarImmediately() return } // 一番下に到達した場合は、何もしない if scrollContentSizeHeight <= scrollOffset + scrollViewHeight { return } // 下へスクロールしていて、 // スクロール方向をまだ保持していない or 保持しているスクロール方向がbottomの場合 // ナビゲーションバーとタブバーを移動させる if scrollBeginningOffset < scrollOffset { if self.scrollDirectionY == .none || self.scrollDirectionY == .bottom { self.scrollDirectionY = .bottom self.moveTabBarAndNavigationBar() return } } // 上へスクロールしていて、 // スクロール方向をまだ保持していない or 保持しているスクロール方向がtopの場合 // ナビゲーションバーとタブバーを移動させる if scrollBeginningOffset > scrollOffset { if self.scrollDirectionY == .none || self.scrollDirectionY == .top { self.scrollDirectionY = .top self.moveTabBarAndNavigationBar() return } } } /// スクロール停止 func scrollViewDidEndDecelerating() { self.scrollBeginningOffsetY = nil self.scrollDirectionY = .none } } extension HideBarManager { /// タブバーとナビゲーションバーを移動させる private func moveTabBarAndNavigationBar() { guard let tabBarFrame = self.calcTabBarFrame(), let navigationBarFrame = self.calcNavigationBarFrame() else { return } self.tabBar?.frame = tabBarFrame self.navigationBar?.frame = navigationBarFrame self.tabBarMovingDistance = tabBarFrame.origin.y - self.tabBarDefaultFrame.origin.y self.navigationBarMovingDistance = navigationBarFrame.origin.y - self.navigationBarDefaultFrame.origin.y self.updateConstraints() } /// スクロール量からタブバーのoffsetを算出し、タブバーのframeを返す private func calcTabBarFrame() -> CGRect? { guard let scrollBeginningOffsetY = self.scrollBeginningOffsetY else { return nil } // 移動量 let movingDistance = self.scrollView.contentOffset.y - scrollBeginningOffsetY var newTabBarOriginY = self.tabBarDefaultFrame.origin.y + movingDistance // もとの位置より上には移動させない let min = self.tabBarDefaultFrame.origin.y if newTabBarOriginY < min { newTabBarOriginY = min } // 画面外に出たらそれ以上は下に移動させない let max = self.tabBarDefaultFrame.origin.y + (self.tabBarDefaultFrame.size.height * 2) if newTabBarOriginY > max { newTabBarOriginY = max } var newTabBarFrame = self.tabBarDefaultFrame! newTabBarFrame.origin.y = newTabBarOriginY return newTabBarFrame } /// スクロール量からナビゲーションバーのoffsetを算出し、ナビゲーションバーのframeを返す private func calcNavigationBarFrame() -> CGRect? { guard let scrollBeginningOffsetY = self.scrollBeginningOffsetY else { return nil } // 移動量 let movingDistance = self.scrollView.contentOffset.y - scrollBeginningOffsetY var newNavigationBarOriginY = self.navigationBarDefaultFrame.origin.y - movingDistance let statusBarHeight = self.scrollView.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 // 画面外に出たらそれ以上は上に移動させない let min = self.navigationBarDefaultFrame.origin.y - (self.navigationBarDefaultFrame.size.height * 2) - statusBarHeight if newNavigationBarOriginY < min { newNavigationBarOriginY = min } // もとの位置より下には移動させない let max = self.navigationBarDefaultFrame.origin.y if newNavigationBarOriginY > self.navigationBarDefaultFrame.origin.y { newNavigationBarOriginY = max } var newNavigationBarFrame = self.navigationBarDefaultFrame! newNavigationBarFrame.origin.y = newNavigationBarOriginY return newNavigationBarFrame } }VC側
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { self.hideBarManager.scrollViewShouldScrollToTop() return true } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { self.hideBarManager.scrollViewWillBeginDragging() } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { self.hideBarManager.scrollViewDidEndDragging(decelerate: decelerate) } func scrollViewDidScroll(_ scrollView: UIScrollView) { self.hideBarManager.scrollViewDidScroll() } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { self.hideBarManager.scrollViewDidEndDecelerating() }さいごに
今回実装したソースコード全体は、GitHubにpushしていますm(_ _)m
実装過程で、iOS13ではちゃんとNavigationBarが隠れたのにiOS14では同じロジックでちゃんと隠れてくれなかったりして
思いの外大変でした。。現状の実装では、非表示状態から再表示される時など見え方が微妙なところもまだまだあるので
もう少しきれいに表示/非表示切り替えられるようアプデしていきたいなーと思います|ω・`)明日の投稿もお楽しみに〜(・ω・)ノ
- 投稿日:2020-12-06T00:18:36+09:00
Ethereumでガチャりたい!
はじめに
Ethereum のゲーム dApps を眺めていると、トークンの売り方として「セール形式」が多い印象です。
セール形式とは、運営さんが事前に発行しておいたトークン(キャラやカード)を販売する形式で、「超強いキャラは値段を高く」、「そこそこの強さであれば安く」といった売り方です。
一方で、スマホアプリでよくあるのが「ガチャ形式」です。ガチャ形式とは、プレイヤーがガチャを引くたびにトークンが発行される形式で、トークンには内容に応じた確率が設定されます。
1回の値段は抑えめにしておき、「超強いキャラは低確率で排出」、「そこそこの強さであれば高確率で排出」といった売り方です。
ゲーム性がスマホアプリとよく似ている dApps は結構目にしますが、ガチャ形式が採用されているケースはほとんど見られません。一体、なぜでしょうか?
色々な理由があると思うのですが、私なりの考えは下記となります。①コントラクトで確率(乱数)を使えない
②コントラクトのバイナリが公開されるのでガチャの仕組みが解析される
③トランザクションに再現性がある(大当たりのトランザクションが再利用される)
④トークンの発行をプレイヤーに委ねるのが怖い(発行処理が直接叩かれのは不安)他にも理由はあると思いますが、dApps とガチャの相性はあまり良くないと思われます。
でも、それでも、ブロックチェーンでガチャを引きたいじゃないですか…。
そんな思いにかられて、検証用のサンプル dApps を作ったので、ご覧くださいませ。CharaMixDemo
ご紹介するサンプル dApps は「CharaMixDemo」といいます。
名前の通り、キャラ(トークン)を生成して合成する内容です。
オープンベータ形式で配信しておりますので、お使いのスマホに合わせて下記のリンクからインストールしていただけます。*Android端末をお使いの方はこちら*
*iOS端末をお使いの方はこちら*
ご注意
このサンプルでは、どこかで見たようなキャラ「39さん」が登場しますが、非営利の検証用に作成したものであり、製品版としてリリースするつもりはありません。ですので、「初音ミクの二次創作ガイドライン」に抵触するものではございません。
(※接続するブロックチェーンも Ethereum のテストネット Rinkeby となります)後の説明のわかりやすさ & 作る本人のモチベーションのためにも、「初音ミク」オマージュのキャラを使わせていただいていおります。
ご理解くださいますようお願いいたします。dAppsの構成と課題への対応
サンプル dApps のクライアントは、iOS / Android アプリとなります。
プレイヤーはアプリ経由で、コイン(ERC20 トークン)を消費してガチャを引き、キャラ(ERC721 トークン)を取得できます。クライアント上でガチャが引かれるとトランザクションが発行され、ブロックチェーン側の窓口である MAIN コントラクトにより処理されます。
MAIN コントラクトはコイン(CMC)とキャラ(CMA)トークンを管理するコントラクトへアクセスし、トークンを実際に発行/焼却します。
では、この構成をもとに、課題への対応を見ていきましょう。*課題①:コントラクトで確率(乱数)を使えない*
乱数(厳密には乱数のシード)はアプリ側で生成することにしました。
この乱数をパラメータに含めてトラザクションを発行するのですが、この時、トランザクションのパラメータを暗号化しておきます。
コントラクト側では受け取ったパラメータを複合化してシードを取り出し、乱数生成に利用します。
*課題②:コントラクトのバイナリが公開されるのでガチャの仕組みが解析される*
ガチャの結果を決定付ける乱数の暗号化さえ見破られなければ、それ以外の処理が解析されても問題ないと割り切ることにしました。
ガチャのロジックを解析するプレイヤーが出てきたとしても、それはそれでゲームを攻略する楽しみの一環だと思います。*課題③:トランザクションに再現性がある(大当たりのトランザクションが再利用される)*
コントラクト側で、前回ガチャが引かれた時間(ブロック番号)を保持するようにしました。
アプリ側ではガチャのトランザクションを発行する際、その時点でのブロック番号をパラメータとしてトランザクションへ含めます。
ガチャ処理が呼ばれた MAIN コントラクトでは、パラメータ内のブロック番号と自身の保持するブロック番号を比較し、古いトランザクションであれば弾きます。また、トランザクションパラメータの暗号化の際には、プレイヤーの Ethereum アドレスも反映させるようにしたため、他者のトランザクションの流用もできなくなっています。
*課題④:トークンの発行をプレイヤーに委ねるのが怖い(発行処理が直接叩かれのは不安)*
アプリの窓口となる MAIN コントラクトがアクセスされた際、真っ先にトランザクションパラメータの複合を試して、失敗した場合は処理を中断するようにしました。
パラメータの複合に成功した場合にのみ、MAIN コントラクトはトークンコントラクトの発行/焼却処理を呼び出します。
これにより、MAIN コントラクトがアプリを経由せずに直接叩かれても、トークンを発行してしまうことはありません。
また、トークンコントラクトの発行/焼却処理は、MAIN コントラクトからのみ呼び出せるようにしました。
直接トークンコントラクトへアクセスして、トークンの発行/焼却処理を呼び出すようなトランザクションは中断されます。
一方で、CMC / CMA トークンコントラクトはそれぞれ、ERC20 / ERC721 に準拠しているため、MetaMask や OpenSea といった外部ツールやマーケットにおけるトレードが可能です。以上で、実装面の説明は終わりです。
ガチャの内容
では、ガチャ内容を詳しく見ていきましょう。
このサンプルには「女の子の生成」と「女の子の合成」の2つのガチャがあります。*生成ガチャ*
候補となる基本素体の女の子から個性(パーツ)をバラバラに抽出して新しい女の子を生成するガチャです。
この時、アプリ上でガチャの引き直し(乱数の更新)が好きなだけ行えます。
気に入ったキャラが生成されたらトランザクションを発行し、新たなトークンとして取得できます。*合成ガチャ*
2人の女の子を合体させて個性(パーツ)をシャッフルし、性能を向上させるガチャです。
この時、アプリ上でガチャの引き直し(乱数の更新)が好きなだけ行えます。
気に入ったキャラが合成されたらトランザクションを発行し、新たなトークンとして取得できます。
合成トークン取得時には、素材となった女の子(トークン)は焼却されます。また、合成により取得される女の子は「バージョン」の値が1つあがります。
このバージョンとは、スマホアプリのガチャにおける「レアリティ」に相当する判断基準で、バージョンが高いほど、その女の子の取得までに支払われたコストが多いことを意味します。
(※バージョンの初期値は0で、同じバージョンの女の子同士でのみ合成が可能です)
(※バージョンNの女の子は「バージョン0のトークンを2のN乗個」消費して出来上がっていると判断できます)例えば、「バージョン8」の女の子であれば、「8回バージョンアップした」=「バージョン0のトークンを256個消費した」ということが一目で分かります。
「このキャラすげぇ!」とうい風にありがたがってもらうための値がバージョンとなります。ガチャを回そう!
では、実際にガチャを回していきましょう。
まずは、生成ガチャから。
引くのは断然「39さんPickUp」です。
ピックアップされている39さんの抽出率は「25%」となります。
…はて、「抽出率」などという見慣れない単語がでてきましたが、これはなんでしょうか?この生成ガチャでは、「目なら目」、「口なら口」といった具合に、基本素体からパーツを選出して新しい女の子を生成します。
このパーツの選出がどのぐらいの確率で行われるかが抽出率となります。
ですので、「39さんPickUp」ガチャでは25%の確率で、39さんのパーツが選出されることを意味しています。ところが、困ったことにトークンを構成するパーツは数十個も存在します。
成分100%の39さんを生成ガチャで引こうと思ったら、非現実的な確率(25%の数十乗)となってしまいます。
そこで、このサンプルで想定する遊びは「生成ガチャでそこそこの成分のキャラをたくさん取得」し、「合成ガチャで少しずつ純度を高めていく」というものになります。生成ガチャで素材厳選
生成ガチャをぶん回して、素材キャラを集めていきましょう。
まずは、39さん成分40%オーバーが出るまで乱数を引き直して、キャラを8体GETしました。
50%オーバーが4体。なかなかの引きでした。合成ガチャで成分抽出
続いて、合成ガチャで39さん成分を抽出していきましょう。
合成ガチャでは素材となる二人の女の子から、半々(50%)の確率でがパーツが選出されます。
ここでは、選択した2体のキャラの「39さん成分の平均」に対して、「+10%」ぐらいの値を目標に合成ガチャを回していくことにします。まずは、「バージョン0」の8体から1ペアずつ厳選し、「バージョン1」の女の子を4体合成しました。
続いて、「バージョン1」の4体から、「バージョン2」を2体厳選。
最後に、「バージョン3」のキャラを厳選してひと段落です。
これで、8体いた「バージョン0」のトークンが、1体に圧縮されました。
最終的な成分は「83%」。
ちょっとチンチクリンな見た目ですが、かなりの39さん成分を抽出できました。目指せ39さん100%
この調子で成分100%を目指しましょう。
生成ガチャと合成ガチャをブン回して、「バージョン3」のキャラを追加で3体作成しました。
続いて、「バージョン3」のキャラから「バージョン4」のキャラを厳選し…
ついに、成分100%の39さんが完成しました!
お〜、かなり39さんです!
ここまでに積み上げた「バージョン0」のトークン数は「2の5乗=32個」で、これがこのトークンの資産的な価値ということになります。パラメータはフレーバー程度のものですが、戦闘力「522」、個体値「80.4%」となりました(すごく強い!多分!)。
これらがこのトークンのゲーム的な価値となり、性能が高いと判断されればプレイヤー間での需要が高まるはずです。OpenSeaへ出品
さて、実際にトークンを作り出したのであれば、マーケット上での価値も気になるところです。
サンプルアプリから「METAデータの更新」をすることで、トークンの METAデータを OpenSea へアップロードできます。試しに何体か出品してみましょう。
自身の手で生み出したトークンがマーケットで売れてくれたりしたら、感慨もひとしおというものです。
(※このサンプルの接続先は Rinkeby なのでリアルマネーは得られませんが…)最後に
トークンの売買が手軽に行えるのが Ethereum の面白味の1つですが、「ここにガチャ要素を入れたら楽しいにきまっている!」との思いで作ったのが今回のサンプル dApps です。
一般的なスマホアプリの「大当たりがでるまで引き続けるガチャ」とは少し毛色が違った、「小当たりを積み重ねていくガチャ」の面白みを感じていただけたのなら幸いです。
興味のある方は、是非サンプルで遊んでみてくださいませ。
ご意見ご感想などございましたら、お教えくださると嬉しいです。では、ここまでご覧いただきありがとうございました。











































