entry-header-eye-catch.html
entry-title-container.html

entry-header-author-info.html
Article by

iOSDC Japan 2025ブース企画「SwiftUIと秘密の修飾子」問題解説

こんにちは。ピクシブでiOSアプリエンジニアをしているああうえです。

ピクシブはiOSDC Japan 2025でSwiftUIのModifierに焦点を当てたゲーム「SwiftUIと秘密の修飾子」を展示していました。この記事では問題についての解説を公開いたします。

SwiftUIと秘密の修飾子について

SwiftUIのModifierにちなんだゲームアプリです。Modifierの呼び出し方について問題文が書かれており、これを満たす端末の操作でポイントが入るような仕組みです。

Join the SwiftUIと秘密の修飾子 beta - TestFlight - Apple

Testflight経由でインストールして挑戦することができます。(本Testflightアプリは予告なく配信を終了する可能性があります)

以下、各問題の解説になります。

onAppear 

想定解法: 画面を開く

onAppear

解説・コメント: 開いてonAppearを呼ぶだけ…と思ったのですが、Day 0で10人に1人くらいGameCenterのAchievementのトーストとクリア演出が被ることでクリア演出が表示されなくなる、という不具合が発覚して焦りました。

onDisappear 

想定解法: 画面を開いて閉じる

onDisappear

onTapGesture 

想定解法: 画面を1回タップする

onTapGesture

解説・コメント: onAppear onDisappear 同様オンボーディング用に用意した問題でした。非エンジニアの方も英文から動作を察してタップしてくださる方が多くて嬉しかったです。

onTapGesture(count:) 

想定解法: 画面を連続で10回タップ

onTapGesture(count:)

解説・コメント: countには10が指定されていました。あまり使うことのないジェスチャーかと思ったのですが、意外と皆さん瞬時に連続タップすれば良いことに気付いており、良いAPIだなと思いました。

onLongPressGesture 

想定解法: 画面を3秒以上長押し

onLongPressGesture

解説・コメント: minimumDurationには3が指定されていました。minimumDistanceが指定されておらず、デフォルト値の10が指定されていたため、かなりシビアな判定になっていました。またiOS 18とiOS 26でもonLongPressGestureが発火するタイミングが違うようです。

onDrag 

想定解法: 灰色のViewをドラッグ(長押し)する

onDrag

解説・コメント: パンジェスチャーのような、指を置いてからすぐ動かす動作をしてハマってる方がいました。たしかにmacOSだとマウスを右クリックしてすぐウィンドウを動かしたりできるので、iOSは長押しすることでdraggableな要素をドラッグできることが分かる良い問題だったのではないでしょうか。

onDrop 

想定解法: 灰色のViewに画像をドロップする

onDrop

解説・コメント: ofには [.image] が指定されていました。画像をドロップすれば良いのですが、画面内にはドラッグできる画像はありません。そのため、iOSの写真アプリなどから画像をドラッグすれば良いのですが、写真をドラッグしながらもう1本の指で秘密の修飾子アプリに戻る操作をする必要があります。

別解: iOS 18以前の端末を利用している場合スクリーンショットを撮影し、スクリーンショットのサムネイルをドラッグ&ドロップすることでクリアしている参加者の方がいらっしゃいました。

onDropの別解

contextMenu 

想定解法: ボタンを長押ししてコンテキストメニューを表示し、スコアをゲットするボタンを押す

contextMenu

解説・コメント: コンテキストメニューとは、現在操作・選択している項目の状況(コンテキスト)に応じてポップアップ表示されるメニューのことです。スマホなら長押しで、PCなら右クリックで表示されることが多いですね。

refreshable 

想定解法: 画面を下に引く

refreshable

onScrollPhaseChange 

想定解法: 一度画面をなぞってスクロールさせた後、慣性スクロール中に画面をタップする

onScrollPhaseChange

解説・コメント: ScrollPhaseとして oldPhase == .decelerating && newPhase == .interacting を指定してあり「慣性スクロール中に画面をタップする」という動作が意外と難易度高いんじゃないかなと思っていたのですが、適当にスクロールしてたら解けたという方が結構いらっしゃって作問者としては少し悔しい気持ちになりました😂

onScrollGeometryChange 

想定解法: 下にひたすらスクロールする

onScrollGeometryChange

解説・コメント: ofには { $0.contentOffset.y < -550 } が指定されていました。そのため、ScrollViewを下に引き続けることでクリアすることができますが…文字が読めなくなる限界くらいまでスクロールする必要があるため、かなり大変な作業になっています。

onGeometryChange 

想定解法: 画面回転をする

onGeometryChange

