iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

【Kotlin】Fragment のイベントを Activity で受け取る

本日も初歩的な内容ですが、Fragment のイベントを Activity で受け取る実装を行ったので、その方法をメモ程度に残しておきます🏃🏻‍♂️

それではやっていく

Fragment 側の実装は下記のようになります。重要な部分は OnboardSignUpTermsOfServiceListener に関するところで、Activity に通知したいイベントを Interface として定義しています。OnboardSignUpTermsOfServiceListenerインスタンス化は、FragmentonAttach() メソッドで行います。onAttach() メソッドは Fragment が Activity に追加される時 に呼び出され、引数として追加する Activity がキャストされて渡されます。また、FragmentActivity から関連付けられなくなった場合にイベントが通知され続けるのを防ぐため onDetach() でリスナーを破棄しています。

class OnboardSignUpTermsOfServiceFragment : Fragment() {

    interface OnboardSignUpTermsOfServiceListener {
        fun onClickNext()
    }

    private var _binding: FragmentOnboardSignUpTermsOfServiceBinding? = null
    private val binding get() = _binding!!
    private var listener: OnboardSignUpTermsOfServiceListener? = null

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentOnboardSignUpTermsOfServiceBinding.inflate(inflater, container, false)

        binding.nextButton.setOnClickListener {
            onClickNext(it)
        }

        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        listener = context as? OnboardSignUpTermsOfServiceListener
        if (listener == null) {
            throw ClassCastException("$context must implement OnboardSignUpTermsOfServiceListener")
        }
    }

    override fun onDetach() {
        super.onDetach()
        listener = null
    }

    fun onClickNext(view: View) {
        listener?.onClickNext()
    }
}

最後に Activity 側で Interface に準拠することでイベントを受け取れるようになります。

class OnboardSignUpActivity : AppCompatActivity(),
    OnboardSignUpTermsOfServiceFragment.OnboardSignUpTermsOfServiceListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_onboard_sign_up)
    }

    // MARK: OnboardSignUpTermsOfServiceListener
    override fun onClickNext() {
        // TODO: Handle events.
    }
}

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

【Swift】Algolia からデータを取得する

みなさんこんにちは。最近個人アプリ開発に熱中しすぎるあまりネタが切れている Yamato です👷‍♀️ というわけで、本日はタイトルの通り Algolia からデータを取得する方法を紹介したいと思います。

結論

というわけで、下記がデータを取得するサンプルになります。SearchClientAlgoliaappIDapiKeyインスタンス化し、index で検索するインデックスを指定します。あとは、Querykeyword を設定すれば全文検索でデータを取得することができます。結果は Swift の Result 型でコールバックされるので、それぞれよしなにハンドリングします。

let perPage = 20
let page = 1
let index = SearchClient(appID: "YOUR_ALGOLIA_APP_ID", apiKey: "YOUR_ALGOLIA_APIK_EY").index(withName: "dev_guide")

let task = index.search(query: Query(keyword).set(\.hitsPerPage, to: perPage).set(\.page, to: page)) { result in
    switch result {
    case .success(let response):
        let guides = response.hits.compactMap { hit -> AlgoliaGuideData? in
            guard let object = hit.object.object(),
                  let data = try? JSONSerialization.data(withJSONObject: object),
                  let guide = try? JSONDecoder().decode(AlgoliaGuideData.self, from: data)
            else { return nil }
            return guide
        }
        // TODO: Handle data.
    case .failure(let error):
        // TODO: Handle error.
    }
}

という感じで本日も以上になります🤺

参考

github.com

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

【Firestore】Swift で該当する月のデータを取得する

今回は、Firestore に保存されているデータで、該当する月のデータのみを取得してくる方法のサンプルを紹介していきたいと思います。

結論

サンプル実装は、Ballcap という Firestore のラッパーライブラリを使っていますが、基本的にやることは同じで isGreaterThanOrEqualToisLessThanOrEqualToDate の範囲を指定してあげます。今回の場合は、該当する月の範囲でデータを取得したいので、Calendar.current.dateComponents([.year, .month], from: date)date が該当する月の初日の Date を取得しています。

github.com

extension DataSource.Query {
    func `where`(_ keyPath: String, inThisMonth date: Date) -> DataSource.Query {
        let components = Calendar.current.dateComponents([.year, .month], from: date)
        guard let start = Calendar.current.date(from: components),
              let end = Calendar.current.date(byAdding: .month, value: 1, to: start) else {
            fatalError("Could not find start date or calculate end date.")
        }
        return `where`(keyPath, isGreaterThanOrEqualTo: start).`where`(keyPath, isLessThanOrEqualTo: end)
    }
}

