Open12

園児に水分補給を促す保育士のためのカウントタイマー【iphone・Apple Watch アプリ】

りゆりゆ

Apple Watchアプリについて

Apple WatchとiPhoneを連携させるための重要なツール
 手首の上で便利な機能や情報を提供する小型アプリケーション

perplexityとGPTで調べました。
https://www.perplexity.ai/search/apple-watchahurinituitejiao-et-b9IS1DwtQ7SPK9rSJGyTEg

GPT より主な機能について

おすすめなアプリも教えてくれました。

アプリ開発をする方法

https://www.genspark.ai/spark?id=7b70516c-75cd-4214-a0e3-73e95aa03db8
https://www.genspark.ai/spark?id=0fd379de-84ca-49f6-9c34-1fb55a61c423

「WatchKit App」をターゲットとして追加する必要がある

Apple Watchアプリは、iPhoneアプリと連携して動作することが多く、
iPhoneにインストールされたアプリがBluetooth経由でApple Watchに転送される
これにより、Apple Watch上でiPhoneアプリの一部機能を利用することができる

新規作成のところでの参考
https://qiita.com/yoshi2045/items/a72f9cd1f48202c01188

りゆりゆ

開発環境を整える

まずはiphoneアプリを開発してみる

→Appleのスマートフォン「iPhone」上で動作するモバイルアプリ

  • macを準備
  • App StoreからXcodeをダウンロード
  • Apple IDの作成: Apple IDが必要(アプリの公開はまだしなくて良い

Xcodeとは・・・??

参考:https://hnavi.co.jp/knowledge/blog/xcode-development/

早速作ってみよう!!

参考:https://qiita.com/yoshi-eng/items/52f7b9aa1d8d7826bdba

とりあえずプロジェクト新規作成までできた!!

理解しづらかった。YOUTUBEで探すことにした。
参考:https://www.youtube.com/watch?v=EHdAqVVzAIE

・新規プロジェクト作成

・Hello World を こんにちは、世界! にしたら反映された!!

・ボタンをつけて、ボタンを押したら実行したことが表示された!

・varだったのでconstに直した!!!
→constに直したら怒られたので、とりあえずvarのまま勉強して
のちに直すことにする

【VStack】縦に文字列が並ぶ

【HStack】横に文字列が並ぶ

【ZStack】文字列が重なる

・色がついたぞ!!!

・形が変わったぞ!!

・色の形やどうやって構成していくのかを実践してみた。

ひとまず基礎はおしまいにし、企画を立てて
作成しながら二次情報を使いながらやっていきたいと思う。

りゆりゆ

Apple Watchアプリ開発やっていくぞ!!

iphoneとの連携の記事があまりない・・・・
とりあえずAppleWatchだけで実装してみる!!

AppleWatchだけのシュミレーター

参考:
https://zenn.dev/naoya_maeda/articles/180d7acce8330b

・varを使わないで欲しい

・どうしてもViewのところはvarw使う必要があるらしい・・・

りゆりゆ

iphoneとの連携アプリを作る

アイデア出し

AppleWatchの特徴

腕に装着しているため、素早くアクセスでき、情報を即座に確認できる
iPhoneを取り出す必要がなく、移動中や運動中でも簡単に利用できる

→これを踏まえ、保育現場で使えると考えた。

保育現場では、iphoneを見ている暇もなく
常に両手を開けていないと予想外の出来事に対応できない。

インタビュー
そして先日保育行事に遊びに行ってきて
現状の保育環境を少しきいてみた。
やはり今年は暑すぎて、廊下もかなりモワモワとしている。
外で遊ぶのは熱中症アラートが出ていたため
最近では部屋でエアコンをかけすごしているという。

そして夏といえばやはり水分補給。
勤めていた時に、隣のクラスの子があまり水分を飲まない子で
熱中症になってしまったことがあった。
当時は何人もいる中で15分という目安の時間で
子ども達に水分補給を促していたが
タイマーなどがないと色々なことに対応する中で
15ふんすぎてしまうこともあった。

全然飲まない子もいるし
そういうときになんとかなんとか
自然な流れで飲めるように声の掛け方を工夫していたが
やはり、子どもにも水分補給という意識をして欲しい。

1つのものが水分補給を促してくれる目安があり
保育者も子どもも同じ立場で一緒に水分補給をとる
生活習慣ができると良いと考えた。

今は保育者が子どもたちにうながしている。
「先生も飲もうーかんぱい!」とかするが
きっかけを作っているのは変わらず保育士だ。

なので同じ目線で水分補給が取れるものを作りたい。

水分補給リマインダー を作りたい!!

りゆりゆ

水分補給リマインダーをつくる!!

ただの数字のリマインダーだと
子どもたちに強制的・義務感がある。

かといって人型で水分がなくなっていくより
遊び感覚で飲めた方がすすんで飲んでくれる。

どんどん潤ったり萎んだりするかわいい水のキャラクターを作りたい。
これはゲームの方じゃなきゃダメなのかな?
(パックちゃんみたいなシュールなものは避けたい。)

ちなみに、タイマーは欲しいが何十人もいるので
ボタンを押していると誰の基準に合わせるか分からなくなるため
繰り返し機能にする
本当は1人一人のタイマーがあったら良い。

調べて・・・
https://www.perplexity.ai/search/apple-watchahurinituitejiao-et-b9IS1DwtQ7SPK9rSJGyTEg

コード挿入してシンプルデザインになった!!

・エラーで使えないっぽい・・・

・ぱっくちゃんみたいになっちゃった。
 形や手足をつけてみる。

・ 手足がついて、ぽくなってきた!!
  が時計は下に入れたいな・・・・

・時計を下に入れて、フォントも少し丸目にしてみた。
 キャラクターは黒い線で縁取り、中の水がなくなるようなところを
 子供達が見てわかるようにしたかった

・0秒になったらお知らせしたい
 音声をつけたい
→ぴー!とかではんく、水の音のような子どもたちの中の世界観を壊さないような
 音をフリー素材で探す。

フリー素材: https://dova-s.jp/
雫があまりなかった。バブルな音にして水感があるものにした。
追加できた。

→音が鳴らなかった

・振動するのはできるのか?
→振動できなかった

期限が迫っているためここで終了。
シュミレーターをiphoneとAppleWatchどっちにも表示させたい

え、なんかだめだ・・・・

連携ができないので一旦iphone用とAppleWatch用で別々で
作ってみることにする。

りゆりゆ
import SwiftUI
import UserNotifications

struct ContentView: View {
    @State private var timeRemaining = 900 // 15分 = 900秒
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        ZStack {
            Color.blue.opacity(0.2)
                .edgesIgnoringSafeArea(.all)

            VStack {
                Text("いっしょに、のもう")
                    .font(.largeTitle)
                    .padding()

                // キャラクターを表示する
                WaterCharacterView(timeRemaining: timeRemaining)
                    .frame(width: 200, height: 300)
                    .padding()

                // その他のUI要素
            }
            .onReceive(timer) { _ in
                if timeRemaining > 0 {
                    timeRemaining -= 1
                } else {
                    sendNotification()
                    timeRemaining = 900 // リセットして再スタート
                }
            }
        }
    }

    func timeString(time: Int) -> String {
        let minutes = Int(time) / 60 % 60
        let seconds = Int(time) % 60
        return String(format:"%02i:%02i", minutes, seconds)
    }

    func sendNotification() {
        let content = UNMutableNotificationContent()
        content.title = "水を飲む時間です!"
        content.body = "休憩して水を飲みましょう。"
        content.sound = .default

        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request)
    }
}

