ハイライト
Xcode 14 は、新しい watchOS アプリを単一の Watch アプリ ターゲットに簡素化します。 WatchOS 9 の TextFieldLink、Stepper、ShareLink、Swift Charts、digitalCrownRotation を使用すると、開発者はウォッチ上でタスクの入力、編集、共有、トレンド チャートの閲覧を完了できます。
主要内容
Apple Watch の生産性向上アプリは iPhone からコピーできません。ユーザーの手首を上げる時間は短く、作業は速く、情報は少なく、入り口は明確でなければなりません。スピーチの冒頭で、迅速なインタラクション、重要な情報への簡単なアクセス、そして中心的な目的に沿ったデザインという 3 つの基準が示されました (01:59)。
このセッションでは、ToDo リスト アプリを使用して、完全なプロセスをつなぎ合わせます。つまり、監視専用アプリの作成、タスク リストの表示、タスクの追加、タスクの編集、タスクの共有、そしてグラフを使用して完了傾向を表示します。各ステップは、watchOS 9 または Xcode 14 の特定の変更に対応します。
最初の変更はエンジニアリング構造です。 Xcode 14 は、新しい Watch アプリを作成するときに単一の Watch アプリ ターゲットを使用します。コード、リソース、ローカリゼーション、Siri インテント、ウィジェット拡張機能はすべてこのターゲットに配置されます。この構造は、watchOS 7 (03:31) までサポートされます。アイコンも、さまざまなデバイス サイズに合わせて拡大縮小できる 1024x1024 の画像に簡略化されています (05:20)。
2 番目の変更は、対話型コントロールにあります。TextFieldLinkボタンからのテキスト入力を担当し、Stepper小さな画面での連続値の調整を担当し、ShareLink共有フォームを開く責任を負い、Swift Charts完了したレコードをヒストグラムに描画します。やっと、digitalCrownRotationチャートをウォッチに合わせてスクロール可能なビューに変更します。
詳細
単一のデータ モデルを使用してタスク リストを推進する
(06:12) このデモでは、まず最小限のデータ モデルを確立します。各タスクは識別可能でハッシュ可能であり、表示用に説明テキストが保存されている必要があります。
struct ListItem: Identifiable, Hashable {
let id = UUID()
var description: String
init(_ description: String) {
self.description = description
}
}
class ItemListModel: NSObject, ObservableObject {
@Published var items = [ListItem]()
}
@main
struct WatchTaskListSampleApp: App {
@StateObject var itemListModel = ItemListModel()
@SceneBuilder var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(itemListModel)
}
}
}
キーポイント:
ListItem従うIdentifiable,ListそしてForEach各ラインを安定して識別できます。 -Hashableタスク値が SwiftUI のデータ比較および収集操作に参加できるようにします。 -ItemListModel従うObservableObject,@Publishedのitemsドライブインターフェイスを更新します。 -@StateObjectアプリの入り口でモデルを作成し、そのライフサイクルはアプリのシーンに従います。 -.environmentObject(itemListModel)モデルをビュー ツリーに挿入すると、リスト行と詳細ページの両方で同じデータを読み取ることができます。
(06:37) このモデルを使用すると、インターフェイスは配列要素を直接バインドできます。リストが空のときにプロンプトを表示し、ユーザーに無意味な空白スペースが表示されないようにします。
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")
}
}
キーポイント:
@EnvironmentObject入り口で注入されたものを読んでくださいItemListModel。ForEach($model.items)バインディング配列を反復処理すると、行ビューが書き込み可能なバインディングを取得します。 -ItemRow(item: $item)単一行で対応するタスクを変更できるようにします。 -model.items.isEmptyブランチは空の状態のテキストを提供します。 -.navigationTitle("Tasks")時計の上部に現在地を明確に表示します。
TextFieldLink を使用してタスクを追加する
(06:59) watchOS 9 新しいTextFieldLink。これはボタンとして表示され、クリックするとシステムのテキスト入力機能が呼び出されます。送信後、入力文字列がクロージャに渡されます。
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))
}
}
}
キーポイント:
TextFieldLink(prompt:)テキスト入力インターフェイスのプロンプトを設定します。 -Label("Add", systemImage: "plus.circle.fill")ボタンにテキストとSFシンボルの両方を与えます。 -onSubmitクロージャは、ユーザーが入力した文字列を受け取ります。 -model.items.append(ListItem($0))入力をリスト項目に直接変換します。
(07:57) ボタンが配置される場所もエクスペリエンスに影響します。短いリストでは、主要なアクションをリストの最後に置くことができます。よく使用されるアクションの長いリストは、毎回一番下までスクロールするのを避けるためにツールバーに配置するのが適しています。
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")
}
}
キーポイント:
.toolbar追加したエントリをシステム レイアウトに残します。 -AddItemLink()以前にカプセル化されたテキスト入力エントリを再利用します。- ツールバーの自動配置により、watchOS リスト内の適切な位置が選択されます。
シートとステッパーを使用してタスクを編集する
(09:40) タスクの詳細は編集プロセスに属します。このスピーチでは、ナビゲーション構造を階層ナビゲーション、ページ ナビゲーション、フル スクリーン ビュー、モーダル シートに分割します。タスクを編集するには、リストに戻る前にユーザーが完了するかキャンセルする必要があるため、ここではシートが使用されます。
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
}
}
}
}
}
}
キーポイント:
@Binding var item行ビューで元の配列内のタスクを編集させます。 -showDetailシートを表示するかどうかを制御します。 -Buttonクリック後showDetailに設定true。.sheet(isPresented:)現在のプロセスの上に詳細ページを表示します。 -ToolbarItem(placement: .confirmationAction)モーダルシート上に完了ボタンを配置します。
(12:36) 推定ワークロード、作成日、完了日を詳細ページに追加します。isComplete計算されたプロパティを使用して完了ステータスをカプセル化し、スイッチを切り替えるときに書き込みまたはクリアしますcompletionDate。
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
}
}
}
}
キーポイント:
estimatedWorkタスクの推定作業負荷を保存します。 -creationDateタスクの作成時間を記録します。 -completionDateはオプションの値です。nil未完成を表します。 -isCompleteからのゲッターcompletionDate派生ステータス。 -isComplete設定者は完了時に現在時刻を書き込み、キャンセル時に時刻をクリアします。
(13:11) watchOS 9Stepper時計上の連続値の編集に適しています。数値だけでなく、論理的に順序付けされた離散値も処理できます。
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")
}
}
}
}
キーポイント:
Form編集項目を watchOS に適した形式に整理します。 -TextFieldバインディングitem.description、ユーザーはタスク名を変更できます。 -Stepper(value:in:step:format:)推定値を以下に制限する0.0...14.0。step: 0.5一度に半日ずつ増減します。 -Toggle(isOn: $item.isComplete)計算されたプロパティを通じて完了ステータスを変更します。
ShareLink を使用して Watch にサポートをリクエストします
(14:41)ShareLinkこれは、watchOS 9 の SwiftUI の共有入口です。このアプリを使用すると、タスクを共有し、トピック、メッセージ、プレビューを事前に入力できます。
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)
)
}
}
}
キーポイント:
ShareLink(item:)共有するコンテンツを指定します。これはタスクの説明文字列です。 -subjectメッセージを共有するためのトピックを提供します。 -message共有コンテンツの最初のテキストを提供します。 -SharePreview共有フォームでプレビュー タイトルを制御します。 -.buttonStyle(.borderedProminent)フォーム内で共有ボタンをより目立つようにします。
Swift チャートを使用して完了傾向を表示する
(15:45) タスク リストと傾向グラフの間にはリストと詳細の関係はありません。このスピーチではページベースのナビゲーションを選択し、ユーザーがリスト ページとグラフ ページの間を水平にスライドできるようにします。
struct ContentView: View {
var body: some View {
TabView {
NavigationStack {
ItemList()
}
NavigationStack {
ProductivityChart()
}
}.tabViewStyle(.page)
}
}
キーポイント:
TabView2 つの兄弟ページをホストします。- 各ページには独自の内容が表示されます
NavigationStack。 .tabViewStyle(.page)横向きの切り替えに適した watchOS ページ スタイルを使用します。
(17:00) Swift Chart は watchOS 9 で利用できます。ここでは、まず完了日別にタスクを集計し、次に使用します。BarMark毎日の完了数をプロットします。
/// 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
}
}
}
キーポイント:
DataElementグラフ内の集計データの一部です。 -id使用date、同じ日のデータは同じ識別子に対応します。 -Dictionary(grouping:by:)完了日ごとにグループ化します。 -compactMap飛び越えるcompletionDate == nil未完了のタスクの数。 -Double($1.count)その日の完了数量をチャートで使用される数値に変換します。 -sortedグラフを日付順に表示します。
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)
}
}
キーポイント:
Chart(data)集約された配列を Swift Chart に引き渡します。 -BarMark単一のデータ系列と離散値に適したヒストグラムを選択します。 -x使用日、y完成した数量を使用します。 -.foregroundStyle(Color.accentColor)アプリのアクセントカラーを使用して列をペイントします。 -.navigationBarTitleDisplayMode(.inline)ナビゲーション タイトルが占めるスペースを減らします。
Digital Crown でチャートをスクロールする
(18:50) ウォッチ画面は、一度にあまりにも多くの列を表示するのには適していません。スピーチ用digitalCrownRotationクラウンを監視し、現在のインデックス、クラウン オフセット、アイドル状態を SwiftUI 状態に保存します。
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
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)
}
}
キーポイント:
highlightedDateIndex現在ドッキングされているデータ ポイント インデックスを保存します。 -crownOffsetクラウンスクロール時の中間オフセット値を保存します。 -isCrownIdle回転と停止を区別します。 -.focusable()ビューが Digital Crown 入力を受信できるようにします。 -digitalCrownRotation(detent:from:through:by:sensitivity:)クラウンストップをデータインデックスにマップします。 -onChangeスクロール オフセットはクロージャに記録されます。 -onIdleクロージャはスクロールの停止を示します。
(21:07) リューズの現在位置を表示するためにグラフを追加しましたRuleMark。これは直線であり、しきい値または現在の選択位置をマークするのに適しています。
/// 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)
}
}
キーポイント:
crownOffsetDateクラウン オフセットを日付に変換します。 -data[0].date.distance(to:)チャートデータの期間を取得します。 -addingTimeInterval開始日から現在のスクロール位置を導出します。 -RuleMark(x:)現在の日付に垂直線を引きます。 -AxisMarks(format:)カスタム日付形式で X 軸ラベルを表示します。
(22:44) 最後のステップは、現在のインデックスに基づいて表示されるデータ範囲を移動することです。このようにして、ユーザーがリューズを回すと、それに応じてチャート ウィンドウがスクロールします。
@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))])
}
キーポイント:
chartDataRange現在表示されている 7 つのデータ ポイントを保存します。- ハイライトされたインデックスが左端に近い場合、ウィンドウは 1 つ左に移動します。
- ハイライトされたインデックスが右端に近い場合、ウィンドウは 1 つ右に移動します。
-
maxそしてmin範囲が配列の境界を越えないようにします。 -chartData現在の範囲内のデータのみを渡しますChart。
重要ポイント
-
時計専用のパンチリストを作成する: 毎日しなければならないことを 3 ~ 5 つ時計に入力します。実行する価値がある理由: セッションの説明 監視専用アプリは、機能が集中し、高速な対話が行われ、データが制限されているシナリオに適しています。開始方法: 使用する
ListItem、ObservableObject、ListそしてTextFieldLink追加と表示のプロセスを設定します。 -
手首を上げることで更新できるワークロード推定ツールを作成します: 各タスクは、名前、推定日数、完了ステータスのみを記録します。実行する価値がある理由:
Stepper小さな画面でも連続した数値を管理できるようになり、キーボードで数値を入力する必要がなくなりました。開始方法: でForm縛られているStepper(value:in:step:format:)そしてToggle(isOn:)。 -
Make a task sharing button to ask companions for help: When a task is stuck, send it directly to your friends from the details page.実行する価値がある理由:
ShareLink現在のアプリを離れることなく、watchOS 上のシステム共有フォームにアクセスします。開始方法: 詳細フォームに追加しますShareLink(item:subject:message:preview:)。 -
先週の完了傾向のみを表示する傾向グラフを作成します: 1 日あたりの完了数がヒストグラムで表示され、デフォルトでは 7 日のみが表示されます。実行する価値がある理由: 講演では、Watch 画面では一度に表示されるデータの量を制限する必要があり、ヒストグラムは単一のデータ系列に適していると指摘しました。開始方法: 完了日を次のように集計します。
ChartData.DataElement、次に使用しますChartそしてBarMark描く。 -
クラウンを使用して履歴データを参照できるチャートを作成します: ユーザーが Digital Crown を回すと、ハイライト ラインと表示されるデータ範囲が移動します。実行する価値がある理由:
digitalCrownRotationこれは時計の自然な入力方法であり、小さな画面で長距離を閲覧するのに適しています。開始方法: 使用するhighlightedDateIndex、crownOffset、RuleMarkそしてchartDataRangeスクロール状態を保存します。
関連セッション
- SwiftUI の新機能 — WWDC 2022 で SwiftUI の新機能を学びましょう
ShareLink、Swift チャート、ナビゲーションおよびレイアウト機能。 - ナビゲーション用の SwiftUI クックブック — スピーチで言及された内容について詳しく学ぶ
NavigationStackおよび SwiftUI ナビゲーション構造。 - 転送可能な会議 — 研究を続ける
ShareLinkその背後にある共有データ モデル。 - Hello Swift Charts — 基本的な構文から Swift チャートの学習を始めます。
- Swift Charts: Raise the bar — チャートの軸、マーカー、およびインタラクションの高度なカスタマイズについて学びます。
コメント
GitHub Issues · utterances