): T {
if (modelClass.isAssignableFrom(SettingsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return SettingsViewModel(application) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
================================================
FILE: android/app/src/main/res/drawable/ic_im_okay.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_need_help.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_notification.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_notification_alerting.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_notification_signaling.xml
================================================
================================================
FILE: android/app/src/main/res/drawable/ic_stop_sos.xml
================================================
================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: android/app/src/main/res/values/colors.xml
================================================
#FFBB86FC
#FF6200EE
#FF3700B3
#FF03DAC5
#FF018786
#FF000000
#FFFFFFFF
================================================
FILE: android/app/src/main/res/values/ic_launcher_background.xml
================================================
#FF3B30
================================================
FILE: android/app/src/main/res/values/strings.xml
================================================
Igatha
================================================
FILE: android/app/src/main/res/values/themes.xml
================================================
================================================
FILE: android/app/src/main/res/xml/backup_rules.xml
================================================
================================================
FILE: android/app/src/main/res/xml/data_extraction_rules.xml
================================================
================================================
FILE: android/app/src/test/java/com/nizarmah/igatha/ExampleUnitTest.kt
================================================
package com.nizarmah.igatha
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: android/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
================================================
FILE: android/gradle/libs.versions.toml
================================================
[versions]
agp = "8.9.2"
gson = "2.10.1"
kotlin = "2.0.0"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.1"
navigationCompose = "2.8.9"
composeBom = "2025.04.01"
workRuntimeKtx = "2.10.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: android/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
================================================
FILE: android/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: android/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: android/settings.gradle.kts
================================================
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Igatha"
include(":app")
================================================
FILE: fastlane/metadata/android/de/full_description.txt
================================================
Igatha ist eine Offline-SOS-App für Notfälle, wenn Kommunikationsnetzwerke ausfallen.
Igatha: Eine Lebensader, wenn Sie sie am meisten brauchen
In Krisenzeiten, wenn herkömmliche Kommunikationsnetze nicht verfügbar sind, bietet Igatha eine Möglichkeit, mithilfe der Bluetooth-Technologie Hilfe zu signalisieren. Igatha wurde für Situationen wie Kriegsgebiete, Naturkatastrophen oder abgelegene Orte entwickelt und funktioniert vollständig offline, damit Sie mit anderen in der Nähe in Kontakt treten können.
Merkmale:
- Offline-SOS-Übertragung: Senden Sie ein SOS-Signal über Bluetooth Low Energy (BLE), ohne dass eine Internet- oder Mobilfunkverbindung erforderlich ist.
- Signalerkennung in der Nähe: Suchen Sie nach SOS-Signalen von anderen in Ihrer Nähe, die möglicherweise ebenfalls Hilfe benötigen.
- Ungefähre Entfernungsschätzung: Machen Sie sich ein Bild davon, wie nah andere Personen sind, die SOS-Signale gesendet oder empfangen haben.
- Automatische Notfallerkennung: Die App überwacht bestimmte Sensordaten, um mögliche Notfälle wie plötzliche Bewegungen zu erkennen, und kann automatisch ein SOS-Signal senden.
Wichtige Informationen:
- Datenschutz respektvoll: Igatha erhebt oder speichert keine personenbezogenen Daten. Alle Vorgänge werden lokal auf Ihrem Gerät ausgeführt.
- Open Source: Der Quellcode der App ist unter github.com/nizarmah/igatha verfügbar. Sie können es gerne überprüfen, beisteuern oder bei Bedarf ändern.
- Einschränkungen: Diese App ist eine frühe Version (MVP) und funktioniert möglicherweise nicht in allen Szenarien perfekt. Die Tests waren begrenzt. Es handelt sich nicht um eine garantierte Rettungsmethode, sie kann jedoch hilfreich sein, wenn keine anderen Optionen verfügbar sind.
Für wen es ist:
- Personen in Gebieten mit beeinträchtigter Kommunikationsinfrastruktur.
- Menschen in Konfliktgebieten oder bei Naturkatastrophen.
- Jeder, der möglicherweise keinen Zugang zu herkömmlichen Kommunikationsnetzen hat.
Haftungsausschluss:
- Igatha soll in Notfällen helfen, sollte aber andere Sicherheitsmaßnahmen nicht ersetzen. Nutzen Sie immer alle verfügbaren Ressourcen, um Ihre Sicherheit zu gewährleisten.
================================================
FILE: fastlane/metadata/android/de/short_description.txt
================================================
SOS-Signalisierung und -Wiederherstellung
================================================
FILE: fastlane/metadata/android/en-US/full_description.txt
================================================
Igatha is an offline SOS app designed for emergencies when communication networks fail.
Igatha: A Lifeline When You Need It Most
In times of crisis, when traditional communication networks are unavailable, Igatha offers a way to signal for help using Bluetooth technology. Designed for situations like war zones, natural disasters, or remote locations, Igatha operates entirely offline to help you connect with others nearby.
Features:
- Offline SOS Broadcasting: Send out an SOS signal via Bluetooth Low Energy (BLE) without needing internet or cellular service.
- Nearby Signal Detection: Scan for SOS signals from others in your vicinity who may also need assistance.
- Approximate Distance Estimation: Get an idea of how close others are who have sent or received SOS signals.
- Automatic Emergency Detection: The app monitors certain sensor data to detect possible emergencies, such as sudden movements, and can automatically send an SOS signal.
Important Information:
- Privacy Respectful: Igatha does not collect or store any personal data. All operations are performed locally on your device.
- Open Source: The app’s source code is available at github.com/nizarmah/igatha. You are welcome to review, contribute, or modify it as needed.
- Limitations: This app is an early version (MVP) and may not function perfectly in all scenarios. Testing has been limited. It is not a guaranteed method of rescue but may provide assistance when no other options are available.
Who It’s For:
- Individuals in areas with compromised communication inrastructure.
- People in conflict zones or experiencing natural disasters.
- Anyone who might find themselves without access to traditional communication networks.
Disclaimer:
- Igatha is intended to assist in emergencies but should not replace other safety measures. Always use all available resources to ensure your safety.
================================================
FILE: fastlane/metadata/android/en-US/short_description.txt
================================================
SOS Signaling & Recovery
================================================
FILE: ios/Igatha/AppDelegate.swift
================================================
//
// AppDelegate.swift
// Igatha
//
// Created by Nizar Mahmoud on 14/10/2024.
//
import SwiftUI
import UserNotifications
class AppDelegate: UIResponder, UIApplicationDelegate {
var emergencyManager: EmergencyManager!
var notificationManager: NotificationManager!
var deepLinkHandler: DeepLinkHandler!
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize managers in the background
Task { await setupManagers() }
return true
}
private func setupManagers() async {
// Initialize notification manager first since it needs to be set as delegate
notificationManager = NotificationManager.shared
// Initialize emergency manager
emergencyManager = EmergencyManager.shared
// Initialize deep link handler
deepLinkHandler = DeepLinkHandler.shared
// Set up notification manager (will request permissions and schedule notifications)
await notificationManager.setup()
}
// Handle deep links when app is launched via URL
func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
return deepLinkHandler.handleDeepLink(url)
}
// Handle deep links in universal links format
func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
if userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL {
return deepLinkHandler.handleDeepLink(url)
}
return false
}
}
================================================
FILE: ios/Igatha/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
"colors" : [
{
"color" : {
"platform" : "universal",
"reference" : "systemRedColor"
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: ios/Igatha/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
"images" : [
{
"filename" : "AppIcon-20@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "AppIcon-20@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "AppIcon-29@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "AppIcon-29@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "AppIcon~ipad.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"filename" : "AppIcon-40@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "AppIcon-40@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "AppIcon@2x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "AppIcon@3x.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"filename" : "AppIcon@2x~ipad 1.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "AppIcon-83.5@2x~ipad.png",
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "AppIcon~ios-marketing.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "20x20"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "29x29"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "38x38"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "40x40"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "60x60"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "3x",
"size" : "64x64"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "68x68"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "76x76"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: ios/Igatha/Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: ios/Igatha/Constants.swift
================================================
//
// Constants.swift
// Igatha
//
// Created by Nizar Mahmoud on 06/10/2024.
//
import CoreBluetooth
struct Constants {
static let SOSBeaconServiceID: CBUUID = CBUUID(string: "1802")
static let SOSBeaconRestoreID: String = "com.nizarmah.igatha.sosbeacon"
// threshold for sudden changes in linear acceleration
// 3.0 g ~= dropping your phone on a hard surface
static let SensorAccelerationThreshold: Double = 3.0
// threshold for sudden changes in rotation
// 6.0 r/s ~= almost a full rotation in 1 second
static let SensorRotationThreshold: Double = 6.0
// threshold for sudden changes in atmospheric pressure
// 0.1 kPa ~= altitude change of approx. 8 to 12 meters
static let SensorPressureThreshold: Double = 0.1
// key for disaster detector enabled setting in app storage
static let DisasterDetectionSettingsKey: String = "disasterDetectionEnabled"
// time window for temporally correlating sensor readings
// if all thresholds exceed in 1.5s then we have an incident
static let DisasterTemporalCorrelationTimeWindow: Double = 1.5
// grace period (seconds) before an incident response is triggered
static let DisasterResponseGracePeriod: Double = 120.0
static let DisasterResponseNotificationID: String = "DISASTER_RESPONSE"
// percentage of how much a new value should affect the old value
static let RSSIExponentialMovingAverageSmoothingFactor: Double = 0.18
// Deep links.
struct DeepLink {
// Properties.
static let Key: String = "deepLink"
static let Scheme: String = "igatha"
// Actions.
struct Settings {
static let Name: String = "OpenSettings"
static let Value: String = "settings"
}
}
// Notification identifiers
struct Notifications {
struct Feedback {
static let Id: String = "feedbackRequest"
// We open Settings instead of FeedbackForm because it's easier
// Chaining NavigationLink(isActive) is a bit difficult, when nested
// Once we start using iOS 16+, we'll be able to open the form directly
static let Link = DeepLink.Settings.self
// Delay before the notification is shown (in seconds)
static let TriggerDelay: TimeInterval = 3 * 24 * 60 * 60
// Key for the timestamp of the feedback request notification
static let TimestampKey: String = "feedbackScheduledTimestamp"
}
}
}
================================================
FILE: ios/Igatha/Igatha.entitlements
================================================
com.apple.developer.sustained-execution
================================================
FILE: ios/Igatha/IgathaApp.swift
================================================
//
// IgathaApp.swift
// Igatha
//
// Created by Nizar Mahmoud on 05/10/2024.
//
import SwiftUI
@main
struct IgathaApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
================================================
FILE: ios/Igatha/Info.plist
================================================
NSBluetoothAlwaysUsageDescription
Used to find SOS signals during emergencies.
NSBluetoothPeripheralUsageDescription
Used to signal SOS during emergencies.
NSMotionUsageDescription
Used to detect disasters which affected the user.
NSLocationWhenInUseUsageDescription
Used to enable the sensors for disaster detection in the foreground.
NSLocationAlwaysAndWhenInUseUsageDescription
Used to enable the sensors for disaster detection in the background.
UIBackgroundModes
bluetooth-peripheral
audio
location
remote-notification
CFBundleURLTypes
CFBundleTypeRole
Editor
CFBundleURLName
com.nizarmah.igatha
CFBundleURLSchemes
igatha
ITSAppUsesNonExemptEncryption
LSMinimumSystemVersion
13.0.0
================================================
FILE: ios/Igatha/Models/Device.swift
================================================
//
// Models.swift
// Igatha
//
// Created by Nizar Mahmoud on 06/10/2024.
//
import Foundation
import CoreBluetooth
extension UUID {
// returns the first 8 chars from a uuid string
var shortName: String {
return self.uuidString.prefix(8).uppercased()
}
}
class Device: Identifiable, ObservableObject {
let id: String
let shortName: String
@Published var rssi: Double
@Published var lastSeen: Date
init(
id: UUID,
rssi: Double,
lastSeen: Date = Date()
) {
self.id = id.uuidString
self.shortName = id.shortName
self.rssi = rssi
self.lastSeen = lastSeen
}
func update(
rssi: Double,
lastSeen: Date = Date()
) {
let oldRSSI = self.rssi
let newRSSI = rssi
// smoothing factor
let alpha = Constants.RSSIExponentialMovingAverageSmoothingFactor
// smoothen the RSSI with exponential moving average
let smoothedRSSI = alpha * newRSSI + (1 - alpha) * oldRSSI
self.rssi = smoothedRSSI
self.lastSeen = lastSeen
}
func estimateDistance(
using pathLossExponent: PathLossExponent = .urban
) -> Double {
// 1 meter ~= -59.0 RSSI
let txPower = -59.0
// path-loss exponent
let n: Double = pathLossExponent.value
let distance = pow(10.0, (txPower - rssi) / (10.0 * n))
// round for simplicity simplicity
return (distance * 1000.0).rounded() / 1000.0
}
}
enum PathLossExponent {
// path-loss exponent (n)
// free open spaces
// n = 2.0
case freeSpace
// indoor environments
// n = 3.0
case indoor
// dense urban environments
// n = 4.0
case urban
var value: Double {
switch self {
case .freeSpace:
return 2.0
case .indoor:
return 3.0
case .urban:
return 4.0
}
}
}
================================================
FILE: ios/Igatha/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
================================================
FILE: ios/Igatha/Sensors/AccelerometerSensor.swift
================================================
//
// AccelerometerSensor.swift
// Igatha
//
// Created by Nizar Mahmoud on 12/10/2024.
//
import Foundation
import CoreMotion
class AccelerometerSensor: Sensor {
weak var delegate: SensorDelegate?
typealias T = CMMotionManager
internal let sensor: CMMotionManager
internal let threshold: Double
private let updateInterval: TimeInterval
public var isAvailable: Bool {
return sensor.isAccelerometerAvailable
}
init(
threshold: Double,
updateInterval: TimeInterval
) {
self.threshold = threshold
self.updateInterval = updateInterval
sensor = CMMotionManager()
sensor.accelerometerUpdateInterval = threshold
}
func startUpdates() {
guard isAvailable else { return }
sensor.startAccelerometerUpdates(
to: .main
) { [weak self] data, error in
guard
let self = self,
let data = data
else { return }
let acceleration = data.acceleration
let totalAcceleration = sqrt(
acceleration.x * acceleration.x +
acceleration.y * acceleration.y +
acceleration.z * acceleration.z
)
guard totalAcceleration > self.threshold else { return }
self.delegate?.sensorExceededThreshold(
sensorType: .accelerometer,
eventTime: Date()
)
}
NSLog("AccelerometerSensor: started updates")
}
func stopUpdates() {
sensor.stopAccelerometerUpdates()
NSLog("AccelerometerSensor: stopped updates")
}
}
================================================
FILE: ios/Igatha/Sensors/BarometerSensor.swift
================================================
//
// BarometerSensor.swift
// Igatha
//
// Created by Nizar Mahmoud on 12/10/2024.
//
import Foundation
import CoreMotion
class BarometerSensor: Sensor {
weak var delegate: SensorDelegate?
typealias T = CMAltimeter
internal let sensor: CMAltimeter
internal let threshold: Double
private var initialPressure: Double?
public var isAvailable: Bool {
return CMAltimeter.isRelativeAltitudeAvailable()
}
init(
threshold: Double
) {
self.threshold = threshold
sensor = CMAltimeter()
}
func startUpdates() {
guard isAvailable else { return }
sensor.startRelativeAltitudeUpdates(
to: .main
) { [weak self] data, error in
guard
let self = self,
let data = data
else { return }
let pressure = data.pressure.doubleValue // in kPa
if self.initialPressure == nil {
self.initialPressure = pressure
return
}
guard let initialPressure = self.initialPressure else { return }
let pressureChange = abs(pressure - initialPressure)
guard pressureChange > self.threshold else { return }
self.delegate?.sensorExceededThreshold(
sensorType: .barometer,
eventTime: Date()
)
}
NSLog("BarometerSensor: started updates")
}
func stopUpdates() {
sensor.stopRelativeAltitudeUpdates()
NSLog("BarometerSensor: stopped updates")
}
}
================================================
FILE: ios/Igatha/Sensors/GyroscopeSensor.swift
================================================
//
// GyroscopeSensor.swift
// Igatha
//
// Created by Nizar Mahmoud on 12/10/2024.
//
import Foundation
import CoreMotion
class GyroscopeSensor: Sensor {
weak var delegate: SensorDelegate?
typealias T = CMMotionManager
internal let sensor: CMMotionManager
internal let threshold: Double
private let updateInterval: TimeInterval
public var isAvailable: Bool {
return sensor.isGyroAvailable
}
init(
threshold: Double,
updateInterval: TimeInterval
) {
self.threshold = threshold
self.updateInterval = updateInterval
sensor = CMMotionManager()
sensor.gyroUpdateInterval = updateInterval
}
func startUpdates() {
guard isAvailable else { return }
sensor.startGyroUpdates(
to: .main
) { [weak self] data, error in
guard
let self = self,
let data = data
else { return }
let rotationRate = data.rotationRate
let totalRotationRate = sqrt(
rotationRate.x * rotationRate.x +
rotationRate.y * rotationRate.y +
rotationRate.z * rotationRate.z
)
guard totalRotationRate > self.threshold else { return }
self.delegate?.sensorExceededThreshold(
sensorType: .gyroscope,
eventTime: Date()
)
}
NSLog("GyroscopeSensor: started updates")
}
func stopUpdates() {
sensor.stopGyroUpdates()
NSLog("GyroscopeSensor: stopped updates")
}
}
================================================
FILE: ios/Igatha/Sensors/SensorType.swift
================================================
//
// SensorType.swift
// Igatha
//
// Created by Nizar Mahmoud on 12/10/2024.
//
import Foundation
enum SensorType {
case accelerometer
case gyroscope
case barometer
}
public protocol AnySensor: AnyObject {
var isAvailable: Bool { get }
func startUpdates()
func stopUpdates()
}
protocol Sensor: AnySensor {
var delegate: SensorDelegate? { get }
associatedtype T
var sensor: T { get }
var threshold: Double { get }
}
protocol SensorDelegate: AnyObject {
func sensorExceededThreshold(sensorType: SensorType, eventTime: Date)
}
================================================
FILE: ios/Igatha/Services/DeepLinkHandler.swift
================================================
//
// DeepLinkHandler.swift
// Igatha
//
// Created by Nizar Mahmoud on 22/04/2025.
//
import Foundation
import SwiftUI
class DeepLinkHandler: ObservableObject {
static let shared = DeepLinkHandler()
@Published var showSettings = false
private init() {
// Listen for deep link events
registerSettingsObserver()
}
// registerSettingsObserver listens for settings deep link notification events
private func registerSettingsObserver() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleSettings),
name: NSNotification.Name(Constants.DeepLink.Settings.Name),
object: nil
)
}
// handleSettings opens the settings view
@objc @MainActor private func handleSettings() {
showSettings = true
}
// handleDeepLink is called from the outside to handle a deep link
@MainActor func handleDeepLink(_ url: URL) -> Bool {
// Check if the URL scheme is igatha
guard let scheme = url.scheme, scheme == Constants.DeepLink.Scheme else {
return false
}
// Check if the host is feedback
if url.host == Constants.DeepLink.Settings.Value {
handleSettings()
return true
}
return false
}
}
================================================
FILE: ios/Igatha/Services/DisasterDetector.swift
================================================
//
// DisasterDetector.swift
// Igatha
//
// Created by Nizar Mahmoud on 12/10/2024.
//
import Foundation
class DisasterDetector {
weak var delegate: DisasterDetectorDelegate?
private let eventTimeWindow: TimeInterval
private var eventTimes: [SensorType: Date] = [:]
private let locationManager: LocationManager
private let accelerometerSensor: AccelerometerSensor
private let gyroscopeSensor: GyroscopeSensor
private let barometerSensor: BarometerSensor
public var isAvailable: Bool {
let isEnabled = UserDefaults.standard.bool(
forKey: Constants.DisasterDetectionSettingsKey
)
return (
isEnabled &&
locationManager.isAvailable &&
accelerometerSensor.isAvailable &&
gyroscopeSensor.isAvailable &&
barometerSensor.isAvailable
)
}
public var isActive: Bool {
return locationManager.isActive
}
init(
accelerationThreshold: Double,
rotationThreshold: Double,
pressureThreshold: Double,
eventTimeWindow: TimeInterval
) {
self.eventTimeWindow = eventTimeWindow
locationManager = LocationManager()
accelerometerSensor = AccelerometerSensor(
threshold: accelerationThreshold,
updateInterval: 0.1
)
gyroscopeSensor = GyroscopeSensor(
threshold: rotationThreshold,
updateInterval: 0.1
)
barometerSensor = BarometerSensor(
threshold: pressureThreshold
)
locationManager.delegate = self
accelerometerSensor.delegate = self
gyroscopeSensor.delegate = self
barometerSensor.delegate = self
delegate?.disasterDetectorAvailabilityUpdate(isAvailable)
}
deinit {
stopDetection()
}
func startDetection() {
guard
isAvailable
&& !isActive
else { return }
locationManager.startUpdates()
NSLog("DisasterDetector: started detection")
}
func stopDetection() {
locationManager.stopUpdates()
NSLog("DisasterDetector: stopped detection")
}
}
extension DisasterDetector: LocationManagerDelegate {
func locationUpdatesStarted() {
guard
isAvailable
&& !isActive
else { return }
accelerometerSensor.startUpdates()
gyroscopeSensor.startUpdates()
barometerSensor.startUpdates()
delegate?.disasterDetectionStarted()
}
func locationUpdatesStopped() {
accelerometerSensor.stopUpdates()
gyroscopeSensor.stopUpdates()
barometerSensor.stopUpdates()
delegate?.disasterDetectionStopped()
}
func locationManagerAvailabilityUpdate(_ isAvailable: Bool) {
delegate?.disasterDetectorAvailabilityUpdate(self.isAvailable)
if !isAvailable {
stopDetection()
}
}
}
extension DisasterDetector: SensorDelegate {
// called when a sensor exceeds a threshold
func sensorExceededThreshold(
sensorType: SensorType,
eventTime: Date
) {
eventTimes[sensorType] = eventTime
checkForIncident()
NSLog("DisasterDetector: \(sensorType) exceeded threshold")
}
// called when an incident is suspected
private func checkForIncident() {
let currentTime = Date()
// ensure all events have occurred
guard eventTimes.count == 3 else { return }
// ensure all events occurred within the time window
for (_, eventTime) in eventTimes {
if currentTime.timeIntervalSince(eventTime) > eventTimeWindow {
// event is too old; do not consider it
return
}
}
NSLog("DisasterDetector: incident detected")
// incident detected
delegate?.disasterDetected()
}
}
protocol DisasterDetectorDelegate: AnyObject {
func disasterDetected()
func disasterDetectionStarted()
func disasterDetectionStopped()
func disasterDetectorAvailabilityUpdate(_ isAvailable: Bool)
}
================================================
FILE: ios/Igatha/Services/EmergencyManager.swift
================================================
//
// EmergencyManager.swift
// Igatha
//
// Created by Nizar Mahmoud on 14/10/2024.
//
import SwiftUI
import UserNotifications
class EmergencyManager: NSObject {
static let shared = EmergencyManager()
weak var delegate: EmergencyManagerDelegate?
private var disasterDetector: DisasterDetector!
public var isDetectorAvailable: Bool {
return disasterDetector.isAvailable
}
public var isDetectorActive: Bool {
return disasterDetector.isActive
}
private var sosBeacon: SOSBeacon!
private var sirenPlayer: SirenPlayer!
public var isSOSAvailable: Bool {
return (
sosBeacon.isAvailable
&& sirenPlayer.isAvailable
)
}
public var isSOSActive: Bool {
return (
sosBeacon.isActive
|| sirenPlayer.isActive
)
}
private var confirmationTimer: Timer?
private let confirmationGracePeriod: TimeInterval = Constants.DisasterResponseGracePeriod
private override init() {
super.init()
sosBeacon = SOSBeacon()
sirenPlayer = SirenPlayer()
disasterDetector = DisasterDetector(
accelerationThreshold: Constants.SensorAccelerationThreshold,
rotationThreshold: Constants.SensorRotationThreshold,
pressureThreshold: Constants.SensorPressureThreshold,
eventTimeWindow: Constants.DisasterTemporalCorrelationTimeWindow
)
sirenPlayer.delegate = self
sosBeacon.delegate = self
disasterDetector.delegate = self
// setup local notifications
setupNotificationCategories()
UNUserNotificationCenter.current().delegate = self
requestNotificationPermissions()
}
deinit {
stopSOS()
disasterDetector.stopDetection()
}
func startDetector() {
guard
isDetectorAvailable
&& !isDetectorActive
else { return }
disasterDetector.startDetection()
delegate?.detectorStarted()
}
func stopDetector() {
disasterDetector.stopDetection()
delegate?.detectorStopped()
}
@objc func startSOS() {
guard
isSOSAvailable
&& !isSOSActive
else { return }
sosBeacon.startBroadcasting()
sirenPlayer.startSiren()
delegate?.sosStarted()
}
func stopSOS() {
sirenPlayer.stopSiren()
sosBeacon.stopBroadcasting()
confirmationTimer?.invalidate()
confirmationTimer = nil
delegate?.sosStopped()
}
// starts confirmation timer for SOS
func startConfirmationTimer() {
confirmationTimer?.invalidate()
// Schedule a new timer
confirmationTimer = Timer.scheduledTimer(
timeInterval: confirmationGracePeriod,
target: self,
selector: #selector(startSOS),
userInfo: nil,
repeats: false
)
}
}
extension EmergencyManager: SirenPlayerDelegate {
// called when siren starts
func sirenStarted() {
// do nothing
}
// called when siren stops
func sirenStopped() {
// do nothing
}
// called when siren availability changes
func sirenAvailabilityUpdate(_ isAvailable: Bool) {
delegate?.sosAvailabilityUpdate(isSOSAvailable)
}
}
extension EmergencyManager: SOSBeaconDelegate {
// called when sos beacon starts
func beaconStarted() {
// do nothing
}
// called when sos beacon stops
func beaconStopped() {
// do nothing
}
// called when sos beacon availability changes
func beaconAvailabilityUpdate(_ isAvailable: Bool) {
delegate?.sosAvailabilityUpdate(isSOSAvailable)
}
}
extension EmergencyManager: DisasterDetectorDelegate {
// called when an disaster is detected
func disasterDetected() {
// handle disaster detected
delegate?.disasterDetected()
// schedule a notification if app is in background
if UIApplication.shared.applicationState != .active {
scheduleDisasterNotification()
}
// start confirmation timer
startConfirmationTimer()
}
// called when disaster detection starts
func disasterDetectionStarted() {
// do nothing
}
// called when disaster detection stops
func disasterDetectionStopped() {
// do nothing
}
// called when disaster detector availablility changes
func disasterDetectorAvailabilityUpdate(
_ isAvailable: Bool
) {
NSLog("EmergencyManager: disaster detector\(isAvailable ? "" : " not") available")
delegate?.detectorAvailabilityUpdate(isDetectorAvailable)
if !isAvailable {
stopDetector()
} else {
startDetector()
}
}
}
extension EmergencyManager: UNUserNotificationCenterDelegate {
// requests notification permissions
private func requestNotificationPermissions() {
UNUserNotificationCenter
.current()
.requestAuthorization(
options: [.alert, .sound, .badge]
) { granted, error in
if let error = error {
print("EmergencyManager: notification permission error: \(error)")
}
}
}
// sets up notification categories and actions
private func setupNotificationCategories() {
let respondAction = UNNotificationAction(
identifier: "RESPOND_ACTION",
title: "I'm Okay",
options: [.foreground]
)
let helpAction = UNNotificationAction(
identifier: "NEED_HELP_ACTION",
title: "Need Help",
options: [.destructive]
)
let category = UNNotificationCategory(
identifier: "DISASTER_CATEGORY",
actions: [respondAction, helpAction],
intentIdentifiers: [],
options: []
)
UNUserNotificationCenter.current().setNotificationCategories([category])
}
// schedules a notification when a disaster is detected
private func scheduleDisasterNotification() {
let content = UNMutableNotificationContent()
content.title = "Disaster Detected"
content.body = "Are you okay?"
content.sound = .default
content.categoryIdentifier = "DISASTER_CATEGORY"
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 1,
repeats: false
)
let request = UNNotificationRequest(
identifier: Constants.DisasterResponseNotificationID,
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("EmergencyManager: failed to schedule notification: \(error)")
}
}
}
// handle notification when app is in foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner, .sound])
}
// handle user response to notification
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
switch response.actionIdentifier {
case "RESPOND_ACTION", UNNotificationDefaultActionIdentifier:
stopSOS()
case "NEED_HELP_ACTION":
startSOS()
default:
break
}
completionHandler()
}
}
protocol EmergencyManagerDelegate: AnyObject {
func disasterDetected()
func detectorStarted()
func detectorStopped()
func detectorAvailabilityUpdate(_ isAvailable: Bool)
func sosStarted()
func sosStopped()
func sosAvailabilityUpdate(_ isAvailable: Bool)
}
================================================
FILE: ios/Igatha/Services/LocationManager.swift
================================================
//
// LocationManager.swift
// Igatha
//
// Created by Nizar Mahmoud on 17/10/2024.
//
import Foundation
import CoreLocation
class LocationManager: NSObject {
weak var delegate: LocationManagerDelegate?
private var locationManager: CLLocationManager!
public var isAvailable: Bool = false
public var isActive: Bool = false
override init() {
super.init()
locationManager = CLLocationManager()
locationManager.delegate = self
// background updates to read core motion sensors
locationManager.allowsBackgroundLocationUpdates = true
// ensure the location updates arent paused by system
locationManager.pausesLocationUpdatesAutomatically = false
// uses radio signals; low battery consumption
locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers
locationManager.requestAlwaysAuthorization()
}
deinit {
stopUpdates()
}
func startUpdates() {
guard
isAvailable
&& !isActive
else { return }
locationManager.startUpdatingLocation()
NSLog("LocationManager: started updates")
delegate?.locationUpdatesStarted()
}
func stopUpdates() {
locationManager.stopUpdatingLocation()
NSLog("LocationManager: stopped updates")
delegate?.locationUpdatesStopped()
}
}
extension LocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined:
locationManager.requestAlwaysAuthorization()
return
case .authorizedAlways:
isAvailable = true
case .authorizedWhenInUse, .restricted, .denied:
isAvailable = false
@unknown default:
isAvailable = false
}
NSLog("LocationManager:\(isAvailable ? "" : " not") available")
delegate?.locationManagerAvailabilityUpdate(isAvailable)
if !isAvailable {
stopUpdates()
}
}
func locationManager(
_ manager: CLLocationManager,
didFailWithError error: Error
) {
isAvailable = false
}
func locationManagerDidResumeLocationUpdates(_ manager: CLLocationManager) {
isActive = true
}
func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
isActive = false
}
}
protocol LocationManagerDelegate: AnyObject {
func locationUpdatesStarted()
func locationUpdatesStopped()
func locationManagerAvailabilityUpdate(_ isAvailable: Bool)
}
================================================
FILE: ios/Igatha/Services/NotificationManager.swift
================================================
//
// NotificationManager.swift
// Igatha
//
// Created by Nizar Mahmoud on 22/04/2025.
//
import Foundation
import UserNotifications
import SwiftUI
class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
static let shared = NotificationManager()
// setup is called when the app is launched
func setup() async {
do {
try await requestAuthorization()
await scheduleFeedbackRequestNotification()
} catch {
NSLog("Error setting up notification manager: \(error)")
}
}
private override init() {
super.init()
// Set the delegate for the notification center
UNUserNotificationCenter.current().delegate = self
}
// requestAuthorization requests notification permission
private func requestAuthorization() async throws {
let options: UNAuthorizationOptions = [.alert, .sound, .badge]
try await UNUserNotificationCenter.current().requestAuthorization(options: options)
}
// scheduleFeedbackRequestNotification schedules the feedback request notification
private func scheduleFeedbackRequestNotification() async {
// Ignore if we have already scheduled the feedback request notification
if UserDefaults.standard.object(
forKey: Constants.Notifications.Feedback.TimestampKey
) != nil {
return
}
let timestamp = Date()
NSLog("Scheduling feedback request notification at \(timestamp)")
// Store the timestamp when the notification was scheduled
UserDefaults.standard.set(
timestamp,
forKey: Constants.Notifications.Feedback.TimestampKey
)
// Create the notification content
let content = UNMutableNotificationContent()
content.title = "Tell us why you use Igatha"
content.body = "Help us improve it for you and others"
content.sound = .default
content.userInfo = [
Constants.DeepLink.Key: Constants.Notifications.Feedback.Link.Value
]
// Schedule the notification for the future
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: Constants.Notifications.Feedback.TriggerDelay,
repeats: false
)
// Create the notification request
let request = UNNotificationRequest(
identifier: Constants.Notifications.Feedback.Id,
content: content,
trigger: trigger
)
// Add the request to the notification center with async/await
do {
try await UNUserNotificationCenter.current().add(request)
} catch {
NSLog("Error scheduling feedback request notification: \(error)")
}
}
// userNotificationCenter handles the notification response
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
// Get the identifier of the notification
let id = response.notification.request.identifier
// Handle the feedback request notification
if id == Constants.Notifications.Feedback.Id {
if
let deepLink = response.notification.request.content.userInfo[Constants.DeepLink.Key] as? String,
deepLink == Constants.Notifications.Feedback.Link.Value
{
// Post notification on the main thread
Task {
await MainActor.run {
NotificationCenter.default.post(
name: NSNotification.Name(Constants.Notifications.Feedback.Link.Name),
object: nil
)
}
}
}
}
completionHandler()
}
// userNotificationCenter handles the notification presentation
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
// Show the notification even if the app is in foreground
completionHandler([.banner, .sound])
}
}
================================================
FILE: ios/Igatha/Services/ProximityScanner.swift
================================================
//
// ProximityScanner.swift
// Igatha
//
// Created by Nizar Mahmoud on 06/10/2024.
//
import Foundation
import CoreBluetooth
class ProximityScanner: NSObject {
weak var delegate: ProximityScannerDelegate?
private var centralManager: CBCentralManager!
public var isActive: Bool {
return centralManager.isScanning
}
public var isAvailable: Bool = false
override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
deinit {
stopScanning()
}
}
extension ProximityScanner: CBCentralManagerDelegate {
// called when the central state changes
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
isAvailable = true
case .poweredOff, .resetting, .unauthorized, .unsupported, .unknown:
isAvailable = false
@unknown default:
isAvailable = false
}
delegate?.scannerAvailabilityUpdate(isAvailable)
if !isAvailable {
stopScanning()
} else {
startScanning()
}
}
func startScanning() {
guard
isAvailable
&& !isActive
else {
return
}
// start scanning for peripherals
centralManager.scanForPeripherals(
withServices: [
Constants.SOSBeaconServiceID
],
options: [
// allowing duplicates updates the rssi value
CBCentralManagerScanOptionAllowDuplicatesKey: true
]
)
}
func stopScanning() {
centralManager.stopScan()
}
// called when a peripheral is detected
func centralManager(
_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi RSSI: NSNumber
) {
delegate?.scannedDevice(
Device(
id: peripheral.identifier,
rssi: RSSI.doubleValue,
lastSeen: Date()
)
)
}
}
protocol ProximityScannerDelegate: AnyObject {
func scannedDevice(_ device: Device)
func scannerAvailabilityUpdate(_ isAvailable: Bool)
}
================================================
FILE: ios/Igatha/Services/SOSBeacon.swift
================================================
//
// SOSBeacon.swift
// Igatha
//
// Created by Nizar Mahmoud on 11/10/2024.
//
import Foundation
import CoreBluetooth
class SOSBeacon: NSObject {
weak var delegate: SOSBeaconDelegate?
private var peripheralManager: CBPeripheralManager!
public var isActive: Bool {
return peripheralManager.isAdvertising
}
public var isAvailable: Bool = false
override init() {
super.init()
peripheralManager = CBPeripheralManager(
delegate: self,
queue: nil,
options: [
CBPeripheralManagerOptionRestoreIdentifierKey: Constants.SOSBeaconRestoreID
]
)
}
deinit {
stopBroadcasting()
}
// start broadcasting (or re-broadcasting) as a peipheral
public func startBroadcasting() {
guard
isAvailable
&& !isActive
else { return }
// start broadcasting
peripheralManager.startAdvertising(
[
CBAdvertisementDataServiceUUIDsKey: [
Constants.SOSBeaconServiceID
]
]
)
delegate?.beaconStarted()
}
// stop broadcasting as a peripheral
public func stopBroadcasting() {
peripheralManager.stopAdvertising()
peripheralManager.removeAllServices()
delegate?.beaconStopped()
}
}
extension SOSBeacon: CBPeripheralManagerDelegate {
// called when the peripheral state changes
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOn:
isAvailable = true
case .poweredOff, .resetting, .unauthorized, .unsupported, .unknown:
isAvailable = false
@unknown default:
isAvailable = false
}
delegate?.beaconAvailabilityUpdate(isAvailable)
if !isAvailable {
stopBroadcasting()
}
}
func peripheralManager(
_ peripheral: CBPeripheralManager,
willRestoreState dict: [String: Any]
) {
// check if it was advertising
guard
let serviceUUIDs = dict[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID],
serviceUUIDs.contains(Constants.SOSBeaconServiceID)
else {
return
}
startBroadcasting()
}
}
protocol SOSBeaconDelegate: AnyObject {
func beaconStarted()
func beaconStopped()
func beaconAvailabilityUpdate(_ isAvailable: Bool)
}
================================================
FILE: ios/Igatha/Services/SirenPlayer.swift
================================================
//
// SirenPlayer.swift
// Igatha
//
// Created by Nizar Mahmoud on 12/10/2024.
//
import AVFoundation
class SirenPlayer {
weak var delegate: SirenPlayerDelegate?
private var audioEngine: AVAudioEngine?
private var playerNode: AVAudioPlayerNode?
public var isActive: Bool {
return playerNode?.isPlaying ?? false
}
public var isAvailable: Bool = false
init() {
checkSupport()
}
deinit {
stopSiren()
}
private func checkSupport() {
let audioSession = AVAudioSession.sharedInstance()
var isAvailable = true
do {
try audioSession.setCategory(
// needed to default to speaker
.playAndRecord,
mode: .default,
options: [
// plays audio on phone speaker
// not connected bluetooth devices
.defaultToSpeaker,
// plays alongside others
.mixWithOthers,
// lowers volume of others
.duckOthers
]
)
// override the output audio port to speaker
try audioSession.overrideOutputAudioPort(.speaker)
// toggle audio session for testing
try audioSession.setActive(true)
try audioSession.setActive(false)
} catch {
print("SirenPlayer: error with audio session: \(error)")
isAvailable = false
}
self.isAvailable = isAvailable
delegate?.sirenAvailabilityUpdate(isAvailable)
}
func startSiren() {
guard
isAvailable
&& !isActive
else { return }
audioEngine = AVAudioEngine()
playerNode = AVAudioPlayerNode()
guard
let audioEngine = audioEngine,
let playerNode = playerNode
else { return }
audioEngine.attach(playerNode)
let audioFormat = AVAudioFormat(
standardFormatWithSampleRate: 44100,
channels: 1
)!
let mainMixer = audioEngine.mainMixerNode
audioEngine.connect(
playerNode,
to: mainMixer,
format: audioFormat
)
let buffer = createSirenBuffer(format: audioFormat)
playerNode.scheduleBuffer(
buffer,
at: nil,
options: .loops,
completionHandler: nil
)
let audioSession = AVAudioSession.sharedInstance()
do {
// activate audio session
try audioSession.setActive(true)
// start audio engine
try audioEngine.start()
// play the siren buffer
playerNode.play()
} catch {
print("SirenPlayer: error starting siren: \(error)")
stopSiren()
return
}
delegate?.sirenStarted()
}
func stopSiren() {
// stop the siren buffer
playerNode?.stop()
playerNode = nil
// stop audio engine
audioEngine?.stop()
audioEngine = nil
let audioSession = AVAudioSession.sharedInstance()
do {
// deactivate audio session
try audioSession.setActive(false)
} catch {
print("SirenPlayer: error stopping siren: \(error)")
return
}
delegate?.sirenStopped()
}
// creates the siren sound
private func createSirenBuffer(format: AVAudioFormat) -> AVAudioPCMBuffer {
// duration of one siren cycle in seconds
let duration: Double = 2.0
let sampleRate = format.sampleRate
let totalSamples = Int(sampleRate * duration)
let buffer = AVAudioPCMBuffer(
pcmFormat: format,
frameCapacity: AVAudioFrameCount(totalSamples)
)!
buffer.frameLength = AVAudioFrameCount(totalSamples)
let channels = buffer.floatChannelData!
let channel = channels[0]
let frequencyStart: Float = 600.0 // starting frequency in Hz
let frequencyEnd: Float = 1200.0 // ending frequency in Hz
let amplitude: Float = 1.0 // volume
for sampleIndex in 0.. $1.rssi
}
}
private let emergencyManager: EmergencyManager
private let proximityScanner: ProximityScanner
init() {
emergencyManager = EmergencyManager.shared
proximityScanner = ProximityScanner()
emergencyManager.delegate = self
proximityScanner.delegate = self
updateSOSAvailability()
}
deinit {
proximityScanner.stopScanning()
}
func startDetector() {
DispatchQueue.global(qos: .background).async {
self.emergencyManager.startDetector()
}
}
func stopDetector() {
DispatchQueue.global(qos: .background).async {
self.emergencyManager.stopDetector()
}
}
func startSOS() {
DispatchQueue.global(qos: .background).async {
self.emergencyManager.startSOS()
}
}
func stopSOS() {
DispatchQueue.global(qos: .background).async {
self.emergencyManager.stopSOS()
}
}
func updateSOSAvailability(
isAvailable: Bool? = nil,
isActive: Bool? = nil
) {
DispatchQueue.main.async {
self.isSOSAvailable = (
isAvailable
?? self.emergencyManager.isSOSAvailable
)
self.isSOSActive = (
isActive
?? self.emergencyManager.isSOSActive
)
}
}
}
extension ContentViewModel: EmergencyManagerDelegate {
func disasterDetected() {
DispatchQueue.main.async {
self.activeAlert = .disasterDetected
}
}
func detectorStarted() {
// do nothing
}
func detectorStopped() {
// do nothing
}
func detectorAvailabilityUpdate(_ isAvailable: Bool) {
// do nothing
}
func sosStarted() {
updateSOSAvailability(isActive: true)
}
func sosStopped() {
updateSOSAvailability(isActive: false)
}
func sosAvailabilityUpdate(_ isAvailable: Bool) {
updateSOSAvailability(isAvailable: isAvailable)
}
}
extension ContentViewModel: ProximityScannerDelegate {
func scannedDevice(_ device: Device) {
DispatchQueue.main.async {
if !self.devicesMap.keys.contains(device.id) {
self.devicesMap[device.id] = device
}
self.devicesMap[device.id]?.update(rssi: device.rssi)
}
}
func scannerAvailabilityUpdate(_ isAvailable: Bool) {
guard isAvailable else {
proximityScanner.stopScanning()
return
}
proximityScanner.startScanning()
}
}
enum AlertType: Identifiable {
case sosConfirmation
case disasterDetected
var id: Int {
switch self {
case .sosConfirmation:
return 1
case .disasterDetected:
return 2
}
}
}
================================================
FILE: ios/Igatha/ViewModels/FeedbackFormViewModel.swift
================================================
//
// FeedbackFormViewModel.swift
// Igatha
//
// Created by Nizar Mahmoud on 20/04/2025.
//
import SwiftUI
import Foundation
// FeedbackFormViewModel handles all logic for the feedback form view.
@MainActor
class FeedbackFormViewModel: ObservableObject {
// formState stores the state of the form.
@Published public var formState: FormState = .idle
// submissionResult stores the result of the form submission.
@Published public var submissionResult: SubmissionResult?
// usageReasons stores all the usage reasons the user selected.
@Published public var usageReasons: Set = []
// Form bindings.
@Published public var customUsage: String = ""
@Published public var ideas: String = ""
@Published public var email: String = ""
// hasCustomUsage checks if the user selected the "other" usage reason.
public var hasCustomUsage: Bool {
return usageReasons.contains(.other)
}
// isUsageReasonSelected checks if the usage reason is selected.
public func isUsageReasonSelected(_ reason: UsageReason) -> Bool {
return usageReasons.contains(reason)
}
// toggleUsageReason updates the selected usage reasons.
public func toggleUsageReason(_ reason: UsageReason) {
// If reason is already selected, remove it.
guard !usageReasons.contains(reason) else {
usageReasons.remove(reason)
return
}
// Otherwise, add it.
usageReasons.insert(reason)
}
// dismissAlert dismisses submission result alert.
public func dismissAlert() {
submissionResult = nil
}
// validateForm validates the form and returns the error if it's invalid.
public func validateForm() -> String? {
// Form is valid if at least one usage reason is selected.
guard !usageReasons.isEmpty else {
return "Please select at least one usage reason."
}
// If "other" is selected, custom usage should not be empty.
if usageReasons.contains(.other) && customUsage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return "Please specify why you chose 'other'."
}
return nil
}
// submit submits the feedback form.
public func submit() async {
// Validate the form and return the error if it's invalid.
if let errMsg = validateForm() {
submissionResult = .err(errMsg)
return
}
// Update form state to submitting.
formState = .submitting
let feedback = Feedback(
usageReasons: usageReasons,
customUsage: customUsage,
ideas: ideas,
email: email
)
let form = UsageFeedbackGoogleForm(feedback: feedback)
// Submit the form asynchronously.
do {
// Try to submit the form.
try await form.submit()
// Show the success alert. Updates are safe due to @MainActor.
submissionResult = .success
} catch {
// Generate a truncated reference ID
let refId = UUID().uuidString.prefix(8)
// Get a descriptive error message
let errorMessage = error.localizedDescription.isEmpty ?
"Connection error. Check your internet connection." : error.localizedDescription
// Log the error with full details for troubleshooting
// TODO: Replace local logging with an error reporting service
NSLog("Error submitting form - Ref: \(refId) - Details: \(errorMessage)")
// Show the error alert with reference ID
submissionResult = .err("Your feedback could not be submitted. Please try again later. (Ref: \(refId))")
}
// Update form state to idle. Updates are safe due to @MainActor.
formState = .idle
}
}
// UsageFeedbackGoogleForm has the submission logic for https://forms.gle/rcu3MZjPYww7Fbnh7. [1]
// This class performs network operations and does not need to be @MainActor.
class UsageFeedbackGoogleForm {
// formUrl is the URL for the POST request.
private let formUrl = URL(string: "https://docs.google.com/forms/u/0/d/e/1FAIpQLSdCdNYIaPcg2-eAs1Mlvwoa6P5Ijqfdb1hmWlaA-poIKpMDtQ/formResponse")!
// feedback is the feedback field value.
private var feedback: Feedback
// feedbackField is the feedback field identifier.
private let feedbackField = "entry.457989095"
// init initializes the form fields.
init(feedback: Feedback) {
self.feedback = feedback
}
// submit submits the feedback form.
public func submit() async throws {
// Create the request.
var request = URLRequest(url: formUrl)
request.httpMethod = "POST"
// Google requires form to be submitted as a urlencoded-form.
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
// We pass the feedback to a single field for flexibility.
var comps = URLComponents()
comps.queryItems = [ URLQueryItem(name: feedbackField, value: feedback.toJSON()) ]
// Add the request body.
request.httpBody = comps.percentEncodedQuery?.data(using: .utf8)
// Send the request.
let (_, response) = try await URLSession.shared.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
throw URLError(.badServerResponse)
}
}
}
// Feedback stores the form data and converts it to a JSON string.
struct Feedback {
// Constant form data.
private let device = "ios"
private let key = "ios-usage-feedback-v1.0.0"
// Dynamic form data.
private var usageReasons: Set
private var customUsage: String
private var ideas: String
private var email: String
// init initializes the form data from the view fields.
init(
usageReasons: Set,
customUsage: String,
ideas: String,
email: String
) {
self.usageReasons = usageReasons
self.customUsage = customUsage
self.ideas = ideas
self.email = email
}
// toJson converts the form data to a JSON string.
public func toJSON() -> String {
let dict: [String: Any] = [
"usage": [
"reasons": usageReasons.map { $0.displayString },
"custom": customUsage,
],
"ideas": ideas,
"email": email,
"device": device,
"key": key,
]
guard
let data = try? JSONSerialization.data(withJSONObject: dict),
let str = String(data: data, encoding: .utf8)
else {
// Fallback to empty JSON string, instead of crashing.
return "{}"
}
return str
}
}
// FormState stores the state of the form.
enum FormState {
case idle
case submitting
}
// SubmissionResult stores the result of the form submission.
enum SubmissionResult: Identifiable {
// id makes each submission result unique for SwiftUI
var id: UUID { UUID() }
case success
case err(String)
}
// UsageReason stores all usage reasons and their examples.
enum UsageReason: Hashable, Identifiable, CaseIterable {
case disasterPreparedness
case adventureTravel
case caregiving
case workplaceSafety
case regionalConflict
case other
var id: Self { self }
// all cases for iteration
static var allCases: [UsageReason] = [
.disasterPreparedness, .adventureTravel, .caregiving, .workplaceSafety, .regionalConflict, .other
]
// displayString is the primary text for the usage reason.
var displayString: String {
switch self {
case .disasterPreparedness: return "Disaster Preparedness"
case .adventureTravel: return "Adventure & Travel"
case .caregiving: return "Caregiving"
case .workplaceSafety: return "Workplace Safety"
case .regionalConflict: return "Regional Conflict or Instability"
case .other: return "Other"
}
}
// exampleString is the subtext for the usage reason.
var exampleString: String? {
switch self {
case .disasterPreparedness: return "e.g., earthquakes, floods, conflicts"
case .adventureTravel: return "e.g., hiking, biking, remote trips"
case .caregiving: return "e.g., monitoring elderly or dependents"
case .workplaceSafety: return "e.g., construction, mining, field jobs"
case .regionalConflict: return "e.g., war, civil unrest, political instability"
case .other: return "Please specify below"
}
}
}
/* Notes
[1]:
I am aware that the Google form can be spammed.
I care most about emails, and this helps me reach users directly.
I made sure there's no long term or financial damage from spam.
This cheap implementation to get emails here outweighs any other.
*/
================================================
FILE: ios/Igatha/ViewModels/SettingsViewModel.swift
================================================
//
// SettingsViewModel.swift
// Igatha
//
// Created by Nizar Mahmoud on 17/10/2024.
//
import SwiftUI
class SettingsViewModel: ObservableObject {
@AppStorage(Constants.DisasterDetectionSettingsKey)
var disasterDetectionEnabled: Bool = true {
didSet {
if disasterDetectionEnabled {
DispatchQueue.global(qos: .background).async {
EmergencyManager.shared.startDetector()
}
} else {
DispatchQueue.global(qos: .background).async {
EmergencyManager.shared.stopDetector()
}
}
}
}
}
================================================
FILE: ios/Igatha/Views/ContentView.swift
================================================
//
// ContentView.swift
// Igatha
//
// Created by Nizar Mahmoud on 05/10/2024.
//
import SwiftUI
struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()
@StateObject private var deepLinkHandler = DeepLinkHandler.shared
var body: some View {
NavigationView {
VStack {
// list of devices
DeviceListView(
devices: viewModel.devices
)
.padding(.bottom, 8)
Spacer()
// sos button
Button(action: {
if viewModel.isSOSActive {
viewModel.stopSOS()
} else {
// show confirmation alert
viewModel.activeAlert = .sosConfirmation
}
}) {
Text(
viewModel.isSOSAvailable
? viewModel.isSOSActive
? "Stop SOS"
: "Send SOS"
: "SOS Unavailable"
)
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(
viewModel.isSOSActive
? Color.gray
: Color.red
)
.opacity(
viewModel.isSOSAvailable
? 1
: 0.75
)
.cornerRadius(8)
}
.disabled(!viewModel.isSOSAvailable)
.padding([.horizontal, .bottom])
.animation(.easeInOut, value: viewModel.isSOSActive)
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(
trailing: NavigationLink(
destination: SettingsView(),
isActive: $deepLinkHandler.showSettings
) {
Image(systemName: "gearshape")
.imageScale(.large)
}
)
}
.alert(item: $viewModel.activeAlert) { alertType in
switch alertType {
case .sosConfirmation:
return Alert(
title: Text("Are you sure?"),
message: Text("This will broadcast your location and start a loud siren."),
primaryButton: .destructive(Text("Yes")) {
viewModel.startSOS()
viewModel.activeAlert = nil
},
secondaryButton: .cancel() {
viewModel.activeAlert = nil
}
)
case .disasterDetected:
return Alert(
title: Text("Disaster Detected"),
message: Text("Are you okay?"),
primaryButton: .default(Text("I'm Okay")) {
viewModel.stopSOS()
viewModel.activeAlert = nil
},
secondaryButton: .destructive(Text("Need Help")) {
viewModel.startSOS()
viewModel.activeAlert = nil
}
)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
#Preview {
ContentView()
}
================================================
FILE: ios/Igatha/Views/DeviceDetailView.swift
================================================
//
// DeviceDetailView.swift
// Igatha
//
// Created by Nizar Mahmoud on 13/10/2024.
//
import SwiftUI
struct DeviceDetailView: View {
@ObservedObject var device: Device
private var timeSinceLastSeen: String {
let interval = Date().timeIntervalSince(device.lastSeen)
let minute = 60.0
let hour = 60.0 * minute
let day = 24.0 * hour
let week = 7.0 * day
if interval < minute {
let seconds = Int(interval)
return "\(seconds) second\(seconds != 1 ? "s" : "") ago"
} else if interval < hour {
let minutes = Int(interval / minute)
return "\(minutes) minute\(minutes != 1 ? "s" : "") ago"
} else if interval < day {
let hours = Int(interval / hour)
return "\(hours) hour\(hours != 1 ? "s" : "") ago"
} else if interval < week {
let days = Int(interval / day)
return "\(days) day\(days != 1 ? "s" : "") ago"
} else {
let weeks = Int(interval / week)
return "\(weeks) week\(weeks != 1 ? "s" : "") ago"
}
}
var body: some View {
TimelineView(.periodic(from: Date(), by: 60)) { context in
let currentDate = context.date
let interval = currentDate.timeIntervalSince(device.lastSeen)
let minute = 60.0
let hour = 60.0 * minute
let day = 24.0 * hour
let week = 7.0 * day
var timeSinceLastSeen: String {
if interval < minute {
let seconds = Int(interval)
return "\(seconds) second\(seconds != 1 ? "s" : "") ago"
} else if interval < hour {
let minutes = Int(interval / minute)
return "\(minutes) minute\(minutes != 1 ? "s" : "") ago"
} else if interval < day {
let hours = Int(interval / hour)
return "\(hours) hour\(hours != 1 ? "s" : "") ago"
} else if interval < week {
let days = Int(interval / day)
return "\(days) day\(days != 1 ? "s" : "") ago"
} else {
let weeks = Int(interval / week)
return "\(weeks) week\(weeks != 1 ? "s" : "") ago"
}
}
Form {
Section {
HStack {
Text("Name")
Spacer()
Text(device.shortName)
.foregroundColor(.secondary)
}
HStack {
Text("ID")
Spacer()
Text(device.id)
.foregroundColor(.secondary)
.multilineTextAlignment(.trailing)
.font(
.system(
.subheadline,
design: .monospaced
)
)
}
} header: {
Text("Identity")
.padding(.vertical, 4)
} footer: {
Text("Identity is pseudonymized for privacy.")
.padding(.vertical, 4)
}
Section {
HStack {
Text("Distance")
Spacer()
Text("\(String(format: "%.1f", device.estimateDistance())) meters")
.font(
.system(
.subheadline,
design: .monospaced
)
)
.foregroundColor(.secondary)
}
} header: {
Text("Location")
.padding(.vertical, 4)
} footer: {
Text("Location is limited by used tech. Direction is not available. Distance is approximate and varies due to signal fluctuations. It is for general guidance only.")
.padding(.vertical, 4)
}
Section {
HStack {
Text("Last Seen")
Spacer()
Text(timeSinceLastSeen)
.foregroundColor(.secondary)
}
} header: {
Text("Status")
.padding(.vertical, 4)
} footer: {
Text("Status shows if the device is active and in range.")
.padding(.vertical, 4)
}
}
.navigationTitle("Device Details")
}
}
}
struct DeviceDetailView_Previews: PreviewProvider {
static var previews: some View {
let mockDevice = Device(
id: UUID(),
rssi: -40
)
return DeviceDetailView(
device: mockDevice
)
.previewLayout(
.sizeThatFits
)
}
}
================================================
FILE: ios/Igatha/Views/DeviceListView.swift
================================================
//
// DeviceListView.swift
// Igatha
//
// Created by Nizar Mahmoud on 13/10/2024.
//
import SwiftUI
struct DeviceListView: View {
let devices: [Device]
var body: some View {
List {
Section {
if devices.isEmpty {
Text(
"No devices found nearby."
)
.foregroundColor(.gray)
.padding()
} else {
ForEach(devices) { device in
NavigationLink(
destination: DeviceDetailView(device: device)
) {
DeviceRowView(device: device)
}
}
}
} header: {
Text("People Seeking Help")
.padding(.vertical, 4)
} footer: {
Text("Note: Distance is approximate and varies due to signal fluctuations. It is for general guidance only.")
.padding(.vertical, 4)
}
}
.listStyle(.automatic)
}
}
struct DeviceListView_Previews: PreviewProvider {
static var previews: some View {
// Creating mock devices
let mockDevices = [
Device(
id: UUID(),
rssi: -40
),
Device(
id: UUID(),
rssi: -60,
lastSeen: Date().addingTimeInterval(-600)
),
Device(
id: UUID(),
rssi: -75
),
Device(
id: UUID(),
rssi: -85
)
]
return DeviceListView(
devices: mockDevices
)
}
}
================================================
FILE: ios/Igatha/Views/DeviceRowView.swift
================================================
//
// DeviceRowView.swift
// Igatha
//
// Created by Nizar Mahmoud on 13/10/2024.
//
import SwiftUI
struct DeviceRowView: View {
@ObservedObject var device: Device
var body: some View {
TimelineView(.periodic(from: Date(), by: 30)) { context in
let currentDate = context.date
let isStale = device.lastSeen < currentDate.addingTimeInterval(-300)
HStack(spacing: 16) {
// device icon
Image(systemName: "person.circle.fill")
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.foregroundColor(.secondary)
VStack(alignment: .leading, spacing: 4) {
// device short name
Text(device.shortName)
.font(.headline)
.foregroundColor(.primary)
// device distance
Text(
"\(String(format: "%.1f", device.estimateDistance())) meters away"
)
.font(
.system(
.subheadline,
design: .monospaced
)
)
.foregroundColor(.primary)
}
}
.padding(.vertical, 4)
.contentShape(Rectangle())
.opacity(isStale ? 0.4 : 1.0)
.animation(.easeInOut, value: isStale)
}
}
}
struct DeviceRowView_Previews: PreviewProvider {
static var previews: some View {
// Creating mock device
let mockDevice = Device(
id: UUID(),
rssi: -40
)
return DeviceRowView(
device: mockDevice
)
}
}
================================================
FILE: ios/Igatha/Views/FeedbackButtonView.swift
================================================
//
// FeedbackRowView.swift
// Igatha
//
// Created by Nizar Mahmoud on 20/04/2025.
//
import SwiftUI
struct FeedbackButtonView: View {
var body: some View {
NavigationLink(
destination: FeedbackFormView()
) {
FeedbackButtonContentView()
}
.padding(.vertical)
.padding(.horizontal, 20)
.contentShape(Rectangle())
.background(
LinearGradient(
// pink-ish gradient
gradient: Gradient(colors: [
Color.pink.opacity(0.6),
Color.purple.opacity(0.6)
]),
// 135 degrees gradient
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.shadow(color: .gray.opacity(0.4), radius: 5, x: 0, y: 2)
// Set chevron color to white.
.foregroundStyle(.white, .white)
}
}
struct FeedbackButtonContentView: View {
var body: some View {
HStack(spacing: 16) {
// heart icon
Image(systemName: "heart")
.resizable()
.scaledToFit()
.frame(width: 30, height: 30)
.foregroundColor(.white)
VStack(alignment: .leading, spacing: 4) {
// text
Text("Tell us why you use Igatha")
.font(.headline)
.foregroundColor(.white)
// subtext
Text("It helps us make it more reliable.")
.font(.subheadline)
.foregroundColor(.white.opacity(0.9))
}
}
}
}
struct FeedbackButtonView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
List {
FeedbackButtonView()
// removes the section padding around the feedback row
.listRowInsets(EdgeInsets())
}
}
}
}
================================================
FILE: ios/Igatha/Views/FeedbackFormView.swift
================================================
//
// FeedbackFormView.swift
// Igatha
//
// Created by Nizar Mahmoud on 20/04/2025.
//
import SwiftUI
// Feedback form to understand why people use Igatha.
struct FeedbackFormView: View {
// vm is the view model with the form logic.
@StateObject private var vm = FeedbackFormViewModel()
// dismiss is the environment variable that dismisses the current view.
// This is not the best approach, but it's good enough for this case.
@Environment(\.dismiss) private var dismiss
// body is the main view of the form.
var body: some View {
Form {
// Usage reasons.
Section {
ForEach(UsageReason.allCases) { reason in
HStack {
// Reason.
VStack(alignment: .leading) {
// Text.
Text(reason.displayString)
// Subtext.
if let examples = reason.exampleString {
Text(examples).font(.caption).foregroundColor(.secondary)
}
}
Spacer()
// If selected, show checkmark.
if vm.isUsageReasonSelected(reason) {
Image(systemName: "checkmark").foregroundColor(.accentColor)
}
}
// Make the row is tappable.
.contentShape(Rectangle())
.onTapGesture {
vm.toggleUsageReason(reason)
}
}
// If other reason, show text field.
if vm.hasCustomUsage {
TextField("Please describe", text: $vm.customUsage)
}
} header: {
Text("Why do you use Igatha?")
} footer: {
Text("Select all that apply.")
}
// Ideas.
Section {
TextEditor(text: $vm.ideas).frame(minHeight: 90)
} header: {
Text("What would make Igatha more helpful?")
} footer: {
Text("Optional. If you have any ideas, don't hesitate.")
}
// Email.
Section {
TextField("Email address", text: $vm.email)
.keyboardType(.emailAddress)
.autocapitalization(.none)
} header: {
Text("Your email")
} footer: {
Text("Optional. We'll only contact you for clarifications.")
}
// Submit.
Section {
Button(action: {
guard vm.formState == .idle else { return }
// Run the async submit function in a Task
Task { await vm.submit() }
}) {
HStack {
Text("Submit")
Spacer()
// If submitting, show progress view.
if vm.formState == .submitting {
ProgressView()
}
}
}
.disabled(vm.formState == .submitting)
}
}
.navigationBarTitle("Share Feedback", displayMode: .inline)
.alert(item: $vm.submissionResult) { result in
switch result {
case .success:
return Alert(
title: Text("Thank you!"),
message: Text("Your feedback helps us improve Igatha."),
dismissButton: .cancel(Text("Done")) {
vm.dismissAlert()
dismiss()
}
)
case .err(let message):
return Alert(
title: Text("Error"),
message: Text(message),
dismissButton: .cancel(Text("OK")) {
vm.dismissAlert()
}
)
}
}
}
}
struct FeedbackFormView_Previews: PreviewProvider {
static var previews: some View {
NavigationView { FeedbackFormView() }
}
}
================================================
FILE: ios/Igatha/Views/SettingsView.swift
================================================
//
// SettingsView.swift
// Igatha
//
// Created by Nizar Mahmoud on 14/10/2024.
//
import SwiftUI
struct SettingsView: View {
@StateObject private var viewModel = SettingsViewModel()
var body: some View {
Form {
// Background services.
Section {
// Disaster detection.
Toggle(isOn: $viewModel.disasterDetectionEnabled) {
Text("Disaster Detection")
Text("Detects disasters and sends SOS when the app is not in use. This requires location permission. This may increase battery consumption.")
.font(.caption)
.foregroundColor(.gray)
}
} header: {
Text("Background Services")
.padding(.vertical, 4)
} footer: {
Text("Services might require additional permissions.")
.padding(.vertical, 4)
}
// Feedback.
Section {
FeedbackButtonView()
// removes the section padding around the feedback row
.listRowInsets(EdgeInsets())
} header: {
Text("Feedback")
.padding(.vertical, 4)
} footer: {
Text("Your feedback helps us improve Igatha, for everyone.")
.padding(.vertical, 4)
}
}
.navigationTitle("Settings")
// Keep the title small.
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
SettingsView()
}
================================================
FILE: ios/Igatha.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXBuildFile section */
594A117D2CC255BF00592E2A /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 594A117C2CC255BF00592E2A /* CoreLocation.framework */; };
595C0AEE2CB1F181009A20E9 /* CoreBluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 595C0AED2CB1F181009A20E9 /* CoreBluetooth.framework */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
594A117C2CC255BF00592E2A /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
595C0AD42CB17946009A20E9 /* Igatha.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Igatha.app; sourceTree = BUILT_PRODUCTS_DIR; };
595C0AED2CB1F181009A20E9 /* CoreBluetooth.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreBluetooth.framework; path = System/Library/Frameworks/CoreBluetooth.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
59C19EE72CBFFC9500564F98 /* Exceptions for "Igatha" folder in "Igatha" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 595C0AD32CB17946009A20E9 /* Igatha */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
595C0AD62CB17946009A20E9 /* Igatha */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
59C19EE72CBFFC9500564F98 /* Exceptions for "Igatha" folder in "Igatha" target */,
);
path = Igatha;
sourceTree = "";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
595C0AD12CB17946009A20E9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
594A117D2CC255BF00592E2A /* CoreLocation.framework in Frameworks */,
595C0AEE2CB1F181009A20E9 /* CoreBluetooth.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
595C0ACB2CB17946009A20E9 = {
isa = PBXGroup;
children = (
595C0AD62CB17946009A20E9 /* Igatha */,
595C0AEA2CB1F179009A20E9 /* Frameworks */,
595C0AD52CB17946009A20E9 /* Products */,
);
sourceTree = "";
};
595C0AD52CB17946009A20E9 /* Products */ = {
isa = PBXGroup;
children = (
595C0AD42CB17946009A20E9 /* Igatha.app */,
);
name = Products;
sourceTree = "";
};
595C0AEA2CB1F179009A20E9 /* Frameworks */ = {
isa = PBXGroup;
children = (
594A117C2CC255BF00592E2A /* CoreLocation.framework */,
595C0AED2CB1F181009A20E9 /* CoreBluetooth.framework */,
);
name = Frameworks;
sourceTree = "";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
595C0AD32CB17946009A20E9 /* Igatha */ = {
isa = PBXNativeTarget;
buildConfigurationList = 595C0AE22CB17948009A20E9 /* Build configuration list for PBXNativeTarget "Igatha" */;
buildPhases = (
595C0AD02CB17946009A20E9 /* Sources */,
595C0AD12CB17946009A20E9 /* Frameworks */,
595C0AD22CB17946009A20E9 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
595C0AD62CB17946009A20E9 /* Igatha */,
);
name = Igatha;
packageProductDependencies = (
);
productName = Igatha;
productReference = 595C0AD42CB17946009A20E9 /* Igatha.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
595C0ACC2CB17946009A20E9 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1600;
LastUpgradeCheck = 1600;
TargetAttributes = {
595C0AD32CB17946009A20E9 = {
CreatedOnToolsVersion = 16.0;
};
};
};
buildConfigurationList = 595C0ACF2CB17946009A20E9 /* Build configuration list for PBXProject "Igatha" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 595C0ACB2CB17946009A20E9;
minimizedProjectReferenceProxies = 1;
preferredProjectObjectVersion = 77;
productRefGroup = 595C0AD52CB17946009A20E9 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
595C0AD32CB17946009A20E9 /* Igatha */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
595C0AD22CB17946009A20E9 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
595C0AD02CB17946009A20E9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
595C0AE02CB17948009A20E9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
595C0AE12CB17948009A20E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
595C0AE32CB17948009A20E9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Igatha/Igatha.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_ASSET_PATHS = "\"Igatha/Preview Content\"";
DEVELOPMENT_TEAM = FYA7HX337S;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Igatha/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Igatha;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Used to find SOS signals during emergencies.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Used to signal SOS during emergencies.";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Used to enable the sensors for disaster detection in the background.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Used to enable the sensors for disaster detection in the foreground.";
INFOPLIST_KEY_NSMotionUsageDescription = "Used to detect disasters which affected the user.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nizarmah.igatha;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
595C0AE42CB17948009A20E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Igatha/Igatha.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 5;
DEVELOPMENT_ASSET_PATHS = "\"Igatha/Preview Content\"";
DEVELOPMENT_TEAM = FYA7HX337S;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Igatha/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Igatha;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Used to find SOS signals during emergencies.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Used to signal SOS during emergencies.";
INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Used to enable the sensors for disaster detection in the background.";
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Used to enable the sensors for disaster detection in the foreground.";
INFOPLIST_KEY_NSMotionUsageDescription = "Used to detect disasters which affected the user.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.nizarmah.igatha;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
595C0ACF2CB17946009A20E9 /* Build configuration list for PBXProject "Igatha" */ = {
isa = XCConfigurationList;
buildConfigurations = (
595C0AE02CB17948009A20E9 /* Debug */,
595C0AE12CB17948009A20E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
595C0AE22CB17948009A20E9 /* Build configuration list for PBXNativeTarget "Igatha" */ = {
isa = XCConfigurationList;
buildConfigurations = (
595C0AE32CB17948009A20E9 /* Debug */,
595C0AE42CB17948009A20E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 595C0ACC2CB17946009A20E9 /* Project object */;
}
================================================
FILE: ios/Igatha.xcodeproj/project.xcworkspace/contents.xcworkspacedata
================================================
================================================
FILE: ios/Igatha.xcodeproj/xcuserdata/nizarmah.xcuserdatad/xcschemes/xcschememanagement.plist
================================================
SchemeUserState
Igatha.xcscheme_^#shared#^_
orderHint
0