WWDC Quick Look 💓 By SwiftGGTeam
Build a productivity app for Apple Watch

Build a productivity app for Apple Watch

元の動画を見る

ハイライト

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従うIdentifiableListそしてForEach各ラインを安定して識別できます。 -Hashableタスク値が SwiftUI のデータ比較および収集操作に参加できるようにします。 -ItemListModel従うObservableObject@Publisheditemsドライブインターフェイスを更新します。 -@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")時計の上部に現在地を明確に表示します。

(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)計算されたプロパティを通じて完了ステータスを変更します。

14:41ShareLinkこれは、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 つ時計に入力します。実行する価値がある理由: セッションの説明 監視専用アプリは、機能が集中し、高速な対話が行われ、データが制限されているシナリオに適しています。開始方法: 使用するListItemObservableObjectListそして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これは時計の自然な入力方法であり、小さな画面で長距離を閲覧するのに適しています。開始方法: 使用するhighlightedDateIndexcrownOffsetRuleMarkそしてchartDataRangeスクロール状態を保存します。

関連セッション

  • SwiftUI の新機能 — WWDC 2022 で SwiftUI の新機能を学びましょうShareLink、Swift チャート、ナビゲーションおよびレイアウト機能。
  • ナビゲーション用の SwiftUI クックブック — スピーチで言及された内容について詳しく学ぶNavigationStackおよび SwiftUI ナビゲーション構造。
  • 転送可能な会議 — 研究を続けるShareLinkその背後にある共有データ モデル。
  • Hello Swift Charts — 基本的な構文から Swift チャートの学習を始めます。
  • Swift Charts: Raise the bar — チャートの軸、マーカー、およびインタラクションの高度なカスタマイズについて学びます。

コメント

GitHub Issues · utterances