20211126のiOSに関する記事は6件です。

required 修飾子とは

用途 サブクラスに、イニシャライザのオーバーライドを強制するとき プロトコルのイニシャライザをオーバーライドするとき また、 required 修飾子は、イニシャライザ init() でのみ使用可能。 インスタンスメソッドでは使用不可。 サブクラスに、イニシャライザのオーバーライドを強制する時 例: class Animal { let name: String // // A(サブクラスにイニシャライザのオーバーライドを強制するときは、required 修飾子を付ける) // required init() { self.name = "unknown" } } class Cat: Animal { // // A をオーバーライド(override 修飾子ではなく、required 修飾子を使うことに注意) // required init() { super.init() } } ポイント サブクラスにイニシャライザのオーバライドを強制する場合、スーパクラスとサブクラスの両方にrequiredをつける。 プロトコルのイニシャライザをオーバライドする時 例: protocol SomeProtocol { // // A // init() } class Animal: SomeProtocol { // // A をオーバーライド // required init() { } } ポイント protocolのイニシャライザはrequiredをつけない protocolのイニシャライザをオーバーライドする時につける 参考 https://qiita.com/cotrpepe/items/3931c0c20ef43f4a18ac
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】async/awaitはどのようにして動いているのかLLVMのレベルで調べてみる

Swift 5.5によって導入されたConcurrencyのうちの、async/awaitがどのように動いているのか調べてみました。 きっかけ 私はもともとasync/awaitなどはJavascriptで使っていたので、Swiftに導入されると聞いたときも「ようやく便利になるね」ぐらいにしか思っていなかったのですが、iOSDC2021のこの発表を見たときにたまげました。 特に驚いたのが、await式がサスペンションポイント(Suspension point)となり、実行が一時保留されるが、その間もスレッドは別の処理に使用されることや、再開されたときは別のスレッドに回ることもあり得るし、同じスレッドの場合もありうる、という話でした。 しかも、並列する複数のタスク間でのコンテキストスイッチは発生せず、呼び出しのコストのみで実現できるので、並列処理を軽量に実行できるそうです。 「なんでこんなことが実現できるの!?」というのが今回興味を持ったきっかけでした。 私はもともと、Mugichaというプログラミング言語を作ったことがあるのですが、この言語はコンパイルしてLLVM-IRを出力することができます。 (?気に入った方はスターしていただけると嬉しいです) なので、LLVMは多少知っていました。このため、SwiftがLLVMレベルでどのように実現しているのか調べてみることにしてみました。 ではさっそく、この謎を読み解いていきましょう。 async/awaitとCoroutine swiftにおける、async/awaitはCoroutine(コルーチン)と同じような仕組みを使用しているようです。 こちらの動画にはCoroutineの仕組みが解説されており、swiftのasync/awaitについてもあわせて説明がありました。 動画の内容をまとめると次のようになります。 動画まとめその1:Coroutineの実装で考慮すべき点 1. 制御権の移動をどのように実現するか 2通りの方法がある。 コンテキストスイッチする スレッドを複数生成してコンテキストスイッチする。 コンパイラの実装は簡単で、ランタイムで処理できるが、実行時のオーバーヘッドが大きい。 関数を分割する サスペンションポイントで関数をramp function(降下関数)と、resume fuction(再開関数)に分ける。 分割方法は、再開関数を1つの関数にまとめるか、サスペンションポイントごとに複数に分割するかなどがある。 2. ローカル変数をどうやって保存するか 3通りの方法がある。 ローカル関数をスタックに積んだままコンテキストスイッチする(stackful corotuines) 関数の分割が不要。 OSレベルでの実装が必要。 関数分割して、それぞれローカル関数を保持する(side allocation) 関数分割が必要。 ローカル関数の保持にヒープメモリのアロケートが必要。 スタックの同居(cohabitation) 関数分割が必要。 コルーチンに入るときには普通にスタックフレームにローカル変数をpushするがyieldで制御を戻すときはスタックをPOPしない コンパイラの実装は複雑。 3. データをどうやって生成するか(yielding data) 2通りの方法がある。 ramp/resume functionから値を返す すぐに値を利用するのに便利 固定したメモリに保存する async/awaitなど値の利用が遅れてやって来る場合に便利 動画まとめその2:Swiftで要求されたこと async/awaitは特殊なケースのジェネレータであること 呼び出し元と呼び出し先で頻繁に情報をやりとりする yield valueと効率的なアクセスができることを優先する Returned-Continuation Loweringを使用する Returned-Continuation Loweringは、コルーチンcoroutine lowering(コルーチンのハンドリング手法)の一種です。(なんて和訳したら良いのかわかりませんでした) サスペンドポイントが「生成された値(yield values)」のリストを取得し、このリストが継続関数と呼ばれる関数ポインタとともに呼び出し元に返されます。コルーチンは、この継続関数ポインターを呼び出すことで再開します。 WWDC21での解説 WWDC21でも、Swift Concurrencyの仕組みについて触れている部分があります。 この中で、async functionsのローカル変数はheap領域を用いていることが解説されています。 (WWDC21「Swift concurrency: Behind the scenes」より引用) 2つの動画から得られた仮説 2つの動画からわかることをまとめます。 まず、SwiftではReturned-Continuation Lowering を使用している、という説明がありました。このため、制御権の移動は、関数分割の手法を用いていることがわかります。また、この手法の場合呼び出し先の関数は、制御を戻すためのポインターを受け取っていると考えられます。 また、WWDC21の動画から、ローカル変数の保持は、side allocationの手法を使っていることがわかります。 LLVM-IRを出力して確認してみる 仮説を検証するために、次のコードをLLVM-IRに変換してみます。 mybarfunc関数が、await付きでmyfoofunc関数を呼び出しています。 なるべくシンプルにしているためエントリー部分もなく、実行しても何も起きません? simple_async_await.swift @available(macOS 12.0.0, *) func myfoofunc() async { print("ok") } @available(macOS 12.0.0, *) func mybarfunc() async { await myfoofunc() } この呼出関係がどうなるか先程の仮説によると、mybarfuncはawaitのところで2つに分割されます。1つめがramp function, 2つめがresume functionとなります。処理の流れとしてはmybarfunc(ramp)からスタートしてまずmyfoofuncが呼ばれ、次に、mybarfunc(resume)が呼ばれます。 図にすると次のようになります。 LLVM-IRを出力してこの仮説を確認してみましょう。 $ swiftc -emit-ir simple_async_await.swift このぐらいのコードでも300行ぐらいのLLVM-IRが出力されます。 まず、関数宣言のところだけ見てみましょう。 simple_async_await.ll ; myfoofuncの宣言 define hidden swifttailcc void @"$s18simple_async_await9myfoofuncyyYaF"(%swift.context* swiftasync %0) #0 { ; mybarfuncの宣言その1 define hidden swifttailcc void @"$s18simple_async_await9mybarfuncyyYaF"(%swift.context* swiftasync %0) #0 { ; mybarfuncの宣言その2 define internal swifttailcc void @"$s18simple_async_await9mybarfuncyyYaFTQ0_"(i8* swiftasync %0) #0 { それぞれの関数名をdemangleすると次のようになります。 simple_async_await.ll ; myfoofuncの宣言 simple_async_await.myfoofunc() async -> () ; mybarfuncの宣言その1 simple_async_await.mybarfunc() async -> () ; mybarfuncの宣言その2 await resume partial function for simple_async_await.mybarfunc() async -> () mybarfuncは、2つの関数に分かれていて、2つめはresume partial functionがついていることがわかります。これは、関数の分割であり、1つ目がramp function、2つめがresume functionになっていることがわかります。 次に、これらの関数の宣言には swifttailcc という呼び出し規約が入っていることがわかります。 swifttailccは、次のように説明されています。 この呼び出し規則は、ほとんどの点で swiftcc と似ていますが、呼び出し先がスタックの引数領域をポップするので、tailcc のように必須のテールコールが可能になります。 tail call呼び出し規約を用いると、末尾位置の呼び出しは常に末尾呼び出しが最適化します。 末尾呼び出しの最適化とは何でしょうか。Wikipediaから引用します。 末尾呼出しのコードを、戻り先を保存しないジャンプに変換することによって、スタックの累積を無くし、効率の向上などを図る手法である。 末尾呼び出しの最適化を用いると、スタックを消費せずにジャンプすることができます。 なんでこんなことをしたいのかというと、よくあるのが再帰処理の最適化で、再帰呼び出しを、ジャンプに置き換えることでスタックの消費を防ぐことができます。 では、この仕組を使ってどのようにしてasync functionから制御を戻しているのでしょうか? この疑問を確認するために、呼び出されるmyfoofuncのLLVM-IR表現をみてみましょう。 simple_async_await.ll define hidden swifttailcc void @"$s18simple_async_await9myfoofuncyyYaF"(%swift.context* swiftasync %0) #0 { entry: call void @coro.devirt.trigger(i8* null) %1 = alloca %swift.context*, align 8 store %swift.context* %0, %swift.context** %1, align 8 %2 = call swiftcc { %swift.bridge*, i8* } @"$ss27_allocateUninitializedArrayySayxG_BptBwlF"(i64 1, %swift.type* getelementptr inbounds (%swift.full_type, %swift.full_type* @"$sypN", i32 0, i32 1)) %3 = extractvalue { %swift.bridge*, i8* } %2, 0 %4 = extractvalue { %swift.bridge*, i8* } %2, 1 %5 = bitcast i8* %4 to %Any* %6 = call swiftcc { i64, %swift.bridge* } @"$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC"(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i64 0, i64 0), i64 2, i1 true) %7 = extractvalue { i64, %swift.bridge* } %6, 0 %8 = extractvalue { i64, %swift.bridge* } %6, 1 %9 = getelementptr inbounds %Any, %Any* %5, i32 0, i32 1 store %swift.type* @"$sSSN", %swift.type** %9, align 8 %10 = getelementptr inbounds %Any, %Any* %5, i32 0, i32 0 %11 = bitcast [24 x i8]* %10 to %TSS* %._guts = getelementptr inbounds %TSS, %TSS* %11, i32 0, i32 0 %._guts._object = getelementptr inbounds %Ts11_StringGutsV, %Ts11_StringGutsV* %._guts, i32 0, i32 0 %._guts._object._countAndFlagsBits = getelementptr inbounds %Ts13_StringObjectV, %Ts13_StringObjectV* %._guts._object, i32 0, i32 0 %._guts._object._countAndFlagsBits._value = getelementptr inbounds %Ts6UInt64V, %Ts6UInt64V* %._guts._object._countAndFlagsBits, i32 0, i32 0 store i64 %7, i64* %._guts._object._countAndFlagsBits._value, align 8 %._guts._object._object = getelementptr inbounds %Ts13_StringObjectV, %Ts13_StringObjectV* %._guts._object, i32 0, i32 1 store %swift.bridge* %8, %swift.bridge** %._guts._object._object, align 8 %12 = call swiftcc %swift.bridge* @"$ss27_finalizeUninitializedArrayySayxGABnlF"(%swift.bridge* %3, %swift.type* getelementptr inbounds (%swift.full_type, %swift.full_type* @"$sypN", i32 0, i32 1)) %13 = call swiftcc { i64, %swift.bridge* } @"$ss5print_9separator10terminatoryypd_S2StFfA0_"() %14 = extractvalue { i64, %swift.bridge* } %13, 0 %15 = extractvalue { i64, %swift.bridge* } %13, 1 %16 = call swiftcc { i64, %swift.bridge* } @"$ss5print_9separator10terminatoryypd_S2StFfA1_"() %17 = extractvalue { i64, %swift.bridge* } %16, 0 %18 = extractvalue { i64, %swift.bridge* } %16, 1 call swiftcc void @"$ss5print_9separator10terminatoryypd_S2StF"(%swift.bridge* %12, i64 %14, %swift.bridge* %15, i64 %17, %swift.bridge* %18) call void @swift_bridgeObjectRelease(%swift.bridge* %18) #1 call void @swift_bridgeObjectRelease(%swift.bridge* %15) #1 call void @swift_bridgeObjectRelease(%swift.bridge* %12) #1 %19 = load %swift.context*, %swift.context** %1, align 8 %20 = bitcast %swift.context* %19 to <{ %swift.context*, void (%swift.context*)*, i32 }>* %21 = getelementptr inbounds <{ %swift.context*, void (%swift.context*)*, i32 }>, <{ %swift.context*, void (%swift.context*)*, i32 }>* %20, i32 0, i32 1 %22 = load void (%swift.context*)*, void (%swift.context*)** %21, align 8 musttail call swifttailcc void %22(%swift.context* swiftasync %19) #1 ret void } 最後のところで、 musttail call という予約語が使われています。これは、末尾呼び出しの最適化を必須にした状態で関数を呼び出していることを示しています。 何を呼び出しているのでしょうか? 呼び出している%22 へ代入される変数をさかのぼっていくと、途中キャストしたり色々していますが、 %22⇒%21(%20の2つめの要素を使用する)⇒%20⇒%19⇒%1⇒%0 というふうにたどれます。%0はmyfoofunc関数の引数です。つまり引数として渡された領域から参照できる関数を呼び出しています。 では、myfoofunc関数はどのような引数で呼ばれるのでしょうか? 呼び出しているmybarfunc関数を抜粋します。 simple_async_await.ll define hidden swifttailcc void @"$s18simple_async_await9mybarfuncyyYaF"(%swift.context* swiftasync %0) #0 { entry: call void @coro.devirt.trigger(i8* null) %1 = bitcast %swift.context* %0 to i8* %async.ctx.frameptr = getelementptr inbounds i8, i8* %1, i32 24 %FramePtr = bitcast i8* %async.ctx.frameptr to %"$s18simple_async_await9mybarfuncyyYaF.Frame"* %2 = getelementptr inbounds %"$s18simple_async_await9mybarfuncyyYaF.Frame", %"$s18simple_async_await9mybarfuncyyYaF.Frame"* %FramePtr, i32 0, i32 0 store %swift.context* %0, %swift.context** %2, align 8 %3 = load i32, i32* getelementptr inbounds (%swift.async_func_pointer, %swift.async_func_pointer* @"$s18simple_async_await9myfoofuncyyYaFTu", i32 0, i32 1), align 8 %4 = zext i32 %3 to i64 %5 = call swiftcc i8* @swift_task_alloc(i64 %4) #1 %.spill.addr = getelementptr inbounds %"$s18simple_async_await9mybarfuncyyYaF.Frame", %"$s18simple_async_await9mybarfuncyyYaF.Frame"* %FramePtr, i32 0, i32 1 store i8* %5, i8** %.spill.addr, align 8 call void @llvm.lifetime.start.p0i8(i64 -1, i8* %5) %6 = bitcast i8* %5 to <{ %swift.context*, void (%swift.context*)*, i32 }>* %7 = load %swift.context*, %swift.context** %2, align 8 %8 = getelementptr inbounds <{ %swift.context*, void (%swift.context*)*, i32 }>, <{ %swift.context*, void (%swift.context*)*, i32 }>* %6, i32 0, i32 0 store %swift.context* %7, %swift.context** %8, align 8 %9 = getelementptr inbounds <{ %swift.context*, void (%swift.context*)*, i32 }>, <{ %swift.context*, void (%swift.context*)*, i32 }>* %6, i32 0, i32 1 store void (%swift.context*)* bitcast (void (i8*)* @"$s18simple_async_await9mybarfuncyyYaFTQ0_" to void (%swift.context*)*), void (%swift.context*)** %9, align 8 %10 = bitcast i8* %5 to %swift.context* musttail call swifttailcc void @"$s18simple_async_await9myfoofuncyyYaF"(%swift.context* swiftasync %10) #1 ret void } 最後の方にある musttail call でmyfoofunc関数が呼ばれていることがわかります。引数は、%10ですが、たどっていくと、 %10⇒%5⇒swift_task_allocで確保した領域 となっていることがわかります。swift_task_allocは調べてみてもわからなかったのですが、WWDC21で説明されていたContinuation(継続、タスクを管理するためのオブジェクト)を取得しているのではないかと思われます。 そして、この領域には、resume functionであるmybarfunc(resume)への参照が格納されています。このあたりです。 simple_async_await.ll store void (%swift.context*)* bitcast (void (i8*)* @"$s18simple_async_await9mybarfuncyyYaFTQ0_" to void (%swift.context*)*), void (%swift.context*)** %9, align 8 %9は、次のようにたどれます。 %9(%6の2つめの要素を使用する)⇒%6⇒%5 %5は先程、swift_task_allocで確保した領域であり、myfoofuncの引数になっていましたね。 myfoofuncは、この引数を受け取って musttail call で呼び出していたので、mybarfunc(resume)が呼ばれることが確認できました。これは上に書いた図の通りの呼び出し関係です。 結論 ということで、async/awaitの実装には、Returned-Continuation Loweringを使用し、関数分割を用いていることがわかりました。また、呼び出し元への制御の移動は、Continuationオブジェクトを使用していることもわかりました。 このように見てみると、awaitをつけて呼び出した場合、制御が戻ってきたときに別のスレッドになる可能性があることは自然なことのように思えます。呼び出し先で別のスレッドに制御が移動して、tail callでジャンプして戻ってくれば、awaitで呼び出した次の行は別のスレッドになります。コード上は次の行ですが、制御上は異なるコンテキストになるわけです。 なお、ローカル変数の保存やデータ生成については調査していません。また、スレッドが生成された場合の制御なども調べたかったのですが、制御権の移動を調べるだけで、LLVM-IRとのにらめっこは限界です? 最後に 今回の内容についての質問や感想は、TwitterでDMなどいただけると嬉しいです。 調べていてだいぶ面白かったので、興味を持ってくれた方はきっと話が合うでしょう? これ以外では、主にiOSを使った3D処理やAR、ML、音声処理などの作品やサンプル、技術情報を発信しています。 作品ができたらTwitterで発信していきますのでフォローをお願いします? Twitterは作品や記事のリンクを貼っています。 https://twitter.com/jugemjugemjugem Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。 https://qiita.com/TokyoYoshida Noteでは、連載記事を書いています。 https://note.com/tokyoyoshida Zennは機械学習が多めです。 https://zenn.dev/tokyoyoshida
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Yahooハッカソンから本開発し、iOSアプリをリリースするまでの軌跡と、エンジニア人生について考えたこと。

@kenmaroです。 普段は主に秘密計算、準同型暗号などの記事について投稿しています。 秘密計算に関連するまとめの記事に関しては以下をご覧ください。 秘密計算エンジニアを始めて1年が経った。 秘密計算エンジニアを始めて2年半が経った。 概要と注意 今回は、秘密計算の話ではなく、個人的に友達と取り組んだプロダクト開発について ポエム的に書いていきたいと思います。 注意 書いているうちにこの記事は完全にポエム化しました。 特に技術のことに関しては書かないのでご注意ください。 参加したヤフーハッカソンについて 今年参加したイベントはいくつかありましたが、 その中で二つほど記事として公開しました。 秘密計算コンペIDASH2021の出場レポート速報!!Team EAGLYS(Kenmaro & Ryohei) YAHOO! JAPAN Digital Hack Day 2021 参加レポート(チーム「あぐりメタル」 上の秘密計算コンペも、YAHOO! JAPANさんのハッカソンも自分で出場することを決め、チーム作りから行いました。 どちらも賞などを取るには至りませんでしたが、友達のみんなと開発を共にし、 一緒に過ごした時間はとてもいい思い出になったように思います。 今回は、その中でも「チームあぐりメタル」としてプロダクト「Yorimichi」というiOSアプリをハッカソンで作成したのち、本開発を始め、11/24に正式にApp Store へとリリースした話をしようと思います。 もし興味がある人がいらっしゃいましたら、 下のツイートからApp Store に飛べますのでぜひよろしくお願いします。 サービスの概要 Yorimichi は、誰もが行なったことのあるであろう「寄り道」をそのままアプリのタイトルにしています。 テーマは「誰かの寄り道を『再現性のあるもの』にし、他のユーザと共有できるサービス」です。 言葉にすると当たり前ですが、あなたが今日訪れたどこかの場所には、次の日には他の人が訪れています。 あなたが明日訪れる場所は、誰かが今日訪れた場所です。 Yorimichiアプリを経由することで、あなたの訪れた場所をフォロワーと共有し、 フォロワーはいいなと思った場所をお気に入りに登録したり、寄り道先に直接登録することで、ナビゲーションをしてもらうことが可能になります。 ユーザからの投稿はマップ上に画像と共に表示されるので、最近流行っている場所や、たくさんの人が訪れた場所は投稿で埋め尽くされます。 地図上いろいろな場所が検索でき、ユーザのコメント共に行き先をナビゲーションしてもらうのはなかなか楽しいのではないでしょうか。 また、ユーザのよくいく行き先や、よく通る道、お気に入りに登録した場所のジャンルなどをもとに、アプリ側からおすすめの場所をレコメンドするプッシュ機能などもあると面白そうだなと思っています。 このような具体的なサービス概要はハッカソンに向けたチームメンバーとのオンラインミーティングの中で次第に固まっていきました。 他にもたくさんの意見や、あったらいいと思われる機能のアイディアは尽きず、このアイディア出しの期間もなかなか楽しい期間であったと思います。 使うことに決まった技術 ハッカソンのテーマに、「誰も取り残さない」デジタル化 などの文言があり、わたしたちのチームはその文言をUIがなるべく直感的で誰でも簡単に使えるもの、 というふうな解釈をしました。 インターフェースを限定的にしないために、なるべく汎用的なアプリや既に多くの人が使っているアプリを経由して(例えばLINEを使ってなど)わたしたちのアプリを操作できるようにしたいと考えましたが、 結局のところiOSアプリから開発に着手することになりました。 ハッカソンに技術提供をしていたMapBox社のマップSDKなどを使い、コアとなる地図上に表示されるおすすめの場所を実装することとなったため、開発に必要な技術スタックは Swift での開発技術 MapBox SDKを組み込む技術 ということになりました。 その段階で私はどちらもほとんど開発経験がなかったため、ゼロからのスタートとなりました。 ハッカソン当日の様子 実際にオンラインで参加したハッカソンでしたが、近くに住んでいるメンバーはオフラインで集まり、オフラインで開発を行いました。24時間という限られた中で、ほとんどの技術が使ったことのない技術であったため開発は難航しました。その時の様子は YAHOO! JAPAN Digital Hack Day 2021 参加レポート(チーム「あぐりメタル」) この記事にまとめています。 あまり興味のある人はいないと思いますが、Yahooハッカソンに次出てみようかなと思っている人がいたらみてみるとイメージは湧くと思いますのでぜひご覧ください。 ハッカソンを終えて ハッカソンが終了し、我々チームあぐりメタルは賞などをいただくには至りませんでした。 周りのチームの開発能力はとても高く、面白いアイディアも多かったため、ある意味納得の結果とはなりましたが、(たぶんハッカソンに参加したことのある方であれば分かると思いますが、)やりきったという高揚感を寝不足の中感じながらその日は終わりました。 その後私は事情があり実家の九州に一週間ほど滞在することになり、手持ち無沙汰の中、気づくとプロダクト開発を続けていました。 決勝に残れたわけではなかったですし、やらなくても誰も気にしないという状況ではあったのですが、 何かもっと良くできないか、覚えたてのSwift知識を駆使し、もう少しUIを良くしてみよう、というような気持ちで開発していました。 UIもさることながら、おすすめの場所を提供するアプリでありながらデータベースはユーザが増えないと空っぽのため、APIの使用ができるような機能を追加しようと思い立ち、 調べたところホットペッパーは無料のレストラン情報APIを提供していること、 Googleは基本的には無料(多くクエリすれば従量課金へ移行)でGoogle Place API を使用可能ということを知り、この二つのAPIをデータベースとしても参照できるようにバックエンドも作り替えていきました。 この辺りまではSwiftに対しての理解がかなり浅く、非同期的な処理などもなんとなくで実装しており、 なぜここにnullが入っているのか、などあまり理解できない状態でとりあえず動くことだけを考え実装していきました。 ハッカソン決勝戦 今回のヤフーハッカソンは前半後半の2日間開催で、前半で決勝に進んだチームが二週間の追加開発期間ののち、決勝ステージに臨むという流れとなっていました。 決勝ステージに残ったのは10チームで、その中の優勝チームに賞金300万円が贈られるという、とても太っ腹な大会でした。 私たちは決勝に残ることはできなかったものの、決勝戦の内容については気になっていたため、 観れる人たちで視聴し、グループチャットで感想などを言い合っていました。 優勝した作品は会話をリアルタイムで可視化するプロダクトで、審査員の方のコメントにあったように、 今すぐにでも飲み会の場で使ってみたいような、とても完成度の高い作品に仕上がっており、とても感動したのを覚えています。 特に見せる相手もいなかったのですが、決勝戦をみる前日に少しだけ前よりかっこいいUIのアプリになっており、それをチームメンバーに見せたところ、よくなったね!というような反応をもらい、少しだけ自己満足しました。笑 決勝戦も終わったタイミングでしたし、少しだけ開発に一区切りついたところだったので、ここで終わってもいいな、あまり続けるモチベーションもないしなーと思っていました。 MapBoxからのインタビューのお誘い ハッカソン決勝戦とほぼ同じタイミングで、チームのメンバーがMapBoxからDiscordにどうやらメッセージが来ているようだ、 ということを知らされました。確認してみると、どうやらMapboxを使った面白いアイディアだったため、賞などはもらえなかったものの、ハッカソンでMapBoxを使ってみたレポート、という形でインタビューさせてくれないか? というような内容を広報の方からメッセージいただいていました。 直接賞などとは関係がなかったですが、私としてはメンバーを集めて24時間(準備期間も含めるともっと長い時間)拘束し、結果が出せなかったため申し訳なかったなあと思っていた矢先のメッセージであり、 何か認知されるようなイベントに繋がり本当によかった!ととても嬉しかったのを覚えています。 メンバーには学生もおり、彼らの名前がインタビュー記事に載ることで少しは参加してくれた恩返しになるのではないか、と思い、思い切って誘ってよかったなあと思った瞬間でした。 偶然私が実家で開発を少しだけ継続していたため、その良くなったUIをスクリーンショットで載せてもらおうか、と考え、チームメンバーの写真やインタビューの中身を用意し MapBoxの方に返信することとなりました。 開発をまた再開し始めた、インタビュー後 インタビュー記事もやりとりが終わり、一旦節目かなという雰囲気が出ていました。 終わろうと思えば終われましたし、また来年ハッカソンでたいなーくらいの感覚でした。 ただ、1、2日過ごすうちに、なぜリリースまで持っていく努力ができないのか、 と変に考えるようになり、もやもやしてきました。 気づけば、もう一度きちんとアプリ開発に向き合ってみてはどうか、という思いが湧いてきました。 決勝戦の日あたりまでに開発した追加開発には、デバイス間でレイアウトが崩れてしまうという問題があり、 汎用的に人に見せれるものではなく、そこが解決していないままでしたし、Storyboardを使わずにコードベースでレイアウトを記述し汎用化させるようなことをググっていたものの、実践できていなかったところに今一度真摯に向き合うことにしました。 試行錯誤の中出会ったiOSアカデミー 結局のところ足りなかったのはベースとなるSwiftへの理解であることは根底ではわかっていたので、 もしアプリをこれ以上良くしようとした場合、自分より圧倒的に経験のある人のコーディングを参考にするしかない、というのは感覚として理解していました。 そこで出会ったのがYoutubeに投稿されていた、iOSでInstagramのクローンを作る動画でした。 全15話くらいで各動画が30分程度でInstagramをフルスクラッチであまりライブラリをヘビーに使わず、 基本的にはUIKitを用いて実装するという動画でした。 Build Instagram App: Part 1 (Swift 5) - 2021 - Xcode 11 - iOS Development この動画を1から視聴しながら手元でコーディングを行い、1から勉強し直すことを決意しました。 幸いなことにReactなどもYoutubeのライブコーディングベースで学習をやった経験があり、 最初は大変ですが絶対にプラスになることはわかっていたので、食らいついて開発することに決めました。 一つ自分が得意なことといえば英語なのですが、基本的に優秀なライブコーダーは海外のディベロッパーが多く、その人たちから吸収するという点についてはとても英語力は役に立ちました。 そんなこんなで15話程度のライブコーディングをやりとげました。動画時間にすると7、8時間程度なのですが、コーディングを実際に行い、ビルドなどを試したり、よくわからないところを何だかんだデバッグする時間が加味されるため、多分トータル30から40時間くらいはかかったと思います。 先程レスポンシブなデザイン(デバイス依存がない)ようなデザインについて言及しましたが、 このディベロッパーは最初の動画でいきなりmain.storyboardを削除するところから動画をスタートしており、それがとても衝撃的でした。(これが普通なのかもしれないですが、その時は衝撃に感じ、これは学ぶべきだ、と感じた瞬間でした。) 恥ずかしながら、この動画を通してDelegateや、クロージャーがなぜこのように使われなければならないのか、 なぜ if let とか guard let みたいにしてわざわざオプションを使ったり、オプションを剥がしたりするのかなど、 Swiftの基礎的なことがやっとわかりました。それと同時に、ハッカソンの前後ではその辺が全くわからない状態でもなんとなく動く物が作れていたこともある意味勉強にはなっていたという反面、 リリースをするなどというレベルからはとてもほど遠いことをやっていたんだな、と感じました。 ここまでで基礎的なところがようやくわかり始めた一方、 まだそれが定着していたわけではなく、そのYoutuberのコーディングお作法の範疇では(例えばこの人はMVCとMVVMの複合型を使っていたので、自分も自然とその方式に従うようになりました。) なんとなくなにをやっているのかわかるようになった、という程度でした。 15話まで実装し終わって気づいたのですが、この動画は完結しておらず、まだ未実装なところが多々残っている状態で動画の更新が終了していました。 ただ、この未実装分を自分で全て実装できるくらいの実力には至っていなかったため、達成感半ばのままとてもモヤモヤな気持ちになりました。 そこから続きはないのか、、といろいろ検索していたところ、どうやらそのYoutuberの方はマイクロソフトのiOSディベロッパーで、個人的にiOSアカデミーというサイトを立ち上げ、そこにいろいろなコンテンツを提供していることがわかりました。 iOS Academy 中身を見てみると、instagram の実装コンテンツがあり、それは完結しているようでした。 彼のコンテンツの質の高さはもうわかっていましたし、クローンアプリがある程度完結するまでやってみたかったので、サブスクリプションを受講することになりました。 今考えると、このいい教材で一旦集中して勉強できたことがとてもよかったです。 結局のところ、iOSアカデミーで実装されているコードは、Youtubeであげられていたものとは異なって(ベースとしては一緒でしたが、コーディング自体は別時期にとられたものであり、課金ユーザ用に少し整理されたものになっていたようです)いたため、1からまたクローンを作ることになり、結局クローンを2周したような形になりましたが、今回は20時間くらいでおそらく実装でき、 前よりわかるようになったなあという感覚を持てるようになりました。 Swiftについてどこまで勉強すればいいか悩み始める 結局クローンまでやったのが功を奏し、もともと作りたかったアプリを再度実装し直すことにしました。 もともとの課題だった、どうやってユーザに寄り道情報をアップロードしてもらうか、というところはSNS的要素を組み込むことによって促進できるのではないかと考え、 SNS機能を入れることを考えました。 元来のMap機能は場所とポストを紐つけて表示する中で必要な機能だったため、UIはInstagramとスナップチャットのいいとこ取りになるのではないかとうっすら考え始めていました。 とりあえずマップ機能のところは参考にできるものがドキュメントくらいしかなかったため、そこを一旦重点的に実装し始めました。 クローンで培われたベース知識により以前よりは高い理解力で実装が進みましたが、つまる場所も多く大変な作業でした。 マップ機能がなんとなく出来上がってきた時、一旦メンバーのK君にUIについてアドバイスをもらうことにしました。 この時点ではUIなどのレベルがハッカソンの時とは1段階違うものになっていたため、驚いた様子で、 とてもよくなった、と言ってくれました。 ただ、実際に使ってみるととてもまた使いたいと思えないものであることもなんとなくわかりました。 細部の作り込み(たとえば画像投稿時のキャプション入力や場所入力が使いづらいと投稿する気が起きなくなる)は、自分の中でそもそも使いやすいと思えるレベルでないととても他の人に使ってもらうことなど不可能だな、と感じました。あたりまえなのですが、エンジニアからすると些細な変更でも大変なことが多いので妥協点を探しやすくなることは全く普通なことだと思います。 また、SNSでのホームとなるフィード画面は、最近のTikTokみたいな、非連続なブロック的なスクロールの方がいいのではないか、という意見をもらいました。 この辺を加味し、自分は70%くらい実装終了したのではないかと思っていたのは幻想であり、実際は30%、もしくはそれ以下くらいの完成度でしかない、ということに気づきました。 ここから、ベース知識がないとまたごそっとコード全体を作り替えなくてはならなくなる未来が待っているかもしれない、と気づき、 まだSwift自体を勉強する必要があるなと思いました。また、フィード画面の実装に関してはiOSアカデミーのTikTokクローン講座が最適で最短だと思い、もう一つクローンを作る覚悟をしました。 Swift自体の勉強では、iOSアカデミー以外の教材が必要であり、どうやらこの教材での書き方は古くはないですが、もっと新しい要素もiOS14などで登場している、というiOSのバージョンアップによる実装の歴史みたいなものも勉強し始めました。 例えばアカデミーではCollectionViewのCompositional Layout というV13のものは多用していましたが、CollectionViewList など、V14で実装されたものや、V13でのDefiableDataSource などの知識は得られていないことがわかり、そのあたりを推していたり解説していたりする他の文献もたくさん見つかったため、その辺をうまいとこ取り入れていないとダメだ、と思うようになり、どういう書き方が新しいのか、というような勉強をしました。 基本的に使っていたのは raywenderlich.com でした。 これを続けて数日経った後、この勉強の仕方だとSwiftのバージョンとか新しい書き方には精通する可能性はあるものの、本質のアプリをデリバリーする、というところにどうしても収束していかない泥沼化するな、と感じ始め、ここの線引きを行い、アカデミー的な実装と、今時の実装を組み合わせ、無理に全てを今時の実装の仕方に置き換えることはやめました。 ここから少し肩の荷が降り、アプリの細かいところの実装が捗るようになりました。 開発の最後の10%は全体の中で1番大変な10% これは私がたまに見てしまうTechLeadというユーチューバーが言っていた言葉です。 ある意味意識高い系というか、煽るような口調もある人なのですが、自分は開発中この人の動画で精神を保っていたところもありました。笑 結局のところ、エンジニアがフォーカスしないといけないのはデリバリーであり、そこが達成できるかどうかが一番大きな違いになるということ。 これを言うのは簡単だが、やるのは難しく、ほとんどの人が最後までやり切ることができないこと。 やりきろうとした時にいくつも壁があるが、デリバリー最後の10%は全体の中でも一番大変な10%であるということを理解しておかないと、終わらせることが非常に苦痛になってしまうこと。 などです。 ここはまさに今回感じたことです。 大枠できたかな、と感じはじめ、そこから細部をチューニングしたり、 自分でモンキーテストみたいなことをやってバグをできるだけ取り除いたりする作業でもかなり大変な作業でした。 最適化を少しだけ考えたあたりでデータベースの構成を2回くらい変更したり、いろいろな変更が出てきたのは最後の工程のほうでした。 工程が後の方であるが故に、些細な変更でも修正することが多く、バグを1つ修正するのに多大な時間がかかったりしました。 バグのことが気になって眠れないみたいな日があるのもたぶんエンジニアあるあるだとおもいますし、私にとってもこの時期はそう言う時期でしんどかったですが、 こういう時こそあまり無理せずに、ゆっくりとこの最後10%を終わらせることにした方がいいと思います。 そうしないと多分辛くなってしまうと思うので。 他の自分が辛くなった時にみるTechLeadの動画を置いておきます。(この動画やタイトルで私の人間性を判断してほしくはないです、あくまでショック療法がたまに必要なだけなので。。) もし暇な人がいたらみてみてください。 Things I wish I knew when I started Programming これの4:07あたりからの、Code is garbage の話 7 Habits for Success - How I think differently than you and why I always win. 7:08 あたりからの最後の10%の話 5 Millionaire Habits that will change your life (as a millionaire) 5:22 あたりからの Executionの話 MapBox 社への来訪 インタビューをMapBoxの方にさせていただいた後、私とチームメンバーのK君で話し、 何かせっかくなので後少し繋げられないかなあと思い、 思い切ってMapBoxジャパンの本社の方に出向いてインタビューのお礼と、プロダクトの内容を今一度説明させていただけないでしょうか? というような内容をメールしてみました。 少し失礼に当たるかなと思ったのですが、快く承諾していただき、とても二人でテンションが上がっていました。 二週間ほど後に設定された来社のスケジュールでしたが、 私個人としても、最後の10%のセクションで書きましたが、楽しい中でも疲労が溜まっており、 できるだけこの二週間でベータ版を完成させ、一つの区切りにしたいと決めました。 この二週間では、体調が悪い日が数日続き、時間をとても有効に使えたとは言えず、 なかなか精神的にも結構落ちている時がありました。 しかしながら、来社する数日前にほとんどの機能を実装し、Appleに申請を出すことを来社前日にやり終えました。 MapBox来訪の当日はあいにくの雨でしたが、広報の方に出迎えていただき、いろいろなことを学べた1日となり、とても充実した1日でした。 もしリリース作業がきちんと完了した時には、SNSなどを使ったリリース情報の拡散などをお手伝いいただけると言っていただき、とても嬉しかったです。 Apple とのやりとり 申請が済んで終わり、と言うわけではなく、iOSアプリの審査は基準値が高く、一筋縄ではいきませんでした。 ただ、感じたこととしてはどこを直せばいいのか、どこが基準を満たしていないのかがとても明確に記述されたリジェクト文が返ってくるということと、 Appleのエンジニアの申請に対する対応がとても早く、数日かかるのかと思っていたのですが実質10時間(多分時差のため)くらいで何かしらのレスポンスが返ってくるため、とても早い対応に感動し、やはりこういうところがすごいんだろうなあと感じました。 結果的に私は2回リジェクトをもらい、3回目の申請でAppStoreへの申請承諾をいただきました。 1回目のリジェクト時にはログイン時の単純なバグが原因で、完全に私のミスでした。 実機でデバッグを続けていたので、きちんとまっさらな環境で最後にデバッグをしていれば気付けるエラーだったのですが、見落としてしまっていたのが原因でした。 2回目のリジェクトはとても細かく分類されており、SNS機能にユーザ通報機能や、ユーザブロック機能、ポストに対する通報機能など、 ユーザが悪意を持った人に対してアプリ上で被害を受けにくいような機構を追加することや、 ユーザのデータにアクセスする際のパーミッションについての文面を修正してほしいと言われました。 また、アプリ全体でどのような機能があるのかをスクリーンキャプチャでまとめて動画化し、キャプションを添えて提出してほしいというようなレビューもいただきました。 この作業もきちんとやって1日くらいはかかりましたが、自分の作っているプロダクトを丁寧にレビューされていることが伝わってきましたし、ブロック機能などの実装は、ユーザの安全第一を考えた時に必須の機能であり、実装して然るべきものだと学ぶことができました。 Appleのエンジニアの素早い対応等、とてもすごいですし、ディベロッパーに対して丁寧にレビューをしていただけるところなどやはり尊敬できる会社だなあと感じました。 そんなこんながあり、朝起きた時にAppleからのメールで、 おめでとうございます、アプリが申請承諾され、App Storeに公開されました! というような文面が届き、自身の初めてのアプリが公開されたことを知り、本当に感動しましたし、やってきてよかったなと思えました。 多分この記事を読んでいるAppleの人などいないと思いますが、本当にレビュー等丁寧にご対応していただき、ありがとうございました。 リリース後、そしてこれから リリースをとりあえず終え、メンバーとも共有し、我々の持つSNSアカウントや、無料のプレスリリースを打てるWebサービスを用い、 宣伝をしたりしています。 ある意味、ハッカソン終了までが一区切り、リリースできたというところも一区切りだと思っています。 ディベロッパーとして、 TechLead の動画を参考にすると、デリバリーまで責任をもて、というか、デリバリーできるようなプロダクトをいくつ持つか、それを楽しんで完遂できるか、と言ったところのゴールというものは一つ達成できたのかなあと思っています。 また、自分を突き動かしてくれたもう一つのものはYahooハッカソンのディレクターをやっていらっしゃる善積さんのブログでした。 ものづくりの機会を最大限に生かすために〜ヤフーのDigital Hack Dayの取り組み ここに書いてあることで、開発の持続可能性ということ、そして作ることの本質を楽しむというか、そこに意義が必ずあるということ。 についていろいろと考えさせられたこのアプリ開発だったと思います。 確かに、新規事業を作る時、リーンスタートアップ等の言葉が使われ、PMFを測るためにMVPを作り、 仮説検証を回していく、というような説明がなされるかと思います。 そういえばこんな記事をかなり前に書いていました。  ベンチャーに関わっていてもいなくても知っておきたい ビジネスモデル仮説検証 ベンチャー企業に私自身勤めていますので、このようなことを身をもって体験できていることには非常に感謝していますし、とても楽しんでいる毎日です。 確かに、仮説検証を回すこと自体、それはもちろん間違っていないと思います。 また、そのようなプロセスの中、必ずしもプロダクトアウトしなかったとしても、むしろプロダクトがないほうが、早くサイクルを回し的確にマーケットフィットしていけるのかもしれません。 その中で、善積さんのブログでもあるように、作ることの本質的なところも楽しみつつ、持続可能な開発を続け、ハッカソンで終わらない開発という取り組みも尊いものだなと共感しました。 エンジニアとして、どういう活動をしていきたいか このアプリ開発を通して、また、ベンチャー企業に3年ほど在籍して開発を進めていく中で、いろいろと考えることがあるのでここにせっかくなので書いてみます。 エンジニアとして活動する中で、私個人の意見として一番有意義なこと、それは 自分が主体的に動いて開発し、自分の魂が入っているプロダクト に携われるような体験が、何回できたか というところではないかなあと思っています。 このプロダクト、というところは職種によってプロジェクト、になるかもしれませんし案件、になるのかもしれませんし いろいろと置き換えることになると思います。 私自身、現在の企業で自分が主体となって作り上げたプロダクトが一つあるな、と自信をもって言えます。 (もし万が一興味を持っていただける人がいるとすれば、EAGLYS株式会社のGateAIを検索よろしくお願いします。https://www.eaglys.co.jp/product/gate-ai) そこに関しては今の環境を作ってくれた周りの方達に感謝しないといけないなあと思っています。 そして、そのプロダクト(まだまだ未熟なプロダクトですが、)の成長を見守り磨き続けることも正解だし、 環境を変えてこのようなゼロイチ開発を新たに行うことも正解かなと思っています。 そんな中、ハッカソンというイベントを原点として、魂を込めたプロダクト(自分としては人生2つ目のプロダクト)のスタートを経験できたことに対して非常に有難いことを経験できた、いろんな人に感謝しよう、と思っています。 プロダクトで大事だと思う二つの観点 上で書いたような 主体的に開発し、さらに自分の魂が篭るようなプロダクト において、魂が入るということは自分の時間が投下されることであり、思いが投下されることだと思います。 その上でモチベーションがなにかないとそこまでコミットできないことは確かですが、私はそこに必要な要素が2つあると考えています。 一つ目は思想です。 そのプロダクトがどういう思想に基づいているのか、ということ。つまり、そのプロダクトが目指す世界観のようなものです。 私はまだブロックチェーンの開発を行ったことはないですが、ブロックチェーンがここまで使われていて世の中の人を魅了しているという点では、この思想の部分が魅力的だからだと思います。 中央集権的な管理体制ではなく、改ざんが不可能な分散型のデータ管理、データ管理の理想系とも言えるこの根底思想によって、多くのディベロッパーが主体的に開発を続けているのだと思います。 自分が現在取り組んでいる秘密計算の分野も、データのプライバシーを守ったままデータの利活用を行う、という究極思想はニッチな分野ではありますが、一定数のディベロッパーや研究者が長年磨き続けているだけの熱量を生み出す思想であろう、ということを考えます。 このような思想的なところは、まさに開発の持続可能性、というところの燃料になるはずだと確信しています。 二つ目はコア技術です。 一つ目の思想を実現するための技術、その中でも新しい技術であったり、現状の技術の課題を解決するものがある時、一番モチベーションが湧くのではないかと思います。 その技術が比較的数学寄りというか、理論寄りのものでもいいですし、エンジニアリング寄りの、実装上の難しさでもどちらでもいいと思います。 これに関しては、一番は研究テーマのようなものがあり、そのテーマにブレイクスルーがあったとき、思想に近づく。 というものだと一番わかりやすいし理想な気はしています。 しかしながら、コア技術のようなものが見当たらないとしても、開発時には必ず困難に当たると思いますし、 そこを解決するための技術もある意味コア技術だと感じています。 また、プロダクトを差別化しようとした時にはなにか新しい技術というか、エッジ的な技術を用いることになりますし、 そこをプロダクト開発の途中で学び、その過程でコア技術に触れていくこともあると思います。 次第に、今取り組んでいる技術が好きだとか、面白い技術だからもっと取り組んでみたいというような愛着のようなものが湧いてくるのだと思います。 このコア技術の習得、というところも、持続可能な開発を可能にする動力源の一つとなると確信しています。 ここからアプリ開発はどこに向かっていくのか とりあえず、ヨリミチに関しては、アンドロイド版も開発する、というところが私が一番にできることかなと思っています。 似たようなサービスが実際にあるなかで、なぜヨリミチなのか、というところを突き詰めてアップデートしていくことも私にできることでしょう。 ユーザをコツコツ増やしていくこと、マーケティングをし、もっといろいろな人に良さを知ってもらうために何ができるか、 そこを考えていくことはアプリ開発と同じくらい、それよりもっと難しいことなのだろうなと感じています。 そのマーケティング的なところから目を背けず、開発と同じくらいの熱量をもってそこもメンバーと頑張って行けたら素晴らしいことだろうな、と思っています。 最後に宣伝 長々とポエムを書いてしまいましたが、本当にたくさんのことを学ばせてくれたアプリ開発でしたし、 これからも無理しないようにしながら、持続可能性を考えた開発を行っていきたいと思います。 もし興味を持っていただける人がいましたら、ぜひ YorimichiApp 使ってみてください! いろいろ書いたのでなんかスッキリしました。 今回はこの辺で。 @kenmaro
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS 15でSwiftUIビューを定期的にリロードします

iOS 15のSwiftUI 3では、新しいビュー構造TimeLineViewがあります。このビュータイプを使って、(あなたが定義した)一定期間ごとにビューをリロードすることができます。 例えば、アニメーションのロード画面を作ることができます。 Thoughts SwiftUIビューはデータの変更時にだけリロードされるのがベストだと思います。 TimelineViewは、既定のリロード時間が設定されたビューを表示するように設計されているのではないかと思います(例えば、WidgetKitは所定のスケジュールに従ってリロードされます) 実装 public init(_ schedule: Schedule, @ViewBuilder content: @escaping (TimelineView<Schedule, Content>.Context) -> Content) コードクロージャに渡された変数Contextがあります。この変数を使って、ビューがリロードされた時間にアクセスすることができます。 import SwiftUI struct ContentView: View { var body: some View { TimelineView(.periodic(from: Date(), by: 1)) { context in Text("\(context.date)") } } } アニメーション付きローディング画面の作成 1秒ごとに画面をリロードできるため、日付から現在の秒の値を読み取り、その値を使用して別の画面を表示することができます。 0、1、2を繰り返すシーケンス番号を生成することができ、その値を使用して別の画面を表示することができます。 let sequenceNo = Calendar.current.component(.second, from: context.date) % 3 struct ContentView: View { var body: some View { Form { TimelineView(.periodic(from: Date(), by: 1)) { context in // A number that's in range 0, 1, 2 let sequenceNo = Calendar.current.component(.second, from: context.date) % 3 HStack { Image(systemName: "gamecontroller") .font(.largeTitle) Image(systemName: "chevron.right") .font(.largeTitle) .foregroundColor((sequenceNo >= 0) ? .green : .init(UIColor.label)) Image(systemName: "chevron.right") .font(.largeTitle) .foregroundColor((sequenceNo >= 1) ? .green : .init(UIColor.label)) Image(systemName: "chevron.right") .font(.largeTitle) .foregroundColor((sequenceNo >= 2) ? .green : .init(UIColor.label)) Image(systemName: "network") .font(.largeTitle) .symbolRenderingMode(.multicolor) } } Text("Connecting your console to the Internet...") } } } 関連記事 SwiftUIのビューは自動的に再描画され、通常は特定の関数をコールして再読み込みさせることはできません。どの変数を監視させるかをSwiftUIのビューに知らせるには、次の変数型を使用できます。 変数が変更されたときにSwiftUIのViewがリロードされるようにする Twitter @MszPro iOS開発に関するニュースレター iOS Dev Letter 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

事業フェーズごとにiOS/Androidアプリ開発で気をつけるべき観点

色々な事業フェーズに携わる中で、事業フェーズ別のアプリ開発に関する観点に対して思う所があったので個人的な見解をまとめます。 前段 事業状況は0→1、1→10、10→100などの局面によって変化していくものです。 その事業の局面ごとに最適なアプリの設計や重視すべき観点というのは変化していくのではないかと私は思います。 もちろん、アプリの特性と組織状況によっても最適なアプリの設計は変わるものですが、そこはサブ要因として今回は踏み込まずに思うところを記載していこうと思います。 0→1フェーズ 0→1フェーズは仮説検証を繰り返し行い、ビジネスが成立するコアサイクルを見つけ出すフェーズです。 この局面ではアプリに必要とされる機能やUXもどんどん変化するため、そもそもネイティブアプリとして作るべきかどうかも含めて検討すべきフェーズです。 可能であれば、NoCodeによるアプリ開発で簡素な仮説検証を繰り返して一定の方向性を見つけてからアプリ開発を開始すべきでしょう。 この段階で実際にアプリを開発する場合、仕様変更が頻発するとともに0→1に興味を持つ少数のエンジニアでどんどん開発を進めていく形になると思われます。 この局面では、アプリ開発において以下の観点を重視すべきでしょう。 安定性よりも作りやすさを優先(スクラップ&ビルド) 学習コストが小さく、参入障壁が低い設計 テストよりも動くものを優先 仮説検証の状況の可視化(定性・定量調査とその可視化) 1→10フェーズ 1→10フェーズはビジネスのコアバリューが形成され、売り上げを拡大していくフェーズです。 この局面ではコアの機能しかないアプリに色々な機能追加やUX改善が求められます。 また、0→1にこだわるエンジニアが抜けたりすることで人の入れ替えが激しくなってくる時期でもあります。 この段階におけるアプリ開発においては以下の観点を重視すべきでしょう。 コア機能に対するテストの実装 開発ドキュメントの整備(暗黙知から形式知への変換) リファクタリング(あるいはリニューアル)の検討と実施 10→100フェーズ 10→100フェーズは事業が安定した状態に入り、運用に重きを置くフェーズです。 この局面ではユーザ数が一定存在することもあり、細かいUI/UXの改善や不具合修正、外部(他アプリや他企業のAPIなど)との連携といった開発がメインとなってきます。 この時期には人員が十分な数配置され、技術主導の開発が進んでいる組織も存在するでしょう。 この段階におけるアプリ開発においては以下の観点を重視すべきだと思われます。 組織体制に応じた設計ルールの制定(人員が十分なら細かく責務を分けて厳密な設計ルールにするなど組織状況に応じた判断が必要) テストの拡充 運用周りの自動化推進 雑感 具体的な設計という意味では、初期フェーズではiOSならMVC、AndroidならMVVMで構成するにとどめておき、Clean Archなどを適用するとしても厳密なルールを適用しない方が小回りが利くと思います。 1→10フェーズではアプリの特性に合わせて設計を考え、場合によってはリニューアルする感じになるでしょう。 1→10でリファクタリングもしてないケースでは、10→100フェーズになってから大掛かりなリニューアルなどを行う羽目になり苦労している現場もあると思います。 逆に、0→1フェーズで作り込みをやりすぎて、1→10になってから作った人が抜けて誰も触れない設計になっているというのも聞いたことがあるので、事業フェーズと人員体制によってアプリに適した設計というのは変化していくことを意識してほしいと思う所です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリのユニバーサルリンクの設定方法

はじめに 調べると古い情報が出てきがちなのでまとめました iOS14,iOS15時代のものです 目次 ユニバーサルリンクにしたいURLを決める apple-app-site-associationを作成する apple-app-site-associationを置く アプリ側の設定 確認 ユニバーサルリンクにしたいURLを決める ↓これにします https://sampleApp/app apple-app-site-associationを作成する apple-app-site-associationを自分で作成します 自分で作らないと大変なことになります { "applinks": { "apps": [], "details": [ { "appID": "{teamId}.{bundleIdentifier}", "paths": [ "/app/*" ] } ] } } apple-app-site-associationを置く apple-app-site-associationを https://sampleApp/.well-known/apple-app-site-association に配置します 正しく配置できているかここで確認できます アプリ側の設定 対象のアプリのIdentifiersから下記を設定 Xcodeから下記を設定 コピペ用 applinks:sampleApp 確認 iPhoneからhttps://sampleApp/appにアクセスしてアプリに遷移すれば完了
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む