Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

DroidKaigi 2023楽しんできました!!

サービス開発パートナー事業部 Androidエンジニアの務台です。

題名にもあるとおり…

DroidKaigi 2023全力で楽しんできました!!!

のでそのご報告です。

DroidKaigiに参加しますの記事はこちら↓ ↓ ↓

techblog.glpgs.com

ブース出展しました

去年に引き続き、Galapagosはスポンサー参加することができましたので、ブースも出展させていただきました。

去年の様子はこちら↓ ↓ ↓

techblog.glpgs.com

弊ブースでは飴ちゃんを配ったり動くbugdroidくんを置いて、一緒にDroidKaigi 2023を盛り上げることができました。

準備をしていただいた皆様、ブースに来ていただいた方々、本当にありがとうございました!

セッションもたくさん見ました

私個人としては二日間で合わせて9つのセッションを見ることができました。どのセッションもとても面白く、今後のエンジニア活動に役立てることができるものばかりでした。わからなかったことは調べて、自分の知識として取り入れていこうと思います!

セッションのまとめとかもブログに書けたらいいなぁ…

他にも

ネイルしたり、

豪華なパーティーでワイワイしたり、

とても、とても楽しかったです!

さいごに

関係者の皆様、本当にありがとうございました!


ガラパゴスでは、一緒に働く仲間を募集しています!

hrmos.co

GalapagosはDroidKaigi 2023を応援しています。

サービス開発パートナー事業部 のiOSエンジニアの亀澤です。電気代が心配な今日この頃です。

DroidKaigi 2023のスポンサー参加します。

今年もDroidKaigiのスポンサー参加できることとなりました。残念ながらiOSDC 2023のスポンサー参加はかなわなかったのですが、参加が決まったDroidKaigiの準備でてんてこ舞いしています。
弊社のロゴや部署名などが変わったので、昨年作ったものが作り直しで必死に準備を進めている真っ最中です。
今回もブース展示を行いますが、前回の反省を元にどのような展示にするか考えています。
イベント自体もたのしみですが弊社はリモートワークが中心となっているので、普段直接会っていない同僚と会えるのも楽しみの一つです。

ドロイド君もバージョンアップして参加予定

DoroidKaigi 2022のブースでお披露目したドロイド君、記事の通りIFTTTBeeBotteを利用してTwitter連携させる予定でした。

改修中のドロイド君
特定のキーワードがツイートされるとそれを検知してダンスするようにしていたドロイド君ですがDrodiKaigi2日目にしてTwitter連携機能を廃止しました。
理由は

  • 無料枠のIFTTTでTwitter Web Hookを使ったがツイートしてから反応するまで2分近くかかる(その後、IFTTTに課金してみたところ改善はしたが30秒はかかる)
  • M5StickがWiFi 2.4GHz帯でしか使えない(会場は5GHzのみ)

と致命的です。
ツイート検知機能の代わりにPIRセンサ(人感センサ)で反応したら動くようにしたのですが準備の時と違い実際には人が多すぎて常にセンサが反応していました。 なんとか一部の方に動く姿をお披露目できましたが納得のいく動きをせず消化不良でした。
今年に入って知人からMakersFairTokyoの出展にも誘われたので、改修版としてセンサを距離センサに変更して近寄ったり離れたりで動きが変わる様に改良しました。 残念ながら枠は落ちてしまいましたが、DoroidKaigiでバージョンアップしたドロイド君をお披露目するつもりですので是非ブースにお立ち寄りください。
それでは皆さんと会場でお会いできるのを楽しみにしています。

ガラパゴスでは一緒に働く仲間を募集しています。 hrmos.co エントリーお待ちしてます!

WidgetKitに入門してみた

 はじめまして。サービス開発パートナー事業部(旧:AIR Design for Apps事業部)iOSエンジニアの住山です。好きなキーボードはHHKBです。 さて今回はWidgetKitに入門してみたので、記事にしていきたいと思います。

実行環境

ディレクトリ構成

説明に不要なファイルは省略してます。

.
├── WidgetPractice.xcodeproj
├── WidgetPractice
│   ├── Assets.xcassets
│   ├── ContentView.swift
│   └── WidgetPracticeApp.swift
└── WidgetGlpgs
    ├── Assets.xcassets
    ├── Info.plist
    ├── WidgetGlpgs.swift
    ├── WidgetGlpgsBundle.swift
    └── WidgetGlpgsLiveActivity.swift

Xcodeの「File」→「New」→「Target」でWidget Extensionを追加すると、必要なファイルが追加されます。(上記ディレクトリ内のWidgetGlpgsフォルダ配下)

ゴール

みんな大好きGithubAPIからjsonを取得して、内容をWidgetに表示。レポジトリはこちら

github.com

ウィジェット作成の概要

  1. Widgetの各種設定
  2. Viewの作成
  3. タイムラインの作成
  4. Previewsの作成

1. Widgetの各種設定

コード

