LoginSignup
15
12

More than 3 years have passed since last update.

SwiftUI 使って Watch Complication を実装してみる!

Posted at

はじめに

Complication を SwiftUI で作れるようになったと聞いて,
WWDC のころは一人で歓喜していたのだが,結局触らずじまい😩
どうしたら SwiftUI 使って Complication 設定できるんだーと
少し困ったので調べてサンプル実装してみました。

Watch 周りの情報本当に少ない・・・

Complications とは

Apple Watch 持っている方はご存知の通り,
タイムリーな情報が Watch Face に表示され,Watch を見るだけで幸せになるものです。
純正では,Activity や 天気情報,バッテリ残量など色々ありますね。

すでに watchOS 7,Series 6 が出ていることもあり,かなり種類も増えてきました。

どんな種類があるかなどは過去に記事書いてるのでご覧ください。
といいつつ古くなってるし Human Interface Guideline(以下 HIG)や
公式の情報を確認した方が良いかもです。

Apple Watch Series 4 の新しい Complication Families の実装を試してみる
https://qiita.com/MilanistaDev/items/6a09bb787d9eee509c0d

Complications を SwiftUI 使う方法

色々(無駄に)調べた結果,
ComplicationController に SwiftUI を import するだけでした💡

CLKTextProviderCLKGaugeProvider
それぞれの Template に設定して表示するのですが,
いくつかの Template で SwiftUI の単一の View だけを使えるものがあります。

  • CLKComplicationTemplateGraphicCornerCircularView
  • CLKComplicationTemplateGraphicCircularView
  • CLKComplicationTemplateGraphicRectangularFullView
  • CLKComplicationTemplateGraphicExtraLargeCircularView

今回は CLKComplicationTemplateGraphicRectangularFullView を使ってみたいと思います。

CLKTextProviderCLKGaugeProvider,Template については,
Xcode から CLKComplicationTemplate.h や HIG の Complications の項目をご覧ください。

https://developer.apple.com/design/human-interface-guidelines/watchos/overview/complications/

今回やること

今回は,GraphicRectangular 部分を
SwiftUI で View を作って表示させてみます。

題材は,東京メトロ東西線の行き先表示器 風にしました。
データの更新は今回は扱いません。(運行はフィクション,妄想です)
朝の 8時台に九段下ゆきの運行が平日一本あるのは事実です(レア)。

スクリーンショット 2020-12-23 8.18.25.png

今回のコードは,Github に Push したので気になる方はご覧ください。

https://github.com/MilanistaDev/WatchComplicationsForSwiftUI

実装環境

  • Xcode 12.2
  • watchOS 7.0 and later

iOS アプリは不要のため,Watch App 単体にしています。
スクリーンショット 2020-12-23 22.13.15.png

実装

Complication に表示させる View の実装

Watch App のファーストビューを SwiftUI で実装し,
その画面を Complication にも表示させてみます。
コードについては VStackHStack を使っていくだけなので説明は省略します。

コード
ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            ForEach(0..<sampleData.count) { index in
                TimeTableView(operationSchedule: sampleData[index])
            }
        }
        .padding()
        .background(Color.noticeBg)
    }
}

struct TimeTableView: View {

    let operationSchedule: OperationSchedule!

    var body: some View {
        HStack {
            Text(operationSchedule.arrivalTime)
                .font(.subheadline)
                .bold()
            Text(operationSchedule.trainType.kind.name)
                .font(.footnote)
                .bold()
                .frame(width: 40.0)
                .foregroundColor(.white)
                .background(operationSchedule.trainType.kind.color)
            Image(operationSchedule.finalStaIcon)
                .resizable()
                .frame(width: 20.0, height: 20.0)
            Spacer()
            Text(operationSchedule.finalStaName)
                .font(.subheadline)
                .bold()
            Spacer()
        }
    }
}

struct OperationSchedule {
    var arrivalTime: String
    var trainType: TrainType
    var finalStaIcon: String
    var finalStaName: String
}

enum TrainType {
    case rapid
    case rapidCommuting    // 通勤快速
    case local             // 各駅停車

    struct Kind {
        var name: String
        var color: Color
    }

    var kind: Kind {
        switch self {
        case .rapid:
            return Kind(name: "快速", color: .red)
        case .rapidCommuting:
            return Kind(name: "通快", color: .green)
        case .local:
            return Kind(name: "各停", color: .blue)
        }
    }
}

let sampleData: [OperationSchedule] = [
    OperationSchedule(arrivalTime: "8:20", trainType: .local,
                      finalStaIcon: "T07", finalStaName: "九段下"),
    OperationSchedule(arrivalTime: "8:24", trainType: .rapidCommuting,
                      finalStaIcon: "T01", finalStaName: "中野"),
    OperationSchedule(arrivalTime: "8:28", trainType: .rapid,
                      finalStaIcon: "JB01", finalStaName: "三鷹")
]

extension Color {
    static let noticeBg = Color(UIColor(named: "noticeBg")!)
}

スクリーンショット 2020-12-23 22.42.20.png

Complication 表示に向けての実装

Xcode 12 で新規に Watch App のプロジェクトを作ると,すでに
ComplicationController.swift があるのでこのクラスで設定を行います。
既存のアプリで設定をする場合は,CLKComplicationDataSource
準拠させたクラスを作ることになります。

今回は,CLKComplicationTemplateGraphicRectangularFullView を使ってみたいので Complication の Family の GraphicRectangular を使えるようにします。
初期値は全ての Family が設定されている(enum の allCases)ので書き換えます。