解説・コメント: forには CGSize.self , ofには \.size が指定されており、viewのsizeプロパティを画面の回転やSplit Viewなどを用いて変化させることでクリアすることができます。

onOpenURL 

想定解法: Safariを開いて pixiv-modifier:// へアクセスする

onOpenURL

解説・コメント: Custom URL Schemeを呼び出す問題を用意してみました。NOTEの部分の長押しコピーを試される方が多かったのですが、 textSelection を付けていませんでした!手打ちさせてしまってすみません!

onSubmit 

想定解法: 問題リストの検索バーで送信をトリガーする

onSubmit

解説・コメント: 初めは問題画面のTextFieldでreturnするだけの予定でしたが、ドキュメントでof引数について調査したところ.searchを見つけました。.searchを指定すると検索フィールドの送信トリガーだけを検知します。問題画面に検索フィールドを配置すると簡単になり過ぎてしまうため、問題リストの検索機能も兼ねて実装しました。

renameAction 

想定解法: 画面右上のボタンをタップすることでTextFieldにフォーカスが当たって文字を書き換えられるようになるので、TextFieldに既に入力されている「秘密の修飾士」を正しいアプリ名である「秘密の修飾”子”」に書き換える

renameAction

解説・コメント: 作問のためにドキュメントを漁って見つけたのがこの.renameAction( https://developer.apple.com/documentation/swiftui/view/renameaction(_:))です。ドキュメントに記載されている利用例通りに、RenameButton をタップすることで .renameActionが発火し、TextFieldにフォーカスが当たるという形で問題を作ってみました。ただ、直接TextFieldを触られては問題にならないので、.allowHitTesting(false)でタップ処理を無効化しています。もう少し「秘密の修飾士」が表示されている箇所をTextFieldとわかりやすいように見た目を整形すればよかったかなと思いました。

swipeActions 

想定解法: 問題が並んでいるListを右にスワイプして🎉をタップする

swipeActions

解説・コメント: edgeには .leading が指定されており右側にスワイプ、またallowsFullSwipeはfalseのためスワイプをして表示されたボタンをタップする必要があります。問題文にListは無いため手こずった方もいるのではないでしょうか。

onChange(of: scenePhase, …) 

想定解法: アプリスイッチャー、コントロールセンター、通知センターを開く。アプリを起動した状態で画面ロックする。

onChange(of: scenePhase, ...)

解説・コメント: アプリを最前面で操作できる状態から、最前面のまま動作を停止している状態にする必要があります。ドキュメントを見なくてもcaseの名前から解けた方もいらっしゃるのではないでしょうか。

onChange(of: colorScheme, …) 

想定解法: ライトモード/ダークモードを切り替える

onChange(of: colorScheme, ...)

解説・コメント: colorSchemeがライトモード・ダークモードのことを指すことがわかっていれば、特に難しいことはありません。切り替えは、設定の「画面表示と明るさ」で変更する、コントロールセンターの明るさを長押しして出るアイコンで切り替える、Siriに「ダークモードにして」と話しかける、といった方法があります。

onChange(of: dynamicTypeSize, …) 

想定解法: アクセシビリティ > 画面表示とテキストサイズ > さらに大きな文字 を開き 「さらに大きな文字」のトグルをONにし、左から8つ目以降のサイズを選択する

onChange(of: dynamicTypeSize, ...)

解説・コメント: この問題は dynamicTypeSize.isAccessibilitySizeonChange で監視しているため、文字サイズを変えるだけではダメで文字サイズがアクセシビリティサイズに変わる必要があります。

extraSmall small medium large extraLarge extraExtraLarge extraExtraExtraLarge accessibilityMedium accessibilityLarge accessibilityExtraLarge accessibilityExtraExtraLarge accessibilityExtraExtraExtraLarge の順なので、「さらに大きな文字」を有効にして左から8つ目以降の文字サイズが指定されている必要がありました。

onChange(of: isSceneCaptured, …) 

想定解法: この画面を表示した上で、コントロールセンター等から画面収録を開始する

onChange(of: isSceneCaptured, ...)

解説・コメント: 文字通り「画面が録画されているかどうか」の変更を検知する問題でした。 isSceneCapturedはiOS 17+でEnvironmentから取得することができます。実はスクリーンショット撮影を検知する簡単な方法が存在しないかを探している最中に見つけました。スクリーンショット検知の方は実装してもModifierから離れてしまうので断念しました。

onInAppPurchaseCompletion 

想定解法: 100,000円のボタンを押して非消耗型課金アイテムを購入

onInAppPurchaseCompletion

解説・コメント: iOS 17+で利用可能な StoreKit.ProductViewonInAppPurchaseCompletion を利用した問題を作ってみました。TestFlightでのみアプリを配信しているため、サンドボックス環境での課金となり実際の請求は発生しないのですが…分かっていても押すのに勇気が試される問題となりました。

onContinueUserActivity 

想定解法: Spotlightでアプリ名である「SwiftUIと秘密の修飾子」と検索し、そこからアプリを開く

onContinueUserActivity

解説・コメント: 第一引数には CSSearchableItemActionType が設定されていました。Core Spotlightを使っていることが想像できる。Spotlightでアプリ名を検索すると「ここから開くとスコアをゲットできるよ」というメッセージが表示される。

onCameraCaptureEvent 

想定解法: 音量ボタン、またはカメラコントロールボタンを押す

onCameraCaptureEvent

解説・コメント: 文字通りで撮影をすればよいのですが、iPhoneのカメラは音量ボタンでシャッターが切れるようになっているので、押せばクリアとなります。iPhone16以降のカメラコントロールボタンでもクリア可能です。

onKeyPress 

想定解法: ハードウェアキーボードを接続し、Spaceキーを押す

onKeyPress

解説・コメント: 実はピクシブのブースにはケーブルが繋がれていないMagic Keyboardがおもむろに置かれていました。これを接続し、キーを押すことでクリアできるようになっていました。

別解:アクセシビリティ > 音声コントロールを有効にして、「スペースキーを押す」と発話することでキー入力の音声コマンドを発火させてクリアするという方法も参加者の方が見つけてくれました

onKeyPressの別解

keyboardShortcut 

想定解法: ハードウェアキーボードを接続し、⌘+Pキーを押す

keyboardShortcut

解説・コメント: 第一引数には.spaceが指定されていました。問題文では確認できませんが、modifiersのデフォルト値には.commandが指定されているため、⌘+Pを押すことでクリアすることができます。

別解: onKeyPress 同様に修飾キーが必要な場合も音声コントロールが利用できます。「コマンドキー Pキー を押す」と発話することでクリアできます。

keyboardShortcutの別解

別解2: iPadOS 26+であれば commands(content:) でCommandMenuが設定されており、メニューから選択するだけでもクリアすることができました。

keyboardShortcutの別解2

onHover 

想定解法: 視線トラッキングで灰色のViewを見る、iPadにトラックパッドを接続しホバーする

onHover

解説・コメント: 元々iPad用の問題を作ろうとしていたのですが、実は視線トラッキングを使えばiPhone単体でも呼び出せることに気付いて普通に問題として配置してみました。

onHoverの別解

widgetURL 

想定解法: ウィジェットを設置。3桁の数字を入力する必要があり、100の位はwidgetの説明欄に、10の位と1の位はショートカットの「数を増やす」「数を減らす」アクションの説明欄に記載がありました。ウィジェットで「934」と入力してウィジェットをタップしてアプリを開くことでクリアとなります。

widgetURL

解説・コメント: widgetURL はウィジェット上のコンポーネントが押された際にディープリンクを設定できるものです。ただウィジェットを押すだけだと面白くないので、謎解き要素で3桁の数字を探してもらうことにしました。インタラクティブウィジェットの実現にはAppIntentを使うというのが面白いなと感じていて、ショートカットアプリから確認できる箇所に数字を隠してみました。

鍵付きクイズの開放について

追加問題のアンロック導線はHome screen quick actionに仕込んでいました。アクセシビリティ関連の問題と最終問題だけロックしていたため、アクセシビリティの機能を使ってアンロックしそうに見えてしまっていました…

accessibilityAction 

想定解法: VoiceOverを有効にして「シークレットアイテム」にフォーカスを当てた状態でダブルタップ

accessibilityAction (音声有り)

解説・コメント: SwiftUIのModifierを眺めていたときにアクセシビリティ用のModifierが存在することに気がついて、作問させていただきました。

accessibilityAdjustableAction 

想定解法: VoiceOverを有効にして「シークレットアイテム」にフォーカスを当てた状態で1本指で上または下にスワイプ

accessibilityAdjustableAction (音声有り)

解説・コメント: AdjustableActionを設定している場合、VoiceOverで調整可能なViewであることを発話して教えてくれるようになっているのが自分としても学びになりました。

accessibilityScrollAction 

想定解法: VoiceOverを有効にして「シークレットアイテム」にフォーカスを当てた状態で3本指で上または下にスワイプ

accessibilityScrollAction (音声有り)

解説・コメント: VoiceOverでスクロールするには3本指でスワイプが必要になります。VoiceOverを初めて有効にする際のチュートリアルでも紹介されていましたね。

accessibilityZoomAction 

想定解法: VoiceOverのローター項目から「拡大または縮小」を有効にしたあと、VoiceOverを有効にする。「シークレットアイテム」にフォーカスを当てた状態で2本指をツイストさせてローター項目から「拡大または縮小」をセット、その後一本指を上または下にスワイプしてローターにセットした「拡大または縮小」のアクションを実行する

accessibilityZoomAction (音声有り)

解説・コメント: VoiceOverのローターという機能を使う必要があり、VoiceOverの中でも結構高度な操作だったのではないでしょうか。自分も二本指でツイストするというアクションを作問時に初めて行いました。

最終問題1 

回答: ariakeとテキストフィールドに入力する

最終問題1

解説・コメント: 穴埋めには、上からtint, disabled, onKeyPressが入ります。tintはToggleの色が変わっていること、disabledはButtonがdisabledな見た目になっていることがヒントです。あとはAppleのドキュメントを眺めたり、Xcodeの補完を使うことでonKeyPressを埋めることができます。onKeyPressはハードウェアキーボードを繋いで任意のキーを押すことでハイライトされるようになっていました。有明での開催にちなんでこのトークンにしてみました。

最終問題2 

想定解法: Voice Overでトグルのオンオフ状態をアプリアイコンと揃える

最終問題2 (音声有り)

解説・コメント: 作問者が流行病に感染した時に10分ほど目が見えなくなったことがあり、突然目が見えなくなるという演出をMetalで実装してみました。ほとんど何も見えなくなってインタラクションもできなくなるため、Voice Overでの操作が必要になります。ちなみに本当に目が見えない状態でも、ホーム画面のアプリロゴにはaccessiblityLabelとaccessibilityHintが仕込んであり、アプリロゴがどういう形状になっているか知ることができるようになっています。

最終問題3 

想定解法: イヤホンを接続し、420という数字を聞き、テキストフィールドに入力する

最終問題3 (音声有り)

解説・コメント: めちゃくちゃ小さな文字で「誰にも聞こえないようにこっそり聞いてください」と書かれているので、これをDynamic Typeのサイズを変更したり、読み上げを使って読み解きます。誰にも聞こえないようにこっそり聞くには…ということなので、イヤホンを繋ぐとこっそり回答が聞こえるようになっていました。

最終問題4 

想定解法: ブースで企画ボードにカメラを向けるとヒント画像が表示される。そのヒントの通りにiPhoneを企画ボードに近づける

最終問題4

解説・コメント: 実はカメラではなくRealityViewが表示されていました。企画ボード画像がRealityKitのAnchorEntityとして登録されており、企画ボードを認識するとヒント画像が表示されるようになっていました。企画ボードの裏にはiPhoneが仕込んであり、ヒント画像通りに企画ボードにiPhoneを近づけるとAirDropのデバイス同士を近づけて共有できる機能でURLを受け取るとクリアできるようになっていました。このような問題にしたのは、ブースに最後戻ってきてもらい、交流できるようにすることが狙いでした。

まとめ

作問者の想定と違う解法を見つけてくださる方もおり、自分たちもiOSのデバイス操作に関して勉強になりました。多くの人に楽しんでもらえて良かったです。

ピクシブにはEntertain(遊び心で楽しませよう。)というバリューがあり、iOSDCの参加者に楽しんでもらったり、普段はしない開発をすることで社内のiOS参加メンバーも楽しんだりすることを考えていました。結果、ゲームを通じて技術的な交流をするという狙いに繋げることができました。

メインの実装や最終問題の作問はああうえが行いましたが、画面を作るためのフォーマットを揃えたり、各問題は作問担当を分散することで、比較的開発メンバーの負荷が少なく実現できたと思います。

最終問題については、ちょっとmodifierと趣旨がズレる部分もありましたが、脱出ゲームが好きなので、iPhoneの色々な機能を呼び出すことで解ける問題を用意してみました。

解説記事で「でも本当にそれで良かったんでしょうか…?」という文言を入れられるような問題を作問できなかったのが少し悔しいですが、トータルで見ると良い難易度になったかなと思います。


ピクシブでは中途iOSアプリエンジニアを募集しています。iOSネイティブな機能を活用して、遊び心のある価値を提供したい方のご応募をお待ちしております。

hrmos.co

20191219012707
ああうえ
2017年4月新卒入社。pixivやpixiv Sketchなど、iOSやAndroidアプリの開発をしてきました。最近はプロダクトマネージャーをやっています。趣味はお絵かきと、うさぎです。
atsuyan
Pastela部 iOSエンジニア。2019年に新卒入社した前職でモバイルアプリエンジニアとして5年勤めた後に2024年2月にピクシブ株式会社に中途入社。現在はPastelaのiPad版アプリの開発を担当。ボルダリングが趣味です。