struct WidgetGlpgs: Widget {
    let kind: String = "WidgetGlpgs"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
            WidgetGlpgsEntryView(entry: entry)
        }
        .configurationDisplayName("Widget Name")
        /// Widgetの説明。
        .description("Widgetの名前")
        /// Widgetのサイズ一覧。
        .supportedFamilies([
                            /// accessoryXXXはロック画面用。iOS16〜
                            .accessoryCircular,
                            .accessoryInline,
                            .accessoryRectangular,
                            /// サイズ一覧
                            /// ExtraLargeはiPadOS専用。(https://developer.apple.com/documentation/widgetkit/widgetfamily/systemextralarge)
                            .systemExtraLarge,
                            .systemLarge,
                            .systemMedium,
                            .systemSmall
        ])
    }
}

.configurationDisplayName("Widget Name")

ウィジェットの名前。

.description("Widgetの説明")

ウィジェットリストでの説明用。

.supportedFamilies([...])

サポートするサイズ一覧です。後述しますが、サイズ毎にViewを作成できます。

  • accessoryXXXはロック画面のWidget用。iOS16〜で使えます。
  • ExtraLargeはiPadOS専用のようです。

developer.apple.com

2. Viewの作成

コード

struct WidgetGlpgsEntryView : View {
    var entry: Provider.Entry
    @Environment(\.widgetFamily) private var widgetFamily

    var body: some View {
        /// サイズ毎にViewのカスタマイズ
        if entry.githubUserInfoModel == nil {
            Text("ロード中")
        } else {
            switch widgetFamily {
            case .systemMedium:
                HStack() {
                    Image(uiImage: ApiClient.getImage())
                        .resizable()
                        .scaledToFit()
                        .frame(height: 100)
                    VStack(alignment: .leading) {
                        Text(entry.githubUserInfoModel!.location ?? "")
                        Text(entry.githubUserInfoModel!.name)
                        Text("PubRepos:  \(entry.githubUserInfoModel!.publicRepos)")
                    }
                }
            default:
                Image(uiImage: ApiClient.getImage())
                    .resizable()
            }
        }
    }
}
  • @Environment(\.widgetFamily)でユーザが追加したWidgetのサイズを取得し、サイズ毎にViewを作成できます。

3. タイムラインの作成

IntentTimelineProvider を継承したProviderでライフサイクルを実装します。下記コードです。

struct Provider: IntentTimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: ConfigurationIntent(), githubUserInfoModel: nil)
    }
    func getSnapshot(for configuration: ConfigurationIntent,
                                   in context: Context, 
                                   completion: @escaping (SimpleEntry) -> ()) {
                                            let entry = SimpleEntry(date: Date(),
                                                                    configuration: configuration, 
                                                                    githubUserInfoModel: GithubUserInfoModel.fake())
                                            completion(entry)}

    func getTimeline(for configuration: ConfigurationIntent, 
                     in context: Context,
                     completion: @escaping (Timeline<Entry>) -> ()) {
        Task {
            var entries: [SimpleEntry] = []
            let currentDate = Date()
            let githubUserInfoModel: GithubUserInfoModel?
           /// 10分毎にViewの更新を依頼。
            let entryDate = Calendar.current.date(byAdding: .minute, value: 10, to: currentDate)!
            let githubUserInfoModel: GithubUserInfoModel? = {
             do {
                 return try await ApiClient.fetch()
             } catch {
                      return nil
                }
            }()
            let entry = SimpleEntry(date: entryDate, configuration: configuration, githubUserInfoModel: githubUserInfoModel)
            entries.append(entry)
            
            let timeline = Timeline(entries: entries, policy: .atEnd)
            completion(timeline)
        }
    }
}

struct SimpleEntry: TimelineEntry {
    var date: Date
    let configuration: ConfigurationIntent
    let githubUserInfoModel: GithubUserInfoModel?
}

placeholder()

Widgetを追加後、最初に仮で表示されるViewを設定します。

getSnapshot()

Widgetギャラリーの表示用Viewを設定します。

getTimeline()

ここでWidgetのライフサイクルを設定します。

  • Calendar.current.date(...)でViewの更新時間を設定します。上記の例では10分毎に更新するよう設定してます。 (試しに1分毎を設定してみましたが、3分前後で更新されました。Viewの更新タイミンについて、完全にはコントロールできないようです。)

    そのため頻繁にTimelineの更新を要求すると、逆にシステム側から制限をくらい、全然更新されない場合もあります。逆によく使われるアプリは更新頻度が高くても、許容されます。この部分に関しては公式の説明がないので、経験でしかなく、まだ謎は多いです。(下のブログから引用。)

engineering.dena.com

4. Previewsの作成

struct WidgetGlpgs_Previews: PreviewProvider {
    static var previews: some View {
        WidgetGlpgsEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), githubUserInfoModel: GithubUserInfoModel.fake()))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}

ここの記事がとても参考になりました。 qiita.com

おわりに

以前から気になっていたWidgetをブログ執筆を気に学べました。実務でも提案できるように、良いユーザ体験を与えられるようなWidgetを模索していきたいです。