20200624のiOSに関する記事は8件です。

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の違いは??

公式のドキュメントの理解が追いついていない

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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で購入する → スムージーを受け取る
スクリーンショット 2020-06-24 10.19.44.png
このようにNFCタグにApp Clipで使うURL情報を書き込んでおくと、ユーザがタップしたときにNSUserActivityを経由してURL情報が渡され、直接注文ページを開くことができる。

App Clipの起動方法いろいろ

  • NFCタグ:URLが記録されたタグを読み込み
  • QRコード:URLが記録されたコードを読み込み
  • 地図アプリ:プレースカードのApp Clip URLをタップ
  • Siri:位置情報ベースのサジェストをタップ
  • Safari:バナーをタップ
  • iMessageアプリ:ウェブページのURLを送信したときに表示されるバナーをタップ

Appleは、App Clip用のビジュアルコードを作成できるツールを今年後半にリリースする予定。

設定手順

大きく分けると以下の3つの手順を踏む。

  1. Webサーバ上の設定の変更とApp Clipプロジェクトの作成
  2. App Clipカードの設定(App Store Connect上でのデフォルト設定)
  3. 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のドメインを追記する。
スクリーンショット 2020-06-24 17.54.50.png
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で起動されるようになる。
スクリーンショット 2020-06-24 19.17.39.png

App Clipカードの表示要件

App Clipカードに載せるメタデータを考えるときは、以下に従うとより適切な表示ができる。
スクリーンショット 2020-06-24 19.47.00.png

画像はApp Clipアクションにマッチしたものにすると良い。

App Store Connectでの設定

アプリとApp Clipの両方を含むビルドをApp Store Connectに送ると、App Store Connect上でApp Clipの設定をするセクションが表示されているはず。ここでデフォルトのApp Clip設定をする。予め定義されたアクションのリストがあるので、それも選択する。
スクリーンショット 2020-06-24 19.51.33.png
Safariやメッセージ以外からもApp Clipにアクセスできるようにしたい場合は、Advanced App Clip ExperiencesセクションのGet Startedをクリックし、高度な設定を行う。この高度な設定をすることでNFCタグやQRコードなどの物理的な方法からも呼び出しが可能になる。

いくつか画面を進めるとこのようなページが表示される。複数のApp Clip Experienceを設定することもできる。
スクリーンショット 2020-06-24 21.29.32.png
次のページでは画像やタイトルなどを指定する。
スクリーンショット 2020-06-24 21.36.02.png

App Clipの複数設定

レストランアプリの場合、注文用App Clip Experience(melamela.example/order)と予約用App Clip Experience(melamela.example/reservation)を設定することもできる。
スクリーンショット 2020-06-24 21.38.46.png

ベストプラクティス

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設定をしていたら、その設定に従った表示が出てくる。
スクリーンショット 2020-06-24 22.24.53.png

デモ

スムージーアプリの実装例はこんなかんじ。
スクリーンショット 2020-06-24 22.40.33.png

Test Flight

アプリとApp Clipの両方を含むビルドをApp Store Connectに配信した後、Test FlightタブからApp Clipセクションを見つけて、「Add App Clip Invocation」をクリックする。タイトルとURLを設定すればTest Flightでテストできるようになる。
スクリーンショット 2020-06-24 22.55.51.png
App Store Connectでのテストの詳細については「What's New in App Store Connect」にて。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Xcode 11でiOS9のプロジェクトの作成方法

はじめに

iOS9以降をサポートするアプリを作ることになったので、最新のXcodeで新規プロジェクトを作ってからiOS9向けにダウングレードする方法について説明します。
(普通に作るとiOS13になる。自分への備忘録も兼ねて記載しておく)

執筆時点の環境

  • macOS Catalina version 10.15.4
  • Xcode version 11.4.1

1. Xcodeで新規プロジェクトを作成

では、Xcodeの操作を順に説明します。

#1.png

File -> New -> Project で、新規プロジェクトを指定します。説明するまでもないですね。
#2.png
Single View Appを選びましたが、他でも同じです。

#3.png
プロジェクト名とかは適当に付けます。

2. TargetにiOS9を指定する

#4.png
当然ですが、ターゲットにiOS9を指定します。

3. InfoからSceneを消す

#5.png
SceneはiOS13からサポートされた機能のため、上図の青で囲まれたScene関係の情報を削除します(4つの情報を下から順に(-)をクリック)。

4. SceneDelegate.swiftを削除

#6.png

使えませんので、SceneDelegate.swiftをプロジェクトから削除します。が、13行目のwindowの変数宣言をAppDelegate.swiftに移すので、コピーしておくとよいかも。

5. AppDelegate.swiftのコードの変更