ComplicationController.swift
func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
    let descriptors = [
        CLKComplicationDescriptor(identifier: "complication", displayName: "WatchComplicationsForSwiftUI", supportedFamilies: [CLKComplicationFamily.graphicRectangular])
        // Multiple complication support can be added here with more descriptors
    ]
    // Call the handler with the currently supported complication descriptors
    handler(descriptors)
}

次に,Complication 表示のために getCurrentTimelineEntry 関数部分に
CLKComplicationTimelineEntry の設定をします。
date は表示する時間,complicationTemplate は表示させるテンプレートです。

ComplicationController.swift
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
    // Call the handler with the current timeline entry
    if let template = getComplicationTemplate(for: complication, using: Date()) {
        let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
        handler(entry)
    } else {
        handler(nil)
    }
}

テンプレートの作成は別関数で行うことが多いので関数を分けています。
SwiftUI を import することで CLKComplicationTemplateGraphicRectangularFullView を使えるようになります。
これだけでなく,SwiftUI が利用可能な Template は全て使えるようになります。

ComplicationController.swift
import ClockKit
import SwiftUI // 追加

// 省略

func getComplicationTemplate(for complication: CLKComplication, using date: Date) -> CLKComplicationTemplate? {
    switch complication.family {
    case .graphicRectangular:
        return CLKComplicationTemplateGraphicRectangularFullView(ContentView())
    default:
        return nil
    }
}

これで表示させる準備が整いました。

Apple Watch 本体で Complication を設定

プロジェクトを Run してアプリを一旦起動して,
Watch Face 画面に戻ります。
画面をロングタップして GraphicRectangularFullView が利用可能な
Infograph ModularModular Compact の Face にします。

Infograph Modular Modular Compact
スクリーンショット 2020-12-23 21.43.05.png スクリーンショット 2020-12-23 21.46.58.png

Complication を設定する画面から選択します。

左右にスワイプ 選択してセット
スクリーンショット 2020-12-23 21.49.45.png スクリーンショット 2020-12-23 8.18.12.png

ComplicationControllergetLocalizableSampleTemplate 関数で
Template を設定することで選択時にサンプルが見えるようになります。
実際にアプリをリリースした際はぜひ実装しておきたい部分です。

ComplicationController.swift
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
    // This method will be called once per supported complication, and the results will be cached
    let template = getComplicationTemplate(for: complication, using: Date())
    handler(template)
}

実装すると,選択時にサンプルビューが表示されています。

スクリーンショット 2020-12-23 23.32.13.png

設定後に Watch Face に戻ってくると・・・
表示されました🎉

スクリーンショット 2020-12-23 8.18.25.png

Complication のプレビュー

SwiftUI で View を実装する際にすごく役立つのが,
Canvas にプレビューされる機能です。
Complication もどう表示されるか確認したいのですが・・・
できます👍

例えば,ComplicationController.swift のファイルの最後に
PreviewProvider を追加し下記のように実装します。

ComplicationController.swift

struct ComplicationController_Previews: PreviewProvider {
    static var previews: some View {
        CLKComplicationTemplateGraphicRectangularFullView(ContentView()).previewContext()
    }
}

previewContext() に引数を取らない場合は,
デフォルトでフルカラーの Watch Face になります。
スクリーンショット 2020-12-23 23.01.28.png

全部の Watch Face tinting のプレビューを確認する場合は下記のようにします。

ComplicationController.swift
struct ComplicationController_Previews: PreviewProvider {
    static var previews: some View {
        ForEach(CLKComplicationTemplate.PreviewFaceColor.allColors) { color in
            CLKComplicationTemplateGraphicRectangularFullView(ContentView()).previewContext(faceColor: color)
        }
    }
}

下記のように,設定可能なカラーのプレビューが一気に確認できます。
preview

SafeArea を無視する

Complication の領域は,
40mm の Apple Watch の場合は 162 × 69pt,
44mm の Apple Watch の場合は 184 × 78pt になりますが,
SafeArea を無視することもできます。

お馴染みの edgesIgnoringSafeArea(_:) を使います。

今回の View だと問題なかったですが,見切れたりする場合に設定します。

注意点

SwiftUI で View を実装できるからといって,
tapGestureButton などを使ったアクションは使えないです。
Complication のタップはあくまでも Watch App の起動になるためです。

また,アニメーションなども使えないです。

Future Work

GaugeProgress View なども重要な要素ですし,扱ってみたいですね。

また今回はあまり触れなかったのですが,
Watch Face Tinting(Watch Face の色) を考慮した,
Complication の設計についても一度やってみたいです。

セッション動画の最後にベストプラクティスの項目があるのですが,
今回紹介できずでした。しっかり確認したいと思います。

おわりに

今回は,SwiftUI 使って Watch Complication を実装してみました。

パズルのようにレイアウトを組んでいた画面も SwiftUI のおかげで改善され,
watchOS 7 からは Complication も SwiftUI で自由に実装できる
Template が登場しました。これからも楽しみなガジェットには違いないですね!

今年はコロナ禍もあり,公私共に積極的に活動できなかったです。
もっと目標を明確にして活動していこうと思います。

ご覧いただきありがとうございました。
良いお年をお迎えください🎍

参考

Building Complications with SwiftUI
https://developer.apple.com/documentation/clockkit/building_complications_with_swiftui

Build complications in SwiftUI (WWDC20セッション動画)
https://developer.apple.com/videos/play/wwdc2020/10048/

15
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
12