- 投稿日:2020-06-24T23:53:26+09:00
iOS app の習得日記
udemy の講座をみながら、
firebase Authを使って、facebookログインの二週目ViewControllerに
import UIKit
import FBSDKCoreKit
import FBSDKLoginKit
import FacebookCore
import FacebookLogin
import Firebase
なぜか?
https://developers.facebook.com/docs/facebook-login/ios
https://firebase.google.com/docs/auth/ios/facebook-login?authuser=0
公式ドキュメントにはこう書いてないのに??SDKの種類
CoreKit とCoreの違いは??公式のドキュメントの理解が追いついていない
- 投稿日:2020-06-24T23:50:36+09:00
App Clipの概要と設定方法
Configure and link your app clipsを見たのでメモです。このセッションにはサンプルコードもついているので合わせて見ると良いと思います。
※ここに出てくるスクリーンショットは、全て上記の動画のものです。
概要
App Clipを使えば、ユーザは簡単かつシームレスにアプリを体験できる。このセッションでは以下のことを説明している。
- App ClipのURLの扱い方
- App Store Connectで設定する方法
- スマートアップバナーを使ってApp Clipを起動する方法
- App ClipをTestFlightで扱う方法
App Clipの使用例
スムージースタンドでは以下のようなシーンが考えられる。
NFCタグに触れる → 端末の画面下部にスムージー注文用のApp Clipカードが表示される → 開くボタンをタップする → App Clipが起動する → スムージー注文ページに遷移する → Apple Payで購入する → スムージーを受け取る
このようにNFCタグにApp Clipで使うURL情報を書き込んでおくと、ユーザがタップしたときにNSUserActivityを経由してURL情報が渡され、直接注文ページを開くことができる。App Clipの起動方法いろいろ
- NFCタグ:URLが記録されたタグを読み込み
- QRコード:URLが記録されたコードを読み込み
- 地図アプリ:プレースカードのApp Clip URLをタップ
- Siri:位置情報ベースのサジェストをタップ
- Safari:バナーをタップ
- iMessageアプリ:ウェブページのURLを送信したときに表示されるバナーをタップ
Appleは、App Clip用のビジュアルコードを作成できるツールを今年後半にリリースする予定。
設定手順
大きく分けると以下の3つの手順を踏む。
- Webサーバ上の設定の変更とApp Clipプロジェクトの作成
- App Clipカードの設定(App Store Connect上でのデフォルト設定)
- WebページにApp Clipを表示するためのスマートバナーを設定
Webサーバ側の準備
WebサイトとApp Clipの関連付けは、Universal Links対応のときと同じようにapple-app-site-associationファイルを使う。今回新たにappclipsの項目を追加する。
{ "appclips": { "apps": [ "ABCDE12345.example.fruta.Clip" ] }, ... }App Clipプロジェクトの準備
Xcodeでプロジェクトの設定に移動し、Signings & Capabilities → Associated DomainsにApp Clipのドメインを追記する。
App ClipがSwiftUIで作られている場合は、以下のようなコードになる。NSUserActivityからwebpageURLを取得している。import SwiftUI @main struct AppClip: App { var body: some Scene { WindowGroup { ContentView() .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in guard let incomingURL = userActivity.webpageURL, let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { return } // Direct to the linked content in your app clip. } } } }ユーザがApp Clipから普通のアプリにアップグレードした場合、App Clipのリンクを経由するとApp Clipではなくアプリが開かれることになる。そのため、アプリにもユニバーサルリンクを処理するための同様のコードを用意する。
もしApp ClipがUIKitのSceneDelegateライフサイクルを使用している場合、以下のようにハンドリングする。
// Handle NSUserActivity in UISceneDelegate. class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { // Get URL components from the incoming user activity guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL, let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true) else { return } // Direct to the linked content in your app clip. } }associated domainを設定してNSUserActivitiesを扱う方法については「What's New in Universal Link」セッションを参照する。
デバッグ
XcodeでApp Clipを試したいときは、スキームエディタ→Argumentsタブから環境変数を設定する。この状態でRunすると、指定したURLで起動されるようになる。
App Clipカードの表示要件
App Clipカードに載せるメタデータを考えるときは、以下に従うとより適切な表示ができる。
画像はApp Clipアクションにマッチしたものにすると良い。
App Store Connectでの設定
アプリとApp Clipの両方を含むビルドをApp Store Connectに送ると、App Store Connect上でApp Clipの設定をするセクションが表示されているはず。ここでデフォルトのApp Clip設定をする。予め定義されたアクションのリストがあるので、それも選択する。
Safariやメッセージ以外からもApp Clipにアクセスできるようにしたい場合は、Advanced App Clip ExperiencesセクションのGet Startedをクリックし、高度な設定を行う。この高度な設定をすることでNFCタグやQRコードなどの物理的な方法からも呼び出しが可能になる。いくつか画面を進めるとこのようなページが表示される。複数のApp Clip Experienceを設定することもできる。
次のページでは画像やタイトルなどを指定する。
App Clipの複数設定
レストランアプリの場合、注文用App Clip Experience(melamela.example/order)と予約用App Clip Experience(melamela.example/reservation)を設定することもできる。
ベストプラクティス
App Clipは可能な限り全てのパスを登録する必要はなく、汎用的なプレフィックスを持ったURLを登録すれば良い。
例えば自転車のオンラインレンタル屋さんの場合。
ある自転車を借りるときのURLはhttps://bikesrental.example/rent?bikeID=2になっている。自転車にはそれぞれIDが振られており、予約するときはそのIDを指定する仕様になっている。この場合、App ClipのURLとして登録するのはhttps://bikesrental.example/rentの一つで良い。次に大手チェーンのカフェ屋さんの場合。
ある店舗のURLはhttps://brighteggcafe.example/store/campbellになっている。全ての店舗でhttps://brighteggcafe.example/store/までは共通なので、このURLを登録すれば良い。もしこのコーヒーショップのクパチーノ店舗だけ特別なApp Clip体験を提供したい場合は
https://brighteggcafe.example/store/cupertinoを登録し、異なる画像やタイトルを登録すれば良い。つまり、基本的には全てをカバーする一般的なURLプレフィックスを登録し、異なる体験を提供したいときだけ、より具体的なURLを登録すれば良い。より高度な設定については「What's New in App Store Connect」にて説明される。デザインのベストプラクティスについては「Design Great App Clips」にて。
Webページでのスマートアプリバナー設定
以下のようなメタタグを書く。iOS 14よりも古いユーザの場合は従来のスマートアプリバナーとして使えるように、app-idも忘れずに指定する。
<meta name="apple-itunes-app" content="app-clip-bundle-id=com.example.fruta.Clip, app-id=123456789">バナーの開くボタンをタップすると、デフォルトのApp Clipカードが表示される。もし高度なApp Clip Experience設定をしていたら、その設定に従った表示が出てくる。
デモ
Test Flight
アプリとApp Clipの両方を含むビルドをApp Store Connectに配信した後、Test FlightタブからApp Clipセクションを見つけて、「Add App Clip Invocation」をクリックする。タイトルとURLを設定すればTest Flightでテストできるようになる。
App Store Connectでのテストの詳細については「What's New in App Store Connect」にて。
- 投稿日:2020-06-24T20:49:22+09:00
Xcode 11でiOS9のプロジェクトの作成方法
はじめに
iOS9以降をサポートするアプリを作ることになったので、最新のXcodeで新規プロジェクトを作ってからiOS9向けにダウングレードする方法について説明します。
(普通に作るとiOS13になる。自分への備忘録も兼ねて記載しておく)執筆時点の環境
- macOS Catalina version 10.15.4
- Xcode version 11.4.1
1. Xcodeで新規プロジェクトを作成
では、Xcodeの操作を順に説明します。
File -> New -> Project で、新規プロジェクトを指定します。説明するまでもないですね。
Single View Appを選びましたが、他でも同じです。2. TargetにiOS9を指定する
3. InfoからSceneを消す
SceneはiOS13からサポートされた機能のため、上図の青で囲まれたScene関係の情報を削除します(4つの情報を下から順に(-)をクリック)。4. SceneDelegate.swiftを削除
使えませんので、SceneDelegate.swiftをプロジェクトから削除します。が、13行目のwindowの変数宣言をAppDelegate.swiftに移すので、コピーしておくとよいかも。
5. AppDelegate.swiftのコードの変更
SceneDelegate.swiftを消したので、AppDelegate.swiftにコンパイルエラーがいくつか出てますが、まず、windowの変数宣言をここ(14行目)に入れます。
コンパイルエラーが出ている2つのメソッドをバッサリ削除します。
以上で、iOS9プロジェクトの出来あがりです。WarningもErrorもなくビルドできるはずです。シミュレータを起動すると、真っ白画面のアプリが立ち上がります。
(寂しいので中央にラベルを貼りました)
おわりに
自分はこの状態のプロジェクトをiOS9プロジェクトの雛型として保存してあります。必要な時にプロジェクトフォルダ一式をコピーしてiOS9アプリ開発時に利用します。 以上
- 投稿日:2020-06-24T20:07:04+09:00
Flutter歴1ヶ月がオンラインハッカソンで初心者チームのTech Leadをしてみた
はじめに
1ヶ月前のGWにもハッカソンに出場し、そこで初めてFlutterを触り5日間でアプリを開発しました。
そのときの記事はこちらです
Flutter初見が5日間のハッカソンでアプリ開発してきた今回は1週間でサービスを作るハッカソンを企画/開催したためその概要と、Flutter初心者チームでTech Lead(笑)な役割をし、メンバーを牽引した軌跡を残せれば良いと思いこの記事を書いています。
CA21Hackathon
目的
今回のハッカソンは
CyberAgentの21卒内定者エンジニアでのハッカソンです。
このハッカソンを開催した目的は2つあります。
- 同期理解のため
- 同期間での技術知見共有
このハッカソンは、内定者数人で週に1度zoomで集まりミーティングを行い
- そもそもどんなイベントを開催するか
- ハッカソンを開催する目的/ テーマ
- 開催日時/希望職種アンケート
- グループ決め
等を1ヶ月間綿密に決めた上で開催されました。
人事の方とかけあったり、詳細を決めてくれたみんなに感謝です?
概要
結果、テーマは
with COVID-19, after COVID-19
開催期間は1週間。
初日と最終日のみ必須参加 とし、 稼働時間はチーム相談として各々調整する形です。
初日:チームでアイデアソンを行い、発表。他の人からフィードバックをもらうフェーズ
2日目〜6日目:任意の稼働時間で各々開発を進める
最終日:開発したサービスに関して発表(発表時間7分、質疑応答3分)
な流れでした。
チーム構成
自分のチームは
サーバーサイドのいないFlutterチーム
です。
チームの構成は
- Android (Flutter経験1ヶ月?♂️)
- Android (Flutter経験なし?♂️)
- iOS (Flutter経験なし?♂️)
- iOS (Flutter経験なし?♂️)
で、自分含め、ほとんどFlutter経験がないメンバーでした。
さらに、全員Firebaseの知識もなく、サーバレスで通信を伴うアプリを作りたい場合にも少し苦労するかもという印象でした。
作成したアプリ
今回のハッカソン企画において、
企画側は
- アンケート作成が面倒臭い
- バランス良くチーム分をするのが大変
- 参加者への連絡が大変
参加者側は
- 毎回のプロフィール情報を入力するのが手間
- メールだと大切な情報を見逃しがち
- チームの管理が面倒
- グループ名を決めるのが面倒くさい
- github, slide, document等リンクの管理が面倒
という課題/面倒がありました。
その面倒ごとを解決できるようなアプリを作ろうと思って生まれたのが
Hack ×2ですHackathonをHackするという意味での命名です、チームメンバーに命名マスターがいて即決まりました(さすが)
ハッカソン期間中はミニマムで実装をしましたが、元々の想定では15画面ほどありました。
シンプルにハッカソンで作る規模ではないですねw
また、要件的に通信が伴うため、サーバーサイドのいないこのチームではFirebase等のmBaaSを使用する必要がありました。
この、FlutterおよびFirebase初心者チームが、どのように膨大な画面数/仕様が存在するアプリを1週間で作成したか、その道のりについて記述できればと思います。
キャッチアップとハッカソンの進行
基本的な進行方向としてはFlutter初見が5日間のハッカソンでアプリ開発してきた をご覧いただければと思います。
大枠で紹介すると、
- 経験者を中心に技術の共有、キャッチアップ方針を定める
- 毎日進捗をすり合わせ、「やること」「やらないこと」を明確にし、確実にタスクをこなす
- オンラインでのコミュニケーションを円滑にするためのツールを活用する
の3つに特に注力していました。
経験者を中心に技術の共有、キャッチアップ方針を定める
自分は、1ヶ月強前にハッカソンで右往左往しながらFlutterのキャッチアップをしました。
その際に無駄だったことや、初めからやればよかったこと等の知見が溜まっていたため、チームメンバーに
- どのような手順で
- 何を参考に
- いつまでに
- 何をするか
をなるべく具体的に提示することで、効率よく学べるように心がけました。
具体的には、
Flutterは状態管理が少し難しい反面、UIは直感的に簡単に組めるため、
まず状態管理に慣れてもらいました。
- udemyを用いて
StatefulWidgetや、Providerの概念を知り、- 以前のおうちハッカソンで書いたコードを参考に
ChangeNotifierを理解し、- ブログや公式ドキュメント、自分のサンプル実装で
StateNotifierを使いこなせるようになるの流れで、初めの2〜3日の時間を使いました。
この状態管理packageを使う過程でWidgetの組み方もある程度は勉強できるため、4日後にはある程度実装できるようになっていたと思います。
この間に、自分は設計やCI,linterを導入したり、FirestoreのModelingで試行錯誤したり、快適に開発ができるような環境づくりに注力しました。
毎日進捗をすり合わせ、「やること」「やらないこと」を明確にし、確実にタスクをこなす
今回のハッカソンは1週間と、期間としては短くはないですが、それでも時間は限られています。
これはハッカソンに限った話ではないですが、限られた時間の中で形にするためには、
「やること」「やらないこと」を明確にする必要があります。さらに、知らない技術に触れる中で「できないこと」も判別してタスクを組むことも大事になってきます。
これらを共通認識として保つために、毎日Discordで進捗確認をし、タスクの割り振りや棚卸し、ゴールから逆算したときの進み具合をすり合わせました。
みんな実装に気を取られていた中、これを率先してくれたメンバーに感謝です?
オンラインでのコミュニケーションを円滑にするためのツールを活用する
今回、コロナや居住地の関係でオンラインでのハッカソン開催となりました。
そのため、コミュニケーションやアイデア出し、その他諸々は工夫する必要がありました。
- Notion
- Figma
- miro
- Whimsical
- Discord
それぞれの詳細な仕様方法等は触れませんが、Notionでドキュメントやその他情報を管理し、進捗やスケジュールの共有はとても有意義でした。
技術の話
今回、膨大な仕様と画面数はさることながら、1番の頑張りポイントはは技術的な挑戦でした。
繰り返しますが、
3/4はFlutter初見、残りはFlutter歴1ヶ月
全員がFirebase初心者(サンプル触った程度)
です。
触ったことのないプラットフォーム自体が挑戦でしたが、さらに最近流行のpacakgeを使用する等、設計にもこだわりました。
Flutterアプリ全体のArchitecture
発表スライドの貼り付けになりますが、アプリ全体のアーキテクチャとしては上図のようになります。
MVSN + Layered Architectureと書いていますが、SNはState Notifierのことです。これは自分が作った造語で、実際にこういった呼び方のアーキテクチャがあるわけではありませんので悪しからず...。
MVVMのViewModelが、
Viewを描画するための状態の保持と、Viewから受け取った入力を適切な形に変換してModelに伝達する役目を持つ
とwikiに定義されているため、広義の意味ではMVVMなのかもしれません。
ただ、普段Androidをしている自分のイメージなViewModelとは少し構造が違うように感じたため、
MVSNと呼んでみました。
使用したpackage
State Notifier
今回の設計の要となるpacakgeです。
スライドに記載したり、上述した通り、State NotifierはつまりViewModelです。
View側で必要な状態をStateクラスで持ち、StateNotifierを継承したクラス側で、状態を変更してあげます。
View側では、Provider packageを利用して単発のイベント呼び出したり、StateをObserveしておくことで、Stateに変更があった場合にWidgetを再描画したりします。
文字だけではイメージがつきづらいと思うため、例をあげます。
ex.) プロフィール詳細画面で、RepositoryからUser情報を取得し、Viewに反映する例
ProfileDetailState@freezed abstract class ProfileDetailState with _$ProfileDetailState { const factory ProfileDetailState({ User user, }) = _ProfileDetailState; }freezedに関しては後述しますが、このProfileDetailStateをKotlinでいうData Classとして記述している
と理解して大丈夫です。
色々書いてますが、基本はfreezedのお作法でLive Templateで補完できるため、ここでは
Userという、プロフィール詳細画面で表示するべき状態をもっていることに注目です。ProfileDetailControllerclass ProfileDetailController extends StateNotifier<ProfileDetailState> with LocatorMixin { ProfileDetailController() : super(const ProfileDetailState()); UserRepository get userRepository => read<UserRepository>(); Future<void> getProfileDetail() async { final User user = await userRepository.getMyInfo(); state = state.copyWith(user: user); } }さきほどの
ProfileDetailStateを持つ、StateNotifierを継承したクラスを作成します。ここでのポイントは2つ
- LocatorMixinを使って
UserRepositoryをinject(read)している- UserRepositoryから取得したuserを
state = state.copyWith(user: user);で更新しているです。
ChangeNotifierとの違いは、Controllerクラス(StateNotifier継承クラス)でローカル変数を持たず、StateNotifierのstateの状態を変更してあげるだけで良いことです。
notifyListenersをわざわざ呼ぶ手間は省けますね。ちなみに、Controllerは基本的には画面ごとに持っています。
ProfileDetailPageclass ProfileDetailPage extends StatelessWidget { @override Widget build(BuildContext context) { return // 略 body: StateNotifierProvider<ProfileDetailController, ProfileDetailState>( create: (_) => ProfileDetailController(), child: Builder( builder: (context) { return Column( children: <Widget>[ Text(context.select<ProfileDetailState, String>((state) => state.user?.fullName ?? 'no name')), GestureDetector( onTap: () async { await context.read<ProfileDetailController>().getProfileDetail(); }, child: const Icon(Icons.add), ) ], ); }, ), ), // 略 } }状態をobserveする際は、
context.select<ProfileDetailState, String>((state) => )単発のイベントを呼ぶ際は
context.read<ProfileDetailController>().getProfileDetail()のようにしています。
state_notifierに関してよりくわしく知りたい方は安定の @itomeさんのブログ を参照ください。
自分の環境設定が悪いのか、
readやselectの補完がデフォルトでは出ず、わざわざ手打ちでprovider pacakgeのimport文を書く必要があったところは少し不便でした。
freezed
freezedに関しては、上記の @itomeさんのブログ や、他にも色々な記事があるためここでは詳細には紹介しません。
簡単にいうと、KotlinのData Classや、Sealed Classに相当するデータ構造クラスが簡単に作成できるpacakageです。
上記のStateNotifierの例で言うと、
state = state.copyWith(user: user);のcopyWithメソッドがfreezedの機能にあたります。冗長なコードを書かずに完結にstateの状態を変更できるため、StateNotifierと相性が良いです。
とても便利なのですが、コード生成が伴うため、
- file構造が少し煩雑になってしまう
- Modelの変更をしたら再度コード生成コマンドを打つ必要がある(忘れがち)
と、少し不便なところもありました。
Json Serializable
上記のfreezedと併用するとより効果を発揮するpackageです。
fromJsonや、toJsonを、ボイラープレートなしにコード生成してくれるpacakgeです。
控えめに言って最高です。
ただこちらも、コード生成が伴うため、freezedで述べたデメリットや、
- プリミティブ型ではない独自のModelクラスは Converterを書く必要があり、ちょっと面倒くさい
- ModelにListが入っている場合、コード生成されたクラスで型が宣言されておらずlintエラー(
missing type parameter)が表示されるのがつらいというつらみポイントもありました。
RxDart
Widgetへの状態変更通知に関してはStateNotifierを使用しましたが、
ログイン状態の変化等の、RepositoryからControllerへの変更通知はStreamを使用することにしました。Dart標準のStreamでも事足りるのですが、BehaviorSubjectが使用したかったためRxDartを採用しました。
自分は普段Androidでインターンをしているため、LiveData/Coroiutineの世界に馴染みがあり少し詰まったポイントではありました。
FirestoreのModeling(議論とご指摘ほしいです)
今回1番苦労したのがこのFirestoreのModelingと実装です。(本当にきつかった、いまだに良く分からない)
AndroidとFlutterというクライアントサイドしか経験がなく、RDBすらまともに設計したことがない状態から、NoSQL風かつSubcollectionという特殊な概念を持ち合わせたFirestoreの設計をしたため、フィードバックいただけるとすごくありがたいです?♂️
概要
こんな感じで設計しました。
※実装している間に辛いところがちょくちょくあったりして、改善の余地ありまくりです。
Modelingに関しては、firestore-data-modelingを参考に勉強しました。
User - Hackathon(多対多)
まず、このアプリのコンセプトが
簡単にオンラインハッカソンを企画/運営できる というものです。
その中に、ユーザーが参加する際に入力すべき項目を減らし、参加障壁を低くするという目的もあります。
そのため、rootにUserがありアプリ全体としてユーザーのプロフィール等を保持し、並列してHackathonなcollectionがある感じです。
Discordをイメージしていただきたいのですが、
Discordでは、新しい
サーバーに参加した際にroot?のUser情報を元にアイコンと名前が表示されます。このHack ×2も同様に、新しいハッカソンに参加した際にrootのUser情報を使いまわしたく、この設計にしました。
多対多の表現は少し困ったのですが、中間テーブルを設けることで表現してみました。
どのユーザーがどのハッカソンに所属しているかを取得する中間テーブルかつ、ドロワーに参加しているハッカソンのアイコンを表示させたいので、urlも同時に持たせることで読み取り回数を減らしました。
Hackathon - Participant/Group/Notification(1対多)
Participant, Group, Notificationは全てHackathonの
SubCollectionで持っています。なぜなら、それぞれいくらでもスケールし得て、documentへの埋め込みだと1MBを超える可能性があるためです。
1対多に関しては、他にもroot collectionで持っている記事があったり、最適があまりよくわかっていません。
Participant とは、ハッカソンの参加者を表しており、Userを埋め込みで持っています。
Userをラップしており、他にはハッカソンで必要な情報(酸化可能日、稼働可能日数、希望職種etc...)のプロパティを持っています。
Group はその名の通り、ハッカソンで組むグループです。(チームという命名のほうが正しい...?)
Group と Participant は1対多の関係で、Participantは何人になるか不明なため
SubCollectionで持たせるようにしています。Notification は今回時間の関係(Modelingで分からないこともあり...)で実装していません。
- ハッカソンの管理者がお知らせを送信することができる
- 参加者はお知らせ画面で閲覧することができる
- 通知バッヂをつける
の要件があるとき、
Hackathon : Notification = 1 : 多になると思うのですが、
Participant : Notification = 1 : 多にもなる感じなのかな...?参加者の既読状況を表すのにはどうするのが正解なんでしょう...。
よければコメントいだければ幸いです。
全体図
Notification周りは未完成なのと、dartのModel Classとして記載しています。
実装
Androidでは簡単なサンプルを実装してみたことがあるのですが、FlutterでFirestoreを扱うのは初めてだったので色々つらみがあありました。
Firestoreからデータを取得してFlutterのfreezedなclassに変換する際、
- idを別で取り出す必要がある
- Future型で返却するためにこねくり回す
- toJson、fromJson時、CustomObjectが内包されている場合はConverterを書く必要がある
ことが手間でした。
これが生コードなのですが、かなり汚く苦悩が見えると思います...HackathonRepository// TODO: エラーハンドリング Future<Hackathon> getHackathon(String hackathonId) async { final DocumentReference hackRef = _firestore.collection('hackathons').document(hackathonId); Future<List<Map<String, dynamic>>> getJsonList(String collectionName) async => (await hackRef.collection(collectionName).getDocuments()).documents.map((document) { if (document.data.isNotEmpty) { return document.data..putIfAbsent('id', () => document.documentID); } else { return <String, dynamic>{}; } }).toList(); // TODO: 並列実行 => fromJsonするやり方を調べる final List<Map<String, dynamic>> participants = await getJsonList('participants'); final List<Map<String, dynamic>> groups = await getJsonList('groups'); final List<Map<String, dynamic>> notifications = await getJsonList('notifications'); await (await prefs).setString(HACKATHON_ID_KEY, hackRef.documentID); return Future.value(Hackathon.fromJson(hackSnapshot.data ..putIfAbsent('id', () => hackRef.documentID) ..putIfAbsent('participants', () => participants) ..putIfAbsent('groups', () => groups) ..putIfAbsent('notifications', () => notifications))); }Hackathonに紐づいているSubcollectionごと取得する良い方法があれば教えていただきたいです。
※今は個別で取得して、putIfAbsentで付け加えてる形。また、
Firestoreにデータをセットする際に、
- freezedなclassはidを@requiredにしているが、idはFirestore側で自動生成させたい場合にDTOクラスを作るのかパラメータをだけで渡すか
とかも結構面倒くさかったですね。
FirestoreのModel設計をコードを織り交ぜて解説している良い記事あれば教えていただきたいです。
おわりに
ハッカソンを通じて、話したことない同期同士で仲良くなったり、同期がどんなことが得意かが分かると同時に、技術的/非技術的な知見を互いに共有することができました。
今後も内定者間や人事の方と合同での企画を予定しているため、今回参加できなかった同期とも徐々に打ち解けていければ良いと思います。
そして、21年度の入社時までに色々な知見を溜め、仲を深め、入社時から即戦力として最高のパフォーマンスを出せるような新卒になれるように、組織として力を入れていきたいと思っています。
行動力、技術力、キャッチアップ力ともに同期を尊敬しました。
素晴らしいイベントでした。企画、運営、協力感謝です。
技術的な内容だけQiitaにして後はnoteに投稿するように分けようかな...?
- 投稿日:2020-06-24T13:13:59+09:00
タイマーアプリをリファクタリングする
はじめに
iPhoneアプリ開発集中講座にあるタイマーアプリのリファクタリングに挑戦してみたいと思います。
楽器アプリの「Day 1 Lesson 4-4 ステップアップ リファクタリングで見通しを改善しよう」(P.165) で学びましたね。タイマーアプリが完成してからでかまいませんので、楽器アプリで学んだことを思い出して、コードの冗長性をなくしてみてください。
(iPhoneアプリ開発集中講座 P.257より引用)リファリングとは
Twitterのツイートを引用しますが、ソフトウェア開発の上で読みづらくなったコードを整理して可読性の向上と勘違いによる不具合発生の防止が主な目的となると思います。
好きな文章を引用
— アユム@1月からWebエンジニア (@ex_endeavor) June 21, 2020
> ソフトウェアの開発は、繰りかえしに次ぐ繰り返しです。読みづらくなるまでコードを書き足し、そのコードをリファクタリングする、その繰り返しです。そして、より簡潔なコードを書くために、本書が少しでもお役に立てばと思います。実際に考えてみる
まずどんな構造にするのか?
タイマアプリは、下記のような用件があると思います
- タイマーカウントダウン
- タイマーのカウントダウンする設定値を読み出し
- タイマーのカウントダウンする設定値の書き込み
- UIの表示
4つほどあると思います。
そこでファイルを分けることを考えます。下記のように分けてみました。
用件 ファイル メモ タイマーカウントダウン CountDownManager.swift(新規) タイマーのカウントダウンを検討する タイマーのカウントダウンする設定値を読み出し SettingManager.swift(新規) UserDefaultsの読み出しを行う タイマーのカウントダウンする設定値の書き込み SettingManager.swift(新規) UserDefaultsの読み出しと、取りうる値を管理する UIの表示 ViewController.swift
SettingViewController.swift従来のファイルのままとする これから紹介するコードはこちらのgithubに公開しますので参考にしてください。
タイマーカウントダウン
タイマーカウントダウンするTimerManager.swiftを作りました。
課題となるのは、1秒毎にタイムアウトしたときにViewController.swiftにどのように通知するか?です。そこで今回はdelegateを用いてUI表示更新してみることにしました。
下記のようにdelegateメソッドを定義しています。
protocol TimerManagerDelegate: class { func timerInterrupt(remainCount:Int) }また、いくつかのメソッドを定義しました。
メソッド名 概要 start タイマーカウントダウンを開始する stop タイマーカウントダウンを停止する clear タイマーの設定値などを変わった時にクリアする また、外部公開する変数(プロパティー)を1つ定義しました。
変数名 概要 timerValue タイマーの設定時間 設定値を管理する
設定値を管理するSettingManager.swiftを作りました。
UIPickerViewで表示する設定の選択肢のリストと、タイマーの設定値の取得と設定する変数(プロパティー)を定義しました。
変数名 概要 setttingArray UIPickerViewで表示する設定の選択肢のリスト timerVaue タイマーの設定時間
UserDefaultsから値を取得、設定する
設定するときは、TimerManager.swiftに設定値を更新して、クリアをする起動
アプリが起動する時にタイマーカウントダウンするTimerManager.swiftのtimerValueを初期化する処理を追加しました。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // タイマーマネージャーのインスタンス取得 let timerManager = TimerManager.shared // 設定マネージャーのインスタンス取得 let settingManager = SettingManager.shared // 設定マネージャーで保持している設定時間をタイマーマネージャーに渡す timerManager.timerValue = settingManager.timerVaue return true }終わりに
様々なリファクタリングがあると思います。
まずは一例として捉えていただければと思います。ありがとうございました。
- 投稿日:2020-06-24T13:11:30+09:00
iOS 14 でさらに強化された位置情報まわりのプライバシー
※本記事は、一般に公開されている情報を元に作成しています。記事中の画像はWWDCのスライドのものを引用させて頂いております
WWDC 2020 開幕しましたね!位置情報まわりも色々変更があったのでまとめてみます。
正確な位置情報を使用するかどうかユーザーが選択できるようになった
iOS 14 では位置情報の使用許可を求められる際「Precise On(正確な位置情報)」オプションが表示されます。Precise は「正確な」という意味です。
このオプションをオフにすると、例えば地図を利用する場合はおなじみの青いドットは表示されず、このような大きめの円が現在地として表示されるようになります。
ユーザーは設定画面からこの設定を変更することができます。
実際にユーザーが居る位置とは数キロメートルずれることが予想されるので、正確な位置情報を前提としたアプリは注意が必要ですね。
ユーザーの許可ステータスは
accuracyAuthorizationで取得できますこの許可ステータスは iOS、Apple Watch と同期されます。
正確な位置情報を必要としないアプリの場合
正確な位置情報を必要としないことがわかっている場合は Info.plist の
NSLocationDefaultAccuracyReducedを true にしておくと、デフォルトでこのオプションが非表示になります。このとき
accuracyAuthorizationは.reducedAccuracyになります。iOS 14時代の位置情報許可ステータスチェック
前述のように、iOS 14以降は「位置情報の使用を許可しているか」に加え「正確な位置情報の使用を許可しているか」が追加されたので、次のようにステータスをチェックすると良いでしょう。(WWDCのスライドから引用)
switch manager.authorizationStatus() { case .authorizedAlways, .authorizedWhenInUse: // 位置情報の使用を許可している case .notDetermined, .denied, .restricted: // 位置情報の使用を許可していない default: // Unhandled case } switch manager.accuracyAuthorization { case .fullAccuracy: // 正確な位置情報の使用を許可している case .reducedAccuracy: // 正確な位置情報の使用を許可していない default: // Unhandled case }一時的にPrecise設定をオンにもできる
例えばナビ機能を使うときだけ正確な位置情報の取得を許可する、みたいな使い方もできます。
if locationManager.accuracyAuthorization == .reducedAccuracy { locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: "WantsToNavigate") { _ in if self.locationManager.accuracyAuthorization == .fullAccuracy { print("begin navigate") } } } else { print("begin navigate") }Info.plist の
NSLocationTemporaryUsageDescriptionDictionaryには「一時的に正確な位置情報を使う理由」を複数セットできます。通常は正確な位置情報を必要としないアプリの場合、とても有効ですね。
App Clips の位置情報について
App Clips から位置情報を使用する場合は選択肢が
- 1度だけ許可
- 明日まで許可
になるので注意してください。
つまり、App Clips は「常に位置情報を許可する」が選択できません。
※これを、12時を過ぎれば馬車もドレスも魔法が解けて元に戻ってしまうシンデレラに例えて説明してるのが面白かったです。
WidgetKit での位置情報について
Widgets で位置情報を使用するには Info.plist に
NSWidgetWantsLocationをセットします。Widgets からは位置情報の許可を求めるダイアログは表示できないので、親アプリで位置情報の使用を許可してもらう必要があります。このあたり、どんなフローでユーザーに許可してもらうか?が重要になってきそうですね。
まとめ
今年の What's new in location をひとことでまとめるなら 「必要なときに、必要な権限を」 といったところでしょうか。
Appleが、よりプライバシーに配慮している印象を受けました。リンク
- 投稿日:2020-06-24T11:37:18+09:00
iOSのWidgetについて
⚠️NDAに違反しない情報のみ掲載します。またスクリーンショットは掲載できません。随時追記
概要
iOSのWidgetは、Androidと違い自由度は少ない。
ただしその分統一感があったり、見やすさや操作性で優れている。
基本的にこれも対応必須になると思うので、備忘録として書いておく。仕様
- 2×2、2×4、4×4の3段階
- iPhoneではホーム画面に表示できる
- iPadはホーム画面で常に表示
- Macでは通知センターに表示される
- タッチされるとアプリが開くため、タッチイベントは利用できない
- SwiftUIで記述可能 #バグ Xcodeからの書き込みの際、再起動しないと反映されない(beta1)
- 投稿日:2020-06-24T02:30:23+09:00
prefersStatusBarHidden が false を返すように実装しても iPhone の Landscape モードではステータスバーが常に非表示となる
概要
最近のバージョンの iOS を搭載した iPhone の Landscape モードでステータスバーを表示させる方法がないか、いろいろと試してみたのですが、どうやらそのような方法はないようです。(方法が見つかりませんでした。)
確認環境
ビルド環境
- macOS 10.15.5
- Xcode 11.5 (11E608c)
- Deployment Target: iOS 11.0
- User Interface が Storyboard の Xcode プロジェクトを使用
実行環境
- iPhone 11 Pro Max (iOS 13.5.1)
- iOS Simulator (iPhone Xs Max, iOS 12.4)
- iOS Simulator (iPad Air 2, iOS 12.4)
試したこと
prefersStatusBarHiddenがfalseを返すようにするステータスバーを表示させたい画面の View Controller で
UIViewControllerクラスのプロパティprefersStatusBarHidden1 をオーバーライドしてfalseを返すようにします。つまり、常にステータスバーを表示するように設定するのです。ViewController.swiftimport UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() } override var prefersStatusBarHidden: Bool { return false } }
Info.plistでView controller-based status bar appearanceの値にYESを設定
prefersStatusBarHiddenをオーバーライドしただけでは、対象の画面を表示した時や画面回転時にprefersStatusBarHiddenの値が参照されません。次の画像のように、 Xcode で対象のビルドターゲットについての
Info.plistの内容を表示し、その中でView controller-based status bar appearanceの項目の値にYESを設定します。これにより、上述のprefersStatusBarHiddenの値が使用されるようになります。なお、
View controller-based status bar appearanceという項目は、Info.plistのソースコードではUIViewControllerBasedStatusBarAppearance2 というキーで表されます。Info.plist<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <!-- 省略 --> <key>UIViewControllerBasedStatusBarAppearance</key> <true/> </dict> </plist>なるべくシンプルな画面構成で動作確認
UINavigationControllerやUITabBarControllerなどの Container View Controller を使う場合、childForStatusBarHiddenを適切に設定しなければなりません。今回はprefersStatusBarHiddenの値の使い方を中心に確認したいため、 こうした Container View Controller は使用せずに、なるべくシンプルな画面構成で動作確認することにしました。Xcodeの新規プロジェクト作成メニューから
Single View Appテンプレートを選択して Xcode プロジェクトを作成し、そのプロジェクト内のViewControllerクラスに変更を加えることにより動作確認しました。確認結果
上記の設定を試してみたのですが、 iPhone では画面表示時や画面回転時に
prefersStatusBarHiddenが呼び出されるものの、 Landscape モードのときにはステータスバーが表示されません。( iPad では常に、また、iPhone が Portrait モードのときには、ステータスバーが表示されます。)Developer Documentation の記述から判断できること
あらためて
prefersStatusBarHiddenについての Developer Documentation1 の Discussion を確認してみたところ、次のように書かれていました。By default, this method returns false with one exception. For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.この記述によると、
prefersStatusBarHiddenが返す値には以下の規則性があるということです。
- デフォルトでは
falseを返す。- iOS 8 以降にリンクされたアプリでは View Controller の Size Class の横幅が
Compactの場合にはtrueを返す。また、 Human Interface Guidelines に記載されている各デバイスの Size Class の一覧表を見ると、 iPhone の全機種の Size Class の横幅が
Compactであることがわかります。実際に動作確認してみた結果と照らし合わせてみると、オーバーライドした
prefersStatusBarHiddenが常にfalseを返すようになっていたとしても、その View Controller のSize Class の横幅がCompactの場合には、trueを返しているものとして iOS の内部では評価されているようです。










