struct WaterCharacterView: View {
    let timeRemaining: Int
    let totalTime = 900

    var body: some View {
        VStack {
            ZStack {
                // 水滴の形
                Circle()
                    .stroke(Color.black, lineWidth: 2)
                    .background(
                        Circle()
                            .fill(LinearGradient(
                                gradient: Gradient(colors: [Color.blue.opacity(0.7), Color.blue]),
                                startPoint: .top,
                                endPoint: .bottom))
                            .scaleEffect(x: 1, y: CGFloat(timeRemaining) / CGFloat(totalTime), anchor: .bottom)
                    )
                    .clipShape(Circle())
                
                // 顔のパーツ
                VStack {
                    Spacer()
                    HStack {
                        Eye()
                        Eye()
                    }
                    .padding(.bottom, 20)
                    Mouth()
                    Spacer()
                        .frame(height: 50)
                }

                // 手と足
                Hand()
                    .rotationEffect(.degrees(-45))
                    .offset(x: -70, y: 60)
                Hand()
                    .rotationEffect(.degrees(45))
                    .offset(x: 70, y: 60)
                Foot()
                    .rotationEffect(.degrees(20))
                    .offset(x: -30, y: 120)
                Foot()
                    .rotationEffect(.degrees(-20))
                    .offset(x: 30, y: 120)
            }
            .clipped()
            .animation(.easeInOut(duration: 1), value: timeRemaining)

            // 時間の表示
            Text(timeString(time: timeRemaining))
                .font(.custom("Avenir Next", size: 24)) // 丸っぽいフォントを使用
                .foregroundColor(.black) // テキストカラーを黒に設定
                .bold()
                .padding(.top, 10) // 少し余白を追加
        }
    }