#7.png
SceneDelegate.swiftを消したので、AppDelegate.swiftにコンパイルエラーがいくつか出てますが、まず、windowの変数宣言をここ(14行目)に入れます。
#8.png

#9.png
コンパイルエラーが出ている2つのメソッドをバッサリ削除します。
#10.png

以上で、iOS9プロジェクトの出来あがりです。WarningもErrorもなくビルドできるはずです。シミュレータを起動すると、真っ白画面のアプリが立ち上がります。
(寂しいので中央にラベルを貼りました)
#11.png

おわりに

自分はこの状態のプロジェクトをiOS9プロジェクトの雛型として保存してあります。必要な時にプロジェクトフォルダ一式をコピーしてiOS9アプリ開発時に利用します。  以上

 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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するという意味での命名です、チームメンバーに命名マスターがいて即決まりました(さすが)

スクリーンショット 2020-06-24 19.34.06.png

ハッカソン期間中はミニマムで実装をしましたが、元々の想定では15画面ほどありました。

シンプルにハッカソンで作る規模ではないですねw 

また、要件的に通信が伴うため、サーバーサイドのいないこのチームではFirebase等のmBaaSを使用する必要がありました。

この、FlutterおよびFirebase初心者チームが、どのように膨大な画面数/仕様が存在するアプリを1週間で作成したか、その道のりについて記述できればと思います。

キャッチアップとハッカソンの進行

基本的な進行方向としてはFlutter初見が5日間のハッカソンでアプリ開発してきた をご覧いただければと思います。

大枠で紹介すると、

  • 経験者を中心に技術の共有、キャッチアップ方針を定める
  • 毎日進捗をすり合わせ、「やること」「やらないこと」を明確にし、確実にタスクをこなす
  • オンラインでのコミュニケーションを円滑にするためのツールを活用する

の3つに特に注力していました。

経験者を中心に技術の共有、キャッチアップ方針を定める

自分は、1ヶ月強前にハッカソンで右往左往しながらFlutterのキャッチアップをしました。
その際に無駄だったことや、初めからやればよかったこと等の知見が溜まっていたため、チームメンバーに

  • どのような手順で
  • 何を参考に
  • いつまでに
  • 何をするか

をなるべく具体的に提示することで、効率よく学べるように心がけました。

具体的には、
Flutterは状態管理が少し難しい反面、UIは直感的に簡単に組めるため、
まず状態管理に慣れてもらいました。

  1. udemyを用いて StatefulWidget や、 Provider の概念を知り、
  2. 以前のおうちハッカソンで書いたコードを参考に ChangeNotifier を理解し、
  3. ブログや公式ドキュメント、自分のサンプル実装で StateNotifier を使いこなせるようになる

の流れで、初めの2〜3日の時間を使いました。

この状態管理packageを使う過程でWidgetの組み方もある程度は勉強できるため、4日後にはある程度実装できるようになっていたと思います。

この間に、自分は設計やCI,linterを導入したり、FirestoreのModelingで試行錯誤したり、快適に開発ができるような環境づくりに注力しました。

毎日進捗をすり合わせ、「やること」「やらないこと」を明確にし、確実にタスクをこなす

今回のハッカソンは1週間と、期間としては短くはないですが、それでも時間は限られています。

これはハッカソンに限った話ではないですが、限られた時間の中で形にするためには、
「やること」「やらないこと」を明確にする必要があります。

さらに、知らない技術に触れる中で「できないこと」も判別してタスクを組むことも大事になってきます。

これらを共通認識として保つために、毎日Discordで進捗確認をし、タスクの割り振りや棚卸し、ゴールから逆算したときの進み具合をすり合わせました。

みんな実装に気を取られていた中、これを率先してくれたメンバーに感謝です?

オンラインでのコミュニケーションを円滑にするためのツールを活用する

今回、コロナや居住地の関係でオンラインでのハッカソン開催となりました。

そのため、コミュニケーションやアイデア出し、その他諸々は工夫する必要がありました。

スクリーンショット 2020-06-24 19.35.12.png

  • Notion
  • Figma
  • miro
  • Whimsical
  • Discord

それぞれの詳細な仕様方法等は触れませんが、Notionでドキュメントやその他情報を管理し、進捗やスケジュールの共有はとても有意義でした。

技術の話

今回、膨大な仕様と画面数はさることながら、1番の頑張りポイントはは技術的な挑戦でした。

繰り返しますが、

3/4はFlutter初見、残りはFlutter歴1ヶ月

全員がFirebase初心者(サンプル触った程度)

です。

触ったことのないプラットフォーム 自体が挑戦でしたが、

さらに最近流行のpacakgeを使用する等、設計にもこだわりました。

