tag:crieit.net,2005:https://crieit.net/tags/iOS/feed 「iOS」の記事 - Crieit Crieitでタグ「iOS」に投稿された最近の記事 2023-10-04T09:54:41+09:00 https://crieit.net/tags/iOS/feed tag:crieit.net,2005:PublicArticle/18592 2023-10-04T09:54:41+09:00 2023-10-04T09:54:41+09:00 https://crieit.net/posts/iOS-Requested-but-did-not-find-extension-point-with-identifier-Xcode-IDEFoundation-IDEResultKitSerializationConverter iOSアプリのビルドでRequested but did not find extension point with identifier Xcode.IDEFoundation.IDEResultKitSerializationConverterエラー <p>iOSのFlutterアプリをビルドしていると突然 <code>Requested but did not find extension point with identifier Xcode.IDEFoundation.IDEResultKitSerializationConverter</code> というエラーがではじめた。</p> <p>調べてみてもなんか対症療法的な感じで人ぞれぞれの解決方法で明確な手順ぽいものがなさそう。困った…。</p> <p>そういえば最初に実行しようとしたときに長過ぎて止まっているような気がしたので中断したりしており、それからで始めたのでもしかしたらそれが悪いのでは…と思い腰を据えて実行後待ってみると普通に実行できた。自分のせいだった。</p> <p>一度それをやるとその後全部エラーになるので、何か途中で止めることでビルド環境がおかしくなってしまうっぽい。そうなったら一度cleanしたりして再度ビルドする必要がある。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17924 2022-01-07T16:19:06+09:00 2022-01-11T17:18:59+09:00 https://crieit.net/posts/Flutter-61d7e96a4daa2 Flutterベースのモバイル向けタイムラインの作成 <h1><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>タイムライン(モバイル向け)の構造を初公開</h1> <p>2018年に初めてFlutterベースのアプリを作ったときは、とても楽しく興奮しました。それから3年経ち、<a target="_blank" rel="nofollow noopener" href="https://quire.io">Quireアプリ</a>もかなり充実して、従来のモバイル向けプロジェクト管理アプリの域を超えるまでになりました。Quireモバイルアプリの現行バージョンは、階層表示、ボード表示だけでなく、タイムライン表示にも対応しています。</p> <p>モバイルアプリ向けのタイムライン表示の作成を決めたときは、簡単にできるとは思いませんでした。当時は類似の既成コンポーネントもなかったためですが、驚いたのは、インターネットでタイムライン表示の構造についての情報も見当たらなかったことです。そこで、いちかばちか、自分たちで作ってみることにしました。</p> <h5><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>モバイルアプリ用のタイムラインでは、以下を計画していました。</h5> <ol> <li>横方向への無限日付スクロール</li> <li>レンダリングオンデマンド(ROD)。ビューポートにあるときのみ実行されるWidgetのState</li> <li>任意の位置に素早く配置</li> <li>操作がかんたんで使いやすいインターフェースと、スムーズなユーザーエクスペリエンス</li> </ol> <p>数週間で初期開発が完了し、以下のような構造になりました。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/52c3e5779da7-20220105.png" alt="" /></p> <ol> <li>タイムラインペインのコアベース(週、週末のセクションなど)</li> <li>タスクリスト(階層構造のタスクリスト)</li> <li>タイムラインペインのビューポートベース双方向リスト</li> <li>1ペインのみのとき、2ペインにまたがるときの両方に対応した期間の横棒</li> <li>期間の横棒上の固定ラベル</li> </ol> <p>上図のように、タスクごとにタイムラインペインが割り当てられ、すべてのタイムラインのスクロール位置は互いに同期されます。</p> <h2 id="インデックスベースのスクロールビュー"><a href="#%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%93%E3%83%A5%E3%83%BC">インデックスベースのスクロールビュー</a></h2> <p>Google Flutter Widgetに似たインデックスベースのスクロールビューを作るために、Centerに引数のあるカスタムスクロールビューを使用します。実装すると、任意の位置まで素早くスクロールできるようになります。スクロール中のどの時点でも、各位置とインデックスを表示できます。</p> <p>イメージ的には、少しスクロールした時点で、新しいCenterの引数でタイムラインをリロードしてビューポートの外に移し、またスクロールするとビューポート内に配置される、という感じです。</p> <h2 id="タイムラインペイン"><a href="#%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%A9%E3%82%A4%E3%83%B3%E3%83%9A%E3%82%A4%E3%83%B3">タイムラインペイン</a></h2> <p>タイムラインをスムーズに使えるように、インデックスベースのスクロール表示と似た発想で、横方向にスクロールできるカスタム「無限双方向スクロールビュー」を実現しました。実装すると、タイムラインをなめらかにスクロールできます。</p> <p>無限双方向スクロールビューには、Flutterの強力なViewportの考え方を活用しました。そして、Backwardリストのインデックスを-1から始まる負の数に変更しました。Index 0に当たる日付が分かるようにフラグも設定して、任意の日付まで素早くスクロールできるようにしました。</p> <pre><code class="js">Widget forwardList = SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return cellBuilder(context, _getIndex(forward: true, index: index)); }) ); Widget backwardList = SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return cellBuilder(context, _getIndex(forward: false, index: index)); }), ); Scrollable( viewportBuilder: (BuildContext context, ViewportOffset offset) { return Viewport( offset: offset, center: forwardListKey, slivers: [ backwardList, forwardList, ] ); }, ) </code></pre> <h2 id="2ペインにまたがるときの問題と解決方法"><a href="#2%E3%83%9A%E3%82%A4%E3%83%B3%E3%81%AB%E3%81%BE%E3%81%9F%E3%81%8C%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E5%95%8F%E9%A1%8C%E3%81%A8%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95">2ペインにまたがるときの問題と解決方法</a></h2> <p>ビューポートには無限のリスト2つがスクロールされるため、期間の横棒が2つのリストにまたがることもあります。そこで、どちらのリストにも完全に同じ期間の横棒を作成し、ぴったり重ねて、リストがビューポートの外に移動してもリスト内のアンカーが壊れないようにしました。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/a56ed0583195-20220105.gif" alt="" /></p> <h2 id="固定ラベルで解決"><a href="#%E5%9B%BA%E5%AE%9A%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A7%E8%A7%A3%E6%B1%BA">固定ラベルで解決</a></h2> <p>モバイル機器の小さい画面ではプロジェクトのどこを見ているかが分かりにくく、使っていると混乱してきます。この問題は、できるだけ多くの情報を提供することで軽減できます。そこで便利なのが固定ラベルです。</p> <p>最初はとにかくシンプルにするため、スクロールビューのスクロール通知に従って、位置を取得してから配置されたラベルに設定していました。固定ラベルを各タイムラインペインの開始位置に表示するには、期間の横棒の現在位置の計算がベースとなります。</p> <p>しかし、新しく配置されたラベルは次のフレームまでしか更新されず、スクロールビューと同じ時間枠で同期されないため、ずれて見えてしまいました。</p> <p>幸いFlutterコミュニティーが、レンダリングレイヤー固定ヘッダーというすばらしい解決方法を教えてくれました。つまりレイアウトのタイミングによる方法です。レンダリングレイヤーにすべてのWidgetをサイズとともに入れるだけでなく、そのピクセルすべてを計算する必要があります。最後にlocalToGlobal関数を、スクロール位置、および2ペインにまたがるときのペイン切り替えに基づいた演算操作と置き換えて、パフォーマンスを向上させました。</p> <h2 id="始まりはこれから"><a href="#%E5%A7%8B%E3%81%BE%E3%82%8A%E3%81%AF%E3%81%93%E3%82%8C%E3%81%8B%E3%82%89">始まりはこれから</a></h2> <p>今は大変な時代ですが、だからこそテクノロジーの分野で貢献したいと考えています。タイムライン表示の作成でまず考えたのは、どうやってFlutterの強力なフレームワークを活用して、ビューコンポーネントを一から作り直すことなく、軽く安定したゴージャスなUIを実現するか、ということでした。</p> <p>各日付単位はインデックスとして、FlutterのSliverに組み込まれています。ほとんどのものはWidgetレイヤーの高レベルの開発概念に留まり、固定ビューのときのみレンダリングレベルに移動します。</p> <p><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quireアプリ</a>をインストールして、Flutterベースのモバイルアプリを使ってみませんか。Quireタイムラインについて気になることは、コメントを投稿するか、<a target="_blank" rel="nofollow noopener" href="https://twitter.com/quire_io">@quire_io</a>でツイートしてお知らせください!</p> <p>※転載許可済み: https://zenn.dev/quireteam/articles/2b2c44c3e49fac</p> uniyeh tag:crieit.net,2005:PublicArticle/17856 2021-12-15T18:22:37+09:00 2021-12-15T18:22:37+09:00 https://crieit.net/posts/Swift-Cartography SwiftでCartographyを使う方法 <pre><code>//画面の横幅の長さを取得する変数を宣言 let width = UIScreen.main.bounds.size.width //banner画像 let banner_image:UIImage = UIImage(named:"logo")! let imageView = UIImageView(image:banner_image) self.view.addSubview(imageView) constrain(imageView, view) { banner_image, view in banner_image.top == banner_image.superview!.top + 120 banner_image.left == banner_image.superview!.left + 25 banner_image.width == width - 50 banner_image.height == (width - 50) / 2 } </code></pre> <p>参考URL</p> <p><a target="_blank" rel="nofollow noopener" href="https://wagtechblog.com/programing/swift-cartography.html">https://wagtechblog.com/programing/swift-cartography.html</a></p> wawa tag:crieit.net,2005:PublicArticle/17749 2021-11-11T17:42:02+09:00 2021-11-21T08:11:46+09:00 https://crieit.net/posts/iPadOS-15 iPadOS 15 にするとカーソルキーが動かなくなる問題の修正方法 <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/danjiro/items/253e5a33a38599098274">フリーソフト9VAeきゅうべえ</a> 、iPad版について。<br /> iPad OS 15 にすると、キーボードのカーソルキーがきかなくなった。Alt+カーソルキーは動作するが、カーソルキーだけ押すと反応しない。</p> <p>調べると、UIKeyCommand の wantsPriorityOverSystemBehavior を YES に設定しないといけないらしい。カーソルキーがOSのショートカットに割り当てられたみたいだ。<br /> 修正には、Xcode 13 (Big Sur以上)が必要</p> <p>修正前</p> <pre><code>- (NSArray *)keyCommands { return @[ [UIKeyCommand keyCommandWithInput: UIKeyInputUpArrow modifierFlags: 0 action: @selector(keyUp)], [UIKeyCommand keyCommandWithInput: UIKeyInputDownArrow modifierFlags: 0 action: @selector(keyDown)], [UIKeyCommand keyCommandWithInput: UIKeyInputLeftArrow modifierFlags: 0 action: @selector(keyLeft)], [UIKeyCommand keyCommandWithInput: UIKeyInputRightArrow modifierFlags: 0 action: @selector(keyRight)] ]; } </code></pre> <p>修正後</p> <pre><code>- (NSArray *)keyCommands { UIKeyCommand *ukey = [UIKeyCommand keyCommandWithInput: UIKeyInputUpArrow modifierFlags: 0 action: @selector(keyUp)]; UIKeyCommand *dkey = [UIKeyCommand keyCommandWithInput: UIKeyInputDownArrow modifierFlags: 0 action: @selector(keyDown)]; UIKeyCommand *lkey = [UIKeyCommand keyCommandWithInput: UIKeyInputLeftArrow modifierFlags: 0 action: @selector(keyLeft)]; UIKeyCommand *rkey = [UIKeyCommand keyCommandWithInput: UIKeyInputRightArrow modifierFlags: 0 action: @selector(keyRight)]; if (@available(iOS 11.0, *)) if(@available(iOS 15.0, *)){ ukey.wantsPriorityOverSystemBehavior = YES; dkey.wantsPriorityOverSystemBehavior = YES; lkey.wantsPriorityOverSystemBehavior = YES; rkey.wantsPriorityOverSystemBehavior = YES; } return @[ ukey, dkey, lkey, rkey ]; } </code></pre> <p>if (@available(iOS 11.0, *)) をいれておかないと、iOS 10 で落ちた。</p> Danjiro Daiwa tag:crieit.net,2005:PublicArticle/17619 2021-08-27T23:19:06+09:00 2021-08-27T23:19:06+09:00 https://crieit.net/posts/AVAudioUnitSampler AVAudioUnitSamplerで指定したチャンネルの音をすべて止める方法 <p>AVAudioUnitSamplerにはチャンネル上でMIDIの音をならすためのstartNoteと、停止させるためのstopNoteメソッドがある。でも、例えば自動で演奏させていて、停止ボタンを押した時に全ての音を消したい、と思った時、全ての音を止めるためのメソッドがどうもないっぽい。</p> <p>ではどうするのか。</p> <p>AVAudioUnitSamplerにはsendMIDIEventという、MIDIイベントを直接送るできることのできる機能がある。それを利用することでチャンネルの音を全て停止することができた。</p> <p>具体的には下記のやり方でできた。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17618 2021-08-27T23:13:02+09:00 2021-08-27T23:22:47+09:00 https://crieit.net/posts/AVFoundation AVFoundationでなぜか実機でだけ音がならない時 <p>AVFoundationで、プログラムは何度見返しても絶対正しいし、シミュレータだと音がなるのになぜか実機だと音がならないということがあった。</p> <p>ちなみに試していたのはAVAudioEngineとAVAudioUnitSamplerを使ったMIDIをならすための簡単な処理。サウンドフォント(sf2ファイル)を使うときも使ってないときも同様。</p> <p>ちなみに、同時に録音したボイスをならす機能もあるのだがそちらはなぜかちゃんと聞こえる。一体何なのか非常に悩んだ。</p> <p>で、結局原因はというとAVAudioSessionのsetCategoryだった。上記にも書いたとおり、MIDIの処理とボイス用の処理があり、それぞれ別クラスになっていた。まずMIDIクラスを初期化し、次にVoiceクラスを初期化していたのだが、そのVoiceクラスのコンストラクタでsetCategoryしていた。そのためMIDIクラス側が無効になってしまっていたっぽい。</p> <p>なぜ実機だけなのかはわからないが。端末全体の音をならす部分の数の問題とかかもしれない。</p> <p>なにはともあれ、各クラスを初期化する前に下記を書いたら両方ちゃんと動作するようになった。</p> <pre><code class="swift"> let session = AVAudioSession.sharedInstance() try session.setCategory( AVAudioSession.Category.playAndRecord, options: AVAudioSession.CategoryOptions.mixWithOthers ) try session.setActive(true) </code></pre> <p>とにかくあまり詳しくないため別の原因や正しい処理方法などあるかもしれないので各々適宜要調査だと思われる。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17602 2021-08-16T02:31:05+09:00 2021-08-22T07:44:40+09:00 https://crieit.net/posts/unity-ios-android-secret-manager 📔Unity で iOS/Android アプリの設定値をセキュアに扱う方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>iOS/Android でユーザーの情報をセキュアに扱う必要があったので、調査したところ Android には <a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> が存在することを知りました。iOS には <a target="_blank" rel="nofollow noopener" href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> が存在します。</p> <p>今回は Unity の iOS/Android プラットフォーム上で設定値を保存するための実装を行う必要があったので、Unity から扱えるようネイティブプラグインを作成しました。今後もこういった要望はありそうでしたので、記事として手順や内容を書き記しておくことにしました。</p> <p>本記事内で紹介しているコードは下記にアップ済みです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample">https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample</a></p> <h1 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h1> <ul> <li>MacBook Air (M1, 2020)</li> <li>Unity 2020.3.15f2</li> <li>Android 6.0 以上 <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> が使用可能なバージョン</li> </ul></li> </ul> <h1 id="Android のネイティブプラグインを作成する"><a href="#Android+%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Android のネイティブプラグインを作成する</a></h1> <p>Android 環境ではまず <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver">External Dependency Manager for Unity</a> を利用して、Unity の Android ネイティブプラグインで <code>EncryptedSharedPreferences</code> 利用可能にします。</p> <h2 id="(追記) Gradle を利用したライブラリのインストール方法"><a href="#%28%E8%BF%BD%E8%A8%98%29+Gradle+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95">(追記) Gradle を利用したライブラリのインストール方法</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://twitter.com/shiena">shiena</a> さんにご教授いただいたのですが、<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/shiena/articles/unity-sqlcipher#gradle%E3%82%92%E5%88%A9%E7%94%A8">こちらの記事</a>のように Gradle を利用することでも簡易にライブラリの取り込みが可能なようでした。</p> <p>手順は上記の記事をご参照いただくとして、Gradle を利用する方法で外部ライブラリを取り込む際の <code>Assets/Plugins/Android/mainTemplate.gradle</code> および <code>Assets/Plugins/Android/gradleTemplate.properties</code> は下記になります。</p> <p>```diff gradle:Plugins/Android/mainTemplate.gradle<br /> dependencies {<br /> implementation fileTree(dir: 'libs', include: ['*.jar'])<br /> + implementation 'androidx.security:security-crypto:1.1.0-alpha03'<br /> <strong>DEPS</strong>}</p> <p>android {</p> <pre><code><br />```diff properties:Assets/Plugins/Android/gradleTemplate.properties org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M org.gradle.parallel=true android.enableR8=**MINIFY_WITH_R_EIGHT** + android.useAndroidX=true unityStreamingAssets=.unity3d**STREAMING_ASSETS** **ADDITIONAL_PROPERTIES** </code></pre> <p><strong>Gradle を利用した方法でライブラリを利用される際は、次の <code>External Dependency Manager for Unity で必要なパッケージをインストールする</code> の手順はスキップ可能です。<code>EncryptedSharedPreferences を利用するためのネイティブコードを追加する</code> のステップから進めてください。</strong></p> <p><code>External Dependency Manager for Unity</code> を利用する方法だと、取り込み先プロジェクト内でライブラリの競合が発生する恐れがあります。Gradle を利用する方法であれば回避が可能です。<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p> <h2 id="External Dependency Manager for Unity で必要なパッケージをインストールする"><a href="#External+Dependency+Manager+for+Unity+%E3%81%A7%E5%BF%85%E8%A6%81%E3%81%AA%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B">External Dependency Manager for Unity で必要なパッケージをインストールする</a></h2> <p><code>External Dependency Manager for Unity</code> をインポートするため <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver/blob/master/external-dependency-manager-latest.unitypackage">unitypackage</a> をダウンロードして、<strong><code>EncryptedSharedPreferences</code> を導入したい Unity プロジェクトを開いてから <code>unitypackage</code> をクリックすることで、<code>External Dependency Manager for Unity</code> を Unity プロジェクトにインポートします。</strong></p> <p><img src="https://i.gyazo.com/1af7cdf4d7d5749e59e151eef1ca5493.png" alt="ダウンロードした <code>unitypackage</code> をクリックして Unity プロジェクトに External Dependency Manager for Unity をインポートする" /></p> <p>Unity プロジェクトの <code>Build Settings</code> からプラットフォームは Android に切り替えておきます。<code>Enable Android Auto-resolution?</code> というダイアログの選択肢はどちらを選んでも構いません。<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p> <p>External Dependency Manager for Unity で各種パッケージを管理する方法は <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver#android-resolver-usage">README</a> に記載がある通り、<strong><code>*Dependencies.xml</code> というファイルを <code>Editor</code> フォルダに配置することで可能になります。</strong></p> <p>今回は <code>EncryptedSharedPreferences</code> を導入するため、下記の xml ファイルを <code>Editor</code> フォルダ内に配置します。</p> <pre><code class="xml"><!-- Assets/Editor/AndroidPluginDependencies.xml --> <?xml version="1.0" encoding="utf-8"?> <dependencies> <androidPackages> <!-- 本記事ではバージョン 1.1.0-alpha03 を利用している --> <androidPackage spec="androidx.security:security-crypto:1.1.0-alpha03"> <androidSdkPackageIds> <!-- Google の Maven リポジトリからインストールするため、 extra-google-m2repository を指定する --> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> </androidPackages> </dependencies> </code></pre> <p>その後、<strong>Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択して、<code>Assets/Editor/AndroidPluginDependencies.xml</code> の内容を元に <code>EncryptedSharedPreferences</code> を利用するのに必要なパッケージを自動で <code>Assets/Plugins/Android</code> フォルダにダウンロードします。</strong></p> <p><img src="https://i.gyazo.com/df394e15149e54dae3e9a81848512ee9.png" alt="1. Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択する" /><br /> <strong>1. Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択する</strong></p> <p><img src="https://i.gyazo.com/f6d2ec95ef9c2afdc857fecef2b165e5.png" alt="2. 実行に成功すると EncryptedSharedPreferences を利用するのに必要なライブラリ群が <code>Assets/Plugins/Android</code> フォルダに配置される" /><br /> <strong>2. 実行に成功すると EncryptedSharedPreferences を利用するのに必要なライブラリ群が <code>Assets/Plugins/Android</code> フォルダに配置される</strong></p> <p>ここまで来ればあとは Android ネイティブコードを <code>Assets/Plugins/Android</code> フォルダ内に配置して Unity 側から叩けるようにするだけです。</p> <h2 id="EncryptedSharedPreferences を利用するためのネイティブコードを追加する"><a href="#EncryptedSharedPreferences+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">EncryptedSharedPreferences を利用するためのネイティブコードを追加する</a></h2> <p>早速下記の Android ネイティブコードを <code>Assets/Plugins/Android~~<del>/SecretManager.java</code> に配置します。</p> <pre><code class="java">// Assets/Plugins/Android/SecretManager.java package com.nikaera; import com.unity3d.player.UnityPlayerActivity; import java.lang.Exception; // External Dependency Manager for Unity によって、 // 必要な jar が含まれているため EncryptedSharedPreferences の利用が可能になる import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Bundle; import android.util.Log; public class SecretManager { private SharedPreferences sharedPreferences; public SecretManager(Context context) { try { // EncryptedSharedPreferences で設定値を保存する際に用いる、 // 暗号鍵を扱うためのラッパークラスをデフォルト設定で作成する MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); // EncryptedSharedPreferences のインスタンスを生成する // コンストラクタで作成した masterKey を指定している this.sharedPreferences = EncryptedSharedPreferences.create( context, context.getPackageName(), masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (Exception e) { e.printStackTrace(); } } /** * 指定したキーで値を保存する関数 * @param key 値を保存する際に用いるキー * @param value 保存したい値 * @return boolean 値の保存に成功したかどうか */ public boolean put(String key, String value) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(key, value); return editor.commit(); } /** * 指定したキーで保存した値を取得する関数 * `put` 関数で保存した値を取得するのに利用する * @param key 取得したい値のキー * @return string キーに紐づく値、存在しなければ空文字が返却される */ public String get(String key) { return sharedPreferences.getString(key, null); } /** * 指定したキーで値を削除する関数 * @param key 削除したい値のキー * @return boolean 値の削除に成功したかどうか */ public boolean delete(String key) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.remove(key); return editor.commit(); } } </code></pre> <p>その後、上記を Unity スクリプトから実行可能にするための C# クラスを作成します。本記事ではファイルを <code>Assets/Scripts/EncryptedSharedPreferences.cs</code> に配置します。</p> <pre><code class="csharp">// Assets/Scripts/EncryptedSharedPreferences.cs</del><del> using UnityEngine; /// <summary> /// 利用するネイティブコードは <c>Assets/Plugins/Android/SecretManager.java</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> /// </remarks> class EncryptedSharedPreferences { private readonly AndroidJavaObject _secretManager; public EncryptedSharedPreferences() { // コンストラクタで com.nikaera.SecretManager のインスタンス生成を行う var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer") .GetStatic<AndroidJavaObject>("currentActivity"); var context = activity.Call<AndroidJavaObject>("getApplicationContext"); _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context); } public bool Put(string key, string value) { return _secretManager.Call<bool>("put", key, value); } public string Get(string key) { return _secretManager.Call<string>("get", key); } public bool Delete(string key) { return _secretManager.Call<bool>("delete", key); } } </code></pre> <p>あとは用途に応じて下記のようなコードで設定値の保存や取得などを行えます。</p> <pre><code class="csharp">// ... var _sharedPreferences = new EncryptedSharedPreferences(); // name をキーとして値を nikaera で保存する _sharedPreferences.Put("name", "nikaera"); // name をキーとして値を取得する var name = _sharedPreferences.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する _sharedPreferences.Delete("name"); // ... </code></pre> <h1 id="iOS のネイティブプラグインを作成する"><a href="#iOS+%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">iOS のネイティブプラグインを作成する</a></h1> <p>iOS の場合は外部ライブラリを利用しないため、<code>External Dependency Manager for Unity</code> は利用しません。<strong>本来であれば Swift で信頼できる外部フレームワークを取り込み利用できると良さそうですが、今回は Objective-C でネイティブプラグインを書いていきます。</strong><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p> <h2 id="Keychain Services を利用するためのネイティブコードを追加する"><a href="#Keychain+Services+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">Keychain Services を利用するためのネイティブコードを追加する</a></h2> <p>早速下記の iOS ネイティブコードを <code>Assets/Plugins/iOS/KeychainService.mm</code> に配置します。</p> <pre><code class="objc">// Assets/Plugins/iOS/KeychainService.mm</del>~~ // Keychain Services を利用するために Security フレームワークを利用する #import <Security/Security.h> extern "C" { // 指定したキーで値を保存する関数 // - param // - dataType: 値を保存する際に用いるキー // - value: 保存したい値 // - return // - 保存時のステータスコードを返却する (0 以外は失敗) int addItem(const char *dataType, const char *value) { NSMutableDictionary* attributes = nil; NSMutableDictionary* query = [NSMutableDictionary dictionary]; NSData* sata = [[NSString stringWithCString:value encoding:NSUTF8StringEncoding] dataUsingEncoding:NSUTF8StringEncoding]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL); if (err == noErr) { attributes = [NSMutableDictionary dictionary]; [attributes setObject:sata forKey:(id)kSecValueData]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate]; err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes); return (int)err; } else if (err == errSecItemNotFound) { attributes = [NSMutableDictionary dictionary]; [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [attributes setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; [attributes setObject:sata forKey:(id)kSecValueData]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrCreationDate]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate]; err = SecItemAdd((CFDictionaryRef)attributes, NULL); return (int)err; } else { return (int)err; } } // 指定したキーで値を取得する関数 // - param // - dataType: 値を取得する際に用いるキー // - return // - キーに紐づく値、存在しなければ空文字が返却される char* getItem(const char *dataType) { NSMutableDictionary* query = [NSMutableDictionary dictionary]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; CFDataRef cfresult = NULL; OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef*)&cfresult); if (err == noErr) { NSData* passwordData = (__bridge_transfer NSData *)cfresult; const char* value = [[[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding] UTF8String]; char *str = strdup(value); return str; } else { return NULL; } } // 指定したキーで値を削除する関数 // - param // - dataType: 値を削除する際に用いるキー // - return // - 保存時のステータスコードを返却する (0 以外は失敗) int deleteItem(const char *dataType) { NSMutableDictionary* query = [NSMutableDictionary dictionary]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; OSStatus err = SecItemDelete((CFDictionaryRef)query); if (err == noErr) { return 0; } else { return (int)err; } } } </code></pre> <p><code>Keychain Services</code> は <code>Security</code> フレームワークを利用するため、<strong><code>KeychainService.mm</code> に対して <code>Security</code> フレームワークの依存関係を設定する必要があります。</strong></p> <p><img src="https://i.gyazo.com/ba82aaced24b83b37bf8c63e1ee7142f.png" alt="<code>KeychainService.mm</code> で <code>Security</code> フレームワークの利用を可能にする" /><br /> <strong><code>KeychainService.mm</code> で <code>Security</code> フレームワークの利用を可能にする</strong></p> <p>その後、上記を Unity スクリプトから実行可能にするための C# クラスを作成します。本記事ではファイルを <code>Assets/Scripts/KeychainService.cs</code> に配置します。</p> <pre><code class="csharp">// Assets/Scripts/KeychainService.cs using System.Runtime.InteropServices; /// <summary> /// 実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> /// </remarks> class KeychainService { #if UNITY_IOS [DllImport("__Internal")] private static extern int addItem(string dataType, string value); [DllImport("__Internal")] private static extern string getItem(string dataType); [DllImport("__Internal")] private static extern int deleteItem(string dataType); #endif public bool Put(string key, string value) { #if UNITY_IOS // 返却されるステータスが 0 なら成功 return addItem(key, value) == 0; #endif } public string Get(string key) { #if UNITY_IOS return getItem(key); #else return null; #endif } public bool Delete(string key) { #if UNITY_IOS // 返却されるステータスが 0 なら成功 return deleteItem(key) == 0; #endif } } </code></pre> <p>あとは用途に応じて下記のようなコードで設定値の保存や取得などを行えます。</p> <pre><code class="csharp">// ... var _keychainService = new KeychainService(); // name をキーとして値を nikaera で保存する _keychainService.Put("name", "nikaera"); // name をキーとして値を取得する var name = _keychainService.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する _keychainService.Delete("name"); // ... </code></pre> <h1 id="(余談) インターフェースで iOS/Android のふるまいを共通化する"><a href="#%28%E4%BD%99%E8%AB%87%29+%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%81%A7+iOS%2FAndroid+%E3%81%AE%E3%81%B5%E3%82%8B%E3%81%BE%E3%81%84%E3%82%92%E5%85%B1%E9%80%9A%E5%8C%96%E3%81%99%E3%82%8B">(余談) インターフェースで iOS/Android のふるまいを共通化する</a></h1> <p>このままだとプラットフォームを切り替える毎にコードを書き直さないとならないので、インターフェースを利用して共通化を行います。</p> <pre><code class="csharp">public interface ISecretManager { /// <summary> /// 指定したキーで値を保存する関数 /// </summary> /// <param name="key">キー</param> /// <param name="value">値</param> /// <returns>保存に成功したかどうか</returns> bool Put(string key, string value); /// <summary> /// 指定したキーの値を取得する関数 /// </summary> /// <param name="key">キー</param> /// <returns>指定したキーで設定された値、無ければ null</returns> string Get(string key); /// <summary> /// 指定したキーの値を削除する関数 /// </summary> /// <param name="key">キー</param> /// <returns>削除に成功したかどうか</returns> bool Delete(string key); } </code></pre> <p>その後、<code>Assets/Scripts/EncryptedSharedPreferences.cs</code> および <code>Assets/Scripts/KeychainService.cs</code> を下記の通り <code>ISecretManager</code> の実装に紐付けます。</p> <pre><code class="csharp">// Assets/Scripts/EncryptedSharedPreferences.cs using UnityEngine; /// <summary> /// 利用するネイティブコードは <c>Assets/Plugins/Android/SecretManager.java</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> /// </remarks> class EncryptedSharedPreferences: ISecretManager { private readonly AndroidJavaObject _secretManager; public EncryptedSharedPreferences() { var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer") .GetStatic<AndroidJavaObject>("currentActivity"); var context = activity.Call<AndroidJavaObject>("getApplicationContext"); _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context); } #region ISecretManager public bool Put(string key, string value) { return _secretManager.Call<bool>("put", key, value); } public string Get(string key) { return _secretManager.Call<string>("get", key); } public bool Delete(string key) { return _secretManager.Call<bool>("delete", key); } #endregion } </code></pre> <pre><code class="csharp">// Assets/Scripts/KeychainService.cs using System.Runtime.InteropServices; /// <summary> /// 実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> /// </remarks> class KeychainService: ISecretManager { #if UNITY_IOS [DllImport("__Internal")] private static extern int addItem(string dataType, string value); [DllImport("__Internal")] private static extern string getItem(string dataType); [DllImport("__Internal")] private static extern int deleteItem(string dataType); #endif // KeychainService.mm に定義した関数を呼び出す #region ISecretManager public bool Put(string key, string value) { #if UNITY_IOS return addItem(key, value) == 0; #else return false; #endif } public string Get(string key) { #if UNITY_IOS return getItem(key); #else return null; #endif } public bool Delete(string key) { #if UNITY_IOS return deleteItem(key) == 0; #else return false; #endif } #endregion } </code></pre> <p>あとは上記をよしなに利用可能な <code>SecretManager</code> クラスを作成します。</p> <pre><code class="csharp">// Assets/Scripts/SecretManager.cs using UnityEngine; /// <summary> /// <em>Editor 利用時のみ PlayerPrefs を利用する</em> /// </summary> /// <remarks><see cref="KeychainService" />, <see cref="EncryptedSharedPreferences" /></remarks> public static class SecretManager { #if UNITY_EDITOR #elif UNITY_ANDROID private static ISecretManager _instance = new EncryptedSharedPreferences(); #elif UNITY_IOS private static ISecretManager _instance = new KeychainService(); #endif public static bool Put(string key, string value) { #if UNITY_EDITOR PlayerPrefs.SetString(key, value); PlayerPrefs.Save(); return true; #elif UNITY_IOS || UNITY_ANDROID return _instance.Put(key, value); #else Debug.Log("Not Implemented."); return false; #endif } public static string Get(string key) { #if UNITY_EDITOR return PlayerPrefs.GetString(key); #elif UNITY_IOS || UNITY_ANDROID return _instance.Get(key); #else Debug.Log("Not Implemented."); return null; #endif } public static bool Delete(string key) { #if UNITY_EDITOR PlayerPrefs.DeleteKey(key); PlayerPrefs.Save(); return true; #elif UNITY_IOS || UNITY_ANDROID return _instance.Delete(key); #else Debug.Log("Not Implemented."); return false; #endif } } </code></pre> <p>これでプラットフォーム間の実装差異を気にすることなく、下記のような記述で設定値の保存や取得などを行えます。<strong>iOS/Android 以外のプラットフォームで追加実装したい場合は<a target="_blank" rel="nofollow noopener" href="https://docs.unity3d.com/ja/2021.1/Manual/PlatformDependentCompilation.html">プラットフォーム依存コンパイル</a>と <code>ISecretManager</code> の実装クラスを新たに作成することで簡単に追加できます。</strong></p> <pre><code class="csharp">// ... // name をキーとして値を nikaera で保存する SecretManager.Put("name", "nikaera"); // name をキーとして値を取得する var name = SecretManager.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する SecretManager.Delete("name"); // ... </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>今回は iOS/Android で設定値をセキュアに扱うための方法についてまとめてみました。実際は <code>Keychain Services</code> 周りは実装が大変なので、<code>External Dependency Manager for Unity</code> とか使って <a target="_blank" rel="nofollow noopener" href="https://github.com/kishikawakatsumi/KeychainAccess">KeychainAccess</a> のような外部ライブラリを利用する構成のほうが良いと思われます。</p> <p>本記事の内容に誤りがあったり、実際にはセキュアな実装ができていない等々あれば是非コメントでご指摘いただけますと幸いです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/topic/security/data?hl=ja">Android デベロッパー  |  Android Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences?hl=ja">EncryptedSharedPreferences  |  Android デベロッパー  |  Android Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services | Apple Developer Documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masaki_shoji/items/6c512c7ebb30a13cda1d">SharedPreferences を自前で難読化するのはもう古い?これからは EncryptedSharedPrefenreces を使おう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sachiko-kame/items/261d42c57207e4b7002a">iOS のキーチェーンについて - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nyhk-oi/items/189236d0627d43e7d658">Unity で IOS にセキュアに値を保存するには KeyChain を使おう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver">googlesamples/unity-jar-resolver: Unity plugin which resolves Android & iOS dependencies and performs version management</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>逆に <code>External Dependency Manager for Unity</code> を利用する方法のメリットは、UnityPackage などでライブラリとして配布する際に、ライブラリを動作させるのに必要な外部パッケージも同梱した状態で配布が可能になるなどがあります。(当然ライセンスには気を付ける必要がありますが...) <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>パッケージの依存関係を自動で解決するかどうかという選択肢になります。本記事では明示的に Resolve を実行するため <code>Disable</code> でも <code>Enable</code> でも進行上の問題はありません。 <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:3" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://cocoapods.org/">CocoaPods</a> もサポートされているようなので、iOS でも Android 同様、外部ライブラリを取り込むのは簡単にできそうでした。例えば <a target="_blank" rel="nofollow noopener" href="https://github.com/kishikawakatsumi/KeychainAccess">KeychainAccess</a> とか使いたい。 <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/16862 2021-04-19T23:32:28+09:00 2021-04-19T23:32:28+09:00 https://crieit.net/posts/AVMutableComposition-insertTimeRange AVMutableCompositionに音声をinsertTimeRangeして死ぬ時 <p>AVMutableCompositionで動画に音声をのせようと思ったときのこと。BGMの長さのほうが動画より短い場合にBGMをループさせようと思ったところ、どうも死んでしまったらしい。</p> <p>insertTimeRangeしている時はまだ死なず、合成のセッションが進んでいる時に死ぬので気づきづらい。いつのまにか <code>figAssetExportSession_CopyProperty signalled err=-16979</code> とか <code>figAssetExportSession_updateProgress</code> とかのエラーログが残っている。</p> <p>ためしにループの最後を削ってみたら成功したため、どうも最後のループの切り取り方がまずくはみだしているかなんかでエラーになっているような感じがした。</p> <p>下記参考URLを見ると、どうも同じ現象が発生していたようで下の方に修正すべき書き込みがあった。試してみたらうまく行った。</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/20211694/loop-avmutablecompositiontrack">https://stackoverflow.com/questions/20211694/loop-avmutablecompositiontrack</a></p> <pre><code>audioDuration = CMTimeSubtract(totalDuration,videoDuration); to: audioDuration = CMTimeSubtract(videoDuration,currentTime); </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16821 2021-04-06T23:16:07+09:00 2021-04-06T23:18:28+09:00 https://crieit.net/posts/Flutter-url-launcher-App-Store-URL Flutterのurl_launcherでApp StoreのURLを開くとフリーズする問題 <p>Flutterで作ったアプリ内で別のアプリに送客をしようと思い、url_launcherでApp StoreのアプリのURLを開くことにしました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://pub.dev/packages/url_launcher">https://pub.dev/packages/url_launcher</a></p> <p>テストしてみると特に問題ない……と思いきや、どうもストアの画面からアプリに戻ってみるとアプリがフリーズしていて動かない…!!! Android側は問題ないのですが、iOS側だけ。</p> <p>調べてみると完全にフリーズしているわけではなく、画面の描画だけがフリーズしており、ボタンは押した時のアニメーションが途中で止まったまま。しかも操作をするとログが流れていくので描画は止まっているけどアプリ自体は動いていて画面遷移などボタンのタップなどは動いている状態のようです。</p> <p>なぜか全然分からず、とにかくタップ時に遷移するのがまずいのかと思い、 <code>Future.delayed</code> をしてみたり、launchを呼び出す時にどうせURLを開くだけだからとawaitを省略していたのを追記してみたりしましたがそれでも全然うまくいきません。</p> <p>多分普通のURLだと大丈夫だと思うのですが、App StoreのURLはダメっぽいようです。Schemeを使って開いたら良いのかな、とも思いましたが、どうもSchemeはちょいちょい変わったりURLが古いっぽかったりして全然安心できなかったので、ひとまず使わない方向で考えていくことにしました。</p> <h2 id="解決方法"><a href="#%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95">解決方法</a></h2> <p>そしてついに解決法を見つけました。大変でした……。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16090 2020-10-01T23:48:46+09:00 2020-10-01T23:55:33+09:00 https://crieit.net/posts/Godot-In-App-Purchase GodotのIn App Purchaseをコールバック形式にしてみるテスト <p>GodotのiOSのIn App Purchase周りは本体に実装されているのだが、これがかなり狂った仕様になっている。コールバックで値を返す代わりに、1秒後に結果を取得するためのメソッドを呼んで取得してくれ、というかなり意味不明な仕様。</p> <p>実際どれくらいに呼べばいいかもわからないし、通信の問題で遅延など発生したら多分取得できないので再度呼ばなきゃいけなかったりとか、そもそも値が帰ってこないパターンはいつまでリトライ繰り返せばいいかもわからないのでかなり厳しすぎる。というか、正直使い物にならない気がする。</p> <p>ということで試しにコールバック形式に変更してみている。</p> <p>Godotのバージョンアップを考えると本来は直接コードをいじることは望ましくないので、一通り完成して実運用できるようなったらモジュール化した方が良さそう。外部ライブラリを含めてのモジュール化をしたことがなく、そのあたりハマったら面倒そうなのでとりあえず直接いじっている。</p> <h2 id="request_product_info"><a href="#request_product_info">request_product_info</a></h2> <p>とりあえずrequest_product_infoを変更してみた。うまくいってるっぽい気がする。このコミット(超雑)。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/godot/commit/d554a4315924e8820fac7d0f992a2229c86dd0d0">https://github.com/dala00/godot/commit/d554a4315924e8820fac7d0f992a2229c86dd0d0</a></p> <h3 id="init"><a href="#init">init</a></h3> <p>とりあえずAdMobモジュールを参考にして、initメソッドをつけた。下記のようにして使う。</p> <pre><code class="python">_in_app_store.init(get_instance_id()) </code></pre> <p>GodotのNodeのインスタンスIDが分かればそれに対してコールバックを返すことができるっぽい。そのため、最初にこれでインスタンスIDを渡してシングルトンに保存しておくベースの処理。</p> <h3 id="処理結果を貯める代わりにコールバックする"><a href="#%E5%87%A6%E7%90%86%E7%B5%90%E6%9E%9C%E3%82%92%E8%B2%AF%E3%82%81%E3%82%8B%E4%BB%A3%E3%82%8F%E3%82%8A%E3%81%AB%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B">処理結果を貯める代わりにコールバックする</a></h3> <p>下記のコードの、コメントアウトしている部分で結果をためている。勘弁してほしい。その代わりに、GodotのNodeの_on_product_info_received` をcallする。</p> <pre><code class="objectivec"> // InAppStore::get_singleton()->_post_event(ret); int instanceId = InAppStore::get_singleton()->get_instance_id(); Object *obj = ObjectDB::get_instance(instanceId); obj->call_deferred("_on_product_info_received", ret); </code></pre> <h3 id="GodotのNode側"><a href="#Godot%E3%81%AENode%E5%81%B4">GodotのNode側</a></h3> <p>こんな感じ。</p> <pre><code class="python">func _ready(): if Engine.has_singleton("InAppStore"): _in_app_store = Engine.get_singleton("InAppStore") _in_app_store.init(get_instance_id()) var result = _in_app_store.request_product_info({"product_ids": ["ticket5"]}) func _on_product_info_received(data): print("_on_product_info_received") print(data) </code></pre> <p>今のところうまくかえってきているっぽい。</p> <pre><code>2020-10-01 23:12:57.362505+0900 ios-formation[2074:940291] _on_product_info_received 2020-10-01 23:12:57.362540+0900 ios-formation[2074:940291] {currency_codes:[JPY], descriptions:[バトルの報酬が5倍にアップします], </code></pre> <p>ひとまず一通りこの形に修正してみたい。</p> <p>(というか可能であればきれいに作ってそのまま本体に取り込まれたい……)</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16087 2020-10-01T08:12:39+09:00 2020-10-01T08:15:04+09:00 https://crieit.net/posts/Godot-iOS GodotのiOS用のモジュールを作った <p>GodotでiOS用のモジュールを作りました。作ったのはSKStoreReviewControllerのrequestReviewを呼んでユーザーにレビューをしてもらうポップアップを出すだけのモジュールです。</p> <h2 id="作り方"><a href="#%E4%BD%9C%E3%82%8A%E6%96%B9">作り方</a></h2> <p>作り方としては、元々iOS用のモジュールとして、AdMobのモジュールがしっかり動いているのは知っていたので、それを参考にしました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/kloder-games/godot-admob">kloder-games/godot-admob: Module Admob for Godot engine</a></p> <p>このリポジトリのGodotのモジュールフォルダにコピーするフォルダがあるのですが、それをコピーして改造していきました。</p> <p>ちなみに完成品はこちらです。動けばOKという感じでやっているので中身の理解はしていません。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/godot-store-review">dala00/godot-store-review: Godot store review module for iOS.</a></p> <p>libフォルダは今回は不要ですので使っていません。外部ライブラリを使う場合は参考にすると良いと思います。</p> <h3 id="最低限必要なファイル"><a href="#%E6%9C%80%E4%BD%8E%E9%99%90%E5%BF%85%E8%A6%81%E3%81%AA%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB">最低限必要なファイル</a></h3> <p>今回利用した最低限必要なファイルは下記です。</p> <ul> <li>SCsub</li> <li>config.py</li> <li>register_types.cpp</li> <li>register_types.h</li> <li>ios/src/godotAdmob.h</li> <li>ios/src/godotAdmob.mm</li> </ul> <p>これらのファイル名や中身のadmobという名称部分を、独自のものに置き換えていきます。</p> <h3 id="対応する内容"><a href="#%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B%E5%86%85%E5%AE%B9">対応する内容</a></h3> <p>不要なプロパティやメソッドを削っていきます。今回はrequestReviewを呼ぶメソッドだけがあればいいので、下記のメソッドを作って後は削除しました。</p> <pre><code class="objectivec">void GodotStoreReview::requestReview() { NSLog(@"Calling requestReview"); if (instance != this) { NSLog(@"GodotStoreReview Module dublicate singleton"); return; } if (@available(iOS 10.3, *)) { if ([SKStoreReviewController class]) { [SKStoreReviewController requestReview]; } else { NSLog(@"SKStoreReviewController not found"); } } else { NSLog(@"SKStoreReviewController not found for this version"); } } </code></pre> <p>バージョンの判定をしているのでごちゃごちゃしていますが、基本的にはinstanceの有無をチェックした後はシンプルに処理を書くだけで大丈夫です。</p> <p>あとは_bind_methodsの中身も書き換え、作ったメソッドをGodotと連携させるための宣言をしておきます。</p> <pre><code class="objectivec">void GodotStoreReview::_bind_methods() { CLASS_DB::bind_method("requestReview", &GodotStoreReview::requestReview); } </code></pre> <p>基本的には以上です。あとはGodotをビルドして実際に試すだけです。Godot側では下記のような感じで実行できるようになります。</p> <pre><code class="python">if Engine.has_singleton("StoreReview"): _store_review = Engine.get_singleton("StoreReview") _store_review.requestReview() </code></pre> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <p>今回はやっていませんが、コールバックもできます。AdMobモジュールでも実装されていますので、必要な方は参考にできます。</p> <p>逆に今回と同様のシンプルなモジュールを作りたい方は今回僕が作ったものを参考にしていただくと簡単そうです。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16057 2020-09-15T09:56:28+09:00 2020-09-15T09:56:28+09:00 https://crieit.net/posts/Godot-file-not-found Godotでプラグインと一緒にビルドするとfile not foundとなる場合 <p>iOSでAdmobプラグインを入れてGodotをビルドしようとしたら下記のようなエラーが出た。</p> <pre><code>object_type_db.h file not found </code></pre> <p>そんな……と思って該当のソースを見てみる。</p> <pre><code class="c">#if VERSION_MAJOR == 3 #include <core/class_db.h> #include <core/engine.h> #else #include "object_type_db.h" #include "core/globals.h" #endif </code></pre> <p>VERSION_MAJORが3の時はそもそも使われていないようなので、つまりGodot側のバージョン指定がうまくいっていない……? と思い今度はGodot側を調べる。</p> <p>Godotにversion.pyがあるので見てみると</p> <pre><code class="python">major = 4 </code></pre> <p>つまりGodot側のソースコードはつまりもう4になっているので、Admob側のバージョンが合っていなくて正常にビルドできていなかったようだ。</p> <p>ということで、Godotには3.2というブランチがあるのでそちらにswitchしてビルドした。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15995 2020-07-03T14:57:19+09:00 2020-07-03T14:57:19+09:00 https://crieit.net/posts/Flutter-IdeaShuffleMemo 新作Flutterアプリ「IdeaShuffleMemo」を広めるために考えている戦略 <p><a href="https://crieit.now.sh/upload_images/0c5772e78aa0f279ed29c9e177857c365efec751c86bc.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0c5772e78aa0f279ed29c9e177857c365efec751c86bc.jpg?mw=700" alt="image" /></a><br /> こんにちは、YuKiOです。</p> <p>7月にFlutterアプリ開発を始めて2作目のアプリ、アイデアを発想するためのメモアプリ「IdeaShuffleMemo」をリリースしました。</p> <p>■AppStore<br /> <a target="_blank" rel="nofollow noopener" href="https://apps.apple.com/jp/app/id1517535550">https://apps.apple.com/jp/app/id1517535550</a></p> <p>■Google Play<br /> <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja">https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja</a></p> <p>1作目はとりあえずリリースすることを目的に作成しましたが、今回は「自分が欲しいもの」というテーマで作成しました。</p> <p>さらに今後のアプリ開発を含めて、機能やアプリの露出方法についても挑戦的に戦略を考えています。</p> <p>このアプリのトライアンドエラーの経験を、次回につなげていければと思っています。</p> <p>機能については、Qiitaに書いています。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e">https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e</a></p> <p>crieitでは、どうやって露出させていくか?考えていることを、書いていきたいと思います。</p> <h3 id="全体の戦略"><a href="#%E5%85%A8%E4%BD%93%E3%81%AE%E6%88%A6%E7%95%A5">全体の戦略</a></h3> <h4 id="アプリの収益化"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%8F%8E%E7%9B%8A%E5%8C%96">アプリの収益化</a></h4> <p>せっかくアプリ開発をするのだから、お金を稼げるようになりたいです。なので、収益を得る方法として、「広告」と「サブスクリプション」の2つを用意しました。</p> <p>サブスクリプションも採用した理由は、コロナの影響もあり「広告」落ち込んでいたこと、広告収入という方法が今後変化する可能性が高く、安定したものではないからです。</p> <p>単発の課金のほうが難易度が探るかもしれませんが、長期的にみて、開発の継続、安定した収益を目指す上ではサブスクリプションがいいと判断しました。</p> <p>最終的に「お金を払ってでも使ってもらえるものを作る」というのが最終的な目標もあり、かなり苦労しましたがサブスクリプション機能を追加しました。</p> <h4 id="アプリの多言語化"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%8C%96">アプリの多言語化</a></h4> <p>日本のアプリ市場は世界的に見ても大きいほうだとは思います。<br /> ただ、1国で月1万円稼げるレベルのアプリだったとして、日本だけにリリースすれば、1万円しか稼げません。もし全世界に配信することが出来れば、市場も広がるので、稼げるチャンスは増えるはず。(そこまで単純なものではないと思いますが)</p> <p>なのでアプリは、最初から多言語化を意識して作りました。<br /> 初めから7カ国ご対応は辛いので、英語と日本語の2つに対応をさせました。</p> <p>それに合わせて、アプリのキャプチャーや紹介文も英語で用意も。</p> <p>英語圏でも反応が少しでれば、クラウドワークスなどを使って、ネイティブにPRできるような文章を作り込んでいきながら、対応できる言語を増やしていくつもりです。</p> <p>まずは英語と日本語圏で収益を出せるかで、継続の判断をしていきたいと思います。</p> <h4 id="アプリのターゲット"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88">アプリのターゲット</a></h4> <p>アプリのターゲットユーザーは、主にアプリやサービス開発者、ビジネスマン、経営者、起業を考えている人です。<br /> まずは、自分と近い範囲のアプリやサービスの開発者に広めたいなと思っています。</p> <h3 id="アプリの具体的な露出戦略"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AA%E9%9C%B2%E5%87%BA%E6%88%A6%E7%95%A5">アプリの具体的な露出戦略</a></h3> <h4 id="■国内での露出"><a href="#%E2%96%A0%E5%9B%BD%E5%86%85%E3%81%A7%E3%81%AE%E9%9C%B2%E5%87%BA">■国内での露出</a></h4> <h5 id="・ツイッター"><a href="#%E3%83%BB%E3%83%84%E3%82%A4%E3%83%83%E3%82%BF%E3%83%BC">・ツイッター</a></h5> <p>まずは自身のツイッターアカウントで告知します。ただこれは、Webサービスの時と同様あまり期待はしていません。フォロワー数が少ない、インフルエンサーでもないので、よほどインパクトがあるサービスでなければ、「リリースしました」のような広告っぽい投稿は敬遠されて反応がでません。</p> <p>ただ少しでも反応を出すために、審査の間でPR動画を作ってみました。(Appleぽさを意識しながら、最後はメタルギアを意識している裏エピソードがあります。)</p> <p>カメラなど映像も趣味なので、別の趣味がいかせて良かったです。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Webサービスやアプリ!斬新なアイデアをひらめきたい人に!アイデアを発想するためのメモアプリ✍️IdeaShuffleMemoをリリースしました!■AppStore<a target="_blank" rel="nofollow noopener" href="https://t.co/gEPzEEJ7mt">https://t.co/gEPzEEJ7mt</a>■Google Play<a target="_blank" rel="nofollow noopener" href="https://t.co/w0vTiOanGE">https://t.co/w0vTiOanGE</a>使ってみてください😁<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&ref_src=twsrc%5Etfw">#プログラミング</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/gQ0dOMNgGB">pic.twitter.com/gQ0dOMNgGB</a></p>— YuKiO | 個人開発&Flutter (@oo_forward) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/oo_forward/status/1278611399749431296?ref_src=twsrc%5Etfw">July 2, 2020</a></blockquote> <p>「#駆けだしエンジニアと繋がりたい」「#プログラミング初心者」のタグは、タグ検索をしているユーザーが多いのでつけています。</p> <p>ただ、特に内容を見ずにいいねを押しているユーザーもいるので、いいねの割りには反応は薄かったりします。</p> <p>まあ、固定ツイートなどの時の見た目として、いいねとリツイートが多い方がいいので、この方法を採用しました。</p> <h5 id="・Qiitaに投稿"><a href="#%E3%83%BBQiita%E3%81%AB%E6%8A%95%E7%A8%BF">・Qiitaに投稿</a></h5> <p>これはアプリの露出と、アプリ制作でお世話になった数多くの方々への恩返し、自分自身への備忘録&技術の精査の意味も含まれていますが、Qiitaに技術記事がメインで、説明の一貫としてアプリの紹介をするのは問題ないかなと思ってます。</p> <p>第1弾は、アプリ開発で使ったパッケージを紹介、それ以降はパスワードロック、データのバックアップ、サブスクリプションなどの、Flutter開発で情報が少なくて困った内容を書いていこうと考えています。</p> <p>■第1段「おすすめのパッケージ」<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e">https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e</a></p> <p>おかげ様で、現時点で1100viewにはなっているので、それなりに露出としては成功しているかなと思います。</p> <h5 id="・Crieit"><a href="#%E3%83%BBCrieit">・Crieit</a></h5> <p>もちろん開発者のコミュニティであるこちらにも投稿。他が技術的な内容になっているので、ここではこのようにアプリを露出させる方法として、内容ががぶらないようにしています。</p> <h5 id="・自分のブログサイト"><a href="#%E3%83%BB%E8%87%AA%E5%88%86%E3%81%AE%E3%83%96%E3%83%AD%E3%82%B0%E3%82%B5%E3%82%A4%E3%83%88">・自分のブログサイト</a></h5> <p>自分のブログ記事は、アプリの紹介ページのような位置付けで書いています。<br /> 今後はアプリのQandAなどを追加して、情報を充実させていきたいと思います。</p> <p>予算とアプリの反響によっては、こちらも多言語対応できれば理想ですね。</p> <h5 id="・プレスリリース"><a href="#%E3%83%BB%E3%83%97%E3%83%AC%E3%82%B9%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9">・プレスリリース</a></h5> <p>Webサービスではやったことがないのですが、プレスリリースにも挑戦しています。プレスリリースの難しいところは、個人を受け付けてなかったり、有料で、さらに1回に数万円と高額な場合があったりと、個人開発レベルでは手が出しづらい所です。</p> <p>本業ではプレスリリースを使っているのですが、無料の場合はやはり無料レベルに留まることがおおく、あまり期待はできません。</p> <p>ただ、今回は1アカウントお試して無料でできる、value pressさんを使いました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.value-press.com/">https://www.value-press.com/</a></p> <p>無料では配信できるメディアなど限られますが、有名なメディアや、ターゲットが好みそうなメディアも多いのが選んだ理由です。</p> <p>ただ、記者は開発者ではないので、アピールする部分を少しヅラすことを意識しました。</p> <p>ターゲットを、コロナ禍で厳しい環境にたっていて、何か変えたいと必死で考えている人向けとすることにして、コロナとは言葉に出していませんが、プレスリリースタイトルは、それを強くい意識した内容になっています。</p> <p>ここで少しでも反応がでれば、ある程度お金をかけてもいいかなと思っています。</p> <p>個人での注意点ですが、電話やメールアドレスが公開されます。勧誘メールが届くようになったりと、色々大変です。</p> <p>公開しても良いメールアドレスや、格安のIP電話を契約して、それを公開するようにしましょう。</p> <h5 id="・ツイッター・フェイスブック広告"><a href="#%E3%83%BB%E3%83%84%E3%82%A4%E3%83%83%E3%82%BF%E3%83%BC%E3%83%BB%E3%83%95%E3%82%A7%E3%82%A4%E3%82%B9%E3%83%96%E3%83%83%E3%82%AF%E5%BA%83%E5%91%8A">・ツイッター・フェイスブック広告</a></h5> <p>これはちょっと迷ってます。今回は実験ですが、それなりに広告費用をかけてみようと思います。<br /> これまでもツイッター広告をつかっているのですが、あまりパフォーマンスがよくありません。</p> <p>もしやるとしたらフェイスブック広告を出してみようかなと思っています。</p> <p>ブログ記事であったり、いくつかの反応を見てから、実行する予定です。</p> <h4 id="■海外戦略"><a href="#%E2%96%A0%E6%B5%B7%E5%A4%96%E6%88%A6%E7%95%A5">■海外戦略</a></h4> <h5 id="・Div"><a href="#%E3%83%BBDiv">・Div</a></h5> <p>海外のQiitaのようなサイトを探してみたのですが、stackoverflowが紹介されていましたが、なんとなく質問コーナーという感じで、技術ブログのようなものではありませんでした。</p> <p>Divはそこそこライトな感じの投稿が多そうだったので、こちらに投稿してみました。<br /> 英語は得意ではありませんが、Google翻訳を活用しながら、自分で書いたQiitaの記事を抜き出し、翻訳しました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://dev.to/oo_forward/flutter-recommended-package-used-in-the-developed-memo-application-15dl">https://dev.to/oo_forward/flutter-recommended-package-used-in-the-developed-memo-application-15dl</a></p> <p>Flutterに記事や、iOSの記事についてそこまでいいねがされていたりする記事がなかったり、どれぐらい動いているのか分かりませんが、2時間まえくらいに投稿した時点で、2件くらい「いいね!」がもらえているので、動いているのは動いているっぽいです。</p> <h5 id="・producthunt"><a href="#%E3%83%BBproducthunt">・producthunt</a></h5> <p><a href="https://crieit.net/posts/7">https://crieit.net/posts/7</a><br /> この記事で紹介さていたのでしりました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.producthunt.com/founder-club/benefits">https://www.producthunt.com/founder-club/benefits</a></p> <p>仕組みがよくわかってないのですが、プロダクトを紹介するというサイトのようです。<br /> 紹介にあたってYoutubeの紹介動画が必要ぽいので、せっかくなら海外用のPV動画も制作して乗っけようと思っています。</p> <p>正直あまりよくわかってない感じですが.</p> <h5 id="・Hacker News"><a href="#%E3%83%BBHacker+News">・Hacker News</a></h5> <p>上記の記事でproducthuntとの連携で紹介されていました。<br /> まだ深くは理解できてないですが、このあたりも挑戦していきたいなと思います。</p> <h5 id="・Medium"><a href="#%E3%83%BBMedium">・Medium</a></h5> <p>Flutter初めてからみるようになったのですが、技術系のブログのようですね。<br /> かなり膨大な投稿量らしいので、Qiitaのよう書いたからといってすぐに反応がでなさそうですが、起点として考えた方がいいなと思います。</p> <p>どんな内容で買えば良いか、ちょっと考えています。</p> <p>せっかく英語で書くなら、日本人として特色も入れられないかなと考えています。</p> <h4 id="■ASO"><a href="#%E2%96%A0ASO">■ASO</a></h4> <p>サイト検索順位ならぬ、アプリ検索順位の最適化ASOについて。<br /> 正直、軽くしか調べてられてませんが、リリースした直後は、評価も何もないので、ダウンロードしてくれそうなユーザーが検索しそうなキーワードを入れるくらいしか、できないのではないかと考えています。</p> <p>SEOと同じ観点でユーザー主義でいくのであれば、ストア内の内容だけは順位が関係しないと勝手に思ってます。</p> <p>例えば、</p> <ul> <li>アプリの有料化率</li> <li>外部リンクからのアクセス数</li> <li>多言語への対応</li> <li>アプリの定着率など。</li> </ul> <p>AppleやGoogleどちらも、アプリのサブスクリプションが長く続く方がいいわけなので、結局SEO同様にアプリをよくしていくこと以外に方法はないと思っています。</p> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <p>今回人生で2回目のアプリをリリースしました。アプリ開発って、アプリのコードを打ち込んで組むだけだと思いがちですが、本気でPRをやろうと思うとやることいっぱいだなと実感しています。個人開発の大変さも実感しています。</p> <p>この方法がうまくいくとも分かりませんし、アプリ自体に需要がなければ、どんなにPRしても難しいと思います。</p> <p>人事を尽くして天命を待つ</p> <p>これも一つの実験で、うまくいかなければ別の方法を試していきたいと思います。</p> <p>ツイッターもやってますので、ぜひフォローください。<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/oo_forward">https://twitter.com/oo_forward</a></p> <p>■AppStore<br /> <a target="_blank" rel="nofollow noopener" href="https://apps.apple.com/jp/app/id1517535550">https://apps.apple.com/jp/app/id1517535550</a></p> <p>■Google Play<br /> <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja">https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja</a></p> YuKiO | 個人開発&Flutter学習中 tag:crieit.net,2005:PublicArticle/15891 2020-05-09T11:07:36+09:00 2020-05-09T11:07:36+09:00 https://crieit.net/posts/Flutter-iOS FlutterでiOSアプリが実行できない場合 <p>Flutterで作ったアプリをiOSでデバッグしたい場合にflutter runしても動かない場合がある。特にまだ1回も動かしたことがない場合など。</p> <p>この場合はまずXcodeで開き、Signingの設定をしておく。これで実行できる基本的な設定が整う。多分Xcodeではまだ動かない。</p> <p>そして再度flutter runを実行する。ここでPodsがインストールされたりなんだかんだで最終的に実行できる。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15886 2020-05-06T00:18:18+09:00 2020-06-19T22:11:13+09:00 https://crieit.net/posts/Godot-AdMob-iOS GodotでAdMobを使う - iOSの場合 <p>GodotでiOSアプリにAdMobを表示する方法。Androidの場合は下記。</p> <p><a href="https://crieit.net/posts/Godot-AdMob-Android">GodotでAdMobを使う - Androidの場合</a></p> <p>実際にやったのはGodot3.2。Androidの場合とは違い、iOSの場合は自分でGodotのテンプレートをビルドしなければならないのでちょっと面倒くさい。</p> <h2 id="AdMobモジュールの取得"><a href="#AdMob%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%81%AE%E5%8F%96%E5%BE%97">AdMobモジュールの取得</a></h2> <p>AdMobモジュールは下記。まずは下記のリポジトリをcloneしておく。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/kloder-games/godot-admob">https://github.com/kloder-games/godot-admob</a></p> <p>基本的にやり方はこのREADMEに書かれている。そちらの How to use の iOS というところ。この話の前提として、Godotのリポジトリと組み合わせてビルドが必要なため、こちらもcloneしておく必要がある。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/godotengine/godot">https://github.com/godotengine/godot</a></p> <h3 id="AdMobモジュールをGodot側にコピーする"><a href="#AdMob%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92Godot%E5%81%B4%E3%81%AB%E3%82%B3%E3%83%94%E3%83%BC%E3%81%99%E3%82%8B">AdMobモジュールをGodot側にコピーする</a></h3> <p>まずはgodot-admobリポジトリのadmobフォルダを、godotリポジトリのmodulesフォルダにコピーする。</p> <h3 id="Google Mobile Ads SDKを配置する(新)"><a href="#Google+Mobile+Ads+SDK%E3%82%92%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B%EF%BC%88%E6%96%B0%EF%BC%89">Google Mobile Ads SDKを配置する(新)</a></h3> <p>2020/6にUIWebViewを含めたビルドではアップロードできなくなったため最新のSDKを利用できるようになった。README通りにやればよい(僕が書いてPR送った)。下記のファイルを <code>admob/ios/lib</code> に配置する。 <code>put_GoogleMobileAds_framework_here</code> のようなファイルがあるのでわかりやすい。</p> <ul> <li>GoogleMobileAds.framework</li> <li>GoogleAppMeasurement.framework</li> <li>GoogleUtilities.xcframework</li> <li>PromisesObjC.xcframework</li> <li>nanopb.xcframework</li> </ul> <h3 id="Google Mobile Ads SDKを配置する(旧)"><a href="#Google+Mobile+Ads+SDK%E3%82%92%E9%85%8D%E7%BD%AE%E3%81%99%E3%82%8B%EF%BC%88%E6%97%A7%EF%BC%89">Google Mobile Ads SDKを配置する(旧)</a></h3> <p>追記)UIWebView排除のため、既に最新のSDKを使えるようになっているため最新バージョンではこれは不要。(追記終わり)</p> <p>次に公式のGoogle Mobile Ads SDKを配置する。現時点でのバージョン指定は7.41.0以下。ちなみにこのバージョンはもう公式ではダウンロードできないので、CocoaPodsを利用して取得した。</p> <pre><code class="ruby">platform :ios, '9.0' pod 'Google-Mobile-Ads-SDK', '7.41.0' </code></pre> <p>上記のPodfileを作って <code>pod install</code>。</p> <p><code>admob/ios/lib</code> に配置しろと書かれているが、これは先程godot側にコピーした方のフォルダ。godot側でビルドして使うため。<code>put_GoogleMobileAds_framework_here</code> と書かれたフォルダがあるので、そこにGoogleMobileAds.frameworkフォルダをコピーしてくる。</p> <h3 id="Godotをビルドする"><a href="#Godot%E3%82%92%E3%83%93%E3%83%AB%E3%83%89%E3%81%99%E3%82%8B">Godotをビルドする</a></h3> <p>これで準備ができたので、godotプロジェクト側をビルドする。詳しくはREADMEに貼られている下記リンクの通り。</p> <p><a target="_blank" rel="nofollow noopener" href="http://docs.godotengine.org/en/stable/development/compiling/compiling_for_ios.html">http://docs.godotengine.org/en/stable/development/compiling/compiling_for_ios.html</a></p> <h4 id="sconsをインストールしてビルド"><a href="#scons%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%97%E3%81%A6%E3%83%93%E3%83%AB%E3%83%89">sconsをインストールしてビルド</a></h4> <p>ビルドにはSConsというビルドツールを利用する。これはbrewでインストールできる。</p> <pre><code>brew install scons </code></pre> <p>ビルド方法は書かれている通り、下記の必要な方。</p> <p>開発用</p> <pre><code>scons p=iphone target=debug </code></pre> <p>リリース用</p> <pre><code>scons p=iphone target=release </code></pre> <p>他にも、エミュレータ用や別のアーキテクチャの組み合わせなど、色々指定もできるためそれは必要であれば前述のリンクの解説を参考に。僕は上記の2つだけでやった。</p> <p>これでbinフォルダにaファイルが出来上がるので、これをXcodeプロジェクト側のトップにあるaファイルと差し替えれば完成。</p> <h5 id="エラーが出たパターン1"><a href="#%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E5%87%BA%E3%81%9F%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%EF%BC%91">エラーが出たパターン1</a></h5> <p>ちなみに下記のようなエラーが出る場合があった。</p> <pre><code>scons: Reading SConscript files ... xcrun: error: SDK "iphoneos" cannot be located xcrun: error: SDK "iphoneos" cannot be located xcrun: error: unable to lookup item 'Path' in SDK 'iphoneos' Failed to find SDK path while running xcrun --sdk iphoneos --show-sdk-path. </code></pre> <p>よくは分からないけど下記で改善できた。GitHubの方はいいねがいっぱいついてるコメントあたり。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/shouta-dev/items/a8e4914ec4d88cffac10">https://qiita.com/shouta-dev/items/a8e4914ec4d88cffac10</a><br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/facebook/react-native/issues/18408">https://github.com/facebook/react-native/issues/18408</a></p> <h5 id="エラーが出たパターン2"><a href="#%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E5%87%BA%E3%81%9F%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%EF%BC%92">エラーが出たパターン2</a></h5> <p>下記のようなエラーが出た。</p> <pre><code>scons: Reading SConscript files ... Checking for C header file mntent.h... (cached) no scons: done reading SConscript files. scons: Building targets ... [Initial build] Compiling ==> platform/iphone/godot_iphone.cpp In file included from platform/iphone/godot_iphone.cpp:33: platform/iphone/os_iphone.h:84:2: error: unknown type name 'VideoMode' [2] VideoMode video_mode; ^ platform/iphone/os_iphone.h:92:33: error: unknown type name 'VideoMode' [2] virtual Error initialize(const VideoMode &p_desired, int p_video_driver, int p_audio_driver); ^ platform/iphone/os_iphone.h:122:2: error: unknown type name 'InputDefault' [2] InputDefault *input; ^ platform/iphone/os_iphone.h:149:48: error: use of undeclared identifier 'InputDefault' [2] void joy_axis(int p_device, int p_axis, const InputDefault::JoyAxis &p_value); ^ platform/iphone/os_iphone.h:166:36: error: unknown type name 'VideoMode' [2] virtual void set_video_mode(const VideoMode &p_video_mode, int p_screen = 0); ^ platform/iphone/os_iphone.h:167:10: error: unknown type name 'VideoMode' [2] virtual VideoMode get_video_mode(int p_screen = 0) const; ^ platform/iphone/os_iphone.h:168:45: error: use of undeclared identifier 'VideoMode' [2] virtual void get_fullscreen_mode_list(List<VideoMode> *p_list, int p_screen = 0) const; ^ 7 errors generated. scons: *** [platform/iphone/godot_iphone.iphone.opt.arm64.o] Error 1 scons: building terminated because of errors. </code></pre> <p>Godot3.2にし、godotもgodot-admobも最新にしたらビルドできるようになった。</p> <p>他にも、iOSの場合はgodot-admobの不具合が原因でだめな場合があった。2020/4月末あたりに解決のPRがマージされたので、それ以降はうまく行った。とにかくうまく行かない場合はissueを見て、改善していれば最新に、不具合発生の真っ最中であればちょっとコミットを戻してみたりして調整したりしなければならない。かなり面倒くさい。僕も何ヶ月も放置していてようやく対処できた。</p> <h2 id="Xcodeの設定"><a href="#Xcode%E3%81%AE%E8%A8%AD%E5%AE%9A">Xcodeの設定</a></h2> <p>さきほどの5つのフレームワークをXcodeプロジェクト側にも追加する必要がある。Xcodeを開いたプロジェクトの中に参照で登録する。frameworkの読み込みパス設定に配置したフォルダの設定を追加する必要がある。xcframeworkの場合は直接Xcodeに登録するのではなく、中のios-armv7_arm64以下等にframeworkフォルダがあるのでそちらを利用する。パスの設定もそこ。</p> <h2 id="Godot側のコード"><a href="#Godot%E5%81%B4%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89">Godot側のコード</a></h2> <p>一応実際のコード。initのところでisRealを指定してあるので、デバッグのテスト広告コードを指定している部分は不要かもしれない。</p> <pre><code class="python">if Engine.has_singleton("AdMob"): admob = Engine.get_singleton("AdMob") if OS.get_name() == 'iOS': # 一般向け(if文とは関係ない) admob.init(!OS.is_debug_build(), get_instance_id()) else: # 子ども向けの例(if文とは関係ない) admob.initWithContentRating(!OS.is_debug_build(), get_instance_id(), true, 'G') if OS.is_debug_build(): admob.loadBanner('ca-app-pub-3940256099942544/6300978111', false) else: if OS.get_name() == 'iOS': admob.loadBanner('iOS用の本番コード', false) else: admob.loadBanner('Android用の本番コード', false) </code></pre> <h2 id="armv7のリンクエラー"><a href="#armv7%E3%81%AE%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%A8%E3%83%A9%E3%83%BC">armv7のリンクエラー</a></h2> <p>ずっとarmv7のリンクエラーが出ていて断念していた時期があるのだが、Godotエディタ側の設定にアーキテクチャの選択があるので、Xcodeプロジェクトをエクスポートする前にそれでarmv7を外しておいたほうが良いかもしれない。このあたりハマると非常に面倒。(これはAdMobすら関係なかったかもしれないが一応メモ)</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15565 2019-11-27T19:05:00+09:00 2019-11-27T19:05:00+09:00 https://crieit.net/posts/Code-sign-error-bundle-format-unrecognized-invalid-or-unsuitable Code sign error : bundle format unrecognized, invalid, or unsuitableの解決方法の一つ <p>Xcodeにて突然、タイトルのようにCodeSign failedというエラーが出てビルドできなくなった。</p> <p>調べてみるとDerivedDataを削除するとか、再起動するとかの情報が出てくるので試してみるが復旧しない。むちゃくちゃ急いでいるときだったので非常に焦った。もしかしてApple Store Connectの方で何か更新とかをしないと駄目なのか……と思ったが、だいぶ昔に解説を見様見真似でやった程度なので何も覚えていない。このタイミングで非常に絶望……。もう無理……。</p> <p>しかし、問題がではじめた前のコミットに戻してビルドしてみると動いた。どうも証明書とかの問題ではなさそう。しかし最新のコミットに戻すとやはり再発。</p> <p>わけも分からず調べていると下記のようなページを見つけた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/29271548/code-sign-error-bundle-format-unrecognized-invalid-or-unsuitable">ios - Code sign error : bundle format unrecognized, invalid, or unsuitable - Stack Overflow</a></p> <p>そこには「Xcode、Resourcesのことあんまり好きじゃないんだってさ……」</p> <p>は、まさか……。</p> <p>そう、最新のコミットではresourcesという名前でフォルダの参照をプロジェクトに追加していた。まさかと思い、半信半疑だったが名前をwwwにして再度フォルダの参照を追加してみてビルドしてみた。</p> <p>いけたああああ!!!!!!</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15130 2019-06-19T23:27:01+09:00 2019-06-19T23:30:43+09:00 https://crieit.net/posts/208f56095ca8e483481917ef0354ccf9 【禅Do】利用しているライブラリについて <h2 id="導入したライブラリについて"><a href="#%E5%B0%8E%E5%85%A5%E3%81%97%E3%81%9F%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">導入したライブラリについて</a></h2> <p>とりあえず当時、Carthageの情報が結構出ていて使いやすそうだったので<br /> 基本的にCarthageを利用してインストールする方針でやっていた。<br /> その中で対応していないものはCocoaPodsを利用するか、直接ソースを入れる形にした。</p> <h3 id="Carthageで入れたもの"><a href="#Carthage%E3%81%A7%E5%85%A5%E3%82%8C%E3%81%9F%E3%82%82%E3%81%AE">Carthageで入れたもの</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/malcommac/SwiftDate">SwiftDate</a> <ul> <li>リマインド機能の日付操作の為に導入</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/DeclarativeHub/Bond">Bond</a> <ul> <li>リアクティブプログラミング的なものを使ってみたかった為</li> <li>全体設計の見通しの良さ向上の為など</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/xmartlabs/Eureka">Eureka</a> <ul> <li>設定画面でリスト型テーブルやForm作成を簡単にするための導入</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/delba/Log">Log</a> <ul> <li>開発用途。デバッグのしやすさ向上の為</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/Quick/Quick">Quick</a> <ul> <li>開発用途。テストコードを書く為(なおちゃんとテストコード書いてない模様)</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/Quick/Nimble">Nimble</a> <ul> <li>開発用途。テストコードを書く為(なおちゃんとテストコード書いてない模様)</li> </ul></li> </ul> <h3 id="CocoaPodsで入れたもの"><a href="#CocoaPods%E3%81%A7%E5%85%A5%E3%82%8C%E3%81%9F%E3%82%82%E3%81%AE">CocoaPodsで入れたもの</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/firebase/firebase-ios-sdk">firebase-ios-sdk</a> <ul> <li>(今もかもだけど)当時モバイルのアナリティクスとしてイケてる感じだった為導入</li> <li>ユーザ動向を知るため</li> </ul></li> </ul> <h3 id="直接入れたもの"><a href="#%E7%9B%B4%E6%8E%A5%E5%85%A5%E3%82%8C%E3%81%9F%E3%82%82%E3%81%AE">直接入れたもの</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/thii/FontAwesome.swift">FontAwesome</a> <ul> <li>各種アイコンに利用</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/DylanVann/DatePickerCell">DatePickerCell</a> <ul> <li>設定画面でのCell操作の為に導入</li> </ul></li> </ul> <h2 id="導入ライブラリのアップデート"><a href="#%E5%B0%8E%E5%85%A5%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88">導入ライブラリのアップデート</a></h2> <p>とりあえず当然ビルドが通らなくなってため、各ライブラリの最新版を利用するように<br /> Cartfileを書き換えてアップデートしてみた。<br /> ただそもそも、Carthageの使い方を忘れていたので、思い出すために下記記事などを参考にした。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/s-harada/items/47295d653ef0cf34d540">Carthage について</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.raywenderlich.com/416-carthage-tutorial-getting-started">Carthage Tutorial: Getting Started</a></p> <p>さらに元々、Carthageディレクトリ以下のBuild/Checkoutsもgit管理下においていたが、<br /> ライブラリアップデートを何回も試してる時に、いちいち削除が面倒になったのと<br /> 下記の記事を読んだので、git管理化から外すことにした。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/mono0926/items/636819c42e96a8c4e12d">CocoaPods・Carthageでインストールした成果物はバージョン管理に含めるべきか?</a></p> <h3 id="アップデート方法"><a href="#%E3%82%A2%E3%83%83%E3%83%97%E3%83%87%E3%83%BC%E3%83%88%E6%96%B9%E6%B3%95">アップデート方法</a></h3> <p>利用しているライブラリのうちほとんどは、Swift3へのアップデート対応等がされていたが<br /> Logライブラリだけはメンテが止まっていたので、forkして対応することにした。<br /> (代替のものを探したが方が良いが、とりあえずビルドを通すのが第一目標なのでそのまま利用)</p> <p>直接ソースを入れるタイプのものも、Carthage対応されるようになっていたので<br /> Carthageを利用してインストールする方式に変更した。</p> <p>メンテナンスされているライブラリはバージョン指定を外して<br /> 最新版をインストールするようにした。</p> <p>下記はSwiftのバージョンを3にして <code>carthage update --platform iOS</code> が<br /> 問題なく実行できるようになった時点でのCartfile。</p> <pre><code># Use libraries github "malcommac/SwiftDate" github "SwiftBond/Bond" github "xmartlabs/Eureka" github "Quick/Quick" github "Quick/Nimble" github "thii/FontAwesome.swift" github "DylanVann/DatePickerCell" ~> 1.0 # Own repos. github "stlwolf/Log" </code></pre> <h3 id="備考"><a href="#%E5%82%99%E8%80%83">備考</a></h3> <ul> <li>carthageでのインストールが成功するところまで行き、Xcode上でライブラリを<br /> 利用しているコードでErrorになってる箇所を修正することができる状態にはなった。</li> <li>Bondライブラリがアプリに入れた時期から大きな変更が入っているので、利用方法を含めて<br /> 再度使い方を調べないと最新版のコードを利用できそうにないことが判明。(ReactiveKitに組み込まれてる?)<br /> 公式Docと下記あたりを参考に別途キャッチアップする。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.raywenderlich.com/667-bond-tutorial-bindings-in-swift">Bond Tutorial: Bindings in Swift | raywenderlich.com</a><br /> <a target="_blank" rel="nofollow noopener" href="http://grandbig.github.io/blog/2017/07/31/swiftbond-1/">Bond, SwiftBondを使ってみよう! - Takahiro Octopress Blog</a></li> </ul> stlwolf tag:crieit.net,2005:PublicArticle/15085 2019-06-10T13:53:49+09:00 2019-06-19T23:31:31+09:00 https://crieit.net/posts/Swift2-3-Swift5 【禅Do】Swift2.3のアプリをSwift5対応にしたい <h2 id="大枠"><a href="#%E5%A4%A7%E6%9E%A0">大枠</a></h2> <p>数年前にSwiftの勉強のために作ったiOSアプリを、リリース後に全く<br /> アップデート対応していなかったので気がついたらSwiftのバージョンも5になってた。</p> <p>このアプリのWeb版出したりとか諸々やりたいことが出来てきているので<br /> とりあえず最新版のSwiftに対応して一度アプリを<br /> 更新リリースするところまでやることにした(頑張る)</p> <h2 id="前提条件"><a href="#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6">前提条件</a></h2> <ul> <li>1,2年ほどiOSアプリ周りを全く触ってなく以下は忘却されている <ul> <li>Swiftの文法、機能について</li> <li>Xcodeでの各種設定について</li> <li>リリースまでに必要なりソース、手順について</li> <li>導入したOSSライブラリについて</li> </ul></li> <li>アプリ自体の機能はかなり控えめなので、コード量自体は多くない</li> <li>当時リアクティブプログラミングの勉強を兼ねて<a target="_blank" rel="nofollow noopener" href="https://github.com/DeclarativeHub/Bond">Bond</a>というライブラリを利用している <ul> <li>RxSwiftとかを入れるほど大した機能はなかったので、機能が控えめなBondを入れた経緯</li> <li>ただ当時から大分実装が変わっているようなので、コレの置き換えが一番手間かかりそう</li> </ul></li> </ul> stlwolf tag:crieit.net,2005:PublicArticle/14666 2018-12-17T05:35:42+09:00 2019-10-23T04:27:49+09:00 https://crieit.net/posts/40 今年スマホアプリを作ってプログラマーになった40代女性の話。 <p>約6年ぐらい前、「C言語がわかるならiPhoneアプリを作ればいい」と言われた時、私はMacを持っていませんでした。Macは憧れの対象ではありましたが高級品で、当時、職業訓練校に行っていた身としては買えると思っていなかったのです。(話が前後しましたが、その職業訓練校でC言語を習いました。)</p> <p>去年(2017年)12月、私は4月から働いていた派遣をやめることにしました。そしてMacを買いました。Macを買うのに必要なのはお金だけではなく、思い切りでした。<br /> 何十年Macに憧れ続けて、このまま死ぬのか?死ぬまで憧れ続けて、買わないまま死ぬのか?</p> <p>今買えるお金があるなら、買おう。</p> <p>ちなみに、その頃までの私の主な職業はテストエンジニアでした。書くとしてもテストコードだけ。<br /> 専門学校は出ていないし、前述の職業訓練を受けるまでは学校で習ったこともありません。<br /> プログラマーという職業もずっと憧れであり、なかなか手が届かないものでした。</p> <p>そして今年(2018年)1月、iPhoneアプリ開発の入門書を手にして本の通りにやってみたところ、謎のエラーが出て、先に進めなくなりました。<br /> モチベーションも低下して、どうしたものかと思っていた頃、若宮さんの話をTwitterで目にしました。</p> <p>若宮さんは、80代でiPhoneアプリを作った、エンジニアではない女性です。<br /> ものすごく勇気をもらえました。</p> <p>私のアプリを作りたいという思いは死ぬまで消えないだろう。<br /> 死ぬまで、たとえ病の床にあったとしても「アプリ作りたかったなぁ・・・」と思い続けるだろう。<br /> ならもういい加減、諦めるのをやめたらどうなんだ?</p> <p>当たり前に開発をしている人から見たら、いちいち大げさかもしれませんが、<br /> 憧れが強すぎるあまり、「開発者」というものが、遠い遠い存在になっていたのです。</p> <p>私を大好きなことから遠ざけていたものは「自分にはできない」という無力感と決めつけでした。<br /> こんな程度の私には無理なんだ、そんなすごいことできるはずがない、<br /> アプリ開発なんて、私よりもっとすごい、もっとできる人しかできないのだ。</p> <p>そんな謎の思い込みを持っていました。</p> <p>作るのは別になんでもよかったんです。<br /> ただ、スマホアプリというのは小さい世界で完結できる。一人でも作れそうだったから。</p> <p>そして諦めるのをやめた私は、<br /> 本の著者の掲示板に質問を送り、最初のエラーは解決し、<br /> またわからなくなったらそこで質問し、<br /> ドットインストールにも入会し、わからなかったら質問して、</p> <p>わからないことはネットで検索しまくって、それでもわからなかったらまた後日検索して、<br /> 新しい本を何冊か買い、勇気をもらえるブログを読んでモチベーションを上げ、</p> <p>アマゾンプライムで孤独のグルメをヘビロテしながら、<br /> iOSアプリを3つ、春に完成させました。</p> <p>友達が、私の作ったシンプルなゲームを気に入ってくれました。<br /> どこの誰か知らない人たちが、私の作ったアプリを使って遊んでくれています。<br /> 毎日誰かがダウンロードしてくれている。<br /> 誰かのスマホで私の作ったアプリやゲームが動いている・・・。</p> <p>一年前の自分にその未来を話したところで、信じなかったと思います。</p> <p>それだけでもとても嬉しいことだったのですが、<br /> 私はずっとプログラミングしていたくて、そうするにはそれを職業にするのがてっとり早い。<br /> でも体力ないので徹夜とか長時間は無理。のめり込むからやれるけど、多分それやると倒れる。<br /> というわけで正社員募集してた会社に問い合わせて、パートで時短で、プログラマー採用してもらいました。</p> <p>そして半年後にはパートも辞めてフリーになりました。<br /> パートしてた会社から仕事もらえる予定です。</p> <p>半年で独立するなよって言われるかもしれませんが、会社はOKしてくれたし、<br /> 毎日同じ会社に同じ時間に行くのってものすごく向いてないんです。</p> <p>あともう一つ、IT勉強会にも憧れていて、仲間がほしくて、10月に開催してみました。<br /> <a target="_blank" rel="nofollow noopener" href="https://smartphoneapp.connpass.com/">https://smartphoneapp.connpass.com/</a><br /> そしたらものすごく楽しくて自分が満たされたので、12月にも開催して、<br /> 地方だし参加者も少ないですが、やっぱりとても楽しくて。謎に楽しい。</p> <p>なのでこれからも続けていこうと思います。</p> <p>自分が思っていたよりも、世界はとても優しかったです。<br /> やりたいことをやりたい!といったら、協力してくれるし、叶えてくれる。<br /> こんな私でも、一番好きで叶えたかったことを叶えられました。</p> <p>いちばん自分に厳しくきつい言葉をかけていたのは自分自身でした。<br /> 世界は優しい。Twitterも優しい。いいねもシェアもひとつひとつに感動しています。</p> <p>読んでいただきありがとうございました!</p> Hata tag:crieit.net,2005:PublicArticle/14648 2018-12-11T17:57:33+09:00 2019-02-22T21:04:03+09:00 https://crieit.net/posts/Android-iOS-100 Android, iOSアプリを100本くらいリリースしたのでいろいろ振り返ってみる <p>こちらはCrieitの<a href="https://crieit.net/advent-calendars/2018/technology">個人開発サービスに用いられている技術 Advent Calendar 2018</a>の12日目の記事です。<br /> 前日は<a href="https://crieit.net/users/rubys8arks">かしい@お笑いSNS作成中</a>さんの<a href="https://crieit.net/posts/SNS-Tumblr">ブログSNS『Tumblr』を無理やりユーザー投稿型メディアとして運営した話</a>についてでした!</p> <h2 id="あんた誰?"><a href="#%E3%81%82%E3%82%93%E3%81%9F%E8%AA%B0%EF%BC%9F">あんた誰?</a></h2> <p>どうもこんにちは<a target="_blank" rel="nofollow noopener" href="https://twitter.com/tecco_master">Tecco</a>です。<br /> 主に <strong>Android, iOSアプリ</strong> を作っています。</p> <p>会社でもいっぱい作ったんですが、元々は個人開発でアプリを作っていました。<br /> リリースした本数が100本(※)を超えたので、数えるのをやめました\(^o^)/</p> <p>Android -> <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/developer?id=Tecco%27s+Project">https://play.google.com/store/apps/developer?id=Tecco's+Project</a><br /> iOS -> <a target="_blank" rel="nofollow noopener" href="https://itunes.apple.com/jp/developer/makoto-nishimoto/id896702438">https://itunes.apple.com/jp/developer/makoto-nishimoto/id896702438</a></p> <p>せっかくの機会なので <strong>どのような技術を今まで使ってきたか</strong> を振り返ってみます。</p> <p>※公開を停止したもの含めます。</p> <h2 id="使ってきた技術一覧"><a href="#%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%8D%E3%81%9F%E6%8A%80%E8%A1%93%E4%B8%80%E8%A6%A7">使ってきた技術一覧</a></h2> <p>集計してみます。 ※Android, iOS同名アプリは別アプリとして集計</p> <p>フロントエンド</p> <div class="table-responsive"><table> <thead> <tr> <th>技術</th> <th>本数</th> </tr> </thead> <tbody> <tr> <td>ネイティブ</td> <td>約100本</td> </tr> <tr> <td>Cordova</td> <td>1本</td> </tr> <tr> <td>Cocos2dx</td> <td>1本</td> </tr> <tr> <td>Unity</td> <td>2本</td> </tr> </tbody> </table></div> <p>バックエンド</p> <div class="table-responsive"><table> <thead> <tr> <th>技術</th> <th>本数</th> </tr> </thead> <tbody> <tr> <td>なし</td> <td>約90本</td> </tr> <tr> <td>自前API</td> <td>2本</td> </tr> <tr> <td>他社API</td> <td>8本</td> </tr> <tr> <td>Firebase</td> <td>7本</td> </tr> </tbody> </table></div> <p>かなりテキトーな仕分けですがこんな感じです。<br /> 数だけあっていろんな分布を期待した方ごめんなさい。</p> <p>見てもらったらわかるように基本的には <strong>ネイティブ × なし</strong> が圧倒的な数を占めます。</p> <h2 id="技術の選定"><a href="#%E6%8A%80%E8%A1%93%E3%81%AE%E9%81%B8%E5%AE%9A">技術の選定</a></h2> <p>基本的に個人開発で心がけていることは <strong>以下の5つ</strong> です。</p> <ol> <li>シンプルイズザベスト</li> <li>3日以内を制限時間とする</li> <li>運用に負担がかかるものはやらない</li> <li>アプリ一本にこだわりすぎない</li> <li>デフォルトを愛する</li> </ol> <p>詳しくは人生でたった一本だけ書いた僕のQiitaに書いてあるので興味があればどうぞ。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/Tecco/items/518094205784c7e855ee">個人で30本スマホアプリをリリースしたときのコツ5つを紹介してみるよ</a></p> <p>やっぱり開発者として、 <strong>メルカリみたいな、SHOWROOMみたいな、LINEみたいな、アプリが作りたい!!</strong> っていうのはわかるんですが、自分は一人なのを最前提で考えなければいけません。</p> <p>もし、自分一人でLINEの機能を全部実装して完成した10年後には<br /> <strong>「スマホアプリとか化石かよ!ポケベルのがまだ風情があるわ!」</strong> とか言われる日がくるかもしれません←ぇ</p> <h2 id="個人開発スタイル(アイディア編)"><a href="#%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%28%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A3%E3%82%A2%E7%B7%A8%29">個人開発スタイル(アイディア編)</a></h2> <p>普段から生活してる際に、何か不満をおもったら、 <strong>アプリで解決できないか?</strong> って考えるのがクセになってます。</p> <p>例)<br /> <strong>事象</strong>: 北海道出身のTeccoさんは駅の土地勘が全く無く、電車が停止しましたが、混雑していて電光掲示板が見えません。さらにヘビーロックを爆音で聞いているのでアナウンスも聞こえません。降りれば良いのか数秒で判断できなくて人生つらい。</p> <p><strong>要求</strong>: 一秒でも早く自分が今いる駅を知りたい。</p> <p><strong>結論</strong>: アプリでワンタップで現在地から一番近い駅を取得したい。</p> <p>その時1時間くらいで作ったアプリがこちらです。(公開は停止中<br /> <a href="https://crieit.now.sh/upload_images/04f15e71d74c3f4090bdd7d954b2f3aa5c0f74691a117.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/04f15e71d74c3f4090bdd7d954b2f3aa5c0f74691a117.png?mw=700" alt="unnamed3.png" /></a></p> <p>こんな感じで普段の生活からアイディアを抽出することは可能です。</p> <h2 id="個人開発スタイル(実装編)"><a href="#%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%28%E5%AE%9F%E8%A3%85%E7%B7%A8%29">個人開発スタイル(実装編)</a></h2> <p>アプリとしては現在地をGoogle Places APIにぶん投げて、リストの一番目を取得してるだけです。<br /> まぁ、お察しの通りクソアプリなので<strong>DLは200いかないくらい</strong>です。</p> <p>たとえばこのアプリもリッチに考えればいろいろできて</p> <ul> <li>複数の駅表示に対応</li> <li>道案内機能を実装</li> <li>駅までの方向を実装</li> </ul> <p>いっぱい考えられると思うのですが、そんなの「Google Mapに任せとけよ」って話なので、個人開発でもう一つ気をつけなければならないのは、 <strong>リッチにすると先駆者がそれ以上のものを作っている可能性が高い</strong> という点です。</p> <p>これは普通のプロダクト開発でも言えることですが、機能が増えるほど <strong>メイン機能を阻害する機能が増えて、ユーザーが何をすれば良いのかわからなくなるという現象</strong> が起きがちです。</p> <p>これは心がけていても、自分でもよくやってしまう失敗なのでやっかいではあります。</p> <h2 id="個人的におもう個人開発のメリット"><a href="#%E5%80%8B%E4%BA%BA%E7%9A%84%E3%81%AB%E3%81%8A%E3%82%82%E3%81%86%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%81%AE%E3%83%A1%E3%83%AA%E3%83%83%E3%83%88">個人的におもう個人開発のメリット</a></h2> <p>僕が個人開発でできるメリットは <strong>単機能でリリースできること</strong> だと思っています。<br /> どうしても会社規模の売上やユーザー規模を獲得するには、アプリに滞在させたり、巡回させる方法を考えなければなりません。(初期の<a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.justyo">Yo</a>みたいな例外はあると思いますが</p> <p>僕は自分のアプリを紹介するときに、 <strong>○○するだけのアプリです</strong> 、って紹介を良くします。</p> <p>たとえばこちらが、Amazonのディスカウント率で検索する<strong>だけ</strong>のアプリです。<br /> <a target="_blank" rel="nofollow noopener" href="https://tecc0.com/amazondiscount_lp.html">Amazon割引ショッピングアプリ</a></p> <p>こちらも初回リリースは3日で行っていますが、<strong>合計50万DL</strong>ほどされています。</p> <p>これは本家Amazonにももちろんある機能ですが、本家は機能が多い分埋もれてしまっています。<br /> なので <strong>大手のサブ機能をメイン機能に持ってくるだけ</strong> で、これくらいの需要があるってこともあります。<br /> (ちなみにこちらのアプリはAmazonさんからも了承いただいております</p> <p>そろそろ書くのが疲れてきたので orz 質問ベースでの回答をいくつかしておきます。</p> <h2 id="よく聞かれる質問"><a href="#%E3%82%88%E3%81%8F%E8%81%9E%E3%81%8B%E3%82%8C%E3%82%8B%E8%B3%AA%E5%95%8F">よく聞かれる質問</a></h2> <p>Q. そんなに出してて管理めんどくさくないの?<br /> A. 他社のAPIのバージョンアップがたまーにと、Androidのバージョンアップで動かなくなったものの対応以外はメンテなしでいけるようにしてるので、<strong>半年に一回くらい1,2日メンテくらい</strong>で済んでます。</p> <p>Q. 両OS出すの面倒くさくない?<br /> A. どっちもある程度わかってくると<strong>頭の中で仕様はコンバートできる</strong>ので、片方のOS作ってしまえばもう片方がほぼ何も考えなくてもできます。</p> <p>Q. クロスプラットフォーム系のやつどう思う?<br /> A. ネイティブでどちらも書いているので、個人的にはあまりメリットは感じられないし、学習コストが高い・本家OSバージョンについていけてない気がする。しかし、<strong>ゲームを作るならUnityの恩恵はでかすぎる</strong>のでUnityで良いと思う。(あくまで個人のスキルセットに依存するので違うケースも多々あると思います</p> <p>Q. 正直個人開発って儲かるの?<br /> A. アプリの量と質は間違いなく上がっているし、大企業が本気だしてきてるのでアプリを認知させるのが難しくなっているので<strong>ユーザー数の獲得</strong>は難しい。しかし、<strong>ユーザー1人あたりの広告単価や課金量</strong>は上がっているので認知されればお小遣いくらいなら割と可能。</p> <p>Q. コストはどれくらいかかるの?<br /> A. ライセンス費はAndroidは25ドルで永久、iOSは年に100ドルくらい。アプリ自体に使ってるサーバもすべて無料枠なのでタダ。ただし、宣伝用のWebページなどのサーバは持ってるので月1000円くらい。月換算で2000~3000円くらいなので、<strong>月1回の飲み会を断れば余裕で賄えるかな</strong>、と。</p> <p>Q. AndroidとiOSどちらから始めればいいの?<br /> A. コスト面では年に100ドル払いたくないならAndroid一択。市場面では海外(特にヨーロッパ)狙いたいならAndroid, 日本のJKとかビジョンが日本マーケット狙いたいならiOS。<strong>KotlinもSwiftもとっても書きやすい</strong>ので、その点ではどちらもおすすめ。個人的にはKotlinが超大好きだし、最終的には好み。</p> <h2 id="最後に"><a href="#%E6%9C%80%E5%BE%8C%E3%81%AB">最後に</a></h2> <p>久しぶりにブログを書きました(゚Д゚)<br /> こんな機会を与えてくれたアドベントカレンダーと開発者の<a href="https://crieit.net/users/dala00">だら</a>さんには感謝(`・ω・´)/</p> <p>良かったらTwitterでもフォローしてください -> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/tecco_master">@tecco_master</a><br /> 直近リリースしたアプリはこちら -> <a target="_blank" rel="nofollow noopener" href="https://mimicha.me">匿名チャットアプリ mimicha</a></p> <p>さて、明日は<a href="https://crieit.net/users/nabettu">nabettu</a>さんです!<br /> 実はいろいろインターネット上でお世話になっていいるので期待(゚Д゚)</p> Tecco XIII