    func timeString(time: Int) -> String {
        let minutes = Int(time) / 60 % 60
        let seconds = Int(time) % 60
        return String(format:"%02i:%02i", minutes, seconds)
    }
}

struct Eye: View {
    var body: some View {
        Circle()
            .fill(Color.black)
            .frame(width: 20, height: 20)
            .padding(.horizontal, 10)
    }
}

struct Mouth: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 0, y: 0))
            path.addQuadCurve(to: CGPoint(x: 40, y: 0), control: CGPoint(x: 20, y: 20))
        }
        .stroke(Color.black, lineWidth: 2)
        .frame(width: 40, height: 20)
        .offset(y: 10)
    }
}

struct Hand: View {
    var body: some View {
        Capsule()
            .fill(Color.blue)
            .frame(width: 20, height: 60)
    }
}

struct Foot: View {
    var body: some View {
        Capsule()
            .fill(Color.blue)
            .frame(width: 20, height: 40)
    }
}

struct WaterReminderApp_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
りゆりゆ

コードの中の水を飲む時間です・・・
って画面に表示されてないけどなんでだ・・・

  func sendNotification() {
        let content = UNMutableNotificationContent()
        content.title = "水を飲む時間です!"
        content.body = "休憩して水を飲みましょう。"
        content.sound = .default

        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request)
    }
りゆりゆ

iphoneも無事できた!!

AppleWatchの画面に対して大きすぎたので修正する。

できたー!!
プロンプトは、キャラクターの手足がバラバラになりやすいので
キャラクターはグループ化にした上で とお願いした。
この聞き方が1番良かったl。動作がイメージとあっていた。

AppleWatch用のコード

import SwiftUI
import UserNotifications

struct ContentView: View {
    @State private var timeRemaining = 900 // 15分 = 900秒
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Color.blue.opacity(0.2)
                    .edgesIgnoringSafeArea(.all)

                VStack {
                    Text("いっしょに、のもう")
                        .font(.system(size: geometry.size.width * 0.06))
                        .padding(.top, geometry.size.height * 0.05)

                    Spacer()

                    // キャラクターを表示する
                    WaterCharacterView(timeRemaining: timeRemaining)
                        .frame(width: geometry.size.width * 0.7, height: geometry.size.height * 0.6)

                    Spacer()

                    // 時間の表示
                    Text(timeString(time: timeRemaining))
                        .font(.system(size: geometry.size.width * 0.08, weight: .bold))
                        .foregroundColor(.white) // テキストカラーを白に設定
                        .padding(.bottom, geometry.size.height * 0.05)
                }
            }
            .onReceive(timer) { _ in
                if timeRemaining > 0 {
                    timeRemaining -= 1
                } else {
                    sendNotification()
                    timeRemaining = 900 // リセットして再スタート
                }
            }
        }
    }

    func timeString(time: Int) -> String {
        let minutes = Int(time) / 60 % 60
        let seconds = Int(time) % 60
        return String(format:"%02i:%02i", minutes, seconds)
    }

    func sendNotification() {
        let content = UNMutableNotificationContent()
        content.title = "水を飲む時間です!"
        content.body = "休憩して水を飲みましょう。"
        content.sound = .default

        let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
        UNUserNotificationCenter.current().add(request)
    }
}

