Application iOS pour la gestion et l'analyse d'horaires de travail avec importation OCR automatique et widgets interactifs
Shifter est une application iOS native spécifiquement conçue pour les employés utilisant WorkJam qui souhaitent une segmentation détaillée de leurs heures de travail. Particulièrement adaptée aux secteurs retail, restauration et services, elle permet d'importer automatiquement des captures d'écran de plannings WorkJam via OCR, d'analyser précisément la répartition horaire par type de shift (Shift 1, Shift 2, Shift 3, etc.), et de visualiser des statistiques détaillées (mensuel/trimestriel/annuel) via des widgets iOS natifs.
Les employés utilisant WorkJam pour leurs horaires reçoivent leurs plannings sous forme de captures d'écran, mais l'application ne propose aucune analyse détaillée par segment. Shifter élimine :
- ❌ La saisie manuelle fastidieuse des horaires depuis WorkJam
- ❌ L'impossibilité de se connecter directement à WorkJam pour récupérer ses horaires
- ❌ L'impossibilité d'analyser la répartition horaire par type de shift (Shift 1, Shift 2, Shift 3, etc.)
- ❌ La difficulté à comparer ses performances entre périodes (mois/trimestre/année)
- ❌ L'absence de vision synthétique des heures par segment
- ❌ Le risque de perte de données lors des réinstallations
✅ Import OCR ultra-rapide depuis WorkJam : Capture d'écran → Reconnaissance texte → Shifts importés avec segments automatiquement
✅ Connexion directe WorkJam : Synchronisation de vos horaires sans capture d'écran, directement depuis votre compte WorkJam
✅ Segmentation détaillée par type de shift : Analyse précise de la répartition horaire (Shift 1, Shift 2, Shift 3, Pause, etc.)
✅ Statistiques intelligentes : Analyse comparative par mois/trimestre/année avec % et delta par segment
✅ Widgets iOS natifs : Accès instantané aux 3 segments prioritaires depuis l'écran d'accueil et écran verrouillé
✅ Backup automatique : Restauration des données après réinstallation
✅ Interface rétro-moderne : Inspirée de system.css (esthétique macOS classique)
✅ Performance optimisée : 0 logs en production, regex pré-compilées, cache intelligent
| Composant | Technologie | Rôle |
|---|---|---|
| Framework UI | SwiftUI | Interface déclarative native iOS |
| Persistance | SwiftData | ORM moderne avec ModelContainer partagé |
| OCR | Vision Framework | Reconnaissance de texte dans les images |
| Widgets | WidgetKit | Widgets natifs iOS (Home + Lock Screen) |
| Partage de données | App Groups | Conteneur partagé app ↔ widget |
| Cache | UserDefaults + JSON | Backup automatique et restauration |
Shifter/
├── WorkScheduleApp/ # Application principale iOS
│ ├── WorkScheduleAppApp.swift # Point d'entrée avec ModelContainer
│ ├── ContentView.swift # Vue principale (statistiques + filtres)
│ ├── Models/
│ │ ├── WorkSchedule.swift # Modèle SwiftData (collection de shifts)
│ │ └── Shift.swift # Modèle SwiftData (shift individuel)
│ ├── ViewModels/
│ │ └── ScheduleViewModel.swift # Logique métier (OCR, backup, export)
│ ├── Views/
│ │ ├── ManageDataView.swift # Gestion des shifts (liste/suppression)
│ │ ├── ShiftStatisticsView.swift # Statistiques détaillées par segment
│ │ ├── SystemCSSTheme.swift # Thème visuel system.css
│ │ └── AboutView.swift # Page À Propos (style macOS Classic)
│ ├── Services/
│ │ └── OCRService.swift # Service de reconnaissance de texte
│ └── Helpers/
│ ├── FiscalCalendarHelper.swift # Logique trimestres fiscaux (Q1-Q4)
│ └── DateFormatterCache.swift # Cache pour formatage de dates
│
├── ShifterWidget/ # Widget iOS (WidgetKit)
│ ├── ShifterWidget.swift # Vues Home Screen + Lock Screen + TimelineProvider
│ ├── WidgetDataProvider.swift # Accès SwiftData depuis le widget
│ └── ShifterWidgetBundle.swift # Configuration du widget
│
└── ShifterShareExtension/ # Extension de partage (import images)
└── ShareViewController.swift # Controller pour partage d'images
┌─────────────────────────────────────────────────────────────────┐
│ IMPORT DE PLANNING │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. Capture d'écran (Photo Picker / Share Extension) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. OCRService.recognizeText(UIImage) → Vision Framework │
│ - Détection de texte dans l'image │
│ - Support multi-formats (WorkJam, PDF, etc.) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. OCRService.parseScheduleText(String) → Parsing Regex │
│ - Extraction dates (format: "lundi 25 novembre") │
│ - Extraction horaires (9h-17h, 10:00 AM–11:30 AM, etc.) │
│ - Extraction segments (Shift 1, Shift 2, etc.) │
│ - Gestion indicateurs temporels ("hier", "Il y a X jours") │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. ScheduleViewModel.importScheduleFromImage() │
│ - Création objets Shift avec SwiftData │
│ - Sauvegarde dans ModelContainer (App Group) │
│ - Backup JSON automatique (Documents/) │
│ - Actualisation widget (WidgetCenter.reloadAllTimelines()) │
│ - Synchronisation Apple Watch (WatchConnectivityManager) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DONNÉES PERSISTÉES │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ App Group Container │ │ Documents Backup │ │
│ │ shifter.sqlite │ │ JSON (auto-restore) │ │
│ │ (SwiftData partagé) │ │ │ │
│ └────────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────────┐
│ Application iOS │ │ Widget iOS │
│ (Statistiques) │ │ (Top 3 Shifts) │
└───────────────────┘ └───────────────────────┘
Conteneur principal pour un ensemble d'horaires importés.
@Model
final class WorkSchedule: Identifiable {
var id: UUID
var title: String
var createdAt: Date
var imageData: Data? // Image originale (optionnel)
var rawOCRText: String? // Texte OCR brut (debug)
@Relationship(deleteRule: .cascade)
var shifts: [Shift] // Relation 1-N avec Shift
// Propriétés calculées
var totalHours: Double // Total d'heures décimales
var totalHoursFormatted: String // Format "42.5h"
var locations: [String] // Lieux uniques triés
var segments: [String] // Segments uniques triés
}Modèle représentant un shift individuel avec optimisation d'index.
@Model
final class Shift: Identifiable {
#Index<Shift>([\.date], [\.segment], [\.date, \.segment]) // Index composites
var id: UUID
var date: Date // Jour du shift
var startTime: Date // Heure de début
var endTime: Date // Heure de fin
var location: String // Lieu de travail
var segment: String // Type de shift (Shift 1, Shift 2, Shift 3...)
var notes: String
var isConfirmed: Bool
@Relationship(deleteRule: .nullify, inverse: \WorkSchedule.shifts)
var schedule: WorkSchedule?
// Propriété calculée
var duration: TimeInterval // Durée en secondes
var durationFormatted: String // Format "7h30"
}Pourquoi des index ?
Les index composites [\.date], [\.segment], [\.date, \.segment] accélèrent les requêtes fréquentes :
- Filtrage par période (mois/trimestre/année)
- Filtrage par segment
- Combinaisons pour statistiques
Service de reconnaissance de texte avec 636 lignes de logique complexe.
| Format Détecté | Exemple | Regex Utilisée |
|---|---|---|
| WorkJam dates | "lundi 25 novembre" |
workJamDateRegex |
| Temps relatif | "Il y a 3 jours", "hier" |
relativeTimeRegex |
| Horaires AM/PM | "10:00 AM–11:30 AM" |
timeRangeAMPMRegex |
| Horaires 24h | "9h-17h", "9:00-17:00" |
timeRange24HRegex1/2 |
- Regex pré-compilées (
static let) : Évite la recompilation à chaque parsing - Cache de parsing : Stocke jusqu'à 20 résultats récents
- Queue concurrente : Thread-safe avec
DispatchQueue.concurrent - Logs conditionnels :
#if DEBUGpour 0 logs en production - 20 segments OCR : Setup, Cycle Counts, GB On Point, Connection, Roundtable, Onboarding, Visuals, etc.
// Texte OCR brut :
"""
lundi 25 novembre
9h-17h Shift 1 Lieu de travail A
17h-18h Pause
"""
// Résultat parsé :
[
(date: 2024-11-25, start: 09:00, end: 17:00, location: "Lieu de travail A", segment: "Shift 1"),
(date: 2024-11-25, start: 17:00, end: 18:00, location: "Lieu de travail A", segment: "Pause")
]ViewModel central gérant la logique métier (535 lignes).
Import OCR
func importScheduleFromImage(_ image: UIImage) async {
// 1. OCR
let text = try await ocrService.recognizeText(from: image)
// 2. Parsing
let parsedShifts = ocrService.parseScheduleText(text)
// 3. Sauvegarde SwiftData
let schedule = WorkSchedule(title: "Import \(Date())")
for parsed in parsedShifts {
let shift = Shift(date: parsed.date, startTime: parsed.startTime, ...)
schedule.shifts.append(shift)
}
modelContext.insert(schedule)
// 4. Backup automatique
await createAutoBackup()
// 5. Actualisation widget
WidgetCenter.shared.reloadAllTimelines()
// 6. Synchronisation Apple Watch
syncToWatch()
}Backup/Restauration Automatique
- Backup : Fichier JSON dans
Documents/shifter_auto_backup.json - Avantage : Survit aux réinstallations via Xcode (certificat développeur)
- Déclencheurs : Après chaque import/modification de données
- Restauration : Automatique au lancement si SwiftData vide
Gestion des trimestres fiscaux personnalisés.
| Trimestre | Mois | Particularité |
|---|---|---|
| Q1 | Oct-Déc | Dernier trimestre année civile |
| Q2 | Jan-Mar | Premier trimestre année civile |
| Q3 | Avr-Juin | - |
| Q4 | Juil-Sept | - |
Pourquoi ces trimestres ?
Alignement avec l'année fiscale de l'entreprise (différent de l'année civile).
enum FiscalCalendarHelper {
static func fiscalQuarter(for date: Date) -> Int {
let month = Calendar.current.component(.month, from: date)
switch month {
case 1...3: return 2
case 4...6: return 3
case 7...9: return 4
default: return 1 // 10-12
}
}
static func quarterLabel(for date: Date) -> String {
"Q\(fiscalQuarter(for: date)) \(fiscalYear(for: date))"
}
}ShifterWidget.swift
├── Provider (TimelineProvider)
│ ├── placeholder() → Vue placeholder
│ ├── getSnapshot() → Aperçu instantané
│ └── getTimeline() → Entrées timeline (rafraîchissement horaire)
│
├── ShifterWidgetEntryView
│ ├── Home Screen Widgets
│ │ ├── SmallWidgetView → Segment #1 avec %
│ │ ├── MediumWidgetView → Top 3 segments (liste compacte)
│ │ └── LargeWidgetView → Top 3 segments (cartes détaillées)
│ │
│ └── Lock Screen Widgets
│ ├── CircularView → % segment principal
│ ├── RectangularView → Nom shift + %
│ └── InlineView → Label trimestre
│
└── WidgetDataProvider
├── getTop3ShiftsWithStats() → [(segment, heures, %, delta)]
└── getQuarterStats() → QuarterStats (heures totales, %)
Configuration App Group : group.com.davidguia.shifter
// WorkScheduleAppApp.swift (Application principale)
let appGroupURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.davidguia.shifter"
)!
let storeURL = appGroupURL.appendingPathComponent("shifter.sqlite")
let modelConfiguration = ModelConfiguration(url: storeURL)
let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
// WidgetDataProvider.swift (Widget)
let containerURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.davidguia.shifter"
)!
let storeURL = containerURL.appendingPathComponent("shifter.sqlite")
modelContainer = try ModelContainer(for: schema, configurations: [modelConfiguration])Résultat : Base de données SwiftData partagée entre app et widget.
Small Widget (Compact)
┌───────────────────────┐
│ Q2 2025 46h / 160h │
│ │
│ Shift 1 │
│ 46% │ ← Pourcentage (priorité espace limité)
│ │
│ +2h vs Q1 │
└───────────────────────┘
Medium Widget (Liste Top 3)
┌─────────────────────────────────────────────────────────────┐
│ Q2 2025 46h / 160h │
│ │
│ 1. Shift 1 7h30 46% +2h vs Q1 │
│ 2. Shift 2 5h00 31% -1h vs Q1 │
│ 3. Shift 3 3h45 23% +0.5h vs Q1 │
└─────────────────────────────────────────────────────────────┘
Large Widget (Cartes Détaillées)
┌─────────────────────────────────────────────────────────────┐
│ Q2 2025 46h / 160h │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Shift 1 │ │
│ │ 7h30 • 46% │ │
│ │ +2h vs Q1 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2. Shift 2 │ │
│ │ 5h00 • 31% │ │
│ │ -1h vs Q1 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 3. Shift 3 │ │
│ │ 3h45 • 23% │ │
│ │ +0.5h vs Q1 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Déclencheurs de rafraîchissement :
- Import de nouveaux shifts →
WidgetCenter.shared.reloadAllTimelines() - Suppression de shift →
WidgetCenter.shared.reloadAllTimelines()+syncToWatch() - Suppression complète →
WidgetCenter.shared.reloadAllTimelines()+syncToWatch() - Timeline automatique → Toutes les heures
Filtrage Widget :
- Exclut segment
"Général"(comme l'app principale) - Garde uniquement les shifts du trimestre fiscal en cours
- Calcule delta vs trimestre précédent
Inspiration : Esthétique macOS classique (System 7, Mac OS 9)
Palette de Couleurs
extension Color {
static let systemBeige = Color(red: 0.95, green: 0.94, blue: 0.90) // Fond principal
static let systemBorder = Color(red: 0.30, green: 0.30, blue: 0.30) // Bordures
static let systemText = Color.black // Texte principal
}Typographie
- Titres :
.system(.title, design: .monospaced)→ Police monospacée rétro - Corps :
.system(.body, design: .monospaced) - Statistiques :
.system(size: 26, weight: .bold, design: .monospaced)
Composants Visuels
- Bordures 2px noires
- Coins arrondis 8px
- Ombres légères pour profondeur
- Espacements généreux (12-16pt)
| Période | Description | Cas d'Usage |
|---|---|---|
| Mois | Vue mensuelle avec jours travaillés | Suivi quotidien, horaires hebdomadaires |
| Trimestre | Vue trimestrielle fiscale (Q1-Q4) | Objectifs trimestriels, comparaison |
| Année | Vue annuelle complète | Bilan annuel, tendances long terme |
Par Période
- Total d'heures travaillées
- Nombre de shifts
- Jours travaillés
- Heures moyennes/jour
Par Segment
- Heures totales par type de shift
- Pourcentage du total
- Delta vs période précédente (↑↓)
- Ranking (Top 3 pour widget)
Exemple de Calcul Delta
// Trimestre actuel (Q2 2025)
Shift 1: 7h30
// Trimestre précédent (Q1 2025)
Shift 1: 5h30
// Delta
+2h vs Q1 (7h30 - 5h30 = +2h)Certificats de développement Xcode expirent après 7 jours → App devient inutilisable.
Système d'Alertes Progressives
| Jours Restants | Badge | Alerte | Action |
|---|---|---|---|
| 6-7 jours | 🟢 Vert | Aucune | - |
| 4-5 jours | 🟢 Vert | Aucune | - |
| 2-3 jours | 🟠 Orange | Avertissement | Notification douce |
| 0-1 jour | 🔴 Rouge | URGENT | Modal bloquante |
Badge Visuel dans l'App
private var daysRemaining: Int {
if UserDefaults.standard.object(forKey: "firstInstallDate") == nil {
UserDefaults.standard.set(Date(), forKey: "firstInstallDate")
}
let installDate = UserDefaults.standard.object(forKey: "firstInstallDate") as! Date
let expiryDate = Calendar.current.date(byAdding: .day, value: 7, to: installDate)!
let components = Calendar.current.dateComponents([.day], from: Date(), to: expiryDate)
return max(0, components.day ?? 0)
}Backup Automatique comme Filet de Sécurité
- Fichier JSON dans
Documents/survit à la réinstallation - Restauration automatique au prochain lancement
- Limitation : Fonctionne uniquement avec même certificat développeur
📖 Guide d'Installation Complet (INSTALLATION.md) - Instructions détaillées pour certificat gratuit
- macOS : Sonoma 15.0+ (pour Xcode)
- Xcode : 26.0+ (pour Swift 6 / iOS 26)
- iOS : 26.0+ (appareil physique ou simulateur)
- Compte Développeur Apple : Gratuit (certificat 7 jours) ou payant ($99/an)
-
Ouvrir le projet
cd Shifter open WorkScheduleApp.xcodeproj -
Configurer Signing & Capabilities
- Sélectionner la target
WorkScheduleApp - Onglet "Signing & Capabilities"
- Choisir votre équipe de développement
- Activer "Automatically manage signing"
⚠️ Important : Répéter pour les targetsShifterWidgetetShifterShareExtension - Sélectionner la target
-
Configurer App Groups
- Vérifier que l'App Group
group.com.davidguia.shifterest actif pour :- ✅ WorkScheduleApp
- ✅ ShifterWidget
- ✅ ShifterShareExtension
Si vous changez l'identifiant, modifier dans :
WorkScheduleAppApp.swiftWidgetDataProvider.swiftShareViewController.swift
- Vérifier que l'App Group
-
Build & Run
Product → Run (⌘R)
- Connecter iPhone via USB
- Sélectionner appareil dans Xcode (en haut à gauche)
- Build & Run (⌘R)
- Sur iPhone : Paramètres → Général → Gestion des appareils → Faire confiance au développeur
- Lancer l'app → Écran vide "Aucun shift trouvé"
- Appuyer sur ➕ (en haut à droite)
- Choisir "Importer Capture d'écran"
- Sélectionner une capture de planning WorkJam
- Attendre l'OCR (1-3 secondes)
- Valider les shifts détectés
Format Attendu :
lundi 25 novembre
9h-17h Shift 1 Lieu de travail A
17h-18h Pause
mardi 26 novembre
10:00 AM–6:00 PM Shift 2 Lieu de travail A
- Segmented Control en haut : Mois / Trimestre / Année
- Flèches ◀︎ ▶︎ : Naviguer entre périodes
- Statistiques : Mise à jour automatique
- Liste scrollable : Tous les segments avec heures/% /delta
- Exclut "Général" : Segments utiles uniquement
- Tri par heures : Du plus grand au plus petit
Menu ⋯ (en haut à gauche)
- Gérer les données : Liste complète des shifts
- Supprimer un shift (swipe gauche)
- Supprimer tout (bouton rouge en bas)
- Exporter JSON : Sauvegarde manuelle
- Importer JSON : Restauration manuelle
- À propos : Informations app + certificat
Ajout Widget
- Écran d'accueil iPhone → Appui long
- Toucher ➕ (en haut à gauche)
- Chercher "Shifter"
- Choisir taille : Small / Medium / Large
- Ajouter au Widget
Rafraîchissement
- Automatique : Toutes les heures
- Manuel : Modifier des shifts dans l'app → Widget mis à jour instantanément
Conventions de Nommage
- Fichiers : PascalCase (
ScheduleViewModel.swift) - Classes/Structs : PascalCase (
WorkSchedule,OCRService) - Propriétés/Méthodes : camelCase (
totalHours,importSchedule()) - Constantes : camelCase (
appGroupIdentifier)
Architecture MVVM
View (SwiftUI) → ViewModel (@Published) → Model (SwiftData)
↓
Service (OCR, Network...)
Commentaires
// MARK: -: Sections majeures///: Documentation API (visible QuickHelp)//: Commentaires inline
- Index SwiftData : Requêtes 3x plus rapides sur
dateetsegment - Cache OCR : Évite parsing redondant (20 entrées max)
- Regex statiques : Pré-compilation avec
static let(gain 40% CPU) - DateFormatterCache : Réutilisation formatters (évite allocations répétées)
- Logs production : 40
print()enveloppés dans#if DEBUG(0 logs en Release) - Compilation wholemodule : Optimisations cross-fichiers activées
- WatchConnectivity optimisé : Envoi différentiel uniquement si données changées
Test Manuel Recommandé
- Import de 10+ shifts variés
- Vérifier statistiques mensuel/trimestriel/annuel
- Tester widget Small/Medium/Large
- Simuler expiration certificat (modifier date install)
- Tester backup/restauration (supprimer app → réinstaller)
Cas Limites à Vérifier
- Import texte vide
- Import sans dates détectées
- Import avec formats horaires mixtes
- Trimestres à cheval sur années (Q1 2024 → Q2 2025)
- Suppression complète puis restauration
- Swift 6.0 : Conformité complète (Sendable, @unchecked Sendable, @preconcurrency)
- iOS 26.0 : Deployment target mis à jour, correction dépréciations
- Suppression Apple Watch : Codebase allégé, target WatchConnectivity retirée
- Migration @Observable : Remplacement @ObservableObject/Published (Swift 17+/26)
- Navigation intelligente : Boutons désactivés sur périodes sans données, saut automatique aux périodes renseignées
- Optimisation assets (-92%) : pngquant sur tous les PNG, 37.8 Mo → 2.8 Mo
- Suppression imagesets redondants : 3 doublons d'icônes 1024px supprimés
- Previews icônes optimisées : AppIconDarkPreview (16 Ko) + AppIconTintedPreview (14 Ko) à 210px
- Refonte page À Propos : Style fenêtres macOS Classic, suppression lien GitHub
- Icônes alternatives : Dark / Tinted sélectionnables dans l'app
- Ajout des icônes d'application (multiple variants)
- Système de mise en cache optimisé
- Navigation optimisée dans les vues d'horaires
- Saisie manuelle de shifts
- Import ZIP de données
- Logger d'application (AppLogger)
- Layout optimisé avec en-tête/pied de page fixes
- Support Apple Watch complet (WatchConnectivity)
- Synchronisation automatique iPhone ↔ Watch
- Widgets Lock Screen iOS 16+
- 7 nouveaux segments OCR détectés
- Optimisations performance (40 print() en DEBUG)
- Support TestFlight (distribution beta)
- Notifications push pour shifts à venir
- Export PDF des statistiques
- Synchronisation iCloud
- Graphiques de tendances
- Widget interactif (boutons)
- Publication App Store
- Intégration Calendrier iOS
- Machine Learning pour prédictions horaires
- Siri Shortcuts
MIT License
Copyright (c) 2025 David Guia
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
David Guia
- GitHub: @david-guia
- Email: contact@davidguia.com
- Apple : Frameworks SwiftUI, SwiftData, Vision, WidgetKit
- system.css : Inspiration design (sakofchit/system.css)
- WorkJam : Format de plannings source
- Building Widgets with SwiftUI
- OCR with Vision Framework
- SwiftData Best Practices
- WatchConnectivity Programming Guide
Fait avec ❤️ pour simplifier la vie des travailleurs à horaires variables


