View in English

  • メニューを開く メニューを閉じる
  • Apple Developer
検索
検索を終了
  • Apple Developer
  • ニュース
  • 見つける
  • デザイン
  • 開発
  • 配信
  • サポート
  • アカウント
次の内容に検索結果を絞り込む

クイックリンク

5 クイックリンク

ビデオ

メニューを開く メニューを閉じる
  • コレクション
  • トピック
  • すべてのビデオ
  • 利用方法

その他のビデオ

ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。

  • 概要
  • トランスクリプト
  • コード
  • Apple Watch用の仕事効率化Appの構築

    手首ががかつてないほど生産的になります。SwiftUIやシステム機能を使用して、優れた生産性を持つAppをApple Watchに構築する方法をご確認ください。手首に快適な作業エクスペリエンスをもたらす方法をはじめ、テキスト入力を得たり、基本的なグラフを表示したり、友人にコンテンツを共有したりする方法について解説します。

    リソース

    • Building a productivity app for Apple Watch
    • watchOS apps
      • HDビデオ
      • SDビデオ

    関連ビデオ

    WWDC22

    • Swift Charts: より高いレベルへ
    • Swift Chartsの紹介
    • SwiftUIのナビゲーション機能
    • SwiftUIの最新情報
    • Transferableの紹介
  • ダウンロード

    こんにちは ようこそ! Anne Hitchcockです 私は watchOSソフトウェアの エンジニアです 今日は watchOSで 生産性向上Appを 作る方法を 紹介したいと思います watchOS 6でSwiftUIと Independent Watch Appが 導入されて以来 より多くのことが できるようになりました 毎年 watchOS上のSwiftUIは 多くの機能を追加しています 同時に watchOSはキーボードのような 新機能も追加しました 全く新しいAppを 作ることができます 今回は その機能を結合して To-Doリストを 記録するAppを 作る方法をご紹介します 新しいWatch Appを作成し 表示するアイテムの 簡単なリストを追加し 人々がそのリストに アイテムを 追加できるようにします これらの機能を 追加していくと 今回はWatch Appの一般的な Appのナビゲーション戦略と その正しい選択方法を 紹介します

    友人とアイテムをシェアし 負担を分担します

    次に Appにチャートを 追加します 生産性の傾向を把握し モチベーションを維持します

    そして デジタルクラウンを使って チャートをスクロールさせ より広いデータ範囲を 表示させることができます

    では さっそく 新しいAppを作りましょう

    Xcodeで新規プロジェクトを 作ります watchOSタブで Appを選択 「次へ」をクリックします

    プロダクト名を選択した後 いくつかの選択肢があります 最も重要なものことは Watch専用Appを 作るかどうかです Watch Appを 作成するために 優れたWatch Appの条件と コンパニオン用のiOS Appが どんな時に必要なのか 説明します

    Watchの優れたAppは インターフェイスのように 素早いインタラクションを 可能にします お気に入りのワークアウトを 素早く開始できます 誰も立ち止まり 腕を上げ タップして何かを 探そうとはしません 重要な情報に 簡単にアクセスできるという 特徴があります

    優れたWatch Appは本質的な 目的にフォーカスしています 例えば 天気予報Appでは 今日の天気予報を表示します 現在の状況です そして 10日間のシンプルな予報です

    必要な情報を簡単に 見つけられるように Appの本質に 焦点を当てましょう 必要な情報や行動を 簡単に見つけられます

    優れたWatch Appは ペアとなるiPhoneとは 独立して使用できるように 設計されています 例えば連絡先Appは iPhoneと同期していますが iPhoneが近くになくても Apple Watch上で 連絡先情報にアクセス することができます

    また Apple Watchで 取得したデータの 履歴を提供したり フィットネスAppのように トレンドの詳細分析で Watch Appのペアとなる iOS Appが必要な理由は たくさんあります

    私たちのAppは 機能を絞っているので 迅速なインタラクション そして限られたデータで Watch専用Appを作成します

    この時点で 私は作成する ターゲットの話を 数分かけてします

    過去にWatch Appを 作ったことがある方 プロジェクトには Watchのターゲットが2つあります ストーリーボード アセット ローカライズ ファイルを含むWatchKit Appターゲットと Appの全コードを含むWatchKit Extensionのターゲットです このデュアルターゲットは watchOSの初期から 引き継いできたものでしたが 複数のWatchターゲットが 必要な理由はもうありません Xcode14から 新しいWatch Appは 1つのWatch Appの ターゲットとなり コード アセット ローカライズのすべて Siri IntentとWidget Extension Watch Appに関連する全てが 1つのターゲットに属しています

    シングルターゲットの Watch AppがwatchOS 7まで サポートされたことは ビックニュースです! プロジェクト構造を シンプルにし 混乱や重複を減らしつつ 最新のwatchOSを 実行していない

    お客様もサポートする ことができます 既存のWatchKit Extensionの ターゲットのAppがある場合 それも引き続き動作します Xcodeを使用して Appを更新でき App Storeから Appを公開できます

    すでに SwiftUIの ライフサイクルを使った Watch Appを使用している場合 シングルターゲットへの移行は Xcode14のツールを使えば 簡単にできます ターゲットの設定をし 「Validate Settings」を Editorメニューから選択します デプロイメント対象が はwatchOS 7以降の場合です ターゲットを閉じるオプションが 提供されます

    まだそうしていないなら 今こそ みなさんのAppを SwiftUIのライフサイクルを 使用するように変換するよう プロセスを開始する 良い機会です シングルターゲットの Watch Appのシンプルさと SwiftUIの機能のすべてを 楽しむことができます

    Xcode 14で簡略化したのは ターゲットではありません 1024x1024ピクセルの画像を 1枚用意するだけで Appのアイコンを 簡単に追加できるように なりました

    Appのアイコン画像は 全てのWatchデバイス用に 拡大縮小されます

    ホーム画面 通知 iPhoneのWatch Appの Appの設定など アイコンを表示して テストしてください

    必要に応じて 特定の小さいサイズ用 カスタム画像を 追加することもできます 例えば Appの アイコンを小さくすると 画像の詳細が 消えてしまう場合 画像の詳細を削除した 特定のアイコン画像を 追加することができます では Appに いくつかの機能を 追加してみましょう データモデルを 作成することから始めます ListItem構造体は IdentifiableとHashableとして 表示用の説明文をセットします

    次に データを格納するため 簡単なモデルを作成し リストアイテムの 配列をパブリッシュします

    そして最後に モデルをEnvironment オブジェクトとして追加し ビューがモデルに アクセスできるようにします

    では SwiftUIでリストを 作ってみましょう まだタスクがないため プレビューすると 空のリストが表示されます

    何とかしないとですね タスクを追加する方法を 提供すべきです

    そこでボタンを 追加したいと思います 入力すると 新しいアイテムが追加されます watchOS 9の新機能 テキストフィールドのリンク ボタンから 呼び出すことができます いくつかのスタイリング オプションがあります これを使えば Appに違和感なく 溶け込めるはずです

    シンプルな文字列で基本的な テキストを作成したり ラベルを使用してカスタム ボタンを作成できます

    ボタンの見た目を foregroundColor foregroundStyle buttonStyleなどの ビューモディファイアで変更します

    AddItemLinkビューを 作成して スタイルと Appで使っている TextFieldLinkの挙動を カプセル化します

    ボタンにはカスタム ラベルを使用し 誰かがテキストを 入力すると 新しいアイテムがリストに 追加されます

    TextFieldLinkを使って 追加ボタンを 追加することにしました TextFieldLinkをどこに置くかを 考える必要があります

    Watch Appでリストに アクションを追加する場合 いくつかの選択肢が あります 短いリストのメイン アクションには リストの末尾に TextFieldLinkを使用します アクションをリストの 末尾に追加するのは 世界時計の 都市リストのように 短いリストの中の メインアクションに 適しています しかし 長いリストが予想される場合 アクションを 実行するたびにリストの 最後までスクロール し続けなければなりません よく使うアクションに対し リストが長い場合は ツールバーアイテムを 使用します

    ツールバーアイテムを 追加するには リストにtoolbarモディファイアを 追加します これは ツールバー アイテムの自動配置で リストにツールバー アイテムを1つ追加します 常にTo-Doリストを 短く保っていると 思いたいのですが そうでない場合が あるのは確かです テキストフィールドリンクを ツールバーに配置し 簡単に アクセスできるようにします

    私たちが達成したことを 少し振り返ってみましょう リストアイテムの モデルを作成し Envrionmentオブジェクトとして 保存し アイテムを表示する リストを作成し テキストフィールドリンク を追加しました

    説明文だけのアイテムを 作成することは 簡単ですが あまり便利とは言えません 優先順位を設定したり 作業量の見積もりを 追加する方法が 必要かもしれません そのために 詳細ビューを追加します その前に Watch上のSwiftUIにおける Appのナビゲーション構造の オプションについて 復習しておきたいと思います 階層型ナビゲーションは リストとディテールの関係性を持つ ビューに使用されます watchOS 9 からはSwiftUIの NavigationStackを使って このタイプのインターフェースを 作成します

    ページベースの ナビゲーションは フラットな構造の ビューに使用されます すべてのビューが 同列であること

    ページベースの ナビゲーションとして良い例が ワークアウトAppの ワークアウト中のビューで スワイプしやすい場所に ワークアウトコントロール メトリクス 再生コントロールがあります

    フルスクリーンAppは 1つのビューを持ち ディスプレイ全体を 使ってそれを表示します これは一般的に ゲームなどのAppに 使われます

    フルスクリーン表示の場合 ディスプレイの端まで コンテンツを拡張するために ignoresSafeAreaモディファイアを 使用し ナビゲーション バーを隠すために visibility値をhiddenにした toolbarモディファイアを使用します

    モーダルシートは 現在のビューの上に スライドして表示される 全画面のビューです 重要なタスクに 使用されるべきです ワークフローの一部として 完了する必要があるものなどです

    差別化することが大切です 階層的なフローと モーダルなシートを

    使い分けることが重要です メールでは階層型を採用しており 各メッセージやスレッドを 詳細ビューで表示します メッセージの詳細から できるアクションがあります ただし リストに戻る前に しなければならないことは 何もありません

    リストに戻り新規メッセージを タップした場合 メールではモーダルシートを 使って表示します モーダルなシートは 正しい選択です 続けるには 新しいメッセージの 詳細を入力したり キャンセルしたりする 必要があるからです

    モーダルシートを 表示させるには シートのプレゼンテーション状態を 制御するプロパティを作成します ユーザーインターフェースの 操作に基づき プロパティを設定し sheetモディファイアを使用して プレゼンテーション状態 プロパティがtrueのときに カスタムモーダルシートの コンテンツを表示します

    モーダルシートにカスタム ツールバーを加えるには ツールバーにアイテムを追加します ツールバーのアイテムは confirmationAction cancelActionなどの モーダルプレースメントを 使用する必要があることに 注意してください

    アイテムを編集しているため この1つのタスクに 集中したいので 詳細ビューには モーダルシートを使い 終了して完了を タップします

    NavigationStackと プログラムの詳細を含む SwiftUIのナビゲーションに ついてもっと学ぶには "ナビゲーションの SwiftUIレシピ" を チェックしてください

    これで 詳細ビューへの ナビゲート方法が決まりました リストアイテムの 構造体を更新します 作成日 完了日を 新しいプロパティとして 追加しました

    これらの詳細を表示 編集できるようにしましょう

    説明文を編集するための 詳細ビューを作成します タスクが完了したかどうかを マークするトグルがあります しかし どうすれば いいのでしょうか? 値はすべて数字に なることが分かっています 有効な値の範囲を 指定することができます

    watchOS 9から ステッパーを 使えばいいんです ステッパーは 素晴らしいオプションです きめ細かな制御を 提供したい場合

    値の範囲を 指定することができます

    ステッパーは 以下のような編集も可能です 必ずしも論理的に連続した 数値でなくてもよいのです 例えば 次のようなことを 書きたいかもしれません あるアイテムの 推定ストレスレベル

    ストレスレベルを示す 絵文字の配列を作成し ステッパーを作成して レベルの絵文字配列の 選択されたインデックスに 値をバインドし 絵文字インデックスの 範囲に対し 範囲設定することができます 数値の段階的な 増加または減少 ストレスレベルを 推定しています

    WWDCのセッションを 準備するのも楽しいですが Watch App開発を共有するのは とても楽しいことです ストレスになるアイテムが あるとき あるいはリストにたくさんの アイテムがありストレスを 感じているとき リストのアイテムを友人に シェアして助けを 求めたいと思うのです

    詳細ビューにボタンを追加して シートを使って アイテムを 共有できるようにします 詳細ビューでボタンを タップしてアイテムを共有し リストから友達を選び 助けを求め メッセージを編集して 送信できるようにしたいです

    これを実現するために 新しいツールが watchOS 9のSwiftUIで 利用できるようになりました ShareLinkです リストアイテムを 共有するには オプションで メッセージの初期テキストを カスタマイズすることが できます そして 共有シートで プレビューを提供します 誰かがそのアイテムを シェアしたとき ShareLinkを使って SwiftUI Appから iOS macOS watchOSで 共有することができます

    ShareLinkの詳細と オプションについては "Transferableの紹介"を ぜひご覧ください いつアイテムを完了したかを 記録したり 物事を終わらせるために 助けを呼んだりすることが できますが 自分の生産性を見るための チャートも追加したいと 思っています 棒グラフを選んだのは データ系列が1つで データ値が明確だからです 異なるデータ値を 持っているからです 棒グラフは一度に表示する データ量を制限すれば Watchのディスプレイで データを明確に 表示することができます チャートビューを Appの ナビゲーション構造に 追加しました ページベースの ナビゲーション戦略を 選択したのは アイテムリストとチャートの間に リストと詳細の関係がない からです 誰かがリストの間を スワイプすると いつでもチャートが 表示されます

    リストとグラフに ページベースの ナビゲーションを追加します まず ItemList構造体を 作成し リストビューを カプセル化します

    コンテンツビューの コンテンツ全体を この新しいアイテム リストに追加しました ここでアイテムリストを カプセル化することで コンテンツビューの中に シンプルで 読みやすいタブビューの コードを持てます

    一時的にプレース ホルダを入れ チャートを作成する前に ナビゲーション構造に 集中します

    コンテンツビューを 設定します アイテムリストとチャートという 2つのタブを持つ ページスタイルの タブビューで表示しています

    ナビゲーション構造を 設定したので このチャートをどのように 構築するか説明します SwiftUI Canvasを 使ってチャートを 描くことができるのは 分かりましたが watchOS 9から もっと簡単な答えがあります Swift Chartsです Swift ChartsはiOSでも macOS tvOSでも利用できます これでSwiftUIを使っているところで チャートを再利用できます

    チャート化したいデータを 集計してみます Swift Chartsに それを表示させます

    完了したアイテムの数を 日付ごとに表示します グラフの集計データを 格納するために

    構造体を作り リストアイテムのデータを 集計し チャートの データ要素に変換するための 小さなメソッドを書きます

    表示するデータを指定して 簡単なグラフを表示します そして データから系列を 定義します 日付をx値として使い 完了したアイテムの数を Y値とします

    Watchに表示させるために ChartのchartXAxis モディファイアで X軸をカスタマイズしています 軸の値ラベルの書式を 指定しています また 縦方向の グリッドラインもいらないので AxisGridLineマークを 省略しました また Y軸もchartYAxisモディファイアで カスタマイズします グリッドラインスタイルを Watchに合うよう指定しています 軸の値ラベルを整数値で フォーマットし 一番上のラベルを 省略しています グラフの上端で 切り取れないよう削除しています Swift Chartsで実現できる 素晴らしいことを もっと知るには "Swift Chartsの紹介" と "Swift Charts: より高い レベルへ" をご覧ください

    我々のチャートは かなり良さそうです もう少しデータを 表示させたいと思います 素晴らしいWatch体験は そのままに スクロール できるようにします これを実現するために デジタルクラウンイベントの コールバックを設定できる 新しいdigitalCrownRotation モディファイアを使用し デジタルクラウン イベントのため チャートの カスタムスクロールの 動作を実装するつもりです

    digitalCrownRotation モディファイアを 追加するために グラフをスクロール時の 状態を保存するプロパティを 追加して準備しましょう highlightedDateIndexは データポイントの インデックスを 現在のスクロール位置とし

    クラウンオフセットを 格納します 現在のクラウンの位置に スクロールさせながら 表示します データポイント上 またはデータポイント間の 中間値であり クラウンが動いている間です

    誰かがクロールしているか どうかを記録するために アイドル状態を保存します この情報を利用して クラウンのスクロールが 止まったり始まったりした時の アニメーションを追加する

    値を格納するための プロパティを作成しました digitalCrownRotation モディファイアを追加しました

    highlightedDateIndex プロパティにバインドします

    機械用語で 戻り止めとは あるものを動かすのに 十分な力が加わるまで その位置を保持する 機構のことです 例えば 車のドアを 開けるとき 停止位置というものが あります もうちょっと頑張って ドアを大きく開けて もう一つの停止位置へ 閉じるには 強く引っ張る必要があります 抵抗に打ち勝つために 引き出すことができます そうしないと 元の位置に 戻ってしまいます それが戻り止めです まさに 車のドアの停止位置が APIでの戻り止めを 理解することに役立ちます 戻り止めとは ビュー上のクラウンの 静止しているノッチ位置です

    onChangeコールバックの ハンドラで isCrownIdleの値を falseにします スクロールして いることがわかるので crownOffset値を を現在の値に設定します スクロール中にチャート上の 現在位置を知らせます

    onIdleコールバックの ハンドラで isCrownIdleの値を trueにします

    クラウンの位置を チャートのスクロールに従って 表示することができます Swift Chartsの RuleMarkを使用します RuleMarkは チャート上の直線です 水平または垂直な線を 表示するために使用します 閾値を表示させることが できます 傾斜した線を表示させたり できます

    クラウンオフセットの 日付の値で RuleMarkを作成します 現在の位置が表示されます

    少しだけ 見栄えを良くするために クラウン位置の 線が薄くなるように クラウンが動かなくなった時 線がフェードするようにしたいです 追加したisCrownIdle プロパティを使用することで 簡単にアニメーションできます

    使用する色の不透明度を 格納するプロパティを追加し それをRuleMarkのforeground Styleに追加しています

    チャートにonChange モディファイアを追加し isCrownIdleの値が変わった時の crownPositionOpacityの値の変更の アニメーションを行います

    そしてRuleMarkのforegroundStyleを opacityを使って更新強います

    チャートの棒の横に 値を表示するために BarMarkにアノテーションを 追加することができます 最後のバーになる時に アノテーションのバーを 先頭側に配置します それ以外の場合は 上部の後ろ側に 配置します

    digitalCrownRotation モディファイアと Swift ChartsのRuleMark SwiftUIのアニメーションだけで どんなことができたのかを 見てみましょう

    スクロール可能なカスタム チャートを作成するための 最後のステップの データ範囲を調整します 可視範囲を格納する プロパティを作成します

    データを提供するために chartData変数を作成します highlightedDateIndexが 変化したとき chartDataRangeを 確認するメソッドを呼び出し 必要であれば 更新してください

    デジタルクラウンで チャートをスクロールさせると チャートはスクロールし データが表示されます 予定していた機能の 実装がすべて終了です

    watchOS 9で 利用ができる SwiftUIの新機能について もっと詳しく知りたい方は "SwiftUIの最新情報"を チェックしてみてください Watch Appや 新しい機能などの 企画を進めるときには 素晴らしいWatch App体験 とは何か について考えてみてください Appをデザインするときに Appのナビゲーション戦略を 考え Appが簡単で直感的で になるようにします よりシンプルで豊富な開発オプション としてSwiftUIを活用します 今後も素晴らしいWatch Appを作り続けてください そして思い出してください あなたのおかげで Appがあるのです!

    • 6:12 - Initial ListItem struct

      struct ListItem: Identifiable, Hashable {
          
          let id = UUID()
          var description: String
          
          init(_ description: String) {
              self.description = description
          }
      }
    • 6:24 - ItemListModel

      class ItemListModel: NSObject, ObservableObject {
          @Published var items = [ListItem]()
      }
    • 6:30 - Add the ItemListModel as an EnvironmentObject

      @main
      struct WatchTaskListSampleApp: App {
          
          @StateObject var itemListModel = ItemListModel()
          
          @SceneBuilder var body: some Scene {
              WindowGroup {
                  ContentView()
                      .environmentObject(itemListModel)
              }
          }
      }
    • 6:37 - Create a simple SwiftUI List

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              List {
                  ForEach($model.items) { $item in
                      ItemRow(item: $item)
                  }
                  
                  if model.items.isEmpty {
                      Text("No items to do!")
                          .foregroundStyle(.gray)
                  }
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:11 - TextFieldLink with a simple String

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink("Add") {
                      model.items.append(ListItem($0))
                  }
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:16 - TextFieldLink with a Label

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink {
                      Label(
                          "Add", 
                          systemImage: "plus.circle.fill")
                  } onSubmit: {
                      model.items.append(ListItem($0))
                  }
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:20 - TextFieldLink with foregroundStyle modifier

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink {
                      Label(
                          "Add", 
                          systemImage: "plus.circle.fill")
                  } onSubmit: {
                      model.items.append(ListItem($0))
                  }
                  .foregroundStyle(.tint)
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:27 - TextFieldLink with buttonStyle modifier

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              VStack {
                  TextFieldLink {
                      Label(
                          "Add", 
                          systemImage: "plus.circle.fill")
                  } onSubmit: {
                      model.items.append(ListItem($0))
                  }
                  .buttonStyle(.borderedProminent)
              }
              .navigationTitle("Tasks")
          }
      }
    • 7:30 - Create the AddItemLink View to encapsulate the style and behavior of the TextFieldLink to add list items

      struct AddItemLink: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              TextFieldLink(prompt: Text("New Item")) {
                  Label("Add",
                        systemImage: "plus.circle.fill")
              } onSubmit: {
                  model.items.append(ListItem($0))
              } 
          }
      }
    • 8:38 - Add a toolbar item to allow people to add new list items

      struct ContentView: View {
          @EnvironmentObject private var model: ItemListModel
          
          var body: some View {
              List {
                  ForEach($model.items) { $item in
                      ItemRow(item: $item)
                  }
                  
                  if model.items.isEmpty {
                      Text("No items to do!")
                          .foregroundStyle(.gray)
                  }
              }
              .toolbar {
                  AddItemLink()
              }
              .navigationTitle("Tasks")
          }
      }
    • 11:40 - Display a modal sheet

      struct ItemRow: View {
          @EnvironmentObject private var model: ItemListModel
          
          @Binding var item: ListItem
          @State private var showDetail = false
          
          var body: some View {
              Button {
                  showDetail = true
              } label: {
                  HStack {
                      Text(item.description)
                          .strikethrough(item.isComplete)
                      Spacer()
                      Image(systemName: "checkmark").opacity(item.isComplete ? 100 : 0)
                  }
              }
              .sheet(isPresented: $showDetail) {
                  ItemDetail(item: $item)
              }
          }
      }
    • 11:58 - Display a modal sheet with custom toolbar items

      struct ItemRow: View {
          @EnvironmentObject private var model: ItemListModel
          
          @Binding var item: ListItem
          @State private var showDetail = false
          
          var body: some View {
              Button {
                  showDetail = true
              } label: {
                  HStack {
                      Text(item.description)
                          .strikethrough(item.isComplete)
                      Spacer()
                      Image(systemName: "checkmark").opacity(item.isComplete ? 100 : 0)
                  }
              }
              .sheet(isPresented: $showDetail) {
                  ItemDetail(item: $item)
                      .toolbar {
                          ToolbarItem(placement: .confirmationAction) {
                              Button("Done") {
                                  showDetail = false
                              }
                          }
                      }
              }
          }
      }
    • 12:36 - Add more properties to the ListItem

      struct ListItem: Identifiable, Hashable {
          
          let id = UUID()
          var description: String
          var estimatedWork: Double = 1.0
          var creationDate = Date()
          var completionDate: Date?
          
          init(_ description: String) {
              self.description = description
          }
      
          var isComplete: Bool {
              get {
                  completionDate != nil
              }
              set {
                  if newValue {
                      guard completionDate == nil else { return }
                      completionDate = Date()
                  } else {
                      completionDate = nil
                  }
              }
          }
      }
    • 12:48 - Create the ItemDetail View with the Stepper

      struct ItemDetail: View {
          @Binding var item: ListItem
          
          var body: some View {
              Form {
                  Section("List Item") {
                      TextField("Item", text: $item.description, prompt: Text("List Item"))
                  }
                  Section("Estimated Work") {
                      Stepper(value: $item.estimatedWork,
                              in: (0.0...14.0),
                              step: 0.5,
                              format: .number) {
                          Text("\(item.estimatedWork, specifier: "%.1f") days")
                      }
                  }
                  
                  Toggle(isOn: $item.isComplete) {
                      Text("Completed")
                  }
              }
          }
      }
    • 13:29 - A Stepper with Emoji

      // Use a Stepper to edit the stress level of an item
      struct StressStepper: View {
          private let stressLevels = [
              "😱", "😡", "😳", "🙁", "🫤", "🙂", "🥳"
          ]
          @State private var stressLevelIndex = 5
          
          var body: some View {
              VStack {
                  Text("Stress Level")
                      .font(.system(.footnote, weight: .bold))
                      .foregroundStyle(.tint)
                  
                  Stepper(value: $stressLevelIndex,
                          in: (0...stressLevels.count-1)) {
                      Text(stressLevels[stressLevelIndex])
                  }
              }
          }
      }
    • 14:43 - Add a ShareLink to the ItemDetail View

      struct ItemDetail: View {
          @Binding var item: ListItem
          
          var body: some View {
              Form {
                  Section("List Item") {
                      TextField("Item", text: $item.description, prompt: Text("List Item"))
                  }
                  Section("Estimated Work") {
                      Stepper(value: $item.estimatedWork,
                              in: (0.0...14.0),
                              step: 0.5,
                              format: .number) {
                          Text("\(item.estimatedWork, specifier: "%.1f") days")
                      }
                  }
                  
                  Toggle(isOn: $item.isComplete) {
                      Text("Completed")
                  }
                  
                  ShareLink(item: item.description,
                            subject: Text("Please help!"),
                            message: Text("(I need some help finishing this.)"),
                            preview: SharePreview("\(item.description)"))
                  .buttonStyle(.borderedProminent)
                  .buttonBorderShape(.roundedRectangle)
                  .listRowInsets(
                      EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
                  )
              }
          }
      }
    • 16:39 - Page-style TabView with navigation titles for each page

      struct ContentView: View {
          var body: some View {
              TabView {
                  NavigationStack {
                      ItemList()
                  }
                  NavigationStack {
                      ProductivityChart()
                  }
              }.tabViewStyle(.page)
          }
      }
    • 17:20 - ChartData struct for aggregate data

      /// Aggregate data for charting productivity.
      struct ChartData {
          struct DataElement: Identifiable {
              var id: Date { return date }
              let date: Date
              let itemsComplete: Double
          }
          
          /// Create aggregate chart data from list items.
          /// - Parameter items: An array of list items to aggregate for charting.
          /// - Returns: The chart data source.
          static func createData(_ items: [ListItem]) -> [DataElement] {
              return Dictionary(grouping: items, by: \.completionDate)
                  .compactMap {
                      guard let date = $0 else { return nil }
                      return DataElement(date: date, itemsComplete: Double($1.count))
                  }
                  .sorted {
                      $0.date < $1.date
                  }
          }
      }
    • 17:36 - Static sample data for chart and basic bar chart

      extension ChartData {
          
          /// Some static sample data for displaying a `Chart`.
          static var chartSampleData: [DataElement] {
              let calendar = Calendar.autoupdatingCurrent
              var startDateComponents = calendar.dateComponents(
                  [.year, .month, .day], from: Date())
              startDateComponents.setValue(22, for: .day)
              startDateComponents.setValue(5, for: .month)
              startDateComponents.setValue(2022, for: .year)
              startDateComponents.setValue(0, for: .hour)
              startDateComponents.setValue(0, for: .minute)
              startDateComponents.setValue(0, for: .second)
              let startDate = calendar.date(from: startDateComponents)!
              
              let itemsToAdd = [
                  6, 3, 1, 4, 1, 2, 7,
                  5, 2, 0, 5, 2, 3, 9
              ]
              var items = [DataElement]()
              for dayOffset in (0..<itemsToAdd.count) {
                  items.append(DataElement(
                      date: calendar.date(byAdding: .day, value: dayOffset, to: startDate)!,
                      itemsComplete: Double(itemsToAdd[dayOffset])))
              }
              
              return items
          }
      }
      
      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
                   
          var body: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
              }
              .navigationTitle("Productivity")
              .navigationBarTitleDisplayMode(.inline)
          }
      }
    • 17:50 - Chart with chartXAxis modifier

      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
        
          private var shortDateFormatStyle = DateFormatStyle(dateFormatTemplate: "Md")
                   
          var body: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
              }
            	.chartXAxis {
                  AxisMarks(format: shortDateFormatStyle)
              }
              .navigationTitle("Productivity")
              .navigationBarTitleDisplayMode(.inline)
          }
      }
      
      /// `ProductivityChart` uses this type to format the dates on the x-axis.
      struct DateFormatStyle: FormatStyle {
          enum CodingKeys: CodingKey {
              case dateFormatTemplate
          }
          
          private var dateFormatTemplate: String
          private var formatter: DateFormatter
          
          init(dateFormatTemplate: String) {
              self.dateFormatTemplate = dateFormatTemplate
              formatter = DateFormatter()
              formatter.locale = Locale.autoupdatingCurrent
              formatter.setLocalizedDateFormatFromTemplate(dateFormatTemplate)
          }
          
          init(from decoder: Decoder) throws {
              let container = try decoder.container(keyedBy: CodingKeys.self)
              dateFormatTemplate = try container.decode(String.self, forKey: .dateFormatTemplate)
              formatter = DateFormatter()
              formatter.setLocalizedDateFormatFromTemplate(dateFormatTemplate)
          }
          
          func encode(to encoder: Encoder) throws {
              var container = encoder.container(keyedBy: CodingKeys.self)
              try container.encode(dateFormatTemplate, forKey: .dateFormatTemplate)
          }
          
          func format(_ value: Date) -> String {
              formatter.string(from: value)
          }
      }
    • 19:05 - Add the digitalCrownRotation modifier

      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
      
          /// The index of the highlighted chart value. This is for crown scrolling.
          @State private var highlightedDateIndex: Int = 0
      
          /// The current offset of the crown while it's rotating. This sample sets the offset with
          /// the value in the DigitalCrownEvent and uses it to show an intermediate
          /// (between detents) chart value in the view.
          @State private var crownOffset: Double = 0.0
      
          @State private var isCrownIdle = true
        
          private var chart: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
              }
            	.chartXAxis {
                  AxisMarks(format: shortDateFormatStyle)
              }
          }
              
          var body: some View {
              chart
                  .focusable()
                  .digitalCrownRotation(
                      detent: $highlightedDateIndex,
                      from: 0,
                      through: data.count - 1,
                      by: 1,
                      sensitivity: .medium
                  ) { crownEvent in
                      isCrownIdle = false
                      crownOffset = crownEvent.offset
                  } onIdle: {
                      isCrownIdle = true
                  }
                  .navigationTitle("Productivity")
                  .navigationBarTitleDisplayMode(.inline)
          }
      }
    • 21:07 - Add a RuleMark to the Chart to show the current Digital Crown position

      /// The date value that corresponds to the crown offset.
      private var crownOffsetDate: Date {
          let dateDistance = data[0].date.distance(
              to: data[data.count - 1].date) * (crownOffset / Double(data.count - 1))
          return data[0].date.addingTimeInterval(dateDistance)
      }
      
      private var chart: some View {
          Chart(data) { dataPoint in
              BarMark(
                  x: .value("Date", dataPoint.date),
                  y: .value(
                      "Completed", 
                      dataPoint.itemsComplete)
              )
              .foregroundStyle(Color.accentColor)
                   
              RuleMark(x: .value("Date", crownOffsetDate))
                  .foregroundStyle(Color.appYellow)
          }
          .chartXAxis {
              AxisMarks(format: shortDateFormatStyle)
          }
      }
    • 21:37 - Add animation to dim the crown position line when the scrolling idle state changes

      struct ProductivityChart: View {
             
          let data = ChartData.createData(
              ListItem.chartSampleData)
      
          /// The index of the highlighted chart value. This is for crown scrolling.
          @State private var highlightedDateIndex: Int = 0
      
          /// The current offset of the crown while it's rotating. This sample sets the offset with
          /// the value in the DigitalCrownEvent and uses it to show an intermediate
          /// (between detents) chart value in the view.
          @State private var crownOffset: Double = 0.0
      
          @State private var isCrownIdle = true
      
          @State var crownPositionOpacity: CGFloat = 0.2
        
          private var chart: some View {
              Chart(data) { dataPoint in
                  BarMark(
                      x: .value("Date", dataPoint.date),
                      y: .value(
                          “Completed", 
                          dataPoint.itemsComplete)
                  )
                  .foregroundStyle(Color.accentColor)
                           
                  RuleMark(x: .value("Date", crownOffsetDate))
                      .foregroundStyle(Color.appYellow.opacity(crownPositionOpacity))
              }
            	.chartXAxis {
                  AxisMarks(format: shortDateFormatStyle)
              }
          }
                   
          var body: some View {
              chart
                  .focusable()
                  .digitalCrownRotation(
                      detent: $highlightedDateIndex,
                      from: 0,
                      through: data.count - 1,
                      by: 1,
                      sensitivity: .medium
                  ) { crownEvent in
                      isCrownIdle = false
                      crownOffset = crownEvent.offset
                  } onIdle: {
                      isCrownIdle = true
                  }
                  .onChange(of: isCrownIdle) { newValue in
                      withAnimation(newValue ? .easeOut : .easeIn) {
                          crownPositionOpacity = newValue ? 0.2 : 1.0
                      }
                  }
                  .navigationTitle("Productivity")
                  .navigationBarTitleDisplayMode(.inline)
          }
      }
    • 22:14 - Add an annotation to the bar chart to display the current value

      private func isLastDataPoint(_ dataPoint: ChartData.DataElement) -> Bool {
          data[chartDataRange.upperBound].id == dataPoint.id
      }
      
      private var chart: some View {
          Chart(chartData) { dataPoint in
              BarMark(x: .value("Date", dataPoint.date, unit: .day),
              y: .value("Completed", dataPoint.itemsComplete))
              .foregroundStyle(Color.accentColor)
              .annotation(
                  position: isLastDataPoint(dataPoint) ? .topLeading : .topTrailing,
                  spacing: 0
              ) {
                  Text("\(dataPoint.itemsComplete, format: .number)")
                      .foregroundStyle(dataPoint.date == crownOffsetDate ? Color.appYellow : Color.clear)
              }
      
              RuleMark(x: .value("Date", crownOffsetDate, unit: .day))
                  .foregroundStyle(Color.appYellow.opacity(crownPositionOpacity))
          }
          .chartXAxis {
              AxisMarks(format: shortDateFormatStyle)
          }
      }
    • 22:44 - Make the chart data range scrollable

      @State var chartDataRange = (0...6)
      
      private func updateChartDataRange() {
          if (highlightedDateIndex - chartDataRange.lowerBound) < 2, chartDataRange.lowerBound > 0 {
              let newLowerBound = max(0, chartDataRange.lowerBound - 1)
              let newUpperBound = min(newLowerBound + 6, data.count - 1)
              chartDataRange = (newLowerBound...newUpperBound)
              return
          }
          if (chartDataRange.upperBound - highlightedDateIndex) < 2, chartDataRange.upperBound < data.count - 1 {
              let newUpperBound = min(chartDataRange.upperBound + 1, data.count - 1)
              let newLowerBound = max(0, newUpperBound - 6)
              chartDataRange = (newLowerBound...newUpperBound)
              return
          }
      }
      
      private var chartData: [ChartData.DataElement] {
          Array(data[chartDataRange.clamped(to: (0...data.count - 1))])
      }
      
      private var chart: some View {
          Chart(chartData) { dataPoint in
              BarMark(x: .value("Date", dataPoint.date, unit: .day),
                      y: .value("Completed", dataPoint.itemsComplete)
              )
              .foregroundStyle(Color.accentColor)
              .annotation(
                  position: isLastDataPoint(dataPoint) ? .topLeading : .topTrailing,
                  spacing: 0
              ) {
                  Text("\(dataPoint.itemsComplete, format: .number)")
                      .foregroundStyle(dataPoint.date == crownOffsetDate ? Color.appYellow : Color.clear)
              }
      
              RuleMark(x: .value("Date", crownOffsetDate, unit: .day))
                  .foregroundStyle(Color.appYellow.opacity(crownPositionOpacity))
          }
          .chartXAxis {
              AxisMarks(format: shortDateFormatStyle)
          }
      }
      
      var body: some View {
          chart
              .focusable()
              .digitalCrownRotation(
                  detent: $highlightedDateIndex,
                  from: 0,
                  through: data.count - 1,
                  by: 1,
                  sensitivity: .medium
              ) { crownEvent in
                  isCrownIdle = false
                  crownOffset = crownEvent.offset
              } onIdle: {
                  isCrownIdle = true
              }
              .onChange(of: isCrownIdle) { newValue in
                  withAnimation(newValue ? .easeOut : .easeIn) {
                      crownPositionOpacity = newValue ? 0.2 : 1.0
                  }
              }
              .onChange(of: highlightedDateIndex) { newValue in
                  withAnimation {
                      updateChartDataRange()
                  }
              }
              .navigationTitle("Productivity")
              .navigationBarTitleDisplayMode(.inline)
      }
  • 特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。

    クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。