struct WaterCharacterView: View {
    let timeRemaining: Int
    let totalTime = 900

    var body: some View {
        GeometryReader { geometry in
            ZStack {
                WaterDropShape(progress: CGFloat(timeRemaining) / CGFloat(totalTime))
                
                CharacterFace()
                    .frame(width: geometry.size.width * 0.6, height: geometry.size.height * 0.3)
                    .position(x: geometry.size.width / 2, y: geometry.size.height * 0.4)

                CharacterLimbs()
                    .frame(width: geometry.size.width, height: geometry.size.height)
            }
            .animation(.easeInOut(duration: 1), value: timeRemaining)
        }
    }
}

struct WaterDropShape: View {
    let progress: CGFloat

    var body: some View {
        Circle()
            .stroke(Color.black, lineWidth: 2)
            .background(
                Circle()
                    .fill(LinearGradient(
                        gradient: Gradient(colors: [Color.blue.opacity(0.7), Color.blue]),
                        startPoint: .top,
                        endPoint: .bottom))
                    .scaleEffect(x: 1, y: progress, anchor: .bottom)
            )
            .clipShape(Circle())
    }
}

struct CharacterFace: View {
    var body: some View {
        VStack {
            HStack {
                Eye()
                Eye()
            }
            Mouth()
        }
    }
}

struct CharacterLimbs: View {
    var body: some View {
        GeometryReader { geometry in
            Group {
                Hand()
                    .rotationEffect(.degrees(-45))
                    .position(x: geometry.size.width * 0.2, y: geometry.size.height * 0.6)
                Hand()
                    .rotationEffect(.degrees(45))
                    .position(x: geometry.size.width * 0.8, y: geometry.size.height * 0.6)
                Foot()
                    .rotationEffect(.degrees(20))
                    .position(x: geometry.size.width * 0.35, y: geometry.size.height * 0.85)
                Foot()
                    .rotationEffect(.degrees(-20))
                    .position(x: geometry.size.width * 0.65, y: geometry.size.height * 0.85)
            }
        }
    }
}

struct Eye: View {
    var body: some View {
        Circle()
            .fill(Color.black)
            .frame(width: 8, height: 8)
            .padding(.horizontal, 4)
    }
}

struct Mouth: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 0, y: 0))
            path.addQuadCurve(to: CGPoint(x: 16, y: 0), control: CGPoint(x: 8, y: 8))
        }
        .stroke(Color.black, lineWidth: 1)
        .frame(width: 16, height: 8)
        .offset(y: 4)
    }
}

struct Hand: View {
    var body: some View {
        Capsule()
            .fill(Color.blue)
            .frame(width: 8, height: 24)
    }
}

struct Foot: View {
    var body: some View {
        Capsule()
            .fill(Color.blue)
            .frame(width: 8, height: 16)
    }
}

struct WaterReminderApp_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
りゆりゆ

今後、できたら保育現場に置くならば
先生の時計をつけてリマインダーすることも大切だけど
子どもたちに自主的な水分補給を促すなら
大きな画面(例えばipadとか)で子どもたちが
見に行けるようなものを設置すると良いかも・・・

りゆりゆ

ときメーターに使えるかも?!

・自分のときめき度を可視化する上で心拍のバイタルを取る
→AppleWatchが必要になってくるかも・・・
→iphone開発もあるかもしれない・・・

AppleWatch使うとしてもみんながみんな持っているわけではないから
使うか迷うけど幅としては持っておこう・・