はじめに
せっかくMac mini M2を買ったのでアプリを作ってみようかと思いました
WatchOS6からWatch単体アプリを作れるようになったということで、Apple Watch単体アプリを作ってみました
アプリ内容
Apple Watchにはメモ帳アプリがなかったのでメモ帳アプリを作りました
最初は複数のメモを管理できるような(いわゆる普通の?iPhoneのメモアプリ的な)ものを考えたのですが、Apple Watchの小さい画面でたくさんメモ取らないだろうと思い、ブキーボード的なちょっと書いてすぐ消すようなメモ用紙的なアプリを作ることにしました
開発環境
- Mac mini M2
- MacOS Sonoma 14.4.1
- Xcode 15.3 (15E204a)
- WatchOS 10
アプリのコード
今回作成したアプリはGithubに置きました
全体のコードはこちらを参照ください
Xcodeプロジェクトの作成
Xcodeを起動し新規プロジェクト作成からWatchOS -> Appを選びます
次の画面でWatch-only Appを選んでプロジェクトを作成します
Widget Extensionの追加
Xcodeのメニュー -> File -> New -> TargetからWatchOSのWidget Extensionを追加します
Product Nameはプロジェクトの中でExtensionとわかりやすい名前をつけば良いかと思います
App Groupsの作成
プロジェクトを選択してTargetからWatchOSアプリを選択します
Signing & Capabilitiesのタブを選択し、+ CapabilityをクリックしApp Groupsを追加します
App Groupsのセクションから、「+」をクリックしてconteinerを追加します
この時決めたコンテナーIDを使ってアプリとウィジェット間でデータをやり取りすることになります
これと同様の操作を先ほど追加したWidget Extensionでも行います
この時App Groupsを追加した際のコンテナーIDはWatchOSアプリ側で作成したconteinerと同じコンテナーIDにします
メモデータの格納
メモデータの格納はUserDefaultsを使っています
今回は1つだけのメモを管理するので、「memo」というキーでUserDefaultsを作ります
またWidgetKitでウィジェットも作るのでApp GroupsのコンテナーIDを指定するようにしています
import Foundation
class MemoData: ObservableObject {
@Published var memo: String {
willSet {
print("willset")
}
didSet {
UserDefaults(suiteName: "group.com.watch.memo")?.set(memo, forKey: "memo")
print("didset")
}
}
// 初期化
init() {
memo = UserDefaults(suiteName: "group.com.watch.memo")?.string(forKey: "memo") ?? " "
}
}
メインView
WatchOSではTextEditorが使えないのでメイン画面にTextを配置し、NavigationStackの遷移先にTextFiledをおいてメモを編集する形式にしています
メイン画面にはNavigationStackとListの中にTextとメモ削除用のButtonを配置します
メモを削除する処理もこのViewに書いています
NavigationStack -> NavigationLinkのdestinationに遷移したいView(ここではEditVew)を指定し、渡したい値(value -> Textに表示させている文字列)を引数として指定します
Textの.onAppear()処理でUserDefaultsの値を表示させます
NavigationStack {
List {
NavigationLink(destination: EditView(value: "\(value)")) {
Text(value)
.onAppear() {
guard let defaultMemo = UserDefaults(suiteName: "group.com.watch.memo")?.object(forKey: "memo")
else{return}
// memoがNullの場合、値に空文字を挿入する
value = defaultMemo as! String
if value == "" {
value = " "
}
}
}
Button(action: {erase()}) {
Label("erase", systemImage: "trash")
}
// 背景色を透明にする
.listRowBackground(Color.clear)
}
}
private func erase() {
let eraseMemo = " "
self.memoData.memo = eraseMemo
self.value = eraseMemo
// Widgetを更新
WidgetCenter.shared.reloadAllTimelines()
}
編集View
メモの編集はメインViewから遷移させたViewで行うようにしました
メモの編集 -> UserDefaultsのアップデートを行なっています
func setMemoData(){
guard let memoData = UserDefaults(suiteName: "group.com.watch.memo")?.object(forKey: "memo")
else{return}
self.value = memoData as! String
}
var body: some View {
TextField("Input Text", text: $value)
.onAppear {
if $value.wrappedValue == " " {
value.self = ""
}
}
.onSubmit {
update(memo: value)
}
}
private func update(memo: String) {
self.memoData.memo = memo
setMemoData()
// Widgetを更新
WidgetCenter.shared.reloadAllTimelines()
}
WidgetExtensionの定義
WidgetExtensionは追加されたテンプレートを割と素直に使っています
ただApple Watchに設定するウォッチフェイスによって表示されるWidgetが異なるので分岐を作って切り替えられるようにしました
struct WatchMemoWidgetEntryView : View {
@Environment(\.widgetFamily) var widgetFamily
@State var entry: Provider.Entry
var body: some View {
switch widgetFamily {
case .accessoryCorner:
CornerComplication(memo: entry.memo)
case .accessoryCircular:
CircularComplication(memo: entry.memo)
case .accessoryInline:
InlineComplication(memo: entry.memo)
case .accessoryRectangular:
RectangularComplication(memo: entry.memo)
@unknown default:
Text("No Complication")
}
}
}
struct InlineComplication : View {
@State var memo: String
var body: some View {
HStack {
Image(systemName:"note.text")
Text(": \(memo)")
.lineLimit(1)
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
struct CircularComplication : View {
@Environment(\.showsWidgetLabel) var showsWidgetLabel
@State var memo: String
// 表示される場所によってアイコンイメージ的なものが一緒に表示される時と
// メモの内容のみが表示される時を分けています
var body: some View {
if showsWidgetLabel {
Image(systemName:"note.text")
.widgetLabel {
Text(memo)
}
.containerBackground(.fill.tertiary, for: .widget)
} else {
Text(memo)
.lineLimit(1)
.containerBackground(.fill.tertiary, for: .widget)
}
}
}
struct CornerComplication : View {
@State var memo: String
var body: some View {
Image(systemName:"note.text")
.widgetLabel {
Text(memo)
.lineLimit(1)
.containerBackground(.fill.tertiary, for: .widget)
}
}
}
struct RectangularComplication : View {
@State var memo: String
var body: some View {
VStack {
HStack {
Image(systemName:"note.text")
.font(.system(size: 10.0))
Spacer()
}
Text(memo)
.lineLimit(2)
.widgetAccentable()
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
おわりに
現状Apple Watchで使えるSwiftUIではTextEditorが使えないので改行を入力することができないようです
Apple Watchはそもそも画面が小さいので複雑な画面構成は向いていないのでいかにシンプルに作るかが重要ですね
Watch OnlyアプリがiPadのSwiftPlaygoundで作れるようになるといいなと思いました