該当する月ではなく、該当する日を取得したい場合は下記のようにします。

extension DataSource.Query {
    func `where`(_ keyPath: String, inThisDay date: Date) -> DataSource.Query {
        let components = Calendar.current.dateComponents([.year, .month, .day], from: date)
        guard let start = Calendar.current.date(from: components),
              let end = Calendar.current.date(byAdding: .day, value: 1, to: start) else {
            fatalError("Could not find start date or calculate end date.")
        }
        return `where`(keyPath, isGreaterThanOrEqualTo: start).`where`(keyPath, isLessThanOrEqualTo: end)
    }
}

という感じで本日も以上になります👷‍♀️ それではまた明日〜

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

【Firestore】複合インデックスを作ってみる

みなさん、Firestore は使っていますか?僕は、個人で開発中のアプリでがっつり利用させてもらっている(Firebase信者)ので日々 Firestore と格闘しています🤺

今回はそんな Firestore で複合インデックスを作成する機会があったのでメモしておきます。

複合インデックスってなんぞや?

複合インデックス とは、複数のフィールドの条件に応じて並べ替えられ、マッピングされたもののことを指します。Firestore では、単一のフィールドに対するインデックスである 単一フィールドインデックスFirestore の自動インデックスという機能によって保持されます。ちなみに、自動的に作成されるインデックスはフィールドの種類によって異なり、具体的には下記のようなものが作成されます。

  • 配列でもなくマップでもないフィールドに対しては、コレクションのスコープを使用する 2 つの単一フィールド インデックス(1 つは昇順モード、1 つは降順モード)が定義されます。

  • マップ フィールドに対しては、マップ内の配列でもなくマップでもないサブフィールドごとに、コレクションのスコープを使用する 1 つの昇順インデックスと 1 つの降順インデックスが作成されます。

  • ドキュメント内の配列フィールドに対しては、コレクションのスコープ配列の内容インデックスが作成、維持されます。

参照: https://firebase.google.com/docs/firestore/query-data/index-overview?hl=ja#automatic_indexing

Firestore では、フィールドの増加により、可能となるフィールドの組み合わせが多くなってしまうため自動的に 複合インデックス が作成される事はなく、今回のように開発者自身がインデックスを設定する必要があります。

実際に作ってみる

