ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
SwiftUIの新機能
SwiftUIを使ってすべてのAppleプラットフォームに対応するアプリを作成する方法を紹介します。SwiftUIの最新アップデートやvisionOS用の新しいシーンタイプについて詳しく学びましょう。最新のデータフローオプションでデータモデルを簡素化する方法やインスペクタービューについても確認します。またアニメーションを強化するAPI、パワフルなScrollView、整った表の作成、改善されたフォーカスやキーボード入力などについて紹介します。
関連する章
- 1:05 - SwiftUI in more places
- 10:21 - Simplified data flow
- 18:46 - Extraordinary animations
- 27:18 - Enhanced interactions
リソース
関連ビデオ
WWDC23
- アプリでシンボルをアニメーションする
- フォーカスを使ったSwiftUIレシピ
- 進化したScrollViewの詳細
- Springsでアニメーション生成
- Swift Chartsの円グラフとインタラクティブ性の詳細
- Swiftの新機能
- SwiftDataでアプリを構築
- SwiftDataについて
- SwiftUIとUIKitを使ったアクセシブルなアプリの作成
- SwiftUIにおけるインスペクタの詳細
- SwiftUIにおける空間コンピューティングの概要
- SwiftUIにおけるObservationの説明
- SwiftUIの高度なアニメーションの世界
- SwiftUIアニメーションの詳細
- SwiftUI向けのMapKitについて
- SwiftUI向けのStoreKitについて
- watchOS 10に向けたアプリのデザインおよび構築方法
- Xcode PreviewsによるプログラマティックなUIの構築
-
ダウンロード
♪ ♪
みなさん こんにちは SwiftUIチームのエンジニアのCurtです 私はJeffです 私もSwiftUIチームのエンジニアです SwiftUIの新機能を みなさんとシェアできて光栄です SwiftUIを使用できる場所が増えました 新しいプラットフォームもです! 新しいデータフロー型で ドメイン作りが簡略化され これまで以上のパワーを提供します インスペクタとテーブルの改良で データ表示が非常に向上します アニメーションAPIを改善し アプリの利用者に美しい体験を 提供することができます フレームワークを通して インタラクションをより向上させます パワフルなscroll viewの改良や Focusとキーボード入力の改善 ボタンやメニューなどの さらなるカスタマイズなどです まず SwiftUIをさらに新しい場所で 使用できるようになりました ヘッドセットやwatchOS 10 新ウィジェットや相互運用性の統合など SwiftUIはアプリの利用者が喜ぶ 素晴らしい体験作成のお役に立ちます 空間コンピューティングは SwiftUIに素晴らしい未来をもたらします 新しいVolumesの3D能力や イマーシブな空間での豊かな体験 3Dジェスチャーに効果やレイアウト RealityKitとの深い統合などがあります コアピースである コントロールセンターのHome Viewや 親しみのあるTVアプリ Safariや Freeformから Keynoteの新環境である Immersiveリハーサルなど SwiftUIがユーザ体験の中心にあります この新プラットフォームでは WindowGroupなど 慣れたシーン型でウインドウを作ります WindowGroupシーンは2Dでレンダーされ depth-sensitiveの 3Dコントロールが備わっています ウインドウ内で NavigationSplitViewやTabViewなど SwiftUIの通常のコンテナを選びます 他のプラットフォーム同様に このコンテナ内ではいつもの SwiftUIコントロールが使用できます さらに深さが必要なら シーンにvolumetric styleを適用します Volumesはボードゲームや 建築モデルのように3D体験を 限られた空間に表示します ほかのアプリの横に表示します メモにアイデアを書いたり Keynoteの編集中に あなたのコンテンツを使用できます Model3Dで静的モデルに ボリュームを与えます 照明効果などのある 動的でインタラクティブなモデルには 新しいRealityViewを使用します フル体験を求めるなら Immersive Spacesです Immersive Spaceシーン型は 環境はめ込み型であれ フルイマーシブであれ イマーシブで空間的な体験を定義できます システムはほかのアプリを全て隠し あなたが作り出した世界に ユーザーを没頭させます ImmersiveSpaceで 混合イマーションタイプとして アプリを実世界と結びつけ コンテンツをユーザーの環境と結合できます アプリの要素をテーブルや表面に設置し 仮想オブジェクトや効果などで 実世界を増大させ豊富にできます さらに完全没入してみましょう あなたのアプリがすべてを コントロールします これらの連結したイマーシブな体験を 大きなファイルサイズでも作動する Model3DやRealityViewで 作ることができます SwiftUIを使って 魔法のような体験を創造できます これらをさらに探求するなら 「Meet SwiftUI for spatial computing」を ご覧ください SwiftUIは部屋を満たす体験を つくりだせますが Appleの最もポータブルな ディスプレイの体験も創造できます watchOS 10はリデザインされた ユーザー体験をお届けします タイミングよく情報を表示し 焦点を当てたコンテンツを一見でき ディスプレイの形状と性能を フルに活用します この美しいフルスクリーンの カラーとイメージを利用し プラットフォーム全体で アプリをアップデートしました これらのデザインの根源は watchOS 10のためにパワーを得た 幾つかの既存のSwiftUI Viewsです NavigationSplitViewと NavigationStackには 美しいトランジションが加わります TabViewでは Digital Crownを活用した vertical pagingが加わります SwiftUIはこの色彩豊かさを Apple Watchアプリにも取り込むため 新しいAPIを用意しました containerBackground modifierで コンテンツを押し出すときにアニメーション化する バックグラウンドウォッシュを設定できます またwatchOSではTab viewsの バックグラウンドも設定できます そして新しいmultiplatform toolbar placementsである topBarLeadingとtopBarTrailingが 既存のbottomBarに加わり これらの小さいdetail viewsを アプリに配置できます これらに加え 幾つかの既存のAPIが初めて watchOSに加わります DatePickerと Listsのselectionがその例です これらの新しい機能で アプリを洗練しましょう まだApple Watchアプリがないなら 今こそ始めるべきです デザインと技術が一体になり これをどう実現したか 「Design and build apps for watchOS 10」でご覧いただけます そしてこれらのアイデアを 「Update your app for watchOS 10」で アプリに応用してみてください watchOS 10のSmart Stackウィジェットで アプリの情報を一見できるようになります どこに表示されようともウィジェットの コアはSwiftUIです。 このような新しい場所でもです iPadOS 17のロック画面のウィジェットは ホーム画面のウィジェットの 素晴らしい追加機能となります またStandby Modeで 大きく大胆なウィジェットが iPhoneの常時表示ディスプレイで 表示されます そしてmacOS Sonomaの デスクトップウィジェットは 日常の生活の中で最新情報を伝えます 新しい場所に加え 新しいテクニックも教えました ウィジェットに interactive controlが追加されます ウィジェットのトグルやボタンが App intentsを使ったapp bundleのコードを 作動できるです またウィジェットのアニメーション化が SwiftUI transitionと animation modifiersで可能になります これらの新機能を使うには 「Bring widgets to new places」と 「Bring widgets to life」をご覧ください 新機能を用いたウィジェットの 開発と洗練には Xcode Previewsが役に立ちます PreviewsはSwift 5.9でマクロを活用し 優雅な新しい構文を供給します Previewの宣言と設定や ウィジェット型の追加 テストのタイムラインを定義できます Xcode Previewsで ウィジェットの現状と 状態間の動画が見られる タイムラインを表示します もちろん新しいPreviewsは 通常のSwiftUI viewsと アプリも使用できます そしてXcode内で Macアプリのpreviews とインタラクトできます 詳細は 「Build programmatic UI with Xcode Previews」で アプリとウィジェットの開発を 加速させるため これらのツールをどう利用できるか 学んでください Previewsを可能にするマクロに加え Swift 5.9にはさらなる改良点があります Swiftの新機能の概要を 「What's new in Swift」で ご覧いただけます またSwiftUIは SwiftUI特定拡張子で ほかのAppleフレームワークに使用できます それらのフレームワークの中から 特に面白いものを いくつかご紹介しましょう MapKitではSwiftUIコード内で Appleのマッピングフレームワークを 活用できるようになりました SwiftUIとMapKitを取り込めばいいだけです ビューにマップを足したり カスタムマーカーやポリラインや ユーザーの現在地を足したり コントロールを設定したり出来ます SwiftUIに マップを足したいなら 「Meet MapKit for SwiftUI」を ご覧ください 2年目を迎えたSwift Chartsにも 改善点が加えられました それらにはScrolling chartsや Selectionの組み込みサポート そして多くのみなさんが要求していた 新しいSectorMarkを備えた ドーナツグラフと円グラフです これらの新機能については 「Explore pie charts and interactivity in Swift Charts」をご覧ください 顧客を集め維持するための 優れた体験をつくるのに役立つのが in-app purchaseと subscription storesです カスタムマーケティングコンテンツで subsrciption store viewを確認でき ブランドにマッチする フルブリードの背景 そして豊富なコントロール設定があります 「Meet StoreKit for SwiftUI」で in-app marketingに チャレンジしてみましょう 新しいプラットフォームやウィジェット 複数プラットフォームの統合 そしてwatchOSの美しさなどSwiftUIは Appleデベロッパ体験を追求し続けます SwiftUIを使える場所が増えて 嬉しいね Jeff もちろん! またすべてのプラットフォームで 利用できる改良点がたくさんあります その通り! それを使ってアプリを作らなきゃ そうだね! 僕のアイデアのこと考えた? 犬のアイデアかい? ああ バードウォッチングの犬版さ! そんなアプリ 必要かな? もちろんさ! 売り込み文句も必要ないよ 一攫千金のアイデアが浮べば アプリを作り始めるべきです 素晴らしいアプリには 素晴らしいデータモデルが必要です ではアプリのデータを扱うための SwiftUIの素晴らしい新機能を 見てみましょう SwiftUIのお気に入りの機能の一つは インターフェイスを アプリの状態の機能に定義することです model typeの定義の仕方で これまで最大のアップグレードがあります Observableマクロです Observable modelsは コードを簡潔で効率よくしながら データフローに 親しみあるSwiftUI patternsを 使用できます これは私が外出中に出会った犬のデータの 保存に設定したmodel classです これをObservableにするため マクロを足します それだけです ObservableObjectと違い propertiesをPublishedと する必要はありません Observable modelsはデータフローのために 既存のSwiftUI mechanismsに 簡単に統合できます DogCard viewを例に見てみます ViewでObservableを使用する時 SwiftUIは自動的にpropertiesに 依存を確立します またproperty wrapperを使う必要はなく view codeはスッキリします ここではisFavorite propertyを 読み込んでおり それが変わると再評価されます Invalidationは読まれたpropertiesだけに 起こりますので 不要なupdatesを起こすことなく modelをintermediate viewsに 通すことができます SwiftUIにはstateとそのviewsへの関係を 定義するツールがあり ObservableObjectと使用できるよう デザインされたものもあります Observableを使用すると それがさらに簡単になります State及びEnvironment dynamic propertiesと 直接働くようになっているからです read-only valuesに加え Observablesはmutual stateを 表すのに最適です Dog sightingフォームがいい例です State dynamic propertyを使って modelを定義し そのpropertiesのbindingsを propertyの編集を担う form elementにパスします 最後にObservable typesはenvironmentに シームレスに統合します アプリを通してviewsは 現在のユーザーをfetchしたいため root viewに environmentを足しました Environment dynamic propertyを使用し user profile viewが値を読みます ここではenvironment keyに typeを使っていますが custom keysもサポートします 「Discover Observation with SwiftUI」をご覧になり このパワフルな新ツールの利用方法を 学ぶことができます Observableで 明瞭で簡潔なコードが書けるため アプリ作成が始めやすくなります しかしお気に入りの犬を 見失わないためdata modelへの変更を 持続させたいとします SwiftDataはdata modelingと managementのための 新フレームワークです 迅速でスケーラブルで SwiftUIとうまく働きます SwiftData modelsはコードだけで表され どのSwiftUIアプリにも最適です Dog model typeを SwiftDataで設定するには ObservableからModel macroに切り替えます 変更はこれだけです SwiftDataが提供するpersistenceに加え ModelsはObservableの利点すべてを 受け取ります 非常にパワフルです ドッグウォッチングアプリの メイン画面には最近会った犬の scrolling stackが表示されます ここでSwiftDataを使うため 変更すべき点を見てみましょう まずアプリのdefinitionに model containerを足し model typeを供給します そしてview codeで犬の配列を Query dynamic propertyに切り替えます QueryはSwiftDataにデータベースから model valueのfetchを指示します 新しい犬に会った時のように データが変わると viewは無効になります Queryは大量のデータセットに対し 非常に効率がよく データの戻り方をカスタム化できます 出会った犬のデータでの ソートの順序変えがその例で アプリ体験を向上させることができます SwiftDataはmacOSとiOSでの 書類のデータ保存にも 非常に適しています 犬のタグをビジュアライズする 簡単なプロトタイプ方法が アプリに必要だと思い Curtやデザイナーとコラボし このドキュメントベースのアプリを 作成しました ドキュメントベースのアプリは initializerでSwiftDataの 機能すべてを利用出来ます SwiftUIは各ドキュメントのストレージに SwiftDataを使いmodel containerを 自動的に設定します SwiftDataとSwiftUIとの 統合については 「Meet SwiftData」と 「Build an app with SwiftData」を ご覧ください SwiftDataサポートに加え DocumentGroupは iOS 17もしくはiPadOS 17上で 数々のplatform affordanceを得ます automatic sharingやdocument renaming またツールバーのundo controlsなどです Inspectorは現在のselection及びcontextの 詳細を表する新しいmodifierです インターフェイスに distinct sectionとして表されます macOSでは Inspectorは trailing sidebarとして現れます Regular size classのiPad OSでもです Compact size classでは sheetとして現れます Inspectorの詳細については 「Inspectors in SwiftUI: discover the details」をご覧ください ダイアログはiOS 17とmacOS Sonomaで 幾つかの新しいcustomization APIが 加わりました ここではimage export dialogに 便利な情報を与えるために 新しいmodifierを使っています Confirmation labelがその一つです 厳密さの強化によって 重要な確認ダイアログに注意を引き suppression toggleを足すことで それに続くやりとりには ダイアログは表さないという 設定が可能です 最後にHelpLinkの追加で ダイアログの目的について さらなる情報へのガイドとなります リストやテーブルは多くのアプリに重要で iOS 17とmacOS Sonomaで SwiftUIにそれらを微調整するための 新しい機能とAPIが加わります Tablesは属性順序と表示のカスタマイズを サポートします SceneStorage dynamic propertyを加えれば これらの設定は アプリを通して持続します Customization state valueのある テーブルを供給し 各columnに独自の stable identifierを割り当てます またTablesにはOutlineGroupの パワーが組み込まれています これは階層構成の 大型データに最適で お気に入りの犬とその飼い主の グループ化がその例です 単にDisclosureTableRowを使って 別の列を含む 列を示し 通常通り残りのテーブルを作成します リスト及びテーブル内のSectionsは programmatic expansionをサポートします アプリのサイドバーに使用し Locationsセクションを 拡張できるものの 最初は閉じた状態で現れます 新しいInitializerはセクションでの 現expansion stateを反映する値を バインディングします 小さいデータセットでも Tablesにrow backgroundsや column headersが どう表示させるかというような styling affordanceが加わりました 最後にstar ratingのような カスタムコントロールも background prominence environment propertyの恩恵を得ます 背景が目立つ時に それほど目立たない 前景スタイルを使用するのは リストのカスタムコントロールに最適です リストとテーブルの微調整をする これらの機能やAPIに加え 性能にも大きな改善を加えました 特に大きいデータセットを扱う時です このことやSwiftUI viewsの 最適化については 「Demystify SwiftUI performance」を ご覧ください ObservableやSwiftDataにInspector そしてテーブルのカスタマイズまで データの扱いが新体験のようになります Jeffが作成した データモデルとテーブルで 素晴らしいアプリの骨組みができました さらにアニメーションAPIの お話をしましょう 犬の写真を眺められる Apple TVアプリはどうでしょう これはviewerを選ぶ時の アニメーションです Keyframe Animator APIを使って 作成しました Keyframe animatorsは いくつものプロパティを並行して アニメーション化することができます Animatorに animatable propertiesを含むvalueと equatable stateを与えます Stateの変化でアニメーションします 最初のクロージャでは animatable propertiesで 変更したviewを作成しました その例がロゴの縦位置オフセットです 次のクロージャで これらのpropertiesが時間を経て どう変わるか定義します 例えば最初のtrackは erticalTranslation propertの アニメーションを定義します Spring animationを使い 最初の4分の1秒で ロゴを30ポイント下げます そしてcubic curveでビーグルを ジャンプそして着地させます 最後にspring animationで 元の位置に戻します ほかのanimated propertiesも さらなるtracksで定義します これらのtracksを並行に作動させ完成です Keyframe animatorsの活用については 「Wind your way through advanced animations in SwiftUI」をご覧ください ジョギング中に出会った犬の 記録のためにApple Watchアプリも 作っています 今のところハッピーなアイコンと 出会いを記録するボタンだけです ボタンをタップした時にアニメーションさせましょう ここではPhase animatorが役に立ちます Phase animatorは keyframe animatorよりシンプルです Parallel tracksに代わりに 一連のフェーズのシーケンスを 順にたどります 1つのアニメーションが終われば 次のアニメーションを始められます Animatorにフェーズのシーケンスを与え sightingCountが変われば アニメーション開始するよう伝えます そして最初のクロージャで 現在のフェーズに基づき 犬のrotationとscaleを設定します 次のクロージャでSwiftUIに各フェーズに どうアニメーションさせるのかを伝えます ここではナイスな spring animationsを使っています これらの名前を私は気に入ってます snappyやbouncy animationなんて 使いたくなるでしょう そしてgrow phaseには custom springを使用しています Springsはdurationとbounceを 設定でき説明が楽になりました SwiftUI animationが使えるなら どこでもこれらのsprinsを使用できます Spring animationsには自然感があり これまでのanimationと匹敵する速さで リアルな摩擦で最終の値に落ち着きます これがiOS 17以降に 作成されたアプリの デフォルトアニメーションです アニメーションは完成です でもジョギング中ですので 感覚フィードバックがあるべきです 感覚フィードバックはタップのような 触知反応を提供し 注意を引いてアクションや イベントを強化します 手首のタップで犬を逃さなかったと 確信が持てます Sensory feedback APIで 感覚フィードバックも簡単です 感覚フィードバックを足すには sensoryFeedback modifierを付け どのようなフィードバックが いつ起こるべきかを特定します sensoryFeedback modifierは 感覚フィードバックをサポートする すべてのプラットフォームで使用できます プラットフォームによって フィードバックが違います 「Human Interface Guidelines」で どのフィードバックが アプリに適切か学べます またスタート画面の動画も visual effects modifierを使って 作っています Visual effects modifierは これらの犬の写真を位置に基づき更新します GeometryReaderは必要ありません 焦点を画面中動かすsimulationを 用いています 焦点とはこの赤い点のことです 犬を表示しているこの基盤目と 座標空間を関連付けます そしてDogCircle view内に visual effectを足します クロージャに変更するコンテンツと geometry proxyを取り込みます Geometry proxyをhelper methodにパスし scaleを計算します Geometry proxyで基盤目のviewと それと比べた1つの犬の円の フレームのサイズを得ることができます それによりシミュレーションの 焦点からの犬の距離を計算でき 焦点の犬をスケールアップできます これらすべてをvisual effectsで GeometryReaderを使わず行えます そして自動的に違ったサイズに適応できます
もう一つ例があります 出会った犬の飼い主に いいワンちゃんメッセージを送る機能を 調整していますが 犬の名前にスタイルを加え 目立たせてみましょう これは別のtext view内でtextを foreground styleに挿入できるので簡単です 見てください スタイリングを このスライダーで調節できます でもどうやって? スタイルの定義はこうです stripeSpacingとangleを asset catalogのカラーと共に custom Metal shaderにパスします SwiftUIの新しいShaderLibraryで Metal shader functionsを 直接SwiftUI shape stylesに変換できます Furdinandの名前に ストライプをレンダリングするようにです
Metal shadersを使用したいなら プロジェクトに新規Metal fileを足し SwiftUIのShaderLibraryandで shader functionを呼び出します またここには もう一ついい例があります スライダーが端に達すると シンボルがバウンドします この効果はmacOSとiOSの Sliderに組み込まれています またsymbol effect modifierで 独自のシンボルに足すこともできます このmodifierをSF Symbolか view hierarchyの すべてのシンボルに適用します Symbolsは多くの効果をサポートし pulseやvariable colorなどの continuous animationsも含みます Scaleで大きさを変えたり appear/disappearや replaceもあります bounce付きevent notificationsもあります 「Animate symbols in your app」で symbol effectsを使って みんなを喜ばす方法を 学んでみてください この例を閉じる前に もう一つの機能をご紹介します ここに単位がありますね これまでは小型英大文字を使っていましたが これからはこのルックスを 単位にtextScale modifierを適応して 得られるようになりました もしこのアプリを中国市場に持ち込むなら 中国の印刷術には 小型英大文字の概念はありませんが 単位は正しい大きさに調整されます ほかにも複数地域に対応させる ツールがあります タイ語などでは長い文字型を使用します これらの言語のテキストが 英語のような短い文字型の言語のテキストに ローカライズされると 長いテキストは幅がなかったり 切り落とされます この問題が起こり得る場合 例えば犬の名前が世界中から クラウドソースされた場合 typesettingLanguage modifierを 適応できます これはテキストにスペースが必要だと SwiftUIに伝えます これらの新APIの使用は楽しいですが 人々を圧倒させないよう アニメーションの選択には気をつけましょう SwiftUIでのアニメーションの 基本については 「Explore SwiftUI animation」を ご覧ください そして「Animate with springs」で Jacobがあらゆるデバイスでの アニメーションの作成について 教えてくれます SwiftUIの新アニメーションAPIは 素晴らしいものです これは氷山の一角で animation completion handlersから カスタム動画の構築まで まだまだたくさんあります みなさんもこれらのAPIをご活用ください これらの新しいアニメーションや効果で アプリがさらに向上します それでは最後の仕上げのための インタラクションAPIを 見てみましょう インタラクションはアプリ体験のコアで これらはiOS 17及びそれ以降のリリースで アップデートされるAPIの一部です 最近出会った犬の画面には 最後の仕上げが必要です Scroll viewで犬のカードをめくる際 出入りするカードに何らかの効果を 足してみましょう Scroll transition modifierは 先程Curtがオープン画面で使った visual effect modifierに似ています ではscroll viewでeffectsを 適用してみましょう Scaleとopacity effectsで 少しのコードを足しただけで 求めていた最後の仕上げができました 好きなドッグランのリストを side-scrolling listでこの画面に足します SwiftUIにこれができる 素晴らしい機能が追加されました Dog cardのvertical stackの上に park cardのhorizontal stackを足します ここでは containerRelativeFrame modifierで これらのpark cardsを horizontal scroll viewのサイズに スケールさせます Countは画面を幾つかの塊に 分割するか特定します Spanは幾つの塊を viewに取り入れるかを指定します これはなかなかですね でもpark cardsをパチンと 位置にはめたいとします 新機能のscrollTargetLayout modifierで簡単です LazyHStackに足しscroll viewを変更し targeted layoutのviewsと整列させます View alignmentに加え scroll viewsはpaging behaviorを 使用するよう定義できます カスタマイズするために 独自のbehaviorを scrollTargetBehavior protocolで 定義できます またscroll viewでトップの犬を 少し褒めてあげましょう 新しいscrollPosition modifierで トップの犬のIDをバインディングします スクロールで更新されていますね いつでもどの犬がトップかわかります Scroll viewでのこれらの改良点について 学びたければ 「Beyond scroll views」をご覧ください Imageはハイダイナミックレンジの コンテンツのレンダリングをサポートします allowedDynamicRange modifierで フィデリティーを維持したままの 美しいイメージが映し出されます しかし際立つイメージに 時折使用するくらいにしましょう SwiftUIで書かれたアプリは アクセシビリティ機能を そのまま使用できますが これからご紹介する 新しいアクセシビリティAPIで さらに向上できます この冒険好きな犬は 少し小さくて見えないので magnification gesture機能で ズームできるようにしました またviewに accessibilityZoomAction modifierを足します これによりVoiceOverのような支援技術が ジェスチャーを使わずに同じ機能に アクセスできるようになります アクションの方向次第で ズームレベルをアップデートすると この冒険好きが何をしてるかが 見えますね VoiceOver:image viewにズーム Appleのプラットフォームの アクセシビリティ機能について 是非とも 「Build accessible apps with SwiftUI and UIKit」をご覧ください Colorは静的メンバー構文を使って アプリのasset catalogで定義された カスタムカラーを調べることができます 使用中はcompile-time safetyで タイポで時間を無駄にしなくなります 先程お見せしたドキュメントアプリで 便利なアクションを含むメニューを ツールバーに足しました メニューのトップ部分はControlGroupで 新しいcompactMenu styleがあり アイテムをアイコンとして 水平スタックの中に表示します Tag color selectorは 新しいpalette styleのある pickerとして定義されます これをシンボルと使用すれば メニューに素晴らしい視覚表現を与えます 特にこのようにラベルに色合いを使って 区別させることができます 最後にpaletteSelectionEffect modifierで pickerで特定のものを示すsymbol variantを 使用することができます メニューが完成し Buddyの鑑札が お気に入りの色になりました テニスボールの黄色です Bordered buttonsは組み込みの形で 円形や角丸長方形など 定義できるようになりました これらのborder shape stylesは iOSとwatchOSとmacOSで使用できます macOSとiOSのボタンは ドラッグに反応するようになり editorのこのボタンのように ポップオーバーが開けます 新しいspringLoadingBehavior modifierは ドラッグポーズしたり macOSでフォースクリックでボタンを押すと アクションを起こすようになっています tvOSのボタンには highlight hover effectが加わります ギャラリーで使用しましたが button labelの image部分だけに適用し このプラットフォームに最適です このボタンのborderless styleは 今回からtvOSに追加されました ハードウェアキーボードはアプリでの対話を 促進させるのに役立つツールです ハードウェアキーボードをサポートする プラットフォームのfocusable viewsは onKeyPress modifierを使って どのキーボード入力にも直接反応できます Modifierはキー入力がマッチすれば アクションを起こします Focusのレシピを知りたければ 「The SwiftUI cookbook for focus」を ご覧ください Scroll transitionsやbehaviors ボタンからフォーカスまで これらのAPIで豊富な機能と 素晴らしいスタイルのアプリを作成できます 僕らのアプリはかなり進展したね ああ 確かに 新しいAPIは楽しいしね それも確かだ SwiftUIは今が面白い時です 新しいプラットフォーム! ObservableとSwiftDataの優雅さは SwiftUIにピッタリです アニメーションの改善もすごいね Scroll viewsも! これらのAPIで デベロッパコミュニティが 何を作り出すか楽しみだね ありがとうございました みなさんの犬によろしく! みなさん 頑張って! ♪
-
-
4:49 - watchOS 10
import SwiftUI #if os(watchOS) struct ContainerBackground_Snippet: View { @State private var selection: Int? @State var date = Date() var body: some View { NavigationSplitView { List(selection: $selection) { NavigationLink("Dates", value: -1) NavigationLink("Zero", value: 0) NavigationLink("One", value: 1) NavigationLink("Two", value: 2) } .containerBackground( Color.green.gradient, for: .navigation) } detail: { switch selection { case -1: DatePicker( "Time", selection: $date, displayedComponents: .hourMinuteAndSecond) .containerBackground( Color.yellow.gradient, for: .navigation) case let value?: DetailView(value: value) .containerBackground( Color.blue.gradient, for: .navigation) default: Text("Choose a link") } } } struct DetailView: View { var value: Int var body: some View { Text("\(value)") .font(.largeTitle) } } } #Preview { ContainerBackground_Snippet() } #endif
-
7:01 - Widget Previews
#Preview(as: .systemSmall) { CaffeineTrackerWidget() } timeline: { CaffeineLogEntry.log1 CaffeineLogEntry.log2 CaffeineLogEntry.log3 CaffeineLogEntry.log4 }
-
7:28 - SwiftUI Preview
#Preview("good dog") { ZStack(alignment: .bottom) { Rectangle() .fill(Color.blue.gradient) Text("Riley") .font(.largeTitle) .padding() .background(.thinMaterial, in: .capsule) .padding() } .ignoresSafeArea() }
-
7:33 - Mac Preview
import SwiftUI struct MacPreview_Snippet: View { @State private var drinks = Drink.sampleData @State private var selection: Drink? var body: some View { NavigationSplitView { List(drinks, selection: $selection) { drink in NavigationLink(drink.name, value: drink) } } detail: { if let selection { DrinkCard(drink: selection) } else { ContentUnavailableView( "Select a drink", systemImage: "cup.and.saucer.fill") } } } } struct DrinkCard: View { var drink: Drink var body: some View { ZStack(alignment: .top) { Rectangle() .fill(Color.blue.gradient) Text(drink.name) .padding([.leading, .trailing], 16) .padding([.top, .bottom], 4) .background(.thinMaterial, in: .capsule) .padding() } } } struct Drink: Identifiable, Hashable { let id = UUID() var name: String static let sampleData: [Drink] = [ Drink(name: "Cappuccino"), Drink(name: "Coffee"), Drink(name: "Espresso"), Drink(name: "Latte"), Drink(name: "Macchiato"), ] } #Preview { MacPreview_Snippet() }
-
8:18 - MapKit
import SwiftUI import MapKit struct Maps_Snippet: View { private let location = CLLocationCoordinate2D( latitude: CLLocationDegrees(floatLiteral: 37.3353), longitude: CLLocationDegrees(floatLiteral: -122.0097)) var body: some View { Map { Marker("Pond", coordinate: location) UserAnnotation() } .mapControls { MapUserLocationButton() MapCompass() } } } #Preview { Maps_Snippet() }
-
8:46 - Scrolling Charts
import SwiftUI import Charts struct ScrollingChart_Snippet: View { @State private var scrollPosition = SalesData.last365Days.first! @State private var selection: SalesData? var body: some View { VStack(alignment: .leading) { VStack(alignment: .leading) { Text(""" Scrolled to: \ \(scrollPosition.day, format: .dateTime.day().month().year()) """) Text(""" Selected: \ \(selection?.day ?? .now, format: .dateTime.day().month().year()) """) .opacity(selection != nil ? 1.0 : 0.0) } .padding([.leading, .trailing]) Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales)) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 30) .chartScrollPosition(x: $scrollPosition) .chartXSelection(value: $selection) } } } struct SalesData: Plottable { var day: Date var sales: Int var primitivePlottable: Date { day } init?(primitivePlottable: Date) { self.day = primitivePlottable self.sales = 0 } init(day: Date, sales: Int) { self.day = day self.sales = sales } static let last365Days: [SalesData] = buildSalesData() private static func buildSalesData() -> [SalesData] { var result: [SalesData] = [] var date = Date.now for _ in 0..<365 { result.append(SalesData(day: date, sales: Int.random(in: 150...250))) date = Calendar.current.date( byAdding: .day, value: -1, to: date)! } return result.reversed() } } #Preview { ScrollingChart_Snippet() }
-
9:00 - Donut and Pie Charts
import SwiftUI import Charts struct DonutChart_Snippet: View { var sales = Bagel.salesData var body: some View { NavigationStack { Chart(sales, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), innerRadius: .ratio(0.6), angularInset: 1.5) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) } .padding() .navigationTitle("Bagel Sales") .toolbarTitleDisplayMode(.inlineLarge) } } } struct Bagel { var name: String var sales: Int static var salesData: [Bagel] = buildSalesData() static func buildSalesData() -> [Bagel] { [ Bagel(name: "Blueberry", sales: 60), Bagel(name: "Everything", sales: 120), Bagel(name: "Choc. Chip", sales: 40), Bagel(name: "Cin. Raisin", sales: 100), Bagel(name: "Plain", sales: 140), Bagel(name: "Onion", sales: 70), Bagel(name: "Sesame Seed", sales: 110), ] } } #Preview { DonutChart_Snippet() }
-
9:31 - StoreKit
import SwiftUI import StoreKit struct SubscriptionStore_Snippet { var body: some View { SubscriptionStoreView(groupID: passGroupID) { PassMarketingContent() .lightMarketingContentStyle() .containerBackground(for: .subscriptionStoreFullHeight) { SkyBackground() } } .backgroundStyle(.clear) .subscriptionStoreButtonLabel(.multiline) .subscriptionStorePickerItemBackground(.thinMaterial) .storeButton(.visible, for: .redeemCode) } }
-
10:56 - Observable Model
import Foundation import SwiftUI @Observable class Dog: Identifiable { var id = UUID() var name = "" var age = 1 var breed = DogBreed.mutt var owner: Person? = nil } class Person: Identifiable { var id = UUID() var name = "" } enum DogBreed { case mutt }
-
11:22 - Observable View Integration
import Foundation import SwiftUI struct DogCard: View { var dog: Dog var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(Color.green) .frame(width: 400, height: 400) } } @Observable class Dog: Identifiable { var id = UUID() var name = "" var isFavorite = false } }
-
12:22 - Observable State Integration
import Foundation import SwiftUI struct AddSightingView: View { @State private var model = DogDetails() var body: some View { Form { Section { TextField("Name", text: $model.dogName) DogBreedPicker(selection: $model.dogBreed) } Section { TextField("Location", text: $model.location) } } } struct DogBreedPicker: View { @Binding var selection: DogBreed var body: some View { Picker("Breed", selection: $selection) { ForEach(DogBreed.allCases) { Text($0.rawValue.capitalized) .tag($0.id) } } } } @Observable class DogDetails { var dogName = "" var dogBreed = DogBreed.mutt var location = "" } enum DogBreed: String, CaseIterable, Identifiable { case mutt case husky case beagle var id: Self { self } } } #Preview { AddSightingView() }
-
12:33 - Observable Environment Integration
import SwiftUI @main private struct WhatsNew2023: App { @State private var currentUser: User? var body: some Scene { WindowGroup { ContentView() .environment(currentUser) } } struct ContentView: View { var body: some View { Color.clear } } struct ProfileView: View { @Environment(User.self) private var currentUser: User? var body: some View { if let currentUser { UserDetails(user: currentUser) } else { Button("Log In") { } } } } struct UserDetails: View { var user: User var body: some View { Text("Hello, \(user.name)") } } @Observable class User: Identifiable { var id = UUID() var name = "" } }
-
13:59 - SwiftData Model Container
import Foundation import SwiftUI import SwiftData @main private struct WhatsNew2023: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Dog.self) } struct ContentView: View { var body: some View { Color.clear } } @Model class Dog { var name = "" var age = 1 } }
-
14:05 - SwiftData Query
import Foundation import SwiftUI import SwiftData struct RecentDogsView: View { @Query(sort: \.dateSpotted) private var dogs: [Dog] var body: some View { ScrollView(.vertical) { LazyVStack { ForEach(dogs) { dog in DogCard(dog: dog) } } } } struct DogCard: View { var dog: Dog var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(Color.green) .frame(width: 400, height: 400) } } @Model class Dog: Identifiable { var name = "" var isFavorite = false var dateSpotted = Date.now } } #Preview { RecentDogsView() }
-
14:52 - SwiftData DocumentGroup
import SwiftUI import SwiftData import UniformTypeIdentifiers @main private struct WhatsNew2023: App { var body: some Scene { DocumentGroup(editing: DogTag.self, contentType: .dogTag) { ContentView() } } struct ContentView: View { var body: some View { Color.clear } } @Model class DogTag { var text = "" } } extension UTType { static var dogTag: UTType { UTType(exportedAs: "com.apple.SwiftUI.dogTag") } }
-
15:33 - Inspector
import SwiftUI struct InspectorContentView: View { @State private var inspectorPresented = true var body: some View { DogTagEditor() .inspector(isPresented: $inspectorPresented) { DogTagInspector() } } struct DogTagEditor: View { var body: some View { Color.clear } } struct DogTagInspector: View { @State private var fontName = FontName.sfHello @State private var fontColor: Color = .white var body: some View { Form { Section("Text Formatting") { Picker("Font", selection: $fontName) { ForEach(FontName.allCases) { Text($0.name).tag($0) } } ColorPicker("Font Color", selection: $fontColor) } } } } enum FontName: Identifiable, CaseIterable { case sfHello case arial case helvetica var id: Self { self } var name: String { switch self { case .sfHello: return "SF Hello" case .arial: return "Arial" case .helvetica: return "Helvetica" } } } } #Preview { InspectorContentView() }
-
16:10 - File Export Dialog Customization
import Foundation import SwiftUI import UniformTypeIdentifiers struct ExportDialogCustomization: View { @State private var isExporterPresented = true @State private var selectedItem = "" var body: some View { Color.clear .fileExporter( isPresented: $isExporterPresented, item: selectedItem, contentTypes: [.plainText], defaultFilename: "ExportedData.txt") { result in handleDataExport(result: result) } .fileExporterFilenameLabel("Export Data") .fileDialogConfirmationLabel("Export Data") } func handleDataExport(result: Result<URL, Error>) { } struct Data: Codable, Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .plainText) } var text = "Exported Data" } }
-
16:19 - Confirmation Dialog Customization
import Foundation import SwiftUI import UniformTypeIdentifiers struct ConfirmationDialogCustomization: View { @State private var showDeleteDialog = false @AppStorage("dialogIsSuppressed") private var dialogIsSuppressed = false var body: some View { Button("Show Dialog") { if !dialogIsSuppressed { showDeleteDialog = true } } .confirmationDialog( "Are you sure you want to delete the selected dog tag?", isPresented: $showDeleteDialog) { Button("Delete dog tag", role: .destructive) { } HelpLink { } } .dialogSeverity(.critical) .dialogSuppressionToggle(isSuppressed: $dialogIsSuppressed) } }
-
17:01 - Table Column Customization
import SwiftUI struct DogSightingsTable: View { private var dogSightings: [DogSighting] = (1..<50).map { .init( name: "Sighting \($0)", date: .now + Double((Int.random(in: -5..<5) * 86400))) } @SceneStorage("columnCustomization") private var columnCustomization: TableColumnCustomization<DogSighting> @State private var selectedSighting: DogSighting.ID? var body: some View { Table( dogSightings, selection: $selectedSighting, columnCustomization: $columnCustomization) { TableColumn("Dog Name", value: \.name) .customizationID("name") TableColumn("Date") { Text($0.date, style: .date) } .customizationID("date") } } struct DogSighting: Identifiable { var id = UUID() var name: String var date: Date } }
-
17:22 - DisclosureTableRow
import SwiftUI struct DogGenealogyTable: View { private static let dogToys = ["🦴", "🧸", "👟", "🎾", "🥏"] private var dogs: [DogGenealogy] = (1..<10).map { .init( name: "Parent \($0)", age: Int.random(in: 8..<12) * 7, favoriteToy: dogToys[Int.random(in: 0..<5)], children: (1..<10).map { .init( name: "Child \($0)", age: Int.random(in: 1..<5) * 7, favoriteToy: dogToys[Int.random(in: 0..<5)]) } ) } var body: some View { Table(of: DogGenealogy.self) { TableColumn("Dog Name", value: \.name) TableColumn("Age (Dog Years)") { Text($0.age, format: .number) } TableColumn("Favorite Toy", value: \.favoriteToy) } rows: { ForEach(dogs) { dog in DisclosureTableRow(dog) { ForEach(dog.children) { child in TableRow(child) } } } } } struct DogGenealogy: Identifiable { var id = UUID() var name: String var age: Int var favoriteToy: String var children: [DogGenealogy] = [] } }
-
17:45 - Programmatic Section Expansion
import SwiftUI struct ExpandableSectionsView: View { @State private var selection: Int? var body: some View { NavigationSplitView { Sidebar(selection: $selection) } detail: { Detail(selection: selection) } } struct Sidebar: View { @Binding var selection: Int? @State private var isSection1Expanded = true @State private var isSection2Expanded = false var body: some View { List(selection: $selection) { Section("First Section", isExpanded: $isSection1Expanded) { ForEach(1..<6, id: \.self) { Text("Item \($0)") } } Section("Second Section", isExpanded: $isSection2Expanded) { ForEach(6..<11, id: \.self) { Text("Item \($0)") } } } } } struct Detail: View { var selection: Int? var body: some View { Text(selection.map { "Selection: \($0)" } ?? "No Selection") } } }
-
17:54 - Table Display Customization And Background Prominence
import SwiftUI struct TableDisplayCustomizationView: View { private var dogSightings: [DogSighting] = (1..<10).map { .init( name: "Dog Breed \($0)", sightings: Int.random(in: 1..<5), rating: Int.random(in: 1..<6)) } @State private var selection: DogSighting.ID? var body: some View { Table(dogSightings, selection: $selection) { TableColumn("Name", value: \.name) TableColumn("Sightings") { Text($0.sightings, format: .number) } TableColumn("Rating") { StarRating(rating: $0.rating) .foregroundStyle(.starRatingForeground) } } .alternatingRowBackgrounds(.disabled) .tableColumnHeaders(.hidden) } struct StarRating: View { var rating: Int var body: some View { HStack(spacing: 1) { ForEach(1...5, id: \.self) { n in Image(systemName: "star") .symbolVariant(n <= rating ? .fill : .none) } } .imageScale(.small) } } struct StarRatingForegroundStyle: ShapeStyle { func resolve(in environment: EnvironmentValues) -> some ShapeStyle { if environment.backgroundProminence == .increased { return AnyShapeStyle(.secondary) } else { return AnyShapeStyle(.yellow) } } } struct DogSighting: Identifiable { var id = UUID() var name: String var sightings: Int var rating: Int } } extension ShapeStyle where Self == TableDisplayCustomizationView.StarRatingForegroundStyle { static var starRatingForeground: TableDisplayCustomizationView.StarRatingForegroundStyle { .init() } }
-
19:19 - Keyframe Animator
import SwiftUI struct KeyframeAnimator_Snippet: View { var body: some View { Logo(color: .blue) Text("Tap the shape") } } struct Logo: View { var color: Color @State private var runPlan = 0 var body: some View { VStack(spacing: 100) { KeyframeAnimator( initialValue: AnimationValues(), trigger: runPlan ) { values in LogoField(color: color) .scaleEffect(values.scale) .rotationEffect(values.rotation, anchor: .bottom) .offset(y: values.verticalTranslation) .frame(width: 240, height: 240) } keyframes: { _ in KeyframeTrack(\.verticalTranslation) { SpringKeyframe(30, duration: 0.25, spring: .smooth) CubicKeyframe(-120, duration: 0.3) CubicKeyframe(-120, duration: 0.5) CubicKeyframe(10, duration: 0.3) SpringKeyframe(0, spring: .bouncy) } KeyframeTrack(\.scale) { SpringKeyframe(0.98, duration: 0.25, spring: .smooth) SpringKeyframe(1.2, duration: 0.5, spring: .smooth) SpringKeyframe(1.0, spring: .bouncy) } KeyframeTrack(\.rotation) { LinearKeyframe(Angle(degrees:0), duration: 0.45) CubicKeyframe(Angle(degrees: 0), duration: 0.1) CubicKeyframe(Angle(degrees: -15), duration: 0.1) CubicKeyframe(Angle(degrees: 15), duration: 0.1) CubicKeyframe(Angle(degrees: -15), duration: 0.1) SpringKeyframe(Angle(degrees: 0), spring: .bouncy) } } .onTapGesture { runPlan += 1 } } } struct AnimationValues { var scale = 1.0 var verticalTranslation = 0.0 var rotation = Angle(degrees: 0.0) } struct LogoField: View { var color: Color var body: some View { ZStack(alignment: .bottom) { RoundedRectangle(cornerRadius: 48) .fill(.shadow(.drop(radius: 5))) .fill(color.gradient) } } } } #Preview { KeyframeAnimator_Snippet() }
-
20:35 - Phase Animator
import SwiftUI struct PhaseAnimator_Snippet: View { @State private var sightingCount = 0 var body: some View { VStack { Spacer() HappyDog() .phaseAnimator( SightingPhases.allCases, trigger: sightingCount ) { content, phase in content .rotationEffect(phase.rotation) .scaleEffect(phase.scale) } animation: { phase in switch phase { case .shrink: .snappy(duration: 0.1) case .spin: .bouncy case .grow: .spring( duration: 0.2, bounce: 0.1, blendDuration: 0.1) case .reset: .linear(duration: 0.0) } } .sensoryFeedback(.increase, trigger: sightingCount) Spacer() Button("There’s One!", action: recordSighting) .zIndex(-1.0) } } func recordSighting() { sightingCount += 1 } enum SightingPhases: CaseIterable { case reset case shrink case spin case grow var rotation: Angle { switch self { case .spin, .grow: Angle(degrees: 360) default: Angle(degrees: 0) } } var scale: Double { switch self { case .reset: 1.0 case .shrink: 0.75 case .spin: 0.85 case .grow: 1.0 } } } } struct HappyDog: View { var body: some View { ZStack(alignment: .center) { Rectangle() .fill(.blue.gradient) Text("🐶") .font(.system(size: 58)) } .clipShape(.rect(cornerRadius: 12)) .frame(width: 96, height: 96) } } #Preview { PhaseAnimator_Snippet() }
-
22:27 - Haptic Feedback
https://developer.apple.com/design/human-interface-guidelines/playing-haptics
-
22:35 - Visual Effects
import SwiftUI struct VisualEffects_Snippet: View { @State private var dogs: [Dog] = manySampleDogs @StateObject private var simulation = Simulation() @State private var showFocalPoint = false var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: itemSpacing) { ForEach(dogs) { dog in DogCircle(dog: dog, focalPoint: simulation.point) } } .opacity(showFocalPoint ? 0.3 : 1.0) .overlay(alignment: .topLeading) { DebugDot(focalPoint: simulation.point) .opacity(showFocalPoint ? 1.0 : 0.0) } .compositingGroup() } .coordinateSpace(.dogGrid) .onTapGesture { withAnimation { showFocalPoint.toggle() } } } var columns: [GridItem] { [GridItem( .adaptive( minimum: imageLength, maximum: imageLength ), spacing: itemSpacing )] } struct DebugDot: View { var focalPoint: CGPoint var body: some View { Circle() .fill(.red) .frame(width: 10, height: 10) .visualEffect { content, proxy in content.offset(position(in: proxy)) } } func position(in proxy: GeometryProxy) -> CGSize { guard let backgroundSize = proxy.bounds(of: .dogGrid)?.size else { return .zero } let frame = proxy.frame(in: .dogGrid) let center = CGPoint( x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0 ) let xOffset = focalPoint.x * backgroundSize.width - center.x let yOffset = focalPoint.y * backgroundSize.height - center.y return CGSize(width: xOffset, height: yOffset) } } /// A self-updating simulation of a point bouncing inside a unit square. @MainActor class Simulation: ObservableObject { @Published var point = CGPoint( x: Double.random(in: 0.001..<1.0), y: Double.random(in: 0.001..<1.0) ) private var velocity = CGVector(dx: 0.0048, dy: 0.0028) private var updateTask: Task<Void, Never>? private var isUpdating = true init() { updateTask = Task.detached { do { while true { try await Task.sleep(for: .milliseconds(16)) await self.updateLocation() } } catch { // fallthrough and exit } } } func toggle() { isUpdating.toggle() } private func updateLocation() { guard isUpdating else { return } point.x += velocity.dx point.y += velocity.dy if point.x < 0 || point.x >= 1.0 { velocity.dx *= -1 point.x += 2 * velocity.dx } if point.y < 0 || point.y >= 1.0 { velocity.dy *= -1 point.y += 2 * velocity.dy } } } } extension CoordinateSpaceProtocol where Self == NamedCoordinateSpace { fileprivate static var dogGrid: Self { .named("dogGrid") } } private func magnitude(dx: Double, dy: Double) -> Double { sqrt(dx * dx + dy * dy) } private struct DogCircle: View { var dog: Dog var focalPoint: CGPoint var body: some View { ZStack { DogImage(dog: dog) .visualEffect { content, geometry in content .scaleEffect(contentScale(in: geometry)) .saturation(contentSaturation(in: geometry)) .opacity(contentOpacity(in: geometry)) } } } } private struct DogImage: View { var dog: Dog var body: some View { Circle() .fill(.shadow(.drop( color: .black.opacity(0.4), radius: 4, x: 0, y: 2))) .fill(dog.color) .strokeBorder(.secondary, lineWidth: 3) .frame(width: imageLength, height: imageLength) } } extension DogCircle { func contentScale(in geometry: GeometryProxy) -> Double { guard let gridSize = geometry.bounds(of: .dogGrid)?.size else { return 0 } let frame = geometry.frame(in: .dogGrid) let center = CGPoint(x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0) let xOffset = focalPoint.x * gridSize.width - center.x let yOffset = focalPoint.y * gridSize.height - center.y let unitMagnitude = magnitude(dx: xOffset, dy: yOffset) / magnitude(dx: gridSize.width, dy: gridSize.height) if unitMagnitude < 0.2 { let d = 3 * (unitMagnitude - 0.2) return 1.0 + 1.2 * d * d * (1 + d) } else { return 1.0 } } func contentOpacity(in geometry: GeometryProxy) -> Double { opacity(for: displacement(in: geometry)) } func contentSaturation(in geometry: GeometryProxy) -> Double { opacity(for: displacement(in: geometry)) } func opacity(for displacement: Double) -> Double { if displacement < 0.3 { return 1.0 } else { return 1.0 - (displacement - 0.3) * 1.43 } } func displacement(in proxy: GeometryProxy) -> Double { guard let backgroundSize = proxy.bounds(of: .dogGrid)?.size else { return 0 } let frame = proxy.frame(in: .dogGrid) let center = CGPoint( x: (frame.minX + frame.maxX) / 2.0, y: (frame.minY + frame.maxY) / 2.0 ) let xOffset = focalPoint.x * backgroundSize.width - center.x let yOffset = focalPoint.y * backgroundSize.height - center.y return magnitude(dx: xOffset, dy: yOffset) / magnitude( dx: backgroundSize.width, dy: backgroundSize.height) } } private struct Dog: Identifiable { let id = UUID() var color: Color } private let imageLength = 100.0 private let itemSpacing = 20.0 private let possibleColors: [Color] = [.red, .orange, .yellow, .green, .blue, .indigo, .purple] private let manySampleDogs: [Dog] = (0..<100).map { Dog(color: possibleColors[$0 % possibleColors.count]) } #Preview { VisualEffects_Snippet() }
-
23:39 - Metal Shader
import SwiftUI struct ShaderUse_Snippet: View { @State private var stripeSpacing: Float = 10.0 @State private var stripeAngle: Float = 0.0 var body: some View { VStack { Text( """ \( Text("Furdinand") .foregroundStyle(stripes) .fontWidth(.expanded) ) \ is a good dog! """ ) .font(.system(size: 56, weight: .heavy).width(.condensed)) .lineLimit(...4) .multilineTextAlignment(.center) Spacer() controls Spacer() } .padding() } var stripes: Shader { ShaderLibrary.angledFill( .float(stripeSpacing), .float(stripeAngle), .color(.blue) ) } @ViewBuilder var controls: some View { Grid(alignment: .trailing) { GridRow { spacingSlider ZStack(alignment: .trailing) { Text("50.0 PX").hidden() // maintains size Text(""" \(stripeSpacing, format: .number.precision(.fractionLength(1))) \ \(Text("PX").textScale(.secondary)) """) .foregroundStyle(.secondary) } } GridRow { angleSlider ZStack(alignment: .trailing) { Text("-0.09π RAD").hidden() // maintains size Text(""" \(stripeAngle / .pi, format: .number.precision(.fractionLength(2)))π \ \(Text("RAD").textScale(.secondary)) """) .foregroundStyle(.secondary) } } } .labelsHidden() } @ViewBuilder var spacingSlider: some View { Slider( value: $stripeSpacing, in: Float(10.0)...50.0) { Text("Spacing") } minimumValueLabel: { Image( systemName: "arrow.down.forward.and.arrow.up.backward") } maximumValueLabel: { Image( systemName: "arrow.up.backward.and.arrow.down.forward") } } @ViewBuilder var angleSlider: some View { Slider( value: $stripeAngle, in: (-.pi / 2)...(.pi / 2)) { Text("Angle") } minimumValueLabel: { Image( systemName: "arrow.clockwise") } maximumValueLabel: { Image( systemName: "arrow.counterclockwise") } } } // NOTE: create a .metal file in your project and add the following to it: /* #include <metal_stdlib> using namespace metal; [[ stitchable ]] half4 angledFill(float2 position, float width, float angle, half4 color) { float pMagnitude = sqrt(position.x * position.x + position.y * position.y); float pAngle = angle + (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x)); float rotatedX = pMagnitude * cos(pAngle); float rotatedY = pMagnitude * sin(pAngle); return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2; } */ #Preview { ShaderUse_Snippet() }
-
25:01 - Symbol Effect
import SwiftUI struct SymbolEffect_Snippet: View { @State private var downloadCount = -2 @State private var isPaused = false var scaleUpActive: Bool { (downloadCount % 2) == 0 } var isHidden: Bool { scaleUpActive } var isShown: Bool { scaleUpActive } var isPlaying: Bool { scaleUpActive } var body: some View { ScrollView { VStack(spacing: 48) { Image(systemName: "rectangle.inset.filled.and.person.filled") .symbolEffect(.pulse) .frame(maxWidth: .infinity) Image(systemName: "arrow.down.circle") .symbolEffect(.bounce, value: downloadCount) Image(systemName: "wifi") .symbolEffect(.variableColor.iterative.reversing) Image(systemName: "bubble.left.and.bubble.right.fill") .symbolEffect(.scale.up, isActive: scaleUpActive) Image(systemName: "cloud.sun.rain.fill") .symbolEffect(.disappear, isActive: isHidden) Image(systemName: isPlaying ? "play.fill" : "pause.fill") .contentTransition(.symbolEffect(.replace.downUp)) } .padding() } .font(.system(size: 64)) .frame(maxWidth: .infinity) .symbolRenderingMode(.multicolor) .preferredColorScheme(.dark) .task { do { while true { try await Task.sleep(for: .milliseconds(1500)) if !isPaused { downloadCount += 1 } } } catch { print("exiting") } } } } #Preview { SymbolEffect_Snippet() }
-
25:35 - Metal Shader (cont.)
import SwiftUI struct ShaderUse_Snippet: View { @State private var stripeSpacing: Float = 10.0 @State private var stripeAngle: Float = 0.0 var body: some View { VStack { Text( """ \( Text("Furdinand") .foregroundStyle(stripes) .fontWidth(.expanded) ) \ is a good dog! """ ) .font(.system(size: 56, weight: .heavy).width(.condensed)) .lineLimit(...4) .multilineTextAlignment(.center) Spacer() controls Spacer() } .padding() } var stripes: Shader { ShaderLibrary.angledFill( .float(stripeSpacing), .float(stripeAngle), .color(.blue) ) } @ViewBuilder var controls: some View { Grid(alignment: .trailing) { GridRow { spacingSlider ZStack(alignment: .trailing) { Text("50.0 PX").hidden() // maintains size Text(""" \(stripeSpacing, format: .number.precision(.fractionLength(1))) \ \(Text("PX").textScale(.secondary)) """) .foregroundStyle(.secondary) } } GridRow { angleSlider ZStack(alignment: .trailing) { Text("-0.09π RAD").hidden() // maintains size Text(""" \(stripeAngle / .pi, format: .number.precision(.fractionLength(2)))π \ \(Text("RAD").textScale(.secondary)) """) .foregroundStyle(.secondary) } } } .labelsHidden() } @ViewBuilder var spacingSlider: some View { Slider( value: $stripeSpacing, in: Float(10.0)...50.0) { Text("Spacing") } minimumValueLabel: { Image( systemName: "arrow.down.forward.and.arrow.up.backward") } maximumValueLabel: { Image( systemName: "arrow.up.backward.and.arrow.down.forward") } } @ViewBuilder var angleSlider: some View { Slider( value: $stripeAngle, in: (-.pi / 2)...(.pi / 2)) { Text("Angle") } minimumValueLabel: { Image( systemName: "arrow.clockwise") } maximumValueLabel: { Image( systemName: "arrow.counterclockwise") } } } // NOTE: create a .metal file in your project and add the following to it: /* #include <metal_stdlib> using namespace metal; [[ stitchable ]] half4 angledFill(float2 position, float width, float angle, half4 color) { float pMagnitude = sqrt(position.x * position.x + position.y * position.y); float pAngle = angle + (position.x == 0.0f ? (M_PI_F / 2.0f) : atan(position.y / position.x)); float rotatedX = pMagnitude * cos(pAngle); float rotatedY = pMagnitude * sin(pAngle); return (color + color * fmod(abs(rotatedX + rotatedY), width) / width) / 2; } */ #Preview { ShaderUse_Snippet() }
-
26:11 - Typesetting Language
import SwiftUI struct TypesettingLanguage_Snippet: View { var dog = Dog( name: "ไมโล", language: .init(languageCode: .thai), imageName: "Puppy_Pitbull") func phrase(for name: Text) -> Text { Text( "Who's a good dog, \(name)?" ) } var body: some View { HStack(spacing: 54) { VStack { phrase(for: Text("Milo")) } VStack { phrase(for: Text(dog.name)) } VStack { phrase(for: dog.nameText) } } .font(.title) .lineLimit(...5) .multilineTextAlignment(.leading) .padding() } struct Dog { var name: String var language: Locale.Language var imageName: String var nameText: Text { Text(name).typesettingLanguage(language) } } } #Preview { TypesettingLanguage_Snippet() }
-
27:46 - ScrollView Transitions And Behaviors
import SwiftUI struct ScrollingRecentDogsView: View { private static let colors: [Color] = [.red, .blue, .brown, .yellow, .purple] private var dogs: [Dog] = (1..<10).map { .init( name: "Dog \($0)", color: colors[Int.random(in: 0..<5)], isFavorite: false) } private var parks: [Park] = (1..<10).map { .init(name: "Park \($0)") } @State private var scrolledID: Dog.ID? var body: some View { ScrollView { LazyVStack { ForEach(dogs) { dog in DogCard(dog: dog, isTop: scrolledID == dog.id) .scrollTransition { content, phase in content .scaleEffect(phase.isIdentity ? 1 : 0.8) .opacity(phase.isIdentity ? 1 : 0) } } } } .scrollPosition(id: $scrolledID) .safeAreaInset(edge: .top) { ScrollView(.horizontal) { LazyHStack { ForEach(parks) { park in ParkCard(park: park) .aspectRatio(3.0 / 2.0, contentMode: .fill) .containerRelativeFrame( .horizontal, count: 5, span: 2, spacing: 8) } } .scrollTargetLayout() } .scrollTargetBehavior(.viewAligned) .padding(.vertical, 8) .fixedSize(horizontal: false, vertical: true) .background(.thinMaterial) } .safeAreaPadding(.horizontal, 16.0) } struct DogCard: View { var dog: Dog var isTop: Bool var body: some View { DogImage(dog: dog) .overlay(alignment: .bottom) { HStack { Text(dog.name) Spacer() if isTop { TopDog() } Spacer() Image(systemName: "heart") .symbolVariant(dog.isFavorite ? .fill : .none) } .font(.headline) .padding(.horizontal, 22) .padding(.vertical, 12) .background(.thinMaterial) } .clipShape(.rect(cornerRadius: 16)) } } struct DogImage: View { var dog: Dog var body: some View { Rectangle() .fill(dog.color.gradient) .frame(height: 400) } } struct TopDog: View { var body: some View { HStack { Image(systemName: "trophy.fill") Text("Top Dog") Image(systemName: "trophy.fill") } } } struct ParkCard: View { var park: Park var body: some View { RoundedRectangle(cornerRadius: 8) .fill(.green.gradient) .overlay { Text(park.name) .padding() } } } struct Dog: Identifiable { var id = UUID() var name: String var color: Color var isFavorite: Bool } struct Park: Identifiable { var id = UUID() var name: String } }
-
31:12 - Menu Enhancements
import SwiftUI struct DogTagEditMenu: View { @State private var selectedColor = TagColor.blue var body: some View { Menu { ControlGroup { Button { } label: { Label("Cut", systemImage: "scissors") } Button { } label: { Label("Copy", systemImage: "doc.on.doc") } Button { } label: { Label("Paste", systemImage: "doc.on.clipboard.fill") } Button { } label: { Label("Duplicate", systemImage: "plus.square.on.square") } } .controlGroupStyle(.compactMenu) Picker("Tag Color", selection: $selectedColor) { ForEach(TagColor.allCases) { Label($0.rawValue.capitalized, systemImage: "tag") .tint($0.color) .tag($0) } } .paletteSelectionEffect(.symbolVariant(.fill)) .pickerStyle(.palette) } label: { Label("Edit", systemImage: "ellipsis.circle") } .menuStyle(.button) } enum TagColor: String, CaseIterable, Identifiable { case blue case brown case green case yellow var id: Self { self } var color: Color { switch self { case .blue: return .blue case .brown: return .brown case .green: return .green case .yellow: return .yellow } } } }
-
32:30 - Highlight Hover Effect
import SwiftUI struct DogGalleryCard: View { @FocusState private var isFocused: Bool var body: some View { Button { } label: { VStack { RoundedRectangle(cornerRadius: 8) .fill(.blue) .frame(width: 888, height: 500) .hoverEffect(.highlight) Text("Name") .opacity(isFocused ? 1 : 0) } } .buttonStyle(.borderless) .focused($isFocused) } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。