تطوير تطبيقات الساعات الذكية
ما هو مطور تطبيقات الساعات الذكية؟
مطور تطبيقات الأجهزة القابلة للارتداء
مطور تطبيقات الأجهزة القابلة للارتداء يركز على إنشاء تطبيقات الساعات الذكية باستخدام Android Wear و WatchOS مع الاستفادة من أدوات مثل Google Fit APIs و HealthKit لتحليل بيانات الصحة واللياقة.
Android Wear
تطوير تطبيقات ساعات Android الذكية
WatchOS
تطوير تطبيقات Apple Watch
HealthKit
تحليل بيانات الصحة من Apple
Fitness APIs
تحليل بيانات اللياقة من Google
اللغات والأدوات المستخدمة
Kotlin/Java
لتطوير تطبيقات Android Wear
Swift
لتطوير تطبيقات WatchOS
HealthKit
إطار عمل Apple لبيانات الصحة
Fitness APIs
أدوات Google لبيانات اللياقة
Android Wear
نظام تشغيل ساعات Android
WatchOS
نظام تشغيل Apple Watch
مهارات مطور الساعات الذكية
Kotlin/Java
للتطوير على منصة Android Wear
Swift
للتطوير على منصة WatchOS
Android Studio
بيئة التطوير لـ Android Wear
Xcode
بيئة التطوير لـ WatchOS
HealthKit
تحليل بيانات الصحة من Apple
Fitness APIs
تحليل بيانات اللياقة من Google
خارطة التعلم خطوة بخطوة
الخطوة 1: تعلم Android Wear
Android Wear هو نظام التشغيل الخاص بأجهزة الساعات الذكية التي تعمل بنظام Android
الأهمية:
الأساس لفهم كيفية بناء التطبيقات التي تعمل على ساعات Android الذكية
الأدوات:
Android Studio مع Kotlin/Java
مثال Android Wear أساسي:
// MainActivity.kt - النشاط الرئيسي للتطبيق
package com.example.wearapp
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.wear.ambient.AmbientModeSupport
import androidx.wear.widget.WearableLinearLayoutManager
import androidx.wear.widget.WearableRecyclerView
class MainActivity : AppCompatActivity(), AmbientModeSupport.AmbientCallbackProvider {
// قائمة البيانات
private val healthData = listOf(
HealthItem("الخطوات", "8,542", "اليوم"),
HealthItem("السعرات الحرارية", "420", "سعرة"),
HealthItem("معدل ضربات القلب", "72", "نبضة/دقيقة"),
HealthItem("المسافة", "6.2", "كم"),
HealthItem("النوم", "7.5", "ساعة")
)
// حالة التطبيق
private var isAmbient = false
private lateinit var ambientController: AmbientModeSupport.AmbientController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// تهيئة وضع Ambient
ambientController = AmbientModeSupport.attach(this)
// إعداد RecyclerView للساعات الذكية
setupRecyclerView()
// تحديث البيانات في الوقت الحقيقي
updateRealTimeData()
}
private fun setupRecyclerView() {
val recyclerView: WearableRecyclerView = findViewById(R.id.recycler_view)
// استخدام WearableLinearLayoutManager للواجهات الدائرية
recyclerView.layoutManager = WearableLinearLayoutManager(this)
// إضافة CurvingLayoutCallback للانحناء على الساعات المستديرة
recyclerView.setEdgeItemsCenteringEnabled(true)
// تعيين المحاذاة
recyclerView.isCircularScrollingGestureEnabled = true
recyclerView.bezelWidth = 15
recyclerView.scrollDegreesPerScreen = 90
// تعيين المحول
val adapter = HealthAdapter(healthData)
recyclerView.adapter = adapter
// إضافة نقرة على العناصر
adapter.setOnItemClickListener { position ->
val item = healthData[position]
showDetailsDialog(item)
}
}
private fun updateRealTimeData() {
// محاكاة تحديث البيانات في الوقت الحقيقي
val heartRateTextView: TextView = findViewById(R.id.heart_rate_text)
val stepsTextView: TextView = findViewById(R.id.steps_text)
// تحديث معدل ضربات القلب كل 5 ثواني
Thread {
while (true) {
Thread.sleep(5000)
runOnUiThread {
// قيم عشوائية لمحاكاة البيانات الحقيقية
val randomHeartRate = (60..100).random()
val randomSteps = (0..100).random()
heartRateTextView.text = "$randomHeartRate"
stepsTextView.text = "${healthData[0].value.toInt() + randomSteps}"
}
}
}.start()
}
private fun showDetailsDialog(item: HealthItem) {
// عرض تفاصيل العنصر
val dialog = HealthDetailDialog(this, item)
dialog.show()
}
override fun getAmbientCallback(): AmbientModeSupport.AmbientCallback {
return object : AmbientModeSupport.AmbientCallback() {
override fun onEnterAmbient(ambientDetails: Bundle?) {
// الدخول إلى وضع Ambient (توفير الطاقة)
isAmbient = true
updateAmbientUI()
}
override fun onExitAmbient() {
// الخروج من وضع Ambient
isAmbient = false
updateAmbientUI()
}
override fun onUpdateAmbient() {
// تحديث الواجهة في وضع Ambient
updateAmbientData()
}
}
}
private fun updateAmbientUI() {
// تحديث الواجهة بناءً على وضع Ambient
val recyclerView: WearableRecyclerView = findViewById(R.id.recycler_view)
recyclerView.alpha = if (isAmbient) 0.7f else 1.0f
}
private fun updateAmbientData() {
// تحديث البيانات في وضع Ambient
// هنا يمكنك تحديث البيانات فقط عند الحاجة لتوفير الطاقة
}
// فئة لعناصر البيانات الصحية
data class HealthItem(
val title: String,
val value: String,
val unit: String
)
}
// HealthAdapter.kt - محول البيانات
class HealthAdapter(private val items: List) :
RecyclerView.Adapter() {
private var itemClickListener: ((Int) -> Unit)? = null
fun setOnItemClickListener(listener: (Int) -> Unit) {
itemClickListener = listener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_health, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
holder.itemView.setOnClickListener {
itemClickListener?.invoke(position)
}
}
override fun getItemCount(): Int = items.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val titleTextView: TextView = itemView.findViewById(R.id.title_text)
private val valueTextView: TextView = itemView.findViewById(R.id.value_text)
private val unitTextView: TextView = itemView.findViewById(R.id.unit_text)
private val iconImageView: ImageView = itemView.findViewById(R.id.icon_image)
fun bind(item: MainActivity.HealthItem) {
titleTextView.text = item.title
valueTextView.text = item.value
unitTextView.text = item.unit
// تعيين الأيقونة المناسبة
val iconRes = when (item.title) {
"الخطوات" -> R.drawable.ic_steps
"السعرات الحرارية" -> R.drawable.ic_calories
"معدل ضربات القلب" -> R.drawable.ic_heart
"المسافة" -> R.drawable.ic_distance
"النوم" -> R.drawable.ic_sleep
else -> R.drawable.ic_default
}
iconImageView.setImageResource(iconRes)
}
}
}
// HealthDetailDialog.kt - حوار التفاصيل
class HealthDetailDialog(
context: Context,
private val item: MainActivity.HealthItem
) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_health_detail)
window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setupViews()
loadHistoricalData()
}
private fun setupViews() {
val titleTextView: TextView = findViewById(R.id.detail_title)
val valueTextView: TextView = findViewById(R.id.detail_value)
val unitTextView: TextView = findViewById(R.id.detail_unit)
val closeButton: Button = findViewById(R.id.close_button)
titleTextView.text = item.title
valueTextView.text = item.value
unitTextView.text = item.unit
closeButton.setOnClickListener {
dismiss()
}
// إضافة رسم بياني للبيانات التاريخية
val chartView: LineChart = findViewById(R.id.chart_view)
setupChart(chartView)
}
private fun setupChart(chart: LineChart) {
// تهيئة الرسم البياني
chart.description.isEnabled = false
chart.setTouchEnabled(true)
chart.isDragEnabled = true
chart.setScaleEnabled(true)
chart.setDrawGridBackground(false)
// البيانات التاريخية (7 أيام)
val entries = ArrayList()
val days = listOf("أحد", "اثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت")
days.forEachIndexed { index, day ->
val value = when (item.title) {
"الخطوات" -> (5000..15000).random().toFloat()
"السعرات الحرارية" -> (300..800).random().toFloat()
"معدل ضربات القلب" -> (60..100).random().toFloat()
"المسافة" -> (3..15).random().toFloat()
"النوم" -> (5..9).random().toFloat()
else -> 0f
}
entries.add(Entry(index.toFloat(), value))
}
val dataSet = LineDataSet(entries, "التاريخ")
dataSet.color = Color.BLUE
dataSet.valueTextColor = Color.BLACK
dataSet.lineWidth = 2f
val lineData = LineData(dataSet)
chart.data = lineData
chart.invalidate()
}
private fun loadHistoricalData() {
// محاكاة تحميل البيانات التاريخية
val progressBar: ProgressBar = findViewById(R.id.progress_bar)
val dataTextView: TextView = findViewById(R.id.historical_data_text)
progressBar.visibility = View.VISIBLE
Thread {
Thread.sleep(1000) // محاكاة تأخير الشبكة
runOnUiThread {
progressBar.visibility = View.GONE
dataTextView.text = "تم تحميل البيانات التاريخية لآخر 30 يوم"
}
}.start()
}
}
// ملف AndroidManifest.xml للإذونات
/*
*/
الخطوة 2: تعلم WatchOS
WatchOS هو نظام التشغيل الخاص بأجهزة Apple Watch
الأهمية:
الأساس لفهم كيفية بناء التطبيقات التي تعمل على ساعات Apple الذكية
الأدوات:
Xcode مع Swift
مثال WatchOS أساسي:
// ContentView.swift - واجهة التطبيق الرئيسية
import SwiftUI
import HealthKit
import WatchKit
struct ContentView: View {
// حالة التطبيق
@State private var stepCount = 0
@State private var heartRate = 72
@State private var caloriesBurned = 0
@State private var distance = 0.0
@State private var isWorkoutActive = false
@State private var workoutTime = 0
// HealthKit
private let healthStore = HKHealthStore()
@State private var healthDataAvailable = false
// بيانات الصحة
private let healthMetrics = [
HealthMetric(title: "الخطوات", value: "0", unit: "خطوة", icon: "figure.walk"),
HealthMetric(title: "معدل ضربات القلب", value: "72", unit: "نبضة/دقيقة", icon: "heart.fill"),
HealthMetric(title: "السعرات الحرارية", value: "0", unit: "سعرة", icon: "flame.fill"),
HealthMetric(title: "المسافة", value: "0", unit: "كم", icon: "map.fill"),
HealthMetric(title: "النوم", value: "0", unit: "ساعة", icon: "bed.double.fill")
]
var body: some View {
ScrollView {
VStack(spacing: 12) {
// رأس التطبيق
HeaderView()
// بيانات الصحة الرئيسية
HealthSummaryView(
stepCount: $stepCount,
heartRate: $heartRate,
caloriesBurned: $caloriesBurned,
distance: $distance
)
// قائمة المقاييس الصحية
HealthMetricsListView(metrics: healthMetrics)
// بدء/إيقاف التمرين
WorkoutControlView(
isActive: $isWorkoutActive,
workoutTime: $workoutTime
)
// التنبيهات
NotificationsView()
// الإعدادات
SettingsView(healthDataAvailable: $healthDataAvailable)
}
.padding()
}
.onAppear {
requestHealthKitAuthorization()
startHealthDataUpdates()
}
}
private func requestHealthKitAuthorization() {
// أنواع البيانات الصحية المطلوبة
let typesToRead: Set = [
HKObjectType.quantityType(forIdentifier: .stepCount)!,
HKObjectType.quantityType(forIdentifier: .heartRate)!,
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
HKObjectType.quantityType(forIdentifier: .distanceWalkingRunning)!,
HKObjectType.categoryType(forIdentifier: .sleepAnalysis)!
]
healthStore.requestAuthorization(toShare: nil, read: typesToRead) { success, error in
DispatchQueue.main.async {
healthDataAvailable = success
if success {
print("تم منح إذن HealthKit")
fetchHealthData()
} else if let error = error {
print("خطأ في إذن HealthKit: \(error.localizedDescription)")
}
}
}
}
private func fetchHealthData() {
fetchStepCount()
fetchHeartRate()
fetchActiveCalories()
fetchDistance()
}
private func fetchStepCount() {
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: now,
options: .strictStartDate
)
let query = HKStatisticsQuery(
quantityType: stepType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { _, result, error in
guard let result = result, let sum = result.sumQuantity() else {
print("خطأ في جلب الخطوات: \(error?.localizedDescription ?? "غير معروف")")
return
}
let steps = sum.doubleValue(for: HKUnit.count())
DispatchQueue.main.async {
stepCount = Int(steps)
}
}
healthStore.execute(query)
}
private func fetchHeartRate() {
let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate)!
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let query = HKSampleQuery(
sampleType: heartRateType,
predicate: nil,
limit: 1,
sortDescriptors: [sortDescriptor]
) { _, samples, error in
guard let samples = samples as? [HKQuantitySample],
let sample = samples.first else {
print("خطأ في جلب معدل ضربات القلب: \(error?.localizedDescription ?? "غير معروف")")
return
}
let heartRateUnit = HKUnit(from: "count/min")
let heartRate = sample.quantity.doubleValue(for: heartRateUnit)
DispatchQueue.main.async {
self.heartRate = Int(heartRate)
}
}
healthStore.execute(query)
}
private func fetchActiveCalories() {
let calorieType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: now,
options: .strictStartDate
)
let query = HKStatisticsQuery(
quantityType: calorieType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { _, result, error in
guard let result = result, let sum = result.sumQuantity() else {
print("خطأ في جلب السعرات الحرارية: \(error?.localizedDescription ?? "غير معروف")")
return
}
let calories = sum.doubleValue(for: HKUnit.kilocalorie())
DispatchQueue.main.async {
caloriesBurned = Int(calories)
}
}
healthStore.execute(query)
}
private func fetchDistance() {
let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: now,
options: .strictStartDate
)
let query = HKStatisticsQuery(
quantityType: distanceType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { _, result, error in
guard let result = result, let sum = result.sumQuantity() else {
print("خطأ في جلب المسافة: \(error?.localizedDescription ?? "غير معروف")")
return
}
let distanceInMeters = sum.doubleValue(for: HKUnit.meter())
let distanceInKm = distanceInMeters / 1000
DispatchQueue.main.async {
distance = distanceInKm
}
}
healthStore.execute(query)
}
private func startHealthDataUpdates() {
// تحديث البيانات كل 10 ثواني
Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { _ in
if healthDataAvailable {
fetchHealthData()
}
}
}
}
// مكون رأس التطبيق
struct HeaderView: View {
var body: some View {
VStack(spacing: 8) {
Image(systemName: "applewatch")
.font(.system(size: 40))
.foregroundColor(.blue)
Text("ساعتي الصحية")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.primary)
Text("تابع صحتك ولياقتك")
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(15)
.shadow(color: .gray.opacity(0.2), radius: 10, x: 0, y: 5)
}
}
// مكون ملخص الصحة
struct HealthSummaryView: View {
@Binding var stepCount: Int
@Binding var heartRate: Int
@Binding var caloriesBurned: Int
@Binding var distance: Double
var body: some View {
VStack(spacing: 15) {
Text("ملخص اليوم")
.font(.headline)
.fontWeight(.semibold)
HStack(spacing: 15) {
VStack {
Text("\(stepCount)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.blue)
Text("خطوة")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
VStack {
Text("\(heartRate)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.red)
Text("نبضة")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.red.opacity(0.1))
.cornerRadius(10)
}
HStack(spacing: 15) {
VStack {
Text("\(caloriesBurned)")
.font(.title)
.fontWeight(.bold)
.foregroundColor(.orange)
Text("سعرة")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange.opacity(0.1))
.cornerRadius(10)
VStack {
Text(String(format: "%.1f", distance))
.font(.title)
.fontWeight(.bold)
.foregroundColor(.green)
Text("كم")
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(10)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(15)
.shadow(color: .gray.opacity(0.2), radius: 10, x: 0, y: 5)
}
}
// نموذج بيانات الصحة
struct HealthMetric: Identifiable {
let id = UUID()
let title: String
let value: String
let unit: String
let icon: String
}
// مكون قائمة مقاييس الصحة
struct HealthMetricsListView: View {
let metrics: [HealthMetric]
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("مقاييس الصحة")
.font(.headline)
.fontWeight(.semibold)
.padding(.horizontal)
ForEach(metrics) { metric in
HealthMetricRow(metric: metric)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(15)
.shadow(color: .gray.opacity(0.2), radius: 10, x: 0, y: 5)
}
}
// مكون صف مقياس الصحة
struct HealthMetricRow: View {
let metric: HealthMetric
var body: some View {
HStack(spacing: 12) {
Image(systemName: metric.icon)
.font(.title2)
.foregroundColor(.blue)
.frame(width: 30)
VStack(alignment: .leading, spacing: 2) {
Text(metric.title)
.font(.subheadline)
.foregroundColor(.primary)
Text("\(metric.value) \(metric.unit)")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.gray)
}
.padding(.vertical, 8)
.padding(.horizontal)
.background(Color(.systemGray6))
.cornerRadius(8)
}
}
// مكون تحكم في التمرين
struct WorkoutControlView: View {
@Binding var isActive: Bool
@Binding var workoutTime: Int
var body: some View {
VStack(spacing: 15) {
Text("التمرين")
.font(.headline)
.fontWeight(.semibold)
Button(action: {
isActive.toggle()
if isActive {
startWorkoutTimer()
} else {
workoutTime = 0
}
}) {
Label(
isActive ? "إيقاف التمرين" : "بدء التمرين",
systemImage: isActive ? "stop.circle.fill" : "play.circle.fill"
)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
if isActive {
Text("الوقت: \(formatTime(workoutTime))")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.green)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(15)
.shadow(color: .gray.opacity(0.2), radius: 10, x: 0, y: 5)
}
private func startWorkoutTimer() {
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
if isActive {
workoutTime += 1
} else {
timer.invalidate()
}
}
}
private func formatTime(_ seconds: Int) -> String {
let minutes = seconds / 60
let remainingSeconds = seconds % 60
return String(format: "%02d:%02d", minutes, remainingSeconds)
}
}
// مكون التنبيهات
struct NotificationsView: View {
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("التنبيهات")
.font(.headline)
.fontWeight(.semibold)
VStack(alignment: .leading, spacing: 8) {
NotificationItem(
title: "تذكير بالماء",
message: "اشرب كوب من الماء",
time: "قبل 30 دقيقة"
)
NotificationItem(
title: "تذكير بالحركة",
message: "تحرك قليلاً",
time: "قبل ساعتين"
)
NotificationItem(
title: "هدف اليوم",
message: "حققت 80% من هدف الخطوات",
time: "اليوم"
)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(15)
.shadow(color: .gray.opacity(0.2), radius: 10, x: 0, y: 5)
}
}
// مكون عنصر التنبيه
struct NotificationItem: View {
let title: String
let message: String
let time: String
var body: some View {
HStack(alignment: .top, spacing: 10) {
Image(systemName: "bell.fill")
.foregroundColor(.orange)
.frame(width: 20)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline)
.fontWeight(.medium)
Text(message)
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Text(time)
.font(.caption2)
.foregroundColor(.gray)
}
.padding(.vertical, 4)
}
}
// مكون الإعدادات
struct SettingsView: View {
@Binding var healthDataAvailable: Bool
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("الإعدادات")
.font(.headline)
.fontWeight(.semibold)
HStack {
Text("وصول HealthKit")
Spacer()
Image(systemName: healthDataAvailable ? "checkmark.circle.fill" : "xmark.circle.fill")
.foregroundColor(healthDataAvailable ? .green : .red)
}
.padding(.vertical, 4)
Button("تحديث البيانات") {
// تحديث جميع البيانات
}
.buttonStyle(.bordered)
.frame(maxWidth: .infinity)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(15)
.shadow(color: .gray.opacity(0.2), radius: 10, x: 0, y: 5)
}
}
// تطبيق WatchOS الرئيسي
@main
struct WatchApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
الخطوة 3: تعلم HealthKit
HealthKit هو إطار عمل من Apple لجمع ومعالجة بيانات الصحة مثل الخطوات، معدل ضربات القلب، وغيرها
الأهمية:
الأساس لفهم كيفية الوصول إلى بيانات الصحة وتحليلها
الأدوات:
Xcode مع Swift
مثال HealthKit متقدم:
// HealthKitManager.swift - مدير HealthKit المتكامل
import Foundation
import HealthKit
import SwiftUI
class HealthKitManager: ObservableObject {
static let shared = HealthKitManager()
private let healthStore = HKHealthStore()
@Published var stepCount: Int = 0
@Published var heartRate: Int = 72
@Published var activeCalories: Int = 0
@Published var distance: Double = 0.0
@Published var sleepHours: Double = 0.0
@Published var oxygenSaturation: Double = 0.0
@Published var bloodPressure: (systolic: Int, diastolic: Int)? = nil
@Published var isAuthorized = false
@Published var isLoading = false
@Published var lastUpdate = Date()
// أنواع البيانات الصحية المدعومة
private var supportedTypes: Set {
var types = Set()
// البيانات الكمية
if let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) {
types.insert(stepType)
}
if let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) {
types.insert(heartRateType)
}
if let calorieType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) {
types.insert(calorieType)
}
if let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning) {
types.insert(distanceType)
}
if let oxygenType = HKQuantityType.quantityType(forIdentifier: .oxygenSaturation) {
types.insert(oxygenType)
}
if let bloodPressureSystolic = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic),
let bloodPressureDiastolic = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic) {
types.insert(bloodPressureSystolic)
types.insert(bloodPressureDiastolic)
}
// البيانات الفئوية
if let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) {
types.insert(sleepType)
}
return types
}
private init() {
checkAuthorization()
}
// طلب الإذن
func requestAuthorization() {
isLoading = true
healthStore.requestAuthorization(toShare: nil, read: supportedTypes) { [weak self] success, error in
DispatchQueue.main.async {
self?.isLoading = false
self?.isAuthorized = success
if success {
print("تم منح إذن HealthKit بنجاح")
self?.fetchAllHealthData()
self?.setupBackgroundDelivery()
} else if let error = error {
print("خطأ في إذن HealthKit: \(error.localizedDescription)")
}
}
}
}
private func checkAuthorization() {
for type in supportedTypes {
let status = healthStore.authorizationStatus(for: type)
if status == .sharingAuthorized {
isAuthorized = true
fetchAllHealthData()
setupBackgroundDelivery()
break
}
}
}
// جلب جميع البيانات الصحية
func fetchAllHealthData() {
guard isAuthorized else { return }
isLoading = true
let group = DispatchGroup()
group.enter()
fetchStepCount { group.leave() }
group.enter()
fetchHeartRate { group.leave() }
group.enter()
fetchActiveCalories { group.leave() }
group.enter()
fetchDistance { group.leave() }
group.enter()
fetchSleepData { group.leave() }
group.enter()
fetchOxygenSaturation { group.leave() }
group.enter()
fetchBloodPressure { group.leave() }
group.notify(queue: .main) { [weak self] in
self?.isLoading = false
self?.lastUpdate = Date()
print("تم تحديث جميع البيانات الصحية")
}
}
// جلب عدد الخطوات
private func fetchStepCount(completion: @escaping () -> Void) {
guard let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount) else {
completion()
return
}
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: now,
options: .strictStartDate
)
let query = HKStatisticsQuery(
quantityType: stepType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { [weak self] _, result, error in
DispatchQueue.main.async {
if let result = result, let sum = result.sumQuantity() {
let steps = sum.doubleValue(for: HKUnit.count())
self?.stepCount = Int(steps)
} else if let error = error {
print("خطأ في جلب الخطوات: \(error.localizedDescription)")
}
completion()
}
}
healthStore.execute(query)
}
// جلب معدل ضربات القلب
private func fetchHeartRate(completion: @escaping () -> Void) {
guard let heartRateType = HKQuantityType.quantityType(forIdentifier: .heartRate) else {
completion()
return
}
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let predicate = HKQuery.predicateForSamples(
withStart: Date().addingTimeInterval(-3600), // آخر ساعة
end: Date(),
options: .strictEndDate
)
let query = HKSampleQuery(
sampleType: heartRateType,
predicate: predicate,
limit: 10,
sortDescriptors: [sortDescriptor]
) { [weak self] _, samples, error in
DispatchQueue.main.async {
if let samples = samples as? [HKQuantitySample] {
// حساب المتوسط
let heartRateUnit = HKUnit(from: "count/min")
let total = samples.reduce(0.0) { $0 + $1.quantity.doubleValue(for: heartRateUnit) }
let average = total / Double(samples.count)
self?.heartRate = Int(average.rounded())
} else if let error = error {
print("خطأ في جلب معدل ضربات القلب: \(error.localizedDescription)")
}
completion()
}
}
healthStore.execute(query)
}
// جلب السعرات الحرارية النشطة
private func fetchActiveCalories(completion: @escaping () -> Void) {
guard let calorieType = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) else {
completion()
return
}
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: now,
options: .strictStartDate
)
let query = HKStatisticsQuery(
quantityType: calorieType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { [weak self] _, result, error in
DispatchQueue.main.async {
if let result = result, let sum = result.sumQuantity() {
let calories = sum.doubleValue(for: HKUnit.kilocalorie())
self?.activeCalories = Int(calories)
} else if let error = error {
print("خطأ في جلب السعرات الحرارية: \(error.localizedDescription)")
}
completion()
}
}
healthStore.execute(query)
}
// جلب المسافة المقطوعة
private func fetchDistance(completion: @escaping () -> Void) {
guard let distanceType = HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning) else {
completion()
return
}
let now = Date()
let startOfDay = Calendar.current.startOfDay(for: now)
let predicate = HKQuery.predicateForSamples(
withStart: startOfDay,
end: now,
options: .strictStartDate
)
let query = HKStatisticsQuery(
quantityType: distanceType,
quantitySamplePredicate: predicate,
options: .cumulativeSum
) { [weak self] _, result, error in
DispatchQueue.main.async {
if let result = result, let sum = result.sumQuantity() {
let distanceInMeters = sum.doubleValue(for: HKUnit.meter())
self?.distance = distanceInMeters / 1000 // تحويل إلى كيلومتر
} else if let error = error {
print("خطأ في جلب المسافة: \(error.localizedDescription)")
}
completion()
}
}
healthStore.execute(query)
}
// جلب بيانات النوم
private func fetchSleepData(completion: @escaping () -> Void) {
guard let sleepType = HKCategoryType.categoryType(forIdentifier: .sleepAnalysis) else {
completion()
return
}
let now = Date()
let startOfYesterday = Calendar.current.startOfDay(for: now.addingTimeInterval(-86400))
let predicate = HKQuery.predicateForSamples(
withStart: startOfYesterday,
end: now,
options: .strictStartDate
)
let query = HKSampleQuery(
sampleType: sleepType,
predicate: predicate,
limit: HKObjectQueryNoLimit,
sortDescriptors: nil
) { [weak self] _, samples, error in
DispatchQueue.main.async {
if let samples = samples as? [HKCategorySample] {
var totalSleep: TimeInterval = 0
for sample in samples {
if sample.value == HKCategoryValueSleepAnalysis.asleep.rawValue {
totalSleep += sample.endDate.timeIntervalSince(sample.startDate)
}
}
self?.sleepHours = totalSleep / 3600 // تحويل إلى ساعات
} else if let error = error {
print("خطأ في جلب بيانات النوم: \(error.localizedDescription)")
}
completion()
}
}
healthStore.execute(query)
}
// جلب تشبع الأكسجين
private func fetchOxygenSaturation(completion: @escaping () -> Void) {
guard let oxygenType = HKQuantityType.quantityType(forIdentifier: .oxygenSaturation) else {
completion()
return
}
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let predicate = HKQuery.predicateForSamples(
withStart: Date().addingTimeInterval(-86400), // آخر 24 ساعة
end: Date(),
options: .strictEndDate
)
let query = HKSampleQuery(
sampleType: oxygenType,
predicate: predicate,
limit: 1,
sortDescriptors: [sortDescriptor]
) { [weak self] _, samples, error in
DispatchQueue.main.async {
if let samples = samples as? [HKQuantitySample],
let sample = samples.first {
let oxygenValue = sample.quantity.doubleValue(for: HKUnit.percent())
self?.oxygenSaturation = oxygenValue * 100 // تحويل إلى نسبة مئوية
} else if let error = error {
print("خطأ في جلب تشبع الأكسجين: \(error.localizedDescription)")
}
completion()
}
}
healthStore.execute(query)
}
// جلب ضغط الدم
private func fetchBloodPressure(completion: @escaping () -> Void) {
guard let systolicType = HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic),
let diastolicType = HKQuantityType.quantityType(forIdentifier: .bloodPressureDiastolic) else {
completion()
return
}
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let predicate = HKQuery.predicateForSamples(
withStart: Date().addingTimeInterval(-86400), // آخر 24 ساعة
end: Date(),
options: .strictEndDate
)
let group = DispatchGroup()
var systolicValue: Double?
var diastolicValue: Double?
group.enter()
let systolicQuery = HKSampleQuery(
sampleType: systolicType,
predicate: predicate,
limit: 1,
sortDescriptors: [sortDescriptor]
) { _, samples, error in
if let samples = samples as? [HKQuantitySample],
let sample = samples.first {
systolicValue = sample.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
}
group.leave()
}
group.enter()
let diastolicQuery = HKSampleQuery(
sampleType: diastolicType,
predicate: predicate,
limit: 1,
sortDescriptors: [sortDescriptor]
) { _, samples, error in
if let samples = samples as? [HKQuantitySample],
let sample = samples.first {
diastolicValue = sample.quantity.doubleValue(for: HKUnit.millimeterOfMercury())
}
group.leave()
}
healthStore.execute(systolicQuery)
healthStore.execute(diastolicQuery)
group.notify(queue: .main) { [weak self] in
if let systolic = systolicValue, let diastolic = diastolicValue {
self?.bloodPressure = (Int(systolic), Int(diastolic))
}
completion()
}
}
// إعداد تحديث الخلفية
private func setupBackgroundDelivery() {
for type in supportedTypes {
let query = HKObserverQuery(sampleType: type, predicate: nil) { [weak self] _, completionHandler, error in
if error == nil {
self?.fetchAllHealthData()
}
completionHandler()
}
healthStore.execute(query)
healthStore.enableBackgroundDelivery(for: type, frequency: .hourly) { success, error in
if success {
print("تم تمكين تحديث الخلفية لـ \(type.identifier)")
}
}
}
}
// حفظ بيانات مخصصة
func saveWorkoutData(calories: Double, distance: Double, duration: TimeInterval, workoutType: HKWorkoutActivityType) {
guard isAuthorized else { return }
let workout = HKWorkout(
activityType: workoutType,
start: Date().addingTimeInterval(-duration),
end: Date(),
duration: duration,
totalEnergyBurned: HKQuantity(unit: HKUnit.kilocalorie(), doubleValue: calories),
totalDistance: HKQuantity(unit: HKUnit.meter(), doubleValue: distance * 1000),
metadata: [HKMetadataKeyIndoorWorkout: false]
)
healthStore.save(workout) { success, error in
if success {
print("تم حفظ بيانات التمرين بنجاح")
} else if let error = error {
print("خطأ في حفظ التمرين: \(error.localizedDescription)")
}
}
}
// الحصول على الاتجاهات والتوقعات
func getHealthTrends(completion: @escaping ([HealthTrend]) -> Void) {
guard isAuthorized else {
completion([])
return
}
var trends: [HealthTrend] = []
let group = DispatchGroup()
// تحليل اتجاه الخطوات
group.enter()
analyzeStepTrend { trend in
if let trend = trend { trends.append(trend) }
group.leave()
}
// تحليل اتجاه معدل ضربات القلب
group.enter()
analyzeHeartRateTrend { trend in
if let trend = trend { trends.append(trend) }
group.leave()
}
group.notify(queue: .main) {
completion(trends)
}
}
private func analyzeStepTrend(completion: @escaping (HealthTrend?) -> Void) {
// تنفيذ تحليل الاتجاهات
completion(HealthTrend(
type: .steps,
direction: .increasing,
percentage: 15.5,
message: "زيادة في متوسط الخطوات اليومية"
))
}
private func analyzeHeartRateTrend(completion: @escaping (HealthTrend?) -> Void) {
completion(HealthTrend(
type: .heartRate,
direction: .stable,
percentage: 2.3,
message: "معدل ضربات القلب مستقر"
))
}
}
// نموذج اتجاه الصحة
struct HealthTrend {
enum TrendType {
case steps, heartRate, calories, distance, sleep
}
enum TrendDirection {
case increasing, decreasing, stable
}
let type: TrendType
let direction: TrendDirection
let percentage: Double
let message: String
}
// HealthKitView.swift - واجهة SwiftUI مع HealthKit
import SwiftUI
struct HealthKitView: View {
@StateObject private var healthManager = HealthKitManager.shared
@State private var showingTrends = false
@State private var healthTrends: [HealthTrend] = []
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 20) {
// رأس التطبيق
HealthHeaderView(
isAuthorized: healthManager.isAuthorized,
lastUpdate: healthManager.lastUpdate
)
// حالة التحميل
if healthManager.isLoading {
ProgressView("جاري تحديث البيانات...")
.padding()
}
// بيانات الصحة
if healthManager.isAuthorized {
HealthDataView(
stepCount: healthManager.stepCount,
heartRate: healthManager.heartRate,
activeCalories: healthManager.activeCalories,
distance: healthManager.distance,
sleepHours: healthManager.sleepHours,
oxygenSaturation: healthManager.oxygenSaturation,
bloodPressure: healthManager.bloodPressure
)
// أزرار التحكم
HealthControlsView(
onRefresh: { healthManager.fetchAllHealthData() },
onShowTrends: {
healthManager.getHealthTrends { trends in
healthTrends = trends
showingTrends = true
}
}
)
} else {
// طلب الإذن
AuthorizationView {
healthManager.requestAuthorization()
}
}
}
.padding()
}
.navigationTitle("HealthKit")
.sheet(isPresented: $showingTrends) {
HealthTrendsView(trends: healthTrends)
}
}
}
}
// باقي مكونات SwiftUI مشابهة للمثال السابق...
الخطوة 4: تعلم Fitness APIs
Fitness APIs هي أدوات Google لجمع ومعالجة بيانات اللياقة البدنية مثل الخطوات، النشاطات، وغيرها
الأهمية:
الأساس لفهم كيفية الوصول إلى بيانات اللياقة البدنية وتحليلها
الأدوات:
Android Studio مع Kotlin/Java
مثال Fitness APIs:
// FitnessManager.kt - مدير Fitness APIs المتكامل
package com.example.wearfitness
import android.app.Activity
import android.content.Context
import android.util.Log
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.fitness.Fitness
import com.google.android.gms.fitness.FitnessOptions
import com.google.android.gms.fitness.data.DataType
import com.google.android.gms.fitness.data.DataSet
import com.google.android.gms.fitness.data.Field
import com.google.android.gms.fitness.request.DataReadRequest
import com.google.android.gms.fitness.request.DataUpdateRequest
import com.google.android.gms.fitness.request.SessionInsertRequest
import com.google.android.gms.fitness.result.DataReadResponse
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import java.util.Calendar
import java.util.Date
import java.util.concurrent.TimeUnit
class FitnessManager(private val context: Context) {
companion object {
private const val TAG = "FitnessManager"
// أنواع البيانات المدعومة
private val SUPPORTED_DATA_TYPES = setOf(
DataType.TYPE_STEP_COUNT_DELTA,
DataType.TYPE_HEART_RATE_BPM,
DataType.TYPE_CALORIES_EXPENDED,
DataType.TYPE_DISTANCE_DELTA,
DataType.TYPE_ACTIVITY_SEGMENT,
DataType.TYPE_SLEEP_SEGMENT
)
}
// حالة المدير
private lateinit var googleSignInClient: GoogleSignInClient
private var fitnessOptions: FitnessOptions? = null
private var currentAccount: GoogleSignInAccount? = null
// بيانات اللياقة
data class FitnessData(
val steps: Long = 0,
val heartRate: Double = 0.0,
val calories: Double = 0.0,
val distance: Double = 0.0,
val activities: List = emptyList(),
val sleepData: SleepData? = null,
val lastUpdated: Date = Date()
)
data class ActivityData(
val type: String,
val duration: Long, // بالمللي ثانية
val startTime: Date,
val endTime: Date,
val calories: Double = 0.0,
val distance: Double = 0.0
)
data class SleepData(
val startTime: Date,
val endTime: Date,
val duration: Long,
val stages: Map // مراحل النوم
)
// الاتجاهات والتوقعات
data class FitnessTrend(
val metric: String,
val currentValue: Double,
val previousValue: Double,
val changePercent: Double,
val trend: TrendDirection,
val suggestion: String
)
enum class TrendDirection {
INCREASING, DECREASING, STABLE
}
init() {
initializeFitnessOptions()
initializeGoogleSignIn()
}
private fun initializeFitnessOptions() {
val builder = FitnessOptions.builder()
// إضافة أنواع البيانات المطلوبة
SUPPORTED_DATA_TYPES.forEach { dataType ->
builder.addDataType(dataType, FitnessOptions.ACCESS_READ)
builder.addDataType(dataType, FitnessOptions.ACCESS_WRITE)
}
fitnessOptions = builder.build()
}
private fun initializeGoogleSignIn() {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(context, gso)
}
// تسجيل الدخول إلى Google Fitness
fun signIn(activity: Activity, requestCode: Int) {
val options = fitnessOptions ?: return
if (!GoogleSignIn.hasPermissions(GoogleSignIn.getLastSignedInAccount(context), options)) {
GoogleSignIn.requestPermissions(
activity,
requestCode,
GoogleSignIn.getLastSignedInAccount(context),
options
)
} else {
// المستخدم مسجل الدخول بالفعل
currentAccount = GoogleSignIn.getLastSignedInAccount(context)
Log.d(TAG, "المستخدم مسجل الدخول بالفعل")
}
}
// معالجة نتيجة تسجيل الدخول
fun handleSignInResult(data: Intent?): Boolean {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
return try {
val account = task.getResult(ApiException::class.java)
currentAccount = account
Log.d(TAG, "تم تسجيل الدخول بنجاح: ${account?.email}")
true
} catch (e: ApiException) {
Log.w(TAG, "فشل تسجيل الدخول: ${e.statusCode}")
false
}
}
// تسجيل الخروج
fun signOut() {
googleSignInClient.signOut().addOnCompleteListener {
currentAccount = null
Log.d(TAG, "تم تسجيل الخروج")
}
}
// التحقق من حالة المصادقة
fun isSignedIn(): Boolean {
return currentAccount != null && GoogleSignIn.hasPermissions(
currentAccount,
fitnessOptions ?: return false
)
}
// جلب بيانات اللياقة الشاملة
suspend fun fetchFitnessData(): FitnessData {
if (!isSignedIn()) {
throw IllegalStateException("المستخدم غير مسجل الدخول")
}
val account = currentAccount ?: throw IllegalStateException("لا يوجد حساب")
return try {
// جلب البيانات المتوازية
val stepsTask = fetchStepCount(account)
val heartRateTask = fetchHeartRate(account)
val caloriesTask = fetchCalories(account)
val distanceTask = fetchDistance(account)
val activitiesTask = fetchActivities(account)
val sleepTask = fetchSleepData(account)
// انتظار جميع المهام
Tasks.await(Tasks.whenAll(
stepsTask, heartRateTask, caloriesTask,
distanceTask, activitiesTask, sleepTask
))
// تجميع النتائج
FitnessData(
steps = stepsTask.result,
heartRate = heartRateTask.result,
calories = caloriesTask.result,
distance = distanceTask.result,
activities = activitiesTask.result,
sleepData = sleepTask.result
)
} catch (e: Exception) {
Log.e(TAG, "خطأ في جلب بيانات اللياقة: ${e.message}")
throw e
}
}
// جلب عدد الخطوات
private fun fetchStepCount(account: GoogleSignInAccount): Task {
return Fitness.getHistoryClient(context, account)
.readDailyTotal(DataType.TYPE_STEP_COUNT_DELTA)
.continueWith { task ->
if (task.isSuccessful) {
val dataSet = task.result
if (dataSet != null && dataSet.dataPoints.isNotEmpty()) {
val steps = dataSet.dataPoints.first()
.getValue(Field.FIELD_STEPS).asInt().toLong()
steps
} else {
0L
}
} else {
Log.e(TAG, "خطأ في جلب الخطوات: ${task.exception}")
0L
}
}
}
// جلب معدل ضربات القلب
private fun fetchHeartRate(account: GoogleSignInAccount): Task {
val endTime = Calendar.getInstance().timeInMillis
val startTime = endTime - TimeUnit.DAYS.toMillis(1)
val request = DataReadRequest.Builder()
.read(DataType.TYPE_HEART_RATE_BPM)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.bucketByTime(1, TimeUnit.HOURS)
.build()
return Fitness.getHistoryClient(context, account)
.readData(request)
.continueWith { task ->
if (task.isSuccessful) {
val response = task.result
val dataPoints = response?.getDataSet(DataType.TYPE_HEART_RATE_BPM)?.dataPoints
if (!dataPoints.isNullOrEmpty()) {
// حساب المتوسط
val sum = dataPoints.sumOf { it.getValue(Field.FIELD_BPM).asFloat() }
(sum / dataPoints.size).toDouble()
} else {
0.0
}
} else {
Log.e(TAG, "خطأ في جلب معدل ضربات القلب: ${task.exception}")
0.0
}
}
}
// جلب السعرات الحرارية
private fun fetchCalories(account: GoogleSignInAccount): Task {
return Fitness.getHistoryClient(context, account)
.readDailyTotal(DataType.TYPE_CALORIES_EXPENDED)
.continueWith { task ->
if (task.isSuccessful) {
val dataSet = task.result
if (dataSet != null && dataSet.dataPoints.isNotEmpty()) {
dataSet.dataPoints.first()
.getValue(Field.FIELD_CALORIES).asFloat().toDouble()
} else {
0.0
}
} else {
Log.e(TAG, "خطأ في جلب السعرات الحرارية: ${task.exception}")
0.0
}
}
}
// جلب المسافة
private fun fetchDistance(account: GoogleSignInAccount): Task {
return Fitness.getHistoryClient(context, account)
.readDailyTotal(DataType.TYPE_DISTANCE_DELTA)
.continueWith { task ->
if (task.isSuccessful) {
val dataSet = task.result
if (dataSet != null && dataSet.dataPoints.isNotEmpty()) {
dataSet.dataPoints.first()
.getValue(Field.FIELD_DISTANCE).asFloat().toDouble()
} else {
0.0
}
} else {
Log.e(TAG, "خطأ في جلب المسافة: ${task.exception}")
0.0
}
}
}
// جلب الأنشطة
private fun fetchActivities(account: GoogleSignInAccount): Task> {
val endTime = Calendar.getInstance().timeInMillis
val startTime = endTime - TimeUnit.DAYS.toMillis(1)
val request = DataReadRequest.Builder()
.read(DataType.TYPE_ACTIVITY_SEGMENT)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.build()
return Fitness.getHistoryClient(context, account)
.readData(request)
.continueWith { task ->
if (task.isSuccessful) {
val response = task.result
val dataSet = response?.getDataSet(DataType.TYPE_ACTIVITY_SEGMENT)
val activities = mutableListOf()
dataSet?.dataPoints?.forEach { dataPoint ->
val activityType = dataPoint.getValue(Field.FIELD_ACTIVITY).asActivity()
val start = Date(dataPoint.getStartTime(TimeUnit.MILLISECONDS))
val end = Date(dataPoint.getEndTime(TimeUnit.MILLISECONDS))
val duration = end.time - start.time
activities.add(
ActivityData(
type = activityType,
duration = duration,
startTime = start,
endTime = end
)
)
}
activities
} else {
Log.e(TAG, "خطأ في جلب الأنشطة: ${task.exception}")
emptyList()
}
}
}
// جلب بيانات النوم
private fun fetchSleepData(account: GoogleSignInAccount): Task {
val endTime = Calendar.getInstance().timeInMillis
val startTime = endTime - TimeUnit.DAYS.toMillis(1)
val request = DataReadRequest.Builder()
.read(DataType.TYPE_SLEEP_SEGMENT)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.build()
return Fitness.getHistoryClient(context, account)
.readData(request)
.continueWith { task ->
if (task.isSuccessful) {
val response = task.result
val dataSet = response?.getDataSet(DataType.TYPE_SLEEP_SEGMENT)
if (!dataSet?.dataPoints.isNullOrEmpty()) {
val dataPoint = dataSet!!.dataPoints.first()
val start = Date(dataPoint.getStartTime(TimeUnit.MILLISECONDS))
val end = Date(dataPoint.getEndTime(TimeUnit.MILLISECONDS))
val duration = end.time - start.time
// تحليل مراحل النوم
val stages = mutableMapOf()
dataSet.dataPoints.forEach { point ->
val stage = point.getValue(Field.FIELD_SLEEP_SEGMENT_TYPE).asInt()
val stageDuration = point.getEndTime(TimeUnit.MILLISECONDS) -
point.getStartTime(TimeUnit.MILLISECONDS)
val stageName = when (stage) {
1 -> "خفيف"
2 -> "عميق"
3 -> "REM"
else -> "مجهول"
}
stages[stageName] = stages.getOrDefault(stageName, 0) + stageDuration
}
SleepData(start, end, duration, stages)
} else {
null
}
} else {
Log.e(TAG, "خطأ في جلب بيانات النوم: ${task.exception}")
null
}
}
}
// حفظ بيانات تمرين
fun saveWorkoutSession(
activityType: String,
startTime: Date,
endTime: Date,
calories: Double,
distance: Double
) {
if (!isSignedIn()) return
val account = currentAccount ?: return
// إنشاء جلسة تمرين
val session = com.google.android.gms.fitness.data.Session.Builder()
.setName("تمرين $activityType")
.setIdentifier("${startTime.time}_${activityType}")
.setDescription("تمرين تم تسجيله من التطبيق")
.setStartTime(startTime.time, TimeUnit.MILLISECONDS)
.setEndTime(endTime.time, TimeUnit.MILLISECONDS)
.setActivity(activityType)
.build()
// إضافة بيانات التمرين
val calorieDataSet = DataSet.builder(DataType.TYPE_CALORIES_EXPENDED)
.add(com.google.android.gms.fitness.data.DataPoint.builder(DataType.TYPE_CALORIES_EXPENDED)
.setTimeInterval(startTime.time, endTime.time, TimeUnit.MILLISECONDS)
.setField(Field.FIELD_CALORIES, calories.toFloat())
.build())
.build()
val distanceDataSet = DataSet.builder(DataType.TYPE_DISTANCE_DELTA)
.add(com.google.android.gms.fitness.data.DataPoint.builder(DataType.TYPE_DISTANCE_DELTA)
.setTimeInterval(startTime.time, endTime.time, TimeUnit.MILLISECONDS)
.setField(Field.FIELD_DISTANCE, distance.toFloat())
.build())
.build()
// إنشاء طلب الإدراج
val insertRequest = SessionInsertRequest.Builder()
.setSession(session)
.addDataSet(calorieDataSet)
.addDataSet(distanceDataSet)
.build()
// إرسال الطلب
Fitness.getSessionsClient(context, account)
.insertSession(insertRequest)
.addOnSuccessListener {
Log.d(TAG, "تم حفظ جلسة التمرين بنجاح")
}
.addOnFailureListener { e ->
Log.e(TAG, "فشل حفظ جلسة التمرين: ${e.message}")
}
}
// تحليل الاتجاهات
suspend fun analyzeTrends(): List {
if (!isSignedIn()) return emptyList()
val account = currentAccount ?: return emptyList()
return try {
val trends = mutableListOf()
// تحليل اتجاه الخطوات (آخر 7 أيام)
val stepTrend = analyzeStepTrend(account)
stepTrend?.let { trends.add(it) }
// تحليل اتجاه السعرات الحرارية
val calorieTrend = analyzeCalorieTrend(account)
calorieTrend?.let { trends.add(it) }
// تحليل اتجاه المسافة
val distanceTrend = analyzeDistanceTrend(account)
distanceTrend?.let { trends.add(it) }
trends
} catch (e: Exception) {
Log.e(TAG, "خطأ في تحليل الاتجاهات: ${e.message}")
emptyList()
}
}
private suspend fun analyzeStepTrend(account: GoogleSignInAccount): FitnessTrend? {
val endTime = Calendar.getInstance().timeInMillis
val startTime = endTime - TimeUnit.DAYS.toMillis(7)
val request = DataReadRequest.Builder()
.aggregate(DataType.TYPE_STEP_COUNT_DELTA, DataType.AGGREGATE_STEP_COUNT_DELTA)
.bucketByTime(1, TimeUnit.DAYS)
.setTimeRange(startTime, endTime, TimeUnit.MILLISECONDS)
.build()
return try {
val response = Tasks.await(
Fitness.getHistoryClient(context, account).readData(request)
)
val buckets = response.buckets
if (buckets.size >= 2) {
val current = buckets.last().getDataSet(DataType.AGGREGATE_STEP_COUNT_DELTA)
val previous = buckets[buckets.size - 2].getDataSet(DataType.AGGREGATE_STEP_COUNT_DELTA)
val currentSteps = current?.dataPoints?.firstOrNull()
?.getValue(Field.FIELD_STEPS)?.asInt()?.toDouble() ?: 0.0
val previousSteps = previous?.dataPoints?.firstOrNull()
?.getValue(Field.FIELD_STEPS)?.asInt()?.toDouble() ?: 0.0
val changePercent = if (previousSteps > 0) {
((currentSteps - previousSteps) / previousSteps) * 100
} else 0.0
val trend = when {
changePercent > 5 -> TrendDirection.INCREASING
changePercent < -5 -> TrendDirection.DECREASING
else -> TrendDirection.STABLE
}
val suggestion = when (trend) {
TrendDirection.INCREASING -> "استمر في هذا الأداء الممتاز!"
TrendDirection.DECREASING -> "حاول زيادة نشاطك اليومي"
TrendDirection.STABLE -> "حافظ على مستواك الحالي"
}
FitnessTrend(
metric = "الخطوات",
currentValue = currentSteps,
previousValue = previousSteps,
changePercent = changePercent,
trend = trend,
suggestion = suggestion
)
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "خطأ في تحليل اتجاه الخطوات: ${e.message}")
null
}
}
// تحليل اتجاه السعرات الحرارية (مشابه لـ analyzeStepTrend)
private suspend fun analyzeCalorieTrend(account: GoogleSignInAccount): FitnessTrend? {
// تنفيذ مشابه لـ analyzeStepTrend
return null
}
// تحليل اتجاه المسافة (مشابه لـ analyzeStepTrend)
private suspend fun analyzeDistanceTrend(account: GoogleSignInAccount): FitnessTrend? {
// تنفيذ مشابه لـ analyzeStepTrend
return null
}
// إنشاء أهداف يومية
fun setDailyGoal(metric: String, targetValue: Double) {
if (!isSignedIn()) return
// حفظ الهدف محلياً أو على السحابة
val prefs = context.getSharedPreferences("fitness_goals", Context.MODE_PRIVATE)
prefs.edit().putFloat("goal_$metric", targetValue.toFloat()).apply()
Log.d(TAG, "تم تعيين هدف $metric إلى $targetValue")
}
// التحقق من تحقيق الأهداف
suspend fun checkGoals(fitnessData: FitnessData): Map {
val prefs = context.getSharedPreferences("fitness_goals", Context.MODE_PRIVATE)
val goals = mutableMapOf()
// التحقق من هدف الخطوات
val stepGoal = prefs.getFloat("goal_steps", 10000f).toDouble()
goals["steps"] = fitnessData.steps >= stepGoal
// التحقق من هدف السعرات الحرارية
val calorieGoal = prefs.getFloat("goal_calories", 500f).toDouble()
goals["calories"] = fitnessData.calories >= calorieGoal
// التحقق من هدف المسافة
val distanceGoal = prefs.getFloat("goal_distance", 5f).toDouble()
goals["distance"] = fitnessData.distance >= distanceGoal
return goals
}
}
// MainActivity.kt - النشاط الرئيسي
class MainActivity : AppCompatActivity() {
private lateinit var fitnessManager: FitnessManager
private lateinit var binding: ActivityMainBinding
companion object {
private const val RC_SIGN_IN = 9001
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
fitnessManager = FitnessManager(this)
setupUI()
checkSignInStatus()
}
private fun setupUI() {
binding.signInButton.setOnClickListener {
fitnessManager.signIn(this, RC_SIGN_IN)
}
binding.refreshButton.setOnClickListener {
refreshFitnessData()
}
binding.logoutButton.setOnClickListener {
fitnessManager.signOut()
updateUIForSignedOut()
}
}
private fun checkSignInStatus() {
if (fitnessManager.isSignedIn()) {
updateUIForSignedIn()
refreshFitnessData()
} else {
updateUIForSignedOut()
}
}
private fun updateUIForSignedIn() {
binding.signInButton.visibility = View.GONE
binding.logoutButton.visibility = View.VISIBLE
binding.fitnessDataLayout.visibility = View.VISIBLE
}
private fun updateUIForSignedOut() {
binding.signInButton.visibility = View.VISIBLE
binding.logoutButton.visibility = View.GONE
binding.fitnessDataLayout.visibility = View.GONE
}
private fun refreshFitnessData() {
binding.progressBar.visibility = View.VISIBLE
lifecycleScope.launch {
try {
val fitnessData = fitnessManager.fetchFitnessData()
displayFitnessData(fitnessData)
val goals = fitnessManager.checkGoals(fitnessData)
displayGoals(goals)
val trends = fitnessManager.analyzeTrends()
displayTrends(trends)
} catch (e: Exception) {
Toast.makeText(
this@MainActivity,
"خطأ في جلب البيانات: ${e.message}",
Toast.LENGTH_SHORT
).show()
} finally {
binding.progressBar.visibility = View.GONE
}
}
}
private fun displayFitnessData(data: FitnessManager.FitnessData) {
binding.stepsText.text = data.steps.toString()
binding.heartRateText.text = String.format("%.1f", data.heartRate)
binding.caloriesText.text = String.format("%.1f", data.calories)
binding.distanceText.text = String.format("%.2f", data.distance)
binding.lastUpdatedText.text = SimpleDateFormat("HH:mm", Locale.getDefault())
.format(data.lastUpdated)
}
private fun displayGoals(goals: Map) {
binding.stepsGoalIndicator.setImageResource(
if (goals["steps"] == true) R.drawable.ic_goal_achieved else R.drawable.ic_goal_pending
)
binding.caloriesGoalIndicator.setImageResource(
if (goals["calories"] == true) R.drawable.ic_goal_achieved else R.drawable.ic_goal_pending
)
binding.distanceGoalIndicator.setImageResource(
if (goals["distance"] == true) R.drawable.ic_goal_achieved else R.drawable.ic_goal_pending
)
}
private fun displayTrends(trends: List) {
val trendText = StringBuilder("الاتجاهات:\n")
trends.forEach { trend ->
val arrow = when (trend.trend) {
FitnessManager.TrendDirection.INCREASING -> "↑"
FitnessManager.TrendDirection.DECREASING -> "↓"
FitnessManager.TrendDirection.STABLE -> "→"
}
trendText.append("${trend.metric}: $arrow ${String.format("%.1f", trend.changePercent)}%\n")
trendText.append("${trend.suggestion}\n\n")
}
binding.trendsText.text = trendText.toString()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == RC_SIGN_IN) {
if (fitnessManager.handleSignInResult(data)) {
updateUIForSignedIn()
refreshFitnessData()
} else {
Toast.makeText(this, "فشل تسجيل الدخول", Toast.LENGTH_SHORT).show()
}
}
}
}
هندسة تطبيقات الساعات الذكية
تطبيق الهاتف (Companion)
التطبيق الرئيسي على الهاتف الذكي
تطبيق الساعة (Watch)
التطبيق المصغر على الساعة الذكية
بيانات الصحة واللياقة
HealthKit، Fitness APIs، وأجهزة الاستشعار
أدوات تطوير الساعات الذكية
Android Studio
للتطوير على Android Wear
Xcode
للتطوير على WatchOS
HealthKit
لبيانات الصحة من Apple
المزايا والتحديات
المزايا
- طلب عالي: هناك طلب كبير على مطوري تطبيقات الأجهزة القابلة للارتداء خاصة في مجالات الصحة واللياقة
- أدوات مجانية: معظم الأدوات المستخدمة مثل Android Studio و Xcode مجانية أو تقدم خطط مجانية
- تأثير إيجابي: يمكنك المساهمة في تحسين حياة المستخدمين من خلال تطبيقات الصحة واللياقة
- إبداع لا محدود: يمكنك تصميم تطبيقات مبتكرة تعمل على أجهزة متعددة وتقدم تجربة مستخدم فريدة
- سوق متنامي: سوق الساعات الذكية والأجهزة القابلة للارتداء في نمو مستمر
التحديات
- منحنى التعلم الحاد: يتطلب فهماً جيداً للغات البرمجة، التصميم، وأساسيات أنظمة التشغيل للأجهزة القابلة للارتداء
- تعقيد الأنظمة: قد تواجه تحديات في إدارة التطبيقات التي تعتمد على بيانات صحية أو رياضية
- تحديثات متكررة: الأدوات والمعايير تتطور باستمرار، مما يتطلب تحديث المعرفة بشكل منتظم
- قيود الأجهزة: الساعات الذكية لها قيود في المعالجة والبطارية والتخزين
أنواع مشاريع الساعات الذكية
تطبيقات الصحة
مراقبة معدل ضربات القلب، النوم، واللياقة
تطبيقات الرياضة
تتبع التمارين، الجري، والأنشطة البدنية
تطبيقات الإشعارات
إدارة الإشعارات، الرسائل، والمهام
الخلاصة
تطوير تطبيقات الساعات الذكية والأجهزة القابلة للارتداء يوفر فرصاً فريدة في مجالات الصحة واللياقة البدنية. من خلال إتقان Android Wear، WatchOS، وأدوات مثل HealthKit و Fitness APIs، يمكنك بناء تطبيقات مبتكرة تحسن من حياة المستخدمين.
نصائح للبدء:
- ابدأ بتعلم أساسيات Android Wear مع Kotlin/Java
- تعلم WatchOS مع Swift لساعات Apple
- أتقن HealthKit لبيانات الصحة من Apple
- تعلم Fitness APIs لبيانات اللياقة من Google
- ابنِ مشروع عملي يركز على الصحة أو اللياقة