Developer Footer

  • ビデオ
  • WWDC22
  • Apple Watch用の仕事効率化Appの構築
  • メニューを開く メニューを閉じる
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    Open Menu Close Menu
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    メニューを開く メニューを閉じる
    • アクセシビリティ
    • アクセサリ
    • App Extension
    • App Store
    • オーディオとビデオ(英語)
    • 拡張現実
    • デザイン
    • 配信
    • 教育
    • フォント(英語)
    • ゲーム
    • ヘルスケアとフィットネス
    • アプリ内課金
    • ローカリゼーション
    • マップと位置情報
    • 機械学習
    • オープンソース(英語)
    • セキュリティ
    • SafariとWeb(英語)
    メニューを開く メニューを閉じる
    • 英語ドキュメント(完全版)
    • 日本語ドキュメント(一部トピック)
    • チュートリアル
    • ダウンロード(英語)
    • フォーラム(英語)
    • ビデオ
    Open Menu Close Menu
    • サポートドキュメント
    • お問い合わせ
    • バグ報告
    • システム状況(英語)
    メニューを開く メニューを閉じる
    • Apple Developer
    • App Store Connect
    • Certificates, IDs, & Profiles(英語)
    • フィードバックアシスタント
    メニューを開く メニューを閉じる
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program(英語)
    • News Partner Program(英語)
    • Video Partner Program(英語)
    • セキュリティ報奨金プログラム(英語)
    • Security Research Device Program(英語)
    Open Menu Close Menu
    • Appleに相談
    • Apple Developer Center
    • App Store Awards(英語)
    • Apple Design Awards
    • Apple Developer Academy(英語)
    • WWDC
    Apple Developerアプリを入手する
    Copyright © 2025 Apple Inc. All rights reserved.
    利用規約 プライバシーポリシー 契約とガイドライン