Flutterアプリ全体のArchitecture

スクリーンショット 2020-06-24 19.37.30.png

発表スライドの貼り付けになりますが、アプリ全体のアーキテクチャとしては上図のようになります。

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 という、プロフィール詳細画面で表示するべき状態をもっていることに注目です。

ProfileDetailController
class 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は基本的には画面ごとに持っています。

ProfileDetailPage
class 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の設計をしたため、フィードバックいただけるとすごくありがたいです?‍♂️

概要

スクリーンショット 2020-06-24 19.39.40.png

こんな感じで設計しました。

※実装している間に辛いところがちょくちょくあったりして、改善の余地ありまくりです。

Modelingに関しては、firestore-data-modelingを参考に勉強しました。

User - Hackathon(多対多)

スクリーンショット 2020-06-24 19.40.28.png

まず、このアプリのコンセプトが

簡単にオンラインハッカソンを企画/運営できる というものです。

その中に、ユーザーが参加する際に入力すべき項目を減らし、参加障壁を低くするという目的もあります。

そのため、rootにUserがありアプリ全体としてユーザーのプロフィール等を保持し、並列してHackathonなcollectionがある感じです。

Discordをイメージしていただきたいのですが、

Discordでは、新しい サーバー に参加した際にroot?のUser情報を元にアイコンと名前が表示されます。

このHack ×2も同様に、新しいハッカソンに参加した際にrootのUser情報を使いまわしたく、この設計にしました。

多対多の表現は少し困ったのですが、中間テーブルを設けることで表現してみました。

どのユーザーがどのハッカソンに所属しているかを取得する中間テーブルかつ、ドロワーに参加しているハッカソンのアイコンを表示させたいので、urlも同時に持たせることで読み取り回数を減らしました。

Hackathon - Participant/Group/Notification(1対多)

スクリーンショット 2020-06-24 19.41.03.png

Participant, Group, Notificationは全てHackathonの SubCollection で持っています。

なぜなら、それぞれいくらでもスケールし得て、documentへの埋め込みだと1MBを超える可能性があるためです。

1対多に関しては、他にもroot collectionで持っている記事があったり、最適があまりよくわかっていません。

Participant とは、ハッカソンの参加者を表しており、Userを埋め込みで持っています。

Userをラップしており、他にはハッカソンで必要な情報(酸化可能日、稼働可能日数、希望職種etc...)のプロパティを持っています。

Group はその名の通り、ハッカソンで組むグループです。(チームという命名のほうが正しい...?)

GroupParticipant は1対多の関係で、Participantは何人になるか不明なため SubCollection で持たせるようにしています。

Notification は今回時間の関係(Modelingで分からないこともあり...)で実装していません。

  • ハッカソンの管理者がお知らせを送信することができる
  • 参加者はお知らせ画面で閲覧することができる
  • 通知バッヂをつける

の要件があるとき、

Hackathon : Notification = 1 : 多 になると思うのですが、

Participant : Notification = 1 : 多 にもなる感じなのかな...?

参加者の既読状況を表すのにはどうするのが正解なんでしょう...。

よければコメントいだければ幸いです。

全体図

Notification周りは未完成なのと、dartのModel Classとして記載しています。

スクリーンショット 2020-06-24 19.41.18.png

実装

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に投稿するように分けようかな...?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

タイマーアプリをリファクタリングする

はじめに

iPhoneアプリ開発集中講座にあるタイマーアプリのリファクタリングに挑戦してみたいと思います。

楽器アプリの「Day 1 Lesson 4-4 ステップアップ リファクタリングで見通しを改善しよう」(P.165) で学びましたね。タイマーアプリが完成してからでかまいませんので、楽器アプリで学んだことを思い出して、コードの冗長性をなくしてみてください。
(iPhoneアプリ開発集中講座 P.257より引用)

リファリングとは

Twitterのツイートを引用しますが、ソフトウェア開発の上で読みづらくなったコードを整理して可読性の向上と勘違いによる不具合発生の防止が主な目的となると思います。

実際に考えてみる

まずどんな構造にするのか?

タイマアプリは、下記のような用件があると思います

  1. タイマーカウントダウン
  2. タイマーのカウントダウンする設定値を読み出し
  3. タイマーのカウントダウンする設定値の書き込み
  4. UIの表示

4つほどあると思います。

そこでファイルを分けることを考えます。下記のように分けてみました。

用件 ファイル メモ
タイマーカウントダウン CountDownManager.swift(新規) タイマーのカウントダウンを検討する
タイマーのカウントダウンする設定値を読み出し SettingManager.swift(新規) UserDefaultsの読み出しを行う
タイマーのカウントダウンする設定値の書き込み SettingManager.swift(新規) UserDefaultsの読み出しと、取りうる値を管理する
UIの表示 ViewController.swift
SettingViewController.swift
従来のファイルのままとする