複合インデックスを作成する手順は主に二つあり、一つ目が Firebase Console 上で作成する方法で、もう一つが Firebase CLI を使って作成する方法です。基本的にプロジェクトが管理しているインデックスを git などでバージョン管理などする必要がない場合は、前者の方法が最も簡単に行うことができます。また、複合クエリ の設定が必要なリクエストを Firestore に投げた場合(複合インデックスの設定が未完了)にはコンソールに複合インデックス` を作成するためのリンクが表示されるので、それを元に作成するとシンプルに必要なクエリだけを追加することができます。

今回は、git などでインデックスをバージョン管理したいので、後者の方法で 複合インデックス を作成していきたいと思います。

ちなみに、Firebase CLI のセットアップは完了している前提ですので、まだ完了していない場合は以前に僕が書いた記事を参照していただければいくらか参考になるかと思います。

yamato8010.hatenablog.com

yamato8010.hatenablog.com

それでは早速、Firebase プロジェクトのルートにある firestore.indexes.json を開いて編集していきます。今回の例では、guide という Map 値に含まれる uid という String 値 と eventDate という Timestamp 値 による 複合インデックス を作成しています。

{
  "indexes": [
    {
      "collectionGroup": "requests",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "guide.uid",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "eventDate",
          "order": "ASCENDING"
        }
      ]
    }
  ],
  "fieldOverrides": []
}

まずは、collectionGroup で、複合インデックスを作成する対象となる Collection を指定します。次に queryScope で、単一のコレクションとして作成するか、もしくは CollectionGroupとして作成するかを選択しています。今回は単一のコレクションとして作成したかったので、COLLECTION としています。最後に、条件となるフィールを fields 配列に設定していきます。今回は前述の通り guide.uideventDate のフィールドを使用したかったので、それぞれ定義をしています。order は、どの並び順でインデックスを保持するのかを決定するために指定する必要があります。今回は、ASCENDING として保持しました。なお、guide.uid によって DESCENDING の順で結果を取得したい場合や、eventDate によって DESCENDING の順で結果したい場合などは、別途 field を追加して設定するか、元の fieldorder を書き換える必要があります。Firestore で不等式句を使用してクエリを行う場合は、デフォルトで ASCENDING によって結果が取得されるので、DESCENDING のみでインデックスを作成して、うっかりそのままリクエストしてしまうとエラーが発生してしまうので注意が必要です。

最後に、作成したインデックスをデプロイしたら完了です🎉

$ firebase deploy --only firestore:indexes

という感じで本日も以上になります👷‍♀️ それではまた明日。

参考

firebase.google.com

firebase.google.com

firebase.google.com

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

【Swift】現在から〇〇時間後のDateを取得する

本日も初学者向けの内容で、現在から〇〇時間後や〇〇日前の Date を取得する方法を紹介したいと思います。

結論

下記のように Calendarインスタンスメソッド date(byAdding:to:options:)DateComponents.hour を、value1 を追加することで 1時間後Date を取得することができます。なお、戻り値として返ってくる Date はオプショナルになります。

let now = Date()
let hourLater = Calendar.current.date(byAdding: .hour, value: 1, to: now) // 1時間後

次に現在から2日後の Date を取得してみます。同様に DateComponents に時間の単位を value に時間差を指定することで取得できます。

let now = Date()
let twoDaysLater = Calendar.current.date(byAdding: .day, value: 2, to: now) // 2日後

また、過去の Date を取得する場合には、value に負の数を指定します。下記が現在から5時間前の Date を取得するサンプルになります。

let now = Date()
let fiveHoursAgo = Calendar.current.date(byAdding: .hour, value: -5, to: now) // 5時間前

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

【Swift】MapView で Map を表示するだけの記事

本日もタイトルの通り短い内容になりますが、初学者向けに SwiftMapViewMap を表示する方法を紹介したいと思います🏃🏻‍♂️

それではやっていく

今回は下記のように、特定の地点で固定表示できるようにするサンプルを実装してみます。

f:id:yum_fishing:20210112224938p:plain:w300

MKMapView の実装は下記のようになります。CLLocationCoordinate2DMakelatitude(緯度) と longitude(経度) を元に座標を取得し、マップのズームレベルを表す MKCoordinateSpan と共に MKCoordinateRegion を初期化します。あとは、MKMapViewインスタンスメソッド setRegion() でマップの表示領域を変更させることができます。MKCoordinateSpan はドキュメントに記載の通り、0 に近づく程ズームレベルが高くなり、1 に近づく程ズームレベルが低くなります。

        let coordinate = CLLocationCoordinate2DMake(latitude, longitude)
        let span = MKCoordinateSpan(latitudeDelta: 0.065, longitudeDelta: 0.065)
        let region = MKCoordinateRegion(center: coordinate, span: span)

        mapView.setRegion(region, animated: false)
        mapView.isZoomEnabled = false
        mapView.isScrollEnabled = false
        mapView.isUserInteractionEnabled = false

たまに、下記のように setCentersetRegion の両方のメソッドを使ってるサンプルを目にしますが、基本的には setRegion で完結するので setCenter の記述は必要ありません。setCenter はズームレベルを気にせずに、マップの中心を座標に移動する時に使用します。

        let coordinate = CLLocationCoordinate2DMake(field.point.latitude, field.point.longitude)
        mapView.setCenter(coordinate, animated: false)

        let span = MKCoordinateSpan(latitudeDelta: 0.065, longitudeDelta: 0.065)
        let region = MKCoordinateRegion(center: coordinate, span: span)

        mapView.setRegion(region, animated: false)
        mapView.isZoomEnabled = false
        mapView.isScrollEnabled = false
        mapView.isUserInteractionEnabled = false

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com

【Swift】Navigation に複数のアイテムを設置

本日も短い内容ですが、初学者向けに NavigationBar に複数のアイテムを設置する方法を紹介したいと思います。iOS標準のカレンダーなんかはアイテムが二つ設置されていますよね👀

f:id:yum_fishing:20210111175656p:plain:w400

結論

UIViewController のプロパティを navigationItemnavigationItem.rightBarButtonItemsUIBarButtonItem を配列としてセットすることで、iOS標準のカレンダーのように複数のアイテムを NavigationBar に並べることができます。また、表示される順序は格納した UIBarButtonItem の配列の順序になります。

        let plusBarButtonItem = UIBarButtonItem(image: plusImage, style: .plain, target: self, action: #selector(plusButtonDidTap(_:)))
        let clearSelectedDatesButtonItem = UIBarButtonItem(image: clearImage, style: .plain, target: self, action: #selector(clearButtonDidTap(_:)))
        navigationItem.rightBarButtonItems = [plusBarButtonItem, clearSelectedDatesButtonItem]

また左側のナビゲーションアイテムに関しても同様に設定が可能でのすので、詳しくはドキュメントを参考にしてみてください👩‍🌾

developer.apple.com

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com