スクリーンショット 2020-06-30 15.11.30.png

これから紹介するコードはこちらの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
    }

終わりに

様々なリファクタリングがあると思います。

まずは一例として捉えていただければと思います。ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS 14 でさらに強化された位置情報まわりのプライバシー

※本記事は、一般に公開されている情報を元に作成しています。記事中の画像はWWDCのスライドのものを引用させて頂いております

WWDC 2020 開幕しましたね!位置情報まわりも色々変更があったのでまとめてみます。

正確な位置情報を使用するかどうかユーザーが選択できるようになった

iOS 14 では位置情報の使用許可を求められる際「Precise On(正確な位置情報)」オプションが表示されます。Precise は「正確な」という意味です。

image.png

このオプションをオフにすると、例えば地図を利用する場合はおなじみの青いドットは表示されず、このような大きめの円が現在地として表示されるようになります。

image.png

ユーザーは設定画面からこの設定を変更することができます。

スクリーンショット 2020-06-24 6.51.00.png

実際にユーザーが居る位置とは数キロメートルずれることが予想されるので、正確な位置情報を前提としたアプリは注意が必要ですね。

ユーザーの許可ステータスは accuracyAuthorization で取得できます

image.png

この許可ステータスは iOS、Apple Watch と同期されます。

正確な位置情報を必要としないアプリの場合

正確な位置情報を必要としないことがわかっている場合は Info.plist の NSLocationDefaultAccuracyReduced を true にしておくと、デフォルトでこのオプションが非表示になります。

スクリーンショット 2020-06-24 8.00.36.png

このとき 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 には「一時的に正確な位置情報を使う理由」を複数セットできます。

image.png

通常は正確な位置情報を必要としないアプリの場合、とても有効ですね。

App Clips の位置情報について

App Clips から位置情報を使用する場合は選択肢が

  • 1度だけ許可
  • 明日まで許可

になるので注意してください。

スクリーンショット 2020-06-24 8.13.08.png

つまり、App Clips は「常に位置情報を許可する」が選択できません。

※これを、12時を過ぎれば馬車もドレスも魔法が解けて元に戻ってしまうシンデレラに例えて説明してるのが面白かったです。

WidgetKit での位置情報について

Widgets で位置情報を使用するには Info.plist に NSWidgetWantsLocation をセットします。

image.png

Widgets からは位置情報の許可を求めるダイアログは表示できないので、親アプリで位置情報の使用を許可してもらう必要があります。このあたり、どんなフローでユーザーに許可してもらうか?が重要になってきそうですね。

まとめ

今年の What's new in location をひとことでまとめるなら 「必要なときに、必要な権限を」 といったところでしょうか。
Appleが、よりプライバシーに配慮している印象を受けました。

リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSのWidgetについて

⚠️NDAに違反しない情報のみ掲載します。またスクリーンショットは掲載できません。随時追記

概要

iOSのWidgetは、Androidと違い自由度は少ない。
ただしその分統一感があったり、見やすさや操作性で優れている。
基本的にこれも対応必須になると思うので、備忘録として書いておく。

仕様

  • 2×2、2×4、4×4の3段階
  • iPhoneではホーム画面に表示できる
  • iPadはホーム画面で常に表示
  • Macでは通知センターに表示される
  • タッチされるとアプリが開くため、タッチイベントは利用できない
  • SwiftUIで記述可能 #バグ Xcodeからの書き込みの際、再起動しないと反映されない(beta1)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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)

試したこと

prefersStatusBarHiddenfalse を返すようにする

ステータスバーを表示させたい画面の View Controller で UIViewController クラスのプロパティ prefersStatusBarHidden1 をオーバーライドして false を返すようにします。つまり、常にステータスバーを表示するように設定するのです。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override var prefersStatusBarHidden: Bool {
        return false
    }
}

Info.plistView controller-based status bar appearance の値に YES を設定

prefersStatusBarHidden をオーバーライドしただけでは、対象の画面を表示した時や画面回転時に prefersStatusBarHidden の値が参照されません。

次の画像のように、 Xcode で対象のビルドターゲットについての Info.plist の内容を表示し、その中で View controller-based status bar appearance の項目の値に YES を設定します。これにより、上述の prefersStatusBarHidden の値が使用されるようになります。

スクリーンショット 2020-06-23 23.52.41.png

なお、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>

なるべくシンプルな画面構成で動作確認

UINavigationControllerUITabBarController などの 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 の内部では評価されているようです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む