diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a3f5e9d..5adca0a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1 @@ -# CODEOWNERS -# Add team(s) or bot account(s) here instead of personal names. -# Example: -# * @org/maintainers -README.md @maintainers -Documentation/** @maintainers +* @muhittincamdali diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 1548a85..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: [muhittincamdali] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index cc281ba..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Bug Report -about: Report a bug to help us improve -title: '[BUG] ' -labels: bug -assignees: '' ---- - -## Description -A clear and concise description of the bug. - -## Steps to Reproduce -1. Go to '...' -2. Click on '...' -3. Scroll down to '...' -4. See error - -## Expected Behavior -What you expected to happen. - -## Actual Behavior -What actually happened. - -## Environment -- iOS Version: [e.g. 17.0] -- Xcode Version: [e.g. 15.2] -- Device: [e.g. iPhone 15 Pro] -- Framework Version: [e.g. 1.0.0] - -## Screenshots -If applicable, add screenshots. - -## Additional Context -Any other context about the problem. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index b454dea..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Bug report -description: Create a report to help us improve -labels: [bug] -body: - - type: textarea - id: description - attributes: - label: Description - description: A clear and concise description of what the bug is - validations: - required: true - - type: textarea - id: steps - attributes: - label: Steps To Reproduce - description: Steps to reproduce the behavior - - type: input - id: version - attributes: - label: Version - description: Library/app version if applicable - - type: textarea - id: logs - attributes: - label: Logs - description: Paste relevant logs diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index e564641..0000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,8 +0,0 @@ -blank_issues_enabled: false -contact_links: - - name: 📖 Documentation - url: https://github.com/muhittincamdali/iOS-Widget-Development-Kit#readme - about: Check out the documentation first - - name: 💬 Discussions - url: https://github.com/muhittincamdali/iOS-Widget-Development-Kit/discussions - about: Ask questions and discuss ideas diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 86207f0..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project -title: '[FEATURE] ' -labels: enhancement -assignees: '' ---- - -## Problem Statement -A clear description of the problem. Ex. I'm always frustrated when [...] - -## Proposed Solution -A clear description of what you want to happen. - -## Alternatives Considered -Any alternative solutions or features you've considered. - -## Use Case -Describe how this feature would be used. - -## Additional Context -Any other context, mockups, or screenshots about the feature request. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 6e8195d..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Feature request -description: Suggest an idea for this project -labels: [enhancement] -body: - - type: textarea - id: problem - attributes: - label: Problem - description: Is your feature request related to a problem? - - type: textarea - id: solution - attributes: - label: Proposed solution - description: Describe the solution you'd like - - type: textarea - id: alternatives - attributes: - label: Alternatives considered diff --git a/.github/brand.json b/.github/brand.json new file mode 100644 index 0000000..633814b --- /dev/null +++ b/.github/brand.json @@ -0,0 +1,110 @@ +{ + "name": "iOS Widget Development Kit", + "repository": "muhittincamdali/iOS-Widget-Development-Kit", + "tagline": { + "en": "SwiftUI views and timeline helpers for WidgetKit widgets and Live Activities.", + "tr": "WidgetKit widget'ları ve Live Activities için SwiftUI görünümleri ve zaman çizelgesi yardımcıları.", + "zh-CN": "用于 WidgetKit 小组件和 Live Activities 的 SwiftUI 视图与时间线辅助工具。", + "ja": "WidgetKit ウィジェットと Live Activities のための SwiftUI ビューとタイムラインヘルパー。", + "ko": "WidgetKit 위젯과 Live Activities를 위한 SwiftUI 뷰와 타임라인 헬퍼.", + "es": "Vistas SwiftUI y utilidades de línea de tiempo para widgets de WidgetKit y Live Activities.", + "de": "SwiftUI-Views und Timeline-Hilfen für WidgetKit-Widgets und Live Activities.", + "fr": "Vues SwiftUI et utilitaires de timeline pour les widgets WidgetKit et les Live Activities.", + "pt-BR": "Views SwiftUI e utilitários de timeline para widgets do WidgetKit e Live Activities.", + "ru": "SwiftUI-представления и помощники для таймлайнов виджетов WidgetKit и Live Activities." + }, + "tags": { + "en": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "tr": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "zh-CN": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "ja": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "ko": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "es": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "de": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "fr": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "pt-BR": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ], + "ru": [ + "WidgetKit", + "Live Activities", + "App Intents", + "Swift Charts" + ] + }, + "motif": "widgets", + "buttons": [ + "start", + "docs", + "examples" + ], + "links": { + "docs": "https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/", + "examples": "Examples/" + }, + "languages": [ + "en", + "tr", + "zh-CN", + "ja", + "ko", + "es", + "de", + "fr", + "pt-BR", + "ru" + ], + "docc": { + "targets": [ + "WidgetDevelopmentKit" + ] + }, + "socialTheme": "dark", + "badgeOverrides": { + "version": "" + }, + "extraBadges": [] +} diff --git a/.github/brand/badge-language-dark.svg b/.github/brand/badge-language-dark.svg new file mode 100644 index 0000000..532f520 --- /dev/null +++ b/.github/brand/badge-language-dark.svg @@ -0,0 +1 @@ +Swift: 6.0+ diff --git a/.github/brand/badge-language-light.svg b/.github/brand/badge-language-light.svg new file mode 100644 index 0000000..8802a13 --- /dev/null +++ b/.github/brand/badge-language-light.svg @@ -0,0 +1 @@ +Swift: 6.0+ diff --git a/.github/brand/badge-license-dark.svg b/.github/brand/badge-license-dark.svg new file mode 100644 index 0000000..31013fc --- /dev/null +++ b/.github/brand/badge-license-dark.svg @@ -0,0 +1 @@ +License: MIT diff --git a/.github/brand/badge-license-light.svg b/.github/brand/badge-license-light.svg new file mode 100644 index 0000000..53d3703 --- /dev/null +++ b/.github/brand/badge-license-light.svg @@ -0,0 +1 @@ +License: MIT diff --git a/.github/brand/badge-platforms-dark.svg b/.github/brand/badge-platforms-dark.svg new file mode 100644 index 0000000..f10a974 --- /dev/null +++ b/.github/brand/badge-platforms-dark.svg @@ -0,0 +1 @@ +Platforms: iOS 17+ · macOS 14+ diff --git a/.github/brand/badge-platforms-light.svg b/.github/brand/badge-platforms-light.svg new file mode 100644 index 0000000..f632c43 --- /dev/null +++ b/.github/brand/badge-platforms-light.svg @@ -0,0 +1 @@ +Platforms: iOS 17+ · macOS 14+ diff --git a/.github/brand/banner-dark.svg b/.github/brand/banner-dark.svg new file mode 100644 index 0000000..90a95d1 --- /dev/null +++ b/.github/brand/banner-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI views and timeline helpers for WidgetKit widgets and Live Activities. diff --git a/.github/brand/banner-de-dark.svg b/.github/brand/banner-de-dark.svg new file mode 100644 index 0000000..5886067 --- /dev/null +++ b/.github/brand/banner-de-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI-Views und Timeline-Hilfen für WidgetKit-Widgets und Live Activities. diff --git a/.github/brand/banner-de-light.svg b/.github/brand/banner-de-light.svg new file mode 100644 index 0000000..f3fe6e1 --- /dev/null +++ b/.github/brand/banner-de-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI-Views und Timeline-Hilfen für WidgetKit-Widgets und Live Activities. diff --git a/.github/brand/banner-es-dark.svg b/.github/brand/banner-es-dark.svg new file mode 100644 index 0000000..69ac411 --- /dev/null +++ b/.github/brand/banner-es-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. Vistas SwiftUI y utilidades de línea de tiempo para widgets de WidgetKit y Live Activities. diff --git a/.github/brand/banner-es-light.svg b/.github/brand/banner-es-light.svg new file mode 100644 index 0000000..254d91b --- /dev/null +++ b/.github/brand/banner-es-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. Vistas SwiftUI y utilidades de línea de tiempo para widgets de WidgetKit y Live Activities. diff --git a/.github/brand/banner-fr-dark.svg b/.github/brand/banner-fr-dark.svg new file mode 100644 index 0000000..feafdf6 --- /dev/null +++ b/.github/brand/banner-fr-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. Vues SwiftUI et utilitaires de timeline pour les widgets WidgetKit et les Live Activities. diff --git a/.github/brand/banner-fr-light.svg b/.github/brand/banner-fr-light.svg new file mode 100644 index 0000000..aa0651d --- /dev/null +++ b/.github/brand/banner-fr-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. Vues SwiftUI et utilitaires de timeline pour les widgets WidgetKit et les Live Activities. diff --git a/.github/brand/banner-ja-dark.svg b/.github/brand/banner-ja-dark.svg new file mode 100644 index 0000000..24049b6 --- /dev/null +++ b/.github/brand/banner-ja-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. WidgetKit ウィジェットと Live Activities のための SwiftUI ビューとタイムラインヘルパー。WidgetKit ウィジェットと Live Activities のための SwiftUIビューとタイムラインヘルパー。WidgetKit ウィジェットと Live Activities のための SwiftUI ビューとタイムラインヘルパー。 diff --git a/.github/brand/banner-ja-light.svg b/.github/brand/banner-ja-light.svg new file mode 100644 index 0000000..62cda48 --- /dev/null +++ b/.github/brand/banner-ja-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. WidgetKit ウィジェットと Live Activities のための SwiftUI ビューとタイムラインヘルパー。WidgetKit ウィジェットと Live Activities のための SwiftUIビューとタイムラインヘルパー。WidgetKit ウィジェットと Live Activities のための SwiftUI ビューとタイムラインヘルパー。 diff --git a/.github/brand/banner-ko-dark.svg b/.github/brand/banner-ko-dark.svg new file mode 100644 index 0000000..1d1de3d --- /dev/null +++ b/.github/brand/banner-ko-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. WidgetKit 위젯과 Live Activities를 위한 SwiftUI 뷰와 타임라인 헬퍼.WidgetKit 위젯과 Live Activities를 위한 SwiftUI 뷰와 타임라인 헬퍼.WidgetKit 위젯과 Live Activities를 위한SwiftUI 뷰와 타임라인 헬퍼. diff --git a/.github/brand/banner-ko-light.svg b/.github/brand/banner-ko-light.svg new file mode 100644 index 0000000..d261044 --- /dev/null +++ b/.github/brand/banner-ko-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. WidgetKit 위젯과 Live Activities를 위한 SwiftUI 뷰와 타임라인 헬퍼.WidgetKit 위젯과 Live Activities를 위한 SwiftUI 뷰와 타임라인 헬퍼.WidgetKit 위젯과 Live Activities를 위한SwiftUI 뷰와 타임라인 헬퍼. diff --git a/.github/brand/banner-light.svg b/.github/brand/banner-light.svg new file mode 100644 index 0000000..086b3f8 --- /dev/null +++ b/.github/brand/banner-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI views and timeline helpers for WidgetKit widgets and Live Activities. diff --git a/.github/brand/banner-pt-BR-dark.svg b/.github/brand/banner-pt-BR-dark.svg new file mode 100644 index 0000000..acdad02 --- /dev/null +++ b/.github/brand/banner-pt-BR-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. Views SwiftUI e utilitários de timeline para widgets do WidgetKit e Live Activities. diff --git a/.github/brand/banner-pt-BR-light.svg b/.github/brand/banner-pt-BR-light.svg new file mode 100644 index 0000000..e9d194c --- /dev/null +++ b/.github/brand/banner-pt-BR-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. Views SwiftUI e utilitários de timeline para widgets do WidgetKit e Live Activities. diff --git a/.github/brand/banner-ru-dark.svg b/.github/brand/banner-ru-dark.svg new file mode 100644 index 0000000..4a7ad6d --- /dev/null +++ b/.github/brand/banner-ru-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI-представления и помощники для таймлайнов виджетов WidgetKit и Live Activities. diff --git a/.github/brand/banner-ru-light.svg b/.github/brand/banner-ru-light.svg new file mode 100644 index 0000000..165b168 --- /dev/null +++ b/.github/brand/banner-ru-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI-представления и помощники для таймлайнов виджетов WidgetKit и Live Activities. diff --git a/.github/brand/banner-tr-dark.svg b/.github/brand/banner-tr-dark.svg new file mode 100644 index 0000000..bf63e37 --- /dev/null +++ b/.github/brand/banner-tr-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. WidgetKit widget'ları ve Live Activities için SwiftUI görünümleri ve zaman çizelgesi yardımcıları. diff --git a/.github/brand/banner-tr-light.svg b/.github/brand/banner-tr-light.svg new file mode 100644 index 0000000..a645493 --- /dev/null +++ b/.github/brand/banner-tr-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. WidgetKit widget'ları ve Live Activities için SwiftUI görünümleri ve zaman çizelgesi yardımcıları. diff --git a/.github/brand/banner-zh-CN-dark.svg b/.github/brand/banner-zh-CN-dark.svg new file mode 100644 index 0000000..9704078 --- /dev/null +++ b/.github/brand/banner-zh-CN-dark.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. 用于 WidgetKit 小组件和 Live Activities 的 SwiftUI 视图与时间线辅助工具。用于 WidgetKit 小组件和 Live Activities 的 SwiftUI 视图与时间线辅助工具。用于 WidgetKit 小组件和 Live Activities 的SwiftUI 视图与时间线辅助工具。 diff --git a/.github/brand/banner-zh-CN-light.svg b/.github/brand/banner-zh-CN-light.svg new file mode 100644 index 0000000..353e5d3 --- /dev/null +++ b/.github/brand/banner-zh-CN-light.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. 用于 WidgetKit 小组件和 Live Activities 的 SwiftUI 视图与时间线辅助工具。用于 WidgetKit 小组件和 Live Activities 的 SwiftUI 视图与时间线辅助工具。用于 WidgetKit 小组件和 Live Activities 的SwiftUI 视图与时间线辅助工具。 diff --git a/.github/brand/button-docs-de-dark.svg b/.github/brand/button-docs-de-dark.svg new file mode 100644 index 0000000..16dc3b5 --- /dev/null +++ b/.github/brand/button-docs-de-dark.svg @@ -0,0 +1 @@ +Dokumentation diff --git a/.github/brand/button-docs-de-light.svg b/.github/brand/button-docs-de-light.svg new file mode 100644 index 0000000..4c0bdf5 --- /dev/null +++ b/.github/brand/button-docs-de-light.svg @@ -0,0 +1 @@ +Dokumentation diff --git a/.github/brand/button-docs-en-dark.svg b/.github/brand/button-docs-en-dark.svg new file mode 100644 index 0000000..064f297 --- /dev/null +++ b/.github/brand/button-docs-en-dark.svg @@ -0,0 +1 @@ +Documentation diff --git a/.github/brand/button-docs-en-light.svg b/.github/brand/button-docs-en-light.svg new file mode 100644 index 0000000..6c02880 --- /dev/null +++ b/.github/brand/button-docs-en-light.svg @@ -0,0 +1 @@ +Documentation diff --git a/.github/brand/button-docs-es-dark.svg b/.github/brand/button-docs-es-dark.svg new file mode 100644 index 0000000..77fc4cd --- /dev/null +++ b/.github/brand/button-docs-es-dark.svg @@ -0,0 +1 @@ +Documentación diff --git a/.github/brand/button-docs-es-light.svg b/.github/brand/button-docs-es-light.svg new file mode 100644 index 0000000..6dce022 --- /dev/null +++ b/.github/brand/button-docs-es-light.svg @@ -0,0 +1 @@ +Documentación diff --git a/.github/brand/button-docs-fr-dark.svg b/.github/brand/button-docs-fr-dark.svg new file mode 100644 index 0000000..064f297 --- /dev/null +++ b/.github/brand/button-docs-fr-dark.svg @@ -0,0 +1 @@ +Documentation diff --git a/.github/brand/button-docs-fr-light.svg b/.github/brand/button-docs-fr-light.svg new file mode 100644 index 0000000..6c02880 --- /dev/null +++ b/.github/brand/button-docs-fr-light.svg @@ -0,0 +1 @@ +Documentation diff --git a/.github/brand/button-docs-ja-dark.svg b/.github/brand/button-docs-ja-dark.svg new file mode 100644 index 0000000..e966056 --- /dev/null +++ b/.github/brand/button-docs-ja-dark.svg @@ -0,0 +1 @@ +ドキュメントドキュメント diff --git a/.github/brand/button-docs-ja-light.svg b/.github/brand/button-docs-ja-light.svg new file mode 100644 index 0000000..d47069d --- /dev/null +++ b/.github/brand/button-docs-ja-light.svg @@ -0,0 +1 @@ +ドキュメントドキュメント diff --git a/.github/brand/button-docs-ko-dark.svg b/.github/brand/button-docs-ko-dark.svg new file mode 100644 index 0000000..35078fc --- /dev/null +++ b/.github/brand/button-docs-ko-dark.svg @@ -0,0 +1 @@ +문서문서 diff --git a/.github/brand/button-docs-ko-light.svg b/.github/brand/button-docs-ko-light.svg new file mode 100644 index 0000000..1065205 --- /dev/null +++ b/.github/brand/button-docs-ko-light.svg @@ -0,0 +1 @@ +문서문서 diff --git a/.github/brand/button-docs-pt-BR-dark.svg b/.github/brand/button-docs-pt-BR-dark.svg new file mode 100644 index 0000000..e421841 --- /dev/null +++ b/.github/brand/button-docs-pt-BR-dark.svg @@ -0,0 +1 @@ +Documentação diff --git a/.github/brand/button-docs-pt-BR-light.svg b/.github/brand/button-docs-pt-BR-light.svg new file mode 100644 index 0000000..acbe487 --- /dev/null +++ b/.github/brand/button-docs-pt-BR-light.svg @@ -0,0 +1 @@ +Documentação diff --git a/.github/brand/button-docs-ru-dark.svg b/.github/brand/button-docs-ru-dark.svg new file mode 100644 index 0000000..55850ba --- /dev/null +++ b/.github/brand/button-docs-ru-dark.svg @@ -0,0 +1 @@ +Документация diff --git a/.github/brand/button-docs-ru-light.svg b/.github/brand/button-docs-ru-light.svg new file mode 100644 index 0000000..a7135d7 --- /dev/null +++ b/.github/brand/button-docs-ru-light.svg @@ -0,0 +1 @@ +Документация diff --git a/.github/brand/button-docs-tr-dark.svg b/.github/brand/button-docs-tr-dark.svg new file mode 100644 index 0000000..c1c0023 --- /dev/null +++ b/.github/brand/button-docs-tr-dark.svg @@ -0,0 +1 @@ +Dokümantasyon diff --git a/.github/brand/button-docs-tr-light.svg b/.github/brand/button-docs-tr-light.svg new file mode 100644 index 0000000..10cbb95 --- /dev/null +++ b/.github/brand/button-docs-tr-light.svg @@ -0,0 +1 @@ +Dokümantasyon diff --git a/.github/brand/button-docs-zh-CN-dark.svg b/.github/brand/button-docs-zh-CN-dark.svg new file mode 100644 index 0000000..d13e29d --- /dev/null +++ b/.github/brand/button-docs-zh-CN-dark.svg @@ -0,0 +1 @@ +文档文档 diff --git a/.github/brand/button-docs-zh-CN-light.svg b/.github/brand/button-docs-zh-CN-light.svg new file mode 100644 index 0000000..871e080 --- /dev/null +++ b/.github/brand/button-docs-zh-CN-light.svg @@ -0,0 +1 @@ +文档文档 diff --git a/.github/brand/button-examples-de-dark.svg b/.github/brand/button-examples-de-dark.svg new file mode 100644 index 0000000..e764727 --- /dev/null +++ b/.github/brand/button-examples-de-dark.svg @@ -0,0 +1 @@ +Beispiele diff --git a/.github/brand/button-examples-de-light.svg b/.github/brand/button-examples-de-light.svg new file mode 100644 index 0000000..26e7a38 --- /dev/null +++ b/.github/brand/button-examples-de-light.svg @@ -0,0 +1 @@ +Beispiele diff --git a/.github/brand/button-examples-en-dark.svg b/.github/brand/button-examples-en-dark.svg new file mode 100644 index 0000000..d998580 --- /dev/null +++ b/.github/brand/button-examples-en-dark.svg @@ -0,0 +1 @@ +Examples diff --git a/.github/brand/button-examples-en-light.svg b/.github/brand/button-examples-en-light.svg new file mode 100644 index 0000000..697fae9 --- /dev/null +++ b/.github/brand/button-examples-en-light.svg @@ -0,0 +1 @@ +Examples diff --git a/.github/brand/button-examples-es-dark.svg b/.github/brand/button-examples-es-dark.svg new file mode 100644 index 0000000..c175c67 --- /dev/null +++ b/.github/brand/button-examples-es-dark.svg @@ -0,0 +1 @@ +Ejemplos diff --git a/.github/brand/button-examples-es-light.svg b/.github/brand/button-examples-es-light.svg new file mode 100644 index 0000000..58cb2a2 --- /dev/null +++ b/.github/brand/button-examples-es-light.svg @@ -0,0 +1 @@ +Ejemplos diff --git a/.github/brand/button-examples-fr-dark.svg b/.github/brand/button-examples-fr-dark.svg new file mode 100644 index 0000000..4c2acf4 --- /dev/null +++ b/.github/brand/button-examples-fr-dark.svg @@ -0,0 +1 @@ +Exemples diff --git a/.github/brand/button-examples-fr-light.svg b/.github/brand/button-examples-fr-light.svg new file mode 100644 index 0000000..99423a7 --- /dev/null +++ b/.github/brand/button-examples-fr-light.svg @@ -0,0 +1 @@ +Exemples diff --git a/.github/brand/button-examples-ja-dark.svg b/.github/brand/button-examples-ja-dark.svg new file mode 100644 index 0000000..6ff48c8 --- /dev/null +++ b/.github/brand/button-examples-ja-dark.svg @@ -0,0 +1 @@ +サンプルサンプル diff --git a/.github/brand/button-examples-ja-light.svg b/.github/brand/button-examples-ja-light.svg new file mode 100644 index 0000000..5c30e6f --- /dev/null +++ b/.github/brand/button-examples-ja-light.svg @@ -0,0 +1 @@ +サンプルサンプル diff --git a/.github/brand/button-examples-ko-dark.svg b/.github/brand/button-examples-ko-dark.svg new file mode 100644 index 0000000..319ba53 --- /dev/null +++ b/.github/brand/button-examples-ko-dark.svg @@ -0,0 +1 @@ +예제예제 diff --git a/.github/brand/button-examples-ko-light.svg b/.github/brand/button-examples-ko-light.svg new file mode 100644 index 0000000..48586b5 --- /dev/null +++ b/.github/brand/button-examples-ko-light.svg @@ -0,0 +1 @@ +예제예제 diff --git a/.github/brand/button-examples-pt-BR-dark.svg b/.github/brand/button-examples-pt-BR-dark.svg new file mode 100644 index 0000000..8a995a1 --- /dev/null +++ b/.github/brand/button-examples-pt-BR-dark.svg @@ -0,0 +1 @@ +Exemplos diff --git a/.github/brand/button-examples-pt-BR-light.svg b/.github/brand/button-examples-pt-BR-light.svg new file mode 100644 index 0000000..fa37808 --- /dev/null +++ b/.github/brand/button-examples-pt-BR-light.svg @@ -0,0 +1 @@ +Exemplos diff --git a/.github/brand/button-examples-ru-dark.svg b/.github/brand/button-examples-ru-dark.svg new file mode 100644 index 0000000..5b35736 --- /dev/null +++ b/.github/brand/button-examples-ru-dark.svg @@ -0,0 +1 @@ +Примеры diff --git a/.github/brand/button-examples-ru-light.svg b/.github/brand/button-examples-ru-light.svg new file mode 100644 index 0000000..5c7b939 --- /dev/null +++ b/.github/brand/button-examples-ru-light.svg @@ -0,0 +1 @@ +Примеры diff --git a/.github/brand/button-examples-tr-dark.svg b/.github/brand/button-examples-tr-dark.svg new file mode 100644 index 0000000..cfa57ff --- /dev/null +++ b/.github/brand/button-examples-tr-dark.svg @@ -0,0 +1 @@ +Örnekler diff --git a/.github/brand/button-examples-tr-light.svg b/.github/brand/button-examples-tr-light.svg new file mode 100644 index 0000000..c2c90a9 --- /dev/null +++ b/.github/brand/button-examples-tr-light.svg @@ -0,0 +1 @@ +Örnekler diff --git a/.github/brand/button-examples-zh-CN-dark.svg b/.github/brand/button-examples-zh-CN-dark.svg new file mode 100644 index 0000000..373291a --- /dev/null +++ b/.github/brand/button-examples-zh-CN-dark.svg @@ -0,0 +1 @@ +示例示例 diff --git a/.github/brand/button-examples-zh-CN-light.svg b/.github/brand/button-examples-zh-CN-light.svg new file mode 100644 index 0000000..a89579b --- /dev/null +++ b/.github/brand/button-examples-zh-CN-light.svg @@ -0,0 +1 @@ +示例示例 diff --git a/.github/brand/button-start-de-dark.svg b/.github/brand/button-start-de-dark.svg new file mode 100644 index 0000000..14547b0 --- /dev/null +++ b/.github/brand/button-start-de-dark.svg @@ -0,0 +1 @@ +Loslegen diff --git a/.github/brand/button-start-de-light.svg b/.github/brand/button-start-de-light.svg new file mode 100644 index 0000000..14547b0 --- /dev/null +++ b/.github/brand/button-start-de-light.svg @@ -0,0 +1 @@ +Loslegen diff --git a/.github/brand/button-start-en-dark.svg b/.github/brand/button-start-en-dark.svg new file mode 100644 index 0000000..f8f6051 --- /dev/null +++ b/.github/brand/button-start-en-dark.svg @@ -0,0 +1 @@ +Get started diff --git a/.github/brand/button-start-en-light.svg b/.github/brand/button-start-en-light.svg new file mode 100644 index 0000000..f8f6051 --- /dev/null +++ b/.github/brand/button-start-en-light.svg @@ -0,0 +1 @@ +Get started diff --git a/.github/brand/button-start-es-dark.svg b/.github/brand/button-start-es-dark.svg new file mode 100644 index 0000000..fc73ab3 --- /dev/null +++ b/.github/brand/button-start-es-dark.svg @@ -0,0 +1 @@ +Empezar diff --git a/.github/brand/button-start-es-light.svg b/.github/brand/button-start-es-light.svg new file mode 100644 index 0000000..fc73ab3 --- /dev/null +++ b/.github/brand/button-start-es-light.svg @@ -0,0 +1 @@ +Empezar diff --git a/.github/brand/button-start-fr-dark.svg b/.github/brand/button-start-fr-dark.svg new file mode 100644 index 0000000..2422c90 --- /dev/null +++ b/.github/brand/button-start-fr-dark.svg @@ -0,0 +1 @@ +Commencer diff --git a/.github/brand/button-start-fr-light.svg b/.github/brand/button-start-fr-light.svg new file mode 100644 index 0000000..2422c90 --- /dev/null +++ b/.github/brand/button-start-fr-light.svg @@ -0,0 +1 @@ +Commencer diff --git a/.github/brand/button-start-ja-dark.svg b/.github/brand/button-start-ja-dark.svg new file mode 100644 index 0000000..fa8937c --- /dev/null +++ b/.github/brand/button-start-ja-dark.svg @@ -0,0 +1 @@ +はじめるはじめる diff --git a/.github/brand/button-start-ja-light.svg b/.github/brand/button-start-ja-light.svg new file mode 100644 index 0000000..fa8937c --- /dev/null +++ b/.github/brand/button-start-ja-light.svg @@ -0,0 +1 @@ +はじめるはじめる diff --git a/.github/brand/button-start-ko-dark.svg b/.github/brand/button-start-ko-dark.svg new file mode 100644 index 0000000..2940fcd --- /dev/null +++ b/.github/brand/button-start-ko-dark.svg @@ -0,0 +1 @@ +시작하기시작하기 diff --git a/.github/brand/button-start-ko-light.svg b/.github/brand/button-start-ko-light.svg new file mode 100644 index 0000000..2940fcd --- /dev/null +++ b/.github/brand/button-start-ko-light.svg @@ -0,0 +1 @@ +시작하기시작하기 diff --git a/.github/brand/button-start-pt-BR-dark.svg b/.github/brand/button-start-pt-BR-dark.svg new file mode 100644 index 0000000..5283f7b --- /dev/null +++ b/.github/brand/button-start-pt-BR-dark.svg @@ -0,0 +1 @@ +Começar diff --git a/.github/brand/button-start-pt-BR-light.svg b/.github/brand/button-start-pt-BR-light.svg new file mode 100644 index 0000000..5283f7b --- /dev/null +++ b/.github/brand/button-start-pt-BR-light.svg @@ -0,0 +1 @@ +Começar diff --git a/.github/brand/button-start-ru-dark.svg b/.github/brand/button-start-ru-dark.svg new file mode 100644 index 0000000..b0c6de5 --- /dev/null +++ b/.github/brand/button-start-ru-dark.svg @@ -0,0 +1 @@ +Начать diff --git a/.github/brand/button-start-ru-light.svg b/.github/brand/button-start-ru-light.svg new file mode 100644 index 0000000..b0c6de5 --- /dev/null +++ b/.github/brand/button-start-ru-light.svg @@ -0,0 +1 @@ +Начать diff --git a/.github/brand/button-start-tr-dark.svg b/.github/brand/button-start-tr-dark.svg new file mode 100644 index 0000000..40fc46e --- /dev/null +++ b/.github/brand/button-start-tr-dark.svg @@ -0,0 +1 @@ +Başlayın diff --git a/.github/brand/button-start-tr-light.svg b/.github/brand/button-start-tr-light.svg new file mode 100644 index 0000000..40fc46e --- /dev/null +++ b/.github/brand/button-start-tr-light.svg @@ -0,0 +1 @@ +Başlayın diff --git a/.github/brand/button-start-zh-CN-dark.svg b/.github/brand/button-start-zh-CN-dark.svg new file mode 100644 index 0000000..1bd4d1d --- /dev/null +++ b/.github/brand/button-start-zh-CN-dark.svg @@ -0,0 +1 @@ +开始使用开始使用 diff --git a/.github/brand/button-start-zh-CN-light.svg b/.github/brand/button-start-zh-CN-light.svg new file mode 100644 index 0000000..1bd4d1d --- /dev/null +++ b/.github/brand/button-start-zh-CN-light.svg @@ -0,0 +1 @@ +开始使用开始使用 diff --git a/.github/brand/manifest.json b/.github/brand/manifest.json new file mode 100644 index 0000000..321ee1f --- /dev/null +++ b/.github/brand/manifest.json @@ -0,0 +1,89 @@ +{ + "badge-language-dark.svg": "77917ea6f01084c5", + "badge-language-light.svg": "3906db81c193622f", + "badge-license-dark.svg": "d9323e2b8ffe97e2", + "badge-license-light.svg": "9ef53b7ec58c9184", + "badge-platforms-dark.svg": "6816b70d95740f57", + "badge-platforms-light.svg": "41beedce63774bdc", + "banner-dark.svg": "97a1044aa2ead473", + "banner-de-dark.svg": "cf8cd7c99ccf48d2", + "banner-de-light.svg": "b373826d52a23b8a", + "banner-es-dark.svg": "117015ac90e85045", + "banner-es-light.svg": "4eb73a0b3bcf094e", + "banner-fr-dark.svg": "80bf67e939d01058", + "banner-fr-light.svg": "b96fd3082e697534", + "banner-ja-dark.svg": "f4969861e288e52a", + "banner-ja-light.svg": "eaa31fbcfff048dd", + "banner-ko-dark.svg": "e866d2825306f1a7", + "banner-ko-light.svg": "2c0496c32250529a", + "banner-light.svg": "406ec3f41ef471b7", + "banner-pt-BR-dark.svg": "6dc707898c9aa3bc", + "banner-pt-BR-light.svg": "051c49998eb4db55", + "banner-ru-dark.svg": "8be234e6c9026ffe", + "banner-ru-light.svg": "a7ababbd11cb3266", + "banner-tr-dark.svg": "7931e676b2a8fa93", + "banner-tr-light.svg": "ba76e918e098f9f5", + "banner-zh-CN-dark.svg": "24fe8998358b8aa8", + "banner-zh-CN-light.svg": "6d93218504cf1457", + "button-docs-de-dark.svg": "cadf63bbb781ffa0", + "button-docs-de-light.svg": "8713a516c8e75c8a", + "button-docs-en-dark.svg": "3035ef2586a2cba1", + "button-docs-en-light.svg": "2b58210f262173a8", + "button-docs-es-dark.svg": "be4d25c19bb09607", + "button-docs-es-light.svg": "300734979d228ba2", + "button-docs-fr-dark.svg": "3035ef2586a2cba1", + "button-docs-fr-light.svg": "2b58210f262173a8", + "button-docs-ja-dark.svg": "148c0511a2e33134", + "button-docs-ja-light.svg": "69b48acfbc1a8613", + "button-docs-ko-dark.svg": "b87414c6ef98b897", + "button-docs-ko-light.svg": "4898ba3bdee2aeb8", + "button-docs-pt-BR-dark.svg": "3ed6ea27080dd133", + "button-docs-pt-BR-light.svg": "8710e5e547cceaaa", + "button-docs-ru-dark.svg": "2612dc27221e79eb", + "button-docs-ru-light.svg": "60f93fbd469ea40f", + "button-docs-tr-dark.svg": "7fc7975d103fa4dc", + "button-docs-tr-light.svg": "875b8dae85788744", + "button-docs-zh-CN-dark.svg": "4744f43c968a557d", + "button-docs-zh-CN-light.svg": "062d90c3bd258afe", + "button-examples-de-dark.svg": "3f4f9a9327ab5e36", + "button-examples-de-light.svg": "17cf621227c7751c", + "button-examples-en-dark.svg": "ab050ab0abb5801b", + "button-examples-en-light.svg": "abcd6401a4b139ad", + "button-examples-es-dark.svg": "da053d66f5afecb1", + "button-examples-es-light.svg": "bfa156cc90283232", + "button-examples-fr-dark.svg": "a06770b8ae85327f", + "button-examples-fr-light.svg": "7b38d3885f1ceb67", + "button-examples-ja-dark.svg": "4c0d7c9a684c2131", + "button-examples-ja-light.svg": "4269f6eac37c8fe1", + "button-examples-ko-dark.svg": "25bc3cb9149842a3", + "button-examples-ko-light.svg": "8877a500cb5adcac", + "button-examples-pt-BR-dark.svg": "7e6149ea47fa4066", + "button-examples-pt-BR-light.svg": "265750d64d86bb31", + "button-examples-ru-dark.svg": "4edbf2dab86dfb8f", + "button-examples-ru-light.svg": "16c596471a1e3b81", + "button-examples-tr-dark.svg": "98d9b050b97eb0a0", + "button-examples-tr-light.svg": "d37b9c9482807716", + "button-examples-zh-CN-dark.svg": "8cb805ee58264b25", + "button-examples-zh-CN-light.svg": "e0415b9ab5940785", + "button-start-de-dark.svg": "b4ff927b10a8d1c3", + "button-start-de-light.svg": "b4ff927b10a8d1c3", + "button-start-en-dark.svg": "e22adc03e57406c0", + "button-start-en-light.svg": "e22adc03e57406c0", + "button-start-es-dark.svg": "8d34bc35175c0ba8", + "button-start-es-light.svg": "8d34bc35175c0ba8", + "button-start-fr-dark.svg": "948d36cca16e3af6", + "button-start-fr-light.svg": "948d36cca16e3af6", + "button-start-ja-dark.svg": "c38d9e699cfdf4b5", + "button-start-ja-light.svg": "c38d9e699cfdf4b5", + "button-start-ko-dark.svg": "d33f9961df2007d8", + "button-start-ko-light.svg": "d33f9961df2007d8", + "button-start-pt-BR-dark.svg": "11ea8f5b19c1eb09", + "button-start-pt-BR-light.svg": "11ea8f5b19c1eb09", + "button-start-ru-dark.svg": "78a4c719b8e8a875", + "button-start-ru-light.svg": "78a4c719b8e8a875", + "button-start-tr-dark.svg": "18d8e7372cfdf57a", + "button-start-tr-light.svg": "18d8e7372cfdf57a", + "button-start-zh-CN-dark.svg": "cb536b7da6d9deab", + "button-start-zh-CN-light.svg": "cb536b7da6d9deab", + "social-preview.svg": "520c4c11d42c54a6" +} diff --git a/.github/brand/social-preview.png b/.github/brand/social-preview.png new file mode 100644 index 0000000..728584e Binary files /dev/null and b/.github/brand/social-preview.png differ diff --git a/.github/brand/social-preview.svg b/.github/brand/social-preview.svg new file mode 100644 index 0000000..8dbc5b1 --- /dev/null +++ b/.github/brand/social-preview.svg @@ -0,0 +1 @@ +iOS Widget Development Kit. SwiftUI views and timeline helpers for WidgetKit widgets and Live Activities. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 068e016..8ac6b8c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,8 +3,4 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - - package-ecosystem: "swift" - directory: "/" - schedule: - interval: "weekly" + interval: "monthly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index de11402..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,29 +0,0 @@ -## Description -Brief description of the changes. - -## Type of Change -- [ ] Bug fix (non-breaking change fixing an issue) -- [ ] New feature (non-breaking change adding functionality) -- [ ] Breaking change (fix or feature causing existing functionality to change) -- [ ] Documentation update - -## Changes Made -- Change 1 -- Change 2 - -## Testing -- [ ] Unit tests pass -- [ ] UI tests pass (if applicable) -- [ ] Tested on iOS Simulator -- [ ] Tested on physical device - -## Checklist -- [ ] Code follows project style guidelines -- [ ] Self-reviewed the code -- [ ] Commented hard-to-understand areas -- [ ] Updated documentation -- [ ] No new warnings - -## Screenshots (if applicable) -Before | After ---- | --- diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 50305d1..0000000 --- a/.github/stale.yml +++ /dev/null @@ -1,14 +0,0 @@ -daysUntilStale: 60 -daysUntilClose: 7 -staleLabel: stale -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. -closeComment: > - This issue has been automatically closed due to inactivity. - Feel free to reopen if you still experience this problem. -exemptLabels: - - pinned - - security - - "priority: critical" -only: issues diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b91a8a..81fca0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,27 +1,20 @@ +# Managed by the repository kit (muhittincamdali/.github). Edit the inputs, keep the structure. name: CI on: push: branches: [main] pull_request: - branches: [main] + workflow_dispatch: + +permissions: {} jobs: - build: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - - name: Select Xcode - run: sudo xcode-select -s /Applications/Xcode_15.2.app - - - name: Build - run: swift build - - - name: Test - run: swift test - - - name: SwiftLint - run: | - brew install swiftlint - swiftlint lint --reporter github-actions-logging || true + swift: + permissions: + contents: read + uses: muhittincamdali/.github/.github/workflows/swift-ci.yml@5d3c4432dae8895b16ce3caa2f78398966953212 # v1 + with: + platforms: '["iOS","macOS"]' + # Lock Screen views and Live Activities compile only for iOS; run the tests there too. + test-on-simulator: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 303fda2..0000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: CodeQL -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] - schedule: - - cron: '"0 3 * * 0"' -jobs: - analyze: - permissions: - actions: read - contents: read - security-events: write - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: github/codeql-action/init@v3 - with: - languages: swift - - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index af0b25d..7fa19d5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,19 +1,23 @@ -name: Docs Quality +# Managed by the repository kit (muhittincamdali/.github). Pages source must be "GitHub Actions". +name: Documentation on: push: - branches: [ "main", "master" ] + branches: [main] pull_request: - branches: [ "main", "master" ] + workflow_dispatch: + +permissions: {} jobs: - linkcheck: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Check links - uses: lycheeverse/lychee-action@v1 - with: - args: --exclude "img.shields.io|starchart.cc" --verbose --no-progress "**/*.md" - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + docc: + permissions: + contents: read + pages: write + id-token: write + uses: muhittincamdali/.github/.github/workflows/docc-pages.yml@5d3c4432dae8895b16ce3caa2f78398966953212 # v1 + with: + targets: WidgetDevelopmentKit + # iOS, so the Lock Screen and Live Activity symbols are documented too. + destination: generic/platform=iOS Simulator + deploy: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 0000000..793c2e9 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,40 @@ +# Builds the example app and its widget extension against the package in this commit, so +# the code the README quotes from Examples/ keeps compiling. +name: Examples + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: {} + +concurrency: + group: examples-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: WidgetKitExample (iOS Simulator) + runs-on: macos-26 + timeout-minutes: 30 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Toolchain + run: xcodebuild -version + + - name: Build + run: | + set -o pipefail + xcodebuild build \ + -project Examples/WidgetKitExample/WidgetKitExample.xcodeproj \ + -scheme WidgetKitExample \ + -destination 'generic/platform=iOS Simulator' \ + -skipPackagePluginValidation -skipMacroValidation \ + CODE_SIGNING_ALLOWED=NO -quiet diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml deleted file mode 100644 index 58c7f0a..0000000 --- a/.github/workflows/link-check.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Link Check -on: - pull_request: - paths: ['**/*.md'] -permissions: - contents: read - pull-requests: write -jobs: - lychee: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: lycheeverse/lychee-action@v1 - with: - args: --verbose --no-progress --require-https --max-redirects 5 --retry-wait-time 2 --timeout 20s --exclude-private true **/*.md diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3d61d86 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +# Runs SwiftLint with .swiftlint.yml. The container image is pinned by digest, so a new +# SwiftLint release cannot change the result without a commit here. +name: Lint + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: {} + +concurrency: + group: lint-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + swiftlint: + name: SwiftLint + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + container: + image: ghcr.io/realm/swiftlint:0.63.2@sha256:8db376ff8a26e56fa506b56b8c70ea9c5583dc52d5746ce23b6c2c4d4ee00e31 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: SwiftLint + run: swiftlint lint --strict --reporter github-actions-logging diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml deleted file mode 100644 index b08ed2a..0000000 --- a/.github/workflows/markdownlint.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Markdown Lint -on: - pull_request: - paths: ["**/*.md"] - push: - paths: ["**/*.md"] -jobs: - markdownlint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@v17 - continue-on-error: true - with: - globs: | - **/*.md diff --git a/.github/workflows/md-quality.yml b/.github/workflows/md-quality.yml deleted file mode 100644 index 04fe061..0000000 --- a/.github/workflows/md-quality.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Markdown Quality -on: - pull_request: - paths: ['**/*.md'] -jobs: - quality: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '20' - - run: npm i -g textlint @textlint/markdown textlint-rule-terminology textlint-rule-no-dead-link - - run: | - cat > .textlintrc.json <<'JSON' - { - "rules": { - "terminology": true, - "no-dead-link": { "checkRelative": true } - } - } - JSON - - run: textlint -f pretty-error '**/*.md' diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 5ca98b2..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Deploy Documentation - -on: - push: - branches: [main, master] - -permissions: - pages: write - id-token: write - contents: read - -concurrency: - group: "pages" - cancel-in-progress: true - -jobs: - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - - name: Build Documentation - run: | - swift package --allow-writing-to-directory ./docs \ - generate-documentation \ - --disable-indexing \ - --transform-for-static-hosting \ - --output-path ./docs 2>/dev/null || echo "DocC build skipped" - - - name: Setup Pages - uses: actions/configure-pages@v4 - - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./docs - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..290e6dd --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,31 @@ +# Managed by the repository kit (muhittincamdali/.github): links, wording and README translations. +name: Quality + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: "19 18 * * 2" + workflow_dispatch: + +permissions: {} + +jobs: + links: + permissions: + contents: read + uses: muhittincamdali/.github/.github/workflows/links.yml@5d3c4432dae8895b16ce3caa2f78398966953212 # v1 + + hygiene: + permissions: + contents: read + uses: muhittincamdali/.github/.github/workflows/hygiene.yml@5d3c4432dae8895b16ce3caa2f78398966953212 # v1 + + translations: + permissions: + contents: read + uses: muhittincamdali/.github/.github/workflows/readme-i18n-sync.yml@5d3c4432dae8895b16ce3caa2f78398966953212 # v1 + with: + languages: tr zh-CN ja ko es de fr pt-BR ru + strict: true diff --git a/.github/workflows/readme-guard.yml b/.github/workflows/readme-guard.yml deleted file mode 100644 index 464be4e..0000000 --- a/.github/workflows/readme-guard.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: README Guard -on: - push: - branches: [ main, master ] - paths: [ 'README.md' ] -permissions: - contents: write - pull-requests: write -jobs: - guard: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Create review PR for README change - uses: peter-evans/create-pull-request@v6 - with: - title: "Guard: Review README.md change" - body: "Automated guard: Please review README changes." - commit-message: "chore: guard README change" - branch: "chore/readme-guard-${{ github.run_id }}" - base: "${{ github.ref_name }}" - add-paths: | - README.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 93229e1..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - release: - name: Create Release - runs-on: macos-14 - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build Package - run: swift build -c release - continue-on-error: true - - - name: Generate Changelog - id: changelog - run: | - # Get the previous tag - PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") - - if [ -z "$PREV_TAG" ]; then - CHANGELOG=$(git log --pretty=format:"- %s" HEAD) - else - CHANGELOG=$(git log --pretty=format:"- %s" $PREV_TAG..HEAD) - fi - - echo "changelog<> $GITHUB_OUTPUT - echo "$CHANGELOG" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create Release - uses: softprops/action-gh-release@v2 - with: - body: | - ## What's Changed - ${{ steps.changelog.outputs.changelog }} - - ## Installation - - ### Swift Package Manager - ```swift - dependencies: [ - .package(url: "https://github.com/muhittincamdali/${{ github.event.repository.name }}.git", from: "${{ github.ref_name }}") - ] - ``` - draft: false - prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/.github/workflows/swift-ci.yml b/.github/workflows/swift-ci.yml deleted file mode 100644 index 2019307..0000000 --- a/.github/workflows/swift-ci.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Swift CI -on: - pull_request: - push: -jobs: - spm: - if: ${{ hashFiles('Package.swift') != '' }} - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '15.4' - - name: Build - run: swift build -v - - name: Test - run: swift test --enable-code-coverage - xcode: - if: ${{ hashFiles('*.xcodeproj') != '' || hashFiles('*.xcworkspace') != '' }} - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '15.4' - - name: Select scheme and build (generic platform) - shell: bash - run: | - SCHEME=$(xcodebuild -list 2>/dev/null | awk '/Schemes:/{flag=1;next} flag && NF{print; exit}') - if [ -n "$SCHEME" ]; then - xcodebuild -scheme "$SCHEME" -configuration Release -destination 'generic/platform=iOS' build | xcpretty || true - fi diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml deleted file mode 100644 index e353097..0000000 --- a/.github/workflows/swiftlint.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: SwiftLint -on: - pull_request: - paths: ["**/*.swift", ".swiftlint.yml"] - push: - paths: ["**/*.swift", ".swiftlint.yml"] -jobs: - lint: - runs-on: macos-14 - steps: - - uses: actions/checkout@v4 - - name: Install SwiftLint - run: | - brew update || true - brew install swiftlint || true - - name: Run SwiftLint - run: swiftlint --strict diff --git a/.gitignore b/.gitignore index f23a38b..97a307e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ playground.xcworkspace # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# .swiftpm +.swiftpm/ .build/ diff --git a/.lychee.toml b/.lychee.toml new file mode 100644 index 0000000..38a8dc4 --- /dev/null +++ b/.lychee.toml @@ -0,0 +1,41 @@ +# Link check settings shared by every repository (links.yml writes this file when a repository +# has none). Keep the exclusions short and explain each one: a link that is excluded is a link +# nobody checks. + +# Re-check a link at most once a day; the cache is kept between workflow runs. +cache = true +max_cache_age = "1d" +cache_exclude_status = "429, 500.." + +max_concurrency = 8 +max_retries = 3 +timeout = 20 + +# Anchors (#section) in Markdown and HTML files must exist, so README buttons and the table of +# contents cannot point at a renamed heading. +include_fragments = "anchor-only" + +# 403/429 from rate limiting are not broken links; everything else outside 2xx is. +accept = ["200..=299", "403", "429"] + +exclude_loopback = true +exclude_private = true +exclude_link_local = true + +exclude = [ + # Sites that answer automated requests with 999 or a sign-in wall. + '^https://(www\.)?linkedin\.com/', + '^https://(www\.)?x\.com/', + # Placeholder hosts used in code samples. + '^https?://(www\.)?example\.(com|org|net)', + '^https?://api\.example\.com', + # Pull request and comparison pages that only exist after a push. + '^https://github\.com/[^/]+/[^/]+/compare/', +] + +exclude_path = [ + "node_modules", + ".build", + "build", + "Pods", +] diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..fb6ae42 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,7 @@ +version: 1 +builder: + configs: + - platform: ios + scheme: WidgetDevelopmentKit + - platform: macos-spm + - documentation_targets: [WidgetDevelopmentKit] diff --git a/.swiftlint.yml b/.swiftlint.yml index 2bd629c..f357589 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -18,12 +18,13 @@ opt_in_rules: - redundant_nil_coalescing - sorted_first_last +included: + - Sources + - Tests + - Examples + excluded: - - Pods - .build - - Package.swift - - */Generated/* - - DerivedData identifier_name: min_length: 2 diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md deleted file mode 100644 index a8300b9..0000000 --- a/ACKNOWLEDGMENTS.md +++ /dev/null @@ -1,25 +0,0 @@ -# Acknowledgments - -## Contributors - -Thanks to everyone who has contributed to this project! - - - - - -## Built With - -- [Swift](https://swift.org) - The Swift programming language -- [SwiftUI](https://developer.apple.com/swiftui/) - Declarative UI framework -- [Swift Package Manager](https://swift.org/package-manager/) - Dependency management - -## Inspiration - -- [Alamofire](https://github.com/Alamofire/Alamofire) - Elegant HTTP networking -- [Kingfisher](https://github.com/onevcat/Kingfisher) - Image downloading and caching -- [SnapKit](https://github.com/SnapKit/SnapKit) - Auto Layout DSL - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/Assets/.gitkeep b/Assets/.gitkeep deleted file mode 100644 index 8db8e31..0000000 --- a/Assets/.gitkeep +++ /dev/null @@ -1,5 +0,0 @@ -# Widget Assets - -Add these files: -- banner.png (800x400) -- widgets-preview.png (widget screenshots) diff --git a/Assets/social-preview.html b/Assets/social-preview.html deleted file mode 100644 index 09ecba5..0000000 --- a/Assets/social-preview.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - -

iOS-Widget-Development-Kit

-

📲 WidgetKit development kit with Live Activities and dynamic widgets

-
- Swift 5.9+ - iOS 15+ - SPM - MIT -
- - diff --git a/Benchmarks/README.md b/Benchmarks/README.md deleted file mode 100644 index 0ab2879..0000000 --- a/Benchmarks/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Performance Benchmarks - -> Last updated: 2026-02-03 -> Environment: MacBook Pro M3, Xcode 15.4, Swift 5.9 - -## Methodology - -All benchmarks run on a clean build with release optimizations enabled. -Each test executes 1000 iterations and reports the median value. - -## Results - -| Operation | Time (ms) | Memory (MB) | Allocations | -|-----------|-----------|-------------|-------------| -| Initialize | 0.02 | 0.5 | 12 | -| Process (small) | 0.15 | 1.2 | 45 | -| Process (medium) | 1.8 | 4.5 | 230 | -| Process (large) | 12.3 | 18.0 | 1,200 | - -## Comparison - -| Framework | Init (ms) | Process (ms) | Memory (MB) | -|-----------|-----------|--------------|-------------| -| **This Library** | **0.02** | **1.8** | **4.5** | -| Competitor A | 0.05 | 3.2 | 8.1 | -| Competitor B | 0.08 | 2.9 | 6.3 | - -## How to Run - -```bash -swift run -c release Benchmarks --iterations 1000 -``` - ---- - -*Benchmarks are run on every release to ensure no performance regressions.* diff --git a/CHANGELOG.md b/CHANGELOG.md index ae18dc1..7eb07c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,170 +1,112 @@ # Changelog -All notable changes to this project will be documented in this file. +All notable changes to this project are documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [3.2.0] - 2024-12-15 +## [Unreleased] -### Added -- Advanced widget customization engine with 50+ templates -- Real-time data integration with WebSocket support -- Comprehensive analytics framework for widget performance tracking -- Dark mode support for all widget templates -- Accessibility improvements with VoiceOver support -- Performance optimization for widget rendering -- New weather widget template with live updates -- Calendar widget with event integration -- Fitness tracking widget with HealthKit integration -- Social media feed widget template - -### Changed -- Improved widget loading performance by 40% -- Enhanced memory management for better battery life -- Updated to iOS 16+ minimum requirement -- Refactored core architecture for better maintainability - -### Fixed -- Memory leaks in widget rendering engine -- Crash issues with live data updates -- Widget refresh timing inconsistencies -- Accessibility navigation issues - -## [3.1.0] - 2024-08-20 - -### Added -- Live data integration framework -- 25 new widget templates -- Widget analytics dashboard -- Custom widget builder interface -- Performance monitoring tools -- Error handling and logging system - -### Changed -- Optimized widget rendering engine -- Enhanced customization options -- Improved documentation - -### Fixed -- Widget refresh issues -- Memory optimization -- UI responsiveness - -## [3.0.0] - 2024-04-10 +## [2.0.0] - 2026-10-14 -### Added -- Complete widget development framework -- Core widget engine with 1000+ lines of code -- Basic widget templates (10 templates) -- Widget customization system -- Documentation and examples - -### Changed -- Initial release with core functionality -- Basic performance optimization -- Foundation architecture - -## [2.5.0] - 2023-12-15 +This is the first release in which the whole package builds and its tests run. The +manifest at the `v1.0.0` tag does not load. After a mid-June 2026 rework the library +target built, but it held only about 240 lines in six files under +`Sources/WidgetDevelopmentKit`; the rest of the Swift code was outside every target, the +manifest depended on a package no source file used, and the test target did not compile +because its Quick/Nimble tests in `Tests/Unit` used packages the manifest never declared. +The changes below restore the code from history, rework the parts that were broken, and +remove the parts that could not work. Because the public API changes substantially, this +release is a new major version. ### Added -- Widget template system -- Basic customization options -- Performance monitoring -- Error handling framework - -### Changed -- Improved widget rendering -- Enhanced user experience -- Better documentation - -## [2.0.0] - 2023-08-10 -### Added -- Core widget functionality -- Basic templates -- Documentation structure -- Test framework - -### Changed -- Initial beta release -- Foundation architecture -- Basic performance optimization - -## [1.0.0] - 2023-04-01 - -### Added -- Initial project setup -- Basic widget framework -- Core architecture -- Documentation foundation +- Unit tests with Swift Testing for the helper logic, timelines, snapshot store, deep links, + hex colors, configuration entities, data loader and retry policy, state store, Live + Activity state machine and the README examples; the iOS simulator run adds Live Activity + state tests. +- `ReadmeSyncTests`, which fail when a Swift block in README.md no longer matches the code + in `Examples/` or `ReadmeExamplesTests.swift`. +- `WidgetMath`, `WidgetFormat`, `MonthGrid` and `CountdownComponents`: pure helpers for + progress clamping, compact numbers, playback times, month grids and countdowns. +- `WidgetUpdatePolicy`, `TimelineBuilder`, `TimelineSchedule` and `WidgetSnapshotStore`, + reworked from the removed `TimelineManager`. +- `WidgetDataLoader`, `RetryPolicy` and `Endpoint`, reworked from `NetworkDataProvider`. +- `WidgetDeepLink`, `HexColor`, `RefreshIntervalOption`, `AccentColorOption` and + `WidgetDevelopmentKitIntents`, reworked from `WidgetConfiguration.swift`. +- `WidgetStateStore` and `SetValueIntent` / `RecordActionIntent` for interactive widgets. +- DocC catalog with getting-started and interactive-widgets articles, which `docs.yml` builds + and deploys to GitHub Pages. +- `Examples/WidgetKitExample`: an iOS app with a widget extension (countdown, steps and + interactive water widgets) that builds against the package in the same commit. +- README translations in Turkish, Simplified Chinese, Japanese, Korean, Spanish, German, + French, Brazilian Portuguese and Russian, with a banner, badges and buttons served from + the repository. +- `scripts/render-screenshots.sh` and an opt-in test suite that renders the README images. +- CI through the shared workflows in `muhittincamdali/.github`: `swift build` and + `swift test` on macOS, `xcodebuild` builds for iOS and macOS, tests on an iOS simulator, + DocC to GitHub Pages, link check, wording scan and README translation check. Separate + workflows build the example app and run SwiftLint. ### Changed -- First stable release -- Core functionality implementation -- Basic widget system -## [2.4.1] - 2023-10-05 +- Manifest uses Swift 6 tools and language mode, iOS 17 and macOS 14, and has no + dependencies. +- Restored the chart, Lock Screen, StandBy, Home Screen template, interactive and Live + Activity views into the `WidgetDevelopmentKit` target, split into files by theme. +- Countdown views take a `referenceDate` (the timeline entry date) instead of reading + `Date()` while rendering; the StandBy countdown no longer shows a seconds column that a + static snapshot cannot update. +- `LockScreenTimerWidget` takes a start and end time and animates with the system timer. +- `LiveActivityManager` became the `LiveActivities` namespace without a singleton, with + optional stale dates. +- `LiveActivityStateMachine` uses typed throws, reports the rejected transition and offers + `canTransition(to:)` and `allowedNextStates()`. ### Fixed -- Minor bug fixes in widget rendering -- Improved error handling for edge cases -- Documentation updates -## [2.4.0] - 2023-09-12 - -### Added -- Enhanced widget performance monitoring -- Additional customization options -- Improved accessibility features - -### Changed -- Optimized memory usage -- Enhanced test coverage - -## [2.3.2] - 2023-08-18 - -### Fixed -- Widget refresh synchronization issues -- Memory leak in background processing -- Improved stability under low memory conditions - -## [2.3.1] - 2023-07-25 - -### Fixed -- Critical bug in widget data persistence -- Improved network error handling -- Enhanced compatibility with iOS 16 - -## [2.3.0] - 2023-06-30 - -### Added -- New widget template system -- Advanced data binding capabilities -- Performance analytics dashboard - -### Changed -- Refactored core architecture -- Enhanced documentation structure - -## [2.2.0] - 2023-05-15 - -### Added -- Widget customization framework -- Enhanced error reporting -- Additional test coverage - -### Changed -- Improved performance optimization -- Updated dependencies - -## [2.1.0] - 2023-03-20 - -### Added -- Core widget functionality -- Basic template system -- Initial performance monitoring - -### Changed -- Architecture improvements -- Enhanced stability +- A string literal in `QuoteWidget` that did not compile. +- The habit tracker header read "7 Day Streak" regardless of the streaks; it now reads + "Last 7 days", which is what the dots show. +- Division by zero and `NaN` progress in fitness, water, podcast, horizontal bar and progress + ring views; force-unwrapped colors and URLs in the progress ring and shortcuts views. +- A trap in timer views when the end date was before the start date. +- `.swiftlint.yml` did not parse (an unquoted `*` started a YAML alias), so SwiftLint ran + with its default rules. +- Day counts that truncated intervals instead of counting calendar days, and month grids + that ignored the calendar's first weekday. +- Duplicate `ForEach` identifiers in weekday headers and Lock Screen stats and events. +- Network requests that were retried on 4xx responses and waited for connectivity inside the + widget extension. +- `TimerActivityView` kept counting down while `isPaused` was set; a paused timer now shows + `pausedTimeRemaining` as a fixed value. + +### Removed + +- About 10,500 lines in `Sources/Enterprise`, `Sources/Integration`, `Sources/Analytics`, + `Sources/Widgets` and `Sources/LiveData`, plus about 6,000 lines of Quick/Nimble tests, + demo app and examples, and the `Documentation/` folder that described them. The sources, + demo app and examples were not compiled by any target, the Quick/Nimble tests belonged + to a test target that could not compile, and much of the code referenced types that did + not exist. +- `WidgetConfiguration` (it shadowed SwiftUI's protocol), the no-op `WidgetDevelopmentKit` + singleton, `TimelinePrewarmer`, `WidgetGallery`, `SmartTimelineProvider` (it force-cast + `nil` to the data type), the Timer-based reload manager, and storage, migration and + metrics singletons tied to hard-coded App Group names. +- `ActionIntent`, which posted a notification inside the widget extension that the app + could never receive. +- The CocoaPods podspec, the Turkish README stub and ten CI workflows that did not build + the package. +- The repository's own code of conduct, security policy, issue forms, pull request template + and funding file; the shared files in `muhittincamdali/.github` apply instead. +- Earlier entries in this file, which listed 2.x and 3.x versions that were never tagged + and are unrelated to this release. + +## [1.0.0] - 2026-02-02 + +Tagged release. The manifest at this tag fails to load (`invalid custom path` for the test +target), so this version cannot be built. + +[Unreleased]: https://github.com/muhittincamdali/iOS-Widget-Development-Kit/compare/v2.0.0...HEAD +[2.0.0]: https://github.com/muhittincamdali/iOS-Widget-Development-Kit/compare/v1.0.0...v2.0.0 +[1.0.0]: https://github.com/muhittincamdali/iOS-Widget-Development-Kit/releases/tag/v1.0.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index 7e9ce32..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,57 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes -* Focusing on what is best for the overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information without explicit permission -* Other conduct which could reasonably be considered inappropriate - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. All complaints will be reviewed and investigated -promptly and fairly. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66c5095..bbffb37 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,56 +1,59 @@ # Contributing -Thanks for your interest in contributing! Here's how to get started. +Thanks for your interest in improving WidgetDevelopmentKit. This file has the steps that are +specific to this package; the general rules (code of conduct, security reports) are in +[muhittincamdali/.github](https://github.com/muhittincamdali/.github). -## Getting Started +## Development setup -1. Fork the repository -2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/REPO_NAME.git` -3. Create a branch: `git checkout -b feature/your-feature` -4. Make your changes -5. Push to your fork: `git push origin feature/your-feature` -6. Open a Pull Request - -## Development Setup - -```bash -# Clone the repo -git clone https://github.com/muhittincamdali/REPO_NAME.git -cd REPO_NAME - -# Open in Xcode -open Package.swift +```sh +git clone https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +cd iOS-Widget-Development-Kit +swift test ``` -## Code Style - -- Follow Swift API Design Guidelines -- Use SwiftLint (config included) -- Write descriptive commit messages -- Add documentation for public APIs - -## Commit Messages - -Use conventional commits: -- `feat:` new feature -- `fix:` bug fix -- `docs:` documentation -- `refactor:` code refactor -- `test:` tests +Open `Package.swift` in Xcode to work on the views. Lock Screen views and Live Activities +only compile for iOS, so also run the tests on a simulator before opening a pull request: -Example: `feat: add dark mode support` - -## Pull Request Process - -1. Update README if needed -2. Add tests for new features -3. Ensure CI passes -4. Request review from maintainers - -## Code of Conduct +```sh +xcodebuild test -scheme WidgetDevelopmentKit \ + -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` -Be respectful and constructive. We're all here to build great software together. +Build the example app and run SwiftLint as CI does: -## Questions? +```sh +xcodebuild build -project Examples/WidgetKitExample/WidgetKitExample.xcodeproj \ + -scheme WidgetKitExample -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO +swiftlint lint --strict +``` -Open an issue or reach out! +Local development uses Xcode 27; CI uses the default Xcode of GitHub's `macos-26` image. + +## Guidelines + +- Keep views thin. Put arithmetic and parsing in pure functions and add tests for them, + including edge cases (zero, negative, `NaN`, empty collections, DST and locale differences). +- Inject dependencies such as `UserDefaults`, clocks and network transports so code can be + tested without an App Group or a server. +- Document public API with DocC comments. `xcodebuild docbuild` should report no warnings. +- If you change a view shown in the README, regenerate the images with + `scripts/render-screenshots.sh`. +- Swift code in README.md must come from `Examples/` or + `Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift`; `ReadmeSyncTests` fails + otherwise. Change the code there first, then copy it into the README. +- README.md is translated into nine languages. Each `README..md` starts with + ``, where `` is the output of + `git hash-object README.md` for the English text it follows. After changing README.md, + update the translations and that SHA; until then the translation check in `quality.yml` + reports them as behind. +- Use [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, + `test:`, `refactor:`, `ci:`, `chore:`). + +## Pull requests + +1. Create a branch from `main`. +2. Make the change with tests. +3. Make sure CI passes: the CI workflow (macOS tests, iOS builds and simulator tests), + Documentation, Examples, Lint and Quality (links, wording, translations). +4. Describe what changed and why in the pull request. diff --git a/Demo/WidgetKitDemo/Managers/WidgetKitManager.swift b/Demo/WidgetKitDemo/Managers/WidgetKitManager.swift deleted file mode 100644 index a650a81..0000000 --- a/Demo/WidgetKitDemo/Managers/WidgetKitManager.swift +++ /dev/null @@ -1,379 +0,0 @@ -import Foundation -import SwiftUI -import WidgetKit -import Combine - -/// Manager class for handling all widget-related operations in the demo app -@available(iOS 16.0, *) -@MainActor -public class WidgetKitManager: ObservableObject { - - // MARK: - Published Properties - - @Published public var availableWidgets: [WidgetModel] = [] - @Published public var activeWidgets: [WidgetModel] = [] - @Published public var enableHomeScreen: Bool = true - @Published public var enableLockScreen: Bool = true - @Published public var enableLiveActivities: Bool = true - @Published public var enableDynamicIsland: Bool = true - @Published public var isConfigured: Bool = false - - // MARK: - Private Properties - - private var configuration = WidgetConfiguration() - private var cancellables = Set() - - // MARK: - Initialization - - public init() { - loadAvailableWidgets() - setupObservers() - } - - // MARK: - Public Methods - - /// Configures the widget manager with custom settings - public func configure(_ block: (inout WidgetConfiguration) -> Void) { - block(&configuration) - isConfigured = true - applyConfiguration() - } - - /// Creates a new widget with specified parameters - public func createWidget(type: WidgetType, size: WidgetSize, content: AnyView) async throws -> WidgetModel { - let widget = WidgetModel( - id: UUID().uuidString, - type: type, - size: size, - title: "New Widget", - subtitle: "Custom widget", - icon: "square.grid.2x2", - isActive: false, - lastUpdated: Date() - ) - - availableWidgets.append(widget) - - // Request widget timeline reload - WidgetCenter.shared.reloadAllTimelines() - - return widget - } - - /// Activates a widget - public func activateWidget(_ widget: WidgetModel) async throws { - guard let index = availableWidgets.firstIndex(where: { $0.id == widget.id }) else { - throw WidgetError.widgetNotFound - } - - availableWidgets[index].isActive = true - activeWidgets.append(availableWidgets[index]) - - // Update widget timeline - WidgetCenter.shared.reloadTimelines(ofKind: widget.kind) - } - - /// Deactivates a widget - public func deactivateWidget(_ widget: WidgetModel) async throws { - guard let availableIndex = availableWidgets.firstIndex(where: { $0.id == widget.id }) else { - throw WidgetError.widgetNotFound - } - - availableWidgets[availableIndex].isActive = false - activeWidgets.removeAll { $0.id == widget.id } - - // Update widget timeline - WidgetCenter.shared.reloadTimelines(ofKind: widget.kind) - } - - /// Updates widget content - public func updateWidget(_ widget: WidgetModel, with data: WidgetData) async throws { - guard let index = availableWidgets.firstIndex(where: { $0.id == widget.id }) else { - throw WidgetError.widgetNotFound - } - - availableWidgets[index].lastUpdated = Date() - - // Store widget data for timeline provider - UserDefaults.standard.set(try? JSONEncoder().encode(data), forKey: "widget_data_\(widget.id)") - - // Reload widget timeline - WidgetCenter.shared.reloadTimelines(ofKind: widget.kind) - } - - /// Refreshes all widgets - public func refreshAllWidgets() async { - WidgetCenter.shared.reloadAllTimelines() - - for index in availableWidgets.indices { - availableWidgets[index].lastUpdated = Date() - } - } - - /// Gets widget statistics - public func getWidgetStatistics() -> WidgetStatistics { - return WidgetStatistics( - totalWidgets: availableWidgets.count, - activeWidgets: activeWidgets.count, - homeScreenWidgets: availableWidgets.filter { $0.type == .homeScreen }.count, - lockScreenWidgets: availableWidgets.filter { $0.type == .lockScreen }.count, - liveActivities: availableWidgets.filter { $0.type == .liveActivity }.count - ) - } - - // MARK: - Private Methods - - private func loadAvailableWidgets() { - // Load sample widgets for demo - availableWidgets = [ - WidgetModel( - id: "weather", - type: .homeScreen, - size: .medium, - title: "Weather Widget", - subtitle: "Current weather conditions", - icon: "cloud.sun.fill", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "calendar", - type: .homeScreen, - size: .small, - title: "Calendar Widget", - subtitle: "Upcoming events", - icon: "calendar", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "fitness", - type: .lockScreen, - size: .small, - title: "Fitness Widget", - subtitle: "Activity rings", - icon: "figure.walk", - isActive: false, - lastUpdated: Date() - ), - WidgetModel( - id: "stocks", - type: .homeScreen, - size: .large, - title: "Stocks Widget", - subtitle: "Market overview", - icon: "chart.line.uptrend.xyaxis", - isActive: false, - lastUpdated: Date() - ), - WidgetModel( - id: "music", - type: .lockScreen, - size: .small, - title: "Music Widget", - subtitle: "Now playing", - icon: "music.note", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "news", - type: .homeScreen, - size: .medium, - title: "News Widget", - subtitle: "Top headlines", - icon: "newspaper", - isActive: false, - lastUpdated: Date() - ), - WidgetModel( - id: "battery", - type: .lockScreen, - size: .small, - title: "Battery Widget", - subtitle: "Device battery status", - icon: "battery.75", - isActive: true, - lastUpdated: Date() - ), - WidgetModel( - id: "reminders", - type: .homeScreen, - size: .small, - title: "Reminders Widget", - subtitle: "Today's tasks", - icon: "checklist", - isActive: false, - lastUpdated: Date() - ) - ] - - // Filter active widgets - activeWidgets = availableWidgets.filter { $0.isActive } - } - - private func setupObservers() { - // Observe widget configuration changes - NotificationCenter.default.publisher(for: .widgetConfigurationChanged) - .sink { [weak self] _ in - Task { @MainActor in - await self?.refreshAllWidgets() - } - } - .store(in: &cancellables) - } - - private func applyConfiguration() { - enableHomeScreen = configuration.enableHomeScreenWidgets - enableLockScreen = configuration.enableLockScreenWidgets - enableLiveActivities = configuration.enableLiveActivities - enableDynamicIsland = configuration.enableDynamicIsland - } -} - -// MARK: - Supporting Types - -@available(iOS 16.0, *) -public struct WidgetModel: Identifiable, Codable { - public let id: String - public let type: WidgetType - public let size: WidgetSize - public let title: String - public let subtitle: String - public let icon: String - public var isActive: Bool - public var lastUpdated: Date - - public var kind: String { - return "com.widgetkit.demo.\(id)" - } -} - -public enum WidgetType: String, Codable, CaseIterable { - case homeScreen = "home_screen" - case lockScreen = "lock_screen" - case standBy = "stand_by" - case liveActivity = "live_activity" - case dynamicIsland = "dynamic_island" - - public var displayName: String { - switch self { - case .homeScreen: return "Home Screen" - case .lockScreen: return "Lock Screen" - case .standBy: return "StandBy" - case .liveActivity: return "Live Activity" - case .dynamicIsland: return "Dynamic Island" - } - } -} - -public enum WidgetSize: String, Codable, CaseIterable { - case small = "small" - case medium = "medium" - case large = "large" - case extraLarge = "extra_large" - - public var displayName: String { - switch self { - case .small: return "Small" - case .medium: return "Medium" - case .large: return "Large" - case .extraLarge: return "Extra Large" - } - } - - public var dimensions: CGSize { - switch self { - case .small: return CGSize(width: 155, height: 155) - case .medium: return CGSize(width: 329, height: 155) - case .large: return CGSize(width: 329, height: 345) - case .extraLarge: return CGSize(width: 329, height: 382) - } - } -} - -public struct WidgetConfiguration { - public var enableHomeScreenWidgets: Bool = true - public var enableLockScreenWidgets: Bool = true - public var enableLiveActivities: Bool = true - public var enableDynamicIsland: Bool = true - public var refreshInterval: TimeInterval = 300 // 5 minutes - public var enableBackgroundRefresh: Bool = true - public var enableInteractions: Bool = true - public var enableDeepLinking: Bool = true -} - -public struct WidgetData: Codable { - public let content: [String: Any] - public let timestamp: Date - - enum CodingKeys: String, CodingKey { - case content - case timestamp - } - - public init(content: [String: Any], timestamp: Date = Date()) { - self.content = content - self.timestamp = timestamp - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - timestamp = try container.decode(Date.self, forKey: .timestamp) - - // Decode content as Data and convert to dictionary - if let data = try? container.decode(Data.self, forKey: .content), - let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { - content = dict - } else { - content = [:] - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(timestamp, forKey: .timestamp) - - // Encode content dictionary as Data - if let data = try? JSONSerialization.data(withJSONObject: content) { - try container.encode(data, forKey: .content) - } - } -} - -public struct WidgetStatistics { - public let totalWidgets: Int - public let activeWidgets: Int - public let homeScreenWidgets: Int - public let lockScreenWidgets: Int - public let liveActivities: Int -} - -public enum WidgetError: LocalizedError { - case widgetNotFound - case invalidConfiguration - case activationFailed - case updateFailed - - public var errorDescription: String? { - switch self { - case .widgetNotFound: - return "Widget not found" - case .invalidConfiguration: - return "Invalid widget configuration" - case .activationFailed: - return "Failed to activate widget" - case .updateFailed: - return "Failed to update widget" - } - } -} - -// MARK: - Notifications - -extension Notification.Name { - static let widgetConfigurationChanged = Notification.Name("widgetConfigurationChanged") - static let widgetActivated = Notification.Name("widgetActivated") - static let widgetDeactivated = Notification.Name("widgetDeactivated") - static let widgetUpdated = Notification.Name("widgetUpdated") -} \ No newline at end of file diff --git a/Demo/WidgetKitDemo/WidgetKitDemoApp.swift b/Demo/WidgetKitDemo/WidgetKitDemoApp.swift deleted file mode 100644 index 191e025..0000000 --- a/Demo/WidgetKitDemo/WidgetKitDemoApp.swift +++ /dev/null @@ -1,743 +0,0 @@ -import SwiftUI -import WidgetKit - -/// Main demo application showcasing all iOS Widget Development Kit features -@main -@available(iOS 16.0, *) -struct WidgetKitDemoApp: App { - @StateObject private var widgetManager = WidgetKitManager() - @StateObject private var themeManager = ThemeManager() - @StateObject private var dataManager = DataManager() - @StateObject private var performanceMonitor = PerformanceMonitor.shared - - init() { - setupApplication() - } - - var body: some Scene { - WindowGroup { - ContentView() - .environmentObject(widgetManager) - .environmentObject(themeManager) - .environmentObject(dataManager) - .environmentObject(performanceMonitor) - .onAppear { - Task { - await initializeFramework() - } - } - } - } - - private func setupApplication() { - // Configure appearance - UINavigationBar.appearance().largeTitleTextAttributes = [ - .foregroundColor: UIColor.label, - .font: UIFont.systemFont(ofSize: 34, weight: .bold) - ] - - // Configure tab bar - UITabBar.appearance().backgroundColor = UIColor.systemBackground - UITabBar.appearance().tintColor = UIColor.systemBlue - } - - private func initializeFramework() async { - // Initialize security - let encryptionManager = EncryptionManager() - let authManager = AuthenticationManager() - let privacyManager = PrivacyManager(encryptionManager: encryptionManager) - - // Initialize performance monitoring - do { - try await performanceMonitor.startMonitoring() - } catch { - print("Failed to start performance monitoring: \(error)") - } - - // Configure widget manager - widgetManager.configure { config in - config.enableHomeScreenWidgets = true - config.enableLockScreenWidgets = true - config.enableLiveActivities = true - config.enableDynamicIsland = true - config.refreshInterval = 300 // 5 minutes - } - - // Setup data sources - await dataManager.setupDataSources() - - // Request necessary permissions - await requestPermissions() - - // Register for remote notifications - await registerForNotifications() - } - - private func requestPermissions() async { - // Request privacy permissions - let privacyManager = PrivacyManager(encryptionManager: EncryptionManager()) - - let purposes: [PrivacyManager.DataPurpose] = [ - .widgetFunctionality, - .performanceAnalytics, - .usageAnalytics, - .personalization - ] - - do { - _ = try await privacyManager.requestConsent(for: purposes) - } catch { - print("Consent request failed: \(error)") - } - } - - private func registerForNotifications() async { - let center = UNUserNotificationCenter.current() - - do { - let granted = try await center.requestAuthorization(options: [.alert, .badge, .sound]) - if granted { - await UIApplication.shared.registerForRemoteNotifications() - } - } catch { - print("Notification authorization failed: \(error)") - } - } -} - -// MARK: - Content View - -@available(iOS 16.0, *) -struct ContentView: View { - @State private var selectedTab = 0 - @EnvironmentObject var widgetManager: WidgetKitManager - @EnvironmentObject var themeManager: ThemeManager - - var body: some View { - TabView(selection: $selectedTab) { - HomeView() - .tabItem { - Label("Home", systemImage: "house.fill") - } - .tag(0) - - WidgetsView() - .tabItem { - Label("Widgets", systemImage: "square.grid.2x2.fill") - } - .tag(1) - - LiveActivitiesView() - .tabItem { - Label("Live", systemImage: "bolt.circle.fill") - } - .tag(2) - - AnalyticsView() - .tabItem { - Label("Analytics", systemImage: "chart.line.uptrend.xyaxis") - } - .tag(3) - - SettingsView() - .tabItem { - Label("Settings", systemImage: "gear") - } - .tag(4) - } - .accentColor(themeManager.currentTheme.accentColor) - } -} - -// MARK: - Home View - -@available(iOS 16.0, *) -struct HomeView: View { - @EnvironmentObject var widgetManager: WidgetKitManager - @State private var showingWelcome = true - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 20) { - // Welcome Card - WelcomeCard() - .padding(.horizontal) - - // Quick Actions - QuickActionsSection() - .padding(.horizontal) - - // Featured Widgets - FeaturedWidgetsSection() - .padding(.horizontal) - - // Recent Activity - RecentActivitySection() - .padding(.horizontal) - - // Performance Overview - PerformanceOverviewCard() - .padding(.horizontal) - } - .padding(.vertical) - } - .navigationTitle("Widget Kit Demo") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - // Show notifications - }) { - Image(systemName: "bell.badge") - } - } - } - } - } -} - -// MARK: - Widgets View - -@available(iOS 16.0, *) -struct WidgetsView: View { - @EnvironmentObject var widgetManager: WidgetKitManager - @State private var selectedCategory = "All" - - let categories = ["All", "Home Screen", "Lock Screen", "StandBy", "Custom"] - - var body: some View { - NavigationView { - VStack { - // Category Picker - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(categories, id: \.self) { category in - CategoryChip( - title: category, - isSelected: selectedCategory == category - ) { - selectedCategory = category - } - } - } - .padding(.horizontal) - } - .padding(.vertical) - - // Widget Grid - ScrollView { - LazyVGrid(columns: [ - GridItem(.flexible()), - GridItem(.flexible()) - ], spacing: 16) { - ForEach(widgetManager.availableWidgets) { widget in - WidgetCard(widget: widget) - } - } - .padding() - } - } - .navigationTitle("Widgets") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Add") { - // Show widget creation - } - } - } - } - } -} - -// MARK: - Live Activities View - -@available(iOS 16.0, *) -struct LiveActivitiesView: View { - @State private var activeLiveActivities: [LiveActivityItem] = [] - @State private var showingCreateActivity = false - - var body: some View { - NavigationView { - VStack { - if activeLiveActivities.isEmpty { - EmptyStateView( - icon: "bolt.circle", - title: "No Active Live Activities", - description: "Start a Live Activity to see real-time updates here" - ) { - showingCreateActivity = true - } - } else { - ScrollView { - VStack(spacing: 16) { - ForEach(activeLiveActivities) { activity in - LiveActivityCard(activity: activity) - .padding(.horizontal) - } - } - .padding(.vertical) - } - } - } - .navigationTitle("Live Activities") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - showingCreateActivity = true - }) { - Image(systemName: "plus.circle.fill") - } - } - } - .sheet(isPresented: $showingCreateActivity) { - CreateLiveActivityView() - } - } - } -} - -// MARK: - Analytics View - -@available(iOS 16.0, *) -struct AnalyticsView: View { - @EnvironmentObject var performanceMonitor: PerformanceMonitor - @State private var selectedTimeRange = "Today" - @State private var selectedMetric = "All" - - let timeRanges = ["Today", "Week", "Month", "Year"] - let metrics = ["All", "CPU", "Memory", "Battery", "Network"] - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 20) { - // Time Range Selector - Picker("Time Range", selection: $selectedTimeRange) { - ForEach(timeRanges, id: \.self) { range in - Text(range).tag(range) - } - } - .pickerStyle(SegmentedPickerStyle()) - .padding(.horizontal) - - // Performance Score Card - PerformanceScoreCard(score: performanceMonitor.performanceScore) - .padding(.horizontal) - - // Metrics Charts - MetricsChartsSection(selectedMetric: selectedMetric) - .padding(.horizontal) - - // Detailed Metrics - DetailedMetricsSection() - .padding(.horizontal) - - // Recommendations - RecommendationsSection() - .padding(.horizontal) - } - .padding(.vertical) - } - .navigationTitle("Analytics") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Export") { - // Export analytics data - } - } - } - } - } -} - -// MARK: - Settings View - -@available(iOS 16.0, *) -struct SettingsView: View { - @EnvironmentObject var themeManager: ThemeManager - @EnvironmentObject var widgetManager: WidgetKitManager - @State private var enableNotifications = true - @State private var enableAnalytics = true - @State private var enableAutoUpdate = true - @State private var selectedTheme = "System" - - var body: some View { - NavigationView { - Form { - // Appearance Section - Section("Appearance") { - Picker("Theme", selection: $selectedTheme) { - Text("System").tag("System") - Text("Light").tag("Light") - Text("Dark").tag("Dark") - } - - ColorPicker("Accent Color", selection: $themeManager.accentColor) - - Toggle("Dynamic Colors", isOn: $themeManager.enableDynamicColors) - } - - // Widget Settings - Section("Widget Settings") { - Toggle("Home Screen Widgets", isOn: $widgetManager.enableHomeScreen) - Toggle("Lock Screen Widgets", isOn: $widgetManager.enableLockScreen) - Toggle("Live Activities", isOn: $widgetManager.enableLiveActivities) - Toggle("Dynamic Island", isOn: $widgetManager.enableDynamicIsland) - - HStack { - Text("Refresh Interval") - Spacer() - Text("5 min") - .foregroundColor(.secondary) - } - } - - // Privacy & Security - Section("Privacy & Security") { - NavigationLink("Privacy Settings") { - PrivacySettingsView() - } - - NavigationLink("Security Settings") { - SecuritySettingsView() - } - - Toggle("Analytics", isOn: $enableAnalytics) - } - - // Notifications - Section("Notifications") { - Toggle("Enable Notifications", isOn: $enableNotifications) - - NavigationLink("Notification Preferences") { - NotificationPreferencesView() - } - } - - // Data Management - Section("Data Management") { - NavigationLink("Cache Management") { - CacheManagementView() - } - - Toggle("Auto Update", isOn: $enableAutoUpdate) - - Button("Clear All Data") { - // Clear data action - } - .foregroundColor(.red) - } - - // About - Section("About") { - HStack { - Text("Version") - Spacer() - Text("1.0.0") - .foregroundColor(.secondary) - } - - NavigationLink("Documentation") { - DocumentationView() - } - - NavigationLink("Support") { - SupportView() - } - - NavigationLink("License") { - LicenseView() - } - } - } - .navigationTitle("Settings") - } - } -} - -// MARK: - Supporting Views - -@available(iOS 16.0, *) -struct WelcomeCard: View { - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Welcome to") - .font(.headline) - .foregroundColor(.secondary) - Text("Widget Kit Demo") - .font(.largeTitle) - .fontWeight(.bold) - } - Spacer() - Image(systemName: "sparkles") - .font(.system(size: 40)) - .foregroundColor(.blue) - } - - Text("Explore the most advanced widget development framework for iOS") - .font(.body) - .foregroundColor(.secondary) - - HStack(spacing: 20) { - StatItem(value: "50+", label: "Widgets") - StatItem(value: "15K+", label: "Lines") - StatItem(value: "95%", label: "Coverage") - } - } - .padding() - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(16) - } -} - -@available(iOS 16.0, *) -struct QuickActionsSection: View { - let actions = [ - QuickAction(icon: "plus.app", title: "Create Widget", color: .blue), - QuickAction(icon: "bolt.fill", title: "Start Live Activity", color: .orange), - QuickAction(icon: "chart.line.uptrend.xyaxis", title: "View Analytics", color: .green), - QuickAction(icon: "gear", title: "Settings", color: .gray) - ] - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("Quick Actions") - .font(.headline) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(actions) { action in - QuickActionButton(action: action) - } - } - } - } - } -} - -@available(iOS 16.0, *) -struct FeaturedWidgetsSection: View { - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Featured Widgets") - .font(.headline) - Spacer() - Button("See All") { - // Navigate to widgets - } - .font(.caption) - } - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 16) { - ForEach(0..<5) { index in - FeaturedWidgetCard(index: index) - } - } - } - } - } -} - -@available(iOS 16.0, *) -struct RecentActivitySection: View { - var body: some View { - VStack(alignment: .leading, spacing: 12) { - Text("Recent Activity") - .font(.headline) - - VStack(spacing: 8) { - ActivityRow(icon: "square.grid.2x2", title: "Weather Widget Updated", time: "2 min ago") - ActivityRow(icon: "bolt.circle", title: "Delivery Tracking Started", time: "15 min ago") - ActivityRow(icon: "chart.bar", title: "Analytics Report Generated", time: "1 hour ago") - } - .padding() - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(12) - } - } -} - -// MARK: - Helper Views - -@available(iOS 16.0, *) -struct StatItem: View { - let value: String - let label: String - - var body: some View { - VStack(spacing: 4) { - Text(value) - .font(.title2) - .fontWeight(.bold) - Text(label) - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -struct QuickAction: Identifiable { - let id = UUID() - let icon: String - let title: String - let color: Color -} - -@available(iOS 16.0, *) -struct QuickActionButton: View { - let action: QuickAction - - var body: some View { - VStack(spacing: 8) { - Image(systemName: action.icon) - .font(.system(size: 24)) - .foregroundColor(.white) - .frame(width: 50, height: 50) - .background(action.color) - .cornerRadius(12) - - Text(action.title) - .font(.caption) - .multilineTextAlignment(.center) - .frame(width: 70) - } - } -} - -@available(iOS 16.0, *) -struct FeaturedWidgetCard: View { - let index: Int - - var body: some View { - VStack { - RoundedRectangle(cornerRadius: 12) - .fill(Color.blue.opacity(0.2)) - .frame(width: 150, height: 150) - .overlay( - Image(systemName: "square.grid.2x2") - .font(.system(size: 40)) - .foregroundColor(.blue) - ) - - Text("Widget \(index + 1)") - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -struct ActivityRow: View { - let icon: String - let title: String - let time: String - - var body: some View { - HStack { - Image(systemName: icon) - .foregroundColor(.blue) - .frame(width: 30) - - Text(title) - .font(.subheadline) - - Spacer() - - Text(time) - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Supporting Types - -@available(iOS 16.0, *) -struct LiveActivityItem: Identifiable { - let id = UUID() - let title: String - let status: String - let progress: Double -} - -@available(iOS 16.0, *) -struct CategoryChip: View { - let title: String - let isSelected: Bool - let action: () -> Void - - var body: some View { - Button(action: action) { - Text(title) - .font(.subheadline) - .fontWeight(isSelected ? .semibold : .regular) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background(isSelected ? Color.blue : Color(UIColor.secondarySystemBackground)) - .foregroundColor(isSelected ? .white : .primary) - .cornerRadius(20) - } - } -} - -// Additional placeholder views for navigation destinations -@available(iOS 16.0, *) -struct CreateLiveActivityView: View { - var body: some View { - Text("Create Live Activity") - } -} - -@available(iOS 16.0, *) -struct PrivacySettingsView: View { - var body: some View { - Text("Privacy Settings") - } -} - -@available(iOS 16.0, *) -struct SecuritySettingsView: View { - var body: some View { - Text("Security Settings") - } -} - -@available(iOS 16.0, *) -struct NotificationPreferencesView: View { - var body: some View { - Text("Notification Preferences") - } -} - -@available(iOS 16.0, *) -struct CacheManagementView: View { - var body: some View { - Text("Cache Management") - } -} - -@available(iOS 16.0, *) -struct DocumentationView: View { - var body: some View { - Text("Documentation") - } -} - -@available(iOS 16.0, *) -struct SupportView: View { - var body: some View { - Text("Support") - } -} - -@available(iOS 16.0, *) -struct LicenseView: View { - var body: some View { - Text("License") - } -} \ No newline at end of file diff --git a/Documentation/API-Reference.md b/Documentation/API-Reference.md deleted file mode 100644 index 167ca79..0000000 --- a/Documentation/API-Reference.md +++ /dev/null @@ -1,851 +0,0 @@ -# Comprehensive API Reference — iOS Widget Development Kit - -## Overview - -This comprehensive API reference covers all classes, methods, properties, and protocols available in the iOS Widget Development Kit. The framework provides enterprise-grade functionality for creating, managing, and optimizing iOS widgets with advanced features including AI/ML integration, security, performance monitoring, and multi-cloud support. - -## Architecture Overview - -The iOS Widget Development Kit is organized into distinct layers: - -- **Core Layer**: Fundamental widget management and development tools -- **Security Layer**: Authentication, encryption, and privacy management -- **Performance Layer**: Monitoring, optimization, and caching systems -- **Enterprise Layer**: AI/ML, analytics, and business intelligence -- **Integration Layer**: APIs, cloud services, and data source management - -## Core API - -### WidgetDevelopmentManager - -The primary entry point for all widget development operations. - -```swift -@available(iOS 16.0, *) -public class WidgetDevelopmentManager: ObservableObject -``` - -#### Properties - -```swift -public static let shared: WidgetDevelopmentManager -public private(set) var isInitialized: Bool -public private(set) var activeWidgets: [Widget] -public private(set) var widgetTemplates: [WidgetTemplate] -public private(set) var configuration: WidgetConfiguration -``` - -#### Methods - -##### Initialization - -```swift -public func initialize(configuration: WidgetConfiguration) async throws -``` -Initializes the widget development manager with the specified configuration. - -**Parameters:** -- `configuration`: The configuration object containing setup parameters - -**Throws:** `WidgetError.configurationInvalid` if configuration is invalid - -##### Widget Creation - -```swift -public func createWidget( - with configuration: WidgetConfiguration, - template: WidgetTemplate? = nil -) async throws -> Widget -``` -Creates a new widget with the specified configuration and optional template. - -**Parameters:** -- `configuration`: Widget configuration specifying type, size, and behavior -- `template`: Optional template to use as a starting point - -**Returns:** The created `Widget` instance - -**Throws:** -- `WidgetError.invalidConfiguration` -- `WidgetError.templateNotFound` -- `WidgetError.creationFailed` - -##### Widget Management - -```swift -public func updateWidget( - id: String, - data: WidgetData, - options: UpdateOptions = UpdateOptions() -) async throws - -public func deleteWidget(id: String) async throws - -public func getWidget(id: String) -> Widget? - -public func getAllWidgets() -> [Widget] -``` - -##### Template Management - -```swift -public func registerTemplate(_ template: WidgetTemplate) async throws - -public func getAvailableTemplates() -> [WidgetTemplate] - -public func getTemplate(named name: String) -> WidgetTemplate? -``` - -### Widget - -The core widget class representing individual widget instances. - -```swift -@available(iOS 16.0, *) -public class Widget: ObservableObject, Identifiable -``` - -#### Properties - -```swift -public let id: String -public let type: WidgetType -public let size: WidgetSize -public private(set) var configuration: WidgetConfiguration -public private(set) var data: WidgetData? -public private(set) var state: WidgetState -public private(set) var lastUpdated: Date -public private(set) var metrics: WidgetMetrics -``` - -#### Methods - -```swift -public func loadData() async throws -> WidgetData -public func refreshData() async throws -public func render() async throws -public func updateConfiguration(_ newConfiguration: WidgetConfiguration) async throws -public func getMetrics() async -> WidgetMetrics -``` - -### WidgetConfiguration - -Configuration object for widget creation and customization. - -```swift -public struct WidgetConfiguration: Codable, Equatable -``` - -#### Properties - -```swift -public var type: WidgetType -public var size: WidgetSize -public var updateInterval: TimeInterval -public var displayName: String -public var backgroundColor: UIColor -public var textColor: UIColor -public var enableAnimations: Bool -public var enableInteractions: Bool -public var dataSource: DataSourceConfiguration? -public var customProperties: [String: Any] -``` - -#### Enumerations - -##### WidgetType - -```swift -public enum WidgetType: String, CaseIterable, Codable { - case weather = "weather" - case calendar = "calendar" - case news = "news" - case fitness = "fitness" - case social = "social" - case productivity = "productivity" - case entertainment = "entertainment" - case custom = "custom" -} -``` - -##### WidgetSize - -```swift -public enum WidgetSize: String, CaseIterable, Codable { - case small = "small" - case medium = "medium" - case large = "large" - case extraLarge = "extra_large" - case accessoryCircular = "accessory_circular" - case accessoryRectangular = "accessory_rectangular" - case accessoryInline = "accessory_inline" -} -``` - -##### WidgetState - -```swift -public enum WidgetState: String, Codable { - case inactive = "inactive" - case loading = "loading" - case active = "active" - case error = "error" - case updating = "updating" -} -``` - -## Security API - -### EncryptionManager - -Enterprise-grade encryption management with AES-256-GCM and Secure Enclave integration. - -```swift -@available(iOS 16.0, *) -public class EncryptionManager: ObservableObject -``` - -#### Methods - -```swift -public func encryptData( - _ data: Data, - keyIdentifier: String? = nil, - requireBiometric: Bool = false -) async throws -> EncryptedData - -public func decryptData( - _ encryptedData: EncryptedData, - requireBiometric: Bool = false -) async throws -> Data - -public func generateSecureKey(identifier: String) async throws -> SecretKey - -public func rotateKeys() async throws -``` - -### AuthenticationManager - -Multi-factor authentication with biometric support. - -```swift -@available(iOS 16.0, *) -public class AuthenticationManager: ObservableObject -``` - -#### Methods - -```swift -public func authenticate( - reason: String? = nil, - allowFallback: Bool = true -) async throws -> AuthenticationResult - -public func validateSession(_ sessionId: String? = nil) async throws -> Bool - -public func createSession( - for userId: String, - permissions: [Permission] = [] -) async throws -> Session - -public func revokeSession(_ sessionId: String) async throws -``` - -### PrivacyManager - -GDPR, CCPA, and HIPAA compliance management. - -```swift -@available(iOS 16.0, *) -public class PrivacyManager: ObservableObject -``` - -#### Methods - -```swift -public func processData( - _ data: T, - for purpose: DataPurpose, - subjectId: String? = nil -) async throws -> ProcessedData - -public func processDataSubjectRequest( - _ request: DataSubjectRequest -) async throws -> DataSubjectResponse - -public func generateConsentForm( - for purposes: [DataPurpose] -) async -> ConsentForm - -public func validateGDPRCompliance() async throws -> ComplianceResult -public func validateCCPACompliance() async throws -> ComplianceResult -public func validateHIPAACompliance() async throws -> ComplianceResult -``` - -## Performance API - -### PerformanceMonitor - -Real-time performance metrics collection and monitoring. - -```swift -@available(iOS 16.0, *) -public class PerformanceMonitor: ObservableObject -``` - -#### Methods - -```swift -public func measureExecutionTime( - _ operation: () async throws -> T, - metricType: MetricType = .renderTime -) async throws -> T - -public func getCurrentMetrics() -> [String: PerformanceMetric] - -public func generateReport( - for period: DateInterval -) async throws -> PerformanceReport - -public func setThreshold( - _ threshold: Double, - for metricType: MetricType -) async - -public func enableAlerts(for metricTypes: [MetricType]) async -``` - -### CacheManager - -Multi-level caching system with intelligent cache policies. - -```swift -@available(iOS 16.0, *) -public class CacheManager: ObservableObject -``` - -#### Methods - -```swift -public func store( - _ object: T, - forKey key: String, - ttl: TimeInterval? = nil, - policy: CachePolicy = .cacheFirst -) async throws - -public func retrieve( - _ type: T.Type, - forKey key: String, - policy: CachePolicy = .cacheFirst -) async throws -> T - -public func removeObject(forKey key: String) async - -public func clearCache(policy: ClearPolicy = .expired) async - -public func getCacheStatistics() async -> CacheStatistics -``` - -### OptimizationEngine - -AI-driven performance optimization with automated tuning. - -```swift -@available(iOS 16.0, *) -public class OptimizationEngine: ObservableObject -``` - -#### Methods - -```swift -public func analyzeAndOptimize() async throws -> [Optimization] - -public func executeOptimization(_ optimization: Optimization) async throws -> OptimizationResult - -public func optimizeMemory() async throws -public func optimizeCPU() async throws -public func optimizeBattery() async throws - -public func getOptimizationInsights() async -> OptimizationInsights -``` - -## Enterprise API - -### AIManager - -Core ML integration for intelligent widget optimization and personalization. - -```swift -@available(iOS 16.0, *) -public class AIManager: ObservableObject -``` - -#### Methods - -```swift -public func predict( - modelType: ModelType, - features: [String: Double], - metadata: [String: String] = [:] -) async throws -> PredictionResult - -public func analyzeUserBehavior( - userId: String, - interactions: [UserInteraction], - context: [String: String] = [:] -) async throws -> UserBehaviorProfile - -public func getContentRecommendations( - for userId: String, - contentTypes: [String] = [], - limit: Int = 10 -) async throws -> [ContentRecommendation] - -public func trainModel( - modelType: ModelType, - trainingData: [[String: Double]], - labels: [Double] -) async throws -``` - -### MLAnalyticsEngine - -Advanced analytics with user segmentation and forecasting. - -```swift -@available(iOS 16.0, *) -public class MLAnalyticsEngine: ObservableObject -``` - -#### Methods - -```swift -public func performUserSegmentation( - criteria: [String: Any] = [:] -) async throws -> [UserSegment] - -public func forecastMetric( - metric: String, - forecastPeriod: TimeInterval -) async throws -> TimeSeriesForecast - -public func detectAnomalies( - in data: [DataPoint], - sensitivity: Double = 0.95 -) async throws -> [AnomalyDetection] - -public func generateInsights( - for segment: UserSegment -) async throws -> [BusinessInsight] -``` - -### AdvancedAnalytics - -Executive dashboards and business intelligence. - -```swift -@available(iOS 16.0, *) -public class AdvancedAnalytics: ObservableObject -``` - -#### Methods - -```swift -public func createDashboard( - name: String, - type: DashboardType, - description: String -) async throws -> Dashboard - -public func generateReport( - type: AnalyticsReport.ReportType, - period: DateInterval -) async throws -> AnalyticsReport - -public func executeQuery( - _ query: AnalyticsQuery -) async throws -> QueryResult - -public func scheduleReport( - report: AnalyticsReport, - schedule: ReportSchedule -) async throws -> ScheduledReport -``` - -## Integration API - -### APIManager - -Enterprise-grade API management with comprehensive security and monitoring. - -```swift -@available(iOS 16.0, *) -public class APIManager: ObservableObject -``` - -#### Methods - -```swift -public func request( - _ request: APIRequest, - responseType: T.Type -) async throws -> APIResponse - -public func graphQL( - _ request: GraphQLRequest, - responseType: T.Type -) async throws -> APIResponse - -public func connectWebSocket( - config: WebSocketConfig, - onMessage: @escaping (Data) -> Void -) async throws -> WebSocketConnection - -public func uploadFile( - data: Data, - to endpoint: String, - metadata: [String: String] = [:] -) async throws -> FileUploadResponse -``` - -### CloudIntegration - -Multi-cloud support with hybrid deployment strategies. - -```swift -@available(iOS 16.0, *) -public class CloudIntegration: ObservableObject -``` - -#### Methods - -```swift -public func upload( - data: Data, - to path: String, - provider: CloudProvider? = nil, - metadata: [String: String] = [:] -) async throws -> CloudFile - -public func download( - path: String, - provider: CloudProvider? = nil -) async throws -> Data - -public func synchronize(paths: [String] = []) async throws - -public func migrate( - from sourceProvider: CloudProvider, - to targetProvider: CloudProvider, - paths: [String] = [] -) async throws -``` - -### DataSourceManager - -Unified data source management with real-time streaming. - -```swift -@available(iOS 16.0, *) -public class DataSourceManager: ObservableObject -``` - -#### Methods - -```swift -public func registerDataSource(_ configuration: DataSourceConfiguration) async throws - -public func executeQuery( - _ query: DataQuery, - responseType: T.Type -) async throws -> DataResponse - -public func streamData( - from dataSourceName: String, - query: String? = nil, - responseType: T.Type -) -> AsyncThrowingStream, Error> - -public func writeData( - _ data: T, - to dataSourceName: String, - path: String? = nil, - options: WriteOptions = WriteOptions() -) async throws -> WriteResult -``` - -## Data Models - -### Core Data Models - -#### WidgetData - -Base protocol for all widget data types. - -```swift -public protocol WidgetData: Codable, Equatable { - var id: String { get } - var timestamp: Date { get } - var metadata: [String: String] { get } -} -``` - -#### WeatherData - -Weather-specific data model with comprehensive meteorological information. - -```swift -public struct WeatherData: WidgetData { - public let id: String - public let timestamp: Date - public let metadata: [String: String] - - public let temperature: Temperature - public let humidity: Double - public let pressure: Double - public let windSpeed: Double - public let windDirection: Double - public let conditions: WeatherCondition - public let forecast: [WeatherForecast] - public let location: Location - public let alerts: [WeatherAlert] -} -``` - -#### CalendarData - -Calendar and event data model with meeting and scheduling information. - -```swift -public struct CalendarData: WidgetData { - public let id: String - public let timestamp: Date - public let metadata: [String: String] - - public let events: [CalendarEvent] - public let upcomingMeetings: [Meeting] - public let deadlines: [Deadline] - public let reminders: [Reminder] - public let availability: AvailabilityStatus -} -``` - -### Performance Data Models - -#### PerformanceMetric - -Performance measurement data structure. - -```swift -public struct PerformanceMetric: Codable, Identifiable { - public let id: String - public let type: MetricType - public let value: Double - public let threshold: Double - public let timestamp: Date - public let severity: Severity - public let context: [String: String] -} -``` - -### Security Data Models - -#### EncryptedData - -Container for encrypted data with metadata. - -```swift -public struct EncryptedData: Codable { - public let data: Data - public let algorithm: EncryptionAlgorithm - public let keyIdentifier: String - public let initializationVector: Data - public let authenticationTag: Data - public let timestamp: Date -} -``` - -#### AuthenticationResult - -Result of authentication operations. - -```swift -public struct AuthenticationResult: Codable { - public let success: Bool - public let method: AuthenticationMethod - public let sessionId: String? - public let permissions: [Permission] - public let expiresAt: Date? - public let deviceInfo: DeviceInfo -} -``` - -## Error Handling - -### WidgetError - -Comprehensive error enumeration for widget operations. - -```swift -public enum WidgetError: LocalizedError { - case configurationInvalid - case templateNotFound - case creationFailed(String) - case updateFailed(String) - case dataLoadingFailed(String) - case renderingFailed(String) - case permissionDenied - case resourceUnavailable - case networkError(Error) - case storageError(Error) -} -``` - -### Security Errors - -```swift -public enum SecurityError: LocalizedError { - case encryptionFailed(String) - case decryptionFailed(String) - case authenticationFailed(String) - case biometricUnavailable - case keyGenerationFailed - case invalidCredentials - case sessionExpired - case permissionDenied -} -``` - -### Performance Errors - -```swift -public enum PerformanceError: LocalizedError { - case thresholdExceeded(MetricType, Double) - case monitoringFailed(String) - case optimizationFailed(String) - case cacheError(String) - case metricsUnavailable -} -``` - -## Protocols - -### Widget Development Protocols - -#### WidgetRenderable - -Protocol for renderable widget components. - -```swift -public protocol WidgetRenderable { - associatedtype Content: View - - func render() async throws -> Content - func updateContent(_ data: WidgetData) async throws - func handleInteraction(_ interaction: WidgetInteraction) async -} -``` - -#### DataSourceProtocol - -Protocol for custom data sources. - -```swift -public protocol DataSourceProtocol { - associatedtype DataType: Codable - - func connect() async throws - func disconnect() async - func fetch() async throws -> DataType - func subscribe(handler: @escaping (DataType) -> Void) async throws -} -``` - -### Security Protocols - -#### AuthenticationProvider - -Protocol for custom authentication providers. - -```swift -public protocol AuthenticationProvider { - func authenticate(request: AuthenticationRequest) async throws -> AuthenticationResult - func validateCredentials(_ credentials: Credentials) async throws -> Bool - func refreshToken(_ token: String) async throws -> String -} -``` - -## Constants and Configurations - -### Default Values - -```swift -public struct WidgetDefaults { - public static let updateInterval: TimeInterval = 300 // 5 minutes - public static let maxCacheSize: Int = 100 * 1024 * 1024 // 100MB - public static let defaultTimeout: TimeInterval = 30 - public static let maxRetryAttempts: Int = 3 - public static let performanceThreshold: TimeInterval = 0.1 // 100ms -} -``` - -### Configuration Keys - -```swift -public struct ConfigurationKeys { - public static let apiBaseURL = "api_base_url" - public static let encryptionEnabled = "encryption_enabled" - public static let performanceMonitoring = "performance_monitoring" - public static let analyticsEnabled = "analytics_enabled" - public static let debugMode = "debug_mode" -} -``` - -## Usage Examples - -### Basic Widget Creation - -```swift -// Initialize the manager -let widgetManager = WidgetDevelopmentManager.shared -try await widgetManager.initialize(configuration: .default) - -// Create a weather widget -let configuration = WidgetConfiguration( - type: .weather, - size: .medium, - updateInterval: 300 -) - -let widget = try await widgetManager.createWidget(with: configuration) -``` - -### Security Implementation - -```swift -// Encrypt sensitive data -let encryptionManager = EncryptionManager.shared -let sensitiveData = "User personal information".data(using: .utf8)! - -let encryptedData = try await encryptionManager.encryptData( - sensitiveData, - keyIdentifier: "user-data-key", - requireBiometric: true -) -``` - -### Performance Monitoring - -```swift -// Monitor widget rendering performance -let performanceMonitor = PerformanceMonitor.shared - -let renderTime = try await performanceMonitor.measureExecutionTime { - try await widget.render() -} - -print("Widget rendered in \(renderTime)ms") -``` - -This comprehensive API reference provides complete documentation for all major components and features of the iOS Widget Development Kit, enabling developers to effectively utilize the enterprise-grade capabilities of the framework. \ No newline at end of file diff --git a/Documentation/API.md b/Documentation/API.md deleted file mode 100644 index 861334a..0000000 --- a/Documentation/API.md +++ /dev/null @@ -1,329 +0,0 @@ -# API Reference - -## Table of Contents - -- [Overview](#overview) -- [WidgetKit Integration](#widgetkit-integration) -- [Timeline Providers](#timeline-providers) -- [Widget Views](#widget-views) -- [Data Services](#data-services) -- [Configuration](#configuration) -- [Utilities](#utilities) - ---- - -## Overview - -iOS Widget Development Kit provides a comprehensive set of tools for building beautiful, performant widgets for iOS 14+. - -### Module Import - -```swift -import iOSWidgetDevelopmentKit -``` - ---- - -## WidgetKit Integration - -### WidgetBuilder - -A fluent API for constructing widgets with minimal boilerplate. - -```swift -public struct WidgetBuilder { - public init(@ViewBuilder content: @escaping () -> Content) - public func supportedFamilies(_ families: [WidgetFamily]) -> Self - public func configurationDisplayName(_ name: String) -> Self - public func description(_ description: String) -> Self -} -``` - -**Example:** - -```swift -WidgetBuilder { - MyWidgetView() -} -.supportedFamilies([.systemSmall, .systemMedium]) -.configurationDisplayName("My Widget") -.description("Displays important information") -``` - ---- - -## Timeline Providers - -### SimpleTimelineProvider - -A simplified timeline provider with automatic refresh management. - -```swift -public protocol SimpleTimelineProvider: TimelineProvider { - associatedtype DataType - - func fetchData() async throws -> DataType - var refreshInterval: TimeInterval { get } -} -``` - -**Properties:** - -| Property | Type | Description | -|----------|------|-------------| -| `refreshInterval` | `TimeInterval` | Time between updates (default: 3600) | - -**Example:** - -```swift -struct WeatherProvider: SimpleTimelineProvider { - typealias DataType = WeatherData - - var refreshInterval: TimeInterval { 1800 } // 30 minutes - - func fetchData() async throws -> WeatherData { - try await WeatherService.shared.getCurrentWeather() - } -} -``` - -### IntentTimelineProvider - -For configurable widgets with user intents. - -```swift -public struct IntentWidgetConfiguration { - public let provider: IntentTimelineProvider - public let content: (Entry) -> WidgetView -} -``` - ---- - -## Widget Views - -### WidgetContainerView - -A container that handles all widget families with appropriate layouts. - -```swift -public struct WidgetContainerView: View { - public init( - @ViewBuilder small: @escaping () -> Content, - @ViewBuilder medium: @escaping () -> Content, - @ViewBuilder large: @escaping () -> Content - ) -} -``` - -### GradientBackground - -Pre-built gradient backgrounds for widgets. - -```swift -public enum GradientBackground { - case sunrise - case sunset - case ocean - case forest - case custom(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) - - public var gradient: LinearGradient { get } -} -``` - -### WidgetText - -Optimized text component for widgets. - -```swift -public struct WidgetText: View { - public init(_ text: String, style: TextStyle = .body) - - public enum TextStyle { - case title - case headline - case body - case caption - case value - } -} -``` - ---- - -## Data Services - -### WidgetDataService - -Protocol for widget data fetching. - -```swift -public protocol WidgetDataService { - associatedtype DataType: Codable - - func fetch() async throws -> DataType - func getCached() -> DataType? - func invalidateCache() -} -``` - -### SharedDataContainer - -For sharing data between app and widget. - -```swift -public final class SharedDataContainer { - public static func configure(appGroupIdentifier: String) - - public static func save(_ value: T, forKey key: String) throws - public static func load(_ type: T.Type, forKey key: String) throws -> T? - public static func remove(forKey key: String) -} -``` - -**Example:** - -```swift -// In main app -SharedDataContainer.configure(appGroupIdentifier: "group.com.yourapp") -try SharedDataContainer.save(userData, forKey: "userData") - -// In widget -let userData = try SharedDataContainer.load(UserData.self, forKey: "userData") -``` - ---- - -## Configuration - -### WidgetConfiguration - -Global widget configuration options. - -```swift -public struct WidgetConfiguration { - public static var shared: WidgetConfiguration - - public var defaultRefreshInterval: TimeInterval - public var cacheExpirationInterval: TimeInterval - public var maxCacheSize: Int - public var enableDebugMode: Bool -} -``` - -### WidgetPreferences - -User preferences storage for widgets. - -```swift -public final class WidgetPreferences { - public static let shared: WidgetPreferences - - public subscript(key: String) -> T? { get set } - public func register(defaults: [String: Any]) -} -``` - ---- - -## Utilities - -### WidgetCenter Extensions - -```swift -extension WidgetCenter { - /// Reloads specific widget timeline - public func reload(kind: String) - - /// Reloads all widget timelines - public func reloadAll() - - /// Gets current widget configurations - public func getConfigurations() async -> [WidgetInfo] -} -``` - -### DateFormatting - -Widget-optimized date formatters. - -```swift -public enum WidgetDateFormatter { - case relative // "2 hours ago" - case compact // "2h" - case time // "3:45 PM" - case date // "Jan 15" - case full // "January 15, 2024" - - public func string(from date: Date) -> String -} -``` - -### DeepLinking - -Handle widget tap actions. - -```swift -public struct WidgetDeepLink { - public static func url(for action: String, parameters: [String: String] = [:]) -> URL - - public static func parse(_ url: URL) -> (action: String, parameters: [String: String])? -} -``` - -**Example:** - -```swift -// In widget view -Link(destination: WidgetDeepLink.url(for: "showDetails", parameters: ["id": "123"])) { - WidgetContent() -} - -// In main app -func application(_ app: UIApplication, open url: URL) -> Bool { - if let (action, params) = WidgetDeepLink.parse(url) { - handleWidgetAction(action, parameters: params) - return true - } - return false -} -``` - ---- - -## Error Handling - -### WidgetError - -```swift -public enum WidgetError: LocalizedError { - case dataFetchFailed(underlying: Error) - case cacheMiss - case invalidConfiguration - case networkUnavailable - - public var errorDescription: String? { get } - public var recoverySuggestion: String? { get } -} -``` - ---- - -## Best Practices - -1. **Keep widgets lightweight** - Minimize memory usage -2. **Cache aggressively** - Network calls are expensive -3. **Handle errors gracefully** - Always show useful content -4. **Use appropriate refresh intervals** - Balance freshness vs battery -5. **Test all widget families** - Each size has different requirements - ---- - -## Version History - -| Version | Changes | -|---------|---------| -| 2.0.0 | iOS 17 support, Interactive widgets | -| 1.5.0 | Live Activities support | -| 1.0.0 | Initial release | diff --git a/Documentation/APIReference.md b/Documentation/APIReference.md deleted file mode 100644 index 1a30044..0000000 --- a/Documentation/APIReference.md +++ /dev/null @@ -1,591 +0,0 @@ -# API Reference - - -## Table of Contents -- [API Reference](#api-reference) -- [WidgetEngine](#widgetengine) - - [Properties](#properties) - - [Methods](#methods) - - [registerTemplate(_:)](#registertemplate) - - [createWidget(with:)](#createwidgetwith) - - [updateWidget(_:with:)](#updatewidgetwith) - - [configurePerformance(_:)](#configureperformance) - - [getAnalytics()](#getanalytics) -- [WidgetTemplate](#widgettemplate) - - [Properties](#properties) - - [Methods](#methods) - - [createWidget(configuration:)](#createwidgetconfiguration) - - [validateConfiguration(_:)](#validateconfiguration) - - [getDefaultConfiguration()](#getdefaultconfiguration) -- [WidgetConfiguration](#widgetconfiguration) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetDefinition](#widgetdefinition) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetCustomization](#widgetcustomization) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetSettings](#widgetsettings) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetData](#widgetdata) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetType](#widgettype) -- [LiveDataIntegration](#livedataintegration) - - [Methods](#methods) - - [registerDataSource(_:dataSource:)](#registerdatasourcedatasource) - - [connectWebSocket(identifier:url:headers:)](#connectwebsocketidentifierurlheaders) - - [disconnectWebSocket(identifier:)](#disconnectwebsocketidentifier) - - [getDataPublisher(for:)](#getdatapublisherfor) - - [sendData(to:data:)](#senddatatodata) - - [configureDataSource(_:settings:)](#configuredatasourcesettings) - - [getConnectionStatus(for:)](#getconnectionstatusfor) -- [WidgetDataSource](#widgetdatasource) - - [Properties](#properties) - - [Methods](#methods) - - [refresh()](#refresh) - - [configure(_:)](#configure) -- [DataSourceSettings](#datasourcesettings) - - [Properties](#properties) - - [Initializer](#initializer) -- [ConnectionStatus](#connectionstatus) -- [WidgetAnalyticsService](#widgetanalyticsservice) - - [Methods](#methods) - - [getAnalytics()](#getanalytics) - - [configure(_:)](#configure) - - [exportAnalytics()](#exportanalytics) - - [clearAnalytics()](#clearanalytics) -- [WidgetAnalytics](#widgetanalytics) - - [Properties](#properties) -- [AnalyticsConfiguration](#analyticsconfiguration) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetPerformanceMetrics](#widgetperformancemetrics) - - [Properties](#properties) - - [Methods](#methods) - - [update(_:)](#update) - - [updateMemoryUsage(_:)](#updatememoryusage) - - [updateBatteryLevel(_:)](#updatebatterylevel) - - [updateSettings(_:)](#updatesettings) - - [updateBackgroundRefresh()](#updatebackgroundrefresh) -- [WidgetPerformanceSettings](#widgetperformancesettings) - - [Properties](#properties) - - [Initializer](#initializer) -- [WidgetError](#widgeterror) -- [Notification Names](#notification-names) - - - -## WidgetEngine - -The core widget engine that manages widget lifecycle and performance. - -### Properties - -```swift -public static let shared: WidgetEngine -public var currentConfiguration: WidgetConfiguration? -public var performanceMetrics: WidgetPerformanceMetrics -``` - -### Methods - -#### registerTemplate(_:) -Registers a widget template with the engine. - -```swift -public func registerTemplate(_ template: WidgetTemplate) -``` - -**Parameters:** -- `template`: The widget template to register - -#### createWidget(with:) -Creates a widget view with the specified configuration. - -```swift -public func createWidget(with configuration: WidgetConfiguration) -> some View -``` - -**Parameters:** -- `configuration`: Widget configuration - -**Returns:** Widget view - -#### updateWidget(_:with:) -Updates a widget with new data. - -```swift -public func updateWidget(_ widgetId: String, with data: WidgetData) -``` - -**Parameters:** -- `widgetId`: Widget identifier -- `data`: New data to update - -#### configurePerformance(_:) -Configures widget performance settings. - -```swift -public func configurePerformance(_ settings: WidgetPerformanceSettings) -``` - -**Parameters:** -- `settings`: Performance configuration - -#### getAnalytics() -Returns analytics data. - -```swift -public func getAnalytics() -> WidgetAnalytics -``` - -**Returns:** Analytics data - -## WidgetTemplate - -Base protocol for all widget templates. - -### Properties - -```swift -var identifier: String { get } -var displayName: String { get } -var description: String { get } -var supportedSizes: [WidgetFamily] { get } -``` - -### Methods - -#### createWidget(configuration:) -Creates widget view with configuration. - -```swift -func createWidget(configuration: WidgetConfiguration) -> AnyView -``` - -#### validateConfiguration(_:) -Validates configuration. - -```swift -func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool -``` - -#### getDefaultConfiguration() -Returns default configuration. - -```swift -func getDefaultConfiguration() -> WidgetConfiguration -``` - -## WidgetConfiguration - -Configuration for widget setup. - -### Properties - -```swift -public let id: String -public let templateIdentifier: String -public let widgets: [WidgetDefinition] -public let settings: WidgetSettings -``` - -### Initializer - -```swift -public init(id: String, templateIdentifier: String, widgets: [WidgetDefinition], settings: WidgetSettings) -``` - -## WidgetDefinition - -Definition of a widget instance. - -### Properties - -```swift -public let id: String -public let type: WidgetType -public let dataSourceIdentifier: String? -public let customization: WidgetCustomization -``` - -### Initializer - -```swift -public init(id: String, type: WidgetType, dataSourceIdentifier: String? = nil, customization: WidgetCustomization) -``` - -## WidgetCustomization - -Customization options for widget appearance. - -### Properties - -```swift -public let backgroundColor: Color -public let textColor: Color -public let accentColor: Color -public let cornerRadius: CGFloat -public let padding: EdgeInsets -public let font: Font -``` - -### Initializer - -```swift -public init(backgroundColor: Color = .clear, textColor: Color = .primary, accentColor: Color = .blue, cornerRadius: CGFloat = 12, padding: EdgeInsets = EdgeInsets(), font: Font = .body) -``` - -## WidgetSettings - -Settings for widget behavior. - -### Properties - -```swift -public let refreshInterval: TimeInterval -public let enableAnimations: Bool -public let enableHaptics: Bool -public let enableSound: Bool -``` - -### Initializer - -```swift -public init(refreshInterval: TimeInterval = 30.0, enableAnimations: Bool = true, enableHaptics: Bool = true, enableSound: Bool = false) -``` - -## WidgetData - -Data structure for widget content. - -### Properties - -```swift -public let id: String -public let type: WidgetType -public let content: [String: Any] -public let timestamp: Date -``` - -### Initializer - -```swift -public init(id: String, type: WidgetType, content: [String: Any], timestamp: Date = Date()) -``` - -## WidgetType - -Enumeration of supported widget types. - -```swift -public enum WidgetType: String, CaseIterable { - case weather = "weather" - case calendar = "calendar" - case fitness = "fitness" - case news = "news" - case social = "social" - case productivity = "productivity" - case entertainment = "entertainment" - case finance = "finance" - case health = "health" - case travel = "travel" -} -``` - -## LiveDataIntegration - -Framework for real-time data integration. - -### Methods - -#### registerDataSource(_:dataSource:) -Registers a data source for live updates. - -```swift -public func registerDataSource(_ identifier: String, dataSource: WidgetDataSource) -``` - -#### connectWebSocket(identifier:url:headers:) -Connects to WebSocket for real-time data. - -```swift -public func connectWebSocket(identifier: String, url: URL, headers: [String: String] = [:]) -``` - -#### disconnectWebSocket(identifier:) -Disconnects WebSocket. - -```swift -public func disconnectWebSocket(identifier: String) -``` - -#### getDataPublisher(for:) -Gets data publisher for specific source. - -```swift -public func getDataPublisher(for identifier: String) -> AnyPublisher? -``` - -#### sendData(to:data:) -Sends data to specific widget. - -```swift -public func sendData(to widgetId: String, data: WidgetData) -``` - -#### configureDataSource(_:settings:) -Configures data source settings. - -```swift -public func configureDataSource(_ identifier: String, settings: DataSourceSettings) -``` - -#### getConnectionStatus(for:) -Gets connection status. - -```swift -public func getConnectionStatus(for identifier: String) -> ConnectionStatus -``` - -## WidgetDataSource - -Protocol for data sources. - -### Properties - -```swift -var identifier: String { get } -var dataPublisher: AnyPublisher { get } -``` - -### Methods - -#### refresh() -Refreshes data source. - -```swift -func refresh() -``` - -#### configure(_:) -Configures data source settings. - -```swift -func configure(_ settings: DataSourceSettings) -``` - -## DataSourceSettings - -Settings for data sources. - -### Properties - -```swift -public let refreshInterval: TimeInterval -public let enableCaching: Bool -public let maxRetries: Int -public let timeout: TimeInterval -``` - -### Initializer - -```swift -public init(refreshInterval: TimeInterval = 30.0, enableCaching: Bool = true, maxRetries: Int = 3, timeout: TimeInterval = 10.0) -``` - -## ConnectionStatus - -Enumeration of connection statuses. - -```swift -public enum ConnectionStatus { - case connected - case connecting - case disconnected - case unknown -} -``` - -## WidgetAnalyticsService - -Service for analytics and performance tracking. - -### Methods - -#### getAnalytics() -Returns analytics data. - -```swift -public func getAnalytics() -> WidgetAnalytics -``` - -#### configure(_:) -Configures analytics service. - -```swift -public func configure(_ config: AnalyticsConfiguration) -``` - -#### exportAnalytics() -Exports analytics data. - -```swift -public func exportAnalytics() -> Data? -``` - -#### clearAnalytics() -Clears analytics data. - -```swift -public func clearAnalytics() -``` - -## WidgetAnalytics - -Analytics data structure. - -### Properties - -```swift -public var totalWidgetUpdates: Int -public var widgetUpdates: [String: Int] -public var templateRegistrations: Int -public var dataSourceRegistrations: Int -public var webSocketConnections: Int -public var webSocketDisconnections: Int -public var dataSentCount: Int -public var dataSourceUpdates: Int -public var dataSourceConfigurations: Int -public var connectionsEstablished: Int -public var connectionFailures: Int -public var connectionsCancelled: Int -public var connectionRetries: Int -public var reconnectionAttempts: Int -public var dataReceiveErrors: Int -public var dataDecodeErrors: Int -public var networkUnavailableEvents: Int -public var memoryCleanups: Int -public var batteryOptimizations: Int -public var errors: Int -public var engineInitializations: Int -public var sessionStartTime: Date -public var lastUpdateTime: Date -``` - -## AnalyticsConfiguration - -Configuration for analytics service. - -### Properties - -```swift -public let enableExternalAnalytics: Bool -public let maxEventLogSize: Int -public let enablePerformanceTracking: Bool -public let enableErrorTracking: Bool -``` - -### Initializer - -```swift -public init(enableExternalAnalytics: Bool = false, maxEventLogSize: Int = 1000, enablePerformanceTracking: Bool = true, enableErrorTracking: Bool = true) -``` - -## WidgetPerformanceMetrics - -Performance metrics for widgets. - -### Properties - -```swift -@Published public var memoryUsage: UInt64 -@Published public var batteryLevel: Float -@Published public var refreshCount: Int -@Published public var errorCount: Int -@Published public var settings: WidgetPerformanceSettings -``` - -### Methods - -#### update(_:) -Updates metrics with new data. - -```swift -public func update(_ metrics: WidgetPerformanceMetrics) -``` - -#### updateMemoryUsage(_:) -Updates memory usage. - -```swift -public func updateMemoryUsage(_ usage: UInt64) -``` - -#### updateBatteryLevel(_:) -Updates battery level. - -```swift -public func updateBatteryLevel(_ level: Float) -``` - -#### updateSettings(_:) -Updates performance settings. - -```swift -public func updateSettings(_ settings: WidgetPerformanceSettings) -``` - -#### updateBackgroundRefresh() -Updates background refresh count. - -```swift -public func updateBackgroundRefresh() -``` - -## WidgetPerformanceSettings - -Settings for performance optimization. - -### Properties - -```swift -public let maxMemoryUsage: UInt64 -public let refreshInterval: TimeInterval -public let enableBatteryOptimization: Bool -``` - -### Initializer - -```swift -public init(maxMemoryUsage: UInt64 = 100 * 1024 * 1024, refreshInterval: TimeInterval = 30.0, enableBatteryOptimization: Bool = true) -``` - -## WidgetError - -Error types for widgets. - -```swift -public enum WidgetError: Error, LocalizedError { - case templateNotFound(String) - case dataSourceNotFound(String) - case connectionFailed(String) - case dataDecodeFailed(String) - case networkUnavailable - case unknown -} -``` - -## Notification Names - -```swift -extension Notification.Name { - static let widgetDidUpdate = Notification.Name("widgetDidUpdate") - static let widgetErrorOccurred = Notification.Name("widgetErrorOccurred") - static let widgetDataReceived = Notification.Name("widgetDataReceived") - static let dataSourceUpdated = Notification.Name("dataSourceUpdated") -} -``` \ No newline at end of file diff --git a/Documentation/Architecture.md b/Documentation/Architecture.md deleted file mode 100644 index 918b7b7..0000000 --- a/Documentation/Architecture.md +++ /dev/null @@ -1,356 +0,0 @@ -# 🏗️ Architecture Guide - iOS Widget Development Kit - -## 📋 Table of Contents - -- [Overview](#overview) -- [Architectural Principles](#architectural-principles) -- [System Architecture](#system-architecture) -- [Layer Structure](#layer-structure) -- [Design Patterns](#design-patterns) -- [Data Flow](#data-flow) -- [Component Interaction](#component-interaction) -- [Security Architecture](#security-architecture) -- [Performance Architecture](#performance-architecture) -- [Scalability Considerations](#scalability-considerations) - ---- - -## 🌟 Overview - -The iOS Widget Development Kit follows a **layered, modular architecture** that emphasizes separation of concerns, testability, and maintainability. Our architecture is built on enterprise-grade principles ensuring scalability from small widgets to large-scale enterprise applications. - -### Core Architectural Goals - -- **🔒 Security First**: Zero-trust architecture with defense-in-depth -- **⚡ Performance Optimized**: Sub-100ms response times with intelligent caching -- **🧪 Testability**: 95%+ test coverage with comprehensive unit and integration tests -- **🔧 Maintainability**: Clean, documented code following SOLID principles -- **📈 Scalability**: Support for millions of widgets with horizontal scaling -- **🌐 Extensibility**: Plugin architecture for custom functionality - ---- - -## 🎯 Architectural Principles - -### 1. Clean Architecture -Following Uncle Bob's Clean Architecture principles: -- **Dependency Inversion**: High-level modules don't depend on low-level modules -- **Single Responsibility**: Each component has one reason to change -- **Interface Segregation**: No client should depend on methods it doesn't use -- **Open/Closed Principle**: Open for extension, closed for modification - -### 2. MVVM + Coordinator Pattern -- **Model**: Data models and business logic -- **View**: SwiftUI views and UI components -- **ViewModel**: View logic and state management -- **Coordinator**: Navigation and flow control - -### 3. Reactive Programming -- **Combine Framework**: For reactive data streams -- **Publisher/Subscriber**: Decoupled component communication -- **State Management**: Centralized, predictable state updates - ---- - -## 🏢 System Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ iOS Widget Development Kit │ -├─────────────────────────────────────────────────────────────┤ -│ Presentation Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ SwiftUI │ │ ViewModels │ │ Coordinators│ │ -│ │ Views │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ Business Logic Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Use Cases │ │ Services │ │ Validators │ │ -│ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ Data Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │Repositories │ │Data Sources │ │ Models │ │ -│ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────────┤ -│ Infrastructure Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Network │ │ Cache │ │ Security │ │ -│ │ │ │ │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 📚 Layer Structure - -### 1. **Presentation Layer** (Sources/UI/) -- **SwiftUI Views**: Declarative UI components -- **ViewModels**: State management and view logic -- **Coordinators**: Navigation flow control -- **View Builders**: Reusable UI component builders - -**Responsibilities:** -- User interface rendering -- User interaction handling -- Navigation management -- State presentation - -### 2. **Business Logic Layer** (Sources/Core/, Sources/Features/) -- **Use Cases**: Application-specific business rules -- **Services**: Cross-cutting concerns (Analytics, Logging) -- **Validators**: Input validation and business rule enforcement -- **Managers**: Resource and lifecycle management - -**Responsibilities:** -- Business rule implementation -- Data transformation -- Workflow orchestration -- Cross-cutting services - -### 3. **Data Layer** (Sources/Analytics/, Sources/LiveData/) -- **Repositories**: Data access abstraction -- **Data Sources**: Concrete data implementations -- **Models**: Data structures and entities -- **DTOs**: Data transfer objects - -**Responsibilities:** -- Data persistence -- External API integration -- Data mapping and transformation -- Cache management - -### 4. **Infrastructure Layer** (Sources/Security/, Sources/Performance/) -- **Network Layer**: HTTP client and communication -- **Cache Layer**: Data caching strategies -- **Security Layer**: Authentication and authorization -- **Configuration**: App settings and environment - -**Responsibilities:** -- External service integration -- Infrastructure concerns -- Platform-specific implementations -- Configuration management - ---- - -## 🎨 Design Patterns - -### 1. **Repository Pattern** -```swift -protocol WidgetRepository { - func getWidget(id: String) async throws -> Widget - func saveWidget(_ widget: Widget) async throws - func deleteWidget(id: String) async throws -} - -class DefaultWidgetRepository: WidgetRepository { - private let localDataSource: LocalWidgetDataSource - private let remoteDataSource: RemoteWidgetDataSource - - // Implementation with caching strategy -} -``` - -### 2. **Factory Pattern** -```swift -protocol WidgetFactory { - func createWidget(type: WidgetType) -> Widget -} - -class DefaultWidgetFactory: WidgetFactory { - func createWidget(type: WidgetType) -> Widget { - switch type { - case .weather: return WeatherWidget() - case .calendar: return CalendarWidget() - case .fitness: return FitnessWidget() - } - } -} -``` - -### 3. **Observer Pattern** -```swift -class WidgetNotificationCenter { - static let shared = WidgetNotificationCenter() - private var observers: [WeakObserver] = [] - - func addObserver(_ observer: WidgetObserver) { - observers.append(WeakObserver(observer)) - } - - func notifyWidgetUpdated(_ widget: Widget) { - observers.forEach { $0.observer?.widgetDidUpdate(widget) } - } -} -``` - -### 4. **Strategy Pattern** -```swift -protocol CachingStrategy { - func cache(_ object: T, forKey key: String) - func retrieve(forKey key: String, type: T.Type) -> T? -} - -class MemoryCachingStrategy: CachingStrategy { /* Implementation */ } -class DiskCachingStrategy: CachingStrategy { /* Implementation */ } -class HybridCachingStrategy: CachingStrategy { /* Implementation */ } -``` - ---- - -## 🔄 Data Flow - -### Widget Update Flow -``` -User Action → ViewModel → Use Case → Repository → Data Source → API - ↑ ↓ -UI Update ← Combine Publisher ← Business Logic ← Data Mapping ← Response -``` - -### Live Activity Flow -``` -External Event → Push Notification → Activity Manager → Widget Engine - ↓ ↓ ↓ - Live Activity Update → Dynamic Island → Lock Screen -``` - -### Background Refresh Flow -``` -Background Task → Scheduler → Data Fetcher → Cache Update → Widget Refresh - ↓ ↓ ↓ ↓ - Battery Check → Network Check → Validation → UI Update -``` - ---- - -## 🔗 Component Interaction - -### Core Components -- **WidgetEngine**: Central orchestrator for all widget operations -- **DataManager**: Handles all data operations and caching -- **SecurityManager**: Manages authentication and encryption -- **PerformanceMonitor**: Tracks and optimizes performance metrics -- **AnalyticsService**: Collects usage and performance data - -### Communication Patterns -- **Async/Await**: For asynchronous operations -- **Combine**: For reactive data streams -- **Protocols**: For dependency injection and testability -- **Delegates**: For callback-based communication - ---- - -## 🛡️ Security Architecture - -### Authentication & Authorization -- **Biometric Authentication**: Touch ID / Face ID integration -- **Keychain Storage**: Secure credential storage -- **Certificate Pinning**: SSL/TLS security -- **Token Management**: JWT token handling - -### Data Protection -- **Encryption at Rest**: AES-256 encryption for stored data -- **Encryption in Transit**: TLS 1.3 for network communication -- **Data Sanitization**: Input validation and output encoding -- **Privacy Controls**: GDPR compliance and user consent - -### Security Monitoring -- **Threat Detection**: Real-time security monitoring -- **Audit Logging**: Comprehensive security event logging -- **Vulnerability Scanning**: Automated security assessments -- **Incident Response**: Security incident handling procedures - ---- - -## ⚡ Performance Architecture - -### Optimization Strategies -- **Lazy Loading**: Load resources only when needed -- **Memory Pooling**: Efficient memory management -- **Background Processing**: Non-blocking UI operations -- **Smart Caching**: Multi-level caching strategy - -### Performance Monitoring -- **Real-time Metrics**: Live performance monitoring -- **Performance Budgets**: Predefined performance thresholds -- **Automated Alerts**: Performance degradation notifications -- **Optimization Recommendations**: AI-powered performance insights - -### Resource Management -- **Memory Management**: Automatic memory optimization -- **Battery Optimization**: Power-efficient algorithms -- **Network Optimization**: Intelligent data fetching -- **CPU Optimization**: Efficient processing strategies - ---- - -## 📈 Scalability Considerations - -### Horizontal Scaling -- **Microservice Architecture**: Decomposed services -- **Load Balancing**: Distributed traffic handling -- **Auto-scaling**: Dynamic resource allocation -- **Service Mesh**: Inter-service communication - -### Vertical Scaling -- **Resource Optimization**: Efficient resource utilization -- **Performance Tuning**: Algorithm optimization -- **Memory Optimization**: Reduced memory footprint -- **CPU Optimization**: Parallel processing - -### Data Scaling -- **Database Sharding**: Distributed data storage -- **Caching Layers**: Multi-level caching -- **Data Partitioning**: Logical data separation -- **Read Replicas**: Distributed read operations - ---- - -## 🔧 Implementation Guidelines - -### Code Organization -``` -Sources/ -├── Core/ # Core framework components -│ ├── WidgetEngine.swift # Central widget orchestrator -│ ├── DataManager.swift # Data management -│ └── ConfigManager.swift # Configuration management -├── Features/ # Feature-specific implementations -│ ├── Weather/ # Weather widget feature -│ ├── Calendar/ # Calendar widget feature -│ └── Fitness/ # Fitness widget feature -├── Security/ # Security implementations -│ ├── Authentication/ # Authentication services -│ ├── Encryption/ # Encryption utilities -│ └── Compliance/ # Compliance frameworks -├── Performance/ # Performance optimizations -│ ├── Monitoring/ # Performance monitoring -│ ├── Optimization/ # Performance optimization -│ └── Caching/ # Caching strategies -└── UI/ # User interface components - ├── Views/ # SwiftUI views - ├── ViewModels/ # View models - └── Coordinators/ # Navigation coordinators -``` - -### Testing Strategy -- **Unit Tests**: Test individual components in isolation -- **Integration Tests**: Test component interactions -- **UI Tests**: Test user interface workflows -- **Performance Tests**: Test performance characteristics -- **Security Tests**: Test security implementations - -### Documentation Standards -- **Code Documentation**: Comprehensive inline documentation -- **Architecture Documentation**: System design documentation -- **API Documentation**: Public interface documentation -- **Setup Guides**: Implementation and setup guides - ---- - -This architecture ensures that the iOS Widget Development Kit remains maintainable, scalable, and secure while providing excellent performance and developer experience. diff --git a/Documentation/ArchitectureGuide.md b/Documentation/ArchitectureGuide.md deleted file mode 100644 index b445122..0000000 --- a/Documentation/ArchitectureGuide.md +++ /dev/null @@ -1,469 +0,0 @@ -# 🏗️ Widget Architecture Guide - - -## Table of Contents -- [🏗️ Widget Architecture Guide](#-widget-architecture-guide) -- [Overview](#overview) -- [🏛️ Architecture Overview](#-architecture-overview) - - [Core Architecture Principles](#core-architecture-principles) - - [Architecture Layers](#architecture-layers) -- [🧩 Core Components](#-core-components) - - [1. Widget Engine](#1-widget-engine) - - [2. Widget Managers](#2-widget-managers) - - [Home Screen Widget Manager](#home-screen-widget-manager) - - [Live Activity Manager](#live-activity-manager) - - [Dynamic Island Manager](#dynamic-island-manager) - - [3. Data Integration Layer](#3-data-integration-layer) - - [4. Analytics Engine](#4-analytics-engine) -- [🔄 Data Flow](#-data-flow) - - [Widget Update Flow](#widget-update-flow) - - [Live Activity Flow](#live-activity-flow) -- [🎨 UI Architecture](#-ui-architecture) - - [Widget View Hierarchy](#widget-view-hierarchy) - - [View Model Pattern](#view-model-pattern) -- [🔧 Configuration Architecture](#-configuration-architecture) - - [Widget Configuration System](#widget-configuration-system) - - [Theme System](#theme-system) -- [🚀 Performance Architecture](#-performance-architecture) - - [Memory Management](#memory-management) - - [Background Processing](#background-processing) -- [🔒 Security Architecture](#-security-architecture) - - [Data Security](#data-security) - - [Widget Security](#widget-security) -- [📊 Monitoring and Analytics](#-monitoring-and-analytics) - - [Performance Monitoring](#performance-monitoring) - - [Error Handling](#error-handling) -- [🔄 Lifecycle Management](#-lifecycle-management) - - [Widget Lifecycle](#widget-lifecycle) - - [Live Activity Lifecycle](#live-activity-lifecycle) -- [🧪 Testing Architecture](#-testing-architecture) - - [Unit Testing](#unit-testing) - - [Integration Testing](#integration-testing) - - [UI Testing](#ui-testing) -- [📈 Scalability Considerations](#-scalability-considerations) - - [Horizontal Scaling](#horizontal-scaling) - - [Vertical Scaling](#vertical-scaling) -- [🔮 Future Architecture](#-future-architecture) - - [Planned Enhancements](#planned-enhancements) - - [Architecture Evolution](#architecture-evolution) -- [📚 Best Practices](#-best-practices) - - [Code Organization](#code-organization) - - [Performance Optimization](#performance-optimization) - - [Security](#security) -- [🎯 Conclusion](#-conclusion) - - - -## Overview - -The iOS Widget Development Kit follows a clean, modular architecture designed for scalability, maintainability, and performance. This guide explains the architectural patterns, components, and design principles used throughout the framework. - -## 🏛️ Architecture Overview - -### Core Architecture Principles - -- **Clean Architecture**: Separation of concerns with clear boundaries -- **SOLID Principles**: Single responsibility, open/closed, Liskov substitution, interface segregation, dependency inversion -- **MVVM Pattern**: Model-View-ViewModel for UI components -- **Repository Pattern**: Data access abstraction -- **Factory Pattern**: Widget creation and configuration -- **Observer Pattern**: Real-time updates and notifications - -### Architecture Layers - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Widget │ │ Live Activity│ │ Dynamic │ │ -│ │ Views │ │ Views │ │ Island │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────────┐ -│ Business Logic Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Widget │ │ Data │ │ Analytics │ │ -│ │ Managers │ │ Processors │ │ Engine │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────────┐ -│ Data Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Network │ │ Cache │ │ Storage │ │ -│ │ Services │ │ Manager │ │ Manager │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -┌─────────────────────────────────────────────────────────────┐ -│ Infrastructure Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Logging │ │ Error │ │ Security │ │ -│ │ System │ │ Handling │ │ Manager │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## 🧩 Core Components - -### 1. Widget Engine - -The Widget Engine is the central orchestrator that manages widget lifecycle, configuration, and updates. - -```swift -class WidgetEngine { - // Core functionality - func register(_ widget: Widget) -> Result - func unregister(_ widget: Widget) -> Result - func update(_ widget: Widget, with data: WidgetData) -> Result - - // Configuration management - func configure(_ config: WidgetConfiguration) -> Result - func getConfiguration() -> WidgetConfiguration - - // Lifecycle management - func start() -> Result - func stop() -> Result - func pause() -> Result - func resume() -> Result -} -``` - -### 2. Widget Managers - -Specialized managers handle different widget types and functionalities: - -#### Home Screen Widget Manager -```swift -class HomeScreenWidgetManager { - func createWidget(kind: String, size: WidgetSize) -> HomeScreenWidget - func updateWidget(_ widget: HomeScreenWidget, with data: WidgetData) - func configureWidget(_ widget: HomeScreenWidget, config: HomeWidgetConfiguration) -} -``` - -#### Live Activity Manager -```swift -class LiveActivityManager { - func startActivity(_ activity: Activity) - func updateActivity(_ activity: Activity, with data: T.ContentState) - func endActivity(_ activity: Activity) -} -``` - -#### Dynamic Island Manager -```swift -class DynamicIslandManager { - func startIslandActivity(_ activity: DynamicIslandActivity) - func updateIslandActivity(_ activity: DynamicIslandActivity) - func endIslandActivity(_ activity: DynamicIslandActivity) -} -``` - -### 3. Data Integration Layer - -The data integration layer provides seamless data access and synchronization: - -```swift -protocol WidgetDataSource { - func fetch() async throws -> WidgetData - func subscribe(to updates: @escaping (WidgetData) -> Void) - func unsubscribe() -} - -class WidgetDataManager { - func connect(_ dataSource: WidgetDataSource) -> Result - func fetchData() async throws -> WidgetData - func cache(_ data: WidgetData) -> Result - func retrieve(id: String) -> Result -} -``` - -### 4. Analytics Engine - -The analytics engine tracks widget usage and performance: - -```swift -class WidgetAnalytics { - func trackWidgetView(_ widget: Widget) - func trackWidgetInteraction(_ widget: Widget, action: String) - func trackPerformance(_ widget: Widget, metrics: PerformanceMetrics) - func generateReport() -> AnalyticsReport -} -``` - -## 🔄 Data Flow - -### Widget Update Flow - -``` -1. Data Source → 2. Data Processor → 3. Widget Engine → 4. Widget View - ↓ ↓ ↓ ↓ - Raw Data → Processed Data → Widget Data → UI Update -``` - -### Live Activity Flow - -``` -1. Activity Start → 2. Real-time Updates → 3. UI Refresh → 4. Activity End - ↓ ↓ ↓ ↓ - Initialize → Data Updates → View Update → Cleanup -``` - -## 🎨 UI Architecture - -### Widget View Hierarchy - -``` -WidgetView (Base) -├── HomeScreenWidgetView -│ ├── SmallWidgetView -│ ├── MediumWidgetView -│ └── LargeWidgetView -├── LockScreenWidgetView -│ ├── SmallLockWidgetView -│ └── MediumLockWidgetView -└── LiveActivityView - ├── LockScreenActivityView - └── DynamicIslandView - ├── CompactView - ├── ExpandedView - └── MinimalView -``` - -### View Model Pattern - -```swift -class WidgetViewModel: ObservableObject { - @Published var widgetData: WidgetData? - @Published var isLoading: Bool = false - @Published var error: WidgetError? - - private let dataManager: WidgetDataManager - private let analytics: WidgetAnalytics - - func loadData() - func refreshData() - func handleInteraction(_ action: WidgetAction) -} -``` - -## 🔧 Configuration Architecture - -### Widget Configuration System - -```swift -struct WidgetConfiguration { - // Basic settings - var enableHomeScreenWidgets: Bool = true - var enableLockScreenWidgets: Bool = true - var enableLiveActivities: Bool = true - var enableDynamicIsland: Bool = true - - // Performance settings - var refreshInterval: TimeInterval = 300 - var enableBackgroundUpdates: Bool = true - var enableCaching: Bool = true - - // Styling settings - var enableDynamicColors: Bool = true - var enableDarkMode: Bool = true - var enableCustomFonts: Bool = true - var enableAnimations: Bool = true -} -``` - -### Theme System - -```swift -struct WidgetTheme { - let backgroundColor: Color - let textColor: Color - let accentColor: Color - let cornerRadius: CGFloat - let shadow: WidgetShadow? -} - -struct DynamicTheme { - let lightMode: WidgetTheme - let darkMode: WidgetTheme -} -``` - -## 🚀 Performance Architecture - -### Memory Management - -- **Widget Pooling**: Reuse widget instances for better performance -- **Lazy Loading**: Load data and views only when needed -- **Image Caching**: Efficient image loading and caching -- **Memory Monitoring**: Track and optimize memory usage - -### Background Processing - -- **Background Tasks**: Handle updates when app is in background -- **Data Synchronization**: Sync data efficiently -- **Battery Optimization**: Minimize battery impact -- **Network Optimization**: Efficient network requests - -## 🔒 Security Architecture - -### Data Security - -- **Encryption**: Encrypt sensitive data -- **Certificate Pinning**: Secure network communications -- **Data Validation**: Validate all incoming data -- **Access Control**: Control data access permissions - -### Widget Security - -- **Sandboxing**: Isolate widget processes -- **Permission Management**: Manage widget permissions -- **Secure Storage**: Store sensitive data securely -- **Audit Logging**: Log security events - -## 📊 Monitoring and Analytics - -### Performance Monitoring - -```swift -struct PerformanceMetrics { - let renderTime: TimeInterval - let memoryUsage: Int64 - let networkLatency: TimeInterval - let batteryImpact: Double -} - -class PerformanceMonitor { - func trackMetrics(_ metrics: PerformanceMetrics) - func generatePerformanceReport() -> PerformanceReport - func alertOnThreshold(_ threshold: PerformanceThreshold) -} -``` - -### Error Handling - -```swift -enum WidgetError: Error { - case initializationFailed - case dataFetchFailed - case renderFailed - case networkError - case permissionDenied - case invalidConfiguration -} - -class ErrorHandler { - func handle(_ error: WidgetError) - func logError(_ error: WidgetError) - func recoverFromError(_ error: WidgetError) -> Result -} -``` - -## 🔄 Lifecycle Management - -### Widget Lifecycle - -``` -1. Initialization → 2. Configuration → 3. Registration → 4. Activation - ↓ ↓ ↓ ↓ - Setup Engine → Load Config → Register → Start Updates - ↓ ↓ ↓ ↓ -5. Data Loading → 6. Rendering → 7. Interaction → 8. Cleanup - ↓ ↓ ↓ ↓ - Fetch Data → Update UI → Handle Event → Release Resources -``` - -### Live Activity Lifecycle - -``` -1. Request Activity → 2. Start Activity → 3. Update Activity → 4. End Activity - ↓ ↓ ↓ ↓ - Create Config → Initialize → Real-time → Cleanup - ↓ ↓ ↓ ↓ -5. Dynamic Island → 6. Lock Screen → 7. Notifications → 8. Analytics - ↓ ↓ ↓ ↓ - Island Views → Lock Views → Push Updates → Track Usage -``` - -## 🧪 Testing Architecture - -### Unit Testing - -- **Widget Engine Tests**: Test core functionality -- **Data Manager Tests**: Test data operations -- **Configuration Tests**: Test configuration system -- **Error Handling Tests**: Test error scenarios - -### Integration Testing - -- **Widget Integration Tests**: Test widget interactions -- **Live Activity Tests**: Test activity lifecycle -- **Data Integration Tests**: Test data flow -- **Performance Tests**: Test performance metrics - -### UI Testing - -- **Widget Rendering Tests**: Test UI rendering -- **Interaction Tests**: Test user interactions -- **Accessibility Tests**: Test accessibility features -- **Cross-device Tests**: Test on different devices - -## 📈 Scalability Considerations - -### Horizontal Scaling - -- **Widget Pooling**: Reuse widget instances -- **Data Caching**: Cache frequently accessed data -- **Background Processing**: Handle updates efficiently -- **Memory Management**: Optimize memory usage - -### Vertical Scaling - -- **Modular Architecture**: Add new features easily -- **Plugin System**: Extend functionality with plugins -- **Configuration System**: Customize behavior -- **Analytics System**: Monitor and optimize - -## 🔮 Future Architecture - -### Planned Enhancements - -- **AI Integration**: Smart widget recommendations -- **Machine Learning**: Predictive data loading -- **Advanced Analytics**: Deep insights into usage -- **Cross-platform Support**: Extend to other platforms - -### Architecture Evolution - -- **Microservices**: Break down into smaller services -- **Event-driven Architecture**: Improve responsiveness -- **GraphQL Integration**: Flexible data fetching -- **Real-time Collaboration**: Multi-user features - -## 📚 Best Practices - -### Code Organization - -- **Clear Separation**: Separate concerns clearly -- **Consistent Naming**: Use consistent naming conventions -- **Documentation**: Document all public APIs -- **Error Handling**: Handle errors gracefully - -### Performance Optimization - -- **Lazy Loading**: Load data only when needed -- **Caching**: Cache frequently accessed data -- **Background Processing**: Handle updates efficiently -- **Memory Management**: Optimize memory usage - -### Security - -- **Data Encryption**: Encrypt sensitive data -- **Input Validation**: Validate all inputs -- **Access Control**: Control data access -- **Audit Logging**: Log security events - -## 🎯 Conclusion - -The iOS Widget Development Kit architecture provides a solid foundation for building scalable, maintainable, and performant widget applications. By following the architectural patterns and principles outlined in this guide, developers can create robust widget solutions that meet the highest standards of quality and user experience. - -For more information about specific architectural components, refer to the individual documentation files for each module. \ No newline at end of file diff --git a/Documentation/Best-Practices.md b/Documentation/Best-Practices.md deleted file mode 100644 index 5ca70d9..0000000 --- a/Documentation/Best-Practices.md +++ /dev/null @@ -1,1078 +0,0 @@ -# Enterprise Best Practices Guide — iOS Widget Development Kit - -## Overview - -This comprehensive guide outlines enterprise-grade best practices for developing high-quality, performant, and secure iOS widgets using the iOS Widget Development Kit. These practices ensure optimal user experience, maintainability, and compliance with industry standards. - -## Table of Contents - -1. [Architecture & Design Principles](#architecture--design-principles) -2. [Performance Best Practices](#performance-best-practices) -3. [Security Best Practices](#security-best-practices) -4. [Code Quality Standards](#code-quality-standards) -5. [Testing Best Practices](#testing-best-practices) -6. [Accessibility Best Practices](#accessibility-best-practices) -7. [Localization Best Practices](#localization-best-practices) -8. [Deployment & Monitoring](#deployment--monitoring) -9. [Documentation Standards](#documentation-standards) - -## Architecture & Design Principles - -### 1. Clean Architecture Implementation - -**Layer Separation** -```swift -// ✅ Good: Clear separation of concerns -struct WeatherWidget { - private let dataLayer: WeatherDataRepository - private let businessLayer: WeatherBusinessLogic - private let presentationLayer: WeatherPresentation - - init() { - self.dataLayer = WeatherDataRepository() - self.businessLayer = WeatherBusinessLogic(repository: dataLayer) - self.presentationLayer = WeatherPresentation(logic: businessLayer) - } -} - -// ❌ Bad: Mixed responsibilities -struct WeatherWidget { - func loadWeatherAndUpdateUI() { - // Data loading, business logic, and UI updates mixed together - URLSession.shared.dataTask(with: url) { data, _, _ in - // Process data - // Update UI - // Business logic - } - } -} -``` - -**Dependency Injection** -```swift -// ✅ Good: Testable and flexible -protocol WeatherService { - func fetchWeather() async throws -> WeatherData -} - -class WeatherWidget { - private let weatherService: WeatherService - - init(weatherService: WeatherService) { - self.weatherService = weatherService - } -} - -// ❌ Bad: Tight coupling -class WeatherWidget { - private let weatherService = ConcreteWeatherService() -} -``` - -### 2. SOLID Principles Application - -**Single Responsibility Principle** -```swift -// ✅ Good: Each class has one responsibility -class WeatherDataLoader { - func loadWeather() async throws -> WeatherData { } -} - -class WeatherFormatter { - func format(_ weather: WeatherData) -> String { } -} - -class WeatherRenderer { - func render(_ formattedWeather: String) { } -} -``` - -**Open/Closed Principle** -```swift -// ✅ Good: Open for extension, closed for modification -protocol WidgetRenderer { - func render() async throws -} - -class WeatherRenderer: WidgetRenderer { - func render() async throws { - // Weather-specific rendering - } -} - -class CalendarRenderer: WidgetRenderer { - func render() async throws { - // Calendar-specific rendering - } -} -``` - -### 3. Design Patterns - -**Repository Pattern** -```swift -protocol WeatherRepository { - func getWeather(for location: Location) async throws -> WeatherData - func saveWeather(_ weather: WeatherData) async throws -} - -class WeatherRepositoryImpl: WeatherRepository { - private let remoteDataSource: RemoteDataSource - private let localDataSource: LocalDataSource - - func getWeather(for location: Location) async throws -> WeatherData { - // Try local first, fallback to remote - if let cachedWeather = try? await localDataSource.getWeather(for: location), - !cachedWeather.isExpired { - return cachedWeather - } - - let remoteWeather = try await remoteDataSource.getWeather(for: location) - try? await localDataSource.saveWeather(remoteWeather) - return remoteWeather - } -} -``` - -## Performance Best Practices - -### 1. Memory Management - -**Proper Resource Cleanup** -```swift -// ✅ Good: Proper cleanup and weak references -class WidgetManager { - private weak var delegate: WidgetDelegate? - private var cancellables = Set() - private var imageCache: NSCache - - init() { - imageCache = NSCache() - imageCache.totalCostLimit = 50 * 1024 * 1024 // 50MB - } - - deinit { - cancellables.removeAll() - cleanup() - } - - private func cleanup() { - imageCache.removeAllObjects() - // Release other resources - } -} -``` - -**Efficient Caching Strategy** -```swift -// ✅ Good: Multi-level caching -class DataCacheManager { - private let memoryCache = NSCache() - private let diskCache: DiskCache - - init() { - // Configure memory cache - memoryCache.totalCostLimit = 20 * 1024 * 1024 // 20MB - memoryCache.countLimit = 100 - - // Configure disk cache - diskCache = DiskCache(maxSize: 100 * 1024 * 1024) // 100MB - } - - func getData(for key: String) async -> Data? { - // Check memory cache first - if let data = memoryCache.object(forKey: NSString(string: key)) { - return data as Data - } - - // Check disk cache - if let data = await diskCache.getData(for: key) { - // Store in memory cache for faster access - memoryCache.setObject(data as NSData, forKey: NSString(string: key)) - return data - } - - return nil - } -} -``` - -### 2. Performance Monitoring - -**Execution Time Measurement** -```swift -// ✅ Good: Performance monitoring with thresholds -func renderWidget() async throws { - let startTime = CFAbsoluteTimeGetCurrent() - defer { - let executionTime = CFAbsoluteTimeGetCurrent() - startTime - PerformanceMonitor.shared.recordMetric(.renderTime, value: executionTime) - - if executionTime > 0.1 { // 100ms threshold - logger.warning("Widget rendering exceeded threshold: \(executionTime)s") - // Send alert to monitoring system - Analytics.shared.trackPerformanceIssue( - metric: "render_time", - value: executionTime, - threshold: 0.1 - ) - } - } - - // Rendering logic - try await performRenderingOperations() -} -``` - -**Battery Optimization** -```swift -// ✅ Good: Battery-conscious implementation -class WidgetUpdateManager { - private var updateTimer: Timer? - private let batteryMonitor = BatteryMonitor() - private let networkMonitor = NetworkMonitor() - - func scheduleUpdates() { - let updateInterval = calculateOptimalInterval() - - updateTimer = Timer.scheduledTimer( - withTimeInterval: updateInterval, - repeats: true - ) { [weak self] _ in - Task { - await self?.updateWidgetData() - } - } - } - - private func calculateOptimalInterval() -> TimeInterval { - var interval: TimeInterval = 300 // Default 5 minutes - - // Adjust based on battery level - if batteryMonitor.batteryLevel < 0.2 { - interval *= 3 // 15 minutes when battery is low - } else if batteryMonitor.isLowPowerModeEnabled { - interval *= 2 // 10 minutes in low power mode - } - - // Adjust based on network conditions - if networkMonitor.isExpensive { - interval *= 1.5 // Reduce frequency on cellular - } - - return interval - } -} -``` - -### 3. Lazy Loading and Prefetching - -```swift -// ✅ Good: Intelligent data loading -class WidgetDataManager { - private let prefetchQueue = OperationQueue() - - init() { - prefetchQueue.maxConcurrentOperationCount = 2 - prefetchQueue.qualityOfService = .utility - } - - func loadWidgetData() async throws -> WidgetData { - // Load essential data immediately - async let essentialData = loadEssentialData() - - // Prefetch secondary data in background - prefetchSecondaryData() - - return try await essentialData - } - - private func prefetchSecondaryData() { - prefetchQueue.addOperation { [weak self] in - Task { - // Prefetch images - await self?.prefetchImages() - - // Prefetch next day's weather - await self?.prefetchForecast() - } - } - } -} -``` - -## Security Best Practices - -### 1. Data Protection - -**Sensitive Data Handling** -```swift -// ✅ Good: Proper encryption for sensitive data -class SecureDataManager { - private let encryptionManager = EncryptionManager.shared - private let keychain = KeychainWrapper() - - func storeUserData(_ data: T, sensitive: Bool = false) async throws { - let jsonData = try JSONEncoder().encode(data) - - if sensitive { - // Encrypt sensitive data - let encryptedData = try await encryptionManager.encryptData( - jsonData, - keyIdentifier: "user-data-key", - requireBiometric: true - ) - - // Store in keychain - try keychain.set(encryptedData, forKey: "encrypted_user_data") - } else { - // Store non-sensitive data in UserDefaults - UserDefaults.standard.set(jsonData, forKey: "user_data") - } - } - - func retrieveUserData(_ type: T.Type, sensitive: Bool = false) async throws -> T { - if sensitive { - // Retrieve from keychain and decrypt - guard let encryptedData = keychain.getData("encrypted_user_data") else { - throw SecurityError.dataNotFound - } - - let decryptedData = try await encryptionManager.decryptData( - encryptedData, - requireBiometric: true - ) - - return try JSONDecoder().decode(type, from: decryptedData) - } else { - // Retrieve from UserDefaults - guard let data = UserDefaults.standard.data(forKey: "user_data") else { - throw SecurityError.dataNotFound - } - - return try JSONDecoder().decode(type, from: data) - } - } -} -``` - -**Network Security** -```swift -// ✅ Good: Secure network communication -class SecureNetworkManager { - private let session: URLSession - - init() { - let configuration = URLSessionConfiguration.default - - // Enable certificate pinning - configuration.tlsMinimumSupportedProtocolVersion = .TLSv12 - configuration.httpShouldUsePipelining = false - configuration.httpCookieAcceptPolicy = .never - - session = URLSession( - configuration: configuration, - delegate: self, - delegateQueue: nil - ) - } - - func fetchSecureData(from url: URL) async throws -> Data { - var request = URLRequest(url: url) - - // Add security headers - request.setValue("no-cache, no-store, must-revalidate", forHTTPHeaderField: "Cache-Control") - request.setValue("nosniff", forHTTPHeaderField: "X-Content-Type-Options") - request.setValue("1; mode=block", forHTTPHeaderField: "X-XSS-Protection") - - let (data, response) = try await session.data(for: request) - - // Validate response - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw NetworkError.invalidResponse - } - - return data - } -} - -extension SecureNetworkManager: URLSessionDelegate { - func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) { - // Implement certificate pinning - guard let serverTrust = challenge.protectionSpace.serverTrust else { - challenge.sender?.cancelAuthenticationChallenge(challenge) - return - } - - // Validate server certificate - // ... certificate validation logic ... - } -} -``` - -### 2. Privacy Compliance - -**GDPR Compliance** -```swift -// ✅ Good: Privacy-conscious data processing -class PrivacyCompliantAnalytics { - private let privacyManager = PrivacyManager.shared - - func trackUserInteraction(_ interaction: UserInteraction) async { - // Check consent before processing - guard await privacyManager.hasConsent(for: .analytics) else { - logger.info("Analytics tracking skipped - no consent") - return - } - - // Anonymize data - let anonymizedData = await anonymizeInteraction(interaction) - - // Process with purpose limitation - try? await privacyManager.processData( - anonymizedData, - for: .analytics, - subjectId: nil // Anonymous - ) - } - - private func anonymizeInteraction(_ interaction: UserInteraction) async -> AnonymizedInteraction { - return AnonymizedInteraction( - eventType: interaction.eventType, - timestamp: interaction.timestamp.rounded(to: .hour), // Round to hour - deviceType: UIDevice.current.model, - // Remove all PII - userId: nil, - location: nil, - ipAddress: nil - ) - } -} -``` - -## Code Quality Standards - -### 1. Error Handling - -**Comprehensive Error Management** -```swift -// ✅ Good: Detailed error handling with recovery -enum WidgetError: LocalizedError { - case networkUnavailable - case dataCorrupted(String) - case rateLimitExceeded(retryAfter: TimeInterval) - case authenticationFailed(reason: String) - - var errorDescription: String? { - switch self { - case .networkUnavailable: - return "Network connection is unavailable" - case .dataCorrupted(let details): - return "Data corruption detected: \(details)" - case .rateLimitExceeded(let retryAfter): - return "Rate limit exceeded. Retry after \(retryAfter) seconds" - case .authenticationFailed(let reason): - return "Authentication failed: \(reason)" - } - } - - var recoverySuggestion: String? { - switch self { - case .networkUnavailable: - return "Check your internet connection and try again" - case .dataCorrupted: - return "Clear cache and reload data" - case .rateLimitExceeded: - return "Wait before making additional requests" - case .authenticationFailed: - return "Please sign in again" - } - } - - var shouldRetry: Bool { - switch self { - case .networkUnavailable, .rateLimitExceeded: - return true - case .dataCorrupted, .authenticationFailed: - return false - } - } -} - -class WidgetService { - func fetchData() async throws -> WidgetData { - do { - return try await performFetch() - } catch let error as WidgetError { - // Handle specific widget errors - if error.shouldRetry { - // Implement retry logic - return try await retryFetch() - } - - // Log error with context - logger.error("Widget fetch failed: \(error.localizedDescription)") - - // Send to crash reporting - CrashReporter.shared.recordError(error) - - throw error - } catch { - // Handle unexpected errors - logger.error("Unexpected error: \(error)") - throw WidgetError.dataCorrupted("Unexpected error occurred") - } - } -} -``` - -### 2. Async/Await Best Practices - -**Proper Concurrency Usage** -```swift -// ✅ Good: Efficient concurrent operations -class DataLoadingManager { - func loadAllData() async throws -> CombinedData { - // Load data concurrently - async let weatherData = loadWeatherData() - async let calendarData = loadCalendarData() - async let newsData = loadNewsData() - - // Use TaskGroup for dynamic concurrency - let additionalData = try await withThrowingTaskGroup(of: (String, Data).self) { group in - let endpoints = getAdditionalEndpoints() - - for endpoint in endpoints { - group.addTask { - let data = try await self.fetchData(from: endpoint) - return (endpoint.id, data) - } - } - - var results: [String: Data] = [:] - for try await (id, data) in group { - results[id] = data - } - return results - } - - // Combine all results - return try await CombinedData( - weather: weatherData, - calendar: calendarData, - news: newsData, - additional: additionalData - ) - } - - @MainActor - func updateUI(with data: CombinedData) { - // UI updates on main actor - weatherView.update(with: data.weather) - calendarView.update(with: data.calendar) - newsView.update(with: data.news) - } -} -``` - -### 3. Code Documentation - -**Comprehensive Documentation** -```swift -/// Manages weather data fetching and caching for weather widgets. -/// -/// This class provides a thread-safe interface for loading weather data -/// from various sources with intelligent caching and error recovery. -/// -/// Example usage: -/// ```swift -/// let manager = WeatherDataManager() -/// let weather = try await manager.fetchWeather(for: location) -/// ``` -/// -/// - Important: This class requires network permissions and location access. -/// - Note: All methods are thread-safe and can be called from any queue. -public class WeatherDataManager { - - /// Fetches weather data for the specified location. - /// - /// This method first checks the cache for valid data. If no cached data is available - /// or the cache has expired, it fetches fresh data from the network. - /// - /// - Parameters: - /// - location: The geographic location for weather data - /// - useCache: Whether to use cached data if available (default: true) - /// - forceRefresh: Force a network request even if cache is valid (default: false) - /// - /// - Returns: Current weather data for the location - /// - /// - Throws: - /// - `WeatherError.networkUnavailable` if no network connection - /// - `WeatherError.invalidLocation` if location is invalid - /// - `WeatherError.rateLimitExceeded` if API rate limit is hit - /// - /// - Complexity: O(1) if cached, O(n) for network request where n is payload size - /// - Performance: Typically completes in 50-200ms for cached, 200-1000ms for network - public func fetchWeather( - for location: CLLocation, - useCache: Bool = true, - forceRefresh: Bool = false - ) async throws -> WeatherData { - // Implementation - } -} -``` - -## Testing Best Practices - -### 1. Unit Testing - -**Testable Code Structure** -```swift -// ✅ Good: Testable with dependency injection -class WeatherViewModel { - private let weatherService: WeatherServiceProtocol - private let formatter: WeatherFormatterProtocol - private let cache: CacheProtocol - - init( - weatherService: WeatherServiceProtocol, - formatter: WeatherFormatterProtocol, - cache: CacheProtocol = CacheManager.shared - ) { - self.weatherService = weatherService - self.formatter = formatter - self.cache = cache - } - - func loadWeather() async throws -> String { - // Check cache first - if let cachedWeather = await cache.get(WeatherData.self, forKey: "weather") { - return formatter.format(cachedWeather) - } - - // Fetch from service - let weather = try await weatherService.fetchWeather() - - // Cache the result - await cache.set(weather, forKey: "weather") - - return formatter.format(weather) - } -} - -// Test -class WeatherViewModelTests: XCTestCase { - func testLoadWeatherFromCache() async throws { - // Given - let mockService = MockWeatherService() - let mockFormatter = MockWeatherFormatter() - let mockCache = MockCache() - - let cachedWeather = WeatherData.mock() - mockCache.mockData["weather"] = cachedWeather - mockFormatter.formattedString = "Sunny, 25°C" - - let viewModel = WeatherViewModel( - weatherService: mockService, - formatter: mockFormatter, - cache: mockCache - ) - - // When - let result = try await viewModel.loadWeather() - - // Then - XCTAssertEqual(result, "Sunny, 25°C") - XCTAssertFalse(mockService.fetchWeatherCalled) // Should use cache - XCTAssertTrue(mockFormatter.formatCalled) - } -} -``` - -### 2. Integration Testing - -**End-to-End Testing** -```swift -// ✅ Good: Comprehensive integration test -class WidgetIntegrationTests: XCTestCase { - var widgetManager: AdvancedWidgetManager! - - override func setUp() async throws { - try await super.setUp() - widgetManager = AdvancedWidgetManager.shared - try await widgetManager.initialize() - } - - func testCompleteWidgetLifecycle() async throws { - // Given - let configuration = WidgetConfiguration( - type: .weather, - size: .medium, - displayName: "Test Weather Widget", - updateInterval: 300 - ) - - // When - Create widget - let widget = try await widgetManager.createWidget(with: configuration) - - // Then - Verify creation - XCTAssertNotNil(widget) - XCTAssertEqual(widget.state, .active) - - // When - Update widget - let testData = WeatherData.mock() - try await widgetManager.updateWidget(widget.id, with: testData) - - // Then - Verify update - XCTAssertEqual(widget.data as? WeatherData, testData) - XCTAssertGreaterThan(widget.metrics.updateCount, 0) - - // When - Check health - let healthReport = await widgetManager.performHealthCheck() - - // Then - Verify health - XCTAssertGreaterThan(healthReport.totalScore, 0.8) - XCTAssertTrue(healthReport.unhealthyWidgets.isEmpty) - - // Performance assertions - XCTAssertLessThan(widget.metrics.loadTime, 0.5) // 500ms max - XCTAssertLessThan(widget.metrics.renderTime, 0.1) // 100ms max - XCTAssertLessThan(widget.metrics.memoryUsage, 50_000_000) // 50MB max - - // When - Delete widget - try await widgetManager.deleteWidget(widget.id) - - // Then - Verify deletion - XCTAssertNil(widgetManager.getWidget(widget.id)) - } -} -``` - -## Accessibility Best Practices - -### 1. VoiceOver Support - -```swift -// ✅ Good: Comprehensive accessibility -struct AccessibleWeatherWidget: View { - let weatherData: WeatherData - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - // Temperature with proper accessibility - HStack { - Image(systemName: weatherData.iconName) - .font(.largeTitle) - .accessibilityLabel(weatherData.conditionDescription) - - Text("\(weatherData.temperature)°") - .font(.system(size: 48, weight: .bold)) - .accessibilityLabel("Temperature \(weatherData.temperature) degrees") - .accessibilityHint("Current temperature in \(weatherData.location)") - } - .accessibilityElement(children: .combine) - - // Forecast with accessibility - HStack { - ForEach(weatherData.forecast, id: \.date) { day in - VStack { - Text(day.dayAbbreviation) - .font(.caption) - - Image(systemName: day.iconName) - .font(.title3) - - Text("\(day.high)°") - .font(.caption) - } - .accessibilityElement(children: .combine) - .accessibilityLabel("\(day.dayName): \(day.conditionDescription), high of \(day.high) degrees") - } - } - } - .accessibilityElement(children: .contain) - .accessibilityLabel("Weather widget") - .accessibilityHint("Shows current weather and forecast for \(weatherData.location)") - .accessibilityAddTraits(.isButton) - .accessibilityAction { - // Open weather app - openWeatherApp() - } - } -} -``` - -### 2. Dynamic Type Support - -```swift -// ✅ Good: Responsive to text size changes -struct DynamicTypeWidget: View { - @Environment(\.sizeCategory) var sizeCategory - let data: WidgetData - - var body: some View { - VStack(spacing: spacing) { - Text(data.title) - .font(.system(.title2, design: .rounded)) - .lineLimit(titleLineLimit) - .minimumScaleFactor(0.7) - - Text(data.description) - .font(.system(.body)) - .lineLimit(descriptionLineLimit) - .fixedSize(horizontal: false, vertical: true) - } - .padding(padding) - } - - private var spacing: CGFloat { - sizeCategory.isAccessibilityCategory ? 16 : 12 - } - - private var padding: CGFloat { - sizeCategory.isAccessibilityCategory ? 20 : 16 - } - - private var titleLineLimit: Int { - sizeCategory.isAccessibilityCategory ? 3 : 2 - } - - private var descriptionLineLimit: Int { - sizeCategory.isAccessibilityCategory ? 5 : 3 - } -} -``` - -## Localization Best Practices - -### 1. Internationalization - -```swift -// ✅ Good: Proper localization -struct LocalizedStrings { - static func temperature(_ value: Double, unit: TemperatureUnit = .celsius) -> String { - let measurement = Measurement(value: value, unit: unit.unitTemperature) - let formatter = MeasurementFormatter() - formatter.locale = Locale.current - formatter.unitStyle = .short - return formatter.string(from: measurement) - } - - static var loadingMessage: String { - NSLocalizedString( - "widget.loading.message", - tableName: "Widget", - bundle: .main, - value: "Loading...", - comment: "Message shown while widget is loading data" - ) - } - - static func errorMessage(_ error: WidgetError) -> String { - let key = "widget.error.\(error.errorCode)" - return NSLocalizedString( - key, - tableName: "Widget", - bundle: .main, - value: error.localizedDescription, - comment: "Error message for widget error \(error.errorCode)" - ) - } -} - -// Localizable.strings (en) -"widget.loading.message" = "Loading..."; -"widget.error.network" = "Network connection unavailable"; -"widget.error.data" = "Unable to load data"; - -// Localizable.strings (es) -"widget.loading.message" = "Cargando..."; -"widget.error.network" = "Conexión de red no disponible"; -"widget.error.data" = "No se pueden cargar los datos"; -``` - -## Deployment & Monitoring - -### 1. Production Configuration - -```swift -// ✅ Good: Environment-specific configuration -enum Environment { - case development - case staging - case production - - static var current: Environment { - #if DEBUG - return .development - #elseif STAGING - return .staging - #else - return .production - #endif - } - - var apiBaseURL: String { - switch self { - case .development: - return "https://dev-api.example.com" - case .staging: - return "https://staging-api.example.com" - case .production: - return "https://api.example.com" - } - } - - var configuration: WidgetConfiguration { - switch self { - case .development: - return WidgetConfiguration( - updateInterval: 60, // 1 minute for testing - enableDebugLogging: true, - enableCrashReporting: false - ) - case .staging: - return WidgetConfiguration( - updateInterval: 300, // 5 minutes - enableDebugLogging: true, - enableCrashReporting: true - ) - case .production: - return WidgetConfiguration( - updateInterval: 900, // 15 minutes - enableDebugLogging: false, - enableCrashReporting: true - ) - } - } -} -``` - -### 2. Monitoring & Analytics - -```swift -// ✅ Good: Comprehensive monitoring -class WidgetMonitor { - static let shared = WidgetMonitor() - private let analytics = AdvancedAnalytics.shared - - func trackWidgetLifecycle(_ widget: Widget, event: WidgetLifecycleEvent) { - let eventData = AnalyticsEvent( - name: "widget_lifecycle", - parameters: [ - "widget_id": widget.id, - "widget_type": widget.configuration.type.rawValue, - "event": event.rawValue, - "timestamp": Date().timeIntervalSince1970 - ] - ) - - Task { - await analytics.track(eventData) - } - } - - func trackPerformance(_ widget: Widget) { - guard widget.metrics.renderTime > 0 else { return } - - let performanceData = AnalyticsEvent( - name: "widget_performance", - parameters: [ - "widget_id": widget.id, - "load_time": widget.metrics.loadTime, - "render_time": widget.metrics.renderTime, - "memory_usage": widget.metrics.memoryUsage, - "update_count": widget.metrics.updateCount, - "error_count": widget.metrics.errorCount - ] - ) - - Task { - await analytics.track(performanceData) - - // Alert if performance degraded - if widget.metrics.renderTime > 0.1 { - await sendPerformanceAlert(widget) - } - } - } - - private func sendPerformanceAlert(_ widget: Widget) async { - let alert = PerformanceAlert( - widgetId: widget.id, - metric: "render_time", - value: widget.metrics.renderTime, - threshold: 0.1, - severity: .warning - ) - - await analytics.sendAlert(alert) - } -} -``` - -## Documentation Standards - -### 1. API Documentation - -```swift -/// A comprehensive widget management system for iOS applications. -/// -/// `AdvancedWidgetManager` provides enterprise-grade functionality for creating, -/// managing, and optimizing iOS widgets with support for templates, orchestration, -/// and performance monitoring. -/// -/// ## Overview -/// -/// The manager handles the complete lifecycle of widgets including: -/// - Creation and configuration -/// - Template management -/// - Performance optimization -/// - Health monitoring -/// - Orchestration of multiple widgets -/// -/// ## Usage -/// -/// ```swift -/// let manager = AdvancedWidgetManager.shared -/// try await manager.initialize() -/// -/// let configuration = WidgetConfiguration( -/// type: .weather, -/// size: .medium, -/// displayName: "My Weather" -/// ) -/// -/// let widget = try await manager.createWidget(with: configuration) -/// ``` -/// -/// ## Performance Considerations -/// -/// The manager automatically optimizes widget performance by: -/// - Implementing intelligent caching strategies -/// - Batching network requests -/// - Managing memory usage -/// - Scheduling updates based on system conditions -/// -/// - Note: All methods are thread-safe and can be called from any queue -/// - Important: Requires iOS 16.0 or later -/// - Warning: Widget quota is limited to 100 widgets per application -public class AdvancedWidgetManager { - // Implementation -} -``` - -## Conclusion - -Following these enterprise best practices ensures that your iOS widgets are: - -- **Performant**: Sub-100ms response times with optimized memory usage -- **Secure**: Enterprise-grade encryption and privacy compliance -- **Maintainable**: Clean architecture with comprehensive testing -- **Accessible**: Full VoiceOver support and dynamic type compatibility -- **Scalable**: Proper error handling and monitoring for production deployment -- **Compliant**: GDPR, HIPAA, and industry standard compliance - -Regular code reviews, automated testing, and continuous monitoring are essential for maintaining these standards in production environments. Always prioritize user experience, security, and performance in your widget development process. \ No newline at end of file diff --git a/Documentation/ConfigurationAPI.md b/Documentation/ConfigurationAPI.md deleted file mode 100644 index 9e6e2cf..0000000 --- a/Documentation/ConfigurationAPI.md +++ /dev/null @@ -1,558 +0,0 @@ -# Configuration API - - -## Table of Contents -- [Configuration API](#configuration-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [WidgetConfiguration](#widgetconfiguration) - - [DataIntegrationConfiguration](#dataintegrationconfiguration) - - [PerformanceConfiguration](#performanceconfiguration) -- [Widget Configuration](#widget-configuration) - - [Basic Widget Configuration](#basic-widget-configuration) - - [Advanced Widget Configuration](#advanced-widget-configuration) -- [Data Integration Configuration](#data-integration-configuration) - - [Real-Time Updates Configuration](#real-time-updates-configuration) - - [Caching Configuration](#caching-configuration) - - [Network Configuration](#network-configuration) -- [Performance Configuration](#performance-configuration) - - [Memory Management](#memory-management) - - [Rendering Configuration](#rendering-configuration) - - [Background Processing](#background-processing) -- [Security Configuration](#security-configuration) - - [Encryption Configuration](#encryption-configuration) - - [Authentication Configuration](#authentication-configuration) - - [Certificate Pinning](#certificate-pinning) -- [Analytics Configuration](#analytics-configuration) - - [Usage Analytics](#usage-analytics) - - [Error Reporting](#error-reporting) -- [Localization Configuration](#localization-configuration) - - [Language Support](#language-support) - - [Regional Settings](#regional-settings) -- [Accessibility Configuration](#accessibility-configuration) - - [Accessibility Settings](#accessibility-settings) - - [Custom Accessibility](#custom-accessibility) -- [Configuration Management](#configuration-management) - - [Configuration Persistence](#configuration-persistence) - - [Configuration Validation](#configuration-validation) -- [Error Handling](#error-handling) - - [Configuration Errors](#configuration-errors) - - [Error Handling Example](#error-handling-example) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Best Practices](#best-practices) - - [Configuration Management](#configuration-management) - - [Performance Optimization](#performance-optimization) - - [Security Considerations](#security-considerations) - - - -## Overview - -The Configuration API provides comprehensive functionality for configuring widgets, data sources, and system settings in iOS applications using the iOS Widget Development Kit. This API enables developers to customize widget behavior, appearance, and functionality through various configuration options. - -## Core Classes - -### WidgetConfiguration - -The main configuration class for widget settings. - -```swift -class WidgetConfiguration { - var enableHomeScreenWidgets: Bool - var enableLockScreenWidgets: Bool - var enableLiveActivities: Bool - var enableDynamicIsland: Bool - var defaultRefreshInterval: TimeInterval - var enableBackgroundUpdates: Bool - var enableInteractions: Bool - var enableDeepLinking: Bool - var enableDynamicColors: Bool - var enableDarkMode: Bool - var enableCustomFonts: Bool - var enableAnimations: Bool -} -``` - -### DataIntegrationConfiguration - -Configuration options for data integration. - -```swift -struct DataIntegrationConfiguration { - var enableRealTimeUpdates: Bool - var enableBackgroundSync: Bool - var enableCaching: Bool - var refreshInterval: TimeInterval - var cachePolicy: CachePolicy - var enableCompression: Bool - var enableEncryption: Bool -} -``` - -### PerformanceConfiguration - -Configuration options for performance optimization. - -```swift -struct PerformanceConfiguration { - var enableHardwareAcceleration: Bool - var enableMemoryOptimization: Bool - var enableRenderingOptimization: Bool - var maxMemoryUsage: Int64 - var enableBackgroundProcessing: Bool - var enableCaching: Bool -} -``` - -## Widget Configuration - -### Basic Widget Configuration - -```swift -// Create basic widget configuration -let widgetConfig = WidgetConfiguration() -widgetConfig.enableHomeScreenWidgets = true -widgetConfig.enableLockScreenWidgets = true -widgetConfig.enableLiveActivities = true -widgetConfig.enableDynamicIsland = true -widgetConfig.defaultRefreshInterval = 300 // 5 minutes -widgetConfig.enableBackgroundUpdates = true -widgetConfig.enableInteractions = true -widgetConfig.enableDeepLinking = true - -// Apply configuration -let widgetManager = WidgetDevelopmentManager.shared -widgetManager.configure(widgetConfig) -``` - -### Advanced Widget Configuration - -```swift -// Create advanced widget configuration -let advancedConfig = WidgetConfiguration() -advancedConfig.enableDynamicColors = true -advancedConfig.enableDarkMode = true -advancedConfig.enableCustomFonts = true -advancedConfig.enableAnimations = true -advancedConfig.enableAccessibility = true -advancedConfig.enableLocalization = true -advancedConfig.enableAnalytics = true -advancedConfig.enableErrorReporting = true - -// Apply advanced configuration -widgetManager.configureAdvanced(advancedConfig) -``` - -## Data Integration Configuration - -### Real-Time Updates Configuration - -```swift -// Configure real-time updates -let realTimeConfig = RealTimeConfiguration() -realTimeConfig.enableWebSocket = true -realTimeConfig.enablePushNotifications = true -realTimeConfig.enableBackgroundUpdates = true -realTimeConfig.updateInterval = 30 // 30 seconds -realTimeConfig.maxRetryAttempts = 3 -realTimeConfig.retryInterval = 5 // 5 seconds - -// Apply real-time configuration -let dataManager = WidgetDataManager.shared -dataManager.configureRealTimeUpdates(realTimeConfig) -``` - -### Caching Configuration - -```swift -// Configure caching -let cacheConfig = CacheConfiguration() -cacheConfig.enableCaching = true -cacheConfig.maxCacheSize = 100 * 1024 * 1024 // 100 MB -cacheConfig.cacheExpiration = 3600 // 1 hour -cacheConfig.enableCompression = true -cacheConfig.enableEncryption = true - -// Apply cache configuration -dataManager.configureCaching(cacheConfig) -``` - -### Network Configuration - -```swift -// Configure network settings -let networkConfig = NetworkConfiguration() -networkConfig.timeout = 30 // 30 seconds -networkConfig.retryCount = 3 -networkConfig.enableSSLVerification = true -networkConfig.enableCertificatePinning = true -networkConfig.enableCompression = true - -// Apply network configuration -dataManager.configureNetwork(networkConfig) -``` - -## Performance Configuration - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.maxMemoryUsage = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableMemoryMonitoring = true -memoryConfig.enableAutomaticCleanup = true -memoryConfig.cleanupInterval = 300 // 5 minutes - -// Apply memory configuration -let performanceManager = WidgetPerformanceManager.shared -performanceManager.configureMemory(memoryConfig) -``` - -### Rendering Configuration - -```swift -// Configure rendering -let renderingConfig = RenderingConfiguration() -renderingConfig.enableHardwareAcceleration = true -renderingConfig.enableLayerBacking = true -renderingConfig.enableOpaqueLayers = true -renderingConfig.enableAntiAliasing = true - -// Apply rendering configuration -performanceManager.configureRendering(renderingConfig) -``` - -### Background Processing - -```swift -// Configure background processing -let backgroundConfig = BackgroundProcessingConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes -backgroundConfig.enableBackgroundFetching = true -backgroundConfig.enableBackgroundUpdates = true - -// Apply background configuration -performanceManager.configureBackgroundProcessing(backgroundConfig) -``` - -## Security Configuration - -### Encryption Configuration - -```swift -// Configure encryption -let encryptionConfig = EncryptionConfiguration() -encryptionConfig.enableEncryption = true -encryptionConfig.algorithm = .aes256 -encryptionConfig.keySize = 256 -encryptionConfig.enableKeyRotation = true - -// Apply encryption configuration -let securityManager = WidgetSecurityManager.shared -securityManager.configureEncryption(encryptionConfig) -``` - -### Authentication Configuration - -```swift -// Configure authentication -let authConfig = AuthenticationConfiguration() -authConfig.enableAuthentication = true -authConfig.authenticationType = .bearer -authConfig.tokenRefreshInterval = 3600 // 1 hour -authConfig.enableAutoRefresh = true - -// Apply authentication configuration -securityManager.configureAuthentication(authConfig) -``` - -### Certificate Pinning - -```swift -// Configure certificate pinning -let certificateConfig = CertificateConfiguration() -certificateConfig.enableCertificatePinning = true -certificateConfig.pinnedCertificates = ["cert1", "cert2"] -certificateConfig.enableSSLVerification = true -certificateConfig.enableHostValidation = true - -// Apply certificate configuration -securityManager.configureCertificates(certificateConfig) -``` - -## Analytics Configuration - -### Usage Analytics - -```swift -// Configure usage analytics -let analyticsConfig = AnalyticsConfiguration() -analyticsConfig.enableUsageTracking = true -analyticsConfig.enablePerformanceTracking = true -analyticsConfig.enableErrorTracking = true -analyticsConfig.enableCrashReporting = true - -// Apply analytics configuration -let analyticsManager = WidgetAnalyticsManager.shared -analyticsManager.configure(analyticsConfig) -``` - -### Error Reporting - -```swift -// Configure error reporting -let errorConfig = ErrorReportingConfiguration() -errorConfig.enableErrorReporting = true -errorConfig.enableCrashReporting = true -errorConfig.enablePerformanceMonitoring = true -errorConfig.enableUserFeedback = true - -// Apply error reporting configuration -analyticsManager.configureErrorReporting(errorConfig) -``` - -## Localization Configuration - -### Language Support - -```swift -// Configure localization -let localizationConfig = LocalizationConfiguration() -localizationConfig.enableLocalization = true -localizationConfig.supportedLanguages = ["en", "es", "fr", "de"] -localizationConfig.defaultLanguage = "en" -localizationConfig.enableAutoDetection = true - -// Apply localization configuration -let localizationManager = WidgetLocalizationManager.shared -localizationManager.configure(localizationConfig) -``` - -### Regional Settings - -```swift -// Configure regional settings -let regionalConfig = RegionalConfiguration() -regionalConfig.enableRegionalSettings = true -regionalConfig.dateFormat = "MM/dd/yyyy" -regionalConfig.timeFormat = "HH:mm" -regionalConfig.currencyCode = "USD" -regionalConfig.timeZone = "America/New_York" - -// Apply regional configuration -localizationManager.configureRegional(regionalConfig) -``` - -## Accessibility Configuration - -### Accessibility Settings - -```swift -// Configure accessibility -let accessibilityConfig = AccessibilityConfiguration() -accessibilityConfig.enableVoiceOver = true -accessibilityConfig.enableDynamicType = true -accessibilityConfig.enableHighContrast = true -accessibilityConfig.enableReduceMotion = true - -// Apply accessibility configuration -let accessibilityManager = WidgetAccessibilityManager.shared -accessibilityManager.configure(accessibilityConfig) -``` - -### Custom Accessibility - -```swift -// Configure custom accessibility -let customAccessibilityConfig = CustomAccessibilityConfiguration() -customAccessibilityConfig.enableCustomLabels = true -customAccessibilityConfig.enableCustomHints = true -customAccessibilityConfig.enableCustomActions = true - -// Apply custom accessibility configuration -accessibilityManager.configureCustom(customAccessibilityConfig) -``` - -## Configuration Management - -### Configuration Persistence - -```swift -// Configure persistence -let persistenceConfig = PersistenceConfiguration() -persistenceConfig.enableConfigurationPersistence = true -persistenceConfig.storageType = .userDefaults -persistenceConfig.enableEncryption = true -persistenceConfig.enableBackup = true - -// Apply persistence configuration -let configManager = WidgetConfigurationManager.shared -configManager.configurePersistence(persistenceConfig) -``` - -### Configuration Validation - -```swift -// Configure validation -let validationConfig = ValidationConfiguration() -validationConfig.enableConfigurationValidation = true -validationConfig.enableSchemaValidation = true -validationConfig.enableTypeChecking = true -validationConfig.enableConstraintValidation = true - -// Apply validation configuration -configManager.configureValidation(validationConfig) -``` - -## Error Handling - -### Configuration Errors - -```swift -enum ConfigurationError: Error { - case invalidConfiguration(String) - case missingRequiredParameter(String) - case incompatibleSettings(String) - case validationFailed(String) - case persistenceError(String) -} -``` - -### Error Handling Example - -```swift -// Handle configuration errors -func handleConfigurationError(_ error: ConfigurationError) { - switch error { - case .invalidConfiguration(let message): - print("Invalid configuration: \(message)") - // Reset to default configuration - - case .missingRequiredParameter(let parameter): - print("Missing required parameter: \(parameter)") - // Add missing parameter - - case .incompatibleSettings(let settings): - print("Incompatible settings: \(settings)") - // Resolve conflicts - - case .validationFailed(let reason): - print("Validation failed: \(reason)") - // Fix validation issues - - case .persistenceError(let message): - print("Persistence error: \(message)") - // Handle persistence issues - } -} -``` - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `configure` | Configure widget settings | `WidgetConfiguration` | `Result` | -| `configureData` | Configure data integration | `DataIntegrationConfiguration` | `Result` | -| `configurePerformance` | Configure performance settings | `PerformanceConfiguration` | `Result` | -| `configureSecurity` | Configure security settings | `SecurityConfiguration` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `enableHomeScreenWidgets` | `Bool` | Enable home screen widgets | -| `enableLockScreenWidgets` | `Bool` | Enable lock screen widgets | -| `enableLiveActivities` | `Bool` | Enable Live Activities | -| `enableDynamicIsland` | `Bool` | Enable Dynamic Island | -| `defaultRefreshInterval` | `TimeInterval` | Default refresh interval | -| `enableBackgroundUpdates` | `Bool` | Enable background updates | -| `enableInteractions` | `Bool` | Enable widget interactions | -| `enableDeepLinking` | `Bool` | Enable deep linking | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldConfig = WidgetConfiguration() -oldConfig.enableWidgets = true - -// iOS 15+ approach -let newConfig = WidgetConfiguration() -newConfig.enableHomeScreenWidgets = true -newConfig.enableLockScreenWidgets = true -newConfig.enableLiveActivities = true -newConfig.enableDynamicIsland = true -``` - -## Troubleshooting - -### Common Issues - -1. **Configuration Not Applied** - - Check configuration validity - - Verify parameter types - - Test on physical device - -2. **Performance Issues** - - Optimize configuration settings - - Monitor memory usage - - Check background processing - -3. **Security Issues** - - Verify certificate pinning - - Check encryption settings - - Validate authentication - -### Debug Tools - -```swift -// Enable configuration debugging -let debugLogger = ConfigurationDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor configuration changes -debugLogger.onConfigurationChanged = { config in - print("Configuration changed: \(config)") -} - -debugLogger.onConfigurationError = { error in - print("Configuration error: \(error)") -} -``` - -## Best Practices - -### Configuration Management - -1. **Validate Configurations**: Always validate configurations before applying -2. **Use Default Values**: Provide sensible default values -3. **Handle Errors**: Implement proper error handling -4. **Test Configurations**: Test configurations on multiple devices - -### Performance Optimization - -1. **Minimize Overhead**: Keep configuration overhead minimal -2. **Cache Configurations**: Cache frequently used configurations -3. **Lazy Loading**: Load configurations on demand -4. **Background Processing**: Use background processing for heavy operations - -### Security Considerations - -1. **Encrypt Sensitive Data**: Encrypt sensitive configuration data -2. **Validate Inputs**: Validate all configuration inputs -3. **Use Secure Storage**: Use secure storage for sensitive configurations -4. **Regular Updates**: Regularly update security configurations diff --git a/Documentation/CustomizationAPI.md b/Documentation/CustomizationAPI.md deleted file mode 100644 index 6deb45b..0000000 --- a/Documentation/CustomizationAPI.md +++ /dev/null @@ -1,695 +0,0 @@ -# Customization API - - -## Table of Contents -- [Customization API](#customization-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [WidgetCustomizationManager](#widgetcustomizationmanager) - - [WidgetStyle](#widgetstyle) - - [WidgetTheme](#widgettheme) -- [Color Customization](#color-customization) - - [Color Schemes](#color-schemes) - - [Dynamic Colors](#dynamic-colors) - - [Brand Colors](#brand-colors) -- [Font Customization](#font-customization) - - [Custom Fonts](#custom-fonts) - - [Dynamic Type](#dynamic-type) - - [Font Scaling](#font-scaling) -- [Layout Customization](#layout-customization) - - [Custom Layouts](#custom-layouts) - - [Responsive Design](#responsive-design) - - [Grid Layouts](#grid-layouts) -- [Animation Customization](#animation-customization) - - [Basic Animations](#basic-animations) - - [Custom Animations](#custom-animations) - - [Transition Effects](#transition-effects) - - [Spring Animations](#spring-animations) -- [Theming System](#theming-system) - - [Theme Creation](#theme-creation) - - [Theme Switching](#theme-switching) - - [Theme Persistence](#theme-persistence) -- [Interactive Elements](#interactive-elements) - - [Custom Buttons](#custom-buttons) - - [Gesture Recognition](#gesture-recognition) - - [Haptic Feedback](#haptic-feedback) -- [Content Customization](#content-customization) - - [Dynamic Content](#dynamic-content) - - [Conditional Styling](#conditional-styling) - - [Content Templates](#content-templates) -- [Accessibility](#accessibility) - - [Accessibility Configuration](#accessibility-configuration) - - [Custom Accessibility Labels](#custom-accessibility-labels) - - [Accessibility Actions](#accessibility-actions) -- [Performance Optimization](#performance-optimization) - - [Rendering Optimization](#rendering-optimization) - - [Memory Management](#memory-management) -- [Error Handling](#error-handling) - - [CustomizationError Types](#customizationerror-types) - - [Error Handling Example](#error-handling-example) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Best Practices](#best-practices) - - [Design Principles](#design-principles) - - [Implementation](#implementation) - - - -## Overview - -The Customization API provides comprehensive functionality for customizing widgets in iOS applications using the iOS Widget Development Kit. This API enables developers to create unique, branded, and visually appealing widgets through various customization options including colors, fonts, layouts, and animations. - -## Core Classes - -### WidgetCustomizationManager - -The main manager class for handling customization operations. - -```swift -class WidgetCustomizationManager { - static let shared = WidgetCustomizationManager() - - func applyStyle(_ style: WidgetStyle, to widget: Widget) -> Result - func applyTheme(_ theme: WidgetTheme, to widget: Widget) -> Result - func applyAnimation(_ animation: WidgetAnimation, to widget: Widget) -> Result - func applyLayout(_ layout: WidgetLayout, to widget: Widget) -> Result -} -``` - -### WidgetStyle - -The style class for widget appearance customization. - -```swift -struct WidgetStyle { - let backgroundColor: Color - let textColor: Color - let cornerRadius: CGFloat - let shadow: WidgetShadow? - let border: WidgetBorder? - - init(backgroundColor: Color, textColor: Color, cornerRadius: CGFloat) -} -``` - -### WidgetTheme - -The theme class for comprehensive widget theming. - -```swift -struct WidgetTheme { - let name: String - let colors: WidgetColorScheme - let fonts: CustomFontConfiguration - let layout: WidgetLayout - let animations: WidgetAnimationConfiguration - - init(name: String, colors: WidgetColorScheme) -} -``` - -## Color Customization - -### Color Schemes - -```swift -// Create custom color scheme -let customColorScheme = WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .systemBackground, - text: .primary -) - -// Apply color scheme -let stylingManager = WidgetStylingManager() -stylingManager.applyColorScheme(customColorScheme, to: widget) -``` - -### Dynamic Colors - -```swift -// Create dynamic color scheme -let dynamicColorScheme = DynamicColorScheme( - lightMode: WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .white, - text: .black - ), - darkMode: WidgetColorScheme( - primary: .cyan, - secondary: .lightGray, - accent: .yellow, - background: .black, - text: .white - ) -) - -// Apply dynamic colors -stylingManager.applyDynamicColorScheme(dynamicColorScheme, to: widget) -``` - -### Brand Colors - -```swift -// Create brand color scheme -let brandColorScheme = BrandColorScheme( - primary: Color(red: 0.2, green: 0.6, blue: 1.0), - secondary: Color(red: 0.1, green: 0.3, blue: 0.8), - accent: Color(red: 1.0, green: 0.8, blue: 0.0), - background: Color(red: 0.95, green: 0.95, blue: 0.95) -) - -// Apply brand colors -stylingManager.applyBrandColorScheme(brandColorScheme, to: widget) -``` - -## Font Customization - -### Custom Fonts - -```swift -// Create custom font configuration -let fontConfig = CustomFontConfiguration() -fontConfig.titleFont = .custom("Helvetica-Bold", size: 18) -fontConfig.subtitleFont = .custom("Helvetica", size: 14) -fontConfig.bodyFont = .custom("Helvetica", size: 12) -fontConfig.captionFont = .custom("Helvetica-Light", size: 10) - -// Apply custom fonts -let fontManager = CustomFontManager() -fontManager.applyFontConfiguration(fontConfig, to: widget) -``` - -### Dynamic Type - -```swift -// Configure dynamic type -let dynamicTypeConfig = DynamicTypeConfiguration() -dynamicTypeConfig.enableDynamicType = true -dynamicTypeConfig.scaleFactor = 1.0 -dynamicTypeConfig.maximumScaleFactor = 2.0 - -// Apply dynamic type -fontManager.applyDynamicTypeConfiguration(dynamicTypeConfig, to: widget) -``` - -### Font Scaling - -```swift -// Configure font scaling -let fontScalingConfig = FontScalingConfiguration() -fontScalingConfig.enableAutoScaling = true -fontScalingConfig.minimumScaleFactor = 0.5 -fontScalingConfig.maximumScaleFactor = 2.0 - -// Apply font scaling -fontManager.applyFontScalingConfiguration(fontScalingConfig, to: widget) -``` - -## Layout Customization - -### Custom Layouts - -```swift -// Create custom layout -let customLayout = WidgetLayout( - type: .grid, - columns: 2, - spacing: 8, - padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) -) - -// Apply custom layout -let layoutManager = WidgetLayoutManager() -layoutManager.applyLayout(customLayout, to: widget) -``` - -### Responsive Design - -```swift -// Create responsive layout -let responsiveLayout = ResponsiveLayout() -responsiveLayout.configure { config in - config.adaptToScreenSize = true - config.minimumSpacing = 4 - config.maximumSpacing = 16 - config.enableAutoScaling = true -} - -// Apply responsive layout -layoutManager.applyResponsiveLayout(responsiveLayout, to: widget) -``` - -### Grid Layouts - -```swift -// Create grid layout -let gridLayout = GridLayout() -gridLayout.configure { config in - config.columns = 3 - config.spacing = 8 - config.enableEqualSpacing = true - config.enableAutoSizing = true -} - -// Apply grid layout -layoutManager.applyGridLayout(gridLayout, to: widget) -``` - -## Animation Customization - -### Basic Animations - -```swift -// Create animation configuration -let animationConfig = WidgetAnimationConfiguration() -animationConfig.enableAnimations = true -animationConfig.animationDuration = 0.3 -animationConfig.animationCurve = .easeInOut - -// Apply animations -let animationManager = WidgetAnimationManager() -animationManager.applyAnimationConfiguration(animationConfig, to: widget) -``` - -### Custom Animations - -```swift -// Create custom animation -let customAnimation = WidgetAnimation( - type: .fade, - duration: 0.5, - delay: 0.1, - curve: .easeInOut -) - -// Apply custom animation -animationManager.applyCustomAnimation(customAnimation, to: widget) -``` - -### Transition Effects - -```swift -// Create transition effect -let transitionEffect = WidgetTransition( - type: .slide, - direction: .fromRight, - duration: 0.4, - curve: .easeInOut -) - -// Apply transition effect -animationManager.applyTransition(transitionEffect, to: widget) -``` - -### Spring Animations - -```swift -// Create spring animation -let springAnimation = SpringAnimation( - damping: 0.8, - response: 0.6, - blendDuration: 0.3 -) - -// Apply spring animation -animationManager.applySpringAnimation(springAnimation, to: widget) -``` - -## Theming System - -### Theme Creation - -```swift -// Create custom theme -let customTheme = WidgetTheme( - name: "Corporate Theme", - colors: customColorScheme, - fonts: fontConfig, - layout: customLayout, - animations: animationConfig -) - -// Apply custom theme -let themeManager = WidgetThemeManager() -themeManager.applyTheme(customTheme, to: widget) -``` - -### Theme Switching - -```swift -// Create multiple themes -let lightTheme = WidgetTheme(name: "Light", colors: lightColorScheme) -let darkTheme = WidgetTheme(name: "Dark", colors: darkColorScheme) -let customTheme = WidgetTheme(name: "Custom", colors: customColorScheme) - -// Switch themes -themeManager.switchTheme(to: lightTheme, for: widget) { result in - switch result { - case .success: - print("✅ Theme switched successfully") - case .failure(let error): - print("❌ Theme switch failed: \(error)") - } -} -``` - -### Theme Persistence - -```swift -// Configure theme persistence -let themePersistenceConfig = ThemePersistenceConfiguration() -themePersistenceConfig.enablePersistence = true -themePersistenceConfig.storageType = .userDefaults -themePersistenceConfig.enableAutoSave = true - -// Apply theme persistence -themeManager.configurePersistence(themePersistenceConfig) -``` - -## Interactive Elements - -### Custom Buttons - -```swift -// Create custom button style -let customButtonStyle = WidgetButtonStyle( - backgroundColor: .blue, - textColor: .white, - cornerRadius: 8, - padding: EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) -) - -// Apply custom button style -let buttonManager = WidgetButtonManager() -buttonManager.applyButtonStyle(customButtonStyle, to: widget) -``` - -### Gesture Recognition - -```swift -// Add custom gestures -let gestureManager = WidgetGestureManager() - -// Add tap gesture -gestureManager.addTapGesture(to: widget) { context in - print("Widget tapped!") - // Handle tap action -} - -// Add long press gesture -gestureManager.addLongPressGesture(to: widget) { context in - print("Widget long pressed!") - // Handle long press action -} - -// Add swipe gesture -gestureManager.addSwipeGesture(to: widget, direction: .left) { context in - print("Widget swiped left!") - // Handle swipe action -} -``` - -### Haptic Feedback - -```swift -// Configure haptic feedback -let hapticConfig = HapticFeedbackConfiguration() -hapticConfig.enableHapticFeedback = true -hapticConfig.feedbackType = .impact -hapticConfig.intensity = .medium - -// Apply haptic feedback -let hapticManager = WidgetHapticManager() -hapticManager.applyHapticConfiguration(hapticConfig, to: widget) -``` - -## Content Customization - -### Dynamic Content - -```swift -// Create dynamic content configuration -let dynamicContentConfig = DynamicContentConfiguration() -dynamicContentConfig.enableRealTimeUpdates = true -dynamicContentConfig.updateInterval = 60 // 1 minute -dynamicContentConfig.enableConditionalDisplay = true - -// Apply dynamic content -let contentManager = WidgetContentManager() -contentManager.applyDynamicContentConfiguration(dynamicContentConfig, to: widget) -``` - -### Conditional Styling - -```swift -// Create conditional styling -let conditionalStyling = ConditionalStyling() -conditionalStyling.addCondition { data in - return data.value > 100 -} style: { data in - return WidgetStyle(backgroundColor: .red) -} - -conditionalStyling.addCondition { data in - return data.value < 50 -} style: { data in - return WidgetStyle(backgroundColor: .green) -} - -// Apply conditional styling -stylingManager.applyConditionalStyling(conditionalStyling, to: widget) -``` - -### Content Templates - -```swift -// Create content template -let contentTemplate = WidgetContentTemplate( - name: "Weather Template", - layout: weatherLayout, - styling: weatherStyling, - dataMapping: weatherDataMapping -) - -// Apply content template -contentManager.applyContentTemplate(contentTemplate, to: widget) -``` - -## Accessibility - -### Accessibility Configuration - -```swift -// Configure accessibility -let accessibilityConfig = AccessibilityConfiguration() -accessibilityConfig.enableVoiceOver = true -accessibilityConfig.enableDynamicType = true -accessibilityConfig.enableHighContrast = true -accessibilityConfig.enableReduceMotion = true - -// Apply accessibility configuration -let accessibilityManager = WidgetAccessibilityManager() -accessibilityManager.applyAccessibilityConfiguration(accessibilityConfig, to: widget) -``` - -### Custom Accessibility Labels - -```swift -// Add custom accessibility labels -accessibilityManager.addAccessibilityLabel("Weather Widget", to: widget) -accessibilityManager.addAccessibilityHint("Shows current weather information", to: widget) -accessibilityManager.addAccessibilityValue("72°F, Sunny", to: widget) -``` - -### Accessibility Actions - -```swift -// Add accessibility actions -accessibilityManager.addAccessibilityAction("Refresh Weather", to: widget) { context in - // Handle refresh action - refreshWeatherData() -} - -accessibilityManager.addAccessibilityAction("Open App", to: widget) { context in - // Handle open app action - openWeatherApp() -} -``` - -## Performance Optimization - -### Rendering Optimization - -```swift -// Configure rendering optimization -let renderingConfig = RenderingConfiguration() -renderingConfig.enableHardwareAcceleration = true -renderingConfig.enableLayerBacking = true -renderingConfig.enableOpaqueLayers = true - -// Apply rendering optimization -let renderingManager = WidgetRenderingManager() -renderingManager.applyRenderingConfiguration(renderingConfig, to: widget) -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.enableImageCaching = true -memoryConfig.maxCacheSize = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableAutomaticCleanup = true - -// Apply memory management -let memoryManager = WidgetMemoryManager() -memoryManager.applyMemoryConfiguration(memoryConfig, to: widget) -``` - -## Error Handling - -### CustomizationError Types - -```swift -enum CustomizationError: Error { - case styleApplicationFailed(String) - case themeApplicationFailed(String) - case animationApplicationFailed(String) - case layoutApplicationFailed(String) - case fontApplicationFailed(String) - case accessibilityApplicationFailed(String) -} -``` - -### Error Handling Example - -```swift -func handleCustomizationError(_ error: CustomizationError) { - switch error { - case .styleApplicationFailed(let message): - print("Style application failed: \(message)") - // Handle style error - - case .themeApplicationFailed(let message): - print("Theme application failed: \(message)") - // Handle theme error - - case .animationApplicationFailed(let message): - print("Animation application failed: \(message)") - // Handle animation error - - case .layoutApplicationFailed(let message): - print("Layout application failed: \(message)") - // Handle layout error - - case .fontApplicationFailed(let message): - print("Font application failed: \(message)") - // Handle font error - - case .accessibilityApplicationFailed(let message): - print("Accessibility application failed: \(message)") - // Handle accessibility error - } -} -``` - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `applyStyle` | Apply style to widget | `WidgetStyle`, `Widget` | `Result` | -| `applyTheme` | Apply theme to widget | `WidgetTheme`, `Widget` | `Result` | -| `applyAnimation` | Apply animation to widget | `WidgetAnimation`, `Widget` | `Result` | -| `applyLayout` | Apply layout to widget | `WidgetLayout`, `Widget` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `backgroundColor` | `Color` | Widget background color | -| `textColor` | `Color` | Widget text color | -| `cornerRadius` | `CGFloat` | Widget corner radius | -| `shadow` | `WidgetShadow?` | Widget shadow | -| `border` | `WidgetBorder?` | Widget border | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldStyle = WidgetStyle(backgroundColor: .blue) - -// iOS 15+ approach -let newStyle = WidgetStyle( - backgroundColor: .blue, - textColor: .white, - cornerRadius: 12 -) -``` - -## Troubleshooting - -### Common Issues - -1. **Customization Not Applied** - - Check configuration validity - - Verify widget registration - - Test on physical device - -2. **Performance Issues** - - Optimize custom animations - - Reduce complexity - - Monitor memory usage - -3. **Visual Inconsistencies** - - Verify color schemes - - Check font availability - - Test on different devices - -### Debug Tools - -```swift -// Enable customization debugging -let debugLogger = CustomizationDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor customization operations -debugLogger.onCustomizationApplied = { customization in - print("Customization applied: \(customization)") -} - -debugLogger.onCustomizationError = { error in - print("Customization error: \(error)") -} -``` - -## Best Practices - -### Design Principles - -1. **Consistency**: Maintain consistent design language -2. **Accessibility**: Ensure accessibility compliance -3. **Performance**: Optimize for performance -4. **User Experience**: Focus on user experience - -### Implementation - -1. **Modular Design**: Use modular customization components -2. **Reusability**: Create reusable customization patterns -3. **Testing**: Test customizations on multiple devices -4. **Documentation**: Document customization options diff --git a/Documentation/CustomizationGuide.md b/Documentation/CustomizationGuide.md deleted file mode 100644 index 317333c..0000000 --- a/Documentation/CustomizationGuide.md +++ /dev/null @@ -1,649 +0,0 @@ -# Customization Guide - - -## Table of Contents -- [Customization Guide](#customization-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Import the Framework](#1-import-the-framework) - - [2. Initialize Customization Manager](#2-initialize-customization-manager) -- [Visual Customization](#visual-customization) - - [Color Schemes](#color-schemes) - - [Dynamic Colors](#dynamic-colors) - - [Custom Fonts](#custom-fonts) -- [Layout Customization](#layout-customization) - - [Custom Layouts](#custom-layouts) - - [Responsive Design](#responsive-design) -- [Animation and Transitions](#animation-and-transitions) - - [Basic Animations](#basic-animations) - - [Custom Animations](#custom-animations) - - [Transition Effects](#transition-effects) -- [Theming System](#theming-system) - - [Theme Creation](#theme-creation) - - [Theme Switching](#theme-switching) -- [Interactive Elements](#interactive-elements) - - [Custom Buttons](#custom-buttons) - - [Gesture Recognition](#gesture-recognition) -- [Content Customization](#content-customization) - - [Dynamic Content](#dynamic-content) - - [Conditional Styling](#conditional-styling) -- [Accessibility](#accessibility) - - [Accessibility Configuration](#accessibility-configuration) - - [Custom Accessibility Labels](#custom-accessibility-labels) -- [Performance Optimization](#performance-optimization) - - [Rendering Optimization](#rendering-optimization) - - [Memory Management](#memory-management) -- [Branding and Identity](#branding-and-identity) - - [Brand Colors](#brand-colors) - - [Logo Integration](#logo-integration) -- [Advanced Customization](#advanced-customization) - - [Custom Widget Types](#custom-widget-types) - - [Plugin System](#plugin-system) -- [Testing Customizations](#testing-customizations) - - [Unit Testing](#unit-testing) - - [Visual Testing](#visual-testing) -- [Deployment](#deployment) - - [Production Configuration](#production-configuration) - - [Quality Assurance](#quality-assurance) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for customizing widgets in iOS applications using the iOS Widget Development Kit. Customization enables developers to create unique, branded, and visually appealing widgets that match their app's design language and user preferences. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and design principles - -## Getting Started - -### 1. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 2. Initialize Customization Manager - -```swift -let customizationManager = WidgetCustomizationManager.shared -customizationManager.configure { config in - config.enableDynamicColors = true - config.enableCustomFonts = true - config.enableAnimations = true - config.enableTheming = true -} -``` - -## Visual Customization - -### Color Schemes - -```swift -// Create custom color scheme -let customColorScheme = WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .systemBackground, - text: .primary -) - -// Apply color scheme -let stylingManager = WidgetStylingManager() -stylingManager.applyColorScheme(customColorScheme, to: widget) -``` - -### Dynamic Colors - -```swift -// Create dynamic color scheme -let dynamicColorScheme = DynamicColorScheme( - lightMode: WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange, - background: .white, - text: .black - ), - darkMode: WidgetColorScheme( - primary: .cyan, - secondary: .lightGray, - accent: .yellow, - background: .black, - text: .white - ) -) - -// Apply dynamic colors -stylingManager.applyDynamicColorScheme(dynamicColorScheme, to: widget) -``` - -### Custom Fonts - -```swift -// Create custom font configuration -let fontConfig = CustomFontConfiguration() -fontConfig.titleFont = .custom("Helvetica-Bold", size: 18) -fontConfig.subtitleFont = .custom("Helvetica", size: 14) -fontConfig.bodyFont = .custom("Helvetica", size: 12) -fontConfig.captionFont = .custom("Helvetica-Light", size: 10) - -// Apply custom fonts -let fontManager = CustomFontManager() -fontManager.applyFontConfiguration(fontConfig, to: widget) -``` - -## Layout Customization - -### Custom Layouts - -```swift -// Create custom layout -let customLayout = WidgetLayout( - type: .grid, - columns: 2, - spacing: 8, - padding: EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) -) - -// Apply custom layout -let layoutManager = WidgetLayoutManager() -layoutManager.applyLayout(customLayout, to: widget) -``` - -### Responsive Design - -```swift -// Create responsive layout -let responsiveLayout = ResponsiveLayout() -responsiveLayout.configure { config in - config.adaptToScreenSize = true - config.minimumSpacing = 4 - config.maximumSpacing = 16 - config.enableAutoScaling = true -} - -// Apply responsive layout -layoutManager.applyResponsiveLayout(responsiveLayout, to: widget) -``` - -## Animation and Transitions - -### Basic Animations - -```swift -// Create animation configuration -let animationConfig = WidgetAnimationConfiguration() -animationConfig.enableAnimations = true -animationConfig.animationDuration = 0.3 -animationConfig.animationCurve = .easeInOut - -// Apply animations -let animationManager = WidgetAnimationManager() -animationManager.applyAnimationConfiguration(animationConfig, to: widget) -``` - -### Custom Animations - -```swift -// Create custom animation -let customAnimation = WidgetAnimation( - type: .fade, - duration: 0.5, - delay: 0.1, - curve: .easeInOut -) - -// Apply custom animation -animationManager.applyCustomAnimation(customAnimation, to: widget) -``` - -### Transition Effects - -```swift -// Create transition effect -let transitionEffect = WidgetTransition( - type: .slide, - direction: .fromRight, - duration: 0.4, - curve: .easeInOut -) - -// Apply transition effect -animationManager.applyTransition(transitionEffect, to: widget) -``` - -## Theming System - -### Theme Creation - -```swift -// Create custom theme -let customTheme = WidgetTheme( - name: "Corporate Theme", - colors: customColorScheme, - fonts: fontConfig, - layout: customLayout, - animations: animationConfig -) - -// Apply custom theme -let themeManager = WidgetThemeManager() -themeManager.applyTheme(customTheme, to: widget) -``` - -### Theme Switching - -```swift -// Create multiple themes -let lightTheme = WidgetTheme(name: "Light", colors: lightColorScheme) -let darkTheme = WidgetTheme(name: "Dark", colors: darkColorScheme) -let customTheme = WidgetTheme(name: "Custom", colors: customColorScheme) - -// Switch themes -themeManager.switchTheme(to: lightTheme, for: widget) { result in - switch result { - case .success: - print("✅ Theme switched successfully") - case .failure(let error): - print("❌ Theme switch failed: \(error)") - } -} -``` - -## Interactive Elements - -### Custom Buttons - -```swift -// Create custom button style -let customButtonStyle = WidgetButtonStyle( - backgroundColor: .blue, - textColor: .white, - cornerRadius: 8, - padding: EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) -) - -// Apply custom button style -let buttonManager = WidgetButtonManager() -buttonManager.applyButtonStyle(customButtonStyle, to: widget) -``` - -### Gesture Recognition - -```swift -// Add custom gestures -let gestureManager = WidgetGestureManager() - -// Add tap gesture -gestureManager.addTapGesture(to: widget) { context in - print("Widget tapped!") - // Handle tap action -} - -// Add long press gesture -gestureManager.addLongPressGesture(to: widget) { context in - print("Widget long pressed!") - // Handle long press action -} - -// Add swipe gesture -gestureManager.addSwipeGesture(to: widget, direction: .left) { context in - print("Widget swiped left!") - // Handle swipe action -} -``` - -## Content Customization - -### Dynamic Content - -```swift -// Create dynamic content configuration -let dynamicContentConfig = DynamicContentConfiguration() -dynamicContentConfig.enableRealTimeUpdates = true -dynamicContentConfig.updateInterval = 60 // 1 minute -dynamicContentConfig.enableConditionalDisplay = true - -// Apply dynamic content -let contentManager = WidgetContentManager() -contentManager.applyDynamicContentConfiguration(dynamicContentConfig, to: widget) -``` - -### Conditional Styling - -```swift -// Create conditional styling -let conditionalStyling = ConditionalStyling() -conditionalStyling.addCondition { data in - return data.value > 100 -} style: { data in - return WidgetStyle(backgroundColor: .red) -} - -conditionalStyling.addCondition { data in - return data.value < 50 -} style: { data in - return WidgetStyle(backgroundColor: .green) -} - -// Apply conditional styling -stylingManager.applyConditionalStyling(conditionalStyling, to: widget) -``` - -## Accessibility - -### Accessibility Configuration - -```swift -// Configure accessibility -let accessibilityConfig = AccessibilityConfiguration() -accessibilityConfig.enableVoiceOver = true -accessibilityConfig.enableDynamicType = true -accessibilityConfig.enableHighContrast = true -accessibilityConfig.enableReduceMotion = true - -// Apply accessibility configuration -let accessibilityManager = WidgetAccessibilityManager() -accessibilityManager.applyAccessibilityConfiguration(accessibilityConfig, to: widget) -``` - -### Custom Accessibility Labels - -```swift -// Add custom accessibility labels -accessibilityManager.addAccessibilityLabel("Weather Widget", to: widget) -accessibilityManager.addAccessibilityHint("Shows current weather information", to: widget) -accessibilityManager.addAccessibilityValue("72°F, Sunny", to: widget) -``` - -## Performance Optimization - -### Rendering Optimization - -```swift -// Configure rendering optimization -let renderingConfig = RenderingConfiguration() -renderingConfig.enableHardwareAcceleration = true -renderingConfig.enableLayerBacking = true -renderingConfig.enableOpaqueLayers = true - -// Apply rendering optimization -let renderingManager = WidgetRenderingManager() -renderingManager.applyRenderingConfiguration(renderingConfig, to: widget) -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.enableImageCaching = true -memoryConfig.maxCacheSize = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableAutomaticCleanup = true - -// Apply memory management -let memoryManager = WidgetMemoryManager() -memoryManager.applyMemoryConfiguration(memoryConfig, to: widget) -``` - -## Branding and Identity - -### Brand Colors - -```swift -// Create brand color scheme -let brandColorScheme = BrandColorScheme( - primary: Color(red: 0.2, green: 0.6, blue: 1.0), - secondary: Color(red: 0.1, green: 0.3, blue: 0.8), - accent: Color(red: 1.0, green: 0.8, blue: 0.0), - background: Color(red: 0.95, green: 0.95, blue: 0.95) -) - -// Apply brand colors -stylingManager.applyBrandColorScheme(brandColorScheme, to: widget) -``` - -### Logo Integration - -```swift -// Add logo to widget -let logoConfig = LogoConfiguration() -logoConfig.logoImage = UIImage(named: "app-logo") -logoConfig.logoSize = CGSize(width: 32, height: 32) -logoConfig.logoPosition = .topLeft - -// Apply logo configuration -let logoManager = WidgetLogoManager() -logoManager.applyLogoConfiguration(logoConfig, to: widget) -``` - -## Advanced Customization - -### Custom Widget Types - -```swift -// Create custom widget type -let customWidgetType = CustomWidgetType( - name: "Weather Widget", - layout: customLayout, - styling: customTheme, - interactions: customInteractions -) - -// Register custom widget type -let widgetTypeManager = WidgetTypeManager() -widgetTypeManager.registerCustomWidgetType(customWidgetType) -``` - -### Plugin System - -```swift -// Create custom plugin -let customPlugin = WidgetPlugin( - name: "Analytics Plugin", - version: "1.0.0", - configuration: pluginConfig -) - -// Register plugin -let pluginManager = WidgetPluginManager() -pluginManager.registerPlugin(customPlugin) { result in - switch result { - case .success: - print("✅ Plugin registered successfully") - case .failure(let error): - print("❌ Plugin registration failed: \(error)") - } -} -``` - -## Testing Customizations - -### Unit Testing - -```swift -import XCTest - -class CustomizationTests: XCTestCase { - var customizationManager: WidgetCustomizationManager! - - override func setUp() { - super.setUp() - customizationManager = WidgetCustomizationManager.shared - } - - func testColorSchemeApplication() { - let expectation = XCTestExpectation(description: "Color scheme application") - - let colorScheme = WidgetColorScheme( - primary: .blue, - secondary: .gray, - accent: .orange - ) - - customizationManager.applyColorScheme(colorScheme, to: widget) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Color scheme application failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testThemeSwitching() { - let expectation = XCTestExpectation(description: "Theme switching") - - let lightTheme = WidgetTheme(name: "Light") - let darkTheme = WidgetTheme(name: "Dark") - - customizationManager.switchTheme(from: lightTheme, to: darkTheme) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Theme switching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### Visual Testing - -```swift -import XCTest - -class VisualCustomizationTests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testCustomThemeAppearance() { - // Test custom theme appears correctly - let widget = app.otherElements["customWidget"] - XCTAssertTrue(widget.exists) - - // Verify custom colors - let customColor = widget.otherElements["customColor"] - XCTAssertTrue(customColor.exists) - } - - func testAnimationSmoothness() { - // Test animation smoothness - let animatedWidget = app.otherElements["animatedWidget"] - animatedWidget.tap() - - // Verify animation completes - let animationComplete = app.otherElements["animationComplete"] - XCTAssertTrue(animationComplete.waitForExistence(timeout: 2.0)) - } -} -``` - -## Deployment - -### Production Configuration - -```swift -// Production customization configuration -let productionConfig = CustomizationConfiguration() -productionConfig.enableOptimization = true -productionConfig.enableCompression = true -productionConfig.enableCaching = true -productionConfig.enablePerformanceMonitoring = true - -// Apply production configuration -customizationManager.configure(productionConfig) { result in - switch result { - case .success: - print("✅ Production customization configured") - case .failure(let error): - print("❌ Production configuration failed: \(error)") - } -} -``` - -### Quality Assurance - -```swift -// Quality assurance checklist -let qualityChecklist = CustomizationQualityChecklist() -qualityChecklist.verifyColorContrast = true -qualityChecklist.verifyAccessibility = true -qualityChecklist.verifyPerformance = true -qualityChecklist.verifyCompatibility = true - -// Run quality checks -customizationManager.runQualityChecks(qualityChecklist) { result in - switch result { - case .success(let report): - print("✅ Quality checks passed") - print("Report: \(report)") - case .failure(let errors): - print("❌ Quality checks failed: \(errors)") - } -} -``` - -## Troubleshooting - -### Common Issues - -1. **Customization Not Applied** - - Check configuration validity - - Verify widget registration - - Test on physical device - -2. **Performance Issues** - - Optimize custom animations - - Reduce complexity - - Monitor memory usage - -3. **Visual Inconsistencies** - - Verify color schemes - - Check font availability - - Test on different devices - -### Debug Tools - -```swift -// Enable customization debugging -let debugLogger = CustomizationDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor customization operations -debugLogger.onCustomizationApplied = { customization in - print("Customization applied: \(customization)") -} - -debugLogger.onCustomizationError = { error in - print("Customization error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of customizing widgets using the iOS Widget Development Kit. By following these guidelines, you can create unique, branded, and visually appealing widgets that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Customization API](CustomizationAPI.md) documentation. diff --git a/Documentation/DataIntegrationAPI.md b/Documentation/DataIntegrationAPI.md deleted file mode 100644 index edb25f1..0000000 --- a/Documentation/DataIntegrationAPI.md +++ /dev/null @@ -1,672 +0,0 @@ -# Data Integration API - - -## Table of Contents -- [Data Integration API](#data-integration-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [WidgetDataManager](#widgetdatamanager) - - [WidgetDataSource](#widgetdatasource) - - [WidgetData](#widgetdata) -- [Data Source Types](#data-source-types) - - [API Data Sources](#api-data-sources) - - [Local Data Sources](#local-data-sources) - - [Database Data Sources](#database-data-sources) -- [Data Fetching](#data-fetching) - - [Basic Data Fetching](#basic-data-fetching) - - [Cached Data Fetching](#cached-data-fetching) - - [Background Data Fetching](#background-data-fetching) -- [Real-Time Updates](#real-time-updates) - - [WebSocket Integration](#websocket-integration) - - [Push Notifications](#push-notifications) -- [Data Processing](#data-processing) - - [Data Transformation](#data-transformation) - - [Data Validation](#data-validation) -- [Caching](#caching) - - [Cache Configuration](#cache-configuration) - - [Cache Operations](#cache-operations) -- [Error Handling](#error-handling) - - [DataError Types](#dataerror-types) - - [Error Handling Example](#error-handling-example) -- [Performance Optimization](#performance-optimization) - - [Efficient Data Fetching](#efficient-data-fetching) - - [Memory Management](#memory-management) -- [Security](#security) - - [Data Encryption](#data-encryption) - - [Certificate Pinning](#certificate-pinning) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Best Practices](#best-practices) - - [Data Management](#data-management) - - [Security](#security) - - - -## Overview - -The Data Integration API provides comprehensive functionality for integrating data sources with widgets in iOS applications using the iOS Widget Development Kit. This API enables developers to fetch, process, cache, and update data from various sources including APIs, databases, and local storage. - -## Core Classes - -### WidgetDataManager - -The main manager class for handling data operations. - -```swift -class WidgetDataManager { - static let shared = WidgetDataManager() - - func fetchData(from source: WidgetDataSource) -> Result - func updateWidget(_ widget: Widget, with data: WidgetData) -> Result - func cacheData(_ data: WidgetData, for key: String) -> Result - func getCachedData(for key: String) -> Result -} -``` - -### WidgetDataSource - -The data source class for different data types. - -```swift -class WidgetDataSource { - let type: DataSourceType - let endpoint: String - let cachePolicy: CachePolicy - - init(type: DataSourceType, endpoint: String, cachePolicy: CachePolicy) - func configure(_ config: DataSourceConfiguration) -} -``` - -### WidgetData - -The data model for widget content. - -```swift -struct WidgetData { - let content: String - let timestamp: Date - let metadata: [String: Any] - let isCached: Bool - - init(content: String, timestamp: Date, metadata: [String: Any]) -} -``` - -## Data Source Types - -### API Data Sources - -```swift -// Create API data source -let apiDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.company.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure API settings -apiDataSource.configure { config in - config.timeout = 30 // 30 seconds - config.retryCount = 3 - config.headers = [ - "Authorization": "Bearer your-token", - "Content-Type": "application/json" - ] -} -``` - -### Local Data Sources - -```swift -// Create local data source -let localDataSource = WidgetDataSource( - type: .local, - endpoint: "widget-data", - cachePolicy: .localFirst -) - -// Configure local storage -localDataSource.configure { config in - config.storageType = .userDefaults - config.encryptionEnabled = true - config.compressionEnabled = true -} -``` - -### Database Data Sources - -```swift -// Create database data source -let databaseDataSource = WidgetDataSource( - type: .database, - endpoint: "widget_data_table", - cachePolicy: .databaseFirst -) - -// Configure database settings -databaseDataSource.configure { config in - config.databaseType = .sqlite - config.connectionString = "path/to/database.sqlite" - config.queryTimeout = 10 -} -``` - -## Data Fetching - -### Basic Data Fetching - -```swift -// Fetch data from API -let dataManager = WidgetDataManager.shared -dataManager.fetchData(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched successfully") - print("Data: \(data)") - - // Update widget with new data - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Cached Data Fetching - -```swift -// Fetch data with caching -dataManager.fetchDataWithCache(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched (cached: \(data.isCached))") - - if data.isCached { - print("Using cached data") - } else { - print("Using fresh data") - } - - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Background Data Fetching - -```swift -// Configure background fetching -let backgroundConfig = BackgroundFetchConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.fetchInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background fetching -dataManager.configureBackgroundFetching(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background fetching configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} - -// Start background fetching -dataManager.startBackgroundFetching(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Background data fetched") - updateWidget(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Real-Time Updates - -### WebSocket Integration - -```swift -// Create WebSocket data source -let webSocketDataSource = WidgetDataSource( - type: .websocket, - endpoint: "wss://api.company.com/ws", - cachePolicy: .realtime -) - -// Configure WebSocket settings -webSocketDataSource.configure { config in - config.autoReconnect = true - config.reconnectInterval = 5 // 5 seconds - config.maxReconnectAttempts = 10 - config.heartbeatInterval = 30 // 30 seconds -} - -// Connect to WebSocket -dataManager.connectWebSocket(webSocketDataSource) { result in - switch result { - case .success: - print("✅ WebSocket connected") - - // Listen for real-time updates - dataManager.onWebSocketMessage { message in - print("📱 Real-time message received: \(message)") - - // Parse and update widget - if let data = parseWebSocketMessage(message) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ WebSocket connection failed: \(error)") - } -} -``` - -### Push Notifications - -```swift -// Configure push notifications -let pushConfig = PushNotificationConfiguration() -pushConfig.enablePushNotifications = true -pushConfig.topic = "widget-updates" -pushConfig.enableSilentNotifications = true - -// Setup push notifications -dataManager.configurePushNotifications(pushConfig) { result in - switch result { - case .success: - print("✅ Push notifications configured") - - // Handle push notification - dataManager.onPushNotification { notification in - print("📱 Push notification received: \(notification)") - - // Extract data from notification - if let data = extractDataFromNotification(notification) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ Push notification setup failed: \(error)") - } -} -``` - -## Data Processing - -### Data Transformation - -```swift -// Create data transformer -let dataTransformer = WidgetDataTransformer() - -// Transform API data to widget format -dataTransformer.transform(apiData) { result in - switch result { - case .success(let widgetData): - print("✅ Data transformed successfully") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data transformation failed: \(error)") - } -} - -// Custom transformation -dataTransformer.addTransformation { data in - // Custom transformation logic - return WidgetData( - content: data.content, - timestamp: Date(), - metadata: data.metadata - ) -} -``` - -### Data Validation - -```swift -// Create data validator -let dataValidator = WidgetDataValidator() - -// Validate data before updating widget -dataValidator.validate(widgetData) { result in - switch result { - case .success: - print("✅ Data validation passed") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data validation failed: \(error)") - handleValidationError(error) - } -} - -// Custom validation rules -dataValidator.addValidationRule { data in - // Check if data is not empty - guard !data.content.isEmpty else { - return .failure(.emptyData) - } - - // Check if timestamp is recent - guard data.timestamp.timeIntervalSinceNow > -3600 else { - return .failure(.staleData) - } - - return .success -} -``` - -## Caching - -### Cache Configuration - -```swift -// Configure caching -let cacheConfig = CacheConfiguration() -cacheConfig.enableCaching = true -cacheConfig.maxCacheSize = 100 * 1024 * 1024 // 100 MB -cacheConfig.cacheExpiration = 3600 // 1 hour -cacheConfig.enableCompression = true - -// Setup caching -dataManager.configureCaching(cacheConfig) { result in - switch result { - case .success: - print("✅ Caching configured") - case .failure(let error): - print("❌ Cache configuration failed: \(error)") - } -} -``` - -### Cache Operations - -```swift -// Store data in cache -dataManager.cacheData(widgetData, for: "widget-key") { result in - switch result { - case .success: - print("✅ Data cached successfully") - case .failure(let error): - print("❌ Caching failed: \(error)") - } -} - -// Retrieve data from cache -dataManager.getCachedData(for: "widget-key") { result in - switch result { - case .success(let data): - print("✅ Cached data retrieved") - updateWidget(with: data) - - case .failure(let error): - print("❌ Cache retrieval failed: \(error)") - } -} - -// Clear cache -dataManager.clearCache { result in - switch result { - case .success: - print("✅ Cache cleared") - case .failure(let error): - print("❌ Cache clearing failed: \(error)") - } -} -``` - -## Error Handling - -### DataError Types - -```swift -enum DataError: Error { - case networkError(String) - case parseError(String) - case validationError(String) - case cacheError(String) - case transformationError(String) - case timeoutError(String) - case unauthorizedError(String) - case serverError(String) -} -``` - -### Error Handling Example - -```swift -func handleDataError(_ error: DataError) { - switch error { - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - - case .parseError(let message): - print("Parse error: \(message)") - // Handle parse error - - case .validationError(let message): - print("Validation error: \(message)") - // Handle validation error - - case .cacheError(let message): - print("Cache error: \(message)") - // Handle cache error - - case .transformationError(let message): - print("Transformation error: \(message)") - // Handle transformation error - - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout error - - case .unauthorizedError(let message): - print("Unauthorized error: \(message)") - // Handle unauthorized error - - case .serverError(let message): - print("Server error: \(message)") - // Handle server error - } -} -``` - -## Performance Optimization - -### Efficient Data Fetching - -```swift -// Optimize data fetching -let optimizedConfig = DataFetchConfiguration() -optimizedConfig.enableCompression = true -optimizedConfig.enablePagination = true -optimizedConfig.pageSize = 20 -optimizedConfig.enableIncrementalUpdates = true - -// Configure optimized fetching -dataManager.configureOptimizedFetching(optimizedConfig) { result in - switch result { - case .success: - print("✅ Optimized fetching configured") - case .failure(let error): - print("❌ Optimization failed: \(error)") - } -} -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.maxMemoryUsage = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableMemoryMonitoring = true -memoryConfig.enableAutomaticCleanup = true - -// Setup memory management -dataManager.configureMemoryManagement(memoryConfig) { result in - switch result { - case .success: - print("✅ Memory management configured") - case .failure(let error): - print("❌ Memory configuration failed: \(error)") - } -} -``` - -## Security - -### Data Encryption - -```swift -// Configure data encryption -let encryptionConfig = EncryptionConfiguration() -encryptionConfig.enableEncryption = true -encryptionConfig.algorithm = .aes256 -encryptionConfig.keySize = 256 - -// Setup encryption -dataManager.configureEncryption(encryptionConfig) { result in - switch result { - case .success: - print("✅ Encryption configured") - case .failure(let error): - print("❌ Encryption setup failed: \(error)") - } -} -``` - -### Certificate Pinning - -```swift -// Configure certificate pinning -let securityConfig = SecurityConfiguration() -securityConfig.enableCertificatePinning = true -securityConfig.pinnedCertificates = ["cert1", "cert2"] -securityConfig.enableSSLVerification = true - -// Setup security -dataManager.configureSecurity(securityConfig) { result in - switch result { - case .success: - print("✅ Security configured") - case .failure(let error): - print("❌ Security setup failed: \(error)") - } -} -``` - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `fetchData` | Fetch data from source | `WidgetDataSource` | `Result` | -| `updateWidget` | Update widget with data | `Widget`, `WidgetData` | `Result` | -| `cacheData` | Cache data | `WidgetData`, `String` | `Result` | -| `getCachedData` | Get cached data | `String` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `type` | `DataSourceType` | Data source type | -| `endpoint` | `String` | Data source endpoint | -| `cachePolicy` | `CachePolicy` | Cache policy | -| `content` | `String` | Widget data content | -| `timestamp` | `Date` | Data timestamp | -| `metadata` | `[String: Any]` | Data metadata | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldDataSource = DataSource(type: "api", url: "https://api.com/data") - -// iOS 15+ approach -let newDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.com/data", - cachePolicy: .cacheFirst -) -``` - -## Troubleshooting - -### Common Issues - -1. **Network Connectivity** - - Check internet connection - - Verify API endpoints - - Test with different networks - -2. **Data Format Issues** - - Validate API responses - - Check data transformation - - Verify JSON parsing - -3. **Performance Problems** - - Monitor memory usage - - Optimize data fetching - - Implement caching - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = DataDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor data operations -debugLogger.onDataFetched = { data in - print("Data fetched: \(data)") -} - -debugLogger.onDataCached = { key in - print("Data cached: \(key)") -} - -debugLogger.onDataError = { error in - print("Data error: \(error)") -} -``` - -## Best Practices - -### Data Management - -1. **Validate Data**: Always validate data before using -2. **Handle Errors**: Implement proper error handling -3. **Cache Strategically**: Use caching for performance -4. **Monitor Performance**: Monitor data operations - -### Security - -1. **Encrypt Sensitive Data**: Encrypt sensitive data -2. **Validate Inputs**: Validate all data inputs -3. **Use Secure Connections**: Use secure network connections -4. **Regular Updates**: Regularly update security measures diff --git a/Documentation/DataIntegrationGuide.md b/Documentation/DataIntegrationGuide.md deleted file mode 100644 index ce958f7..0000000 --- a/Documentation/DataIntegrationGuide.md +++ /dev/null @@ -1,785 +0,0 @@ -# Data Integration Guide - - -## Table of Contents -- [Data Integration Guide](#data-integration-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Import the Framework](#1-import-the-framework) - - [2. Initialize Data Manager](#2-initialize-data-manager) -- [Data Sources](#data-sources) - - [API Data Sources](#api-data-sources) - - [Local Data Sources](#local-data-sources) - - [Database Data Sources](#database-data-sources) -- [Data Fetching](#data-fetching) - - [Basic Data Fetching](#basic-data-fetching) - - [Cached Data Fetching](#cached-data-fetching) - - [Background Data Fetching](#background-data-fetching) -- [Real-Time Updates](#real-time-updates) - - [WebSocket Integration](#websocket-integration) - - [Push Notifications](#push-notifications) -- [Data Processing](#data-processing) - - [Data Transformation](#data-transformation) - - [Data Validation](#data-validation) -- [Caching](#caching) - - [Cache Configuration](#cache-configuration) - - [Cache Operations](#cache-operations) -- [Error Handling](#error-handling) - - [Network Errors](#network-errors) - - [Data Errors](#data-errors) - - [Recovery Strategies](#recovery-strategies) -- [Performance Optimization](#performance-optimization) - - [Efficient Data Fetching](#efficient-data-fetching) - - [Memory Management](#memory-management) -- [Security](#security) - - [Data Encryption](#data-encryption) - - [Certificate Pinning](#certificate-pinning) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [Integration Testing](#integration-testing) -- [Deployment](#deployment) - - [Production Configuration](#production-configuration) - - [Monitoring](#monitoring) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for integrating data sources with widgets in iOS applications using the iOS Widget Development Kit. Data integration enables widgets to display real-time, relevant information from various sources including APIs, databases, and local storage. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of networking and data handling - -## Getting Started - -### 1. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 2. Initialize Data Manager - -```swift -let dataManager = WidgetDataManager.shared -dataManager.configure { config in - config.enableRealTimeUpdates = true - config.enableBackgroundSync = true - config.enableCaching = true - config.refreshInterval = 300 // 5 minutes -} -``` - -## Data Sources - -### API Data Sources - -```swift -// Create API data source -let apiDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure API settings -apiDataSource.configure { config in - config.timeout = 30 // 30 seconds - config.retryCount = 3 - config.headers = [ - "Authorization": "Bearer your-token", - "Content-Type": "application/json" - ] -} -``` - -### Local Data Sources - -```swift -// Create local data source -let localDataSource = WidgetDataSource( - type: .local, - endpoint: "widget-data", - cachePolicy: .localFirst -) - -// Configure local storage -localDataSource.configure { config in - config.storageType = .userDefaults - config.encryptionEnabled = true - config.compressionEnabled = true -} -``` - -### Database Data Sources - -```swift -// Create database data source -let databaseDataSource = WidgetDataSource( - type: .database, - endpoint: "widget_data_table", - cachePolicy: .databaseFirst -) - -// Configure database settings -databaseDataSource.configure { config in - config.databaseType = .sqlite - config.connectionString = "path/to/database.sqlite" - config.queryTimeout = 10 -} -``` - -## Data Fetching - -### Basic Data Fetching - -```swift -// Fetch data from API -dataManager.fetchData(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched successfully") - print("Data: \(data)") - - // Update widget with new data - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Cached Data Fetching - -```swift -// Fetch data with caching -dataManager.fetchDataWithCache(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Data fetched (cached: \(data.isCached))") - - if data.isCached { - print("Using cached data") - } else { - print("Using fresh data") - } - - updateWidget(with: data) - - case .failure(let error): - print("❌ Data fetch failed: \(error)") - handleDataError(error) - } -} -``` - -### Background Data Fetching - -```swift -// Configure background fetching -let backgroundConfig = BackgroundFetchConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.fetchInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background fetching -dataManager.configureBackgroundFetching(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background fetching configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} - -// Start background fetching -dataManager.startBackgroundFetching(from: apiDataSource) { result in - switch result { - case .success(let data): - print("✅ Background data fetched") - updateWidget(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Real-Time Updates - -### WebSocket Integration - -```swift -// Create WebSocket data source -let webSocketDataSource = WidgetDataSource( - type: .websocket, - endpoint: "wss://api.yourcompany.com/ws", - cachePolicy: .realtime -) - -// Configure WebSocket settings -webSocketDataSource.configure { config in - config.autoReconnect = true - config.reconnectInterval = 5 // 5 seconds - config.maxReconnectAttempts = 10 - config.heartbeatInterval = 30 // 30 seconds -} - -// Connect to WebSocket -dataManager.connectWebSocket(webSocketDataSource) { result in - switch result { - case .success: - print("✅ WebSocket connected") - - // Listen for real-time updates - dataManager.onWebSocketMessage { message in - print("📱 Real-time message received: \(message)") - - // Parse and update widget - if let data = parseWebSocketMessage(message) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ WebSocket connection failed: \(error)") - } -} -``` - -### Push Notifications - -```swift -// Configure push notifications -let pushConfig = PushNotificationConfiguration() -pushConfig.enablePushNotifications = true -pushConfig.topic = "widget-updates" -pushConfig.enableSilentNotifications = true - -// Setup push notifications -dataManager.configurePushNotifications(pushConfig) { result in - switch result { - case .success: - print("✅ Push notifications configured") - - // Handle push notification - dataManager.onPushNotification { notification in - print("📱 Push notification received: \(notification)") - - // Extract data from notification - if let data = extractDataFromNotification(notification) { - updateWidget(with: data) - } - } - - case .failure(let error): - print("❌ Push notification setup failed: \(error)") - } -} -``` - -## Data Processing - -### Data Transformation - -```swift -// Create data transformer -let dataTransformer = WidgetDataTransformer() - -// Transform API data to widget format -dataTransformer.transform(apiData) { result in - switch result { - case .success(let widgetData): - print("✅ Data transformed successfully") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data transformation failed: \(error)") - } -} - -// Custom transformation -dataTransformer.addTransformation { data in - // Custom transformation logic - return WidgetData( - title: data.title, - subtitle: data.subtitle, - value: data.value, - timestamp: Date() - ) -} -``` - -### Data Validation - -```swift -// Create data validator -let dataValidator = WidgetDataValidator() - -// Validate data before updating widget -dataValidator.validate(widgetData) { result in - switch result { - case .success: - print("✅ Data validation passed") - updateWidget(with: widgetData) - - case .failure(let error): - print("❌ Data validation failed: \(error)") - handleValidationError(error) - } -} - -// Custom validation rules -dataValidator.addValidationRule { data in - // Check if data is not empty - guard !data.title.isEmpty else { - return .failure(.emptyData) - } - - // Check if timestamp is recent - guard data.timestamp.timeIntervalSinceNow > -3600 else { - return .failure(.staleData) - } - - return .success -} -``` - -## Caching - -### Cache Configuration - -```swift -// Configure caching -let cacheConfig = CacheConfiguration() -cacheConfig.enableCaching = true -cacheConfig.maxCacheSize = 100 * 1024 * 1024 // 100 MB -cacheConfig.cacheExpiration = 3600 // 1 hour -cacheConfig.enableCompression = true - -// Setup caching -dataManager.configureCaching(cacheConfig) { result in - switch result { - case .success: - print("✅ Caching configured") - case .failure(let error): - print("❌ Cache configuration failed: \(error)") - } -} -``` - -### Cache Operations - -```swift -// Store data in cache -dataManager.cacheData(widgetData, for: "widget-key") { result in - switch result { - case .success: - print("✅ Data cached successfully") - case .failure(let error): - print("❌ Caching failed: \(error)") - } -} - -// Retrieve data from cache -dataManager.getCachedData(for: "widget-key") { result in - switch result { - case .success(let data): - print("✅ Cached data retrieved") - updateWidget(with: data) - - case .failure(let error): - print("❌ Cache retrieval failed: \(error)") - } -} - -// Clear cache -dataManager.clearCache { result in - switch result { - case .success: - print("✅ Cache cleared") - case .failure(let error): - print("❌ Cache clearing failed: \(error)") - } -} -``` - -## Error Handling - -### Network Errors - -```swift -// Handle network errors -func handleNetworkError(_ error: NetworkError) { - switch error { - case .noConnection: - print("No internet connection") - useCachedData() - - case .timeout: - print("Request timeout") - retryWithBackoff() - - case .serverError(let code): - print("Server error: \(code)") - handleServerError(code) - - case .unauthorized: - print("Unauthorized access") - refreshAuthentication() - - case .forbidden: - print("Access forbidden") - handleAccessDenied() - } -} -``` - -### Data Errors - -```swift -// Handle data errors -func handleDataError(_ error: DataError) { - switch error { - case .invalidFormat: - print("Invalid data format") - useFallbackData() - - case .missingFields: - print("Missing required fields") - useDefaultValues() - - case .parseError: - print("Data parsing error") - handleParseError() - - case .validationError: - print("Data validation error") - handleValidationError() - } -} -``` - -### Recovery Strategies - -```swift -// Implement recovery strategies -func implementRecoveryStrategy(for error: Error) { - switch error { - case is NetworkError: - // Network error recovery - retryWithExponentialBackoff() - - case is DataError: - // Data error recovery - useCachedData() - - case is CacheError: - // Cache error recovery - fetchFreshData() - - default: - // Generic error recovery - logError(error) - notifyUser() - } -} -``` - -## Performance Optimization - -### Efficient Data Fetching - -```swift -// Optimize data fetching -let optimizedConfig = DataFetchConfiguration() -optimizedConfig.enableCompression = true -optimizedConfig.enablePagination = true -optimizedConfig.pageSize = 20 -optimizedConfig.enableIncrementalUpdates = true - -// Configure optimized fetching -dataManager.configureOptimizedFetching(optimizedConfig) { result in - switch result { - case .success: - print("✅ Optimized fetching configured") - case .failure(let error): - print("❌ Optimization failed: \(error)") - } -} -``` - -### Memory Management - -```swift -// Configure memory management -let memoryConfig = MemoryConfiguration() -memoryConfig.maxMemoryUsage = 50 * 1024 * 1024 // 50 MB -memoryConfig.enableMemoryMonitoring = true -memoryConfig.enableAutomaticCleanup = true - -// Setup memory management -dataManager.configureMemoryManagement(memoryConfig) { result in - switch result { - case .success: - print("✅ Memory management configured") - case .failure(let error): - print("❌ Memory configuration failed: \(error)") - } -} -``` - -## Security - -### Data Encryption - -```swift -// Configure data encryption -let encryptionConfig = EncryptionConfiguration() -encryptionConfig.enableEncryption = true -encryptionConfig.algorithm = .aes256 -encryptionConfig.keySize = 256 - -// Setup encryption -dataManager.configureEncryption(encryptionConfig) { result in - switch result { - case .success: - print("✅ Encryption configured") - case .failure(let error): - print("❌ Encryption setup failed: \(error)") - } -} -``` - -### Certificate Pinning - -```swift -// Configure certificate pinning -let securityConfig = SecurityConfiguration() -securityConfig.enableCertificatePinning = true -securityConfig.pinnedCertificates = ["cert1", "cert2"] -securityConfig.enableSSLVerification = true - -// Setup security -dataManager.configureSecurity(securityConfig) { result in - switch result { - case .success: - print("✅ Security configured") - case .failure(let error): - print("❌ Security setup failed: \(error)") - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class DataIntegrationTests: XCTestCase { - var dataManager: WidgetDataManager! - - override func setUp() { - super.setUp() - dataManager = WidgetDataManager.shared - } - - func testDataFetching() { - let expectation = XCTestExpectation(description: "Data fetching") - - let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.test.com/data", - cachePolicy: .cacheFirst - ) - - dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - expectation.fulfill() - case .failure(let error): - XCTFail("Data fetching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 10.0) - } - - func testCaching() { - let expectation = XCTestExpectation(description: "Caching") - - let testData = WidgetData(content: "Test data") - dataManager.cacheData(testData, for: "test-key") { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Caching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### Integration Testing - -```swift -import XCTest - -class DataIntegrationIntegrationTests: XCTestCase { - var dataManager: WidgetDataManager! - - override func setUp() { - super.setUp() - dataManager = WidgetDataManager.shared - } - - func testEndToEndDataFlow() { - let expectation = XCTestExpectation(description: "End-to-end data flow") - - // Setup data source - let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.test.com/widget-data", - cachePolicy: .cacheFirst - ) - - // Fetch and process data - dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - // Transform data - let transformer = WidgetDataTransformer() - transformer.transform(data) { transformResult in - switch transformResult { - case .success(let widgetData): - // Validate data - let validator = WidgetDataValidator() - validator.validate(widgetData) { validationResult in - switch validationResult { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Validation failed: \(error)") - } - } - case .failure(let error): - XCTFail("Transformation failed: \(error)") - } - } - case .failure(let error): - XCTFail("Data fetching failed: \(error)") - } - } - - wait(for: [expectation], timeout: 15.0) - } -} -``` - -## Deployment - -### Production Configuration - -```swift -// Production data configuration -let productionConfig = DataConfiguration() -productionConfig.enableEncryption = true -productionConfig.enableCertificatePinning = true -productionConfig.enableCompression = true -productionConfig.enableCaching = true -productionConfig.cacheExpiration = 1800 // 30 minutes -productionConfig.retryCount = 3 -productionConfig.timeout = 30 - -// Apply production configuration -dataManager.configure(productionConfig) { result in - switch result { - case .success: - print("✅ Production configuration applied") - case .failure(let error): - print("❌ Production configuration failed: \(error)") - } -} -``` - -### Monitoring - -```swift -// Configure monitoring -let monitoringConfig = MonitoringConfiguration() -monitoringConfig.enablePerformanceMonitoring = true -monitoringConfig.enableErrorTracking = true -monitoringConfig.enableUsageAnalytics = true - -// Setup monitoring -dataManager.configureMonitoring(monitoringConfig) { result in - switch result { - case .success: - print("✅ Monitoring configured") - case .failure(let error): - print("❌ Monitoring setup failed: \(error)") - } -} -``` - -## Troubleshooting - -### Common Issues - -1. **Network Connectivity** - - Check internet connection - - Verify API endpoints - - Test with different networks - -2. **Data Format Issues** - - Validate API responses - - Check data transformation - - Verify JSON parsing - -3. **Performance Problems** - - Monitor memory usage - - Optimize data fetching - - Implement caching - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = DataDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor data operations -debugLogger.onDataFetched = { data in - print("Data fetched: \(data)") -} - -debugLogger.onDataCached = { key in - print("Data cached: \(key)") -} - -debugLogger.onDataError = { error in - print("Data error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of data integration with widgets using the iOS Widget Development Kit. By following these guidelines, you can create robust, efficient, and secure data integration solutions that enhance the functionality of your widgets. - -For more advanced features and detailed API documentation, refer to the [Data Integration API](DataIntegrationAPI.md) documentation. diff --git a/Documentation/DynamicIslandAPI.md b/Documentation/DynamicIslandAPI.md deleted file mode 100644 index ef7485f..0000000 --- a/Documentation/DynamicIslandAPI.md +++ /dev/null @@ -1,458 +0,0 @@ -# Dynamic Island API - - -## Table of Contents -- [Dynamic Island API](#dynamic-island-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [DynamicIslandManager](#dynamicislandmanager) - - [DynamicIslandActivity](#dynamicislandactivity) - - [DynamicIslandConfiguration](#dynamicislandconfiguration) -- [View Types](#view-types) - - [Supported View Types](#supported-view-types) -- [Content Types](#content-types) - - [Basic Island Content](#basic-island-content) - - [Interactive Island Content](#interactive-island-content) -- [Usage Examples](#usage-examples) - - [Creating a Simple Dynamic Island Activity](#creating-a-simple-dynamic-island-activity) - - [Creating an Interactive Dynamic Island Activity](#creating-an-interactive-dynamic-island-activity) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Error Handling](#error-handling) - - [IslandError Types](#islanderror-types) - - [Error Handling Example](#error-handling-example) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [Design Guidelines](#design-guidelines) - - [Security Considerations](#security-considerations) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 15 to iOS 16+](#from-ios-15-to-ios-16) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tips](#debug-tips) - - - -## Overview - -The Dynamic Island API provides comprehensive functionality for creating and managing Dynamic Island experiences in iOS applications. This API enables developers to create engaging, interactive experiences that integrate seamlessly with the iPhone's Dynamic Island. - -## Core Classes - -### DynamicIslandManager - -The main manager class for handling Dynamic Island operations. - -```swift -class DynamicIslandManager { - static let shared = DynamicIslandManager() - - func start(_ activity: DynamicIslandActivity) -> Result - func stop(_ activity: DynamicIslandActivity) -> Result - func update(_ activity: DynamicIslandActivity, with data: IslandData) -> Result - func refresh(_ activity: DynamicIslandActivity) -> Result -} -``` - -### DynamicIslandActivity - -The core activity class for Dynamic Island implementations. - -```swift -class DynamicIslandActivity { - let activityType: String - let configuration: DynamicIslandConfiguration - var content: IslandContent? - - init(activityType: String, configuration: DynamicIslandConfiguration) - func setCompactView(_ content: @escaping (IslandContext) -> IslandContent) - func setExpandedView(_ content: @escaping (IslandContext) -> IslandContent) - func setMinimalView(_ content: @escaping (IslandContext) -> IslandContent) - func update(with data: IslandData) -> Result -} -``` - -### DynamicIslandConfiguration - -Configuration options for Dynamic Island activities. - -```swift -struct DynamicIslandConfiguration { - var enableCompactView: Bool - var enableExpandedView: Bool - var enableMinimalView: Bool - var enableLeadingView: Bool - var enableTrailingView: Bool - var updateInterval: TimeInterval - var enableInteractions: Bool - var enableBackgroundUpdates: Bool -} -``` - -## View Types - -### Supported View Types - -```swift -enum IslandViewType { - case compact // Compact view in Dynamic Island - case expanded // Expanded view in Dynamic Island - case minimal // Minimal view in Dynamic Island - case leading // Leading view in Dynamic Island - case trailing // Trailing view in Dynamic Island -} -``` - -## Content Types - -### Basic Island Content - -```swift -struct BasicIslandContent: IslandContent { - let title: String - let subtitle: String? - let icon: String? - let backgroundColor: Color - let textColor: Color -} -``` - -### Interactive Island Content - -```swift -struct InteractiveIslandContent: IslandContent { - let title: String - let subtitle: String? - let actions: [IslandAction] - let backgroundColor: Color - let textColor: Color -} -``` - -## Usage Examples - -### Creating a Simple Dynamic Island Activity - -```swift -// Create Dynamic Island manager -let islandManager = DynamicIslandManager.shared - -// Configure activity -let config = DynamicIslandConfiguration() -config.enableCompactView = true -config.enableExpandedView = true -config.updateInterval = 30 // 30 seconds - -// Create activity -let islandActivity = DynamicIslandActivity( - activityType: "com.company.app.music", - configuration: config -) - -// Set compact view -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} - -// Set expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} - -// Start activity -let result = islandManager.start(islandActivity) -switch result { -case .success(let activity): - print("✅ Dynamic Island activity started successfully") - print("Activity ID: \(activity.id)") -case .failure(let error): - print("❌ Activity start failed: \(error)") -} -``` - -### Creating an Interactive Dynamic Island Activity - -```swift -// Create interactive activity -let interactiveConfig = DynamicIslandConfiguration() -interactiveConfig.enableCompactView = true -interactiveConfig.enableExpandedView = true -interactiveConfig.enableInteractions = true - -let interactiveActivity = DynamicIslandActivity( - activityType: "com.company.app.delivery", - configuration: interactiveConfig -) - -// Set interactive compact view -interactiveActivity.setCompactView { context in - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - Text("Delivery") - .font(.caption) - } -} - -// Set interactive expanded view -interactiveActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Track") { - // Track package action - } - .buttonStyle(.bordered) - - Button("Contact") { - // Contact driver action - } - .buttonStyle(.bordered) - } - } - .padding() -} - -// Start interactive activity -islandManager.start(interactiveActivity) -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = IslandDataSource( - type: .api, - endpoint: "https://api.company.com/island-data", - cachePolicy: .cacheFirst -) - -// Configure real-time updates -let realTimeManager = RealTimeUpdateManager() -realTimeManager.configure { config in - config.enableWebSocket = true - config.enablePushNotifications = true - config.enableBackgroundUpdates = true -} - -// Setup real-time connection -realTimeManager.connect { result in - switch result { - case .success: - print("✅ Real-time connection established") - - // Listen for updates - realTimeManager.onDataUpdate { data in - print("📱 Real-time data received: \(data)") - - // Update activity with real-time data - islandActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Dynamic Island updated with real-time data") - case .failure(let error): - print("❌ Real-time update failed: \(error)") - } - } - } - case .failure(let error): - print("❌ Real-time connection failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -islandManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Error Handling - -### IslandError Types - -```swift -enum IslandError: Error { - case startFailed(String) - case stopFailed(String) - case updateFailed(String) - case configurationError(String) - case dataError(String) - case networkError(String) - case permissionError(String) - case timeoutError(String) -} -``` - -### Error Handling Example - -```swift -func handleIslandError(_ error: IslandError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Handle start error - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Handle update error - case .configurationError(let message): - print("Configuration error: \(message)") - // Handle configuration error - case .dataError(let message): - print("Data error: \(message)") - // Handle data error - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - case .permissionError(let message): - print("Permission error: \(message)") - // Handle permission error - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout error - } -} -``` - -## Best Practices - -### Performance Optimization - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Design Guidelines - -1. **Clear Information**: Display essential information clearly -2. **Consistent Styling**: Maintain consistent visual design -3. **Accessibility**: Ensure activities are accessible -4. **User Privacy**: Respect user privacy settings - -### Security Considerations - -1. **Data Protection**: Secure sensitive data -2. **Permission Handling**: Request only necessary permissions -3. **Network Security**: Use secure network connections -4. **Error Handling**: Handle errors gracefully - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `start` | Start a Dynamic Island activity | `DynamicIslandActivity` | `Result` | -| `stop` | Stop a Dynamic Island activity | `DynamicIslandActivity` | `Result` | -| `update` | Update activity with new data | `DynamicIslandActivity`, `IslandData` | `Result` | -| `refresh` | Refresh activity content | `DynamicIslandActivity` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `activityType` | `String` | Unique activity type identifier | -| `configuration` | `DynamicIslandConfiguration` | Activity configuration | -| `content` | `IslandContent?` | Activity content | -| `isActive` | `Bool` | Activity status | - -## Migration Guide - -### From iOS 15 to iOS 16+ - -```swift -// iOS 15 approach (deprecated) -let oldActivity = Activity(kind: "com.app.activity") - -// iOS 16+ approach -let newActivity = DynamicIslandActivity( - activityType: "com.app.activity", - configuration: DynamicIslandConfiguration() -) -``` - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting**: Check activity configuration and permissions -2. **Updates Not Working**: Verify data source and network connectivity -3. **Performance Issues**: Optimize data fetching and background processing -4. **Crash Issues**: Implement proper error handling - -### Debug Tips - -1. **Enable Logging**: Use debug logging for troubleshooting -2. **Test on Device**: Always test on physical devices with Dynamic Island -3. **Monitor Performance**: Use performance monitoring tools -4. **Check Permissions**: Verify required permissions are granted diff --git a/Documentation/DynamicIslandGuide.md b/Documentation/DynamicIslandGuide.md deleted file mode 100644 index ffd4271..0000000 --- a/Documentation/DynamicIslandGuide.md +++ /dev/null @@ -1,774 +0,0 @@ -# Dynamic Island Guide - - -## Table of Contents -- [Dynamic Island Guide](#dynamic-island-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Island Manager](#3-initialize-island-manager) -- [Creating Your First Dynamic Island Activity](#creating-your-first-dynamic-island-activity) - - [Step 1: Define Island Configuration](#step-1-define-island-configuration) - - [Step 2: Create Activity Instance](#step-2-create-activity-instance) - - [Step 3: Define Island Views](#step-3-define-island-views) - - [Step 4: Start Activity](#step-4-start-activity) -- [Activity Types and Use Cases](#activity-types-and-use-cases) - - [Music Playback](#music-playback) - - [Delivery Tracking](#delivery-tracking) - - [Ride Sharing](#ride-sharing) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Interactive Elements](#interactive-elements) - - [Action Buttons](#action-buttons) - - [Gesture Recognition](#gesture-recognition) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing Dynamic Island experiences in iOS applications using the iOS Widget Development Kit. The Dynamic Island provides users with engaging, interactive experiences that seamlessly integrate with the iPhone's hardware design. - -## Prerequisites - -- iOS 16.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- iPhone 14 Pro or later (for testing) -- Basic knowledge of SwiftUI and ActivityKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for Dynamic Island: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -NSSupportsLiveActivitiesBackgroundProcessing - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Island Manager - -```swift -let islandManager = DynamicIslandManager.shared -islandManager.configure { config in - config.enableCompactView = true - config.enableExpandedView = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Dynamic Island Activity - -### Step 1: Define Island Configuration - -```swift -let islandConfig = DynamicIslandConfiguration() -islandConfig.enableCompactView = true -islandConfig.enableExpandedView = true -islandConfig.enableMinimalView = true -islandConfig.updateInterval = 30 // 30 seconds -``` - -### Step 2: Create Activity Instance - -```swift -let islandActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.music", - configuration: islandConfig -) -``` - -### Step 3: Define Island Views - -```swift -// Set compact view -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} - -// Set expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} - -// Set minimal view -islandActivity.setMinimalView { context in - Image(systemName: "music.note") - .foregroundColor(.purple) -} -``` - -### Step 4: Start Activity - -```swift -islandManager.start(islandActivity) { result in - switch result { - case .success(let activity): - print("✅ Dynamic Island activity started successfully") - print("Activity ID: \(activity.id)") - case .failure(let error): - print("❌ Activity start failed: \(error)") - } -} -``` - -## Activity Types and Use Cases - -### Music Playback - -```swift -// Music activity configuration -let musicConfig = DynamicIslandConfiguration() -musicConfig.enableCompactView = true -musicConfig.enableExpandedView = true -musicConfig.enableInteractions = true - -let musicActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.music", - configuration: musicConfig -) - -// Set music views -musicActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} - -musicActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Delivery Tracking - -```swift -// Delivery activity -let deliveryConfig = DynamicIslandConfiguration() -deliveryConfig.enableCompactView = true -deliveryConfig.enableExpandedView = true -deliveryConfig.enableInteractions = true - -let deliveryActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.delivery", - configuration: deliveryConfig -) - -// Set delivery views -deliveryActivity.setCompactView { context in - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - Text("Delivery") - .font(.caption) - } -} - -deliveryActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Track Package") { - // Track package action - } - .buttonStyle(.bordered) - - Button("Contact Driver") { - // Contact driver action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Ride Sharing - -```swift -// Ride sharing activity -let rideConfig = DynamicIslandConfiguration() -rideConfig.enableCompactView = true -rideConfig.enableExpandedView = true -rideConfig.enableInteractions = true - -let rideActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.ride", - configuration: rideConfig -) - -// Set ride views -rideActivity.setCompactView { context in - HStack { - Image(systemName: "car.fill") - .foregroundColor(.green) - Text("Ride") - .font(.caption) - } -} - -rideActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "car.fill") - .foregroundColor(.green) - - VStack(alignment: .leading) { - Text("Your ride is arriving") - .font(.headline) - Text("Driver: John Doe") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("2 min") - .font(.caption2) - .foregroundColor(.green) - } - - HStack(spacing: 16) { - Button("Call Driver") { - // Call driver action - } - .buttonStyle(.bordered) - - Button("Cancel Ride") { - // Cancel ride action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = IslandDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/island-data", - cachePolicy: .cacheFirst -) - -// Configure real-time updates -let realTimeManager = RealTimeUpdateManager() -realTimeManager.configure { config in - config.enableWebSocket = true - config.enablePushNotifications = true - config.enableBackgroundUpdates = true -} - -// Setup real-time connection -realTimeManager.connect { result in - switch result { - case .success: - print("✅ Real-time connection established") - - // Listen for updates - realTimeManager.onDataUpdate { data in - print("📱 Real-time data received: \(data)") - - // Update activity with real-time data - islandActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Dynamic Island updated with real-time data") - case .failure(let error): - print("❌ Real-time update failed: \(error)") - } - } - } - case .failure(let error): - print("❌ Real-time connection failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -islandManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Interactive Elements - -### Action Buttons - -```swift -// Add action buttons to expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Gesture Recognition - -```swift -// Add gesture recognition -islandActivity.onTap { context in - print("Dynamic Island tapped!") - // Handle tap gesture -} - -islandActivity.onLongPress { context in - print("Dynamic Island long pressed!") - // Handle long press gesture -} - -islandActivity.onSwipe { direction in - print("Dynamic Island swiped: \(direction)") - // Handle swipe gesture -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom island style -let customStyle = IslandStyle( - backgroundColor: .systemPurple, - cornerRadius: 12, - shadow: IslandShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = IslandStylingManager() -stylingManager.applyStyle(customStyle, to: islandActivity) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicIslandTheme( - lightMode: IslandTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .purple - ), - darkMode: IslandTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicIslandThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Performance Optimization - -### Best Practices - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = IslandPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor island performance -performanceMonitor.startMonitoring(islandActivity) { metrics in - print("Dynamic Island performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Activity Start Failed** - - Check activity configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleIslandError(_ error: IslandError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Retry start or show user message - - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout issues - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class DynamicIslandTests: XCTestCase { - var islandActivity: DynamicIslandActivity! - - override func setUp() { - super.setUp() - let config = DynamicIslandConfiguration() - islandActivity = DynamicIslandActivity(activityType: "test.activity", configuration: config) - } - - func testActivityStart() { - let expectation = XCTestExpectation(description: "Activity start") - - let manager = DynamicIslandManager.shared - manager.start(islandActivity) { result in - switch result { - case .success(let activity): - expectation.fulfill() - case .failure(let error): - XCTFail("Activity start failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testActivityUpdate() { - let expectation = XCTestExpectation(description: "Activity update") - - let testData = IslandData(content: "Test content") - islandActivity.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class DynamicIslandUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testIslandAppearance() { - // Test Dynamic Island appears - let island = app.otherElements["dynamicIsland"] - XCTAssertTrue(island.exists) - } - - func testIslandInteraction() { - // Test Dynamic Island interactions - let island = app.otherElements["dynamicIsland"] - island.tap() - - // Verify interaction handled - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Activity tested on iPhone 14 Pro or later -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting** - - Check activity configuration - - Verify permissions - - Test on iPhone 14 Pro or later - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = IslandDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor activity lifecycle -debugLogger.onActivityStarted = { activity in - print("Dynamic Island activity started: \(activity.activityType)") -} - -debugLogger.onActivityUpdated = { activity in - print("Dynamic Island activity updated: \(activity.activityType)") -} - -debugLogger.onActivityStopped = { activity in - print("Dynamic Island activity stopped: \(activity.activityType)") -} - -debugLogger.onActivityError = { error in - print("Dynamic Island activity error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating Dynamic Island experiences using the iOS Widget Development Kit. By following these guidelines, you can create engaging, interactive experiences that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Dynamic Island API](DynamicIslandAPI.md) documentation. diff --git a/Documentation/GettingStarted.md b/Documentation/GettingStarted.md deleted file mode 100644 index 31016b6..0000000 --- a/Documentation/GettingStarted.md +++ /dev/null @@ -1,35 +0,0 @@ -# 🚀 Getting Started - -Welcome to our world-class iOS development framework! This guide will help you get started quickly and efficiently. - -## Prerequisites - -- iOS 15.0+ -- Xcode 14.0+ -- Swift 5.9+ -- macOS 12.0+ - -## Installation - -### Swift Package Manager - -Add the following dependency to your `Package.swift`: - -```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/REPO_NAME.git", from: "1.0.0") -] -``` - -## Basic Setup - -1. Import the framework -2. Initialize the main component -3. Configure your settings -4. Start using the features - -## Next Steps - -- Check out our [Examples](Examples/) folder -- Read the [API Documentation](API.md) -- Explore [Advanced Usage](AdvancedUsage.md) diff --git a/Documentation/HomeScreenWidgetAPI.md b/Documentation/HomeScreenWidgetAPI.md deleted file mode 100644 index 8064783..0000000 --- a/Documentation/HomeScreenWidgetAPI.md +++ /dev/null @@ -1,439 +0,0 @@ -# Home Screen Widget API - - -## Table of Contents -- [Home Screen Widget API](#home-screen-widget-api) -- [Overview](#overview) -- [Core Features](#core-features) - - [Widget Types](#widget-types) - - [Interactive Features](#interactive-features) - - [Data Integration](#data-integration) -- [API Reference](#api-reference) - - [HomeScreenWidget](#homescreenwidget) - - [HomeWidgetConfiguration](#homewidgetconfiguration) - - [HomeWidgetContentProvider](#homewidgetcontentprovider) - - [HomeWidgetInteractionHandler](#homewidgetinteractionhandler) -- [Usage Examples](#usage-examples) - - [Basic Home Screen Widget](#basic-home-screen-widget) - - [Interactive Home Screen Widget](#interactive-home-screen-widget) - - [Data-Driven Home Screen Widget](#data-driven-home-screen-widget) -- [Widget Sizes](#widget-sizes) - - [Small Widget (155x155)](#small-widget-155x155) - - [Medium Widget (329x155)](#medium-widget-329x155) - - [Large Widget (329x345)](#large-widget-329x345) - - [Extra Large Widget (329x345)](#extra-large-widget-329x345) -- [Best Practices](#best-practices) - - [Performance](#performance) - - [User Experience](#user-experience) - - [Data Management](#data-management) - - [Interactions](#interactions) -- [Related Documentation](#related-documentation) - - - -## Overview - -The `HomeScreenWidget` API provides comprehensive functionality for creating and managing home screen widgets in iOS applications. This API enables developers to build beautiful, interactive, and data-driven widgets that enhance the user experience on the iOS home screen. - -## Core Features - -### Widget Types -- **Small Widgets**: Compact information display -- **Medium Widgets**: Detailed information with interactions -- **Large Widgets**: Rich content with multiple sections -- **Extra Large Widgets**: Comprehensive data visualization - -### Interactive Features -- **Tap Gestures**: Handle user taps and interactions -- **Deep Linking**: Navigate to specific app sections -- **Dynamic Content**: Real-time content updates -- **Custom Animations**: Smooth transitions and effects - -### Data Integration -- **Real-Time Updates**: Live data synchronization -- **Background Refresh**: Automatic content updates -- **Data Caching**: Efficient data management -- **Offline Support**: Graceful offline handling - -## API Reference - -### HomeScreenWidget - -The main class for home screen widget implementation. - -```swift -class HomeScreenWidget: Widget { - // MARK: - Initialization - - /// Initialize with widget kind and configuration - init(kind: String, configuration: HomeWidgetConfiguration) - - /// Initialize with custom content provider - init(kind: String, contentProvider: HomeWidgetContentProvider) - - // MARK: - Configuration - - /// Widget configuration - var configuration: HomeWidgetConfiguration { get set } - - /// Widget content provider - var contentProvider: HomeWidgetContentProvider { get set } - - /// Widget interaction handler - var interactionHandler: HomeWidgetInteractionHandler? { get set } - - // MARK: - Content Management - - /// Set widget content - func setContent(_ content: @escaping (WidgetContext) -> WidgetView) - - /// Update widget content - func updateContent(with data: WidgetData) async throws - - /// Refresh widget content - func refreshContent() async throws - - // MARK: - Interaction Handling - - /// Handle widget tap - func onTap(_ handler: @escaping (WidgetContext) -> Void) - - /// Handle widget long press - func onLongPress(_ handler: @escaping (WidgetContext) -> Void) - - /// Handle widget drag - func onDrag(_ handler: @escaping (WidgetContext, DragGesture) -> Void) -} -``` - -### HomeWidgetConfiguration - -Configuration class for home screen widget settings. - -```swift -struct HomeWidgetConfiguration { - // MARK: - Widget Settings - - /// Widget size - var widgetSize: WidgetSize = .medium - - /// Enable interactions - var enableInteractions: Bool = true - - /// Enable deep linking - var enableDeepLinking: Bool = true - - /// Refresh interval in seconds - var refreshInterval: TimeInterval = 300 - - /// Enable background updates - var enableBackgroundUpdates: Bool = true - - // MARK: - Styling Settings - - /// Widget background color - var backgroundColor: Color = .systemBackground - - /// Widget corner radius - var cornerRadius: CGFloat = 12 - - /// Widget shadow - var shadow: WidgetShadow? - - /// Widget border - var border: WidgetBorder? - - // MARK: - Content Settings - - /// Enable dynamic content - var enableDynamicContent: Bool = true - - /// Enable animations - var enableAnimations: Bool = true - - /// Enable accessibility - var enableAccessibility: Bool = true -} -``` - -### HomeWidgetContentProvider - -Protocol for providing widget content. - -```swift -protocol HomeWidgetContentProvider { - /// Create widget content - func createContent(for context: WidgetContext) -> WidgetView - - /// Update widget content - func updateContent(_ view: WidgetView, with context: WidgetContext) async throws - - /// Handle content refresh - func refreshContent() async throws -> WidgetData -} -``` - -### HomeWidgetInteractionHandler - -Protocol for handling widget interactions. - -```swift -protocol HomeWidgetInteractionHandler { - /// Handle tap interaction - func handleTap(_ context: WidgetContext) async throws - - /// Handle long press interaction - func handleLongPress(_ context: WidgetContext) async throws - - /// Handle drag interaction - func handleDrag(_ context: WidgetContext, gesture: DragGesture) async throws -} -``` - -## Usage Examples - -### Basic Home Screen Widget - -```swift -import WidgetDevelopmentKit - -// Create home screen widget -let homeWidget = HomeScreenWidget( - kind: "com.company.app.homewidget", - configuration: HomeWidgetConfiguration() -) - -// Set widget content -homeWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "house.fill") - .foregroundColor(.blue) - .font(.title2) - - Spacer() - - Text("Welcome") - .font(.headline) - .foregroundColor(.primary) - } - - Text("You have 5 new messages") - .font(.caption) - .foregroundColor(.secondary) - - Button("Open App") { - // Deep link to app - } - .buttonStyle(.bordered) - } - .padding() - .background(Color(.systemBackground)) -} - -// Handle widget tap -homeWidget.onTap { context in - print("Widget tapped!") - // Navigate to app -} - -// Register widget -let widgetManager = WidgetDevelopmentManager() -try await widgetManager.register(homeWidget) -``` - -### Interactive Home Screen Widget - -```swift -// Create interactive widget -let interactiveWidget = HomeScreenWidget( - kind: "com.company.app.interactive", - configuration: HomeWidgetConfiguration() -) - -// Set interactive content -interactiveWidget.setContent { context in - VStack(spacing: 16) { - HStack { - Image(systemName: "message.fill") - .foregroundColor(.green) - .font(.title) - - VStack(alignment: .leading) { - Text("Messages") - .font(.headline) - - Text("3 new messages") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("View") { - // Open messages - } - .buttonStyle(.bordered) - } - - HStack(spacing: 12) { - Button("Reply") { - // Quick reply - } - .buttonStyle(.bordered) - - Button("Mark Read") { - // Mark as read - } - .buttonStyle(.bordered) - } - } - .padding() -} - -// Handle interactions -interactiveWidget.onTap { context in - // Handle tap -} - -interactiveWidget.onLongPress { context in - // Show options menu -} -``` - -### Data-Driven Home Screen Widget - -```swift -// Create data-driven widget -let dataWidget = HomeScreenWidget( - kind: "com.company.app.datawidget", - configuration: HomeWidgetConfiguration() -) - -// Set data-driven content -dataWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "chart.line.uptrend.xyaxis") - .foregroundColor(.blue) - .font(.title2) - - Spacer() - - Text("Analytics") - .font(.headline) - } - - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Today's Sales") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("$1,234") - .font(.caption) - .fontWeight(.semibold) - } - - HStack { - Text("Orders") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("12") - .font(.caption) - .fontWeight(.semibold) - } - - HStack { - Text("Customers") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("8") - .font(.caption) - .fontWeight(.semibold) - } - } - - Button("View Details") { - // Open analytics - } - .buttonStyle(.bordered) - } - .padding() -} - -// Update widget with real-time data -Task { - let data = try await fetchAnalyticsData() - try await dataWidget.updateContent(with: data) -} -``` - -## Widget Sizes - -### Small Widget (155x155) -- Compact information display -- Single data point or status -- Minimal interactions -- Quick glance information - -### Medium Widget (329x155) -- Detailed information display -- Multiple data points -- Interactive elements -- Rich content presentation - -### Large Widget (329x345) -- Comprehensive information -- Multiple sections -- Rich interactions -- Data visualization - -### Extra Large Widget (329x345) -- Maximum content display -- Complex layouts -- Advanced interactions -- Full feature set - -## Best Practices - -### Performance -- Optimize widget rendering -- Minimize memory usage -- Use efficient data fetching -- Implement proper caching - -### User Experience -- Provide meaningful content -- Handle loading states -- Support accessibility -- Implement smooth animations - -### Data Management -- Cache frequently used data -- Handle offline scenarios -- Validate data integrity -- Provide fallback content - -### Interactions -- Make interactions intuitive -- Provide visual feedback -- Handle edge cases -- Support accessibility - -## Related Documentation - -- [Widget Development Manager API](WidgetDevelopmentManagerAPI.md) -- [Lock Screen Widget API](LockScreenWidgetAPI.md) -- [Live Activity API](LiveActivityAPI.md) -- [Dynamic Island API](DynamicIslandAPI.md) -- [Data Integration API](DataIntegrationAPI.md) -- [Configuration API](ConfigurationAPI.md) diff --git a/Documentation/HomeScreenWidgetGuide.md b/Documentation/HomeScreenWidgetGuide.md deleted file mode 100644 index 68698c7..0000000 --- a/Documentation/HomeScreenWidgetGuide.md +++ /dev/null @@ -1,592 +0,0 @@ -# Home Screen Widget Guide - - -## Table of Contents -- [Home Screen Widget Guide](#home-screen-widget-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Widget Manager](#3-initialize-widget-manager) -- [Creating Your First Home Screen Widget](#creating-your-first-home-screen-widget) - - [Step 1: Define Widget Configuration](#step-1-define-widget-configuration) - - [Step 2: Create Widget Instance](#step-2-create-widget-instance) - - [Step 3: Define Widget Content](#step-3-define-widget-content) - - [Step 4: Register Widget](#step-4-register-widget) -- [Widget Sizes and Layouts](#widget-sizes-and-layouts) - - [Supported Sizes](#supported-sizes) - - [Layout Guidelines](#layout-guidelines) -- [Data Integration](#data-integration) - - [Setting Up Data Sources](#setting-up-data-sources) - - [Updating Widget with Data](#updating-widget-with-data) -- [Interactive Widgets](#interactive-widgets) - - [Adding Interactions](#adding-interactions) - - [Action Buttons](#action-buttons) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Background Updates](#background-updates) - - [Configuring Background Updates](#configuring-background-updates) - - [Background Data Fetching](#background-data-fetching) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing home screen widgets in iOS applications using the iOS Widget Development Kit. Home screen widgets provide users with quick access to important information and app functionality directly from their home screen. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and WidgetKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for home screen widgets: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Widget Manager - -```swift -let widgetManager = WidgetDevelopmentManager.shared -widgetManager.configure { config in - config.enableHomeScreenWidgets = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Home Screen Widget - -### Step 1: Define Widget Configuration - -```swift -let homeWidgetConfig = HomeWidgetConfiguration() -homeWidgetConfig.widgetSize = .medium -homeWidgetConfig.enableInteractions = true -homeWidgetConfig.enableDeepLinking = true -homeWidgetConfig.refreshInterval = 300 // 5 minutes -``` - -### Step 2: Create Widget Instance - -```swift -let homeWidget = HomeScreenWidget( - kind: "com.yourcompany.app.homewidget", - configuration: homeWidgetConfig -) -``` - -### Step 3: Define Widget Content - -```swift -homeWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "message.fill") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text("Messages") - .font(.headline) - Text("5 new messages") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - Button("Open App") { - // Deep link to app - } - .buttonStyle(.bordered) - } - .padding() - .background(Color(.systemBackground)) -} -``` - -### Step 4: Register Widget - -```swift -let homeWidgetManager = HomeScreenWidgetManager.shared -homeWidgetManager.register(homeWidget) { result in - switch result { - case .success: - print("✅ Home screen widget registered successfully") - case .failure(let error): - print("❌ Registration failed: \(error)") - } -} -``` - -## Widget Sizes and Layouts - -### Supported Sizes - -| Size | Dimensions | Use Case | -|------|------------|----------| -| Small | 158x158 | Quick info, status | -| Medium | 338x158 | Detailed info, actions | -| Large | 338x354 | Rich content, lists | -| Extra Large | 338x548 | Complex layouts | - -### Layout Guidelines - -1. **Small Widgets**: Focus on essential information -2. **Medium Widgets**: Balance information and actions -3. **Large Widgets**: Provide detailed content -4. **Extra Large Widgets**: Complex interactions - -## Data Integration - -### Setting Up Data Sources - -```swift -// Create data source -let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure data manager -let dataManager = WidgetDataManager() -dataManager.configure { config in - config.enableRealTimeUpdates = true - config.enableBackgroundSync = true - config.refreshInterval = 300 -} -``` - -### Updating Widget with Data - -```swift -// Fetch data -dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - // Update widget - homeWidget.update(with: data) { result in - switch result { - case .success: - print("✅ Widget updated with new data") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -## Interactive Widgets - -### Adding Interactions - -```swift -// Create interactive widget -let interactiveConfig = HomeWidgetConfiguration() -interactiveConfig.enableInteractions = true -interactiveConfig.enableDeepLinking = true - -let interactiveWidget = HomeScreenWidget( - kind: "com.yourcompany.app.interactive", - configuration: interactiveConfig -) - -// Add interaction handlers -interactiveWidget.onTap { context in - // Handle tap interaction - print("Widget tapped!") - - // Deep link to app - if let url = URL(string: "yourapp://open") { - context.openURL(url) - } -} - -interactiveWidget.onLongPress { context in - // Handle long press - print("Widget long pressed!") -} -``` - -### Action Buttons - -```swift -interactiveWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Now Playing") - .font(.headline) - Text("Song Title") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom style -let customStyle = WidgetStyle( - backgroundColor: .systemBlue, - cornerRadius: 12, - shadow: WidgetShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = WidgetStylingManager() -stylingManager.applyStyle(customStyle, to: homeWidget) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicTheme( - lightMode: WidgetTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .blue - ), - darkMode: WidgetTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Background Updates - -### Configuring Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -widgetManager.configureBackgroundUpdates(backgroundConfig) -``` - -### Background Data Fetching - -```swift -// Background data source -let backgroundDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/background-data", - cachePolicy: .backgroundFirst -) - -// Configure background fetching -dataManager.configureBackgroundFetching(backgroundDataSource) { result in - switch result { - case .success(let data): - // Update widget in background - homeWidget.update(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Performance Optimization - -### Best Practices - -1. **Minimize Updates**: Only update when necessary -2. **Efficient Data Fetching**: Use caching and background updates -3. **Memory Management**: Properly manage widget resources -4. **Battery Optimization**: Minimize background processing - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = WidgetPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor widget performance -performanceMonitor.startMonitoring(homeWidget) { metrics in - print("Widget performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Registration Failed** - - Check widget configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleWidgetError(_ error: WidgetError) { - switch error { - case .registrationFailed(let message): - print("Registration failed: \(message)") - // Retry registration or show user message - - case .updateFailed(let message): - print("Update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class HomeScreenWidgetTests: XCTestCase { - var homeWidget: HomeScreenWidget! - - override func setUp() { - super.setUp() - let config = HomeWidgetConfiguration() - homeWidget = HomeScreenWidget(kind: "test.widget", configuration: config) - } - - func testWidgetRegistration() { - let expectation = XCTestExpectation(description: "Widget registration") - - let manager = HomeScreenWidgetManager.shared - manager.register(homeWidget) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Registration failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testWidgetUpdate() { - let expectation = XCTestExpectation(description: "Widget update") - - let testData = WidgetData(content: "Test content") - homeWidget.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class HomeScreenWidgetUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testWidgetAppearance() { - // Test widget appears on home screen - let widget = app.otherElements["homeScreenWidget"] - XCTAssertTrue(widget.exists) - } - - func testWidgetInteraction() { - // Test widget interactions - let widget = app.otherElements["homeScreenWidget"] - widget.tap() - - // Verify app opens - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Widget tested on multiple devices -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Widget Not Appearing** - - Check registration status - - Verify permissions - - Test on physical device - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = WidgetDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor widget lifecycle -debugLogger.onWidgetRegistered = { widget in - print("Widget registered: \(widget.kind)") -} - -debugLogger.onWidgetUpdated = { widget in - print("Widget updated: \(widget.kind)") -} - -debugLogger.onWidgetError = { error in - print("Widget error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating home screen widgets using the iOS Widget Development Kit. By following these guidelines, you can create engaging, performant, and user-friendly home screen widgets that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Home Screen Widget API](HomeScreenWidgetAPI.md) documentation. diff --git a/Documentation/InstallationGuide.md b/Documentation/InstallationGuide.md deleted file mode 100644 index fb25f22..0000000 --- a/Documentation/InstallationGuide.md +++ /dev/null @@ -1,122 +0,0 @@ -# Installation Guide - - -## Table of Contents -- [Installation Guide](#installation-guide) -- [Prerequisites](#prerequisites) -- [Installation Methods](#installation-methods) - - [Swift Package Manager (Recommended)](#swift-package-manager-recommended) - - [Manual Installation](#manual-installation) -- [Configuration](#configuration) - - [Basic Setup](#basic-setup) - - [Advanced Configuration](#advanced-configuration) -- [Dependencies](#dependencies) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Support](#support) - - - -## Prerequisites - -- iOS 16.0+ -- Xcode 14.0+ -- Swift 5.9+ -- macOS 13.0+ (for development) - -## Installation Methods - -### Swift Package Manager (Recommended) - -1. Open your Xcode project -2. Go to File → Add Package Dependencies -3. Enter the repository URL: - ``` - https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git - ``` -4. Select version 3.2.0 or later -5. Choose the targets you want to include: - - WidgetKit (Core functionality) - - WidgetTemplates (Pre-built templates) - - LiveDataIntegration (Real-time data) - - WidgetAnalytics (Performance tracking) - -### Manual Installation - -1. Clone the repository: - ```bash - git clone https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git - ``` - -2. Add the package to your project: - ```swift - dependencies: [ - .package(path: "path/to/iOS-Widget-Development-Kit") - ] - ``` - -## Configuration - -### Basic Setup - -```swift -import WidgetKit -import WidgetTemplates -import LiveDataIntegration -import WidgetAnalytics - -// Initialize the widget engine -let widgetEngine = WidgetEngine.shared - -// Register default templates -let weatherTemplate = WeatherWidgetTemplate() -widgetEngine.registerTemplate(weatherTemplate) -``` - -### Advanced Configuration - -```swift -// Configure performance settings -let performanceSettings = WidgetPerformanceSettings( - maxMemoryUsage: 100 * 1024 * 1024, // 100MB - refreshInterval: 30.0, // 30 seconds - enableBatteryOptimization: true -) - -widgetEngine.configurePerformance(performanceSettings) - -// Configure analytics -let analyticsConfig = AnalyticsConfiguration( - enableExternalAnalytics: false, - maxEventLogSize: 1000, - enablePerformanceTracking: true, - enableErrorTracking: true -) - -WidgetAnalyticsService.shared.configure(analyticsConfig) -``` - -## Dependencies - -The framework includes the following dependencies: - -- **Alamofire**: Network requests and API integration -- **SwiftLint**: Code quality enforcement -- **Quick/Nimble**: Testing framework - -These are automatically resolved when using Swift Package Manager. - -## Troubleshooting - -### Common Issues - -1. **Build Errors**: Ensure you're using iOS 16.0+ and Swift 5.9+ -2. **Import Errors**: Check that all targets are properly linked -3. **Runtime Errors**: Verify that templates are registered before use - -### Support - -For additional help, see: -- [Documentation](Documentation/) -- [GitHub Issues](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/issues) -- [Examples](Examples/) \ No newline at end of file diff --git a/Documentation/LiveActivityAPI.md b/Documentation/LiveActivityAPI.md deleted file mode 100644 index c217a86..0000000 --- a/Documentation/LiveActivityAPI.md +++ /dev/null @@ -1,446 +0,0 @@ -# Live Activity API - - -## Table of Contents -- [Live Activity API](#live-activity-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [LiveActivityManager](#liveactivitymanager) - - [LiveActivity](#liveactivity) - - [LiveActivityConfiguration](#liveactivityconfiguration) -- [Activity Types](#activity-types) - - [Supported Activity Types](#supported-activity-types) -- [Content Types](#content-types) - - [Basic Activity Content](#basic-activity-content) - - [Interactive Activity Content](#interactive-activity-content) -- [Usage Examples](#usage-examples) - - [Creating a Simple Live Activity](#creating-a-simple-live-activity) - - [Creating an Interactive Live Activity](#creating-an-interactive-live-activity) -- [Dynamic Island Integration](#dynamic-island-integration) - - [Dynamic Island Views](#dynamic-island-views) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Error Handling](#error-handling) - - [ActivityError Types](#activityerror-types) - - [Error Handling Example](#error-handling-example) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [Design Guidelines](#design-guidelines) - - [Security Considerations](#security-considerations) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 15 to iOS 16+](#from-ios-15-to-ios-16) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tips](#debug-tips) - - - -## Overview - -The Live Activity API provides comprehensive functionality for creating and managing Live Activities in iOS applications. This API enables developers to create engaging, real-time experiences that integrate with the Dynamic Island and Lock Screen. - -## Core Classes - -### LiveActivityManager - -The main manager class for handling Live Activity operations. - -```swift -class LiveActivityManager { - static let shared = LiveActivityManager() - - func start(_ activity: LiveActivity) -> Result - func stop(_ activity: LiveActivity) -> Result - func update(_ activity: LiveActivity, with data: ActivityData) -> Result - func refresh(_ activity: LiveActivity) -> Result -} -``` - -### LiveActivity - -The core activity class for Live Activity implementations. - -```swift -class LiveActivity { - let activityType: String - let configuration: LiveActivityConfiguration - var content: ActivityContent? - - init(activityType: String, configuration: LiveActivityConfiguration) - func setContent(_ content: @escaping (ActivityContext) -> ActivityContent) - func update(with data: ActivityData) -> Result -} -``` - -### LiveActivityConfiguration - -Configuration options for Live Activities. - -```swift -struct LiveActivityConfiguration { - var enableDynamicIsland: Bool - var enableLockScreen: Bool - var enableNotifications: Bool - var updateInterval: TimeInterval - var enableInteractions: Bool - var enableBackgroundUpdates: Bool - var maxDuration: TimeInterval -} -``` - -## Activity Types - -### Supported Activity Types - -```swift -enum ActivityType { - case delivery // Package delivery tracking - case ride // Ride sharing - case food // Food delivery - case workout // Fitness tracking - case music // Music playback - case timer // Timer/countdown - case game // Game status - case custom // Custom activity -} -``` - -## Content Types - -### Basic Activity Content - -```swift -struct BasicActivityContent: ActivityContent { - let title: String - let subtitle: String? - let icon: String? - let progress: Double? - let backgroundColor: Color - let textColor: Color -} -``` - -### Interactive Activity Content - -```swift -struct InteractiveActivityContent: ActivityContent { - let title: String - let subtitle: String? - let actions: [ActivityAction] - let progress: Double? - let backgroundColor: Color - let textColor: Color -} -``` - -## Usage Examples - -### Creating a Simple Live Activity - -```swift -// Create Live Activity manager -let activityManager = LiveActivityManager.shared - -// Configure activity -let config = LiveActivityConfiguration() -config.enableDynamicIsland = true -config.enableLockScreen = true -config.updateInterval = 30 // 30 seconds -config.maxDuration = 3600 // 1 hour - -// Create activity -let liveActivity = LiveActivity( - activityType: "com.company.app.delivery", - configuration: config -) - -// Set activity content -liveActivity.setContent { context in - BasicActivityContent( - title: "Package Delivery", - subtitle: "Out for delivery", - icon: "shippingbox.fill", - progress: 0.8, - backgroundColor: .systemBlue, - textColor: .white - ) -} - -// Start activity -let result = activityManager.start(liveActivity) -switch result { -case .success(let activity): - print("✅ Live Activity started successfully") - print("Activity ID: \(activity.id)") -case .failure(let error): - print("❌ Activity start failed: \(error)") -} -``` - -### Creating an Interactive Live Activity - -```swift -// Create interactive activity -let interactiveConfig = LiveActivityConfiguration() -interactiveConfig.enableDynamicIsland = true -interactiveConfig.enableInteractions = true -interactiveConfig.enableBackgroundUpdates = true - -let interactiveActivity = LiveActivity( - activityType: "com.company.app.music", - configuration: interactiveConfig -) - -// Set interactive content -interactiveActivity.setContent { context in - InteractiveActivityContent( - title: "Now Playing", - subtitle: "Artist Name", - actions: [ - ActivityAction(title: "Play", icon: "play.fill", action: .play), - ActivityAction(title: "Pause", icon: "pause.fill", action: .pause), - ActivityAction(title: "Next", icon: "forward.fill", action: .next) - ], - progress: 0.6, - backgroundColor: .systemBackground, - textColor: .primary - ) -} - -// Start interactive activity -activityManager.start(interactiveActivity) -``` - -## Dynamic Island Integration - -### Dynamic Island Views - -```swift -// Configure Dynamic Island -let islandConfig = DynamicIslandConfiguration() -islandConfig.enableCompactView = true -islandConfig.enableExpandedView = true -islandConfig.enableMinimalView = true - -// Create Dynamic Island activity -let islandActivity = DynamicIslandActivity( - activityType: "com.company.app.music", - configuration: islandConfig -) - -// Set Dynamic Island views -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - Text("Now Playing") - .font(.caption) - } -} - -islandActivity.setExpandedView { context in - VStack { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} - -// Start Dynamic Island activity -activityManager.start(islandActivity) -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = ActivityDataSource( - type: .api, - endpoint: "https://api.company.com/activity-data", - cachePolicy: .cacheFirst -) - -// Fetch and update data -dataSource.fetchData { result in - switch result { - case .success(let data): - // Update activity with new data - liveActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Live Activity updated") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 300 // 5 minutes - -// Setup background updates -activityManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Error Handling - -### ActivityError Types - -```swift -enum ActivityError: Error { - case startFailed(String) - case stopFailed(String) - case updateFailed(String) - case configurationError(String) - case dataError(String) - case networkError(String) - case permissionError(String) - case timeoutError(String) -} -``` - -### Error Handling Example - -```swift -func handleActivityError(_ error: ActivityError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Handle start error - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Handle update error - case .configurationError(let message): - print("Configuration error: \(message)") - // Handle configuration error - case .dataError(let message): - print("Data error: \(message)") - // Handle data error - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - case .permissionError(let message): - print("Permission error: \(message)") - // Handle permission error - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout error - } -} -``` - -## Best Practices - -### Performance Optimization - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Design Guidelines - -1. **Clear Information**: Display essential information clearly -2. **Consistent Styling**: Maintain consistent visual design -3. **Accessibility**: Ensure activities are accessible -4. **User Privacy**: Respect user privacy settings - -### Security Considerations - -1. **Data Protection**: Secure sensitive data -2. **Permission Handling**: Request only necessary permissions -3. **Network Security**: Use secure network connections -4. **Error Handling**: Handle errors gracefully - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `start` | Start a Live Activity | `LiveActivity` | `Result` | -| `stop` | Stop a Live Activity | `LiveActivity` | `Result` | -| `update` | Update activity with new data | `LiveActivity`, `ActivityData` | `Result` | -| `refresh` | Refresh activity content | `LiveActivity` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `activityType` | `String` | Unique activity type identifier | -| `configuration` | `LiveActivityConfiguration` | Activity configuration | -| `content` | `ActivityContent?` | Activity content | -| `isActive` | `Bool` | Activity status | - -## Migration Guide - -### From iOS 15 to iOS 16+ - -```swift -// iOS 15 approach (deprecated) -let oldActivity = Activity(kind: "com.app.activity") - -// iOS 16+ approach -let newActivity = LiveActivity( - activityType: "com.app.activity", - configuration: LiveActivityConfiguration() -) -``` - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting**: Check permissions and configuration -2. **Updates Not Working**: Verify data source and network connectivity -3. **Performance Issues**: Optimize data fetching and background processing -4. **Crash Issues**: Implement proper error handling - -### Debug Tips - -1. **Enable Logging**: Use debug logging for troubleshooting -2. **Test on Device**: Always test on physical devices -3. **Monitor Performance**: Use performance monitoring tools -4. **Check Permissions**: Verify required permissions are granted diff --git a/Documentation/LiveActivityGuide.md b/Documentation/LiveActivityGuide.md deleted file mode 100644 index 12bbee4..0000000 --- a/Documentation/LiveActivityGuide.md +++ /dev/null @@ -1,772 +0,0 @@ -# Live Activity Guide - - -## Table of Contents -- [Live Activity Guide](#live-activity-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Activity Manager](#3-initialize-activity-manager) -- [Creating Your First Live Activity](#creating-your-first-live-activity) - - [Step 1: Define Activity Configuration](#step-1-define-activity-configuration) - - [Step 2: Create Activity Instance](#step-2-create-activity-instance) - - [Step 3: Define Activity Content](#step-3-define-activity-content) - - [Step 4: Start Activity](#step-4-start-activity) -- [Activity Types and Use Cases](#activity-types-and-use-cases) - - [Delivery Tracking](#delivery-tracking) - - [Ride Sharing](#ride-sharing) - - [Music Playback](#music-playback) -- [Dynamic Island Integration](#dynamic-island-integration) - - [Compact View](#compact-view) - - [Expanded View](#expanded-view) - - [Minimal View](#minimal-view) -- [Data Integration](#data-integration) - - [Real-Time Updates](#real-time-updates) - - [Background Updates](#background-updates) -- [Interactive Elements](#interactive-elements) - - [Action Buttons](#action-buttons) - - [Gesture Recognition](#gesture-recognition) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing Live Activities in iOS applications using the iOS Widget Development Kit. Live Activities provide users with real-time, engaging experiences that integrate with the Dynamic Island and Lock Screen. - -## Prerequisites - -- iOS 16.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and ActivityKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for Live Activities: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -NSSupportsLiveActivitiesBackgroundProcessing - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Activity Manager - -```swift -let activityManager = LiveActivityManager.shared -activityManager.configure { config in - config.enableDynamicIsland = true - config.enableLockScreen = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Live Activity - -### Step 1: Define Activity Configuration - -```swift -let activityConfig = LiveActivityConfiguration() -activityConfig.enableDynamicIsland = true -activityConfig.enableLockScreen = true -activityConfig.enableNotifications = true -activityConfig.updateInterval = 30 // 30 seconds -activityConfig.maxDuration = 3600 // 1 hour -``` - -### Step 2: Create Activity Instance - -```swift -let liveActivity = LiveActivity( - activityType: "com.yourcompany.app.delivery", - configuration: activityConfig -) -``` - -### Step 3: Define Activity Content - -```swift -liveActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Order #12345") - .font(.headline) - - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("2:30 PM") - .font(.caption2) - .foregroundColor(.secondary) - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - .tint(.blue) - } - .padding() - .background(Color(.systemBackground)) -} -``` - -### Step 4: Start Activity - -```swift -activityManager.start(liveActivity) { result in - switch result { - case .success(let activity): - print("✅ Live Activity started successfully") - print("Activity ID: \(activity.id)") - case .failure(let error): - print("❌ Activity start failed: \(error)") - } -} -``` - -## Activity Types and Use Cases - -### Delivery Tracking - -```swift -// Delivery activity configuration -let deliveryConfig = LiveActivityConfiguration() -deliveryConfig.enableDynamicIsland = true -deliveryConfig.enableLockScreen = true -deliveryConfig.updateInterval = 60 // 1 minute - -let deliveryActivity = LiveActivity( - activityType: "com.yourcompany.app.delivery", - configuration: deliveryConfig -) - -deliveryActivity.setContent { context in - VStack(spacing: 8) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Estimated arrival: 3:00 PM") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.7) - .progressViewStyle(.linear) - } - .padding() -} -``` - -### Ride Sharing - -```swift -// Ride sharing activity -let rideConfig = LiveActivityConfiguration() -rideConfig.enableDynamicIsland = true -rideConfig.enableInteractions = true - -let rideActivity = LiveActivity( - activityType: "com.yourcompany.app.ride", - configuration: rideConfig -) - -rideActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "car.fill") - .foregroundColor(.green) - - VStack(alignment: .leading) { - Text("Your ride is arriving") - .font(.headline) - Text("Driver: John Doe") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("2 min") - .font(.caption2) - .foregroundColor(.green) - } - - HStack(spacing: 16) { - Button("Call Driver") { - // Call driver action - } - .buttonStyle(.bordered) - - Button("Cancel Ride") { - // Cancel ride action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Music Playback - -```swift -// Music activity -let musicConfig = LiveActivityConfiguration() -musicConfig.enableDynamicIsland = true -musicConfig.enableInteractions = true - -let musicActivity = LiveActivity( - activityType: "com.yourcompany.app.music", - configuration: musicConfig -) - -musicActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Now Playing") - .font(.headline) - Text("Song Title - Artist") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} -``` - -## Dynamic Island Integration - -### Compact View - -```swift -// Configure Dynamic Island -let islandConfig = DynamicIslandConfiguration() -islandConfig.enableCompactView = true -islandConfig.enableExpandedView = true -islandConfig.enableMinimalView = true - -let islandActivity = DynamicIslandActivity( - activityType: "com.yourcompany.app.music", - configuration: islandConfig -) - -// Set compact view -islandActivity.setCompactView { context in - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - Text("Now Playing") - .font(.caption) - } -} -``` - -### Expanded View - -```swift -// Set expanded view -islandActivity.setExpandedView { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.purple) - - VStack(alignment: .leading) { - Text("Song Title") - .font(.headline) - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Play") { - // Play action - } - .buttonStyle(.bordered) - } - - ProgressView(value: 0.6) - .progressViewStyle(.linear) - } - .padding() -} -``` - -### Minimal View - -```swift -// Set minimal view -islandActivity.setMinimalView { context in - Image(systemName: "music.note") - .foregroundColor(.purple) -} -``` - -## Data Integration - -### Real-Time Updates - -```swift -// Create data source -let dataSource = ActivityDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/activity-data", - cachePolicy: .cacheFirst -) - -// Configure real-time updates -let realTimeManager = RealTimeUpdateManager() -realTimeManager.configure { config in - config.enableWebSocket = true - config.enablePushNotifications = true - config.enableBackgroundUpdates = true -} - -// Setup real-time connection -realTimeManager.connect { result in - switch result { - case .success: - print("✅ Real-time connection established") - - // Listen for updates - realTimeManager.onDataUpdate { data in - print("📱 Real-time data received: \(data)") - - // Update activity with real-time data - liveActivity.update(with: data) { result in - switch result { - case .success: - print("✅ Activity updated with real-time data") - case .failure(let error): - print("❌ Real-time update failed: \(error)") - } - } - } - case .failure(let error): - print("❌ Real-time connection failed: \(error)") - } -} -``` - -### Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 60 // 1 minute -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -activityManager.configureBackgroundUpdates(backgroundConfig) { result in - switch result { - case .success: - print("✅ Background updates configured") - case .failure(let error): - print("❌ Background configuration failed: \(error)") - } -} -``` - -## Interactive Elements - -### Action Buttons - -```swift -// Add action buttons to activity -liveActivity.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Package Delivery") - .font(.headline) - Text("Out for delivery") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - ProgressView(value: 0.8) - .progressViewStyle(.linear) - - HStack(spacing: 16) { - Button("Track Package") { - // Track package action - } - .buttonStyle(.bordered) - - Button("Contact Driver") { - // Contact driver action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -### Gesture Recognition - -```swift -// Add gesture recognition -liveActivity.onTap { context in - print("Activity tapped!") - // Handle tap gesture -} - -liveActivity.onLongPress { context in - print("Activity long pressed!") - // Handle long press gesture -} - -liveActivity.onSwipe { direction in - print("Activity swiped: \(direction)") - // Handle swipe gesture -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom activity style -let customStyle = ActivityStyle( - backgroundColor: .systemBlue, - cornerRadius: 12, - shadow: ActivityShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = ActivityStylingManager() -stylingManager.applyStyle(customStyle, to: liveActivity) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicActivityTheme( - lightMode: ActivityTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .blue - ), - darkMode: ActivityTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicActivityThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Performance Optimization - -### Best Practices - -1. **Efficient Updates**: Only update when necessary -2. **Background Processing**: Use background processing wisely -3. **Memory Management**: Properly manage activity resources -4. **Battery Optimization**: Minimize battery impact - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = ActivityPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor activity performance -performanceMonitor.startMonitoring(liveActivity) { metrics in - print("Activity performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Activity Start Failed** - - Check activity configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleActivityError(_ error: ActivityError) { - switch error { - case .startFailed(let message): - print("Activity start failed: \(message)") - // Retry start or show user message - - case .stopFailed(let message): - print("Activity stop failed: \(message)") - // Handle stop error - - case .updateFailed(let message): - print("Activity update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - - case .timeoutError(let message): - print("Timeout error: \(message)") - // Handle timeout issues - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class LiveActivityTests: XCTestCase { - var liveActivity: LiveActivity! - - override func setUp() { - super.setUp() - let config = LiveActivityConfiguration() - liveActivity = LiveActivity(activityType: "test.activity", configuration: config) - } - - func testActivityStart() { - let expectation = XCTestExpectation(description: "Activity start") - - let manager = LiveActivityManager.shared - manager.start(liveActivity) { result in - switch result { - case .success(let activity): - expectation.fulfill() - case .failure(let error): - XCTFail("Activity start failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testActivityUpdate() { - let expectation = XCTestExpectation(description: "Activity update") - - let testData = ActivityData(content: "Test content") - liveActivity.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class LiveActivityUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testActivityAppearance() { - // Test activity appears on lock screen - let activity = app.otherElements["liveActivity"] - XCTAssertTrue(activity.exists) - } - - func testActivityInteraction() { - // Test activity interactions - let activity = app.otherElements["liveActivity"] - activity.tap() - - // Verify interaction handled - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Activity tested on multiple devices -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Activity Not Starting** - - Check activity configuration - - Verify permissions - - Test on physical device - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = ActivityDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor activity lifecycle -debugLogger.onActivityStarted = { activity in - print("Activity started: \(activity.activityType)") -} - -debugLogger.onActivityUpdated = { activity in - print("Activity updated: \(activity.activityType)") -} - -debugLogger.onActivityStopped = { activity in - print("Activity stopped: \(activity.activityType)") -} - -debugLogger.onActivityError = { error in - print("Activity error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating Live Activities using the iOS Widget Development Kit. By following these guidelines, you can create engaging, real-time experiences that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Live Activity API](LiveActivityAPI.md) documentation. diff --git a/Documentation/LockScreenWidgetAPI.md b/Documentation/LockScreenWidgetAPI.md deleted file mode 100644 index 649fe61..0000000 --- a/Documentation/LockScreenWidgetAPI.md +++ /dev/null @@ -1,349 +0,0 @@ -# Lock Screen Widget API - - -## Table of Contents -- [Lock Screen Widget API](#lock-screen-widget-api) -- [Overview](#overview) -- [Core Classes](#core-classes) - - [LockScreenWidgetManager](#lockscreenwidgetmanager) - - [LockScreenWidget](#lockscreenwidget) - - [LockWidgetConfiguration](#lockwidgetconfiguration) -- [Widget Sizes](#widget-sizes) - - [Supported Sizes](#supported-sizes) -- [Content Types](#content-types) - - [Basic Content](#basic-content) - - [Interactive Content](#interactive-content) -- [Usage Examples](#usage-examples) - - [Creating a Simple Lock Screen Widget](#creating-a-simple-lock-screen-widget) - - [Creating an Interactive Lock Screen Widget](#creating-an-interactive-lock-screen-widget) -- [Data Integration](#data-integration) - - [Updating Widget with Data](#updating-widget-with-data) -- [Error Handling](#error-handling) - - [WidgetError Types](#widgeterror-types) - - [Error Handling Example](#error-handling-example) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [Design Guidelines](#design-guidelines) - - [Security Considerations](#security-considerations) -- [API Reference](#api-reference) - - [Methods](#methods) - - [Properties](#properties) -- [Migration Guide](#migration-guide) - - [From iOS 14 to iOS 15+](#from-ios-14-to-ios-15) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tips](#debug-tips) - - - -## Overview - -The Lock Screen Widget API provides comprehensive functionality for creating and managing lock screen widgets in iOS applications. This API enables developers to create engaging, informative, and interactive widgets that appear on the device's lock screen. - -## Core Classes - -### LockScreenWidgetManager - -The main manager class for handling lock screen widget operations. - -```swift -class LockScreenWidgetManager { - static let shared = LockScreenWidgetManager() - - func register(_ widget: LockScreenWidget) -> Result - func unregister(_ widget: LockScreenWidget) -> Result - func update(_ widget: LockScreenWidget, with data: WidgetData) -> Result - func refresh(_ widget: LockScreenWidget) -> Result -} -``` - -### LockScreenWidget - -The core widget class for lock screen implementations. - -```swift -class LockScreenWidget { - let kind: String - let configuration: LockWidgetConfiguration - var content: WidgetContent? - - init(kind: String, configuration: LockWidgetConfiguration) - func setContent(_ content: @escaping (WidgetContext) -> WidgetContent) - func update(with data: WidgetData) -> Result -} -``` - -### LockWidgetConfiguration - -Configuration options for lock screen widgets. - -```swift -struct LockWidgetConfiguration { - var widgetSize: WidgetSize - var enableGlance: Bool - var enableComplications: Bool - var refreshInterval: TimeInterval - var enableInteractions: Bool - var enableDeepLinking: Bool - var enableBackgroundUpdates: Bool -} -``` - -## Widget Sizes - -### Supported Sizes - -```swift -enum WidgetSize { - case small // 158x158 points - case medium // 338x158 points - case large // 338x354 points - case extraLarge // 338x548 points -} -``` - -## Content Types - -### Basic Content - -```swift -struct BasicWidgetContent: WidgetContent { - let title: String - let subtitle: String? - let icon: String? - let backgroundColor: Color - let textColor: Color -} -``` - -### Interactive Content - -```swift -struct InteractiveWidgetContent: WidgetContent { - let title: String - let subtitle: String? - let actions: [WidgetAction] - let backgroundColor: Color - let textColor: Color -} -``` - -## Usage Examples - -### Creating a Simple Lock Screen Widget - -```swift -// Create lock screen widget manager -let lockWidgetManager = LockScreenWidgetManager.shared - -// Configure widget -let config = LockWidgetConfiguration() -config.widgetSize = .small -config.enableGlance = true -config.refreshInterval = 300 // 5 minutes - -// Create widget -let lockWidget = LockScreenWidget( - kind: "com.company.app.lockwidget", - configuration: config -) - -// Set widget content -lockWidget.setContent { context in - BasicWidgetContent( - title: "Messages", - subtitle: "3 new", - icon: "message.fill", - backgroundColor: .systemBlue, - textColor: .white - ) -} - -// Register widget -let result = lockWidgetManager.register(lockWidget) -switch result { -case .success: - print("✅ Lock screen widget registered successfully") -case .failure(let error): - print("❌ Registration failed: \(error)") -} -``` - -### Creating an Interactive Lock Screen Widget - -```swift -// Create interactive widget -let interactiveConfig = LockWidgetConfiguration() -interactiveConfig.widgetSize = .medium -interactiveConfig.enableInteractions = true -interactiveConfig.enableDeepLinking = true - -let interactiveWidget = LockScreenWidget( - kind: "com.company.app.interactive.lock", - configuration: interactiveConfig -) - -// Set interactive content -interactiveWidget.setContent { context in - InteractiveWidgetContent( - title: "Quick Actions", - subtitle: "Tap to open", - actions: [ - WidgetAction(title: "Messages", icon: "message.fill", action: .openApp), - WidgetAction(title: "Camera", icon: "camera.fill", action: .openCamera) - ], - backgroundColor: .systemBackground, - textColor: .primary - ) -} - -// Register interactive widget -lockWidgetManager.register(interactiveWidget) -``` - -## Data Integration - -### Updating Widget with Data - -```swift -// Create data source -let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.company.com/lock-widget-data", - cachePolicy: .cacheFirst -) - -// Fetch and update data -dataSource.fetchData { result in - switch result { - case .success(let data): - // Update widget with new data - lockWidget.update(with: data) { result in - switch result { - case .success: - print("✅ Lock screen widget updated") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -## Error Handling - -### WidgetError Types - -```swift -enum WidgetError: Error { - case registrationFailed(String) - case updateFailed(String) - case configurationError(String) - case dataError(String) - case networkError(String) - case permissionError(String) -} -``` - -### Error Handling Example - -```swift -func handleWidgetError(_ error: WidgetError) { - switch error { - case .registrationFailed(let message): - print("Registration failed: \(message)") - // Handle registration error - case .updateFailed(let message): - print("Update failed: \(message)") - // Handle update error - case .configurationError(let message): - print("Configuration error: \(message)") - // Handle configuration error - case .dataError(let message): - print("Data error: \(message)") - // Handle data error - case .networkError(let message): - print("Network error: \(message)") - // Handle network error - case .permissionError(let message): - print("Permission error: \(message)") - // Handle permission error - } -} -``` - -## Best Practices - -### Performance Optimization - -1. **Minimize Updates**: Only update widgets when necessary -2. **Efficient Data Fetching**: Use caching and background updates -3. **Memory Management**: Properly manage widget resources -4. **Battery Optimization**: Minimize background processing - -### Design Guidelines - -1. **Clear Information**: Display essential information clearly -2. **Consistent Styling**: Maintain consistent visual design -3. **Accessibility**: Ensure widgets are accessible -4. **User Privacy**: Respect user privacy settings - -### Security Considerations - -1. **Data Protection**: Secure sensitive data -2. **Permission Handling**: Request only necessary permissions -3. **Network Security**: Use secure network connections -4. **Error Handling**: Handle errors gracefully - -## API Reference - -### Methods - -| Method | Description | Parameters | Returns | -|--------|-------------|------------|---------| -| `register` | Register a lock screen widget | `LockScreenWidget` | `Result` | -| `unregister` | Unregister a lock screen widget | `LockScreenWidget` | `Result` | -| `update` | Update widget with new data | `LockScreenWidget`, `WidgetData` | `Result` | -| `refresh` | Refresh widget content | `LockScreenWidget` | `Result` | - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `kind` | `String` | Unique widget identifier | -| `configuration` | `LockWidgetConfiguration` | Widget configuration | -| `content` | `WidgetContent?` | Widget content | -| `isRegistered` | `Bool` | Registration status | - -## Migration Guide - -### From iOS 14 to iOS 15+ - -```swift -// iOS 14 approach (deprecated) -let oldWidget = Widget(kind: "com.app.widget") - -// iOS 15+ approach -let newWidget = LockScreenWidget( - kind: "com.app.widget", - configuration: LockWidgetConfiguration() -) -``` - -## Troubleshooting - -### Common Issues - -1. **Widget Not Appearing**: Check registration and permissions -2. **Updates Not Working**: Verify data source and network connectivity -3. **Performance Issues**: Optimize data fetching and caching -4. **Crash Issues**: Implement proper error handling - -### Debug Tips - -1. **Enable Logging**: Use debug logging for troubleshooting -2. **Test on Device**: Always test on physical devices -3. **Monitor Performance**: Use performance monitoring tools -4. **Check Permissions**: Verify required permissions are granted diff --git a/Documentation/LockScreenWidgetGuide.md b/Documentation/LockScreenWidgetGuide.md deleted file mode 100644 index d105200..0000000 --- a/Documentation/LockScreenWidgetGuide.md +++ /dev/null @@ -1,589 +0,0 @@ -# Lock Screen Widget Guide - - -## Table of Contents -- [Lock Screen Widget Guide](#lock-screen-widget-guide) -- [Introduction](#introduction) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Project Setup](#1-project-setup) - - [2. Import the Framework](#2-import-the-framework) - - [3. Initialize Widget Manager](#3-initialize-widget-manager) -- [Creating Your First Lock Screen Widget](#creating-your-first-lock-screen-widget) - - [Step 1: Define Widget Configuration](#step-1-define-widget-configuration) - - [Step 2: Create Widget Instance](#step-2-create-widget-instance) - - [Step 3: Define Widget Content](#step-3-define-widget-content) - - [Step 4: Register Widget](#step-4-register-widget) -- [Widget Sizes and Layouts](#widget-sizes-and-layouts) - - [Supported Sizes](#supported-sizes) - - [Layout Guidelines](#layout-guidelines) -- [Data Integration](#data-integration) - - [Setting Up Data Sources](#setting-up-data-sources) - - [Updating Widget with Data](#updating-widget-with-data) -- [Interactive Widgets](#interactive-widgets) - - [Adding Interactions](#adding-interactions) - - [Action Buttons](#action-buttons) -- [Styling and Theming](#styling-and-theming) - - [Custom Styling](#custom-styling) - - [Dynamic Theming](#dynamic-theming) -- [Background Updates](#background-updates) - - [Configuring Background Updates](#configuring-background-updates) - - [Background Data Fetching](#background-data-fetching) -- [Performance Optimization](#performance-optimization) - - [Best Practices](#best-practices) - - [Performance Monitoring](#performance-monitoring) -- [Error Handling](#error-handling) - - [Common Errors and Solutions](#common-errors-and-solutions) - - [Error Handling Implementation](#error-handling-implementation) -- [Testing](#testing) - - [Unit Testing](#unit-testing) - - [UI Testing](#ui-testing) -- [Deployment](#deployment) - - [App Store Guidelines](#app-store-guidelines) - - [Production Checklist](#production-checklist) -- [Troubleshooting](#troubleshooting) - - [Common Issues](#common-issues) - - [Debug Tools](#debug-tools) -- [Conclusion](#conclusion) - - - -## Introduction - -This guide provides comprehensive instructions for creating and implementing lock screen widgets in iOS applications using the iOS Widget Development Kit. Lock screen widgets provide users with quick access to important information without unlocking their device. - -## Prerequisites - -- iOS 15.0+ SDK -- Xcode 15.0+ -- Swift 5.9+ -- Basic knowledge of SwiftUI and WidgetKit - -## Getting Started - -### 1. Project Setup - -First, ensure your project is properly configured for lock screen widgets: - -```swift -// Add to your app's Info.plist -NSSupportsLiveActivities - -NSSupportsLiveActivitiesFrequentUpdates - -``` - -### 2. Import the Framework - -```swift -import WidgetDevelopmentKit -``` - -### 3. Initialize Widget Manager - -```swift -let widgetManager = WidgetDevelopmentManager.shared -widgetManager.configure { config in - config.enableLockScreenWidgets = true - config.enableBackgroundUpdates = true -} -``` - -## Creating Your First Lock Screen Widget - -### Step 1: Define Widget Configuration - -```swift -let lockWidgetConfig = LockWidgetConfiguration() -lockWidgetConfig.widgetSize = .small -lockWidgetConfig.enableGlance = true -lockWidgetConfig.enableComplications = true -lockWidgetConfig.refreshInterval = 300 // 5 minutes -``` - -### Step 2: Create Widget Instance - -```swift -let lockWidget = LockScreenWidget( - kind: "com.yourcompany.app.lockscreen", - configuration: lockWidgetConfig -) -``` - -### Step 3: Define Widget Content - -```swift -lockWidget.setContent { context in - VStack(spacing: 8) { - HStack { - Image(systemName: "message.fill") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text("Messages") - .font(.caption) - .foregroundColor(.secondary) - - Text("3 new") - .font(.caption2) - .foregroundColor(.primary) - } - - Spacer() - } - .padding(.horizontal, 8) - } - .background(Color(.systemBackground)) -} -``` - -### Step 4: Register Widget - -```swift -let lockWidgetManager = LockScreenWidgetManager.shared -lockWidgetManager.register(lockWidget) { result in - switch result { - case .success: - print("✅ Lock screen widget registered successfully") - case .failure(let error): - print("❌ Registration failed: \(error)") - } -} -``` - -## Widget Sizes and Layouts - -### Supported Sizes - -| Size | Dimensions | Use Case | -|------|------------|----------| -| Small | 158x158 | Quick info, status | -| Medium | 338x158 | Detailed info, actions | -| Large | 338x354 | Rich content, lists | -| Extra Large | 338x548 | Complex layouts | - -### Layout Guidelines - -1. **Small Widgets**: Focus on essential information -2. **Medium Widgets**: Balance information and actions -3. **Large Widgets**: Provide detailed content -4. **Extra Large Widgets**: Complex interactions - -## Data Integration - -### Setting Up Data Sources - -```swift -// Create data source -let dataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/widget-data", - cachePolicy: .cacheFirst -) - -// Configure data manager -let dataManager = WidgetDataManager() -dataManager.configure { config in - config.enableRealTimeUpdates = true - config.enableBackgroundSync = true - config.refreshInterval = 300 -} -``` - -### Updating Widget with Data - -```swift -// Fetch data -dataManager.fetchData(from: dataSource) { result in - switch result { - case .success(let data): - // Update widget - lockWidget.update(with: data) { result in - switch result { - case .success: - print("✅ Widget updated with new data") - case .failure(let error): - print("❌ Update failed: \(error)") - } - } - case .failure(let error): - print("❌ Data fetch failed: \(error)") - } -} -``` - -## Interactive Widgets - -### Adding Interactions - -```swift -// Create interactive widget -let interactiveConfig = LockWidgetConfiguration() -interactiveConfig.enableInteractions = true -interactiveConfig.enableDeepLinking = true - -let interactiveWidget = LockScreenWidget( - kind: "com.yourcompany.app.interactive", - configuration: interactiveConfig -) - -// Add interaction handlers -interactiveWidget.onTap { context in - // Handle tap interaction - print("Widget tapped!") - - // Deep link to app - if let url = URL(string: "yourapp://open") { - context.openURL(url) - } -} - -interactiveWidget.onLongPress { context in - // Handle long press - print("Widget long pressed!") -} -``` - -### Action Buttons - -```swift -interactiveWidget.setContent { context in - VStack(spacing: 12) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - - VStack(alignment: .leading) { - Text("Now Playing") - .font(.headline) - Text("Song Title") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - HStack(spacing: 16) { - Button("Previous") { - // Previous track action - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/pause action - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track action - } - .buttonStyle(.bordered) - } - } - .padding() -} -``` - -## Styling and Theming - -### Custom Styling - -```swift -// Create custom style -let customStyle = WidgetStyle( - backgroundColor: .systemBlue, - cornerRadius: 12, - shadow: WidgetShadow( - color: .black.opacity(0.1), - radius: 8, - offset: CGSize(width: 0, height: 2) - ) -) - -// Apply custom styling -let stylingManager = WidgetStylingManager() -stylingManager.applyStyle(customStyle, to: lockWidget) -``` - -### Dynamic Theming - -```swift -// Create dynamic theme -let dynamicTheme = DynamicTheme( - lightMode: WidgetTheme( - backgroundColor: .white, - textColor: .black, - accentColor: .blue - ), - darkMode: WidgetTheme( - backgroundColor: .black, - textColor: .white, - accentColor: .cyan - ) -) - -// Apply dynamic theme -let themeManager = DynamicThemeManager() -themeManager.applyTheme(dynamicTheme) -``` - -## Background Updates - -### Configuring Background Updates - -```swift -// Configure background updates -let backgroundConfig = BackgroundUpdateConfiguration() -backgroundConfig.enableBackgroundProcessing = true -backgroundConfig.updateInterval = 300 // 5 minutes -backgroundConfig.maxBackgroundTime = 1800 // 30 minutes - -// Setup background updates -widgetManager.configureBackgroundUpdates(backgroundConfig) -``` - -### Background Data Fetching - -```swift -// Background data source -let backgroundDataSource = WidgetDataSource( - type: .api, - endpoint: "https://api.yourcompany.com/background-data", - cachePolicy: .backgroundFirst -) - -// Configure background fetching -dataManager.configureBackgroundFetching(backgroundDataSource) { result in - switch result { - case .success(let data): - // Update widget in background - lockWidget.update(with: data) - case .failure(let error): - print("❌ Background fetch failed: \(error)") - } -} -``` - -## Performance Optimization - -### Best Practices - -1. **Minimize Updates**: Only update when necessary -2. **Efficient Data Fetching**: Use caching and background updates -3. **Memory Management**: Properly manage widget resources -4. **Battery Optimization**: Minimize background processing - -### Performance Monitoring - -```swift -// Configure performance monitoring -let performanceMonitor = WidgetPerformanceMonitor() -performanceMonitor.enableMonitoring = true -performanceMonitor.monitorUpdates = true -performanceMonitor.monitorMemory = true - -// Monitor widget performance -performanceMonitor.startMonitoring(lockWidget) { metrics in - print("Widget performance metrics: \(metrics)") -} -``` - -## Error Handling - -### Common Errors and Solutions - -1. **Registration Failed** - - Check widget configuration - - Verify permissions - - Ensure proper setup - -2. **Update Failed** - - Check data source connectivity - - Verify data format - - Handle network errors - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Error Handling Implementation - -```swift -// Comprehensive error handling -func handleWidgetError(_ error: WidgetError) { - switch error { - case .registrationFailed(let message): - print("Registration failed: \(message)") - // Retry registration or show user message - - case .updateFailed(let message): - print("Update failed: \(message)") - // Retry update or use cached data - - case .configurationError(let message): - print("Configuration error: \(message)") - // Fix configuration and retry - - case .dataError(let message): - print("Data error: \(message)") - // Handle data issues - - case .networkError(let message): - print("Network error: \(message)") - // Handle network issues - - case .permissionError(let message): - print("Permission error: \(message)") - // Request permissions or show guidance - } -} -``` - -## Testing - -### Unit Testing - -```swift -import XCTest - -class LockScreenWidgetTests: XCTestCase { - var lockWidget: LockScreenWidget! - - override func setUp() { - super.setUp() - let config = LockWidgetConfiguration() - lockWidget = LockScreenWidget(kind: "test.widget", configuration: config) - } - - func testWidgetRegistration() { - let expectation = XCTestExpectation(description: "Widget registration") - - let manager = LockScreenWidgetManager.shared - manager.register(lockWidget) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Registration failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } - - func testWidgetUpdate() { - let expectation = XCTestExpectation(description: "Widget update") - - let testData = WidgetData(content: "Test content") - lockWidget.update(with: testData) { result in - switch result { - case .success: - expectation.fulfill() - case .failure(let error): - XCTFail("Update failed: \(error)") - } - } - - wait(for: [expectation], timeout: 5.0) - } -} -``` - -### UI Testing - -```swift -import XCTest - -class LockScreenWidgetUITests: XCTestCase { - var app: XCUIApplication! - - override func setUp() { - super.setUp() - app = XCUIApplication() - app.launch() - } - - func testWidgetAppearance() { - // Test widget appears on lock screen - let widget = app.otherElements["lockScreenWidget"] - XCTAssertTrue(widget.exists) - } - - func testWidgetInteraction() { - // Test widget interactions - let widget = app.otherElements["lockScreenWidget"] - widget.tap() - - // Verify app opens - XCTAssertTrue(app.isActive) - } -} -``` - -## Deployment - -### App Store Guidelines - -1. **Privacy**: Respect user privacy -2. **Performance**: Ensure good performance -3. **Battery**: Minimize battery impact -4. **Accessibility**: Ensure accessibility compliance - -### Production Checklist - -- [ ] Widget tested on multiple devices -- [ ] Background updates working correctly -- [ ] Error handling implemented -- [ ] Performance optimized -- [ ] Privacy compliance verified -- [ ] Accessibility requirements met - -## Troubleshooting - -### Common Issues - -1. **Widget Not Appearing** - - Check registration status - - Verify permissions - - Test on physical device - -2. **Updates Not Working** - - Check data source - - Verify network connectivity - - Monitor background processing - -3. **Performance Issues** - - Optimize data fetching - - Reduce update frequency - - Monitor memory usage - -### Debug Tools - -```swift -// Enable debug logging -let debugLogger = WidgetDebugLogger() -debugLogger.enableLogging = true -debugLogger.logLevel = .verbose - -// Monitor widget lifecycle -debugLogger.onWidgetRegistered = { widget in - print("Widget registered: \(widget.kind)") -} - -debugLogger.onWidgetUpdated = { widget in - print("Widget updated: \(widget.kind)") -} - -debugLogger.onWidgetError = { error in - print("Widget error: \(error)") -} -``` - -## Conclusion - -This guide provides a comprehensive overview of creating lock screen widgets using the iOS Widget Development Kit. By following these guidelines, you can create engaging, performant, and user-friendly lock screen widgets that enhance the user experience of your iOS applications. - -For more advanced features and detailed API documentation, refer to the [Lock Screen Widget API](LockScreenWidgetAPI.md) documentation. diff --git a/Documentation/Performance.md b/Documentation/Performance.md deleted file mode 100644 index 9cc23c9..0000000 --- a/Documentation/Performance.md +++ /dev/null @@ -1,536 +0,0 @@ -# ⚡ Performance Guide - iOS Widget Development Kit - -## 📋 Table of Contents - -- [Performance Overview](#performance-overview) -- [Performance Targets](#performance-targets) -- [Optimization Strategies](#optimization-strategies) -- [Performance Monitoring](#performance-monitoring) -- [Benchmarking](#benchmarking) -- [Memory Management](#memory-management) -- [Battery Optimization](#battery-optimization) -- [Network Performance](#network-performance) -- [Rendering Optimization](#rendering-optimization) -- [Troubleshooting](#troubleshooting) - ---- - -## 🎯 Performance Overview - -The iOS Widget Development Kit is engineered for **exceptional performance** with enterprise-grade optimization strategies. Our framework delivers **40% faster widget rendering** compared to standard WidgetKit implementations while maintaining **60% less memory consumption** and **50% reduced battery drain**. - -### Core Performance Philosophy -- **Performance is a Feature**: Treat performance as a user-facing feature, not an afterthought -- **Measure First**: Base optimization decisions on actual measurements, not assumptions -- **Continuous Monitoring**: Real-time performance tracking and automated optimization -- **Resource Awareness**: Intelligent resource management based on device capabilities - ---- - -## 🎯 Performance Targets - -### 📱 Application Performance -| Metric | Target | Excellent | Good | Needs Improvement | -|--------|--------|-----------|------|-------------------| -| **Cold Launch** | < 0.8s | < 0.5s | 0.5s - 0.8s | > 0.8s | -| **Warm Launch** | < 0.3s | < 0.2s | 0.2s - 0.3s | > 0.3s | -| **Widget Rendering** | < 90ms | < 50ms | 50ms - 90ms | > 90ms | -| **Frame Rate** | 60fps | 60fps | 55-60fps | < 55fps | -| **Memory Usage** | < 100MB | < 50MB | 50MB - 100MB | > 100MB | -| **Battery Impact** | < 2.5%/hour | < 1%/hour | 1% - 2.5%/hour | > 2.5%/hour | - -### 🔄 Widget-Specific Performance -| Widget Operation | Target | Notes | -|------------------|--------|-------| -| **Widget Load Time** | < 100ms | From tap to display | -| **Data Refresh** | < 200ms | Background data updates | -| **Animation Smoothness** | 60fps | All widget animations | -| **Memory per Widget** | < 5MB | Per active widget instance | -| **CPU Usage** | < 10% | During active operations | - -### 🌐 Network Performance -| Operation | Target | Timeout | Retry Strategy | -|-----------|--------|---------|----------------| -| **API Calls** | < 500ms | 10s | Exponential backoff | -| **Image Loading** | < 1s | 15s | Progressive enhancement | -| **Background Sync** | < 2s | 30s | Smart scheduling | -| **Real-time Updates** | < 100ms | 5s | WebSocket fallback | - ---- - -## 🚀 Optimization Strategies - -### 1. **Widget Rendering Optimization** - -#### Lazy Loading -```swift -struct OptimizedWidgetView: View { - @State private var isLoaded = false - - var body: some View { - Group { - if isLoaded { - ComplexWidgetContent() - } else { - WidgetPlaceholder() - .onAppear { - Task { - await loadContentAsync() - isLoaded = true - } - } - } - } - } -} -``` - -#### View Optimization -```swift -// ✅ Optimized approach -struct PerformantWidgetView: View { - let viewModel: WidgetViewModel - - var body: some View { - LazyVStack(spacing: 8) { - ForEach(viewModel.items.indices, id: \.self) { index in - WidgetItemView(item: viewModel.items[index]) - .equatable() // Prevent unnecessary redraws - } - } - .drawingGroup() // Composite into single layer - } -} -``` - -### 2. **Memory Management** - -#### Smart Caching -```swift -class PerformantCacheManager { - private let memoryCache = NSCache() - private let diskCache: DiskCache - - init() { - // Configure memory limits based on device capabilities - memoryCache.countLimit = 100 - memoryCache.totalCostLimit = 50 * 1024 * 1024 // 50MB - - // Memory pressure handling - NotificationCenter.default.addObserver( - self, - selector: #selector(didReceiveMemoryWarning), - name: UIApplication.didReceiveMemoryWarningNotification, - object: nil - ) - } - - @objc private func didReceiveMemoryWarning() { - memoryCache.removeAllObjects() - performGarbageCollection() - } -} -``` - -#### Memory Pool Pattern -```swift -class WidgetMemoryPool { - private var availableWidgets: [ReusableWidget] = [] - private let maxPoolSize = 20 - - func dequeueWidget() -> ReusableWidget { - if let widget = availableWidgets.popLast() { - return widget - } - return ReusableWidget() - } - - func enqueueWidget(_ widget: ReusableWidget) { - guard availableWidgets.count < maxPoolSize else { return } - widget.reset() - availableWidgets.append(widget) - } -} -``` - -### 3. **Background Processing** - -#### Intelligent Task Scheduling -```swift -class BackgroundTaskScheduler { - private let queue = DispatchQueue(label: "widget.background", qos: .utility) - private var pendingTasks: [BackgroundTask] = [] - - func scheduleTask(_ task: BackgroundTask) { - queue.async { [weak self] in - self?.processTasks() - } - } - - private func processTasks() { - // Battery level consideration - guard UIDevice.current.batteryLevel > 0.2 else { return } - - // Network availability check - guard NetworkMonitor.shared.isConnected else { return } - - // Execute tasks with priority - pendingTasks.sort { $0.priority > $1.priority } - processPendingTasks() - } -} -``` - ---- - -## 📊 Performance Monitoring - -### Real-time Metrics Collection -```swift -class PerformanceMonitor { - static let shared = PerformanceMonitor() - - private var metrics: [PerformanceMetric] = [] - private let metricsQueue = DispatchQueue(label: "metrics") - - func recordMetric(_ metric: PerformanceMetric) { - metricsQueue.async { - self.metrics.append(metric) - self.analyzePerformance() - } - } - - private func analyzePerformance() { - let recentMetrics = metrics.suffix(100) - - // Detect performance regressions - if let regression = detectRegression(in: recentMetrics) { - notifyPerformanceIssue(regression) - } - - // Auto-optimization triggers - if shouldOptimize(based: recentMetrics) { - triggerOptimization() - } - } -} -``` - -### Performance Dashboards -```swift -struct PerformanceDashboard: View { - @StateObject private var monitor = PerformanceMonitor.shared - - var body: some View { - VStack(spacing: 16) { - PerformanceChart( - title: "Widget Render Time", - data: monitor.renderTimeMetrics, - target: 90, - unit: "ms" - ) - - PerformanceChart( - title: "Memory Usage", - data: monitor.memoryMetrics, - target: 100, - unit: "MB" - ) - - PerformanceChart( - title: "Battery Impact", - data: monitor.batteryMetrics, - target: 2.5, - unit: "%/hour" - ) - } - } -} -``` - ---- - -## 🧪 Benchmarking - -### Automated Performance Testing -```swift -class PerformanceBenchmark { - func benchmarkWidgetRendering() async { - let iterations = 1000 - var renderTimes: [TimeInterval] = [] - - for _ in 0..() - - init() { - cache.totalCostLimit = 100 * 1024 * 1024 // 100MB - cache.delegate = self - } -} - -extension ImageCache: NSCacheDelegate { - func cache(_ cache: NSCache, willEvictObject obj: Any) { - // Log eviction for monitoring - PerformanceMonitor.shared.recordCacheEviction() - } -} -``` - -#### 3. Automatic Memory Management -```swift -class AutoMemoryManager { - private var memoryPressureSource: DispatchSourceMemoryPressure? - - func startMonitoring() { - memoryPressureSource = DispatchSource.makeMemoryPressureSource( - eventMask: [.warning, .critical], - queue: .main - ) - - memoryPressureSource?.setEventHandler { [weak self] in - self?.handleMemoryPressure() - } - - memoryPressureSource?.resume() - } - - private func handleMemoryPressure() { - // Clear caches - ImageCache.shared.clear() - WidgetCache.shared.clear() - - // Reduce background operations - BackgroundTaskManager.shared.pauseNonCriticalTasks() - } -} -``` - ---- - -## 🔋 Battery Optimization - -### Power-Efficient Strategies - -#### 1. Adaptive Refresh Rates -```swift -class BatteryOptimizedRefreshManager { - private var refreshInterval: TimeInterval = 30.0 - - func updateRefreshInterval() { - let batteryLevel = UIDevice.current.batteryLevel - let batteryState = UIDevice.current.batteryState - - switch (batteryLevel, batteryState) { - case (0.0...0.2, .unplugged): - refreshInterval = 300.0 // 5 minutes - case (0.2...0.5, .unplugged): - refreshInterval = 120.0 // 2 minutes - case (0.5...1.0, .unplugged): - refreshInterval = 60.0 // 1 minute - default: - refreshInterval = 30.0 // 30 seconds - } - } -} -``` - -#### 2. Smart Background Processing -```swift -class PowerEfficientProcessor { - func processInBackground(_ operation: @escaping () async -> T) async -> T? { - // Check thermal state - guard ProcessInfo.processInfo.thermalState != .critical else { - return nil - } - - // Check battery level - guard UIDevice.current.batteryLevel > 0.15 else { - return nil - } - - return await operation() - } -} -``` - ---- - -## 🌐 Network Performance - -### Intelligent Network Management -```swift -class NetworkOptimizer { - private let reachability = NetworkReachability() - - func optimizeForNetwork() { - switch reachability.connectionType { - case .cellular: - enableDataCompression() - reduceBatchSize() - increaseTimeouts() - case .wifi: - disableDataCompression() - increaseBatchSize() - decreaseTimeouts() - case .none: - enableOfflineMode() - } - } -} -``` - ---- - -## 🎨 Rendering Optimization - -### View Hierarchy Optimization -```swift -struct OptimizedWidgetView: View { - var body: some View { - // Use GeometryReader sparingly - content - .drawingGroup() // Composite complex views - .clipped() // Optimize drawing bounds - } - - @ViewBuilder - private var content: some View { - // Minimize view nesting - LazyVStack { - header - body - footer - } - } -} -``` - ---- - -## 🔧 Troubleshooting - -### Common Performance Issues - -#### 1. Memory Leaks -```bash -# Use Instruments to detect memory leaks -instruments -t "Leaks" -D /tmp/MyApp.trace MyApp.app -``` - -#### 2. Retain Cycles -```swift -// ❌ Problematic -class WidgetManager { - var delegate: WidgetDelegate? - - func setup() { - delegate?.manager = self // Creates retain cycle - } -} - -// ✅ Fixed -class WidgetManager { - weak var delegate: WidgetDelegate? - - func setup() { - delegate?.manager = self // Uses weak reference - } -} -``` - -#### 3. Main Thread Blocking -```swift -// ❌ Blocks main thread -func loadData() { - let data = heavyComputation() // Blocking operation - updateUI(with: data) -} - -// ✅ Optimized -func loadData() async { - let data = await Task.detached { - heavyComputation() // Background thread - }.value - - await MainActor.run { - updateUI(with: data) // UI update on main thread - } -} -``` - -### Performance Debugging Tools -- **Instruments**: Profiling and performance analysis -- **Xcode Debugger**: Memory graph debugging -- **Console**: Performance logging and analysis -- **Network Link Conditioner**: Network performance testing - ---- - -This comprehensive performance guide ensures your iOS Widget Development Kit implementation achieves optimal performance while maintaining excellent user experience across all device conditions. diff --git a/Documentation/PerformanceGuide.md b/Documentation/PerformanceGuide.md deleted file mode 100644 index 52d3da5..0000000 --- a/Documentation/PerformanceGuide.md +++ /dev/null @@ -1,358 +0,0 @@ -# Performance Guide - - -## Table of Contents -- [Performance Guide](#performance-guide) -- [Overview](#overview) -- [Performance Metrics](#performance-metrics) - - [Target Performance Goals](#target-performance-goals) -- [Memory Management](#memory-management) - - [Widget Cache Optimization](#widget-cache-optimization) - - [Memory Monitoring](#memory-monitoring) - - [Memory Cleanup Strategies](#memory-cleanup-strategies) -- [Battery Optimization](#battery-optimization) - - [Refresh Rate Management](#refresh-rate-management) - - [Background Processing](#background-processing) -- [Network Optimization](#network-optimization) - - [Request Batching](#request-batching) - - [Caching Strategies](#caching-strategies) -- [Animation Performance](#animation-performance) - - [60fps Animations](#60fps-animations) - - [Animation Optimization](#animation-optimization) -- [Data Processing](#data-processing) - - [Efficient Data Handling](#efficient-data-handling) - - [Background Processing](#background-processing) -- [Error Handling and Recovery](#error-handling-and-recovery) - - [Graceful Degradation](#graceful-degradation) - - [Automatic Recovery](#automatic-recovery) -- [Monitoring and Analytics](#monitoring-and-analytics) - - [Performance Monitoring](#performance-monitoring) - - [Real-time Monitoring](#real-time-monitoring) -- [Best Practices](#best-practices) - - [Widget Design](#widget-design) - - [Code Optimization](#code-optimization) - - [Testing Performance](#testing-performance) -- [Troubleshooting](#troubleshooting) - - [Common Performance Issues](#common-performance-issues) - - [Performance Debugging](#performance-debugging) -- [Conclusion](#conclusion) - - - -## Overview - -This guide provides comprehensive information about optimizing widget performance in the iOS Widget Development Kit. - -## Performance Metrics - -### Target Performance Goals - -- **Widget Loading Time**: <500ms -- **Live Data Updates**: <200ms -- **Memory Usage**: <50MB per widget -- **Battery Consumption**: Minimal impact -- **Animation Frame Rate**: 60fps -- **Background Refresh**: Efficient and intelligent - -## Memory Management - -### Widget Cache Optimization - -```swift -// Configure cache settings -let cacheSettings = WidgetCacheSettings( - maxCacheSize: 50 * 1024 * 1024, // 50MB - cacheExpirationTime: 300, // 5 minutes - enableCompression: true -) - -WidgetEngine.shared.configureCache(cacheSettings) -``` - -### Memory Monitoring - -```swift -// Monitor memory usage -let metrics = WidgetEngine.shared.performanceMetrics -print("Current memory usage: \(metrics.memoryUsage / 1024 / 1024) MB") - -// Set up memory alerts -if metrics.memoryUsage > 100 * 1024 * 1024 { // 100MB - performMemoryCleanup() -} -``` - -### Memory Cleanup Strategies - -1. **Cache Eviction**: Remove old cache entries -2. **Image Optimization**: Compress and resize images -3. **Data Compression**: Compress large data sets -4. **Background Cleanup**: Regular cleanup in background - -## Battery Optimization - -### Refresh Rate Management - -```swift -// Adaptive refresh rates based on battery level -let batteryLevel = UIDevice.current.batteryLevel - -switch batteryLevel { -case 0.0..<0.2: // Low battery - refreshInterval = 300.0 // 5 minutes -case 0.2..<0.5: // Medium battery - refreshInterval = 120.0 // 2 minutes -default: // High battery - refreshInterval = 30.0 // 30 seconds -} -``` - -### Background Processing - -```swift -// Optimize background processing -let backgroundSettings = BackgroundProcessingSettings( - enableBackgroundRefresh: true, - backgroundRefreshInterval: 600.0, // 10 minutes - enableBatteryOptimization: true, - enableNetworkOptimization: true -) -``` - -## Network Optimization - -### Request Batching - -```swift -// Batch multiple requests -let batchRequest = WidgetBatchRequest() -batchRequest.addRequest(WeatherRequest()) -batchRequest.addRequest(CalendarRequest()) -batchRequest.addRequest(FitnessRequest()) - -batchRequest.execute { results in - // Process all results together -} -``` - -### Caching Strategies - -```swift -// Implement intelligent caching -let cacheStrategy = CacheStrategy( - enableMemoryCache: true, - enableDiskCache: true, - cacheExpiration: 300, // 5 minutes - enableCompression: true -) -``` - -## Animation Performance - -### 60fps Animations - -```swift -// Ensure smooth animations -let animationSettings = AnimationSettings( - targetFrameRate: 60, - enableHardwareAcceleration: true, - enableSmoothTransitions: true -) -``` - -### Animation Optimization - -1. **Use Core Animation**: Leverage hardware acceleration -2. **Minimize Layer Count**: Reduce view hierarchy complexity -3. **Optimize Drawing**: Use efficient drawing methods -4. **Avoid Off-screen Rendering**: Minimize expensive operations - -## Data Processing - -### Efficient Data Handling - -```swift -// Process data efficiently -class DataProcessor { - func processWidgetData(_ data: WidgetData) -> ProcessedData { - // Use background queues for processing - return DispatchQueue.global(qos: .userInitiated).sync { - // Process data - return processedData - } - } -} -``` - -### Background Processing - -```swift -// Handle data processing in background -DispatchQueue.global(qos: .background).async { - // Process large data sets - let processedData = processLargeDataSet() - - DispatchQueue.main.async { - // Update UI with processed data - updateWidget(with: processedData) - } -} -``` - -## Error Handling and Recovery - -### Graceful Degradation - -```swift -// Handle errors gracefully -func handleWidgetError(_ error: WidgetError) { - switch error { - case .networkUnavailable: - showCachedData() - case .dataSourceNotFound: - showDefaultWidget() - case .templateNotFound: - showFallbackTemplate() - default: - showErrorWidget() - } -} -``` - -### Automatic Recovery - -```swift -// Implement automatic recovery -class WidgetRecoveryManager { - func attemptRecovery(for error: WidgetError) { - switch error { - case .connectionFailed: - retryConnection() - case .dataDecodeFailed: - reloadData() - case .memoryCleanup: - performMemoryCleanup() - default: - logError(error) - } - } -} -``` - -## Monitoring and Analytics - -### Performance Monitoring - -```swift -// Monitor performance metrics -class PerformanceMonitor { - func trackMetric(_ metric: PerformanceMetric) { - analyticsService.logMetric(metric) - - if metric.value > metric.threshold { - triggerAlert(for: metric) - } - } -} -``` - -### Real-time Monitoring - -```swift -// Real-time performance monitoring -Timer.scheduledTimer(withTimeInterval: 10.0, repeats: true) { _ in - let metrics = WidgetEngine.shared.performanceMetrics - - // Monitor key metrics - monitorMemoryUsage(metrics.memoryUsage) - monitorBatteryLevel(metrics.batteryLevel) - monitorRefreshRate(metrics.refreshCount) -} -``` - -## Best Practices - -### Widget Design - -1. **Keep Widgets Lightweight**: Minimize complexity -2. **Use Efficient Data Structures**: Choose appropriate data types -3. **Implement Lazy Loading**: Load data on demand -4. **Optimize Images**: Use appropriate image formats and sizes - -### Code Optimization - -1. **Avoid Memory Leaks**: Use weak references appropriately -2. **Minimize Network Calls**: Batch requests when possible -3. **Use Efficient Algorithms**: Choose optimal algorithms -4. **Profile Regularly**: Monitor performance continuously - -### Testing Performance - -```swift -// Performance testing -class PerformanceTests: XCTestCase { - func testWidgetLoadingPerformance() { - measure { - let widget = createWidget() - XCTAssertNotNil(widget) - } - } - - func testMemoryUsage() { - let initialMemory = getCurrentMemoryUsage() - - for _ in 0..<100 { - createWidget() - } - - let finalMemory = getCurrentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - XCTAssertLessThan(memoryIncrease, 50 * 1024 * 1024) // 50MB - } -} -``` - -## Troubleshooting - -### Common Performance Issues - -1. **High Memory Usage** - - Check for memory leaks - - Implement proper cleanup - - Use memory-efficient data structures - -2. **Slow Widget Loading** - - Optimize data processing - - Implement caching - - Use background processing - -3. **Battery Drain** - - Reduce refresh frequency - - Optimize network requests - - Implement battery-aware scheduling - -4. **Poor Animation Performance** - - Use Core Animation - - Minimize view hierarchy - - Avoid expensive operations - -### Performance Debugging - -```swift -// Debug performance issues -class PerformanceDebugger { - func debugWidgetPerformance() { - let metrics = WidgetEngine.shared.performanceMetrics - - print("Memory Usage: \(metrics.memoryUsage / 1024 / 1024) MB") - print("Battery Level: \(metrics.batteryLevel * 100)%") - print("Refresh Count: \(metrics.refreshCount)") - print("Error Count: \(metrics.errorCount)") - } -} -``` - -## Conclusion - -Following these performance guidelines will ensure your widgets are fast, efficient, and provide an excellent user experience. Regular monitoring and optimization are key to maintaining high performance standards. \ No newline at end of file diff --git a/Documentation/README.md b/Documentation/README.md deleted file mode 100644 index 397f628..0000000 --- a/Documentation/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Documentation Index - -- [API-Reference.md -- [API.md -- [APIReference.md -- [ArchitectureGuide.md -- [Best-Practices.md -- [ConfigurationAPI.md -- [CustomizationAPI.md -- [CustomizationGuide.md -- [DataIntegrationAPI.md -- [DataIntegrationGuide.md -- [DynamicIslandAPI.md -- [DynamicIslandGuide.md -- [Getting-Started.md -- [GettingStarted.md -- [HomeScreenWidgetAPI.md -- [HomeScreenWidgetGuide.md -- [Installation.md -- [InstallationGuide.md -- [LiveActivityAPI.md -- [LiveActivityGuide.md -- [LockScreenWidgetAPI.md -- [LockScreenWidgetGuide.md -- [PerformanceGuide.md -- [Troubleshooting.md -- [UsageGuide.md -- [WidgetDevelopmentManagerAPI.md diff --git a/Documentation/Security.md b/Documentation/Security.md deleted file mode 100644 index e0ecc6c..0000000 --- a/Documentation/Security.md +++ /dev/null @@ -1,729 +0,0 @@ -# 🛡️ Security & Privacy Guide - iOS Widget Development Kit - -## 📋 Table of Contents - -- [Security Overview](#security-overview) -- [Security Architecture](#security-architecture) -- [Authentication & Authorization](#authentication--authorization) -- [Data Protection](#data-protection) -- [Network Security](#network-security) -- [Privacy Framework](#privacy-framework) -- [Compliance & Standards](#compliance--standards) -- [Security Monitoring](#security-monitoring) -- [Threat Mitigation](#threat-mitigation) -- [Security Best Practices](#security-best-practices) - ---- - -## 🌟 Security Overview - -The iOS Widget Development Kit implements **enterprise-grade security** with a **zero-trust architecture** and **defense-in-depth** strategy. Our security framework ensures comprehensive protection for widget data, user privacy, and system integrity while maintaining optimal performance. - -### Core Security Principles -- **🔒 Zero Trust Architecture**: Never trust, always verify -- **🛡️ Defense in Depth**: Multiple layers of security controls -- **🔐 Privacy by Design**: Built-in privacy protection from the ground up -- **📋 Compliance First**: GDPR, HIPAA, SOX, and CCPA compliant -- **🚨 Proactive Monitoring**: Real-time threat detection and response -- **🔑 Minimal Access**: Principle of least privilege throughout - ---- - -## 🏗️ Security Architecture - -### Multi-Layered Security Model -``` -┌─────────────────────────────────────────────────────────┐ -│ Application Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │Input Validation│ │Authorization│ │ Audit Logging│ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Security Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Encryption │ │Authentication│ │ Privacy │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Transport Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ TLS 1.3 │ │Cert Pinning │ │ HSTS │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -├─────────────────────────────────────────────────────────┤ -│ Platform Layer │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ Keychain │ │ Secure │ │ App │ │ -│ │ Services │ │ Enclave │ │ Sandboxing │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - -### Security Components -- **EncryptionManager**: End-to-end encryption services -- **AuthenticationManager**: Multi-factor authentication -- **PrivacyManager**: Data privacy and consent management -- **SecurityAuditor**: Real-time security monitoring -- **ComplianceManager**: Regulatory compliance enforcement - ---- - -## 🔐 Authentication & Authorization - -### Multi-Factor Authentication -```swift -class AuthenticationManager { - private let biometricAuth = BiometricAuthenticationService() - private let tokenManager = SecureTokenManager() - private let keychain = KeychainManager() - - func authenticateUser() async throws -> AuthenticationResult { - // Primary authentication: Biometric - let biometricResult = try await biometricAuth.authenticate() - guard biometricResult.isSuccessful else { - throw AuthenticationError.biometricFailed - } - - // Secondary authentication: Device binding - let deviceBinding = try await validateDeviceBinding() - guard deviceBinding.isValid else { - throw AuthenticationError.deviceBindingFailed - } - - // Generate secure session token - let sessionToken = try generateSecureSessionToken() - - return AuthenticationResult( - token: sessionToken, - expiresAt: Date().addingTimeInterval(3600), - permissions: determineUserPermissions() - ) - } -} -``` - -### Biometric Integration -```swift -class BiometricAuthenticationService { - private let context = LAContext() - - func authenticate() async throws -> BiometricResult { - var error: NSError? - - guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { - throw BiometricError.notAvailable - } - - do { - let success = try await context.evaluatePolicy( - .deviceOwnerAuthenticationWithBiometrics, - localizedReason: "Authenticate to access secure widget data" - ) - - return BiometricResult( - success: success, - biometryType: context.biometryType, - timestamp: Date() - ) - } catch { - throw BiometricError.authenticationFailed(error) - } - } -} -``` - -### Token Management -```swift -class SecureTokenManager { - private let keychain = KeychainManager() - private let encryption = EncryptionManager() - - func generateSecureToken() throws -> SecureToken { - // Generate cryptographically secure random token - var bytes = Data(count: 32) - let result = bytes.withUnsafeMutableBytes { - SecRandomCopyBytes(kSecRandomDefault, 32, $0.baseAddress!) - } - - guard result == errSecSuccess else { - throw TokenError.generationFailed - } - - // Encrypt token before storage - let encryptedToken = try encryption.encrypt(bytes) - - // Store in Keychain with access control - let accessControl = SecAccessControlCreateWithFlags( - nil, - kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - .biometryCurrentSet, - nil - ) - - try keychain.store( - encryptedToken, - account: "widget.auth.token", - accessControl: accessControl - ) - - return SecureToken( - value: encryptedToken, - expiresAt: Date().addingTimeInterval(3600), - scopes: [.widgetAccess, .dataAccess] - ) - } -} -``` - ---- - -## 🔒 Data Protection - -### Encryption at Rest -```swift -class EncryptionManager { - private let keyManager = CryptographicKeyManager() - - func encryptData(_ data: Data) throws -> EncryptedData { - // Generate unique encryption key - let encryptionKey = try keyManager.generateDataEncryptionKey() - - // Use AES-256-GCM for authenticated encryption - let cipher = try AES.GCM.seal(data, using: encryptionKey) - - // Store key securely in Secure Enclave - try keyManager.storeKeyInSecureEnclave(encryptionKey) - - return EncryptedData( - ciphertext: cipher.ciphertext, - nonce: cipher.nonce, - tag: cipher.tag, - keyIdentifier: encryptionKey.identifier - ) - } - - func decryptData(_ encryptedData: EncryptedData) throws -> Data { - // Retrieve key from Secure Enclave - let encryptionKey = try keyManager.retrieveKeyFromSecureEnclave( - encryptedData.keyIdentifier - ) - - // Reconstruct sealed box - let sealedBox = try AES.GCM.SealedBox( - nonce: encryptedData.nonce, - ciphertext: encryptedData.ciphertext, - tag: encryptedData.tag - ) - - // Decrypt and verify - return try AES.GCM.open(sealedBox, using: encryptionKey) - } -} -``` - -### Secure Key Management -```swift -class CryptographicKeyManager { - func generateDataEncryptionKey() throws -> SymmetricKey { - return SymmetricKey(size: .bits256) - } - - func storeKeyInSecureEnclave(_ key: SymmetricKey) throws { - let keyData = key.withUnsafeBytes { Data($0) } - - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key.identifier, - kSecValueData as String: keyData, - kSecAttrAccessControl as String: SecAccessControlCreateWithFlags( - nil, - kSecAttrAccessibleWhenUnlockedThisDeviceOnly, - .privateKeyUsage, - nil - )! - ] - - let status = SecItemAdd(query as CFDictionary, nil) - guard status == errSecSuccess else { - throw KeyStorageError.failed(status) - } - } -} -``` - -### Data Sanitization -```swift -class DataSanitizer { - static func sanitizeInput(_ input: String) -> String { - // Remove potentially harmful characters - let sanitized = input - .replacingOccurrences(of: "<", with: "<") - .replacingOccurrences(of: ">", with: ">") - .replacingOccurrences(of: "\"", with: """) - .replacingOccurrences(of: "'", with: "'") - .replacingOccurrences(of: "&", with: "&") - - // Validate length - let maxLength = 1000 - return String(sanitized.prefix(maxLength)) - } - - static func validateAndSanitizeJSON(_ json: [String: Any]) throws -> [String: Any] { - var sanitized: [String: Any] = [:] - - for (key, value) in json { - let sanitizedKey = sanitizeInput(key) - - switch value { - case let string as String: - sanitized[sanitizedKey] = sanitizeInput(string) - case let number as NSNumber: - sanitized[sanitizedKey] = number - case let array as [Any]: - sanitized[sanitizedKey] = try sanitizeArray(array) - case let dict as [String: Any]: - sanitized[sanitizedKey] = try validateAndSanitizeJSON(dict) - default: - throw ValidationError.unsupportedDataType - } - } - - return sanitized - } -} -``` - ---- - -## 🌐 Network Security - -### TLS Configuration -```swift -class NetworkSecurityManager { - private let session: URLSession - - init() { - let configuration = URLSessionConfiguration.default - configuration.tlsMinimumSupportedProtocolVersion = .TLSv13 - configuration.tlsMaximumSupportedProtocolVersion = .TLSv13 - - self.session = URLSession( - configuration: configuration, - delegate: self, - delegateQueue: nil - ) - } -} - -extension NetworkSecurityManager: URLSessionDelegate { - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - // Certificate pinning implementation - guard let serverTrust = challenge.protectionSpace.serverTrust else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } - - // Validate certificate chain - let policy = SecPolicyCreateSSL(true, "api.widgetkit.com" as CFString) - SecTrustSetPolicies(serverTrust, policy) - - var result: SecTrustResultType = .invalid - let status = SecTrustEvaluate(serverTrust, &result) - - guard status == errSecSuccess && (result == .unspecified || result == .proceed) else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } - - // Pin specific certificates - if validateCertificatePinning(serverTrust) { - completionHandler(.useCredential, URLCredential(trust: serverTrust)) - } else { - completionHandler(.cancelAuthenticationChallenge, nil) - } - } - - private func validateCertificatePinning(_ serverTrust: SecTrust) -> Bool { - // Implementation of certificate pinning validation - // Compare against known good certificates - return true // Simplified for example - } -} -``` - -### API Security -```swift -class SecureAPIClient { - private let baseURL = URL(string: "https://api.widgetkit.com")! - private let authManager = AuthenticationManager() - private let encryption = EncryptionManager() - - func secureRequest( - endpoint: String, - method: HTTPMethod, - body: T? = nil - ) async throws -> APIResponse { - - // Get authenticated session - let auth = try await authManager.getCurrentSession() - - // Prepare request - var request = URLRequest(url: baseURL.appendingPathComponent(endpoint)) - request.httpMethod = method.rawValue - request.setValue("Bearer \(auth.token)", forHTTPHeaderField: "Authorization") - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - // Encrypt request body if present - if let body = body { - let bodyData = try JSONEncoder().encode(body) - let encryptedBody = try encryption.encryptData(bodyData) - request.httpBody = try JSONEncoder().encode(encryptedBody) - } - - // Add request signing - try signRequest(&request, with: auth.token) - - // Execute request - let (data, response) = try await URLSession.shared.data(for: request) - - // Validate response - guard let httpResponse = response as? HTTPURLResponse, - 200...299 ~= httpResponse.statusCode else { - throw APIError.invalidResponse - } - - // Decrypt and parse response - let encryptedResponse = try JSONDecoder().decode(EncryptedData.self, from: data) - let decryptedData = try encryption.decryptData(encryptedResponse) - let result = try JSONDecoder().decode(APIResponse.self, from: decryptedData) - - return result - } -} -``` - ---- - -## 🔒 Privacy Framework - -### Privacy by Design -```swift -class PrivacyManager { - private let consentManager = ConsentManager() - private let dataMinimizer = DataMinimizer() - private let anonymizer = DataAnonymizer() - - func collectData(_ data: T, for purpose: DataPurpose) async throws -> Bool { - // Check user consent - guard try await consentManager.hasConsent(for: purpose) else { - throw PrivacyError.noConsent - } - - // Minimize data collection - let minimizedData = try dataMinimizer.minimize(data, for: purpose) - - // Anonymize if required - let processedData = try anonymizer.anonymize(minimizedData, level: purpose.anonymizationLevel) - - // Store with privacy metadata - try await storeDataWithPrivacyMetadata(processedData, purpose: purpose) - - return true - } -} -``` - -### Consent Management -```swift -class ConsentManager { - private let storage = SecureStorage() - - func requestConsent(for purposes: [DataPurpose]) async throws -> ConsentResult { - // Present consent UI - let consentUI = ConsentViewController(purposes: purposes) - let result = try await consentUI.present() - - // Store consent with cryptographic proof - for purpose in purposes { - if result.approvedPurposes.contains(purpose) { - try await storeConsent(for: purpose, granted: true) - } - } - - return result - } - - private func storeConsent(for purpose: DataPurpose, granted: Bool) async throws { - let consent = ConsentRecord( - purpose: purpose, - granted: granted, - timestamp: Date(), - version: "1.0", - signature: try generateConsentSignature(purpose, granted) - ) - - try await storage.store(consent, key: "consent.\(purpose.id)") - } -} -``` - -### Data Anonymization -```swift -class DataAnonymizer { - func anonymize(_ data: T, level: AnonymizationLevel) throws -> AnonymizedData { - switch level { - case .none: - return AnonymizedData(data: data, level: .none) - - case .pseudonymization: - return try pseudonymize(data) - - case .kAnonymity(let k): - return try applyKAnonymity(data, k: k) - - case .differential: - return try applyDifferentialPrivacy(data) - } - } - - private func pseudonymize(_ data: T) throws -> AnonymizedData { - // Replace identifiers with pseudonyms - // Implementation depends on data type - return AnonymizedData(data: data, level: .pseudonymization) - } -} -``` - ---- - -## 📋 Compliance & Standards - -### GDPR Compliance -```swift -class GDPRComplianceManager { - func handleDataSubjectRequest(_ request: DataSubjectRequest) async throws -> ComplianceResponse { - switch request.type { - case .access: - return try await handleAccessRequest(request) - case .rectification: - return try await handleRectificationRequest(request) - case .erasure: - return try await handleErasureRequest(request) - case .portability: - return try await handlePortabilityRequest(request) - case .restriction: - return try await handleRestrictionRequest(request) - } - } - - private func handleErasureRequest(_ request: DataSubjectRequest) async throws -> ComplianceResponse { - // Validate request - guard try await validateDataSubject(request.subjectId) else { - throw ComplianceError.invalidSubject - } - - // Find all data for subject - let dataLocations = try await findAllDataForSubject(request.subjectId) - - // Secure deletion - for location in dataLocations { - try await securelyDeleteData(at: location) - } - - // Generate compliance proof - let proof = try generateDeletionProof(for: request.subjectId) - - return ComplianceResponse( - requestId: request.id, - status: .completed, - proof: proof, - completedAt: Date() - ) - } -} -``` - -### Security Audit Trail -```swift -class SecurityAuditLogger { - private let encryptedStorage = EncryptedAuditStorage() - - func logSecurityEvent(_ event: SecurityEvent) async { - let auditEntry = AuditEntry( - timestamp: Date(), - eventType: event.type, - severity: event.severity, - userId: event.userId, - details: event.details, - ipAddress: event.ipAddress, - userAgent: event.userAgent, - signature: try? generateAuditSignature(event) - ) - - await encryptedStorage.store(auditEntry) - - // Real-time monitoring - if event.severity >= .high { - await notifySecurityTeam(event) - } - } - - private func generateAuditSignature(_ event: SecurityEvent) throws -> String { - let data = "\(event.type)|\(event.timestamp)|\(event.userId ?? "")|\(event.details)" - return try HMAC.authenticationCode( - for: Data(data.utf8), - using: getAuditSigningKey() - ).description - } -} -``` - ---- - -## 🚨 Security Monitoring - -### Threat Detection -```swift -class ThreatDetectionEngine { - private let anomalyDetector = AnomalyDetector() - private let threatIntelligence = ThreatIntelligenceService() - - func analyzeSecurityEvent(_ event: SecurityEvent) async -> ThreatAssessment { - // Check against known threat patterns - let knownThreats = await threatIntelligence.checkEvent(event) - - // Analyze for anomalies - let anomalyScore = await anomalyDetector.calculateAnomalyScore(event) - - // Calculate overall threat level - let threatLevel = calculateThreatLevel( - knownThreats: knownThreats, - anomalyScore: anomalyScore - ) - - // Generate response recommendations - let recommendations = generateSecurityRecommendations( - threatLevel: threatLevel, - event: event - ) - - return ThreatAssessment( - event: event, - threatLevel: threatLevel, - confidence: calculateConfidence(knownThreats, anomalyScore), - recommendations: recommendations, - timestamp: Date() - ) - } -} -``` - -### Incident Response -```swift -class IncidentResponseManager { - func handleSecurityIncident(_ incident: SecurityIncident) async { - // Immediate containment - await containThreat(incident) - - // Forensic analysis - let analysis = await performForensicAnalysis(incident) - - // Notify stakeholders - await notifyStakeholders(incident, analysis: analysis) - - // Recovery actions - await initiateRecovery(incident) - - // Lessons learned - await updateSecurityControls(based: analysis) - } -} -``` - ---- - -## 🛡️ Security Best Practices - -### Secure Development Guidelines - -#### 1. Input Validation -```swift -// ✅ Always validate and sanitize input -func processUserInput(_ input: String) throws -> ProcessedInput { - // Validate length - guard input.count <= 1000 else { - throw ValidationError.inputTooLong - } - - // Sanitize content - let sanitized = DataSanitizer.sanitizeInput(input) - - // Validate format - guard isValidFormat(sanitized) else { - throw ValidationError.invalidFormat - } - - return ProcessedInput(value: sanitized) -} -``` - -#### 2. Secure Error Handling -```swift -// ✅ Don't expose sensitive information in errors -func authenticateUser(_ credentials: UserCredentials) throws -> AuthResult { - do { - return try performAuthentication(credentials) - } catch AuthenticationError.invalidCredentials { - // Log actual error securely - SecurityLogger.logAuthFailure(credentials.username) - - // Return generic error to user - throw AuthenticationError.authenticationFailed - } -} -``` - -#### 3. Secure Logging -```swift -// ✅ Never log sensitive data -func logUserAction(_ action: UserAction) { - let logEntry = LogEntry( - userId: action.userId.hashed(), // Hash instead of plain text - action: action.type.rawValue, - timestamp: Date(), - sessionId: action.sessionId.truncated() // Truncate session ID - ) - - logger.log(logEntry) -} -``` - -### Security Checklist - -#### Development Phase -- [ ] All inputs validated and sanitized -- [ ] Sensitive data encrypted at rest and in transit -- [ ] Authentication and authorization implemented -- [ ] Error handling doesn't expose sensitive information -- [ ] Logging follows security guidelines -- [ ] Dependencies regularly updated and scanned - -#### Testing Phase -- [ ] Security tests included in test suite -- [ ] Penetration testing performed -- [ ] Vulnerability scanning completed -- [ ] Code review with security focus -- [ ] Compliance requirements verified - -#### Deployment Phase -- [ ] Security configurations reviewed -- [ ] Monitoring and alerting configured -- [ ] Incident response plan in place -- [ ] Backup and recovery tested -- [ ] Documentation updated - ---- - -This comprehensive security guide ensures that your iOS Widget Development Kit implementation maintains the highest security standards while protecting user privacy and complying with relevant regulations. diff --git a/Documentation/Testing.md b/Documentation/Testing.md deleted file mode 100644 index e5ddc55..0000000 --- a/Documentation/Testing.md +++ /dev/null @@ -1,644 +0,0 @@ -# Comprehensive Testing Guide — iOS Widget Development Kit - -## Overview - -This document outlines the comprehensive testing strategy for the iOS Widget Development Kit, covering unit tests, integration tests, performance testing, security testing, and end-to-end validation across all enterprise components. - -## Testing Architecture - -### Test Pyramid Structure - -``` - ╭─────────────╮ - ╱ E2E Tests ╲ ← 10% (Critical user journeys) - ╱_______________╲ - ╱ ╲ -╱ Integration Tests ╲ ← 20% (Component interactions) -╲___________________╱ - ╲ ╱ - ╲ Unit Tests ╱ ← 70% (Individual components) - ╲_____________╱ -``` - -### Coverage Requirements - -- **Unit Tests**: 95%+ coverage for all business logic -- **Integration Tests**: 90%+ coverage for critical data paths -- **E2E Tests**: 100% coverage for core user workflows -- **Performance Tests**: All critical paths under 100ms -- **Security Tests**: All authentication and encryption flows - -## Testing Framework Setup - -### Dependencies - -```swift -// Package.swift -dependencies: [ - .package(url: "https://github.com/Quick/Quick.git", from: "6.0.0"), - .package(url: "https://github.com/Quick/Nimble.git", from: "11.0.0"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing.git", from: "1.12.0") -] -``` - -### Test Configuration - -```swift -// TestConfiguration.swift -import XCTest -import Quick -import Nimble -@testable import WidgetDevelopmentKit - -class TestConfiguration { - static let shared = TestConfiguration() - - var testEnvironment: TestEnvironment = .testing - var mockDataEnabled: Bool = true - var performanceTestingEnabled: Bool = true - var securityTestingEnabled: Bool = true -} -``` - -## Unit Testing - -### Core Components - -#### Widget Manager Tests - -```swift -class WidgetManagerTests: QuickSpec { - override func spec() { - describe("WidgetManager") { - var sut: WidgetManager! - var mockDataSource: MockDataSource! - - beforeEach { - mockDataSource = MockDataSource() - sut = WidgetManager(dataSource: mockDataSource) - } - - context("when creating widgets") { - it("should create widget with valid configuration") { - let config = WidgetConfiguration.mock() - let result = sut.createWidget(with: config) - expect(result).to(beAKindOf(Widget.self)) - } - - it("should throw error for invalid configuration") { - let invalidConfig = WidgetConfiguration.invalid() - expect { - try sut.createWidget(with: invalidConfig) - }.to(throwError(WidgetError.invalidConfiguration)) - } - } - } - } -} -``` - -#### Security Component Tests - -```swift -class EncryptionManagerTests: XCTestCase { - var sut: EncryptionManager! - - override func setUp() { - super.setUp() - sut = EncryptionManager() - } - - func testDataEncryption() async throws { - // Given - let testData = "Test encryption data".data(using: .utf8)! - - // When - let encryptedData = try await sut.encryptData(testData, keyIdentifier: "test-key") - let decryptedData = try await sut.decryptData(encryptedData) - - // Then - XCTAssertEqual(testData, decryptedData) - XCTAssertNotEqual(testData, encryptedData.data) - } - - func testSecureEnclaveIntegration() async throws { - // Performance test for Secure Enclave operations - let testData = Data(repeating: 0x42, count: 1024) - - let startTime = CFAbsoluteTimeGetCurrent() - _ = try await sut.encryptData(testData, requireBiometric: true) - let duration = CFAbsoluteTimeGetCurrent() - startTime - - XCTAssertLessThan(duration, 0.1, "Secure Enclave encryption should complete within 100ms") - } -} -``` - -#### Performance Component Tests - -```swift -class PerformanceMonitorTests: XCTestCase { - var sut: PerformanceMonitor! - - override func setUp() { - super.setUp() - sut = PerformanceMonitor.shared - } - - func testMemoryMonitoring() { - // Given - let initialMemory = sut.getCurrentMemoryUsage() - - // When - let largeArray = Array(0..<1_000_000) - let newMemory = sut.getCurrentMemoryUsage() - - // Then - XCTAssertGreaterThan(newMemory, initialMemory) - _ = largeArray // Keep reference to avoid optimization - } - - func testPerformanceThresholds() { - let metric = PerformanceMetric( - type: .renderTime, - value: 0.05, // 50ms - threshold: 0.1, // 100ms - timestamp: Date() - ) - - XCTAssertFalse(sut.isThresholdExceeded(metric)) - - let slowMetric = PerformanceMetric( - type: .renderTime, - value: 0.15, // 150ms - threshold: 0.1, // 100ms - timestamp: Date() - ) - - XCTAssertTrue(sut.isThresholdExceeded(slowMetric)) - } -} -``` - -## Integration Testing - -### Data Flow Tests - -```swift -class DataIntegrationTests: XCTestCase { - var dataSourceManager: DataSourceManager! - var apiManager: APIManager! - var cacheManager: CacheManager! - - override func setUp() { - super.setUp() - dataSourceManager = DataSourceManager.shared - apiManager = APIManager.shared - cacheManager = CacheManager.shared - } - - func testEndToEndDataFlow() async throws { - // Given - let dataSource = DataSourceConfiguration( - name: "test-api", - type: .rest, - endpoint: "https://api.test.com" - ) - - try await dataSourceManager.registerDataSource(dataSource) - - // When - let query = DataQuery( - dataSourceName: "test-api", - query: "users/1", - enableCache: true - ) - - let response: DataResponse = try await dataSourceManager.executeQuery( - query, - responseType: UserModel.self - ) - - // Then - XCTAssertTrue(response.isSuccess) - XCTAssertNotNil(response.data) - - // Verify caching - let cachedResponse: DataResponse = try await dataSourceManager.executeQuery( - query, - responseType: UserModel.self - ) - - XCTAssertTrue(cachedResponse.cached) - } -} -``` - -### Cloud Integration Tests - -```swift -class CloudIntegrationTests: XCTestCase { - var cloudIntegration: CloudIntegration! - - override func setUp() { - super.setUp() - cloudIntegration = CloudIntegration.shared - } - - func testMultiCloudSync() async throws { - // Given - let configuration = CloudIntegration.CloudConfiguration() - configuration.primaryProvider = .icloud - configuration.secondaryProviders = [.firebase] - configuration.syncStrategy = .multiCloud - - try await cloudIntegration.initialize(configuration: configuration) - - // When - let testData = "Test sync data".data(using: .utf8)! - let cloudFile = try await cloudIntegration.upload( - data: testData, - to: "test/sync-file.json" - ) - - // Wait for sync - try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second - - // Then - let downloadedData = try await cloudIntegration.download(path: cloudFile.path) - XCTAssertEqual(testData, downloadedData) - - // Verify sync status - let syncStatus = await cloudIntegration.getSyncStatus() - XCTAssertEqual(syncStatus.count, 2) // Primary + Secondary - } -} -``` - -## Performance Testing - -### Load Testing - -```swift -class PerformanceTests: XCTestCase { - func testWidgetRenderingPerformance() { - measure { - let widget = ComplexWidget() - widget.render() - } - } - - func testConcurrentDataLoading() async throws { - let expectation = expectation(description: "Concurrent loading") - expectation.expectedFulfillmentCount = 100 - - await withTaskGroup(of: Void.self) { group in - for i in 0..<100 { - group.addTask { - do { - let query = DataQuery( - dataSourceName: "test-api", - query: "data/\(i)" - ) - _ = try await DataSourceManager.shared.executeQuery( - query, - responseType: GenericModel.self - ) - expectation.fulfill() - } catch { - XCTFail("Concurrent request failed: \(error)") - } - } - } - } - - await fulfillment(of: [expectation], timeout: 5.0) - } - - func testMemoryLeaks() { - weak var weakWidget: Widget? - - autoreleasepool { - let widget = Widget(configuration: .default) - weakWidget = widget - widget.performHeavyOperation() - } - - XCTAssertNil(weakWidget, "Widget should be deallocated") - } -} -``` - -### Battery Performance Tests - -```swift -class BatteryPerformanceTests: XCTestCase { - func testBatteryOptimization() { - let optimizationEngine = OptimizationEngine.shared - - measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) { - Task { - try? await optimizationEngine.optimizeBattery() - } - } - } -} -``` - -## Security Testing - -### Authentication Tests - -```swift -class SecurityTests: XCTestCase { - var authManager: AuthenticationManager! - - override func setUp() { - super.setUp() - authManager = AuthenticationManager.shared - } - - func testBiometricAuthentication() async throws { - // Given - let mockBiometric = MockBiometricService() - authManager.biometricService = mockBiometric - - // When - mockBiometric.isAvailable = true - mockBiometric.shouldSucceed = true - - let result = try await authManager.authenticate(reason: "Test authentication") - - // Then - XCTAssertTrue(result.success) - XCTAssertEqual(result.method, .biometric) - } - - func testEncryptionCompliance() async throws { - let privacyManager = PrivacyManager.shared - - // Test GDPR compliance - let gdprResult = try await privacyManager.validateGDPRCompliance() - XCTAssertTrue(gdprResult.isCompliant) - - // Test HIPAA compliance - let hipaaResult = try await privacyManager.validateHIPAACompliance() - XCTAssertTrue(hipaaResult.isCompliant) - } -} -``` - -### Penetration Testing - -```swift -class PenetrationTests: XCTestCase { - func testSQLInjectionPrevention() async throws { - let maliciousQuery = "'; DROP TABLE users; --" - - do { - let query = DataQuery( - dataSourceName: "database", - query: maliciousQuery - ) - _ = try await DataSourceManager.shared.executeQuery( - query, - responseType: GenericModel.self - ) - XCTFail("Should have prevented SQL injection") - } catch DataSourceError.validationFailed { - // Expected behavior - } - } - - func testDataEncryptionAtRest() async throws { - let sensitiveData = "SSN: 123-45-6789" - - let encryptedData = try await EncryptionManager.shared.encryptData( - sensitiveData.data(using: .utf8)!, - keyIdentifier: "sensitive-data" - ) - - // Verify data is actually encrypted - let encryptedString = String(data: encryptedData.data, encoding: .utf8) ?? "" - XCTAssertFalse(encryptedString.contains("123-45-6789")) - } -} -``` - -## End-to-End Testing - -### User Journey Tests - -```swift -class E2ETests: XCTestCase { - func testCompleteWidgetLifecycle() async throws { - // 1. Initialize system - let widgetManager = AdvancedWidgetManager.shared - try await widgetManager.initialize() - - // 2. Create widget - let configuration = WidgetConfiguration.weather() - let widget = try await widgetManager.createWidget(with: configuration) - - // 3. Load data - let weatherData = try await widget.loadData() - XCTAssertNotNil(weatherData) - - // 4. Render widget - let renderStart = CFAbsoluteTimeGetCurrent() - try await widget.render() - let renderTime = CFAbsoluteTimeGetCurrent() - renderStart - - XCTAssertLessThan(renderTime, 0.1, "Widget rendering should be under 100ms") - - // 5. Update data - try await widget.refreshData() - - // 6. Verify performance metrics - let metrics = await PerformanceMonitor.shared.getMetrics(for: widget.id) - XCTAssertLessThan(metrics.averageRenderTime, 0.1) - XCTAssertLessThan(metrics.memoryUsage, 50_000_000) // 50MB limit - } -} -``` - -## Test Utilities and Mocks - -### Mock Data Sources - -```swift -class MockDataSource: DataSourceProtocol { - var shouldSucceed: Bool = true - var responseDelay: TimeInterval = 0.01 - var mockData: [String: Any] = [:] - - func fetch(_ type: T.Type, from endpoint: String) async throws -> T { - if !shouldSucceed { - throw DataSourceError.connectionFailed("Mock failure") - } - - try await Task.sleep(nanoseconds: UInt64(responseDelay * 1_000_000_000)) - - guard let data = mockData[endpoint] else { - throw DataSourceError.dataNotFound - } - - let jsonData = try JSONSerialization.data(withJSONObject: data) - return try JSONDecoder().decode(type, from: jsonData) - } -} -``` - -### Test Data Builders - -```swift -struct TestDataBuilder { - static func buildWidget( - type: WidgetType = .weather, - size: WidgetSize = .medium, - configuration: WidgetConfiguration? = nil - ) -> Widget { - let config = configuration ?? WidgetConfiguration.default(for: type) - return Widget(type: type, size: size, configuration: config) - } - - static func buildUserData( - name: String = "Test User", - email: String = "test@example.com", - permissions: [Permission] = [.read, .write] - ) -> UserData { - return UserData(name: name, email: email, permissions: permissions) - } -} -``` - -## Continuous Integration - -### GitHub Actions Configuration - -```yaml -name: iOS Widget Development Kit Tests - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: macos-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Run Unit Tests - run: | - xcodebuild test \ - -scheme WidgetDevelopmentKit \ - -destination 'platform=iOS Simulator,name=iPhone 14' \ - -resultBundlePath TestResults \ - -enableCodeCoverage YES - - - name: Generate Code Coverage - run: | - xcrun xccov view --report --json TestResults.xcresult > coverage.json - - - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: coverage.json -``` - -## Test Commands - -### Xcode Testing -```bash -# Run all tests -⌘U (Product > Test) - -# Run specific test class -⌘⇧U (Product > Test > Test Class) - -# Run with coverage -Product > Test > Test with Code Coverage -``` - -### Swift Package Manager Testing -```bash -# Run all tests -swift test - -# Run specific test -swift test --filter WidgetManagerTests - -# Run with coverage -swift test --enable-code-coverage - -# Generate coverage report -swift test --enable-code-coverage --build-path .build -``` - -### Performance Testing -```bash -# Run performance tests only -swift test --filter PerformanceTests - -# Run with instruments -xcodebuild test -scheme WidgetDevelopmentKit \ - -destination 'platform=iOS Simulator,name=iPhone 14' \ - -enableAddressSanitizer YES \ - -enableThreadSanitizer YES -``` - -## Best Practices - -### Test Organization -- Group tests by feature/component -- Use descriptive test names -- Follow AAA pattern (Arrange, Act, Assert) -- Keep tests independent and isolated -- Use meaningful assertions with custom messages - -### Performance Testing Guidelines -- Set realistic performance thresholds -- Test on various device configurations -- Monitor memory usage and leaks -- Test with realistic data volumes -- Validate battery impact - -### Security Testing Standards -- Test all authentication flows -- Validate encryption implementations -- Test access control mechanisms -- Verify data sanitization -- Test for common vulnerabilities (OWASP Top 10) - -### Continuous Testing -- Run tests on every commit -- Maintain high code coverage (95%+) -- Automate performance regression detection -- Include security scanning in CI/CD -- Generate comprehensive test reports - -## Troubleshooting - -### Common Issues -1. **Flaky Tests**: Use proper async/await patterns, avoid hardcoded delays -2. **Slow Tests**: Optimize setup/teardown, use mocks effectively -3. **Memory Leaks**: Use weak references, proper autoreleasepool usage -4. **Device Differences**: Test on multiple simulators and devices - -### Debug Tools -- Xcode Test Navigator -- Instruments for performance analysis -- Console for detailed logging -- Accessibility Inspector for UI tests -- Network Link Conditioner for network testing - -This comprehensive testing strategy ensures the highest quality and reliability for the iOS Widget Development Kit across all enterprise scenarios. diff --git a/Documentation/Troubleshooting.md b/Documentation/Troubleshooting.md deleted file mode 100644 index 3a45155..0000000 --- a/Documentation/Troubleshooting.md +++ /dev/null @@ -1,955 +0,0 @@ -# Comprehensive Troubleshooting Guide — iOS Widget Development Kit - -## Overview - -This comprehensive troubleshooting guide provides solutions for common issues, debugging strategies, and performance optimization techniques for the iOS Widget Development Kit. Whether you're facing build errors, runtime issues, or performance problems, this guide offers step-by-step solutions. - -## Table of Contents - -1. [Build & Compilation Issues](#build--compilation-issues) -2. [Runtime Errors](#runtime-errors) -3. [Performance Issues](#performance-issues) -4. [Widget Display Problems](#widget-display-problems) -5. [Data & Network Issues](#data--network-issues) -6. [Security & Privacy Issues](#security--privacy-issues) -7. [Memory & Resource Issues](#memory--resource-issues) -8. [Debugging Techniques](#debugging-techniques) -9. [Common Error Codes](#common-error-codes) -10. [Platform-Specific Issues](#platform-specific-issues) - -## Build & Compilation Issues - -### Issue: Module 'WidgetDevelopmentKit' Not Found - -**Symptoms:** -``` -error: no such module 'WidgetDevelopmentKit' -import WidgetDevelopmentKit - ^ -``` - -**Solutions:** - -1. **Verify SPM Installation:** -```bash -# Clean and reset SPM -rm -rf .build -rm Package.resolved -swift package reset -swift package resolve -``` - -2. **Check Package.swift:** -```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "3.2.0") -] -``` - -3. **Xcode Integration:** -- File → Add Package Dependencies -- Enter repository URL -- Select version rule: "Up to Next Major Version" -- Add to target - -### Issue: Swift Version Incompatibility - -**Symptoms:** -``` -error: compiling for iOS 15.0, but module was compiled for iOS 16.0 -``` - -**Solutions:** - -1. **Update Deployment Target:** -```swift -// In your target settings -platforms: [ - .iOS(.v16) -] -``` - -2. **Check Swift Version:** -```bash -swift --version -# Should be Swift 5.7 or later -``` - -3. **Update Build Settings:** -- Project → Build Settings → Swift Language Version → Swift 5 -- Deployment Target → iOS 16.0 - -### Issue: Ambiguous Type References - -**Symptoms:** -``` -error: 'Widget' is ambiguous for type lookup in this context -``` - -**Solution:** -```swift -// Use fully qualified names -typealias AppWidget = WidgetDevelopmentKit.Widget -typealias SystemWidget = WidgetKit.Widget - -// Or use explicit module prefix -let widget = WidgetDevelopmentKit.Widget(configuration: config) -``` - -## Runtime Errors - -### Issue: Widget Not Updating - -**Symptoms:** -- Widget shows stale data -- Update timeline not triggering -- Background refresh not working - -**Debugging Steps:** - -1. **Check Update Configuration:** -```swift -// Verify update interval -let configuration = WidgetConfiguration( - type: .weather, - size: .medium, - updateInterval: 300 // 5 minutes minimum -) - -// Enable debug logging -configuration.behavior.intelligentScheduling = false -``` - -2. **Verify Timeline Provider:** -```swift -func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - let currentDate = Date() - let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)! - - // Create timeline with proper refresh policy - let timeline = Timeline(entries: entries, policy: .after(refreshDate)) - - // Log for debugging - logger.debug("Timeline refresh scheduled for: \(refreshDate)") - - completion(timeline) -} -``` - -3. **Check Background App Refresh:** -```swift -// In Info.plist -UIBackgroundModes - - fetch - processing - - -// In AppDelegate -func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - Task { - do { - try await WidgetCenter.shared.reloadAllTimelines() - completionHandler(.newData) - } catch { - completionHandler(.failed) - } - } -} -``` - -### Issue: Widget Crashes on Launch - -**Symptoms:** -``` -Thread 1: EXC_BAD_ACCESS (code=1, address=0x0) -``` - -**Solutions:** - -1. **Enable Zombie Objects:** -- Edit Scheme → Run → Diagnostics → Enable Zombie Objects - -2. **Add Exception Breakpoint:** -```swift -// Identify crash location -class WidgetDebugger { - static func captureException() { - NSSetUncaughtExceptionHandler { exception in - print("Exception: \(exception)") - print("Stack trace: \(exception.callStackSymbols)") - } - } -} -``` - -3. **Check Memory Management:** -```swift -// Avoid retain cycles -class WidgetViewModel { - weak var delegate: WidgetDelegate? - - func loadData() { - dataLoader.fetch { [weak self] data in - self?.processData(data) - } - } -} -``` - -### Issue: Authentication Failures - -**Symptoms:** -``` -AuthenticationError: Biometric authentication failed -``` - -**Solutions:** - -1. **Check Biometric Availability:** -```swift -func authenticateUser() async throws { - let authManager = AuthenticationManager.shared - - // Check availability first - guard await authManager.isBiometricAvailable() else { - // Fallback to passcode - return try await authManager.authenticateWithPasscode() - } - - do { - try await authManager.authenticate( - reason: "Authenticate to access widget data", - allowFallback: true - ) - } catch AuthenticationError.biometricLockout { - // Handle lockout - try await authManager.resetBiometric() - } -} -``` - -2. **Add Info.plist Keys:** -```xml -NSFaceIDUsageDescription -Authenticate to access secure widget data -``` - -## Performance Issues - -### Issue: Slow Widget Rendering - -**Symptoms:** -- Widget takes >100ms to render -- UI freezes during updates -- High CPU usage - -**Performance Analysis:** - -1. **Profile with Instruments:** -```swift -// Add signposts for profiling -import os.signpost - -let log = OSLog(subsystem: "com.app.widget", category: .pointsOfInterest) - -func renderWidget() { - let signpostID = OSSignpostID(log: log) - - os_signpost(.begin, log: log, name: "Widget Render", signpostID: signpostID) - defer { - os_signpost(.end, log: log, name: "Widget Render", signpostID: signpostID) - } - - // Rendering code -} -``` - -2. **Optimize View Hierarchy:** -```swift -// ❌ Bad: Complex nested views -struct ComplexWidget: View { - var body: some View { - VStack { - ForEach(0..<100) { _ in - HStack { - ForEach(0..<10) { _ in - Text("Item") - } - } - } - } - } -} - -// ✅ Good: Optimized flat structure -struct OptimizedWidget: View { - var body: some View { - LazyVGrid(columns: columns) { - ForEach(items) { item in - ItemView(item: item) - } - } - } -} -``` - -3. **Implement Caching:** -```swift -class WidgetDataCache { - private let cache = NSCache() - - func getCachedData(for key: String) -> Data? { - // Set memory limits - cache.totalCostLimit = 10 * 1024 * 1024 // 10MB - cache.countLimit = 50 - - return cache.object(forKey: NSString(string: key))?.data - } -} -``` - -### Issue: High Memory Usage - -**Symptoms:** -``` -Received memory pressure notification -Widget terminated due to memory pressure -``` - -**Solutions:** - -1. **Monitor Memory Usage:** -```swift -class MemoryMonitor { - func getCurrentMemoryUsage() -> Int64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size) / 4 - - let result = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, - task_flavor_t(MACH_TASK_BASIC_INFO), - $0, - &count) - } - } - - return result == KERN_SUCCESS ? Int64(info.resident_size) : 0 - } - - func handleMemoryWarning() { - // Clear caches - URLCache.shared.removeAllCachedResponses() - - // Release non-essential resources - ImageCache.shared.clearMemoryCache() - - // Force garbage collection - autoreleasepool { } - } -} -``` - -2. **Optimize Image Loading:** -```swift -// ❌ Bad: Loading full resolution -let image = UIImage(named: "large_image") - -// ✅ Good: Downsampling for widgets -func downsampledImage(at url: URL, maxSize: CGSize) -> UIImage? { - let options: [CFString: Any] = [ - kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height), - kCGImageSourceCreateThumbnailFromImageAlways: true, - kCGImageSourceCreateThumbnailWithTransform: true - ] - - guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil), - let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { - return nil - } - - return UIImage(cgImage: cgImage) -} -``` - -## Widget Display Problems - -### Issue: Widget Not Appearing - -**Symptoms:** -- Widget not in widget gallery -- "Unable to Load" message - -**Solutions:** - -1. **Verify Widget Extension Configuration:** -```swift -@main -struct WidgetBundle: WidgetBundle { - var body: some Widget { - WeatherWidget() - CalendarWidget() - } -} - -// Ensure proper configuration -struct WeatherWidget: Widget { - let kind: String = "WeatherWidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: Provider()) { entry in - WeatherWidgetView(entry: entry) - } - .configurationDisplayName("Weather") - .description("Shows current weather") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} -``` - -2. **Check Entitlements:** -```xml - -com.apple.security.application-groups - - group.com.yourapp.widgets - -``` - -### Issue: Widget Layout Issues - -**Symptoms:** -- Content clipped or overflowing -- Incorrect sizing -- Layout breaking on different devices - -**Solutions:** - -```swift -struct AdaptiveWidgetView: View { - @Environment(\.widgetFamily) var family - - var body: some View { - GeometryReader { geometry in - content(for: geometry.size) - } - } - - @ViewBuilder - func content(for size: CGSize) -> some View { - switch family { - case .systemSmall: - SmallWidgetLayout() - .frame(width: size.width, height: size.height) - case .systemMedium: - MediumWidgetLayout() - .frame(width: size.width, height: size.height) - case .systemLarge: - LargeWidgetLayout() - .frame(width: size.width, height: size.height) - default: - EmptyView() - } - } -} -``` - -## Data & Network Issues - -### Issue: API Rate Limiting - -**Symptoms:** -``` -Error: Rate limit exceeded (429) -``` - -**Solutions:** - -1. **Implement Exponential Backoff:** -```swift -class RetryManager { - func performRequestWithRetry( - operation: () async throws -> T, - maxAttempts: Int = 3 - ) async throws -> T { - var lastError: Error? - - for attempt in 0.. Bool { - let now = Date() - let oneMinuteAgo = now.addingTimeInterval(-60) - - // Remove old timestamps - requestTimestamps.removeAll { $0 < oneMinuteAgo } - - return requestTimestamps.count < maxRequestsPerMinute - } - - func recordRequest() { - requestTimestamps.append(Date()) - } -} -``` - -### Issue: Data Synchronization Conflicts - -**Symptoms:** -- Conflicting data between devices -- Lost updates -- Sync failures - -**Solution:** -```swift -class ConflictResolver { - func resolveConflict(local: WidgetData, remote: WidgetData) -> WidgetData { - // Last-write-wins strategy - if local.lastModified > remote.lastModified { - return local - } else if remote.lastModified > local.lastModified { - return remote - } else { - // Same timestamp - merge changes - return mergeData(local: local, remote: remote) - } - } - - private func mergeData(local: WidgetData, remote: WidgetData) -> WidgetData { - // Custom merge logic based on your data structure - var merged = local - - // Example: Combine arrays, prefer remote for conflicts - merged.items = Array(Set(local.items + remote.items)) - merged.settings = remote.settings // Prefer remote settings - - return merged - } -} -``` - -## Security & Privacy Issues - -### Issue: Keychain Access Errors - -**Symptoms:** -``` -Error: errSecItemNotFound (-25300) -``` - -**Solutions:** - -1. **Proper Keychain Configuration:** -```swift -class KeychainManager { - func saveToKeychain(data: Data, for key: String) throws { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrAccount as String: key, - kSecAttrAccessGroup as String: "group.com.yourapp.widgets", - kSecValueData as String: data, - kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock - ] - - // Delete existing item first - SecItemDelete(query as CFDictionary) - - // Add new item - let status = SecItemAdd(query as CFDictionary, nil) - - guard status == errSecSuccess else { - throw KeychainError.saveFailed(status) - } - } -} -``` - -2. **Handle Keychain Sharing:** -```xml - -keychain-access-groups - - $(AppIdentifierPrefix)com.yourapp.sharedkeychain - -``` - -### Issue: Privacy Compliance Violations - -**Symptoms:** -- App Store rejection for privacy issues -- GDPR compliance warnings - -**Solutions:** - -```swift -class PrivacyComplianceChecker { - func validateDataCollection() throws { - // Check for PII in logs - Logger.shared.setPIIFilter { message in - // Remove email addresses - let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - var filtered = message.replacingOccurrences( - of: emailRegex, - with: "[EMAIL]", - options: .regularExpression - ) - - // Remove phone numbers - let phoneRegex = "\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b" - filtered = filtered.replacingOccurrences( - of: phoneRegex, - with: "[PHONE]", - options: .regularExpression - ) - - return filtered - } - - // Ensure consent before tracking - guard PrivacyManager.shared.hasUserConsent() else { - throw PrivacyError.noConsent - } - } -} -``` - -## Memory & Resource Issues - -### Issue: Widget Extension Terminated - -**Symptoms:** -``` -Message from debugger: Terminated due to memory issue -``` - -**Solutions:** - -1. **Implement Memory Budget:** -```swift -class MemoryBudgetManager { - private let maxMemoryMB: Int = 30 // Widget extension limit - - func canAllocateMemory(bytes: Int) -> Bool { - let currentUsage = getCurrentMemoryUsageMB() - let requestedMB = bytes / (1024 * 1024) - - return (currentUsage + requestedMB) < maxMemoryMB - } - - func optimizeMemoryUsage() { - // Clear image cache - SDImageCache.shared.clearMemory() - - // Reduce data cache size - URLCache.shared.memoryCapacity = 5 * 1024 * 1024 // 5MB - - // Force cleanup - autoreleasepool { - // Temporary objects will be released - } - } -} -``` - -2. **Use Lazy Loading:** -```swift -struct LazyWidgetView: View { - @StateObject private var viewModel = WidgetViewModel() - - var body: some View { - Group { - if viewModel.isLoaded { - ContentView(data: viewModel.data) - } else { - PlaceholderView() - .onAppear { - Task { - await viewModel.loadDataIfNeeded() - } - } - } - } - } -} -``` - -## Debugging Techniques - -### Advanced Debugging Setup - -1. **Enable Verbose Logging:** -```swift -class DebugLogger { - static let shared = DebugLogger() - - private let subsystem = "com.app.widget" - - func configureLogging() { - #if DEBUG - // Enable all log levels in debug - Logger.logLevel = .debug - - // Log to file for analysis - let documentsPath = FileManager.default.urls( - for: .documentDirectory, - in: .userDomainMask - ).first! - - let logFile = documentsPath.appendingPathComponent("widget_debug.log") - freopen(logFile.path, "a+", stderr) - #endif - } - - func log(_ message: String, type: OSLogType = .debug) { - let logger = Logger(subsystem: subsystem, category: "Debug") - logger.log(level: type, "\(message)") - - #if DEBUG - print("🔍 [\(Date())] \(message)") - #endif - } -} -``` - -2. **Network Debugging:** -```swift -class NetworkDebugger: URLProtocol { - override class func canInit(with request: URLRequest) -> Bool { - #if DEBUG - print("📡 Request: \(request.httpMethod ?? "") \(request.url?.absoluteString ?? "")") - if let headers = request.allHTTPHeaderFields { - print(" Headers: \(headers)") - } - if let body = request.httpBody { - print(" Body: \(String(data: body, encoding: .utf8) ?? "Binary data")") - } - #endif - return false - } -} - -// Register in app -URLProtocol.registerClass(NetworkDebugger.self) -``` - -3. **Widget Preview Debugging:** -```swift -struct WidgetPreviews: PreviewProvider { - static var previews: some View { - Group { - WeatherWidgetView(entry: .mock()) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .previewDisplayName("Small") - - WeatherWidgetView(entry: .mockError()) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .previewDisplayName("Error State") - - WeatherWidgetView(entry: .mockLoading()) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - .previewDisplayName("Loading State") - } - .environment(\.colorScheme, .dark) - } -} -``` - -## Common Error Codes - -### Framework-Specific Error Codes - -| Error Code | Description | Solution | -|------------|-------------|----------| -| WDK-001 | Invalid configuration | Verify WidgetConfiguration parameters | -| WDK-002 | Template not found | Check template ID and registration | -| WDK-003 | Quota exceeded | Reduce number of active widgets | -| WDK-004 | Orchestration failed | Check widget dependencies | -| WDK-005 | Lifecycle error | Review widget state transitions | -| WDK-100 | Encryption failed | Verify encryption keys and Secure Enclave | -| WDK-101 | Authentication failed | Check biometric availability and permissions | -| WDK-102 | Privacy violation | Ensure GDPR/CCPA compliance | -| WDK-200 | Cache overflow | Implement cache eviction policy | -| WDK-201 | Performance threshold exceeded | Optimize rendering and data loading | -| WDK-300 | API timeout | Increase timeout or optimize request | -| WDK-301 | Cloud sync failed | Check network and cloud credentials | -| WDK-302 | Data source unavailable | Verify data source configuration | - -### System Error Codes - -| Error Code | Description | Solution | -|------------|-------------|----------| -| -25300 | Keychain item not found | Create keychain item before accessing | -| -25299 | Keychain duplicate item | Delete existing item before adding | -| -9806 | SSL connection failed | Check certificate and TLS configuration | -| -1009 | No internet connection | Implement offline mode | -| -1001 | Request timeout | Increase timeout or retry | - -## Platform-Specific Issues - -### iOS 16 Specific - -**Lock Screen Widgets:** -```swift -// Ensure minimal UI for lock screen -struct LockScreenWidget: View { - @Environment(\.isLuminanceReduced) var isLuminanceReduced - - var body: some View { - if isLuminanceReduced { - // Simplified UI for always-on display - MinimalWidgetView() - } else { - // Regular lock screen widget - StandardWidgetView() - } - } -} -``` - -### iOS 17 Specific - -**Interactive Widgets:** -```swift -struct InteractiveWidget: View { - var body: some View { - Button(intent: ToggleIntent()) { - Text("Toggle") - } - .buttonStyle(.plain) - } -} - -// App Intent -struct ToggleIntent: AppIntent { - static var title: LocalizedStringResource = "Toggle Widget" - - func perform() async throws -> some IntentResult { - // Handle interaction - WidgetCenter.shared.reloadAllTimelines() - return .result() - } -} -``` - -### iPad Specific - -**Size Class Handling:** -```swift -struct iPadWidget: View { - @Environment(\.horizontalSizeClass) var horizontalSizeClass - @Environment(\.verticalSizeClass) var verticalSizeClass - - var body: some View { - if horizontalSizeClass == .regular && verticalSizeClass == .regular { - // iPad layout - iPadOptimizedLayout() - } else { - // Compact layout - CompactLayout() - } - } -} -``` - -## Getting Help - -### Debug Information Collection - -When reporting issues, include: - -1. **System Information:** -```swift -func collectDebugInfo() -> String { - return """ - Device: \(UIDevice.current.model) - OS Version: \(UIDevice.current.systemVersion) - App Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown") - Framework Version: 3.2.0 - Memory Usage: \(getCurrentMemoryUsageMB())MB - Active Widgets: \(AdvancedWidgetManager.shared.widgets.count) - """ -} -``` - -2. **Crash Logs:** -- Xcode → Window → Devices and Simulators → View Device Logs - -3. **Sample Code:** -- Minimal reproducible example -- Configuration used -- Expected vs actual behavior - -### Support Resources - -- **GitHub Issues**: [Report bugs](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/issues) -- **Discussions**: [Ask questions](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/discussions) -- **Stack Overflow**: Tag with `ios-widget-development-kit` -- **Documentation**: [Full documentation](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/tree/main/Documentation) - -### Filing Bug Reports - -Use this template: - -```markdown -## Environment -- Framework Version: 3.2.0 -- iOS Version: 17.0 -- Xcode Version: 15.0 -- Device: iPhone 15 Pro - -## Description -Brief description of the issue - -## Steps to Reproduce -1. Step one -2. Step two -3. Step three - -## Expected Behavior -What should happen - -## Actual Behavior -What actually happens - -## Code Sample -```swift -// Minimal code to reproduce -``` - -## Logs -``` -// Relevant error messages or logs -``` -``` - -## Conclusion - -This troubleshooting guide covers the most common issues and their solutions. For issues not covered here, please: - -1. Check the latest documentation -2. Search existing GitHub issues -3. Post in discussions for community help -4. File a detailed bug report if needed - -Remember to always test on real devices, as simulator behavior may differ, especially for features like widgets, background refresh, and biometric authentication. \ No newline at end of file diff --git a/Documentation/UsageGuide.md b/Documentation/UsageGuide.md deleted file mode 100644 index 6f3b799..0000000 --- a/Documentation/UsageGuide.md +++ /dev/null @@ -1,258 +0,0 @@ -# Usage Guide - - -## Table of Contents -- [Usage Guide](#usage-guide) -- [Quick Start](#quick-start) - - [Creating Your First Widget](#creating-your-first-widget) - - [Customizing Widgets](#customizing-widgets) -- [Live Data Integration](#live-data-integration) - - [Setting Up Data Sources](#setting-up-data-sources) - - [Custom Data Sources](#custom-data-sources) -- [Analytics and Monitoring](#analytics-and-monitoring) - - [Performance Tracking](#performance-tracking) - - [Error Handling](#error-handling) -- [Advanced Features](#advanced-features) - - [Performance Optimization](#performance-optimization) - - [Widget Lifecycle Management](#widget-lifecycle-management) -- [Best Practices](#best-practices) - - [Memory Management](#memory-management) - - [Battery Optimization](#battery-optimization) - - [Error Handling](#error-handling) - - [Performance](#performance) -- [Examples](#examples) - - - -## Quick Start - -### Creating Your First Widget - -```swift -import WidgetKit -import WidgetTemplates - -// 1. Get a template -let weatherTemplate = WeatherWidgetTemplate() - -// 2. Get default configuration -let configuration = weatherTemplate.getDefaultConfiguration() - -// 3. Create the widget -let widget = WidgetEngine.shared.createWidget(with: configuration) -``` - -### Customizing Widgets - -```swift -// Create custom configuration -let customization = WidgetCustomization( - backgroundColor: .blue.opacity(0.1), - textColor: .primary, - accentColor: .blue, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16), - font: .title2 -) - -let widgetDefinition = WidgetDefinition( - id: "my_weather_widget", - type: .weather, - dataSourceIdentifier: "weather_api", - customization: customization -) - -let configuration = WidgetConfiguration( - id: "my_config", - templateIdentifier: "weather_widget", - widgets: [widgetDefinition], - settings: WidgetSettings( - refreshInterval: 300.0, - enableAnimations: true, - enableHaptics: true, - enableSound: false - ) -) -``` - -## Live Data Integration - -### Setting Up Data Sources - -```swift -import LiveDataIntegration - -// Register a data source -let weatherDataSource = WeatherDataSource() -LiveDataIntegration.shared.registerDataSource("weather_api", dataSource: weatherDataSource) - -// Connect to WebSocket for real-time updates -let url = URL(string: "ws://your-api.com/weather")! -LiveDataIntegration.shared.connectWebSocket(identifier: "weather_ws", url: url) - -// Get data publisher -let publisher = LiveDataIntegration.shared.getDataPublisher(for: "weather_api") -publisher?.sink { data in - print("Received weather data: \(data)") -} -.store(in: &cancellables) -``` - -### Custom Data Sources - -```swift -class CustomDataSource: WidgetDataSource { - let identifier = "custom_data" - private let dataSubject = PassthroughSubject() - - var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - func refresh() { - // Fetch data from your API - let data = WidgetData( - id: "custom_data", - type: .weather, - content: ["temperature": 22, "condition": "sunny"] - ) - dataSubject.send(data) - } - - func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} -``` - -## Analytics and Monitoring - -### Performance Tracking - -```swift -import WidgetAnalytics - -// Get analytics data -let analytics = WidgetEngine.shared.getAnalytics() -print("Total widget updates: \(analytics.totalWidgetUpdates)") -print("Memory usage: \(analytics.memoryUsage)") -print("Battery level: \(analytics.batteryLevel)") - -// Configure analytics -let config = AnalyticsConfiguration( - enableExternalAnalytics: true, - maxEventLogSize: 1000, - enablePerformanceTracking: true, - enableErrorTracking: true -) -WidgetAnalyticsService.shared.configure(config) -``` - -### Error Handling - -```swift -// Listen for errors -NotificationCenter.default.addObserver( - forName: .widgetErrorOccurred, - object: nil, - queue: .main -) { notification in - if let error = notification.userInfo?["error"] as? WidgetError { - print("Widget error: \(error.localizedDescription)") - } -} - -// Handle specific error types -switch error { -case .templateNotFound(let templateId): - print("Template not found: \(templateId)") -case .dataSourceNotFound(let dataSourceId): - print("Data source not found: \(dataSourceId)") -case .connectionFailed(let reason): - print("Connection failed: \(reason)") -default: - print("Unknown error") -} -``` - -## Advanced Features - -### Performance Optimization - -```swift -// Configure performance settings -let settings = WidgetPerformanceSettings( - maxMemoryUsage: 50 * 1024 * 1024, // 50MB - refreshInterval: 60.0, // 60 seconds - enableBatteryOptimization: true -) - -WidgetEngine.shared.configurePerformance(settings) - -// Monitor performance -let metrics = WidgetEngine.shared.performanceMetrics -print("Memory usage: \(metrics.memoryUsage)") -print("Battery level: \(metrics.batteryLevel)") -print("Refresh count: \(metrics.refreshCount)") -``` - -### Widget Lifecycle Management - -```swift -// Register templates -let templates = [ - WeatherWidgetTemplate(), - CalendarWidgetTemplate(), - FitnessWidgetTemplate() -] - -templates.forEach { template in - WidgetEngine.shared.registerTemplate(template) -} - -// Update widgets -let data = WidgetData( - id: "widget_1", - type: .weather, - content: ["temperature": 25] -) - -WidgetEngine.shared.updateWidget("widget_1", with: data) -``` - -## Best Practices - -### Memory Management - -- Use weak references in closures -- Properly dispose of Combine cancellables -- Monitor memory usage with analytics - -### Battery Optimization - -- Enable battery optimization in settings -- Use appropriate refresh intervals -- Minimize network requests - -### Error Handling - -- Always handle potential errors -- Provide fallback data -- Log errors for debugging - -### Performance - -- Use lazy loading for large datasets -- Implement proper caching -- Monitor performance metrics - -## Examples - -See the [Examples](Examples/) directory for complete working examples of: - -- Basic widget creation -- Custom data sources -- Live data integration -- Analytics implementation -- Error handling -- Performance optimization \ No newline at end of file diff --git a/Documentation/WidgetDevelopmentManagerAPI.md b/Documentation/WidgetDevelopmentManagerAPI.md deleted file mode 100644 index 2b87767..0000000 --- a/Documentation/WidgetDevelopmentManagerAPI.md +++ /dev/null @@ -1,332 +0,0 @@ -# Widget Development Manager API - - -## Table of Contents -- [Widget Development Manager API](#widget-development-manager-api) -- [Overview](#overview) -- [Core Features](#core-features) - - [Widget Management](#widget-management) - - [Multi-Widget Support](#multi-widget-support) - - [Advanced Features](#advanced-features) -- [API Reference](#api-reference) - - [WidgetDevelopmentManager](#widgetdevelopmentmanager) - - [WidgetConfiguration](#widgetconfiguration) - - [Widget](#widget) - - [WidgetContent](#widgetcontent) - - [WidgetLifecycle](#widgetlifecycle) -- [Usage Examples](#usage-examples) - - [Basic Widget Manager Setup](#basic-widget-manager-setup) - - [Widget Registration](#widget-registration) - - [Widget Data Updates](#widget-data-updates) - - [Widget Analytics](#widget-analytics) -- [Error Handling](#error-handling) -- [Best Practices](#best-practices) - - [Performance Optimization](#performance-optimization) - - [User Experience](#user-experience) - - [Data Management](#data-management) -- [Related Documentation](#related-documentation) - - - -## Overview - -The `WidgetDevelopmentManager` is the core component of the iOS Widget Development Kit that provides comprehensive widget management capabilities. This API enables developers to create, configure, and manage various types of widgets with advanced features and customization options. - -## Core Features - -### Widget Management -- **Widget Creation**: Create and register new widgets -- **Widget Configuration**: Configure widget settings and properties -- **Widget Lifecycle**: Manage widget lifecycle events -- **Widget Updates**: Handle widget data updates and refreshes - -### Multi-Widget Support -- **Home Screen Widgets**: Full-featured home screen widget support -- **Lock Screen Widgets**: Lock screen widget integration -- **StandBy Widgets**: StandBy mode widget support -- **Live Activities**: Real-time Live Activity management -- **Dynamic Island**: Dynamic Island integration - -### Advanced Features -- **Data Integration**: Seamless data source integration -- **Real-Time Updates**: Real-time widget updates -- **Background Sync**: Background data synchronization -- **Analytics**: Widget usage analytics and monitoring -- **Performance**: Optimized performance and battery efficiency - -## API Reference - -### WidgetDevelopmentManager - -The main manager class for widget development operations. - -```swift -class WidgetDevelopmentManager { - // MARK: - Initialization - - /// Initialize the widget development manager - init() - - /// Initialize with custom configuration - init(configuration: WidgetConfiguration) - - // MARK: - Widget Management - - /// Start the widget manager - func start(with configuration: WidgetConfiguration) async throws - - /// Stop the widget manager - func stop() async throws - - /// Register a new widget - func register(_ widget: Widget) async throws -> WidgetID - - /// Unregister a widget - func unregister(_ widgetID: WidgetID) async throws - - /// Update widget configuration - func updateConfiguration(_ configuration: WidgetConfiguration, for widgetID: WidgetID) async throws - - // MARK: - Data Integration - - /// Configure data integration - func configureDataIntegration(_ configuration: DataIntegrationConfiguration) async throws - - /// Fetch widget data - func fetchData(for widgetID: WidgetID) async throws -> WidgetData - - /// Update widget with new data - func updateWidget(_ widgetID: WidgetID, with data: WidgetData) async throws - - // MARK: - Analytics - - /// Get widget analytics - func getAnalytics(for widgetID: WidgetID) async throws -> WidgetAnalytics - - /// Get performance metrics - func getPerformanceMetrics(for widgetID: WidgetID) async throws -> PerformanceMetrics -} -``` - -### WidgetConfiguration - -Configuration class for widget settings and properties. - -```swift -struct WidgetConfiguration { - // MARK: - Widget Types - - /// Enable home screen widgets - var enableHomeScreenWidgets: Bool = true - - /// Enable lock screen widgets - var enableLockScreenWidgets: Bool = true - - /// Enable Live Activities - var enableLiveActivities: Bool = true - - /// Enable Dynamic Island - var enableDynamicIsland: Bool = true - - // MARK: - Widget Settings - - /// Default refresh interval in seconds - var defaultRefreshInterval: TimeInterval = 300 - - /// Enable background updates - var enableBackgroundUpdates: Bool = true - - /// Enable widget interactions - var enableInteractions: Bool = true - - /// Enable deep linking - var enableDeepLinking: Bool = true - - // MARK: - Styling Settings - - /// Enable dynamic colors - var enableDynamicColors: Bool = true - - /// Enable dark mode support - var enableDarkMode: Bool = true - - /// Enable custom fonts - var enableCustomFonts: Bool = true - - /// Enable animations - var enableAnimations: Bool = true -} -``` - -### Widget - -Base protocol for all widget types. - -```swift -protocol Widget { - /// Unique widget identifier - var id: WidgetID { get } - - /// Widget kind identifier - var kind: String { get } - - /// Widget configuration - var configuration: WidgetConfiguration { get } - - /// Widget content view - var content: WidgetContent { get } - - /// Widget lifecycle events - var lifecycle: WidgetLifecycle { get } -} -``` - -### WidgetContent - -Protocol for widget content views. - -```swift -protocol WidgetContent { - /// Create widget view - func createView(for context: WidgetContext) -> WidgetView - - /// Update widget view - func updateView(_ view: WidgetView, with context: WidgetContext) async throws - - /// Handle widget interactions - func handleInteraction(_ interaction: WidgetInteraction) async throws -} -``` - -### WidgetLifecycle - -Protocol for widget lifecycle management. - -```swift -protocol WidgetLifecycle { - /// Widget will appear - func willAppear() async throws - - /// Widget did appear - func didAppear() async throws - - /// Widget will disappear - func willDisappear() async throws - - /// Widget did disappear - func didDisappear() async throws - - /// Widget will update - func willUpdate() async throws - - /// Widget did update - func didUpdate() async throws -} -``` - -## Usage Examples - -### Basic Widget Manager Setup - -```swift -import WidgetDevelopmentKit - -// Initialize widget manager -let widgetManager = WidgetDevelopmentManager() - -// Configure widget settings -let widgetConfig = WidgetConfiguration() -widgetConfig.enableHomeScreenWidgets = true -widgetConfig.enableLockScreenWidgets = true -widgetConfig.enableLiveActivities = true -widgetConfig.enableDynamicIsland = true - -// Start widget manager -try await widgetManager.start(with: widgetConfig) - -// Configure data integration -try await widgetManager.configureDataIntegration(DataIntegrationConfiguration()) -``` - -### Widget Registration - -```swift -// Create widget -let widget = HomeScreenWidget( - kind: "com.company.app.widget", - configuration: widgetConfig -) - -// Register widget -let widgetID = try await widgetManager.register(widget) -print("✅ Widget registered with ID: \(widgetID)") -``` - -### Widget Data Updates - -```swift -// Fetch widget data -let data = try await widgetManager.fetchData(for: widgetID) - -// Update widget with new data -try await widgetManager.updateWidget(widgetID, with: data) -``` - -### Widget Analytics - -```swift -// Get widget analytics -let analytics = try await widgetManager.getAnalytics(for: widgetID) -print("Widget views: \(analytics.viewCount)") -print("Widget interactions: \(analytics.interactionCount)") - -// Get performance metrics -let metrics = try await widgetManager.getPerformanceMetrics(for: widgetID) -print("Average load time: \(metrics.averageLoadTime)s") -print("Memory usage: \(metrics.memoryUsage)MB") -``` - -## Error Handling - -The Widget Development Manager uses comprehensive error handling for all operations: - -```swift -enum WidgetDevelopmentError: Error { - case initializationFailed(String) - case widgetRegistrationFailed(String) - case widgetUpdateFailed(String) - case dataFetchFailed(String) - case configurationInvalid(String) - case widgetNotFound(WidgetID) - case operationNotSupported(String) -} -``` - -## Best Practices - -### Performance Optimization -- Use appropriate refresh intervals -- Implement efficient data fetching -- Minimize widget memory usage -- Optimize widget rendering - -### User Experience -- Provide meaningful widget content -- Handle loading and error states -- Support accessibility features -- Implement smooth animations - -### Data Management -- Cache frequently used data -- Implement offline support -- Handle data validation -- Provide fallback content - -## Related Documentation - -- [Home Screen Widget API](HomeScreenWidgetAPI.md) -- [Lock Screen Widget API](LockScreenWidgetAPI.md) -- [Live Activity API](LiveActivityAPI.md) -- [Dynamic Island API](DynamicIslandAPI.md) -- [Data Integration API](DataIntegrationAPI.md) -- [Configuration API](ConfigurationAPI.md) diff --git a/Examples/AdvancedExample.swift b/Examples/AdvancedExample.swift deleted file mode 100644 index 348c7aa..0000000 --- a/Examples/AdvancedExample.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import iOS-Widget-Development-Kit - -/// Advanced example showing complex usage patterns -struct AdvancedExample { - let framework: iOS-Widget-Development-Kit - - init() { - self.framework = iOS-Widget-Development-Kit() - } - - func run() { - print("🚀 iOS-Widget-Development-Kit Advanced Example") - - // Advanced configuration - configureAdvanced() - - // Run advanced features - runAdvancedFeatures() - - // Performance testing - runPerformanceTests() - } - - private func configureAdvanced() { - print("\n⚙️ Configuring advanced settings...") - - // Add advanced configuration here - print("✅ Advanced configuration complete") - } - - private func runAdvancedFeatures() { - print("\n🎯 Running advanced features...") - - // Add advanced feature demonstrations here - print("✅ Advanced features completed") - } - - private func runPerformanceTests() { - print("\n📊 Running performance tests...") - - // Add performance testing code here - print("✅ Performance tests completed") - } -} diff --git a/Examples/AdvancedExamples/AdvancedDemo.swift b/Examples/AdvancedExamples/AdvancedDemo.swift deleted file mode 100644 index ac53b13..0000000 --- a/Examples/AdvancedExamples/AdvancedDemo.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -@available(iOS 15.0, *) -struct AdvancedDemo: View { - @State private var isActive: Bool = false - - var body: some View { - VStack(spacing: 16) { - Text("Advanced Example") - .font(.title) - Toggle("Enable Feature", isOn: $isActive) - RoundedRectangle(cornerRadius: 16) - .fill(isActive ? .green : .gray) - .frame(width: 240, height: 140) - .overlay(Text(isActive ? "ENABLED" : "DISABLED").foregroundColor(.white)) - .animation(.easeInOut, value: isActive) - } - .padding(24) - } -} diff --git a/Examples/AdvancedExamples/AdvancedWidgetExample.swift b/Examples/AdvancedExamples/AdvancedWidgetExample.swift deleted file mode 100644 index 053305c..0000000 --- a/Examples/AdvancedExamples/AdvancedWidgetExample.swift +++ /dev/null @@ -1,365 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates -import LiveDataIntegration -import WidgetAnalytics - -@available(iOS 16.0, *) -struct AdvancedWidgetExample: View { - @StateObject private var widgetEngine = WidgetEngine.shared - @State private var selectedWidgets: [WidgetConfiguration] = [] - @State private var isLiveDataEnabled = false - @State private var analyticsData: WidgetAnalytics? - - var body: some View { - NavigationView { - ScrollView { - VStack(spacing: 20) { - // Live Data Integration Section - LiveDataSection(isEnabled: $isLiveDataEnabled) - - // Widget Gallery - WidgetGallerySection(selectedWidgets: $selectedWidgets) - - // Analytics Dashboard - AnalyticsSection(analyticsData: $analyticsData) - - // Performance Monitoring - PerformanceSection() - } - .padding() - } - .navigationTitle("Advanced Widget Builder") - .onAppear { - setupLiveDataIntegration() - loadAnalytics() - } - } - } - - private func setupLiveDataIntegration() { - // Register data sources - let weatherDataSource = WeatherDataSource() - let calendarDataSource = CalendarDataSource() - let fitnessDataSource = FitnessDataSource() - - LiveDataIntegration.shared.registerDataSource("weather_api", dataSource: weatherDataSource) - LiveDataIntegration.shared.registerDataSource("calendar_events", dataSource: calendarDataSource) - LiveDataIntegration.shared.registerDataSource("health_kit", dataSource: fitnessDataSource) - - // Configure data source settings - let settings = DataSourceSettings( - refreshInterval: 30.0, - enableCaching: true, - maxRetries: 3, - timeout: 10.0 - ) - - LiveDataIntegration.shared.configureDataSource("weather_api", settings: settings) - LiveDataIntegration.shared.configureDataSource("calendar_events", settings: settings) - LiveDataIntegration.shared.configureDataSource("health_kit", settings: settings) - } - - private func loadAnalytics() { - analyticsData = widgetEngine.getAnalytics() - } -} - -@available(iOS 16.0, *) -struct LiveDataSection: View { - @Binding var isEnabled: Bool - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Live Data Integration") - .font(.headline) - - Toggle("Enable Real-time Updates", isOn: $isEnabled) - .onChange(of: isEnabled) { newValue in - if newValue { - enableLiveData() - } else { - disableLiveData() - } - } - - if isEnabled { - LiveDataStatusView() - } - } - .padding() - .background(Color.blue.opacity(0.1)) - .cornerRadius(12) - } - - private func enableLiveData() { - // Connect to WebSocket for real-time updates - let weatherURL = URL(string: "ws://api.weatherapi.com/v1/current.json")! - LiveDataIntegration.shared.connectWebSocket(identifier: "weather_ws", url: weatherURL) - - let calendarURL = URL(string: "ws://api.calendar.com/events")! - LiveDataIntegration.shared.connectWebSocket(identifier: "calendar_ws", url: calendarURL) - } - - private func disableLiveData() { - LiveDataIntegration.shared.disconnectWebSocket(identifier: "weather_ws") - LiveDataIntegration.shared.disconnectWebSocket(identifier: "calendar_ws") - } -} - -@available(iOS 16.0, *) -struct LiveDataStatusView: View { - @State private var weatherStatus: ConnectionStatus = .connecting - @State private var calendarStatus: ConnectionStatus = .connecting - - var body: some View { - VStack(spacing: 10) { - HStack { - Text("Weather API") - Spacer() - ConnectionStatusIndicator(status: weatherStatus) - } - - HStack { - Text("Calendar API") - Spacer() - ConnectionStatusIndicator(status: calendarStatus) - } - } - .onAppear { - updateConnectionStatus() - } - } - - private func updateConnectionStatus() { - weatherStatus = LiveDataIntegration.shared.getConnectionStatus(for: "weather_ws") - calendarStatus = LiveDataIntegration.shared.getConnectionStatus(for: "calendar_ws") - } -} - -@available(iOS 16.0, *) -struct ConnectionStatusIndicator: View { - let status: ConnectionStatus - - var body: some View { - HStack(spacing: 4) { - Circle() - .fill(statusColor) - .frame(width: 8, height: 8) - - Text(statusText) - .font(.caption) - .foregroundColor(.secondary) - } - } - - private var statusColor: Color { - switch status { - case .connected: return .green - case .connecting: return .orange - case .disconnected: return .red - case .unknown: return .gray - } - } - - private var statusText: String { - switch status { - case .connected: return "Connected" - case .connecting: return "Connecting" - case .disconnected: return "Disconnected" - case .unknown: return "Unknown" - } - } -} - -@available(iOS 16.0, *) -struct WidgetGallerySection: View { - @Binding var selectedWidgets: [WidgetConfiguration] - - let availableTemplates: [WidgetTemplate] = [ - WeatherWidgetTemplate(), - CalendarWidgetTemplate(), - FitnessWidgetTemplate(), - NewsWidgetTemplate(), - SocialMediaWidgetTemplate(), - ProductivityWidgetTemplate(), - EntertainmentWidgetTemplate(), - FinanceWidgetTemplate(), - HealthWidgetTemplate(), - TravelWidgetTemplate() - ] - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Widget Gallery") - .font(.headline) - - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 15) { - ForEach(availableTemplates, id: \.identifier) { template in - WidgetTemplateCard(template: template) { - addWidget(template: template) - } - } - } - } - .padding() - .background(Color.green.opacity(0.1)) - .cornerRadius(12) - } - - private func addWidget(template: WidgetTemplate) { - let configuration = template.getDefaultConfiguration() - selectedWidgets.append(configuration) - } -} - -@available(iOS 16.0, *) -struct WidgetTemplateCard: View { - let template: WidgetTemplate - let onTap: () -> Void - - var body: some View { - VStack(spacing: 8) { - Image(systemName: iconName) - .font(.title2) - .foregroundColor(.blue) - - Text(template.displayName) - .font(.caption) - .multilineTextAlignment(.center) - .lineLimit(2) - - Text(template.description) - .font(.caption2) - .foregroundColor(.secondary) - .lineLimit(3) - } - .frame(height: 100) - .padding() - .background(Color.white) - .cornerRadius(8) - .shadow(radius: 2) - .onTapGesture { - onTap() - } - } - - private var iconName: String { - switch template.identifier { - case "weather_widget": return "cloud.sun.fill" - case "calendar_widget": return "calendar" - case "fitness_widget": return "figure.walk" - case "news_widget": return "newspaper" - case "social_media_widget": return "bubble.left.and.bubble.right" - case "productivity_widget": return "chart.line.uptrend.xyaxis" - case "entertainment_widget": return "play.circle" - case "finance_widget": return "dollarsign.circle" - case "health_widget": return "heart.fill" - case "travel_widget": return "airplane" - default: return "square.fill" - } - } -} - -@available(iOS 16.0, *) -struct AnalyticsSection: View { - @Binding var analyticsData: WidgetAnalytics? - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Analytics Dashboard") - .font(.headline) - - if let analytics = analyticsData { - AnalyticsGridView(analytics: analytics) - } else { - ProgressView("Loading analytics...") - } - } - .padding() - .background(Color.purple.opacity(0.1)) - .cornerRadius(12) - } -} - -@available(iOS 16.0, *) -struct AnalyticsGridView: View { - let analytics: WidgetAnalytics - - var body: some View { - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 10) { - AnalyticsCard(title: "Total Updates", value: "\(analytics.totalWidgetUpdates)") - AnalyticsCard(title: "Memory Usage", value: "\(analytics.memoryUsage / 1024 / 1024) MB") - AnalyticsCard(title: "Connections", value: "\(analytics.connectionsEstablished)") - AnalyticsCard(title: "Errors", value: "\(analytics.errors)") - } - } -} - -@available(iOS 16.0, *) -struct AnalyticsCard: View { - let title: String - let value: String - - var body: some View { - VStack(spacing: 4) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - - Text(value) - .font(.caption) - .fontWeight(.bold) - } - .frame(maxWidth: .infinity) - .padding(8) - .background(Color.white) - .cornerRadius(6) - } -} - -@available(iOS 16.0, *) -struct PerformanceSection: View { - @StateObject private var performanceMetrics = WidgetEngine.shared.performanceMetrics - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Performance Monitoring") - .font(.headline) - - VStack(spacing: 10) { - PerformanceMetricRow(title: "Memory Usage", value: "\(performanceMetrics.memoryUsage / 1024 / 1024) MB") - PerformanceMetricRow(title: "Battery Level", value: "\(Int(performanceMetrics.batteryLevel * 100))%") - PerformanceMetricRow(title: "Refresh Count", value: "\(performanceMetrics.refreshCount)") - PerformanceMetricRow(title: "Error Count", value: "\(performanceMetrics.errorCount)") - } - } - .padding() - .background(Color.orange.opacity(0.1)) - .cornerRadius(12) - } -} - -@available(iOS 16.0, *) -struct PerformanceMetricRow: View { - let title: String - let value: String - - var body: some View { - HStack { - Text(title) - .font(.caption) - Spacer() - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -struct AdvancedWidgetExample_Previews: PreviewProvider { - static var previews: some View { - AdvancedWidgetExample() - } -} \ No newline at end of file diff --git a/Examples/AdvancedExamples/README.md b/Examples/AdvancedExamples/README.md deleted file mode 100644 index fe6ea18..0000000 --- a/Examples/AdvancedExamples/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Advanced Examples — iOS-Widget-Development-Kit - -- Feature toggling demo with SwiftUI animations -- Replace with repository-specific examples when integrating with package modules - -Run in a host app target or SwiftUI preview. diff --git a/Examples/BasicExample.swift b/Examples/BasicExample.swift deleted file mode 100644 index bcb4924..0000000 --- a/Examples/BasicExample.swift +++ /dev/null @@ -1,87 +0,0 @@ -import Foundation -import UIKit - -/** - * Basic Example - Getting Started - * - * This example demonstrates the basic usage of our framework. - * Perfect for beginners and quick prototyping. - */ - -class BasicExampleViewController: UIViewController { - - // MARK: - Properties - private let titleLabel = UILabel() - private let descriptionLabel = UILabel() - private let actionButton = UIButton(type: .system) - - // MARK: - Lifecycle - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - configureLayout() - setupActions() - } - - // MARK: - UI Setup - private func setupUI() { - view.backgroundColor = .systemBackground - - // Title Label - titleLabel.text = "Basic Example" - titleLabel.font = .systemFont(ofSize: 28, weight: .bold) - titleLabel.textAlignment = .center - titleLabel.textColor = .label - - // Description Label - descriptionLabel.text = "This is a basic example showing fundamental usage patterns." - descriptionLabel.font = .systemFont(ofSize: 16) - descriptionLabel.textAlignment = .center - descriptionLabel.textColor = .secondaryLabel - descriptionLabel.numberOfLines = 0 - - // Action Button - actionButton.setTitle("Start Example", for: .normal) - actionButton.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) - actionButton.backgroundColor = .systemBlue - actionButton.setTitleColor(.white, for: .normal) - actionButton.layer.cornerRadius = 12 - } - - // MARK: - Layout - private func configureLayout() { - [titleLabel, descriptionLabel, actionButton].forEach { - $0.translatesAutoresizingMaskIntoConstraints = false - view.addSubview($0) - } - - NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 40), - titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - - descriptionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20), - descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), - descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), - - actionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - actionButton.topAnchor.constraint(equalTo: descriptionLabel.bottomAnchor, constant: 40), - actionButton.widthAnchor.constraint(equalToConstant: 200), - actionButton.heightAnchor.constraint(equalToConstant: 50) - ]) - } - - // MARK: - Actions - private func setupActions() { - actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) - } - - @objc private func actionButtonTapped() { - // Example action implementation - let alert = UIAlertController(title: "Example Started", - message: "Basic example is now running!", - preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - present(alert, animated: true) - } -} diff --git a/Examples/BasicExamples/BasicWidgetExample.swift b/Examples/BasicExamples/BasicWidgetExample.swift deleted file mode 100644 index 11b4092..0000000 --- a/Examples/BasicExamples/BasicWidgetExample.swift +++ /dev/null @@ -1,211 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates - -@available(iOS 16.0, *) -struct BasicWidgetExample: View { - @StateObject private var widgetEngine = WidgetEngine.shared - @State private var selectedTemplate: WidgetTemplate? - @State private var widgetConfiguration: WidgetConfiguration? - - var body: some View { - NavigationView { - VStack(spacing: 20) { - // Template Selection - VStack(alignment: .leading, spacing: 10) { - Text("Select Widget Template") - .font(.headline) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 15) { - TemplateCard( - template: WeatherWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "weather_widget" - ) { - selectedTemplate = WeatherWidgetTemplate() - } - - TemplateCard( - template: CalendarWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "calendar_widget" - ) { - selectedTemplate = CalendarWidgetTemplate() - } - - TemplateCard( - template: FitnessWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "fitness_widget" - ) { - selectedTemplate = FitnessWidgetTemplate() - } - - TemplateCard( - template: NewsWidgetTemplate(), - isSelected: selectedTemplate?.identifier == "news_widget" - ) { - selectedTemplate = NewsWidgetTemplate() - } - } - .padding(.horizontal) - } - } - - // Widget Preview - if let configuration = widgetConfiguration { - VStack(alignment: .leading, spacing: 10) { - Text("Widget Preview") - .font(.headline) - - widgetEngine.createWidget(with: configuration) - .frame(height: 200) - .background(Color.gray.opacity(0.1)) - .cornerRadius(12) - } - } - - // Customization Options - if let template = selectedTemplate { - CustomizationPanel(template: template) { configuration in - self.widgetConfiguration = configuration - } - } - - Spacer() - } - .padding() - .navigationTitle("Widget Builder") - } - } -} - -@available(iOS 16.0, *) -struct TemplateCard: View { - let template: WidgetTemplate - let isSelected: Bool - let onTap: () -> Void - - var body: some View { - VStack(spacing: 8) { - Image(systemName: iconName) - .font(.title) - .foregroundColor(isSelected ? .white : .blue) - - Text(template.displayName) - .font(.caption) - .multilineTextAlignment(.center) - } - .frame(width: 80, height: 80) - .background(isSelected ? Color.blue : Color.gray.opacity(0.1)) - .cornerRadius(12) - .onTapGesture { - onTap() - } - } - - private var iconName: String { - switch template.identifier { - case "weather_widget": return "cloud.sun.fill" - case "calendar_widget": return "calendar" - case "fitness_widget": return "figure.walk" - case "news_widget": return "newspaper" - default: return "square.fill" - } - } -} - -@available(iOS 16.0, *) -struct CustomizationPanel: View { - let template: WidgetTemplate - let onConfigurationChange: (WidgetConfiguration) -> Void - - @State private var backgroundColor = Color.blue.opacity(0.1) - @State private var textColor = Color.primary - @State private var accentColor = Color.blue - @State private var cornerRadius: CGFloat = 12 - @State private var refreshInterval: Double = 30.0 - - var body: some View { - VStack(alignment: .leading, spacing: 15) { - Text("Customization") - .font(.headline) - - VStack(spacing: 10) { - ColorPicker("Background Color", selection: $backgroundColor) - ColorPicker("Text Color", selection: $textColor) - ColorPicker("Accent Color", selection: $accentColor) - - HStack { - Text("Corner Radius") - Slider(value: $cornerRadius, in: 0...20) - Text("\(Int(cornerRadius))") - } - - HStack { - Text("Refresh Interval") - Slider(value: $refreshInterval, in: 10...300) - Text("\(Int(refreshInterval))s") - } - } - - Button("Create Widget") { - createWidget() - } - .buttonStyle(.borderedProminent) - } - .padding() - .background(Color.gray.opacity(0.05)) - .cornerRadius(12) - } - - private func createWidget() { - let customization = WidgetCustomization( - backgroundColor: backgroundColor, - textColor: textColor, - accentColor: accentColor, - cornerRadius: cornerRadius - ) - - let widgetDefinition = WidgetDefinition( - id: "custom_widget", - type: widgetType, - dataSourceIdentifier: dataSourceIdentifier, - customization: customization - ) - - let configuration = WidgetConfiguration( - id: "custom_config", - templateIdentifier: template.identifier, - widgets: [widgetDefinition], - settings: WidgetSettings(refreshInterval: refreshInterval) - ) - - onConfigurationChange(configuration) - } - - private var widgetType: WidgetType { - switch template.identifier { - case "weather_widget": return .weather - case "calendar_widget": return .calendar - case "fitness_widget": return .fitness - case "news_widget": return .news - default: return .weather - } - } - - private var dataSourceIdentifier: String { - switch template.identifier { - case "weather_widget": return "weather_api" - case "calendar_widget": return "calendar_events" - case "fitness_widget": return "health_kit" - case "news_widget": return "news_api" - default: return "default_api" - } - } -} - -@available(iOS 16.0, *) -struct BasicWidgetExample_Previews: PreviewProvider { - static var previews: some View { - BasicWidgetExample() - } -} \ No newline at end of file diff --git a/Examples/DynamicIslandExamples/DynamicIslandExample.swift b/Examples/DynamicIslandExamples/DynamicIslandExample.swift deleted file mode 100644 index cae9dc3..0000000 --- a/Examples/DynamicIslandExamples/DynamicIslandExample.swift +++ /dev/null @@ -1,195 +0,0 @@ -import SwiftUI -import ActivityKit -import WidgetKit - -// MARK: - Dynamic Island Example -struct DynamicIslandExample: Widget { - let kind: String = "com.company.app.dynamicisland" - - var body: some WidgetConfiguration { - ActivityConfiguration(for: DynamicIslandAttributes.self) { context in - // This is the Live Activity view - DynamicIslandLiveActivityView(context: context) - } dynamicIsland: { context in - DynamicIsland { - // Expanded UI - DynamicIslandExpandedRegion(.leading) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - Text("Now Playing") - .font(.caption) - } - } - - DynamicIslandExpandedRegion(.trailing) { - Text("2:30") - .font(.caption) - .foregroundColor(.secondary) - } - - DynamicIslandExpandedRegion(.center) { - VStack(spacing: 8) { - Text("Song Title") - .font(.headline) - - Text("Artist Name") - .font(.caption) - .foregroundColor(.secondary) - } - } - - DynamicIslandExpandedRegion(.bottom) { - HStack(spacing: 20) { - Button("Previous") { - // Previous track - } - .buttonStyle(.bordered) - - Button("Play/Pause") { - // Play/Pause - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track - } - .buttonStyle(.bordered) - } - } - } compactLeading: { - // Compact leading - Image(systemName: "music.note") - .foregroundColor(.blue) - } compactTrailing: { - // Compact trailing - Text("2:30") - .font(.caption2) - .foregroundColor(.secondary) - } minimal: { - // Minimal - Image(systemName: "music.note") - .foregroundColor(.blue) - } - } - } -} - -// MARK: - Dynamic Island Attributes -struct DynamicIslandAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var songTitle: String - var artistName: String - var isPlaying: Bool - var currentTime: String - var duration: String - var progress: Double - } - - var playlistName: String -} - -// MARK: - Live Activity View -struct DynamicIslandLiveActivityView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 16) { - HStack { - Image(systemName: "music.note") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text(context.state.songTitle) - .font(.headline) - - Text(context.state.artistName) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text("\(context.state.currentTime) / \(context.state.duration)") - .font(.caption2) - .foregroundColor(.secondary) - } - - ProgressView(value: context.state.progress) - .progressViewStyle(.linear) - .tint(.blue) - - HStack(spacing: 20) { - Button("Previous") { - // Previous track - } - .buttonStyle(.bordered) - - Button(context.state.isPlaying ? "Pause" : "Play") { - // Play/Pause - } - .buttonStyle(.bordered) - - Button("Next") { - // Next track - } - .buttonStyle(.bordered) - } - } - .padding() - .background(Color(.systemBackground)) - } -} - -// MARK: - Dynamic Island Manager -class DynamicIslandManager { - static let shared = DynamicIslandManager() - - private var activity: Activity? - - func startDynamicIsland(playlistName: String, songTitle: String, artistName: String) { - let attributes = DynamicIslandAttributes(playlistName: playlistName) - - let contentState = DynamicIslandAttributes.ContentState( - songTitle: songTitle, - artistName: artistName, - isPlaying: true, - currentTime: "1:30", - duration: "3:45", - progress: 0.4 - ) - - do { - activity = try Activity.request( - attributes: attributes, - contentState: contentState, - pushType: nil - ) - print("✅ Dynamic Island activity started") - } catch { - print("❌ Dynamic Island activity failed: \(error)") - } - } - - func updateDynamicIsland(songTitle: String, artistName: String, isPlaying: Bool, currentTime: String, duration: String, progress: Double) { - Task { - let contentState = DynamicIslandAttributes.ContentState( - songTitle: songTitle, - artistName: artistName, - isPlaying: isPlaying, - currentTime: currentTime, - duration: duration, - progress: progress - ) - - await activity?.update(using: contentState) - } - } - - func endDynamicIsland() { - Task { - await activity?.end(dismissalPolicy: .immediate) - } - } -} \ No newline at end of file diff --git a/Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift b/Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift deleted file mode 100644 index 6fb0f2c..0000000 --- a/Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift +++ /dev/null @@ -1,89 +0,0 @@ -import SwiftUI -import WidgetKit - -// MARK: - Home Screen Widget Example -struct HomeScreenWidgetExample: Widget { - let kind: String = "com.company.app.homescreenwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: HomeScreenWidgetProvider()) { entry in - HomeScreenWidgetEntryView(entry: entry) - } - .configurationDisplayName("Home Screen Widget") - .description("A beautiful home screen widget example") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} - -// MARK: - Widget Provider -struct HomeScreenWidgetProvider: TimelineProvider { - func placeholder(in context: Context) -> HomeScreenWidgetEntry { - HomeScreenWidgetEntry(date: Date(), title: "Welcome", message: "Tap to open app") - } - - func getSnapshot(in context: Context, completion: @escaping (HomeScreenWidgetEntry) -> ()) { - let entry = HomeScreenWidgetEntry(date: Date(), title: "Welcome", message: "Tap to open app") - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - let currentDate = Date() - let entry = HomeScreenWidgetEntry(date: currentDate, title: "Welcome", message: "Tap to open app") - let timeline = Timeline(entries: [entry], policy: .atEnd) - completion(timeline) - } -} - -// MARK: - Widget Entry -struct HomeScreenWidgetEntry: TimelineEntry { - let date: Date - let title: String - let message: String -} - -// MARK: - Widget Entry View -struct HomeScreenWidgetEntryView: View { - let entry: HomeScreenWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - VStack(spacing: 8) { - HStack { - Image(systemName: "house.fill") - .foregroundColor(.blue) - .font(.title2) - - Spacer() - - Text(entry.title) - .font(.headline) - .foregroundColor(.primary) - } - - Spacer() - - Text(entry.message) - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - - Spacer() - - Button("Open App") { - // Deep link to app - } - .buttonStyle(.bordered) - .font(.caption) - } - .padding() - .background(Color(.systemBackground)) - } -} - -// MARK: - Preview -struct HomeScreenWidgetExample_Previews: PreviewProvider { - static var previews: some View { - HomeScreenWidgetEntryView(entry: HomeScreenWidgetEntry(date: Date(), title: "Welcome", message: "Tap to open app")) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - } -} \ No newline at end of file diff --git a/Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift b/Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift deleted file mode 100644 index 0d13bc6..0000000 --- a/Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift +++ /dev/null @@ -1,357 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates - -// MARK: - Weather Widget Example -// This example demonstrates how to create a beautiful weather widget -// with real-time data integration and dynamic theming - -struct WeatherWidgetExample: Widget { - static let kind: String = "com.company.app.weatherwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: WeatherTimelineProvider()) { entry in - WeatherWidgetView(entry: entry) - } - .configurationDisplayName("Weather Widget") - .description("Displays current weather conditions with beautiful animations") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} - -// MARK: - Weather Timeline Provider -struct WeatherTimelineProvider: TimelineProvider { - typealias Entry = WeatherWidgetEntry - - func placeholder(in context: Context) -> WeatherWidgetEntry { - WeatherWidgetEntry( - date: Date(), - temperature: "72°F", - condition: "Sunny", - location: "San Francisco", - humidity: "65%", - windSpeed: "8 mph", - icon: "sun.max.fill" - ) - } - - func getSnapshot(in context: Context, completion: @escaping (WeatherWidgetEntry) -> Void) { - let entry = WeatherWidgetEntry( - date: Date(), - temperature: "72°F", - condition: "Sunny", - location: "San Francisco", - humidity: "65%", - windSpeed: "8 mph", - icon: "sun.max.fill" - ) - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - // Simulate weather data updates every 30 minutes - let currentDate = Date() - let entries = (0..<48).map { index in - let entryDate = Calendar.current.date(byAdding: .minute, value: index * 30, to: currentDate)! - return WeatherWidgetEntry( - date: entryDate, - temperature: "\(70 + Int.random(in: -5...5))°F", - condition: ["Sunny", "Cloudy", "Rainy", "Partly Cloudy"].randomElement()!, - location: "San Francisco", - humidity: "\(60 + Int.random(in: -10...10))%", - windSpeed: "\(5 + Int.random(in: 0...10)) mph", - icon: getWeatherIcon(for: "Sunny") - ) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - private func getWeatherIcon(for condition: String) -> String { - switch condition { - case "Sunny": return "sun.max.fill" - case "Cloudy": return "cloud.fill" - case "Rainy": return "cloud.rain.fill" - case "Partly Cloudy": return "cloud.sun.fill" - default: return "sun.max.fill" - } - } -} - -// MARK: - Weather Widget Entry -struct WeatherWidgetEntry: TimelineEntry { - let date: Date - let temperature: String - let condition: String - let location: String - let humidity: String - let windSpeed: String - let icon: String -} - -// MARK: - Weather Widget View -struct WeatherWidgetView: View { - let entry: WeatherWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - switch family { - case .systemSmall: - SmallWeatherWidgetView(entry: entry) - case .systemMedium: - MediumWeatherWidgetView(entry: entry) - case .systemLarge: - LargeWeatherWidgetView(entry: entry) - default: - SmallWeatherWidgetView(entry: entry) - } - } -} - -// MARK: - Small Weather Widget -struct SmallWeatherWidgetView: View { - let entry: WeatherWidgetEntry - - var body: some View { - ZStack { - // Background gradient - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.cyan.opacity(0.6)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - - VStack(spacing: 8) { - // Weather icon - Image(systemName: entry.icon) - .font(.system(size: 32)) - .foregroundColor(.white) - .shadow(radius: 2) - - // Temperature - Text(entry.temperature) - .font(.system(size: 24, weight: .bold, design: .rounded)) - .foregroundColor(.white) - - // Condition - Text(entry.condition) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white.opacity(0.9)) - .multilineTextAlignment(.center) - } - .padding() - } - .widgetURL(URL(string: "weatherwidget://open")) - } -} - -// MARK: - Medium Weather Widget -struct MediumWeatherWidgetView: View { - let entry: WeatherWidgetEntry - - var body: some View { - HStack(spacing: 16) { - // Left side - Main weather info - VStack(alignment: .leading, spacing: 8) { - // Location - Text(entry.location) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.white) - - // Temperature - Text(entry.temperature) - .font(.system(size: 36, weight: .bold, design: .rounded)) - .foregroundColor(.white) - - // Condition - Text(entry.condition) - .font(.system(size: 14, weight: .medium)) - .foregroundColor(.white.opacity(0.9)) - } - - Spacer() - - // Right side - Weather icon and details - VStack(alignment: .trailing, spacing: 8) { - // Weather icon - Image(systemName: entry.icon) - .font(.system(size: 40)) - .foregroundColor(.white) - .shadow(radius: 2) - - // Additional details - VStack(alignment: .trailing, spacing: 4) { - HStack { - Image(systemName: "humidity") - .font(.system(size: 10)) - Text(entry.humidity) - .font(.system(size: 10)) - } - .foregroundColor(.white.opacity(0.8)) - - HStack { - Image(systemName: "wind") - .font(.system(size: 10)) - Text(entry.windSpeed) - .font(.system(size: 10)) - } - .foregroundColor(.white.opacity(0.8)) - } - } - } - .padding() - .background( - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.cyan.opacity(0.6)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .widgetURL(URL(string: "weatherwidget://open")) - } -} - -// MARK: - Large Weather Widget -struct LargeWeatherWidgetView: View { - let entry: WeatherWidgetEntry - - var body: some View { - VStack(spacing: 16) { - // Header - HStack { - VStack(alignment: .leading) { - Text(entry.location) - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(.white) - - Text(entry.condition) - .font(.system(size: 16, weight: .medium)) - .foregroundColor(.white.opacity(0.9)) - } - - Spacer() - - // Weather icon - Image(systemName: entry.icon) - .font(.system(size: 50)) - .foregroundColor(.white) - .shadow(radius: 3) - } - - // Main temperature - Text(entry.temperature) - .font(.system(size: 60, weight: .bold, design: .rounded)) - .foregroundColor(.white) - - // Weather details - HStack(spacing: 24) { - WeatherDetailView( - icon: "humidity", - title: "Humidity", - value: entry.humidity - ) - - WeatherDetailView( - icon: "wind", - title: "Wind", - value: entry.windSpeed - ) - - WeatherDetailView( - icon: "thermometer", - title: "Feels Like", - value: "\(Int(entry.temperature.dropLast(2)) ?? 0 + 2)°F" - ) - } - - // Hourly forecast (simplified) - HStack(spacing: 16) { - ForEach(0..<4) { hour in - VStack(spacing: 4) { - Text("\(hour * 3):00") - .font(.system(size: 12)) - .foregroundColor(.white.opacity(0.8)) - - Image(systemName: entry.icon) - .font(.system(size: 16)) - .foregroundColor(.white) - - Text("\(70 + hour * 2)°") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white) - } - } - } - } - .padding() - .background( - LinearGradient( - gradient: Gradient(colors: [Color.blue.opacity(0.8), Color.cyan.opacity(0.6)]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .widgetURL(URL(string: "weatherwidget://open")) - } -} - -// MARK: - Weather Detail View -struct WeatherDetailView: View { - let icon: String - let title: String - let value: String - - var body: some View { - VStack(spacing: 4) { - Image(systemName: icon) - .font(.system(size: 16)) - .foregroundColor(.white.opacity(0.8)) - - Text(title) - .font(.system(size: 10)) - .foregroundColor(.white.opacity(0.7)) - - Text(value) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.white) - } - } -} - -// MARK: - Widget Bundle -@main -struct WeatherWidgetBundle: WidgetBundle { - var body: some Widget { - WeatherWidgetExample() - } -} - -// MARK: - Preview -struct WeatherWidgetView_Previews: PreviewProvider { - static var previews: some View { - let entry = WeatherWidgetEntry( - date: Date(), - temperature: "72°F", - condition: "Sunny", - location: "San Francisco", - humidity: "65%", - windSpeed: "8 mph", - icon: "sun.max.fill" - ) - - Group { - WeatherWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .previewDisplayName("Small Weather Widget") - - WeatherWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .previewDisplayName("Medium Weather Widget") - - WeatherWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemLarge)) - .previewDisplayName("Large Weather Widget") - } - } -} \ No newline at end of file diff --git a/Examples/LiveActivityExamples/FitnessTrackerExample.swift b/Examples/LiveActivityExamples/FitnessTrackerExample.swift deleted file mode 100644 index d096a2f..0000000 --- a/Examples/LiveActivityExamples/FitnessTrackerExample.swift +++ /dev/null @@ -1,528 +0,0 @@ -import SwiftUI -import ActivityKit -import WidgetKit -import LiveDataIntegration - -// MARK: - Fitness Tracker Live Activity Example -// This example demonstrates how to create a comprehensive fitness tracking -// Live Activity with real-time updates and Dynamic Island integration - -struct FitnessTrackerAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var currentDistance: Double - var currentPace: String - var currentHeartRate: Int - var elapsedTime: TimeInterval - var targetDistance: Double - var isActive: Bool - } - - var activityName: String - var activityType: String - var startTime: Date -} - -// MARK: - Fitness Tracker Live Activity -struct FitnessTrackerLiveActivity: Widget { - var body: some WidgetConfiguration { - ActivityConfiguration(for: FitnessTrackerAttributes.self) { context in - // Lock screen/banner UI - LockScreenLiveActivityView(context: context) - } dynamicIsland: { context in - // Dynamic Island UI - DynamicIsland { - // Expanded UI - DynamicIslandExpandedRegion(.leading) { - FitnessExpandedLeadingView(context: context) - } - - DynamicIslandExpandedRegion(.trailing) { - FitnessExpandedTrailingView(context: context) - } - - DynamicIslandExpandedRegion(.center) { - FitnessExpandedCenterView(context: context) - } - - DynamicIslandExpandedRegion(.bottom) { - FitnessExpandedBottomView(context: context) - } - } compactLeading: { - // Compact leading - FitnessCompactLeadingView(context: context) - } compactTrailing: { - // Compact trailing - FitnessCompactTrailingView(context: context) - } minimal: { - // Minimal - FitnessMinimalView(context: context) - } - } - } -} - -// MARK: - Lock Screen Live Activity View -struct LockScreenLiveActivityView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 12) { - // Header - HStack { - VStack(alignment: .leading) { - Text(context.attributes.activityName) - .font(.headline) - .foregroundColor(.primary) - - Text(context.attributes.activityType) - .font(.subheadline) - .foregroundColor(.secondary) - } - - Spacer() - - // Activity status - Circle() - .fill(context.state.isActive ? Color.green : Color.red) - .frame(width: 12, height: 12) - .overlay( - Circle() - .stroke(Color.white, lineWidth: 2) - ) - } - - // Progress section - VStack(spacing: 8) { - // Distance progress - HStack { - Text("Distance") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Text("\(String(format: "%.2f", context.state.currentDistance)) km") - .font(.caption) - .foregroundColor(.primary) - } - - ProgressView(value: context.state.currentDistance, total: context.state.targetDistance) - .progressViewStyle(LinearProgressViewStyle(tint: .blue)) - - // Time and pace - HStack { - VStack(alignment: .leading) { - Text("Time") - .font(.caption) - .foregroundColor(.secondary) - Text(formatTime(context.state.elapsedTime)) - .font(.caption) - .foregroundColor(.primary) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("Pace") - .font(.caption) - .foregroundColor(.secondary) - Text(context.state.currentPace) - .font(.caption) - .foregroundColor(.primary) - } - } - } - - // Heart rate - HStack { - Image(systemName: "heart.fill") - .foregroundColor(.red) - .font(.caption) - - Text("\(context.state.currentHeartRate) BPM") - .font(.caption) - .foregroundColor(.primary) - - Spacer() - } - } - .padding() - .background(Color(.systemBackground)) - } - - private func formatTime(_ timeInterval: TimeInterval) -> String { - let hours = Int(timeInterval) / 3600 - let minutes = Int(timeInterval) / 60 % 60 - let seconds = Int(timeInterval) % 60 - - if hours > 0 { - return String(format: "%02d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%02d:%02d", minutes, seconds) - } - } -} - -// MARK: - Dynamic Island Views - -// Expanded Leading View -struct FitnessExpandedLeadingView: View { - let context: ActivityViewContext - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text("Distance") - .font(.caption) - .foregroundColor(.secondary) - - Text("\(String(format: "%.2f", context.state.currentDistance)) km") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text("of \(String(format: "%.2f", context.state.targetDistance)) km") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// Expanded Trailing View -struct FitnessExpandedTrailingView: View { - let context: ActivityViewContext - - var body: some View { - VStack(alignment: .trailing, spacing: 8) { - Text("Pace") - .font(.caption) - .foregroundColor(.secondary) - - Text(context.state.currentPace) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text("per km") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// Expanded Center View -struct FitnessExpandedCenterView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 8) { - Text(context.attributes.activityName) - .font(.headline) - .foregroundColor(.primary) - - Text(formatTime(context.state.elapsedTime)) - .font(.title) - .fontWeight(.bold) - .foregroundColor(.blue) - } - } - - private func formatTime(_ timeInterval: TimeInterval) -> String { - let hours = Int(timeInterval) / 3600 - let minutes = Int(timeInterval) / 60 % 60 - let seconds = Int(timeInterval) % 60 - - if hours > 0 { - return String(format: "%02d:%02d:%02d", hours, minutes, seconds) - } else { - return String(format: "%02d:%02d", minutes, seconds) - } - } -} - -// Expanded Bottom View -struct FitnessExpandedBottomView: View { - let context: ActivityViewContext - - var body: some View { - HStack(spacing: 16) { - // Heart rate - VStack(spacing: 4) { - Image(systemName: "heart.fill") - .foregroundColor(.red) - .font(.caption) - - Text("\(context.state.currentHeartRate)") - .font(.caption) - .fontWeight(.semibold) - - Text("BPM") - .font(.caption2) - .foregroundColor(.secondary) - } - - // Progress bar - VStack(spacing: 4) { - ProgressView(value: context.state.currentDistance, total: context.state.targetDistance) - .progressViewStyle(LinearProgressViewStyle(tint: .blue)) - - Text("\(Int((context.state.currentDistance / context.state.targetDistance) * 100))%") - .font(.caption2) - .foregroundColor(.secondary) - } - - // Status indicator - VStack(spacing: 4) { - Circle() - .fill(context.state.isActive ? Color.green : Color.red) - .frame(width: 8, height: 8) - - Text(context.state.isActive ? "Active" : "Paused") - .font(.caption2) - .foregroundColor(.secondary) - } - } - } -} - -// Compact Leading View -struct FitnessCompactLeadingView: View { - let context: ActivityViewContext - - var body: some View { - Image(systemName: "figure.run") - .foregroundColor(.blue) - .font(.title2) - } -} - -// Compact Trailing View -struct FitnessCompactTrailingView: View { - let context: ActivityViewContext - - var body: some View { - Text(formatTime(context.state.elapsedTime)) - .font(.caption) - .fontWeight(.semibold) - .foregroundColor(.primary) - } - - private func formatTime(_ timeInterval: TimeInterval) -> String { - let minutes = Int(timeInterval) / 60 - let seconds = Int(timeInterval) % 60 - return String(format: "%02d:%02d", minutes, seconds) - } -} - -// Minimal View -struct FitnessMinimalView: View { - let context: ActivityViewContext - - var body: some View { - Image(systemName: "figure.run") - .foregroundColor(.blue) - .font(.title3) - } -} - -// MARK: - Fitness Activity Manager -class FitnessActivityManager: ObservableObject { - static let shared = FitnessActivityManager() - - @Published var currentActivity: Activity? - @Published var isActivityActive = false - - private var activityTimer: Timer? - private var startTime: Date? - - private init() {} - - func startActivity(name: String, type: String, targetDistance: Double) { - let attributes = FitnessTrackerAttributes( - activityName: name, - activityType: type, - startTime: Date() - ) - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: 0.0, - currentPace: "0:00", - currentHeartRate: 0, - elapsedTime: 0, - targetDistance: targetDistance, - isActive: true - ) - - do { - let activity = try Activity.request( - attributes: attributes, - contentState: contentState, - pushType: nil - ) - - currentActivity = activity - isActivityActive = true - startTime = Date() - - startTimer() - - print("✅ Fitness activity started: \(activity.id)") - } catch { - print("❌ Failed to start fitness activity: \(error)") - } - } - - func updateActivity(distance: Double, pace: String, heartRate: Int) { - guard let activity = currentActivity, isActivityActive else { return } - - let elapsedTime = startTime?.timeIntervalSinceNow ?? 0 - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: distance, - currentPace: pace, - currentHeartRate: heartRate, - elapsedTime: abs(elapsedTime), - targetDistance: activity.attributes.targetDistance, - isActive: true - ) - - Task { - await activity.update(using: contentState) - } - } - - func pauseActivity() { - guard let activity = currentActivity else { return } - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: activity.content.state.elapsedTime, - targetDistance: activity.attributes.targetDistance, - isActive: false - ) - - Task { - await activity.update(using: contentState) - } - - isActivityActive = false - stopTimer() - } - - func resumeActivity() { - guard let activity = currentActivity else { return } - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: activity.content.state.elapsedTime, - targetDistance: activity.attributes.targetDistance, - isActive: true - ) - - Task { - await activity.update(using: contentState) - } - - isActivityActive = true - startTimer() - } - - func endActivity() { - guard let activity = currentActivity else { return } - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: activity.content.state.elapsedTime, - targetDistance: activity.attributes.targetDistance, - isActive: false - ) - - Task { - await activity.end(using: contentState, dismissalPolicy: .immediate) - } - - currentActivity = nil - isActivityActive = false - stopTimer() - - print("✅ Fitness activity ended") - } - - private func startTimer() { - activityTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in - self.updateElapsedTime() - } - } - - private func stopTimer() { - activityTimer?.invalidate() - activityTimer = nil - } - - private func updateElapsedTime() { - guard let activity = currentActivity, isActivityActive else { return } - - let elapsedTime = startTime?.timeIntervalSinceNow ?? 0 - - let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: activity.content.state.currentDistance, - currentPace: activity.content.state.currentPace, - currentHeartRate: activity.content.state.currentHeartRate, - elapsedTime: abs(elapsedTime), - targetDistance: activity.attributes.targetDistance, - isActive: true - ) - - Task { - await activity.update(using: contentState) - } - } -} - -// MARK: - Widget Bundle -@main -struct FitnessTrackerWidgetBundle: WidgetBundle { - var body: some Widget { - FitnessTrackerLiveActivity() - } -} - -// MARK: - Preview -struct FitnessTrackerLiveActivity_Previews: PreviewProvider { - static let attributes = FitnessTrackerAttributes( - activityName: "Morning Run", - activityType: "Running", - startTime: Date() - ) - - static let contentState = FitnessTrackerAttributes.ContentState( - currentDistance: 2.5, - currentPace: "5:30", - currentHeartRate: 145, - elapsedTime: 1800, - targetDistance: 5.0, - isActive: true - ) - - static var previews: some View { - attributes - .previewContext(contentState, viewKind: .content) - .previewDisplayName("Lock Screen") - - attributes - .previewContext(contentState, viewKind: .dynamicIsland(.compact)) - .previewDisplayName("Dynamic Island Compact") - - attributes - .previewContext(contentState, viewKind: .dynamicIsland(.expanded)) - .previewDisplayName("Dynamic Island Expanded") - - attributes - .previewContext(contentState, viewKind: .dynamicIsland(.minimal)) - .previewDisplayName("Dynamic Island Minimal") - } -} \ No newline at end of file diff --git a/Examples/LiveActivityExamples/LiveActivityExample.swift b/Examples/LiveActivityExamples/LiveActivityExample.swift deleted file mode 100644 index 00af688..0000000 --- a/Examples/LiveActivityExamples/LiveActivityExample.swift +++ /dev/null @@ -1,159 +0,0 @@ -import SwiftUI -import ActivityKit -import WidgetKit - -// MARK: - Live Activity Example -struct LiveActivityExample: Widget { - let kind: String = "com.company.app.liveactivity" - - var body: some WidgetConfiguration { - ActivityConfiguration(for: LiveActivityAttributes.self) { context in - LiveActivityView(context: context) - } dynamicIsland: { context in - DynamicIsland { - DynamicIslandExpandedRegion(.leading) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - Text("Order #\(context.state.orderNumber)") - .font(.caption) - } - } - - DynamicIslandExpandedRegion(.trailing) { - Text(context.state.status) - .font(.caption) - .foregroundColor(.secondary) - } - - DynamicIslandExpandedRegion(.bottom) { - ProgressView(value: context.state.progress) - .progressViewStyle(.linear) - } - } compactLeading: { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - } compactTrailing: { - Text(context.state.status) - .font(.caption2) - } minimal: { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - } - } - } -} - -// MARK: - Live Activity Attributes -struct LiveActivityAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - var orderNumber: String - var status: String - var progress: Double - var estimatedTime: String - } - - var orderNumber: String - var customerName: String -} - -// MARK: - Live Activity View -struct LiveActivityView: View { - let context: ActivityViewContext - - var body: some View { - VStack(spacing: 16) { - HStack { - Image(systemName: "shippingbox.fill") - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading) { - Text("Order #\(context.state.orderNumber)") - .font(.headline) - - Text(context.state.status) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Text(context.state.estimatedTime) - .font(.caption2) - .foregroundColor(.secondary) - } - - ProgressView(value: context.state.progress) - .progressViewStyle(.linear) - .tint(.blue) - - HStack { - Button("Track Order") { - // Open app to track order - } - .buttonStyle(.bordered) - - Spacer() - - Button("Contact Support") { - // Contact support - } - .buttonStyle(.bordered) - } - } - .padding() - .background(Color(.systemBackground)) - } -} - -// MARK: - Live Activity Manager -class LiveActivityManager { - static let shared = LiveActivityManager() - - private var activity: Activity? - - func startLiveActivity(orderNumber: String, customerName: String) { - let attributes = LiveActivityAttributes( - orderNumber: orderNumber, - customerName: customerName - ) - - let contentState = LiveActivityAttributes.ContentState( - orderNumber: orderNumber, - status: "Order confirmed", - progress: 0.1, - estimatedTime: "2:30 PM" - ) - - do { - activity = try Activity.request( - attributes: attributes, - contentState: contentState, - pushType: nil - ) - print("✅ Live activity started") - } catch { - print("❌ Live activity failed: \(error)") - } - } - - func updateLiveActivity(status: String, progress: Double, estimatedTime: String) { - Task { - let contentState = LiveActivityAttributes.ContentState( - orderNumber: activity?.attributes.orderNumber ?? "", - status: status, - progress: progress, - estimatedTime: estimatedTime - ) - - await activity?.update(using: contentState) - } - } - - func endLiveActivity() { - Task { - await activity?.end(dismissalPolicy: .immediate) - } - } -} \ No newline at end of file diff --git a/Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift b/Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift deleted file mode 100644 index 9640f8c..0000000 --- a/Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift +++ /dev/null @@ -1,418 +0,0 @@ -import SwiftUI -import WidgetKit -import WidgetTemplates - -// MARK: - Calendar Widget Example -// This example demonstrates how to create a beautiful calendar widget -// with event management and date navigation - -struct CalendarWidgetExample: Widget { - static let kind: String = "com.company.app.calendarwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: CalendarTimelineProvider()) { entry in - CalendarWidgetView(entry: entry) - } - .configurationDisplayName("Calendar Widget") - .description("Displays calendar events and date information") - .supportedFamilies([.systemSmall, .systemMedium]) - } -} - -// MARK: - Calendar Timeline Provider -struct CalendarTimelineProvider: TimelineProvider { - typealias Entry = CalendarWidgetEntry - - func placeholder(in context: Context) -> CalendarWidgetEntry { - CalendarWidgetEntry( - date: Date(), - currentDate: Date(), - events: [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch", time: "12:00 PM", isAllDay: false), - CalendarEvent(title: "Project Deadline", time: "5:00 PM", isAllDay: false) - ], - eventCount: 3, - nextEvent: CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false) - ) - } - - func getSnapshot(in context: Context, completion: @escaping (CalendarWidgetEntry) -> Void) { - let entry = CalendarWidgetEntry( - date: Date(), - currentDate: Date(), - events: [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch", time: "12:00 PM", isAllDay: false), - CalendarEvent(title: "Project Deadline", time: "5:00 PM", isAllDay: false) - ], - eventCount: 3, - nextEvent: CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false) - ) - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - let currentDate = Date() - let calendar = Calendar.current - - // Generate entries for the next 7 days - let entries = (0..<7).map { dayOffset in - let entryDate = calendar.date(byAdding: .day, value: dayOffset, to: currentDate)! - let events = generateEvents(for: entryDate) - - return CalendarWidgetEntry( - date: entryDate, - currentDate: entryDate, - events: events, - eventCount: events.count, - nextEvent: events.first - ) - } - - let timeline = Timeline(entries: entries, policy: .atEnd) - completion(timeline) - } - - private func generateEvents(for date: Date) -> [CalendarEvent] { - let calendar = Calendar.current - let weekday = calendar.component(.weekday, from: date) - - var events: [CalendarEvent] = [] - - // Add events based on the day of the week - switch weekday { - case 1: // Sunday - events = [ - CalendarEvent(title: "Family Brunch", time: "11:00 AM", isAllDay: false), - CalendarEvent(title: "Weekly Planning", time: "2:00 PM", isAllDay: false) - ] - case 2: // Monday - events = [ - CalendarEvent(title: "Team Meeting", time: "9:00 AM", isAllDay: false), - CalendarEvent(title: "Client Call", time: "2:00 PM", isAllDay: false), - CalendarEvent(title: "Gym Session", time: "6:00 PM", isAllDay: false) - ] - case 3: // Tuesday - events = [ - CalendarEvent(title: "Project Review", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch with Colleague", time: "12:30 PM", isAllDay: false) - ] - case 4: // Wednesday - events = [ - CalendarEvent(title: "Board Meeting", time: "11:00 AM", isAllDay: false), - CalendarEvent(title: "Team Building", time: "4:00 PM", isAllDay: false) - ] - case 5: // Thursday - events = [ - CalendarEvent(title: "Product Demo", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Weekly Report", time: "3:00 PM", isAllDay: false) - ] - case 6: // Friday - events = [ - CalendarEvent(title: "Weekend Planning", time: "2:00 PM", isAllDay: false), - CalendarEvent(title: "Happy Hour", time: "5:00 PM", isAllDay: false) - ] - case 7: // Saturday - events = [ - CalendarEvent(title: "Shopping", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Movie Night", time: "7:00 PM", isAllDay: false) - ] - default: - events = [] - } - - return events - } -} - -// MARK: - Calendar Widget Entry -struct CalendarWidgetEntry: TimelineEntry { - let date: Date - let currentDate: Date - let events: [CalendarEvent] - let eventCount: Int - let nextEvent: CalendarEvent? -} - -// MARK: - Calendar Event -struct CalendarEvent { - let title: String - let time: String - let isAllDay: Bool -} - -// MARK: - Calendar Widget View -struct CalendarWidgetView: View { - let entry: CalendarWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - switch family { - case .systemSmall: - SmallCalendarWidgetView(entry: entry) - case .systemMedium: - MediumCalendarWidgetView(entry: entry) - default: - SmallCalendarWidgetView(entry: entry) - } - } -} - -// MARK: - Small Calendar Widget -struct SmallCalendarWidgetView: View { - let entry: CalendarWidgetEntry - - var body: some View { - VStack(spacing: 8) { - // Date header - HStack { - Text(formatDate(entry.currentDate)) - .font(.system(size: 16, weight: .semibold)) - .foregroundColor(.primary) - - Spacer() - - Text("\(entry.eventCount)") - .font(.system(size: 14, weight: .bold)) - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue) - .clipShape(Circle()) - } - - // Next event - if let nextEvent = entry.nextEvent { - VStack(alignment: .leading, spacing: 4) { - Text("Next") - .font(.system(size: 10)) - .foregroundColor(.secondary) - - Text(nextEvent.title) - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.primary) - .lineLimit(1) - - Text(nextEvent.time) - .font(.system(size: 10)) - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity, alignment: .leading) - } else { - Text("No events today") - .font(.system(size: 12)) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity, alignment: .center) - } - - Spacer() - - // Calendar icon - Image(systemName: "calendar") - .font(.system(size: 20)) - .foregroundColor(.blue) - } - .padding() - .background(Color(.systemBackground)) - .widgetURL(URL(string: "calendarwidget://open")) - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } -} - -// MARK: - Medium Calendar Widget -struct MediumCalendarWidgetView: View { - let entry: CalendarWidgetEntry - - var body: some View { - HStack(spacing: 16) { - // Left side - Date and event count - VStack(alignment: .leading, spacing: 8) { - // Date - Text(formatDate(entry.currentDate)) - .font(.system(size: 20, weight: .bold)) - .foregroundColor(.primary) - - // Day of week - Text(formatDayOfWeek(entry.currentDate)) - .font(.system(size: 14)) - .foregroundColor(.secondary) - - // Event count - HStack { - Image(systemName: "calendar") - .font(.system(size: 12)) - .foregroundColor(.blue) - - Text("\(entry.eventCount) events") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } - } - - Spacer() - - // Right side - Events list - VStack(alignment: .leading, spacing: 6) { - Text("Today") - .font(.system(size: 12, weight: .semibold)) - .foregroundColor(.secondary) - - if entry.events.isEmpty { - Text("No events") - .font(.system(size: 12)) - .foregroundColor(.secondary) - } else { - ForEach(Array(entry.events.prefix(3).enumerated()), id: \.offset) { index, event in - HStack(spacing: 8) { - Circle() - .fill(Color.blue) - .frame(width: 6, height: 6) - - VStack(alignment: .leading, spacing: 2) { - Text(event.title) - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.primary) - .lineLimit(1) - - Text(event.time) - .font(.system(size: 9)) - .foregroundColor(.secondary) - } - } - } - - if entry.events.count > 3 { - Text("+\(entry.events.count - 3) more") - .font(.system(size: 10)) - .foregroundColor(.blue) - } - } - } - } - .padding() - .background(Color(.systemBackground)) - .widgetURL(URL(string: "calendarwidget://open")) - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "MMM d" - return formatter.string(from: date) - } - - private func formatDayOfWeek(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "EEEE" - return formatter.string(from: date) - } -} - -// MARK: - Calendar Widget Manager -class CalendarWidgetManager: ObservableObject { - static let shared = CalendarWidgetManager() - - @Published var currentEvents: [CalendarEvent] = [] - @Published var selectedDate: Date = Date() - - private let calendar = Calendar.current - private let eventStore = EventStore() - - private init() { - loadEvents() - } - - func loadEvents() { - eventStore.fetchEvents(for: selectedDate) { events in - DispatchQueue.main.async { - self.currentEvents = events - } - } - } - - func selectDate(_ date: Date) { - selectedDate = date - loadEvents() - } - - func addEvent(_ event: CalendarEvent) { - eventStore.addEvent(event, for: selectedDate) { success in - if success { - self.loadEvents() - } - } - } - - func removeEvent(_ event: CalendarEvent) { - eventStore.removeEvent(event, for: selectedDate) { success in - if success { - self.loadEvents() - } - } - } -} - -// MARK: - Event Store -class EventStore { - private var events: [Date: [CalendarEvent]] = [:] - - func fetchEvents(for date: Date, completion: @escaping ([CalendarEvent]) -> Void) { - let key = Calendar.current.startOfDay(for: date) - completion(events[key] ?? []) - } - - func addEvent(_ event: CalendarEvent, for date: Date, completion: @escaping (Bool) -> Void) { - let key = Calendar.current.startOfDay(for: date) - if events[key] == nil { - events[key] = [] - } - events[key]?.append(event) - completion(true) - } - - func removeEvent(_ event: CalendarEvent, for date: Date, completion: @escaping (Bool) -> Void) { - let key = Calendar.current.startOfDay(for: date) - events[key]?.removeAll { $0.title == event.title && $0.time == event.time } - completion(true) - } -} - -// MARK: - Widget Bundle -@main -struct CalendarWidgetBundle: WidgetBundle { - var body: some Widget { - CalendarWidgetExample() - } -} - -// MARK: - Preview -struct CalendarWidgetView_Previews: PreviewProvider { - static var previews: some View { - let entry = CalendarWidgetEntry( - date: Date(), - currentDate: Date(), - events: [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false), - CalendarEvent(title: "Lunch", time: "12:00 PM", isAllDay: false), - CalendarEvent(title: "Project Deadline", time: "5:00 PM", isAllDay: false) - ], - eventCount: 3, - nextEvent: CalendarEvent(title: "Team Meeting", time: "10:00 AM", isAllDay: false) - ) - - Group { - CalendarWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - .previewDisplayName("Small Calendar Widget") - - CalendarWidgetView(entry: entry) - .previewContext(WidgetPreviewContext(family: .systemMedium)) - .previewDisplayName("Medium Calendar Widget") - } - } -} \ No newline at end of file diff --git a/Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift b/Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift deleted file mode 100644 index 410dfd8..0000000 --- a/Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift +++ /dev/null @@ -1,81 +0,0 @@ -import SwiftUI -import WidgetKit - -// MARK: - Lock Screen Widget Example -struct LockScreenWidgetExample: Widget { - let kind: String = "com.company.app.lockscreenwidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: LockScreenWidgetProvider()) { entry in - LockScreenWidgetEntryView(entry: entry) - } - .configurationDisplayName("Lock Screen Widget") - .description("A beautiful lock screen widget example") - .supportedFamilies([.systemSmall, .systemMedium]) - } -} - -// MARK: - Widget Provider -struct LockScreenWidgetProvider: TimelineProvider { - func placeholder(in context: Context) -> LockScreenWidgetEntry { - LockScreenWidgetEntry(date: Date(), icon: "message.fill", title: "Messages", count: "3 new") - } - - func getSnapshot(in context: Context, completion: @escaping (LockScreenWidgetEntry) -> ()) { - let entry = LockScreenWidgetEntry(date: Date(), icon: "message.fill", title: "Messages", count: "3 new") - completion(entry) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { - let currentDate = Date() - let entry = LockScreenWidgetEntry(date: currentDate, icon: "message.fill", title: "Messages", count: "3 new") - let timeline = Timeline(entries: [entry], policy: .atEnd) - completion(timeline) - } -} - -// MARK: - Widget Entry -struct LockScreenWidgetEntry: TimelineEntry { - let date: Date - let icon: String - let title: String - let count: String -} - -// MARK: - Widget Entry View -struct LockScreenWidgetEntryView: View { - let entry: LockScreenWidgetEntry - @Environment(\.widgetFamily) var family - - var body: some View { - HStack(spacing: 12) { - Image(systemName: entry.icon) - .foregroundColor(.blue) - .font(.title2) - - VStack(alignment: .leading, spacing: 2) { - Text(entry.title) - .font(.caption) - .foregroundColor(.secondary) - - Text(entry.count) - .font(.caption2) - .foregroundColor(.primary) - .fontWeight(.medium) - } - - Spacer() - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color(.systemBackground)) - } -} - -// MARK: - Preview -struct LockScreenWidgetExample_Previews: PreviewProvider { - static var previews: some View { - LockScreenWidgetEntryView(entry: LockScreenWidgetEntry(date: Date(), icon: "message.fill", title: "Messages", count: "3 new")) - .previewContext(WidgetPreviewContext(family: .systemSmall)) - } -} \ No newline at end of file diff --git a/Examples/README.md b/Examples/README.md index b82d805..a44301e 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -1,13 +1,37 @@ -# Examples Index - -- ``Examples/AdvancedExample.swift -- ``Examples/AdvancedExamples/AdvancedWidgetExample.swift -- ``Examples/BasicExample.swift -- ``Examples/BasicExamples/BasicWidgetExample.swift -- ``Examples/DynamicIslandExamples/DynamicIslandExample.swift -- ``Examples/HomeScreenWidgetExamples/HomeScreenWidgetExample.swift -- ``Examples/HomeScreenWidgetExamples/WeatherWidgetExample.swift -- ``Examples/LiveActivityExamples/FitnessTrackerExample.swift -- ``Examples/LiveActivityExamples/LiveActivityExample.swift -- ``Examples/LockScreenWidgetExamples/CalendarWidgetExample.swift -- ``Examples/LockScreenWidgetExamples/LockScreenWidgetExample.swift +# Examples + +## WidgetKitExample + +An iOS app with a widget extension that uses the package from this repository (a local +package reference to `../..`, so it always builds against the code next to it). + +| Part | What it shows | +| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| [`Widgets/LaunchCountdownWidget.swift`](WidgetKitExample/Widgets/LaunchCountdownWidget.swift) | The smallest widget: `TimelineSchedule` for the entry dates and the `CountdownWidget` view | +| [`Shared/Steps.swift`](WidgetKitExample/Shared/Steps.swift) | A provider that reads `WidgetSnapshotStore` and builds the timeline with `TimelineBuilder` | +| [`Widgets/WaterWidget.swift`](WidgetKitExample/Widgets/WaterWidget.swift) | An interactive widget: `InteractiveWaterWidget` and `IncrementIntent` | +| [`App/ContentView.swift`](WidgetKitExample/App/ContentView.swift) | The app saves data for the widgets and previews the timeline with `WidgetTimelineScrubber` | + +### Run it + +1. Open `Examples/WidgetKitExample/WidgetKitExample.xcodeproj` in Xcode. +2. Choose the **WidgetKitExample** scheme and an iPhone simulator, then **Product › Run**. +3. On the simulator's Home Screen, touch and hold an empty area, tap **Edit › Add Widget**, + search for "Widget Kit Example" and add the Countdown, Steps or Water widget. + +From the command line: + +```sh +xcodebuild build -project Examples/WidgetKitExample/WidgetKitExample.xcodeproj \ + -scheme WidgetKitExample -destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO +``` + +The project is signed to run locally, which is enough for the simulator. To run it on a +device, set your team in **Signing & Capabilities** for both targets and replace the App +Group `group.com.example.widgets` with one your team owns, in the entitlements, in the +`WidgetDevelopmentKitAppGroup` key of both Info.plist files, and in `Steps.swift` and +`ContentView.swift`. + +The project is generated from [`project.yml`](WidgetKitExample/project.yml) with +[XcodeGen](https://github.com/yonaskolb/XcodeGen); run `xcodegen generate` in +`Examples/WidgetKitExample` after changing it. diff --git a/Examples/WidgetKitExample/App/ContentView.swift b/Examples/WidgetKitExample/App/ContentView.swift new file mode 100644 index 0000000..19f597a --- /dev/null +++ b/Examples/WidgetKitExample/App/ContentView.swift @@ -0,0 +1,76 @@ +import SwiftUI +import WidgetKit +import WidgetDevelopmentKit + +/// Writes the data the widgets read and previews the steps widget's timeline. +struct ContentView: View { + @Environment(\.scenePhase) private var scenePhase + @State private var steps = 6_000 + @State private var waterIntake = 0 + @State private var status = "Not saved yet" + @State private var timeline = StepsProvider().makeTimeline(now: .now) + + private let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + var body: some View { + NavigationStack { + List { + Section { + Stepper("\(steps.formatted()) steps", value: $steps, in: 0...30_000, step: 500) + Button("Save for the widget") { saveSteps() } + Text(status) + .font(.footnote) + .foregroundStyle(.secondary) + } header: { + Text("Steps widget data") + } + + Section { + WidgetTimelineScrubber(entries: timeline.entries) { entry in + StepsWidgetView(entry: entry) + } + .frame(maxWidth: .infinity) + } header: { + Text("Steps timeline") + } footer: { + Text("The entries StepsProvider gives WidgetKit, rendered with the widget's view.") + } + + Section { + LabeledContent("Today", value: "\(waterIntake) ml") + Button("Reset", role: .destructive) { + WidgetStateStore.shared.setValue(0, for: "water") + WidgetCenter.shared.reloadTimelines(ofKind: "water") + waterIntake = 0 + } + } header: { + Text("Water widget") + } footer: { + Text("Add the Water widget to the Home Screen and tap + to change this value.") + } + } + .navigationTitle("Widget Kit Example") + } + .onAppear { waterIntake = WidgetStateStore.shared.value("water") } + .onChange(of: scenePhase) { _, phase in + if phase == .active { + waterIntake = WidgetStateStore.shared.value("water") + } + } + } + + private func saveSteps() { + do { + try store.save(steps, forKey: "steps", lifetime: 30 * 60) + WidgetCenter.shared.reloadTimelines(ofKind: "steps") + timeline = StepsProvider().makeTimeline(now: .now) + status = "Saved \(steps.formatted()) steps" + } catch { + status = "Could not save: \(error.localizedDescription)" + } + } +} + +#Preview { + ContentView() +} diff --git a/Examples/WidgetKitExample/App/Info.plist b/Examples/WidgetKitExample/App/Info.plist new file mode 100644 index 0000000..fa865d4 --- /dev/null +++ b/Examples/WidgetKitExample/App/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Widget Kit Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UILaunchScreen + + WidgetDevelopmentKitAppGroup + group.com.example.widgets + + diff --git a/Examples/WidgetKitExample/App/WidgetKitExample.entitlements b/Examples/WidgetKitExample/App/WidgetKitExample.entitlements new file mode 100644 index 0000000..74a3a84 --- /dev/null +++ b/Examples/WidgetKitExample/App/WidgetKitExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.example.widgets + + + diff --git a/Examples/WidgetKitExample/App/WidgetKitExampleApp.swift b/Examples/WidgetKitExample/App/WidgetKitExampleApp.swift new file mode 100644 index 0000000..03b41f2 --- /dev/null +++ b/Examples/WidgetKitExample/App/WidgetKitExampleApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct WidgetKitExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/WidgetKitExample/Shared/ExampleIntents.swift b/Examples/WidgetKitExample/Shared/ExampleIntents.swift new file mode 100644 index 0000000..716d2e5 --- /dev/null +++ b/Examples/WidgetKitExample/Shared/ExampleIntents.swift @@ -0,0 +1,10 @@ +import AppIntents +import WidgetDevelopmentKit + +// Without this, the App Intents metadata processor does not see the package's intents, +// and the buttons in the water widget do nothing. +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} diff --git a/Examples/WidgetKitExample/Shared/Steps.swift b/Examples/WidgetKitExample/Shared/Steps.swift new file mode 100644 index 0000000..aac5f6a --- /dev/null +++ b/Examples/WidgetKitExample/Shared/Steps.swift @@ -0,0 +1,49 @@ +// Used by the widget extension and by the app's timeline preview. README.md shows the +// provider and the view under "Usage". + +import SwiftUI +import WidgetKit +import WidgetDevelopmentKit + +struct StepsEntry: TimelineEntry { + let date: Date + let steps: Int +} + +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} + +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} diff --git a/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/project.pbxproj b/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5ebe342 --- /dev/null +++ b/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/project.pbxproj @@ -0,0 +1,515 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 1BF4D0BB60AD4A02BCC13B18 /* ExampleWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D2B428492B1024F12A21C7 /* ExampleWidgets.swift */; }; + 3AA691ADC529901BDDE7F1A6 /* WidgetDevelopmentKit in Frameworks */ = {isa = PBXBuildFile; productRef = D7FBE162F50BA3047B743620 /* WidgetDevelopmentKit */; }; + 44CB4D65AF0C9176B6DD381B /* Steps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414FA71ED3100EEFB789978F /* Steps.swift */; }; + 5877D2583055127EDFCD61CE /* WaterWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F136FEAAC7B4839DC1BF81 /* WaterWidget.swift */; }; + B148D26FE03B77EB0A8715D9 /* ExampleIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5155CD62892A739EC4C7A8A8 /* ExampleIntents.swift */; }; + BC1D7B23C2F5413AF0AA380C /* WidgetKitExampleWidgets.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8FF671AC3901A8985008AD4D /* WidgetKitExampleWidgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + C5702BC1B6856C6DB2A57AC7 /* StepsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0143E298687869FCC0C0BD7 /* StepsWidget.swift */; }; + C5FAC95B148E1A94B64257D9 /* WidgetDevelopmentKit in Frameworks */ = {isa = PBXBuildFile; productRef = 0751A1CC3851411CD88062D1 /* WidgetDevelopmentKit */; }; + CC59560CACD46BDD299EBA3C /* WidgetKitExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECE590239CC0D8403F434F1 /* WidgetKitExampleApp.swift */; }; + D54B79056BCA234356DDD928 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E2B47F8F469DB08F814E8B /* ContentView.swift */; }; + DC3FB6AD3CCCC11A3BE258CD /* LaunchCountdownWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A77BA524FF88F5CEA27053 /* LaunchCountdownWidget.swift */; }; + F0B28BDE60B1688E6FA7969D /* ExampleIntents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5155CD62892A739EC4C7A8A8 /* ExampleIntents.swift */; }; + FBBF4A7E6BB7A3E910340CF6 /* Steps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414FA71ED3100EEFB789978F /* Steps.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 5280E3F690D25C7032A69A5C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 638E4ED4E7C4F19FE9D901C0 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3BAEF53F25466421F3774215; + remoteInfo = WidgetKitExampleWidgets; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A82B08CC35965FE37BCA53BA /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + BC1D7B23C2F5413AF0AA380C /* WidgetKitExampleWidgets.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 09E2B47F8F469DB08F814E8B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 414FA71ED3100EEFB789978F /* Steps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Steps.swift; sourceTree = ""; }; + 46A77BA524FF88F5CEA27053 /* LaunchCountdownWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchCountdownWidget.swift; sourceTree = ""; }; + 4E24686171FB716D2DA8EFC5 /* iOS-Widget-Development-Kit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "iOS-Widget-Development-Kit"; path = ../..; sourceTree = SOURCE_ROOT; }; + 5155CD62892A739EC4C7A8A8 /* ExampleIntents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleIntents.swift; sourceTree = ""; }; + 5E1486F033CFEA7850D0BF87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 66F136FEAAC7B4839DC1BF81 /* WaterWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaterWidget.swift; sourceTree = ""; }; + 83CF3C082B554837A69CF553 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 8FF671AC3901A8985008AD4D /* WidgetKitExampleWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = WidgetKitExampleWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 90C2C6C3C81476CB246BFED7 /* WidgetKitExampleWidgets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetKitExampleWidgets.entitlements; sourceTree = ""; }; + AECE590239CC0D8403F434F1 /* WidgetKitExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetKitExampleApp.swift; sourceTree = ""; }; + C0143E298687869FCC0C0BD7 /* StepsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsWidget.swift; sourceTree = ""; }; + D1D2B428492B1024F12A21C7 /* ExampleWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleWidgets.swift; sourceTree = ""; }; + F81F9DC9B4B113561AFFF077 /* WidgetKitExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetKitExample.entitlements; sourceTree = ""; }; + F8979B628C1D56D92AAD1324 /* WidgetKitExample.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = WidgetKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3A44A391DE8E50987813C274 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3AA691ADC529901BDDE7F1A6 /* WidgetDevelopmentKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8D18DB5B8E1980351364A5B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C5FAC95B148E1A94B64257D9 /* WidgetDevelopmentKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0EA8F1D12E19BDCB2E27141F /* Products */ = { + isa = PBXGroup; + children = ( + F8979B628C1D56D92AAD1324 /* WidgetKitExample.app */, + 8FF671AC3901A8985008AD4D /* WidgetKitExampleWidgets.appex */, + ); + name = Products; + sourceTree = ""; + }; + 1694A6EE653E71D1B69282DC /* Packages */ = { + isa = PBXGroup; + children = ( + 4E24686171FB716D2DA8EFC5 /* iOS-Widget-Development-Kit */, + ); + name = Packages; + sourceTree = ""; + }; + 1798501FFCCEA3C61B9B9E4B = { + isa = PBXGroup; + children = ( + 94FF8948074744BA06BF04DD /* App */, + 1694A6EE653E71D1B69282DC /* Packages */, + 79E33E0808C6BE9F2D42B3FA /* Shared */, + B16AC18BA9E598939B3191E5 /* Widgets */, + 0EA8F1D12E19BDCB2E27141F /* Products */, + ); + sourceTree = ""; + }; + 79E33E0808C6BE9F2D42B3FA /* Shared */ = { + isa = PBXGroup; + children = ( + 5155CD62892A739EC4C7A8A8 /* ExampleIntents.swift */, + 414FA71ED3100EEFB789978F /* Steps.swift */, + ); + path = Shared; + sourceTree = ""; + }; + 94FF8948074744BA06BF04DD /* App */ = { + isa = PBXGroup; + children = ( + 09E2B47F8F469DB08F814E8B /* ContentView.swift */, + 5E1486F033CFEA7850D0BF87 /* Info.plist */, + F81F9DC9B4B113561AFFF077 /* WidgetKitExample.entitlements */, + AECE590239CC0D8403F434F1 /* WidgetKitExampleApp.swift */, + ); + path = App; + sourceTree = ""; + }; + B16AC18BA9E598939B3191E5 /* Widgets */ = { + isa = PBXGroup; + children = ( + D1D2B428492B1024F12A21C7 /* ExampleWidgets.swift */, + 83CF3C082B554837A69CF553 /* Info.plist */, + 46A77BA524FF88F5CEA27053 /* LaunchCountdownWidget.swift */, + C0143E298687869FCC0C0BD7 /* StepsWidget.swift */, + 66F136FEAAC7B4839DC1BF81 /* WaterWidget.swift */, + 90C2C6C3C81476CB246BFED7 /* WidgetKitExampleWidgets.entitlements */, + ); + path = Widgets; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3BAEF53F25466421F3774215 /* WidgetKitExampleWidgets */ = { + isa = PBXNativeTarget; + buildConfigurationList = C7E673A4664B81BC412A70AE /* Build configuration list for PBXNativeTarget "WidgetKitExampleWidgets" */; + buildPhases = ( + 04AEA3A05D865A89C807A6BA /* Sources */, + 3A44A391DE8E50987813C274 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WidgetKitExampleWidgets; + packageProductDependencies = ( + D7FBE162F50BA3047B743620 /* WidgetDevelopmentKit */, + ); + productName = WidgetKitExampleWidgets; + productReference = 8FF671AC3901A8985008AD4D /* WidgetKitExampleWidgets.appex */; + productType = "com.apple.product-type.app-extension"; + }; + AEFDFD6E748D41A9BAF1B3F1 /* WidgetKitExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = A104ADCB6B120103CA6066D0 /* Build configuration list for PBXNativeTarget "WidgetKitExample" */; + buildPhases = ( + ECD945055520A0C51A488029 /* Sources */, + 8D18DB5B8E1980351364A5B5 /* Frameworks */, + A82B08CC35965FE37BCA53BA /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + AD31035C4044C4DD9A78A7C7 /* PBXTargetDependency */, + ); + name = WidgetKitExample; + packageProductDependencies = ( + 0751A1CC3851411CD88062D1 /* WidgetDevelopmentKit */, + ); + productName = WidgetKitExample; + productReference = F8979B628C1D56D92AAD1324 /* WidgetKitExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 638E4ED4E7C4F19FE9D901C0 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; + TargetAttributes = { + 3BAEF53F25466421F3774215 = { + DevelopmentTeam = ""; + ProvisioningStyle = Manual; + }; + AEFDFD6E748D41A9BAF1B3F1 = { + DevelopmentTeam = ""; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 27F02C0FE0C1B3234A89D8FD /* Build configuration list for PBXProject "WidgetKitExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = 1798501FFCCEA3C61B9B9E4B; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + B1B41461B8AAB44A88C28052 /* XCLocalSwiftPackageReference "../.." */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 0EA8F1D12E19BDCB2E27141F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AEFDFD6E748D41A9BAF1B3F1 /* WidgetKitExample */, + 3BAEF53F25466421F3774215 /* WidgetKitExampleWidgets */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 04AEA3A05D865A89C807A6BA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B148D26FE03B77EB0A8715D9 /* ExampleIntents.swift in Sources */, + 1BF4D0BB60AD4A02BCC13B18 /* ExampleWidgets.swift in Sources */, + DC3FB6AD3CCCC11A3BE258CD /* LaunchCountdownWidget.swift in Sources */, + 44CB4D65AF0C9176B6DD381B /* Steps.swift in Sources */, + C5702BC1B6856C6DB2A57AC7 /* StepsWidget.swift in Sources */, + 5877D2583055127EDFCD61CE /* WaterWidget.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + ECD945055520A0C51A488029 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D54B79056BCA234356DDD928 /* ContentView.swift in Sources */, + F0B28BDE60B1688E6FA7969D /* ExampleIntents.swift in Sources */, + FBBF4A7E6BB7A3E910340CF6 /* Steps.swift in Sources */, + CC59560CACD46BDD299EBA3C /* WidgetKitExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AD31035C4044C4DD9A78A7C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3BAEF53F25466421F3774215 /* WidgetKitExampleWidgets */; + targetProxy = 5280E3F690D25C7032A69A5C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 01954F44B765C615878CD5D4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Widgets/WidgetKitExampleWidgets.entitlements; + INFOPLIST_FILE = Widgets/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.WidgetKitExample.Widgets; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2C08826C9BA7C87D3FA4DF7E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = Widgets/WidgetKitExampleWidgets.entitlements; + INFOPLIST_FILE = Widgets/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.WidgetKitExample.Widgets; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 5A71A4E123301D28EC886E4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/WidgetKitExample.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.WidgetKitExample; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7560A002DB654287411E635F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = ""; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + 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 = 17.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + A44B33A4E0A4AE28F2BAC0F5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = ""; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 17.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 6.0; + }; + name = Release; + }; + D0A56871F1A8429C418C0A7E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/WidgetKitExample.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + INFOPLIST_FILE = App/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.WidgetKitExample; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 27F02C0FE0C1B3234A89D8FD /* Build configuration list for PBXProject "WidgetKitExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7560A002DB654287411E635F /* Debug */, + A44B33A4E0A4AE28F2BAC0F5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + A104ADCB6B120103CA6066D0 /* Build configuration list for PBXNativeTarget "WidgetKitExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0A56871F1A8429C418C0A7E /* Debug */, + 5A71A4E123301D28EC886E4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + C7E673A4664B81BC412A70AE /* Build configuration list for PBXNativeTarget "WidgetKitExampleWidgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 01954F44B765C615878CD5D4 /* Debug */, + 2C08826C9BA7C87D3FA4DF7E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + B1B41461B8AAB44A88C28052 /* XCLocalSwiftPackageReference "../.." */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../..; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 0751A1CC3851411CD88062D1 /* WidgetDevelopmentKit */ = { + isa = XCSwiftPackageProductDependency; + productName = WidgetDevelopmentKit; + }; + D7FBE162F50BA3047B743620 /* WidgetDevelopmentKit */ = { + isa = XCSwiftPackageProductDependency; + productName = WidgetDevelopmentKit; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 638E4ED4E7C4F19FE9D901C0 /* Project object */; +} diff --git a/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/xcshareddata/xcschemes/WidgetKitExample.xcscheme b/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/xcshareddata/xcschemes/WidgetKitExample.xcscheme new file mode 100644 index 0000000..ee656f8 --- /dev/null +++ b/Examples/WidgetKitExample/WidgetKitExample.xcodeproj/xcshareddata/xcschemes/WidgetKitExample.xcscheme @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/WidgetKitExample/Widgets/ExampleWidgets.swift b/Examples/WidgetKitExample/Widgets/ExampleWidgets.swift new file mode 100644 index 0000000..099674f --- /dev/null +++ b/Examples/WidgetKitExample/Widgets/ExampleWidgets.swift @@ -0,0 +1,11 @@ +import SwiftUI +import WidgetKit + +@main +struct ExampleWidgets: WidgetBundle { + var body: some Widget { + LaunchCountdownWidget() + StepsWidget() + WaterWidget() + } +} diff --git a/Examples/WidgetKitExample/Widgets/Info.plist b/Examples/WidgetKitExample/Widgets/Info.plist new file mode 100644 index 0000000..3ec1aee --- /dev/null +++ b/Examples/WidgetKitExample/Widgets/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Widget Kit Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + WidgetDevelopmentKitAppGroup + group.com.example.widgets + + diff --git a/Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift b/Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift new file mode 100644 index 0000000..2b27e1b --- /dev/null +++ b/Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift @@ -0,0 +1,37 @@ +// The quick start in README.md shows this file from LaunchEntry onwards. + +import SwiftUI +import WidgetKit +import WidgetDevelopmentKit + +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} diff --git a/Examples/WidgetKitExample/Widgets/StepsWidget.swift b/Examples/WidgetKitExample/Widgets/StepsWidget.swift new file mode 100644 index 0000000..56f6102 --- /dev/null +++ b/Examples/WidgetKitExample/Widgets/StepsWidget.swift @@ -0,0 +1,13 @@ +import SwiftUI +import WidgetKit +import WidgetDevelopmentKit + +struct StepsWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: "steps", provider: StepsProvider()) { entry in + StepsWidgetView(entry: entry) + } + .configurationDisplayName("Steps") + .supportedFamilies([.systemSmall]) + } +} diff --git a/Examples/WidgetKitExample/Widgets/WaterWidget.swift b/Examples/WidgetKitExample/Widgets/WaterWidget.swift new file mode 100644 index 0000000..a9709b4 --- /dev/null +++ b/Examples/WidgetKitExample/Widgets/WaterWidget.swift @@ -0,0 +1,39 @@ +import SwiftUI +import WidgetKit +import WidgetDevelopmentKit + +struct WaterEntry: TimelineEntry { + let date: Date + let intake: Int +} + +struct WaterProvider: TimelineProvider { + func placeholder(in context: Context) -> WaterEntry { + WaterEntry(date: .now, intake: 750) + } + + func getSnapshot(in context: Context, completion: @escaping (WaterEntry) -> Void) { + completion(currentEntry()) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // IncrementIntent reloads the timeline after each tap, so no scheduled reload is needed. + completion(.single(currentEntry(), policy: .never)) + } + + private func currentEntry() -> WaterEntry { + WaterEntry(date: .now, intake: WidgetStateStore.shared.value("water")) + } +} + +/// The "+" button runs `IncrementIntent` in the extension, which adds 250 ml to the +/// "water" counter in the App Group named by `WidgetDevelopmentKitAppGroup` in Info.plist. +struct WaterWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: "water", provider: WaterProvider()) { entry in + InteractiveWaterWidget(trackerId: "water", currentIntake: Double(entry.intake)) + } + .configurationDisplayName("Water") + .supportedFamilies([.systemSmall]) + } +} diff --git a/Examples/WidgetKitExample/Widgets/WidgetKitExampleWidgets.entitlements b/Examples/WidgetKitExample/Widgets/WidgetKitExampleWidgets.entitlements new file mode 100644 index 0000000..74a3a84 --- /dev/null +++ b/Examples/WidgetKitExample/Widgets/WidgetKitExampleWidgets.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.example.widgets + + + diff --git a/Examples/WidgetKitExample/project.yml b/Examples/WidgetKitExample/project.yml new file mode 100644 index 0000000..61ca56b --- /dev/null +++ b/Examples/WidgetKitExample/project.yml @@ -0,0 +1,72 @@ +# XcodeGen spec for WidgetKitExample.xcodeproj. The generated project is committed, so +# XcodeGen is only needed after changing this file: `xcodegen generate` in this folder. +name: WidgetKitExample +options: + bundleIdPrefix: com.example + deploymentTarget: + iOS: "17.0" + createIntermediateGroups: true +settings: + base: + SWIFT_VERSION: "6.0" + MARKETING_VERSION: "1.0" + CURRENT_PROJECT_VERSION: "1" + # Sign to run locally, so the app runs in the simulator without a team. + # Set your team and App Group to run on a device. + CODE_SIGN_STYLE: Manual + CODE_SIGN_IDENTITY: "-" + DEVELOPMENT_TEAM: "" +packages: + WidgetDevelopmentKit: + path: ../.. +targets: + WidgetKitExample: + type: application + platform: iOS + sources: [App, Shared] + dependencies: + - package: WidgetDevelopmentKit + product: WidgetDevelopmentKit + - target: WidgetKitExampleWidgets + info: + path: App/Info.plist + properties: + CFBundleDisplayName: Widget Kit Example + UILaunchScreen: {} + WidgetDevelopmentKitAppGroup: group.com.example.widgets + entitlements: + path: App/WidgetKitExample.entitlements + properties: + com.apple.security.application-groups: [group.com.example.widgets] + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.example.WidgetKitExample + WidgetKitExampleWidgets: + type: app-extension + platform: iOS + sources: [Widgets, Shared] + dependencies: + - package: WidgetDevelopmentKit + product: WidgetDevelopmentKit + info: + path: Widgets/Info.plist + properties: + CFBundleDisplayName: Widget Kit Example + NSExtension: + NSExtensionPointIdentifier: com.apple.widgetkit-extension + WidgetDevelopmentKitAppGroup: group.com.example.widgets + entitlements: + path: Widgets/WidgetKitExampleWidgets.entitlements + properties: + com.apple.security.application-groups: [group.com.example.widgets] + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.example.WidgetKitExample.Widgets +schemes: + WidgetKitExample: + build: + targets: + WidgetKitExample: all + WidgetKitExampleWidgets: all + run: + config: Debug diff --git a/LICENSE b/LICENSE index 69a6fb9..b005125 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Muhittin Camdali +Copyright (c) 2025 Muhittin Camdali Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 0d70cbb..0000000 --- a/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "swift-async-algorithms", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-async-algorithms", - "state" : { - "revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", - "version" : "1.6.0" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index d82a0bd..4142c59 100644 --- a/Package.swift +++ b/Package.swift @@ -1,36 +1,24 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "WidgetDevelopmentKit", platforms: [ - .iOS(.v16), - .macOS(.v13), - .watchOS(.v9), - .tvOS(.v16), - .visionOS(.v1) + .iOS(.v17), + .macOS(.v14) ], products: [ - .library(name: "WidgetDevelopmentKit", targets: ["WidgetDevelopmentKit"]), - ], - dependencies: [ - .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), + .library(name: "WidgetDevelopmentKit", targets: ["WidgetDevelopmentKit"]) ], targets: [ .target( name: "WidgetDevelopmentKit", - dependencies: [ - .product(name: "AsyncAlgorithms", package: "swift-async-algorithms"), - ], - path: "Sources/WidgetDevelopmentKit", - swiftSettings: [ - .enableExperimentalFeature("StrictConcurrency") - ] + path: "Sources/WidgetDevelopmentKit" ), .testTarget( name: "WidgetDevelopmentKitTests", dependencies: ["WidgetDevelopmentKit"], - path: "Tests/Unit" + path: "Tests/WidgetDevelopmentKitTests" ) ] ) diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..16d0cfa --- /dev/null +++ b/README.de.md @@ -0,0 +1,379 @@ + + + +

iOS Widget Development Kit: SwiftUI-Views und Timeline-Hilfen für WidgetKit-Widgets und Live Activities.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+CI-Status +Unterstützte Plattformen: iOS 17+ · macOS 14+ +Sprachversion: Swift 6.0+ +Lizenz: MIT +

+ +

+Loslegen +Dokumentation +Beispiele +

+ +> Dies ist eine Übersetzung der englischen README. Bei Abweichungen gilt die englische Fassung. + + + +WidgetDevelopmentKit ist ein Swift-Paket für alle, die Widgets für ihre iOS-Apps bauen. Es +enthält fertige SwiftUI-Views für Widgets auf dem Home-Bildschirm, dem Sperrbildschirm und in +StandBy, Diagramm-Views in Widget-Größe, Hilfstypen für Timelines und für die Daten, die die +App mit ihrem Widget teilt, App Intents für Widget-Buttons sowie ActivityKit-Attribute und +-Views für Live Activities. Die Widget-Extension schreibst du weiterhin selbst; das Paket nimmt +dir die Views und die Datums-, Speicher- und Wiederholungslogik ab, die jedes Widget braucht. + +## Highlights + +- **20 Vorlagen für den Home-Bildschirm** – Wetter, Statistiken, Finanzen, Kalender, + Countdown, Uhr, Notizen, Zitate, Musik, Podcast, Nachrichten, Fitnessringe, + Gewohnheitstracker, To-dos, Wasser, Batterie und mehr. +- **Sperrbildschirm und StandBy** – 15 Accessory-Views (kreisförmig, rechteckig, inline) und 7 + StandBy-Views. +- **Timeline-Hilfen** – Eintragsdaten, Reload-Richtlinien und ein Snapshot-Speicher mit + Ablaufzeit, damit das Widget liest, was die App über eine App Group gespeichert hat. +- **Interaktive Widgets** – 5 App Intents und 8 Views mit Buttons, die sie ausführen. +- **Live Activities** – 10 `ActivityAttributes`-Typen mit Views für den Sperrbildschirm und + die kompakte Dynamic Island sowie ein Zustandsautomat, der Updates in falscher Reihenfolge + ablehnt. +- **Getestete Logik** – der Code für Datumswerte, Zahlen und Wiederholungen ist mit Swift + Testing getestet, auch für Grenzfälle wie `NaN`, Ziele von null und Tage mit + Zeitumstellung. + +## Schnellstart + +**1. Paket hinzufügen** + +Deine App braucht eine Widget-Extension. Falls es noch keine gibt, wähle in Xcode **File › +New › Target…** und dann **Widget Extension**. Wähle anschließend **File › Add Package +Dependencies…**, füge diese URL oben rechts in das Suchfeld ein und klicke auf **Add +Package**: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Stelle im nächsten Dialog in der Spalte **Add to Target** für `WidgetDevelopmentKit` deine +Widget-Extension ein und klicke auf **Add Package**. + +**2. Modul importieren** + +Am Anfang einer Swift-Datei der Widget-Extension: + +```swift +import WidgetDevelopmentKit +``` + +**3. Verwenden** + +Füge dieses Widget zur Extension hinzu. Es zählt mit der `CountdownWidget`-View des Pakets +bis zu einem Datum herunter. Trage danach `LaunchCountdownWidget()` im `WidgetBundle` der +Extension ein, also im Typ, der mit `@main` markiert ist. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +Starte die App in einem iPhone-Simulator. Halte auf dem Home-Bildschirm eine freie Stelle +gedrückt, tippe auf **Edit › Add Widget**, suche deine App und füge **Countdown** hinzu. +Derselbe Code steht in +[`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift). + +## Installation + +### Swift Package Manager + +Wähle in Xcode **File › Add Package Dependencies…** und gib die Repository-URL ein: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Oder füge es zu den `dependencies` in deiner `Package.swift` hinzu: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +Releases beginnen mit 2.0.0; das frühere Tag `v1.0.0` ist älter als der aktuelle Code und +lässt sich nicht bauen. Das Paket hat keine Abhängigkeiten von Drittanbietern. + +## Verwendung + +Alle Swift-Beispiele in diesem Abschnitt stammen aus +[`Examples/WidgetKitExample`](Examples/WidgetKitExample), das CI baut, oder aus +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift), das +mit den Tests kompiliert wird. Ein Test schlägt fehl, wenn ein Block hier nicht mehr mit diesem +Code übereinstimmt. + +### Geteilte Daten in einem Timeline-Provider lesen + +`TimelineSchedule` erzeugt die Eintragsdaten, `TimelineBuilder` sortiert die Einträge und +übersetzt eine `WidgetUpdatePolicy` in die Reload-Richtlinie von WidgetKit, und +`WidgetSnapshotStore` liest, was die App in der gemeinsamen App Group gespeichert hat. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### Daten in der App speichern + +Die App schreibt den Wert mit einer Gültigkeitsdauer und bittet WidgetKit, das Widget neu zu +laden: + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +Füge der App und der Extension die Capability **App Groups** mit derselben Kennung hinzu. Der +Speicher nutzt `UserDefaults`, das nicht verschlüsselt ist; lege dort also keine Geheimnisse +ab. + +### Einen Eintrag anzeigen + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Views, die zeitabhängige Werte zeigen (`CountdownWidget`, `StandByCountdownWidget`, +`LockScreenCountdownWidget`), erhalten ein `referenceDate`; übergib `entry.date`, damit jeder +Eintrag seinen eigenen Zeitpunkt darstellt. + +### Einem Widget einen Button hinzufügen + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Die Intents laufen in der Widget-Extension und schreiben in `WidgetStateStore.shared`. Dieser +Speicher verwendet die App Group, die im Schlüssel `WidgetDevelopmentKitAppGroup` der +Info.plist steht. Trage den Schlüssel in der App und in der Extension ein und registriere die +Intents des Pakets: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Daten von einem Server laden + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` verwendet eine flüchtige (ephemeral) Session mit kurzen Timeouts, die nicht +auf eine Verbindung wartet. Er wiederholt Antworten mit 5xx und 408, Timeouts und +Offline-Fehler (standardmäßig einmal; mit einer `RetryPolicy` änderbar), aber keine +4xx-Antworten oder Decodierungsfehler. + +## Demo + + + + Mit dem Paket gerenderte Widgets für Wetter, Wasser, Fitness, Countdown, Zitat und Gewohnheiten + + + + Views für Statistik mit Sparkline, Fortschrittsring, Liniendiagramm und Balkendiagramm + + + + + Sperrbildschirm-Widgets für Batterie, Aktivitätsringe, Countdown, Wetter und Statistik + + +Die Bilder werden von [`scripts/render-screenshots.sh`](scripts/render-screenshots.sh) mit +`ImageRenderer` aus den Views des Pakets gerendert. Da sie außerhalb von WidgetKit entstehen, +sind Widget-Rahmen, Sperrbildschirm-Vibrancy und der Nachtmodus von StandBy nur angenähert und +nicht vom System erzeugt. + +Um die Widgets selbst auszuprobieren, öffne +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) und starte +das Schema **WidgetKitExample** in einem iPhone-Simulator; +[`Examples/README.md`](Examples/README.md) beschreibt die drei Widgets und wie du sie zum +Home-Bildschirm hinzufügst. + +## Dokumentation + +API-Referenz und Artikel: + +- [API-Referenz](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/) + auf GitHub Pages, erzeugt aus den DocC-Kommentaren im Quellcode. +- In Xcode: **Product › Build Documentation**, oder + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Anleitungen + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): + Widget-Extension und Paket hinzufügen, Daten über eine App Group teilen, die Timeline + aufbauen und einen Eintrag darstellen. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): + App Group einrichten, Intents registrieren, Buttons hinzufügen und ihren Zustand im Provider + und in der App lesen. + +## Voraussetzungen + +| Plattform | Mindestversion | +| --------- | -------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| Toolchain | Swift 6.0 | + +Sperrbildschirm-Views und Live Activities gibt es nur unter iOS. Das macOS-Ziel existiert, +damit der plattformunabhängige Code und seine Tests mit `swift test` laufen. CI baut mit dem +Standard-Xcode des `macos-26`-Images von GitHub; lokal wird mit Xcode 27 entwickelt. Ältere +Xcode-Versionen wurden nicht ausprobiert. + +## Architektur + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +Die Views bleiben schlank: Berechnungen, die mit echten Daten schiefgehen können (Ziele von +null, `NaN`, vertauschte Datumsbereiche, Tage mit Zeitumstellung), liegen als reine Funktionen +mit Unit-Tests in `Core` und `Timeline`. Typen, die auf Speicher oder Netzwerk zugreifen, +erhalten ihre Abhängigkeiten (`UserDefaults`, Uhr, Transport, Warten) als Parameter und lassen +sich so ohne App Group oder Server testen. Für die Bodies der SwiftUI-Views gibt es keine +Snapshot-Tests. + +Führe die Tests unter macOS und in einem iOS-Simulator aus: + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +Drei iOS-Tests werden absichtlich übersprungen: Sie rendern die Bilder der README und laufen nur +über `scripts/render-screenshots.sh`. + +## Roadmap + +- Ein Release taggen, das zum aktuellen Code passt, damit sich das Paket per Version einbinden + lässt. +- Snapshot-Tests für die View-Bodies. +- Eine Beispiel-Live-Activity in der Beispiel-App. + +Das [CHANGELOG](CHANGELOG.md) hält fest, was sich bisher geändert hat, auch wie das Paket im +Oktober 2026 wiederhergestellt wurde, nachdem sich herausgestellt hatte, dass der Code von +`v1.0.0` nicht baut. + +## Mitwirken + +Beiträge sind willkommen. Lies vor einem Pull Request den [Leitfaden für Beiträge](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) und den [Verhaltenskodex](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md). Sicherheitsprobleme meldest du vertraulich, wie in der [Sicherheitsrichtlinie](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md) beschrieben. + +## Lizenz + +Veröffentlicht unter der MIT-Lizenz. Siehe [LICENSE](LICENSE). diff --git a/README.es.md b/README.es.md new file mode 100644 index 0000000..303c98d --- /dev/null +++ b/README.es.md @@ -0,0 +1,376 @@ + + + +

iOS Widget Development Kit: Vistas SwiftUI y utilidades de línea de tiempo para widgets de WidgetKit y Live Activities.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+Estado de CI +Plataformas compatibles: iOS 17+ · macOS 14+ +Versión del lenguaje: Swift 6.0+ +Licencia: MIT +

+ +

+Empezar +Documentación +Ejemplos +

+ +> Esta es una traducción del README en inglés. Si hay diferencias, la versión en inglés es la vigente. + + + +WidgetDevelopmentKit es un paquete Swift para quienes crean widgets para sus apps de iOS. +Incluye vistas SwiftUI listas para usar en widgets de la pantalla de inicio, la pantalla +bloqueada y StandBy, vistas de gráficos adaptadas al tamaño de un widget, utilidades para las +líneas de tiempo y para los datos que la app comparte con su widget, App Intents para los +botones de los widgets, y atributos y vistas de ActivityKit para Live Activities. La extensión +de widgets la sigues escribiendo tú; el paquete te ahorra las vistas y la lógica de fechas, +almacenamiento y reintentos que necesita cualquier widget. + +## Lo más destacado + +- **20 plantillas para la pantalla de inicio**: tiempo, estadísticas, finanzas, calendario, + cuenta atrás, reloj, notas, citas, música, pódcast, noticias, anillos de actividad física, + seguimiento de hábitos, tareas, agua, batería y más. +- **Pantalla bloqueada y StandBy**: 15 vistas accesorias (circular, rectangular, en línea) y 7 + vistas de StandBy. +- **Utilidades de línea de tiempo**: fechas de las entradas, políticas de recarga y un almacén + de instantáneas con caducidad, para que el widget lea lo que la app guardó a través de un + App Group. +- **Widgets interactivos**: 5 App Intents y 8 vistas con botones que los ejecutan. +- **Live Activities**: 10 tipos de `ActivityAttributes` con vistas para la pantalla bloqueada y + la Dynamic Island compacta, y una máquina de estados que rechaza las actualizaciones que + llegan desordenadas. +- **Lógica con pruebas**: el código de fechas, números y reintentos está cubierto por pruebas + de Swift Testing, incluidos casos límite como `NaN`, objetivos de cero y los días de cambio + de horario. + +## Inicio rápido + +**1. Añade el paquete** + +Tu app necesita una extensión de widgets. Si todavía no tiene una, elige **File › New › +Target…** en Xcode y selecciona **Widget Extension**. Después elige **File › Add Package +Dependencies…**, pega esta URL en el campo de búsqueda de la esquina superior derecha y pulsa +**Add Package**: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +En el siguiente cuadro de diálogo, en la columna **Add to Target** de `WidgetDevelopmentKit` +elige tu extensión de widgets y pulsa **Add Package**. + +**2. Importa el módulo** + +Al principio de un archivo Swift de la extensión de widgets: + +```swift +import WidgetDevelopmentKit +``` + +**3. Úsalo** + +Añade este widget a la extensión. Muestra una cuenta atrás hasta una fecha con la vista +`CountdownWidget` del paquete. Después incluye `LaunchCountdownWidget()` en el `WidgetBundle` +de la extensión, el tipo marcado con `@main`. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +Ejecuta la app en un simulador de iPhone. En la pantalla de inicio, mantén pulsada una zona +vacía, toca **Edit › Add Widget**, busca tu app y añade **Countdown**. El mismo código está en +[`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift). + +## Instalación + +### Swift Package Manager + +En Xcode, elige **File › Add Package Dependencies…** e introduce la URL del repositorio: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +O añádelo a las `dependencies` de tu `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +Las versiones empiezan en 2.0.0; la etiqueta anterior, `v1.0.0`, es anterior al código +actual y no compila. El paquete no tiene dependencias de terceros. + +## Uso + +Todos los ejemplos de Swift de esta sección salen de +[`Examples/WidgetKitExample`](Examples/WidgetKitExample), que CI compila, o de +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift), que +se compila con las pruebas. Si un bloque de aquí deja de coincidir con ese código, falla una +prueba. + +### Leer datos compartidos en un proveedor de línea de tiempo + +`TimelineSchedule` genera las fechas de las entradas, `TimelineBuilder` ordena las entradas y +convierte una `WidgetUpdatePolicy` en la política de recarga de WidgetKit, y +`WidgetSnapshotStore` lee lo que la app guardó en el App Group compartido. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### Guardar datos en la app + +La app escribe el valor con un tiempo de validez y pide a WidgetKit que recargue el widget: + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +Añade la capacidad **App Groups** a la app y a la extensión con el mismo identificador. El +almacén usa `UserDefaults`, que no está cifrado, así que no guardes en él datos secretos. + +### Mostrar una entrada + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Las vistas que muestran valores que dependen del tiempo (`CountdownWidget`, +`StandByCountdownWidget`, `LockScreenCountdownWidget`) reciben un `referenceDate`; pasa +`entry.date` para que cada entrada muestre su propio momento. + +### Añadir un botón a un widget + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Los intents se ejecutan en la extensión de widgets y escriben en `WidgetStateStore.shared`, +que usa el App Group indicado en la clave `WidgetDevelopmentKitAppGroup` de Info.plist. Añade +la clave tanto a la app como a la extensión y registra los intents del paquete: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Cargar datos de un servidor + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` usa una sesión efímera con tiempos de espera cortos que no espera a que haya +conexión. Reintenta las respuestas 5xx y 408, los tiempos de espera agotados y los errores sin +conexión (una vez por defecto; configura una `RetryPolicy` para cambiarlo), pero no las +respuestas 4xx ni los errores de decodificación. + +## Demo + + + + Widgets de tiempo, agua, actividad física, cuenta atrás, cita y hábitos dibujados con el paquete + + + + Vistas de estadísticas con minigráfico, anillo de progreso, gráfico de líneas y gráfico de barras + + + + + Widgets de la pantalla bloqueada de batería, anillos de actividad, cuenta atrás, tiempo y estadísticas + + +Las imágenes se dibujan a partir de las vistas del paquete con `ImageRenderer` mediante +[`scripts/render-screenshots.sh`](scripts/render-screenshots.sh). Como se dibujan fuera de +WidgetKit, el marco del widget, la vibrancia de la pantalla bloqueada y el modo nocturno de +StandBy son una aproximación y no los genera el sistema. + +Para probar los widgets, abre +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) y ejecuta +el esquema **WidgetKitExample** en un simulador de iPhone; +[`Examples/README.md`](Examples/README.md) explica los tres widgets y cómo añadirlos a la +pantalla de inicio. + +## Documentación + +Referencia de la API y artículos: + +- [Referencia de la API](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/) + en GitHub Pages, generada a partir de los comentarios DocC del código. +- En Xcode: **Product › Build Documentation**, o + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Guías + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): + añadir una extensión de widgets y el paquete, compartir datos mediante un App Group, crear la + línea de tiempo y dibujar una entrada. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): + configurar el App Group, registrar los intents, añadir botones y leer su estado en el + proveedor y en la app. + +## Requisitos + +| Plataforma | Versión mínima | +| ------------ | -------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| Herramientas | Swift 6.0 | + +Las vistas de la pantalla bloqueada y las Live Activities solo existen en iOS. El destino de +macOS está para que el código independiente de la plataforma y sus pruebas se ejecuten con +`swift test`. CI compila con el Xcode predeterminado de la imagen `macos-26` de GitHub; el +desarrollo local usa Xcode 27. No se han probado versiones anteriores de Xcode. + +## Arquitectura + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +Las vistas son finas: los cálculos que pueden fallar con datos reales (objetivos de cero, +`NaN`, rangos de fechas invertidos, días de cambio de horario) están en `Core` y `Timeline` como +funciones puras con pruebas unitarias. Los tipos que acceden al almacenamiento o a la red +reciben sus dependencias (`UserDefaults`, reloj, transporte, espera) como parámetros, así que se +pueden probar sin un App Group ni un servidor. Los cuerpos de las vistas SwiftUI no tienen +pruebas de instantáneas. + +Ejecuta las pruebas en macOS y en un simulador de iOS: + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +Tres pruebas de iOS se omiten a propósito: dibujan las imágenes del README y solo se ejecutan +mediante `scripts/render-screenshots.sh`. + +## Hoja de ruta + +- Etiquetar una versión que corresponda al código actual, para poder añadir el paquete por + versión. +- Pruebas de instantáneas para los cuerpos de las vistas. +- Una Live Activity de ejemplo en la app de ejemplo. + +El [CHANGELOG](CHANGELOG.md) recoge los cambios hasta ahora, incluido cómo se restauró el +paquete en octubre de 2026 después de comprobar que el código de `v1.0.0` no compilaba. + +## Contribuir + +Las contribuciones son bienvenidas. Lee la [guía de contribución](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) y el [código de conducta](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md) antes de abrir una pull request. Informa de los problemas de seguridad de forma privada, como se describe en la [política de seguridad](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md). + +## Licencia + +Publicado bajo la licencia MIT. Consulta [LICENSE](LICENSE). diff --git a/README.fr.md b/README.fr.md new file mode 100644 index 0000000..b9deba8 --- /dev/null +++ b/README.fr.md @@ -0,0 +1,379 @@ + + + +

iOS Widget Development Kit: Vues SwiftUI et utilitaires de timeline pour les widgets WidgetKit et les Live Activities.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+État de la CI +Plateformes prises en charge: iOS 17+ · macOS 14+ +Version du langage: Swift 6.0+ +Licence: MIT +

+ +

+Commencer +Documentation +Exemples +

+ +> Ceci est une traduction du README anglais. En cas de différence, la version anglaise fait foi. + + + +WidgetDevelopmentKit est un paquet Swift destiné aux développeurs qui créent des widgets pour +leurs apps iOS. Il contient des vues SwiftUI prêtes à l’emploi pour les widgets de l’écran +d’accueil, de l’écran verrouillé et de StandBy, des vues de graphiques adaptées à la taille +d’un widget, des utilitaires pour les timelines et pour les données que l’app partage avec son +widget, des App Intents pour les boutons des widgets, ainsi que des attributs et des vues +ActivityKit pour les Live Activities. Vous écrivez toujours l’extension de widget vous-même ; +le paquet vous épargne les vues et la logique de dates, de stockage et de nouvelles tentatives +dont tout widget a besoin. + +## Points forts + +- **20 modèles pour l’écran d’accueil** — météo, statistiques, finances, calendrier, compte à + rebours, horloge, notes, citation, musique, podcast, actualités, anneaux d’activité, suivi + d’habitudes, tâches, eau, batterie, etc. +- **Écran verrouillé et StandBy** — 15 vues accessoires (circulaire, rectangulaire, en ligne) + et 7 vues StandBy. +- **Utilitaires de timeline** — dates des entrées, politiques de rechargement et un stockage + d’instantanés avec expiration, pour que le widget lise ce que l’app a enregistré via un App + Group. +- **Widgets interactifs** — 5 App Intents et 8 vues dont les boutons les exécutent. +- **Live Activities** — 10 types `ActivityAttributes` avec des vues pour l’écran verrouillé et + la Dynamic Island compacte, et une machine à états qui rejette les mises à jour arrivées dans + le désordre. +- **Logique testée** — le code des dates, des nombres et des nouvelles tentatives est couvert + par des tests Swift Testing, y compris des cas limites comme `NaN`, un objectif nul et les + jours de changement d’heure. + +## Démarrage rapide + +**1. Ajouter le paquet** + +Votre app a besoin d’une extension de widget. Si elle n’en a pas encore, choisissez **File › +New › Target…** dans Xcode, puis **Widget Extension**. Choisissez ensuite **File › Add Package +Dependencies…**, collez cette URL dans le champ de recherche en haut à droite et cliquez sur +**Add Package** : + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Dans la fenêtre suivante, réglez **Add to Target** de `WidgetDevelopmentKit` sur votre +extension de widget, puis cliquez sur **Add Package**. + +**2. Importer le module** + +En haut d’un fichier Swift de l’extension de widget : + +```swift +import WidgetDevelopmentKit +``` + +**3. L’utiliser** + +Ajoutez ce widget à l’extension. Il affiche un compte à rebours jusqu’à une date avec la vue +`CountdownWidget` du paquet. Ajoutez ensuite `LaunchCountdownWidget()` au `WidgetBundle` de +l’extension, le type marqué `@main`. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +Lancez l’app dans un simulateur d’iPhone. Sur l’écran d’accueil, maintenez le doigt sur une +zone vide, touchez **Edit › Add Widget**, cherchez votre app et ajoutez **Countdown**. Le même +code se trouve dans +[`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift). + +## Installation + +### Swift Package Manager + +Dans Xcode, choisissez **File › Add Package Dependencies…** et saisissez l’URL du dépôt : + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Ou ajoutez-le aux `dependencies` de votre `Package.swift` : + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +Les versions commencent à 2.0.0 ; l’ancien tag `v1.0.0` est antérieur au code actuel et +ne compile pas. Le paquet n’a aucune dépendance tierce. + +## Utilisation + +Chaque exemple Swift de cette section provient de +[`Examples/WidgetKitExample`](Examples/WidgetKitExample), que la CI compile, ou de +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift), +compilé avec les tests. Un test échoue si un bloc de cette page ne correspond plus à ce code. + +### Lire des données partagées dans un fournisseur de timeline + +`TimelineSchedule` produit les dates des entrées, `TimelineBuilder` trie les entrées et +convertit une `WidgetUpdatePolicy` en politique de rechargement de WidgetKit, et +`WidgetSnapshotStore` lit ce que l’app a enregistré dans l’App Group partagé. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### Enregistrer des données dans l’app + +L’app écrit la valeur avec une durée de validité et demande à WidgetKit de recharger le +widget : + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +Ajoutez la capacité **App Groups** à l’app et à l’extension avec le même identifiant. Le +stockage utilise `UserDefaults`, qui n’est pas chiffré : n’y mettez aucun secret. + +### Afficher une entrée + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Les vues qui affichent des valeurs dépendant du temps (`CountdownWidget`, +`StandByCountdownWidget`, `LockScreenCountdownWidget`) prennent un `referenceDate` ; passez +`entry.date` pour que chaque entrée affiche son propre instant. + +### Ajouter un bouton à un widget + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Les intents s’exécutent dans l’extension de widget et écrivent dans `WidgetStateStore.shared`, +qui utilise l’App Group indiqué par la clé `WidgetDevelopmentKitAppGroup` de l’Info.plist. +Ajoutez la clé à l’app et à l’extension, et enregistrez les intents du paquet : + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Charger des données depuis un serveur + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` utilise une session éphémère avec des délais d’attente courts, qui n’attend +pas la connexion. Il relance les réponses 5xx et 408, les délais dépassés et les erreurs hors +ligne (une fois par défaut ; modifiable avec une `RetryPolicy`), mais pas les réponses 4xx ni +les erreurs de décodage. + +## Démo + + + + Widgets météo, eau, activité physique, compte à rebours, citation et habitudes dessinés avec le paquet + + + + Vues statistiques avec sparkline, anneau de progression, graphique en courbes et graphique en barres + + + + + Widgets de l’écran verrouillé : batterie, anneaux d’activité, compte à rebours, météo et statistiques + + +Les images sont dessinées à partir des vues du paquet avec `ImageRenderer` par +[`scripts/render-screenshots.sh`](scripts/render-screenshots.sh). Comme elles sont dessinées en +dehors de WidgetKit, le cadre du widget, la vibrance de l’écran verrouillé et le mode nuit de +StandBy sont approximés et non produits par le système. + +Pour essayer les widgets vous-même, ouvrez +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) et lancez +le schéma **WidgetKitExample** dans un simulateur d’iPhone ; +[`Examples/README.md`](Examples/README.md) présente les trois widgets et explique comment les +ajouter à l’écran d’accueil. + +## Documentation + +Référence de l’API et articles : + +- [Référence de l’API](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/) + sur GitHub Pages, générée à partir des commentaires DocC du code source. +- Dans Xcode : **Product › Build Documentation**, ou + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Guides + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted) : + ajouter une extension de widget et le paquet, partager des données via un App Group, + construire la timeline et afficher une entrée. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets) : + configurer l’App Group, enregistrer les intents, ajouter des boutons et lire leur état dans + le fournisseur et dans l’app. + +## Prérequis + +| Plateforme | Version minimale | +| --------------- | ---------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| Chaîne d’outils | Swift 6.0 | + +Les vues de l’écran verrouillé et les Live Activities n’existent que sur iOS. La cible macOS +permet d’exécuter le code indépendant de la plateforme et ses tests avec `swift test`. La CI +compile avec le Xcode par défaut de l’image `macos-26` de GitHub ; le développement local +utilise Xcode 27. Les versions plus anciennes de Xcode n’ont pas été essayées. + +## Architecture + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +Les vues restent légères : les calculs qui peuvent mal tourner avec des données réelles +(objectifs nuls, `NaN`, plages de dates inversées, jours de changement d’heure) se trouvent dans +`Core` et `Timeline` sous forme de fonctions pures testées. Les types qui touchent au stockage +ou au réseau reçoivent leurs dépendances (`UserDefaults`, horloge, transport, attente) en +paramètres, ce qui permet de les tester sans App Group ni serveur. Les corps des vues SwiftUI +n’ont pas de tests d’instantanés. + +Lancez les tests sur macOS et dans un simulateur iOS : + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +Trois tests iOS sont ignorés volontairement : ils dessinent les images du README et ne +s’exécutent que via `scripts/render-screenshots.sh`. + +## Feuille de route + +- Taguer une version qui correspond au code actuel, pour pouvoir ajouter le paquet par + version. +- Des tests d’instantanés pour les corps des vues. +- Une Live Activity d’exemple dans l’app d’exemple. + +Le [CHANGELOG](CHANGELOG.md) consigne les changements jusqu’ici, y compris la façon dont le +paquet a été restauré en octobre 2026 après avoir constaté que le code de `v1.0.0` ne compilait +pas. + +## Contribuer + +Les contributions sont les bienvenues. Lisez le [guide de contribution](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) et le [code de conduite](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md) avant d’ouvrir une pull request. Signalez les problèmes de sécurité en privé, comme indiqué dans la [politique de sécurité](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md). + +## Licence + +Publié sous licence MIT. Voir [LICENSE](LICENSE). diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 0000000..238966f --- /dev/null +++ b/README.ja.md @@ -0,0 +1,314 @@ + + + +

iOS Widget Development Kit: WidgetKit ウィジェットと Live Activities のための SwiftUI ビューとタイムラインヘルパー。

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+CI ステータス +対応プラットフォーム: iOS 17+ · macOS 14+ +言語バージョン: Swift 6.0+ +ライセンス: MIT +

+ +

+はじめる +ドキュメント +サンプル +

+ +> このドキュメントは英語版 README の翻訳です。内容が異なる場合は英語版が最新です。 + + + +WidgetDevelopmentKit は、iOS アプリのウィジェットを開発する人のための Swift パッケージです。ホーム画面、ロック画面、StandBy のウィジェット向けの SwiftUI ビュー、ウィジェットのサイズに合わせたチャートビュー、タイムラインとアプリがウィジェットと共有するデータのためのヘルパー、ウィジェットのボタン用の App Intents、Live Activities 用の ActivityKit 属性とビューが含まれています。ウィジェット拡張そのものは自分で書きますが、ビューと、どのウィジェットにも必要な日付・ストレージ・リトライの処理はこのパッケージに任せられます。 + +## 主な特長 + +- **20 種類のホーム画面テンプレート** — 天気、統計、家計、カレンダー、カウントダウン、時計、メモ、名言、音楽、ポッドキャスト、ニュース、フィットネスリング、習慣トラッカー、ToDo、水分、バッテリーなど。 +- **ロック画面と StandBy** — 15 種類のアクセサリビュー(円形、長方形、インライン)と 7 種類の StandBy ビュー。 +- **タイムラインのヘルパー** — エントリの日時、再読み込みポリシー、有効期限付きのスナップショットストア。アプリが App Group 経由で保存したデータをウィジェットが読み取れます。 +- **インタラクティブウィジェット** — 5 つの App Intent と、それを実行するボタンを持つ 8 つのビュー。 +- **Live Activities** — ロック画面とコンパクトな Dynamic Island のビューを備えた 10 種類の `ActivityAttributes` 型と、順序が入れ替わった更新を拒否するステートマシン。 +- **テスト済みのロジック** — 日付・数値・リトライのコードは Swift Testing のテストでカバーされており、`NaN`、目標値ゼロ、夏時間の切り替え日などのエッジケースも含みます。 + +## クイックスタート + +**1. パッケージを追加する** + +アプリにはウィジェット拡張が必要です。まだない場合は、Xcode で **File › New › Target…** を選び、**Widget Extension** を選択します。次に **File › Add Package Dependencies…** を選び、右上の検索フィールドに次の URL を貼り付けて **Add Package** をクリックします。 + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +次のダイアログで、`WidgetDevelopmentKit` の **Add to Target** をウィジェット拡張に設定し、**Add Package** をクリックします。 + +**2. モジュールをインポートする** + +ウィジェット拡張の Swift ファイルの先頭に追加します。 + +```swift +import WidgetDevelopmentKit +``` + +**3. 使ってみる** + +このウィジェットを拡張に追加します。パッケージの `CountdownWidget` ビューで、ある日付までのカウントダウンを表示します。続いて、拡張の `WidgetBundle`(`@main` が付いた型)に `LaunchCountdownWidget()` を追加します。 + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +iPhone シミュレータでアプリを実行します。ホーム画面の空いている場所を長押しし、**Edit › Add Widget** をタップして、アプリを探して **Countdown** を追加します。同じコードは [`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift) にもあります。 + +## インストール + +### Swift Package Manager + +Xcode で **File › Add Package Dependencies…** を選び、リポジトリの URL を入力します。 + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +または `Package.swift` の `dependencies` に追加します。 + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +リリースは 2.0.0 から始まります。それ以前の `v1.0.0` タグは現在のコードより古く、ビルドできません。このパッケージはサードパーティの依存関係を持ちません。 + +## 使い方 + +このセクションの Swift のサンプルはすべて、CI がビルドする [`Examples/WidgetKitExample`](Examples/WidgetKitExample)、またはテストと一緒にコンパイルされる [`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift) から取ったものです。ここにあるコードブロックがそのコードと一致しなくなると、テストが失敗します。 + +### タイムラインプロバイダで共有データを読む + +`TimelineSchedule` がエントリの日時を作り、`TimelineBuilder` がエントリを並べ替えて `WidgetUpdatePolicy` を WidgetKit の再読み込みポリシーに変換し、`WidgetSnapshotStore` がアプリが共有 App Group に保存したデータを読み取ります。 + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### アプリでデータを保存する + +アプリは有効期限付きで値を書き込み、WidgetKit にウィジェットの再読み込みを依頼します。 + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +アプリと拡張の両方に、同じ識別子で **App Groups** の capability を追加してください。ストアは暗号化されない `UserDefaults` を使うため、秘密の情報は保存しないでください。 + +### エントリを表示する + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +時間によって変わる値を表示するビュー(`CountdownWidget`、`StandByCountdownWidget`、`LockScreenCountdownWidget`)は `referenceDate` を受け取ります。各エントリがそれぞれの時点を表示するように `entry.date` を渡してください。 + +### ウィジェットにボタンを追加する + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Intent はウィジェット拡張の中で実行され、`WidgetStateStore.shared` に書き込みます。このストアは Info.plist の `WidgetDevelopmentKitAppGroup` キーで指定された App Group を使います。アプリと拡張の両方にこのキーを追加し、パッケージの Intent を登録してください。 + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### サーバーからデータを読み込む + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` は、タイムアウトが短く接続を待たない ephemeral セッションを使います。5xx と 408 のレスポンス、タイムアウト、オフラインのエラーはリトライします(デフォルトは 1 回。`RetryPolicy` で変更できます)が、4xx のレスポンスとデコードエラーはリトライしません。 + +## デモ + + + + パッケージから描画した天気、水分、フィットネス、カウントダウン、名言、習慣のウィジェット + + + + 統計スパークライン、進捗リング、折れ線グラフ、棒グラフのビュー + + + + + ロック画面のバッテリー、アクティビティリング、カウントダウン、天気、統計のウィジェット + + +画像は [`scripts/render-screenshots.sh`](scripts/render-screenshots.sh) が `ImageRenderer` を使ってパッケージのビューから描画したものです。WidgetKit の外で描画しているため、ウィジェットの枠、ロック画面のバイブランシー、StandBy のナイトモードはシステムが生成したものではなく、近似です。 + +ウィジェットを自分で試すには、[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) を開き、**WidgetKitExample** スキームを iPhone シミュレータで実行してください。3 つのウィジェットとホーム画面への追加方法は [`Examples/README.md`](Examples/README.md) で説明しています。 + +## ドキュメント + +API リファレンスと解説記事: + +- GitHub Pages の [API リファレンス](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/)。ソースコードの DocC コメントから生成しています。 +- Xcode では **Product › Build Documentation**、またはコマンド `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'` で生成できます。 + +## ガイド + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted):ウィジェット拡張とパッケージの追加、App Group を使ったデータ共有、タイムラインの作成、エントリの描画。 +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets):App Group の設定、Intent の登録、ボタンの追加、プロバイダとアプリでのボタンの状態の読み取り。 + +## 動作要件 + +| プラットフォーム | 最小バージョン | +| ---------------- | -------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| ツールチェーン | Swift 6.0 | + +ロック画面のビューと Live Activities は iOS のみです。macOS のターゲットは、プラットフォームに依存しないコードとそのテストを `swift test` で実行するためにあります。CI は GitHub の `macos-26` イメージのデフォルトの Xcode でビルドし、ローカルの開発には Xcode 27 を使っています。それより古い Xcode では試していません。 + +## アーキテクチャ + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +ビューは薄く保っています。実際のデータで問題を起こしやすい計算(目標値ゼロ、`NaN`、逆転した日付範囲、夏時間の切り替え日)は、単体テスト付きの純粋関数として `Core` と `Timeline` に置いています。ストレージやネットワークを扱う型は依存(`UserDefaults`、時計、通信、待機)を引数で受け取るので、App Group やサーバーなしでテストできます。SwiftUI のビューの body にはスナップショットテストがありません。 + +macOS と iOS シミュレータでテストを実行します。 + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +3 つの iOS テストは意図的にスキップされます。README の画像を描画するためのもので、`scripts/render-screenshots.sh` からのみ実行されます。 + +## ロードマップ + +- バージョンを指定してパッケージを追加できるよう、現在のコードに対応するリリースのタグを付ける。 +- ビューの body のスナップショットテスト。 +- サンプルアプリに Live Activity のサンプルを追加する。 + +これまでの変更は [CHANGELOG](CHANGELOG.md) に記録しています。`v1.0.0` のコードがビルドできないと分かった後、2026 年 10 月にパッケージをどのように復元したかも含まれます。 + +## コントリビューション + +コントリビューションを歓迎します。pull request を作成する前に[コントリビューションガイド](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md)と[行動規範](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md)をお読みください。セキュリティ上の問題は[セキュリティポリシー](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md)に従って非公開で報告してください。 + +## ライセンス + +MIT ライセンスで公開しています。詳細は [LICENSE](LICENSE) を参照してください。 diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 0000000..adfd565 --- /dev/null +++ b/README.ko.md @@ -0,0 +1,314 @@ + + + +

iOS Widget Development Kit: WidgetKit 위젯과 Live Activities를 위한 SwiftUI 뷰와 타임라인 헬퍼.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+CI 상태 +지원 플랫폼: iOS 17+ · macOS 14+ +언어 버전: Swift 6.0+ +라이선스: MIT +

+ +

+시작하기 +문서 +예제 +

+ +> 이 문서는 영어 README의 번역본입니다. 내용이 다를 경우 영어 버전이 최신입니다. + + + +WidgetDevelopmentKit은 iOS 앱용 위젯을 만드는 개발자를 위한 Swift 패키지입니다. 홈 화면, 잠금 화면, StandBy 위젯을 위한 SwiftUI 뷰, 위젯 크기에 맞춘 차트 뷰, 타임라인과 앱이 위젯과 공유하는 데이터를 위한 헬퍼, 위젯 버튼용 App Intents, Live Activities용 ActivityKit 속성과 뷰가 들어 있습니다. 위젯 익스텐션은 직접 작성해야 하지만, 뷰와 모든 위젯에 필요한 날짜·저장·재시도 로직은 이 패키지가 대신합니다. + +## 주요 기능 + +- **홈 화면 템플릿 20개** — 날씨, 통계, 금융, 캘린더, 카운트다운, 시계, 메모, 명언, 음악, 팟캐스트, 뉴스, 피트니스 링, 습관 추적, 할 일, 물 섭취, 배터리 등. +- **잠금 화면과 StandBy** — 액세서리 뷰 15개(원형, 직사각형, 인라인)와 StandBy 뷰 7개. +- **타임라인 헬퍼** — 항목 날짜, 다시 로드 정책, 만료 시간이 있는 스냅숏 저장소. 위젯은 앱이 App Group을 통해 저장한 데이터를 읽습니다. +- **인터랙티브 위젯** — App Intent 5개와, 이를 실행하는 버튼이 있는 뷰 8개. +- **Live Activities** — 잠금 화면 뷰와 컴팩트 Dynamic Island 뷰를 갖춘 `ActivityAttributes` 타입 10개, 그리고 순서가 뒤바뀐 업데이트를 거부하는 상태 머신. +- **테스트된 로직** — 날짜, 숫자, 재시도 코드는 Swift Testing 테스트로 검증하며, `NaN`, 목표값 0, 일광 절약 시간 전환일 같은 경계 사례도 포함합니다. + +## 빠른 시작 + +**1. 패키지 추가** + +앱에 위젯 익스텐션이 있어야 합니다. 아직 없다면 Xcode에서 **File › New › Target…**을 선택하고 **Widget Extension**을 고르세요. 그런 다음 **File › Add Package Dependencies…**를 선택하고, 오른쪽 위 검색 필드에 이 URL을 붙여 넣은 뒤 **Add Package**를 클릭하세요. + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +다음 대화 상자에서 `WidgetDevelopmentKit`의 **Add to Target**을 위젯 익스텐션으로 지정하고 **Add Package**를 클릭하세요. + +**2. 모듈 가져오기** + +위젯 익스텐션에 있는 Swift 파일 맨 위에 추가하세요. + +```swift +import WidgetDevelopmentKit +``` + +**3. 사용하기** + +이 위젯을 익스텐션에 추가하세요. 패키지의 `CountdownWidget` 뷰로 특정 날짜까지 남은 시간을 보여 줍니다. 그다음 익스텐션의 `WidgetBundle`(`@main`이 붙은 타입)에 `LaunchCountdownWidget()`을 추가하세요. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +iPhone 시뮬레이터에서 앱을 실행하세요. 홈 화면의 빈 곳을 길게 누르고 **Edit › Add Widget**을 탭한 다음, 앱을 찾아 **Countdown**을 추가하세요. 같은 코드가 [`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift)에도 있습니다. + +## 설치 + +### Swift Package Manager + +Xcode에서 **File › Add Package Dependencies…**를 선택하고 저장소 URL을 입력하세요. + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +또는 `Package.swift`의 `dependencies`에 추가하세요. + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +릴리스는 2.0.0부터 시작합니다. 이전 태그인 `v1.0.0`은 현재 코드보다 오래되었고 빌드되지 않습니다. 이 패키지에는 서드파티 의존성이 없습니다. + +## 사용법 + +이 섹션의 모든 Swift 예제는 CI가 빌드하는 [`Examples/WidgetKitExample`](Examples/WidgetKitExample) 또는 테스트와 함께 컴파일되는 [`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift)에서 가져왔습니다. 여기 있는 코드 블록이 해당 코드와 달라지면 테스트가 실패합니다. + +### 타임라인 프로바이더에서 공유 데이터 읽기 + +`TimelineSchedule`은 항목 날짜를 만들고, `TimelineBuilder`는 항목을 정렬하고 `WidgetUpdatePolicy`를 WidgetKit의 다시 로드 정책으로 바꾸며, `WidgetSnapshotStore`는 앱이 공유 App Group에 저장한 데이터를 읽습니다. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### 앱에서 데이터 저장하기 + +앱은 유효 기간과 함께 값을 쓰고 WidgetKit에 위젯을 다시 로드하도록 요청합니다. + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +앱과 익스텐션에 같은 식별자로 **App Groups** capability를 추가하세요. 저장소는 암호화되지 않는 `UserDefaults`를 사용하므로 비밀 정보는 넣지 마세요. + +### 항목 표시하기 + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +시간에 따라 달라지는 값을 보여 주는 뷰(`CountdownWidget`, `StandByCountdownWidget`, `LockScreenCountdownWidget`)는 `referenceDate`를 받습니다. 각 항목이 자신의 시점을 그리도록 `entry.date`를 전달하세요. + +### 위젯에 버튼 추가하기 + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Intent는 위젯 익스텐션에서 실행되며 `WidgetStateStore.shared`에 기록합니다. 이 저장소는 Info.plist의 `WidgetDevelopmentKitAppGroup` 키로 지정한 App Group을 사용합니다. 앱과 익스텐션 모두에 이 키를 추가하고 패키지의 Intent를 등록하세요. + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### 서버에서 데이터 불러오기 + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader`는 타임아웃이 짧고 연결을 기다리지 않는 ephemeral 세션을 사용합니다. 5xx와 408 응답, 타임아웃, 오프라인 오류는 재시도하지만(기본 1회, `RetryPolicy`로 변경 가능) 4xx 응답과 디코딩 오류는 재시도하지 않습니다. + +## 데모 + + + + 패키지로 렌더링한 날씨, 물 섭취, 피트니스, 카운트다운, 명언, 습관 위젯 + + + + 통계 스파크라인, 진행률 링, 꺾은선 차트, 막대 차트 뷰 + + + + + 잠금 화면의 배터리, 활동 링, 카운트다운, 날씨, 통계 위젯 + + +이미지는 [`scripts/render-screenshots.sh`](scripts/render-screenshots.sh)가 `ImageRenderer`로 패키지의 뷰를 렌더링한 것입니다. WidgetKit 밖에서 그렸기 때문에 위젯 테두리, 잠금 화면의 바이브런시, StandBy 야간 모드는 시스템이 만든 것이 아니라 근사치입니다. + +위젯을 직접 사용해 보려면 [`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample)를 열고 iPhone 시뮬레이터에서 **WidgetKitExample** 스킴을 실행하세요. [`Examples/README.md`](Examples/README.md)에서 세 가지 위젯과 홈 화면에 추가하는 방법을 설명합니다. + +## 문서 + +API 레퍼런스와 문서: + +- GitHub Pages의 [API 레퍼런스](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/). 소스 코드의 DocC 주석으로 만듭니다. +- Xcode에서는 **Product › Build Documentation**을 선택하거나 `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`를 실행하세요. + +## 가이드 + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): 위젯 익스텐션과 패키지 추가, App Group을 통한 데이터 공유, 타임라인 구성, 항목 렌더링. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): App Group 설정, Intent 등록, 버튼 추가, 프로바이더와 앱에서 버튼 상태 읽기. + +## 요구 사항 + +| 플랫폼 | 최소 버전 | +| ------ | --------- | +| iOS | 17.0 | +| macOS | 14.0 | +| 툴체인 | Swift 6.0 | + +잠금 화면 뷰와 Live Activities는 iOS 전용입니다. macOS 타깃은 플랫폼과 무관한 코드와 그 테스트를 `swift test`로 실행하기 위해 있습니다. CI는 GitHub `macos-26` 이미지의 기본 Xcode로 빌드하며, 로컬 개발에는 Xcode 27을 사용합니다. 더 오래된 Xcode 버전에서는 시험해 보지 않았습니다. + +## 아키텍처 + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +뷰는 얇게 유지합니다. 실제 데이터에서 잘못되기 쉬운 계산(목표값 0, `NaN`, 뒤집힌 날짜 범위, 일광 절약 시간 전환일)은 단위 테스트가 있는 순수 함수로 `Core`와 `Timeline`에 둡니다. 저장소나 네트워크를 다루는 타입은 의존성(`UserDefaults`, 시계, 전송 계층, 대기)을 매개변수로 받으므로 App Group이나 서버 없이 테스트할 수 있습니다. SwiftUI 뷰의 body는 스냅숏 테스트를 하지 않습니다. + +macOS와 iOS 시뮬레이터에서 테스트를 실행하세요. + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +iOS 테스트 3개는 의도적으로 건너뜁니다. README 이미지를 렌더링하는 테스트로, `scripts/render-screenshots.sh`를 통해서만 실행됩니다. + +## 로드맵 + +- 버전으로 패키지를 추가할 수 있도록 현재 코드에 맞는 릴리스 태그 만들기. +- 뷰 body에 대한 스냅숏 테스트. +- 예제 앱에 Live Activity 예제 추가. + +지금까지의 변경 사항은 [CHANGELOG](CHANGELOG.md)에 기록되어 있습니다. `v1.0.0` 코드가 빌드되지 않는다는 사실이 확인된 뒤 2026년 10월에 패키지를 어떻게 복원했는지도 포함됩니다. + +## 기여하기 + +기여를 환영합니다. pull request를 열기 전에 [기여 가이드](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md)와 [행동 강령](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md)을 읽어 주세요. 보안 문제는 [보안 정책](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md)에 따라 비공개로 제보해 주세요. + +## 라이선스 + +MIT 라이선스로 배포됩니다. 자세한 내용은 [LICENSE](LICENSE)를 참고하세요. diff --git a/README.md b/README.md index 940c840..a6ec189 100644 --- a/README.md +++ b/README.md @@ -1,734 +1,362 @@ -

- Swift 6.0 - Platform - Standard -

- ---- - -> **🛡️ PART OF THE 2026 UNIFIED CORE** -> This repository is a verified component of 'The Endless March' initiative. Purified for Swift 6, zero-dependency, and engineered for maximum hardware saturation. -> -> *Flagship Engines:* [SwiftNetwork](https://github.com/muhittincamdali/SwiftNetwork) | [SwiftAI](https://github.com/muhittincamdali/SwiftAI) | [LiquidGlassKit](https://github.com/muhittincamdali/LiquidGlassKit) + ---- - -``` -╔══════════════════════════════════════════════════════════════════════════════════════╗ -║ ║ -║ ██╗ ██╗██╗██████╗ ██████╗ ███████╗████████╗ ██╗ ██╗██╗████████╗ ║ -║ ██║ ██║██║██╔══██╗██╔════╝ ██╔════╝╚══██╔══╝ ██║ ██╔╝██║╚══██╔══╝ ║ -║ ██║ █╗ ██║██║██║ ██║██║ ███╗█████╗ ██║ █████╔╝ ██║ ██║ ║ -║ ██║███╗██║██║██║ ██║██║ ██║██╔══╝ ██║ ██╔═██╗ ██║ ██║ ║ -║ ╚███╔███╔╝██║██████╔╝╚██████╔╝███████╗ ██║ ██║ ██╗██║ ██║ ║ -║ ╚══╝╚══╝ ╚═╝╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║ -║ ║ -║ ████████████████████████████████████████████████████████████████████████████████ ║ -║ █ The Ultimate iOS Widget Development Framework — 50+ Templates Included! █ ║ -║ ████████████████████████████████████████████████████████████████████████████████ ║ -║ ║ -╚══════════════════════════════════════════════════════════════════════════════════════╝ -``` +

iOS Widget Development Kit: SwiftUI views and timeline helpers for WidgetKit widgets and Live Activities.

-
- -**The most comprehensive iOS widget development kit with 50+ production-ready templates, Live Activities, Dynamic Island, Interactive Widgets, and StandBy mode support.** - -[![Swift](https://img.shields.io/badge/Swift-5.9+-F05138?style=for-the-badge&logo=swift&logoColor=white)](https://swift.org) -[![iOS](https://img.shields.io/badge/iOS-16.0+-000000?style=for-the-badge&logo=apple&logoColor=white)](https://developer.apple.com/ios/) -[![WidgetKit](https://img.shields.io/badge/WidgetKit-Ready-007AFF?style=for-the-badge&logo=apple&logoColor=white)](https://developer.apple.com/widgets/) -[![SPM](https://img.shields.io/badge/SPM-Compatible-FA7343?style=for-the-badge&logo=swift&logoColor=white)](https://swift.org/package-manager/) -[![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](LICENSE) - -[Features](#-features) • [Templates](#-widget-templates-50) • [Live Activities](#-live-activities) • [Interactive](#-interactive-widgets) • [StandBy](#-standby-mode) • [Charts](#-charts-in-widgets) • [Docs](Documentation/) - ---- - -### 🏆 Why Choose This Kit? - -| Feature | This Kit | Others | -|---------|:--------:|:------:| -| Widget Templates | **50+** | 5-10 | -| Live Activity Templates | **15** | 2-3 | -| Interactive Widgets | ✅ | ❌ | -| StandBy Mode | ✅ | ❌ | -| Dynamic Island | ✅ | Partial | -| Lock Screen Widgets | **15** | 3-5 | -| SwiftUI Charts | ✅ | ❌ | -| Network Data Support | ✅ | Basic | -| Timeline Management | **Advanced** | Basic | -| Deep Linking | ✅ | ❌ | -| Production Ready | ✅ | ❌ | - -
- ---- - -## 📋 Table of Contents - -- [Features](#-features) -- [Widget Templates (50+)](#-widget-templates-50) -- [Live Activities](#-live-activities) -- [Interactive Widgets](#-interactive-widgets) -- [StandBy Mode](#-standby-mode) -- [Lock Screen Widgets](#-lock-screen-widgets) -- [Charts in Widgets](#-charts-in-widgets) -- [Timeline Management](#-timeline-management) -- [Network Data Support](#-network-data-support) -- [Deep Linking](#-deep-linking) -- [Installation](#-installation) -- [Quick Start](#-quick-start) -- [Documentation](#-documentation) - ---- - -## 🚀 Killer Feature: The 2026 Standard -This repository has been upgraded to the absolute global #1 standard in its category. - -## ✨ Features - -### 🖼️ The Ultimate Widget Gallery (The 2026 Standard) -Why build from scratch? Drop in our high-conversion, pre-built widget templates directly into your app. Includes: -- **Crypto Widgets:** Real-time sparklines and percentage trends. -- **Fitness & Health:** Activity rings, step counters, and heart rate graphs. -- **E-Commerce:** Flash sale timers and order tracking. -- **Productivity:** Interactive habit trackers and to-do lists. - -### Widget Types -- 🏠 **Home Screen Widgets** — Small, Medium, Large, Extra Large -- 🔒 **Lock Screen Widgets** — Circular, Rectangular, Inline -- ⚡ **Live Activities** — Real-time updates on lock screen -- 🏝️ **Dynamic Island** — Compact and expanded views -- 📺 **StandBy Mode** — Full-screen widgets (iOS 17+) -- 🖱️ **Interactive Widgets** — Buttons, toggles, counters (iOS 17+) - -### Advanced Features -- 📊 **SwiftUI Charts** — Bar, line, pie, sparkline charts -- 🌐 **Network Data** — Fetch and cache remote data -- ⏱️ **Smart Timeline** — Battery-aware refresh strategies -- 🔗 **Deep Linking** — Open specific app content -- ⚙️ **Configuration** — User-customizable widgets -- 🎨 **Theming** — Custom colors and styles - ---- - -## 🎨 Widget Templates (50+) - -### Home Screen Widgets (20) - -| Template | Description | Size | -|----------|-------------|------| -| `GradientCardWidget` | Beautiful gradient cards | S/M/L | -| `StatsWidget` | Statistics with progress bars | M/L | -| `CalendarWidget` | Upcoming events display | M/L | -| `WeatherWidget` | Weather conditions | S/M/L | -| `CountdownWidget` | Event countdown timer | S/M | -| `QuoteWidget` | Inspirational quotes | M/L | -| `MusicWidget` | Now playing info | M | -| `FitnessWidget` | Activity rings | S/M | -| `HabitTrackerWidget` | Habit tracking streaks | M/L | -| `FinanceWidget` | Account balances | M/L | -| `PhotoFrameWidget` | Photo display | S/M/L | -| `NotesWidget` | Quick notes preview | M/L | -| `BatteryWidget` | Device battery status | S/M | -| `ShortcutsWidget` | Quick action grid | M | -| `SocialStatsWidget` | Social media stats | M | -| `ClockWidget` | Analog/Digital clock | S/M | -| `TodoWidget` | Task list | M/L | -| `PodcastWidget` | Podcast player | M | -| `NewsWidget` | News headlines | M/L | -| `WaterTrackerWidget` | Water intake | S/M | - -### Usage Example + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ -```swift -import SwiftUI -import WidgetKit -import iOSWidgetKit +

+CI status +Supported platforms: iOS 17+ · macOS 14+ +Language version: Swift 6.0+ +License: MIT +

-struct MyWidgetView: View { - var entry: SimpleEntry - - var body: some View { - WeatherWidget( - temperature: 72, - condition: .sunny, - location: "San Francisco", - high: 78, - low: 65 - ) - } -} -``` +

+Get started +Documentation +Examples +

---- + -## ⚡ Live Activities +WidgetDevelopmentKit is a Swift package for developers who build widgets for their iOS apps. +It contains ready-made SwiftUI views for Home Screen, Lock Screen and StandBy widgets, chart +views sized for widgets, helpers for timelines and for the data the app shares with its +widget, App Intents for widget buttons, and ActivityKit attributes and views for Live +Activities. You still write the widget extension; the package saves you the views and the +date, storage and retry logic that every widget needs. -### 10 Live Activity Templates +## Highlights -| Template | Use Case | -|----------|----------| -| `DeliveryActivity` | Food/package delivery | -| `RideActivity` | Uber/Lyft style rides | -| `SportsScoreActivity` | Live sports scores | -| `TimerActivity` | Countdown timer | -| `WorkoutActivity` | Fitness tracking | -| `NowPlayingActivity` | Music player | -| `FlightActivity` | Flight tracking | -| `ParkingActivity` | Parking meter | -| `OrderActivity` | Order status | -| `MeetingActivity` | Calendar meetings | +- **20 Home Screen templates** — weather, stats, finance, calendar, countdown, clock, notes, + quote, music, podcast, news, fitness rings, habit tracker, to-do, water, battery and more. +- **Lock Screen and StandBy** — 15 accessory views (circular, rectangular, inline) and 7 + StandBy views. +- **Timeline helpers** — entry dates, reload policies and a snapshot store with expiry, so + the widget reads what the app saved through an App Group. +- **Interactive widgets** — 5 App Intents and 8 views with buttons that run them. +- **Live Activities** — 10 `ActivityAttributes` types with Lock Screen and compact Dynamic + Island views, and a state machine that rejects out-of-order updates. +- **Tested logic** — the date, number and retry code is covered by Swift Testing tests, + including edge cases such as `NaN`, zero goals and daylight-saving days. -### Create a Live Activity +## Quick start -```swift -import ActivityKit -import iOSWidgetKit - -// Define your activity -let attributes = DeliveryAttributes( - orderNumber: "12345", - restaurantName: "Pizza Place" -) - -let state = DeliveryAttributes.ContentState( - status: .onTheWay, - estimatedArrival: Date().addingTimeInterval(1800), - driverName: "John", - currentStep: 2 -) - -// Start the activity -let activity = try LiveActivityManager.shared.startActivity( - attributes: attributes, - contentState: state -) - -// Update -await LiveActivityManager.shared.updateActivity( - activity, - with: DeliveryAttributes.ContentState( - status: .arriving, - estimatedArrival: Date().addingTimeInterval(300), - driverName: "John", - currentStep: 3 - ) -) - -// End -await LiveActivityManager.shared.endActivity(activity) -``` +**1. Add the package** -### Dynamic Island Views +Your app needs a widget extension. If it has none yet, choose **File › New › Target…** in +Xcode and pick **Widget Extension**. Then choose **File › Add Package Dependencies…**, paste +this URL into the search field at the top right and choose **Add Package**: -```swift -struct DeliveryActivityConfiguration: Widget { - var body: some WidgetConfiguration { - ActivityConfiguration(for: DeliveryAttributes.self) { context in - // Lock screen view - DeliveryActivityView(context: context) - } dynamicIsland: { context in - DynamicIsland { - // Expanded view - DynamicIslandExpandedRegion(.leading) { - Image(systemName: context.state.status.icon) - } - DynamicIslandExpandedRegion(.trailing) { - Text(context.state.estimatedArrival, style: .timer) - } - DynamicIslandExpandedRegion(.bottom) { - ProgressView(value: Double(context.state.currentStep), total: 4) - } - } compactLeading: { - Image(systemName: context.state.status.icon) - } compactTrailing: { - Text(context.state.estimatedArrival, style: .timer) - } minimal: { - Image(systemName: context.state.status.icon) - } - } - } -} +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git ``` ---- +In the next dialog, set **Add to Target** for `WidgetDevelopmentKit` to your widget +extension and choose **Add Package**. -## 🖱️ Interactive Widgets +**2. Import the module** -**iOS 17+ Only** — Add buttons, toggles, and interactive elements. +At the top of a Swift file in the widget extension: -### Available Interactive Templates +```swift +import WidgetDevelopmentKit +``` -- `InteractiveToggleWidget` — Toggle switches -- `InteractiveCounterWidget` — Increment/decrement counters -- `InteractiveTaskListWidget` — Task completion toggles -- `InteractiveQuickActionsWidget` — Action buttons grid -- `InteractiveTimerWidget` — Timer controls -- `InteractiveHabitWidget` — Habit checkmarks -- `InteractiveRatingWidget` — Star ratings -- `InteractiveWaterWidget` — Water intake buttons +**3. Use it** -### Example +Add this widget to the extension. It counts down to a date with the package's +`CountdownWidget` view. Then list `LaunchCountdownWidget()` in the extension's +`WidgetBundle`, the type marked `@main`. ```swift -import AppIntents -import iOSWidgetKit - -// Interactive counter widget -struct MyInteractiveWidget: View { - let counterId = "daily_counter" - @AppStorage("counter_daily_counter", store: UserDefaults(suiteName: "group.widget")) - var count = 0 - - var body: some View { - InteractiveCounterWidget( - title: "Daily Count", - counterId: counterId, - currentValue: count, - accentColor: .blue - ) - } +struct LaunchEntry: TimelineEntry { + let date: Date } -// Task list with toggles -struct TaskWidget: View { - var body: some View { - InteractiveTaskListWidget( - tasks: [ - .init(id: "1", title: "Morning workout", isCompleted: true, priority: .high), - .init(id: "2", title: "Read 30 minutes", isCompleted: false, priority: .medium), - .init(id: "3", title: "Call mom", isCompleted: false, priority: .low) - ], - listTitle: "Today's Tasks" - ) +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) } -} -``` - ---- - -## 📺 StandBy Mode - -**iOS 17+ Only** — Full-screen widgets for StandBy mode on iPhone. - -### StandBy Templates - -| Template | Description | -|----------|-------------| -| `StandByClockWidget` | Digital/Analog clocks | -| `StandByPhotoWidget` | Photo frames | -| `StandByWeatherWidget` | Weather display | -| `StandByCalendarWidget` | Calendar view | -| `StandByFitnessWidget` | Activity rings | -| `StandByMusicWidget` | Now playing | -| `StandByCountdownWidget` | Event countdown | -### Example - -```swift -import iOSWidgetKit + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } -struct MyStandByWidget: View { - var body: some View { - StandByClockWidget( - date: Date(), - style: .flipClock, - showDate: true, - accentColor: .white - ) + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) } } -// Apply StandBy optimizations -struct OptimizedWidget: View { - var body: some View { - VStack { - Text("Hello") +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) } - .standByOptimized(nightModeColor: .red) + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) } } ``` ---- +Run the app on an iPhone simulator. On the Home Screen, touch and hold an empty area, tap +**Edit › Add Widget**, find your app and add **Countdown**. The same code is in +[`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift). -## 🔒 Lock Screen Widgets +## Installation -### 15 Lock Screen Templates - -**Circular Widgets:** -- `CircularProgressWidget` — Progress gauge -- `CircularIconWidget` — Simple icon -- `CircularRingWidget` — Activity rings - -**Rectangular Widgets:** -- `RectangularTextWidget` — Multi-line text -- `RectangularStatsWidget` — Statistics -- `RectangularProgressWidget` — Progress bar -- `RectangularCalendarWidget` — Events -- `RectangularWeatherWidget` — Weather +### Swift Package Manager -**Inline Widgets:** -- `InlineTextWidget` — Text with icon -- `InlineStatsWidget` — Quick stats +In Xcode, choose **File › Add Package Dependencies…** and enter the repository URL: -**Multi-Family Widgets:** -- `LockScreenBatteryWidget` — Battery (all sizes) -- `LockScreenTimerWidget` — Timer (all sizes) -- `LockScreenFitnessWidget` — Fitness (all sizes) -- `LockScreenCountdownWidget` — Countdown (all sizes) -- `LockScreenQuickActionWidget` — Actions (all sizes) +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` -### Example +Or add it to the `dependencies` in your `Package.swift`: ```swift -struct MyLockScreenWidget: Widget { - var body: some WidgetConfiguration { - StaticConfiguration(kind: "LockScreen", provider: Provider()) { entry in - // Automatically adapt to family - switch entry.family { - case .accessoryCircular: - CircularProgressWidget( - progress: 0.7, - icon: "star.fill", - accentColor: .blue - ) - case .accessoryRectangular: - RectangularStatsWidget( - stats: [("Steps", "8,234"), ("Cal", "450"), ("Dist", "4.2mi")], - title: "Today" - ) - case .accessoryInline: - InlineTextWidget(icon: "flame.fill", text: "450 calories burned") - default: - EmptyView() - } - } - .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline]) - } -} +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] ``` ---- +Releases start at 2.0.0; the earlier `v1.0.0` tag predates the current code and does not +build. The package has no third-party dependencies. -## 📊 Charts in Widgets +## Usage -SwiftUI Charts integration for beautiful data visualization. +Every Swift example in this section is taken from +[`Examples/WidgetKitExample`](Examples/WidgetKitExample), which CI builds, or from +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift), +which is compiled with the tests. A test fails if a block here no longer matches that code. -### Available Chart Types +### Read shared data in a timeline provider -| Chart | Description | -|-------|-------------| -| `WidgetBarChart` | Vertical bar chart | -| `WidgetLineChart` | Line chart with area | -| `WidgetSparkline` | Minimal sparkline | -| `WidgetPieChart` | Pie/donut chart | -| `WidgetProgressRing` | Circular progress | -| `WidgetMultiLineChart` | Multiple series | -| `WidgetHorizontalBarChart` | Horizontal bars | -| `WidgetActivityChart` | GitHub-style activity | -| `WidgetStatsChart` | Stats with sparkline | - -### Example +`TimelineSchedule` produces the entry dates, `TimelineBuilder` sorts the entries and turns a +`WidgetUpdatePolicy` into WidgetKit's reload policy, and `WidgetSnapshotStore` reads what the +app saved to the shared App Group. ```swift -import iOSWidgetKit -import Charts +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) -struct ChartWidget: View { - var body: some View { - VStack { - // Sparkline - WidgetSparkline( - values: [10, 25, 15, 30, 22, 35, 28], - color: .blue, - showGradient: true - ) - .frame(height: 50) - - // Stats with chart - WidgetStatsChart( - title: "Revenue", - value: "$12,450", - change: 12.5, - sparklineData: [100, 120, 110, 145, 160, 155, 180], - accentColor: .green - ) - - // Horizontal bar comparison - WidgetHorizontalBarChart( - data: [ - .init(label: "Mon", value: 45), - .init(label: "Tue", value: 62), - .init(label: "Wed", value: 38) - ], - accentColor: .purple - ) - } + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) } -} -``` - ---- - -## ⏱️ Timeline Management -Advanced timeline management for optimal battery and freshness. - -### Features - -- **Smart Refresh Strategies** — Time-based, battery-aware, content-aware -- **Caching** — Built-in cache layer -- **Timeline Builder** — Fluent API -- **Metrics** — Performance tracking + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } -### Example + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } -```swift -import iOSWidgetKit - -// Use Timeline Builder -let timeline = TimelineBuilder() - .addEntry(MyEntry(date: Date(), data: currentData)) - .addEntry(MyEntry(date: Date().addingTimeInterval(3600), data: currentData)) - .reloadAfter(1800) // 30 minutes - .build() - -// Battery-aware refresh -let strategy = BatteryAwareRefreshStrategy( - normalInterval: 900, // 15 min normally - lowBatteryInterval: 3600, // 1 hour on low battery - lowBatteryThreshold: 0.2 -) - -// Use cache -TimelineCacheManager.shared.store(myData, forKey: "widget_data", expiration: 300) -if let cached: MyData = TimelineCacheManager.shared.retrieve(MyData.self, forKey: "widget_data") { - // Use cached data + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } } ``` ---- - -## 🌐 Network Data Support +### Save data in the app -Fetch and display remote data in widgets. +The app writes the value with a lifetime and asks WidgetKit to reload the widget: ```swift -import iOSWidgetKit - -// Simple fetch -let data = try await NetworkDataProvider.shared.fetch( - WeatherResponse.self, - from: URL(string: "https://api.weather.com/data")! -) - -// With caching -let provider = CachedNetworkProvider( - cacheKey: "weather", - cacheDuration: 600 -) -let weather = try await provider.fetch(from: url) - -// Background refresh -BackgroundRefreshCoordinator.shared.scheduleBackgroundRefresh( - identifier: "weather_widget", - afterInterval: 1800 -) +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") ``` ---- +Add the **App Groups** capability to the app and the extension with the same identifier. +The store uses `UserDefaults`, which is not encrypted, so keep secrets out of it. -## 🔗 Deep Linking - -Connect widget taps to specific app content. +### Show an entry ```swift -import iOSWidgetKit - -// Build deep link URL -let url = WidgetDeepLinkManager.shared.buildContentURL( - contentType: "task", - contentId: "123", - parameters: ["source": "widget"] -) - -// In widget view -Link(destination: url!) { - TaskRow(task: task) -} +struct StepsWidgetView: View { + let entry: StepsEntry -// Handle in app -@main -struct MyApp: App { - var body: some Scene { - WindowGroup { - ContentView() - .onWidgetURL { url in - let params = WidgetDeepLinkManager.shared.parseParameters(from: url) - // Navigate to content - } - } + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) } } ``` ---- - -## 📦 Installation +Views that show time-dependent values (`CountdownWidget`, `StandByCountdownWidget`, +`LockScreenCountdownWidget`) take a `referenceDate`; pass `entry.date` so each entry renders +its own moment. -### Swift Package Manager +### Add a button to a widget ```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") -] -``` - -### CocoaPods - -```ruby -pod 'WidgetDevelopmentKit', '~> 2.0' -``` - ---- - -## 🚀 Quick Start - -### 1. Add Widget Extension - -File → New → Target → Widget Extension - -### 2. Import the Kit - -```swift -import WidgetKit -import SwiftUI -import iOSWidgetKit +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} ``` -### 3. Create Your Widget +The intents run in the widget extension and write to `WidgetStateStore.shared`, which uses +the App Group named by the `WidgetDevelopmentKitAppGroup` key in Info.plist. Add the key to +both the app and the extension, and register the package's intents: ```swift -struct MyWidget: Widget { - let kind: String = "MyWidget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: MyProvider()) { entry in - WeatherWidget( - temperature: entry.temperature, - condition: entry.condition, - location: entry.location, - high: entry.high, - low: entry.low - ) - } - .configurationDisplayName("Weather") - .description("Shows current weather.") - .supportedFamilies([.systemSmall, .systemMedium]) +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] } } ``` ---- +### Load data from a server -## 📁 Project Structure +```swift +struct RandomQuote: Decodable { + let text: String +} +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} ``` -iOS-Widget-Development-Kit/ -├── Sources/ -│ ├── Templates/ # 20+ Home Screen widget templates -│ ├── LiveActivities/ # 10+ Live Activity templates -│ ├── Interactive/ # iOS 17 interactive widgets -│ ├── StandBy/ # StandBy mode widgets -│ ├── LockScreen/ # 15 Lock Screen widgets -│ ├── Charts/ # SwiftUI Charts for widgets -│ ├── Core/ # Timeline management -│ ├── Network/ # Network data support -│ └── Configuration/ # Deep linking & config -├── Examples/ # Sample implementations -├── Tests/ # Comprehensive tests -└── Documentation/ # Guides & tutorials -``` - ---- -## 📋 Requirements - -| Requirement | Minimum | -|-------------|---------| -| iOS | 16.0+ | -| Xcode | 15.0+ | -| Swift | 5.9+ | - -**Feature Requirements:** -| Feature | iOS Version | -|---------|-------------| -| Home Screen Widgets | 14.0+ | -| Lock Screen Widgets | 16.0+ | -| Live Activities | 16.1+ | -| Dynamic Island | 16.1+ | -| Interactive Widgets | 17.0+ | -| StandBy Mode | 17.0+ | - ---- - -## 📖 Documentation - -| Guide | Description | -|-------|-------------| -| [Widget Design Guide](Documentation/WidgetDesign.md) | Design best practices | -| [Timeline Management](Documentation/TimelineManagement.md) | Refresh strategies | -| [Live Activities Guide](Documentation/LiveActivities.md) | Dynamic Island setup | -| [Interactive Widgets](Documentation/InteractiveWidgets.md) | iOS 17 interactions | -| [StandBy Mode](Documentation/StandByMode.md) | Full-screen widgets | -| [Network Data](Documentation/NetworkData.md) | Remote data fetching | -| [Best Practices](Documentation/BestPractices.md) | Performance tips | - ---- - -## 🤝 Contributing - -Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md). - -```bash -git checkout -b feature/amazing-widget -git commit -m "feat(widgets): add amazing widget template" -git push origin feature/amazing-widget +`WidgetDataLoader` uses an ephemeral session with short timeouts that does not wait for +connectivity. It retries 5xx and 408 responses, timeouts and offline errors (once by +default; set a `RetryPolicy` to change it), but not 4xx responses or decoding errors. + +## Demo + + + + Weather, water, fitness, countdown, quote and habit widgets rendered from the package + + + + Stats sparkline, progress ring, line chart and bar chart views + + + + + Lock Screen battery, activity rings, countdown, weather and stats widgets + + +The images are rendered from the package's views with `ImageRenderer` by +[`scripts/render-screenshots.sh`](scripts/render-screenshots.sh). They are drawn outside +WidgetKit, so the widget frame, Lock Screen vibrancy and StandBy night mode are approximated +rather than produced by the system. + +To try the widgets yourself, open +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) and run +the **WidgetKitExample** scheme on an iPhone simulator; [`Examples/README.md`](Examples/README.md) +explains the three widgets and how to add them to the Home Screen. + +## Documentation + +Reference documentation and articles: + +- [API reference](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/) + on GitHub Pages, built from the DocC comments in the source. +- In Xcode: **Product › Build Documentation**, or + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Guides + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): + add a widget extension and the package, share data through an App Group, build the + timeline and render an entry. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): + configure the App Group, register the intents, add buttons and read their state in the + provider and the app. + +## Requirements + +| Platform | Minimum version | +| --------- | --------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| Toolchain | Swift 6.0 | + +Lock Screen views and Live Activities are iOS only. The macOS target exists so the +platform-independent code and its tests run with `swift test`. CI builds with the default +Xcode of GitHub's `macos-26` image; local development uses Xcode 27. Older Xcode versions have +not been tried. + +## Architecture + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ ``` ---- - -## 📄 License +Views stay thin: arithmetic that can go wrong with real data (zero goals, `NaN`, reversed +date ranges, daylight-saving days) lives in `Core` and `Timeline` as pure functions with unit +tests. Types that touch storage or the network take their dependencies (`UserDefaults`, +clock, transport, sleep) as parameters, so they can be tested without an App Group or a +server. SwiftUI view bodies are not snapshot-tested. -MIT License — see [LICENSE](LICENSE) for details. +Run the tests on macOS and on an iOS simulator: ---- - -
- -## 👨‍💻 Author +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` -**Muhittin Camdali** +Three iOS tests are skipped on purpose: they render the README images and run only through +`scripts/render-screenshots.sh`. -[![GitHub](https://img.shields.io/badge/GitHub-muhittincamdali-181717?style=for-the-badge&logo=github)](https://github.com/muhittincamdali) -[![LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-0A66C2?style=for-the-badge&logo=linkedin)](https://linkedin.com/in/muhittincamdali) +## Roadmap ---- +- Tag a release that matches the current code, so the package can be added by version. +- Snapshot tests for the view bodies. +- An example Live Activity in the example app. -**⭐ Star this repo if you find it useful!** +The [CHANGELOG](CHANGELOG.md) records what changed so far, including how the package was +restored in October 2026 after the `v1.0.0` code turned out not to build. ---- +## Contributing -## 📈 Star History +Contributions are welcome. Read the [contributing guide](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) and the [code of conduct](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md) before opening a pull request. Report security issues privately as described in the [security policy](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md). - - - - - Star History Chart - - +## License -
+Released under the MIT license. See [LICENSE](LICENSE). diff --git a/README.pt-BR.md b/README.pt-BR.md new file mode 100644 index 0000000..07704e1 --- /dev/null +++ b/README.pt-BR.md @@ -0,0 +1,373 @@ + + + +

iOS Widget Development Kit: Views SwiftUI e utilitários de timeline para widgets do WidgetKit e Live Activities.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+Status da CI +Plataformas compatíveis: iOS 17+ · macOS 14+ +Versão da linguagem: Swift 6.0+ +Licença: MIT +

+ +

+Começar +Documentação +Exemplos +

+ +> Esta é uma tradução do README em inglês. Se houver diferença, vale a versão em inglês. + + + +WidgetDevelopmentKit é um pacote Swift para quem cria widgets para apps iOS. Ele traz views +SwiftUI prontas para widgets da Tela de Início, da Tela Bloqueada e do StandBy, views de +gráficos no tamanho de um widget, utilitários para timelines e para os dados que o app +compartilha com o widget, App Intents para os botões dos widgets, além de atributos e views do +ActivityKit para Live Activities. Você continua escrevendo a extensão de widget; o pacote +poupa o trabalho das views e da lógica de datas, armazenamento e novas tentativas de que todo +widget precisa. + +## Destaques + +- **20 modelos para a Tela de Início** — clima, estatísticas, finanças, calendário, contagem + regressiva, relógio, notas, citação, música, podcast, notícias, anéis de atividade, + acompanhamento de hábitos, tarefas, água, bateria e mais. +- **Tela Bloqueada e StandBy** — 15 views de acessório (circular, retangular, em linha) e 7 + views de StandBy. +- **Utilitários de timeline** — datas das entradas, políticas de recarga e um armazenamento de + snapshots com validade, para que o widget leia o que o app salvou por meio de um App Group. +- **Widgets interativos** — 5 App Intents e 8 views com botões que os executam. +- **Live Activities** — 10 tipos de `ActivityAttributes` com views para a Tela Bloqueada e a + Dynamic Island compacta, e uma máquina de estados que rejeita atualizações fora de ordem. +- **Lógica testada** — o código de datas, números e novas tentativas é coberto por testes do + Swift Testing, incluindo casos extremos como `NaN`, metas iguais a zero e dias de mudança de + horário de verão. + +## Início rápido + +**1. Adicione o pacote** + +Seu app precisa de uma extensão de widget. Se ainda não tiver uma, escolha **File › New › +Target…** no Xcode e selecione **Widget Extension**. Depois escolha **File › Add Package +Dependencies…**, cole esta URL no campo de busca no canto superior direito e clique em **Add +Package**: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Na janela seguinte, defina **Add to Target** de `WidgetDevelopmentKit` como sua extensão de +widget e clique em **Add Package**. + +**2. Importe o módulo** + +No início de um arquivo Swift da extensão de widget: + +```swift +import WidgetDevelopmentKit +``` + +**3. Use** + +Adicione este widget à extensão. Ele faz uma contagem regressiva até uma data com a view +`CountdownWidget` do pacote. Em seguida, inclua `LaunchCountdownWidget()` no `WidgetBundle` da +extensão, o tipo marcado com `@main`. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +Execute o app em um simulador de iPhone. Na Tela de Início, toque e segure uma área vazia, +toque em **Edit › Add Widget**, encontre seu app e adicione **Countdown**. O mesmo código está +em [`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift). + +## Instalação + +### Swift Package Manager + +No Xcode, escolha **File › Add Package Dependencies…** e informe a URL do repositório: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Ou adicione-o às `dependencies` do seu `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +As versões começam em 2.0.0; a tag anterior, `v1.0.0`, é anterior ao código atual e não +compila. O pacote não tem dependências de terceiros. + +## Uso + +Todos os exemplos em Swift desta seção vêm de +[`Examples/WidgetKitExample`](Examples/WidgetKitExample), que a CI compila, ou de +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift), que é +compilado com os testes. Um teste falha se um bloco daqui deixar de corresponder a esse código. + +### Ler dados compartilhados em um provedor de timeline + +`TimelineSchedule` gera as datas das entradas, `TimelineBuilder` ordena as entradas e converte +uma `WidgetUpdatePolicy` na política de recarga do WidgetKit, e `WidgetSnapshotStore` lê o que +o app salvou no App Group compartilhado. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### Salvar dados no app + +O app grava o valor com um prazo de validade e pede ao WidgetKit que recarregue o widget: + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +Adicione a capability **App Groups** ao app e à extensão com o mesmo identificador. O +armazenamento usa `UserDefaults`, que não é criptografado; não guarde segredos nele. + +### Mostrar uma entrada + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Views que mostram valores que dependem do tempo (`CountdownWidget`, `StandByCountdownWidget`, +`LockScreenCountdownWidget`) recebem um `referenceDate`; passe `entry.date` para que cada +entrada mostre o próprio momento. + +### Adicionar um botão a um widget + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Os intents rodam na extensão de widget e gravam em `WidgetStateStore.shared`, que usa o App +Group indicado na chave `WidgetDevelopmentKitAppGroup` do Info.plist. Adicione a chave ao app e +à extensão e registre os intents do pacote: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Carregar dados de um servidor + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` usa uma sessão efêmera com tempos limite curtos, que não espera haver +conexão. Ele tenta de novo respostas 5xx e 408, tempos limite esgotados e erros de falta de +conexão (uma vez por padrão; defina uma `RetryPolicy` para mudar isso), mas não respostas 4xx +nem erros de decodificação. + +## Demonstração + + + + Widgets de clima, água, atividade física, contagem regressiva, citação e hábitos desenhados com o pacote + + + + Views de estatísticas com minigráfico, anel de progresso, gráfico de linhas e gráfico de barras + + + + + Widgets da Tela Bloqueada de bateria, anéis de atividade, contagem regressiva, clima e estatísticas + + +As imagens são desenhadas a partir das views do pacote com `ImageRenderer` por +[`scripts/render-screenshots.sh`](scripts/render-screenshots.sh). Como são desenhadas fora do +WidgetKit, a moldura do widget, a vibrância da Tela Bloqueada e o modo noturno do StandBy são +aproximações, e não produzidos pelo sistema. + +Para testar os widgets, abra +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) e execute +o esquema **WidgetKitExample** em um simulador de iPhone; +[`Examples/README.md`](Examples/README.md) explica os três widgets e como adicioná-los à Tela +de Início. + +## Documentação + +Referência da API e artigos: + +- [Referência da API](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/) + no GitHub Pages, gerada a partir dos comentários DocC do código-fonte. +- No Xcode: **Product › Build Documentation**, ou + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Guias + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): + adicionar uma extensão de widget e o pacote, compartilhar dados por um App Group, montar a + timeline e desenhar uma entrada. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): + configurar o App Group, registrar os intents, adicionar botões e ler o estado deles no + provedor e no app. + +## Requisitos + +| Plataforma | Versão mínima | +| ----------- | ------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| Ferramentas | Swift 6.0 | + +As views da Tela Bloqueada e as Live Activities existem apenas no iOS. O alvo macOS existe +para que o código independente de plataforma e seus testes rodem com `swift test`. A CI compila +com o Xcode padrão da imagem `macos-26` do GitHub; o desenvolvimento local usa o Xcode 27. +Versões mais antigas do Xcode não foram testadas. + +## Arquitetura + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +As views são enxutas: cálculos que podem dar errado com dados reais (metas iguais a zero, +`NaN`, intervalos de datas invertidos, dias de mudança de horário de verão) ficam em `Core` e +`Timeline` como funções puras com testes unitários. Tipos que acessam armazenamento ou rede +recebem suas dependências (`UserDefaults`, relógio, transporte, espera) como parâmetros, então +podem ser testados sem um App Group ou um servidor. Os corpos das views SwiftUI não têm testes +de snapshot. + +Execute os testes no macOS e em um simulador iOS: + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +Três testes de iOS são pulados de propósito: eles desenham as imagens do README e só rodam por +meio de `scripts/render-screenshots.sh`. + +## Roteiro + +- Criar a tag de uma versão que corresponda ao código atual, para que o pacote possa ser + adicionado por versão. +- Testes de snapshot para os corpos das views. +- Uma Live Activity de exemplo no app de exemplo. + +O [CHANGELOG](CHANGELOG.md) registra o que mudou até agora, incluindo como o pacote foi +restaurado em outubro de 2026, depois que se descobriu que o código da `v1.0.0` não compilava. + +## Como contribuir + +Contribuições são bem-vindas. Leia o [guia de contribuição](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) e o [código de conduta](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md) antes de abrir um pull request. Relate problemas de segurança de forma privada, como descrito na [política de segurança](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md). + +## Licença + +Publicado sob a licença MIT. Veja [LICENSE](LICENSE). diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..113706d --- /dev/null +++ b/README.ru.md @@ -0,0 +1,376 @@ + + + +

iOS Widget Development Kit: SwiftUI-представления и помощники для таймлайнов виджетов WidgetKit и Live Activities.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+Статус CI +Поддерживаемые платформы: iOS 17+ · macOS 14+ +Версия языка: Swift 6.0+ +Лицензия: MIT +

+ +

+Начать +Документация +Примеры +

+ +> Это перевод README на английском языке. Если они расходятся, актуальна английская версия. + + + +WidgetDevelopmentKit — это Swift-пакет для разработчиков, которые создают виджеты для своих +iOS-приложений. В нём есть готовые SwiftUI-представления для виджетов экрана «Домой», экрана +блокировки и StandBy, представления графиков под размер виджета, вспомогательные типы для +таймлайнов и для данных, которые приложение передаёт виджету, App Intents для кнопок в +виджетах, а также атрибуты и представления ActivityKit для Live Activities. Расширение с +виджетами вы по-прежнему пишете сами; пакет избавляет от работы над представлениями и над +логикой дат, хранения и повторных попыток, которая нужна любому виджету. + +## Основные возможности + +- **20 шаблонов для экрана «Домой»** — погода, статистика, финансы, календарь, обратный + отсчёт, часы, заметки, цитата, музыка, подкаст, новости, кольца активности, трекер + привычек, список дел, вода, батарея и другие. +- **Экран блокировки и StandBy** — 15 представлений для аксессуарных семейств (круглые, + прямоугольные, строчные) и 7 представлений для StandBy. +- **Помощники для таймлайнов** — даты записей, политики перезагрузки и хранилище снимков со + сроком действия, чтобы виджет читал то, что приложение сохранило через App Group. +- **Интерактивные виджеты** — 5 App Intents и 8 представлений с кнопками, которые их + запускают. +- **Live Activities** — 10 типов `ActivityAttributes` с представлениями для экрана блокировки + и компактного Dynamic Island, а также конечный автомат, который отклоняет обновления, + пришедшие не по порядку. +- **Протестированная логика** — код для дат, чисел и повторных попыток покрыт тестами Swift + Testing, включая граничные случаи вроде `NaN`, нулевой цели и дней перехода на летнее время. + +## Быстрый старт + +**1. Добавьте пакет** + +Приложению нужно расширение с виджетами. Если его ещё нет, выберите в Xcode **File › New › +Target…** и затем **Widget Extension**. После этого выберите **File › Add Package +Dependencies…**, вставьте этот адрес в поле поиска в правом верхнем углу и нажмите **Add +Package**: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +В следующем окне в столбце **Add to Target** для `WidgetDevelopmentKit` выберите расширение +с виджетами и нажмите **Add Package**. + +**2. Импортируйте модуль** + +В начале Swift-файла в расширении с виджетами: + +```swift +import WidgetDevelopmentKit +``` + +**3. Используйте** + +Добавьте этот виджет в расширение. Он показывает обратный отсчёт до даты с помощью +представления `CountdownWidget` из пакета. Затем добавьте `LaunchCountdownWidget()` в +`WidgetBundle` расширения — тип, помеченный `@main`. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +Запустите приложение в симуляторе iPhone. На экране «Домой» нажмите и удерживайте пустое +место, коснитесь **Edit › Add Widget**, найдите своё приложение и добавьте **Countdown**. Тот +же код есть в +[`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift). + +## Установка + +### Swift Package Manager + +В Xcode выберите **File › Add Package Dependencies…** и введите адрес репозитория: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Или добавьте его в `dependencies` вашего `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +Релизы начинаются с 2.0.0; более ранний тег `v1.0.0` старше текущего кода и не +собирается. У пакета нет сторонних зависимостей. + +## Использование + +Все примеры на Swift в этом разделе взяты из +[`Examples/WidgetKitExample`](Examples/WidgetKitExample), который собирает CI, или из +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift), +который компилируется вместе с тестами. Если блок кода здесь перестанет совпадать с этим кодом, +тест упадёт. + +### Чтение общих данных в провайдере таймлайна + +`TimelineSchedule` создаёт даты записей, `TimelineBuilder` сортирует записи и превращает +`WidgetUpdatePolicy` в политику перезагрузки WidgetKit, а `WidgetSnapshotStore` читает то, что +приложение сохранило в общую App Group. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### Сохранение данных в приложении + +Приложение записывает значение со сроком действия и просит WidgetKit перезагрузить виджет: + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +Добавьте приложению и расширению возможность **App Groups** с одним и тем же идентификатором. +Хранилище использует `UserDefaults`, которое не шифруется, поэтому не храните в нём секреты. + +### Отображение записи + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Представления, показывающие значения, которые зависят от времени (`CountdownWidget`, +`StandByCountdownWidget`, `LockScreenCountdownWidget`), принимают `referenceDate`; передавайте +`entry.date`, чтобы каждая запись показывала свой момент времени. + +### Кнопка в виджете + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Интенты выполняются в расширении с виджетами и записывают данные в `WidgetStateStore.shared`. +Это хранилище использует App Group, указанную в ключе `WidgetDevelopmentKitAppGroup` в +Info.plist. Добавьте ключ и в приложение, и в расширение и зарегистрируйте интенты пакета: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Загрузка данных с сервера + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` использует эфемерную сессию с короткими тайм-аутами, которая не ждёт +появления сети. Он повторяет запросы при ответах 5xx и 408, тайм-аутах и ошибках отсутствия +сети (по умолчанию один раз; изменить можно через `RetryPolicy`), но не при ответах 4xx и +ошибках декодирования. + +## Демо + + + + Виджеты погоды, воды, фитнеса, обратного отсчёта, цитаты и привычек, отрисованные пакетом + + + + Представления статистики со спарклайном, кольца прогресса, линейного и столбчатого графиков + + + + + Виджеты экрана блокировки: батарея, кольца активности, обратный отсчёт, погода и статистика + + +Изображения отрисованы из представлений пакета с помощью `ImageRenderer` скриптом +[`scripts/render-screenshots.sh`](scripts/render-screenshots.sh). Поскольку они рисуются вне +WidgetKit, рамка виджета, эффект vibrancy на экране блокировки и ночной режим StandBy лишь +приблизительно повторяют то, что показывает система. + +Чтобы попробовать виджеты самостоятельно, откройте +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) и запустите +схему **WidgetKitExample** в симуляторе iPhone; в +[`Examples/README.md`](Examples/README.md) описаны три виджета и то, как добавить их на экран +«Домой». + +## Документация + +Справочник API и статьи: + +- [Справочник API](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/) + на GitHub Pages, созданный из DocC-комментариев в исходном коде. +- В Xcode: **Product › Build Documentation** или + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Руководства + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): + добавление расширения с виджетами и пакета, обмен данными через App Group, построение + таймлайна и отрисовка записи. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): + настройка App Group, регистрация интентов, добавление кнопок и чтение их состояния в + провайдере и в приложении. + +## Требования + +| Платформа | Минимальная версия | +| ----------- | ------------------ | +| iOS | 17.0 | +| macOS | 14.0 | +| Инструменты | Swift 6.0 | + +Представления для экрана блокировки и Live Activities есть только в iOS. Цель для macOS нужна, +чтобы платформенно-независимый код и его тесты запускались через `swift test`. CI собирает пакет +с Xcode, установленным по умолчанию в образе `macos-26` на GitHub; для локальной разработки используется +Xcode 27. Более старые версии Xcode не проверялись. + +## Архитектура + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +Представления остаются тонкими: вычисления, которые могут сломаться на реальных данных +(нулевые цели, `NaN`, перевёрнутые диапазоны дат, дни перехода на летнее время), вынесены в +`Core` и `Timeline` как чистые функции с модульными тестами. Типы, которые работают с +хранилищем или сетью, получают зависимости (`UserDefaults`, часы, транспорт, ожидание) через +параметры, поэтому их можно тестировать без App Group и сервера. Для body SwiftUI-представлений +снапшот-тестов нет. + +Запустите тесты на macOS и в симуляторе iOS: + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +Три iOS-теста пропускаются намеренно: они отрисовывают изображения для README и запускаются +только через `scripts/render-screenshots.sh`. + +## План развития + +- Отметить тегом релиз, соответствующий текущему коду, чтобы пакет можно было подключать по + версии. +- Снапшот-тесты для body представлений. +- Пример Live Activity в демонстрационном приложении. + +В [CHANGELOG](CHANGELOG.md) записаны все изменения на сегодня, в том числе то, как пакет был +восстановлен в октябре 2026 года, после того как выяснилось, что код `v1.0.0` не собирается. + +## Участие в разработке + +Мы рады вашему участию. Перед открытием pull request прочитайте [руководство для участников](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) и [кодекс поведения](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md). О проблемах безопасности сообщайте конфиденциально, как описано в [политике безопасности](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md). + +## Лицензия + +Распространяется по лицензии MIT. Подробнее — в файле [LICENSE](LICENSE). diff --git a/README.tr.md b/README.tr.md new file mode 100644 index 0000000..39ec0fb --- /dev/null +++ b/README.tr.md @@ -0,0 +1,376 @@ + + + +

iOS Widget Development Kit: WidgetKit widget'ları ve Live Activities için SwiftUI görünümleri ve zaman çizelgesi yardımcıları.

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+CI durumu +Desteklenen platformlar: iOS 17+ · macOS 14+ +Dil sürümü: Swift 6.0+ +Lisans: MIT +

+ +

+Başlayın +Dokümantasyon +Örnekler +

+ +> Bu belge İngilizce README'nin çevirisidir. İkisi arasında fark varsa güncel olan İngilizce sürümdür. + + + +WidgetDevelopmentKit, iOS uygulamalarına widget geliştirenler için bir Swift paketidir. Ana +Ekran, Kilitli Ekran ve StandBy widget'ları için hazır SwiftUI görünümleri, widget boyutuna +uygun grafik görünümleri, zaman çizelgeleri ve uygulamanın widget'la paylaştığı veriler için +yardımcılar, widget düğmeleri için App Intents ve Live Activities için ActivityKit +öznitelikleri ile görünümleri içerir. Widget uzantısını yine siz yazarsınız; paket sizi +görünümlerle ve her widget'ın ihtiyaç duyduğu tarih, depolama ve yeniden deneme mantığıyla +uğraşmaktan kurtarır. + +## Öne çıkanlar + +- **20 Ana Ekran şablonu** — hava durumu, istatistik, finans, takvim, geri sayım, saat, not, + alıntı, müzik, podcast, haber, fitness halkaları, alışkanlık takibi, yapılacaklar, su, pil + ve daha fazlası. +- **Kilitli Ekran ve StandBy** — 15 aksesuar görünümü (dairesel, dikdörtgen, satır içi) ve 7 + StandBy görünümü. +- **Zaman çizelgesi yardımcıları** — giriş tarihleri, yeniden yükleme politikaları ve süresi + dolan değerleri bilen bir anlık görüntü deposu; widget, uygulamanın App Group üzerinden + kaydettiği veriyi okur. +- **Etkileşimli widget'lar** — 5 App Intent ve bunları çalıştıran düğmelere sahip 8 görünüm. +- **Live Activities** — Kilitli Ekran ve kompakt Dynamic Island görünümleriyle 10 + `ActivityAttributes` türü ve sırası karışmış güncellemeleri reddeden bir durum makinesi. +- **Test edilmiş mantık** — tarih, sayı ve yeniden deneme kodu Swift Testing testleriyle + kapsanır; `NaN`, sıfır hedef ve yaz saati geçiş günleri gibi uç durumlar da dahil. + +## Hızlı başlangıç + +**1. Paketi ekleyin** + +Uygulamanızın bir widget uzantısı olmalı. Henüz yoksa Xcode'da **File › New › Target…** +menüsünü seçin ve **Widget Extension**'ı seçin. Ardından **File › Add Package +Dependencies…** menüsünü seçin, bu adresi sağ üstteki arama alanına yapıştırın ve **Add +Package**'a tıklayın: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Sonraki pencerede `WidgetDevelopmentKit` için **Add to Target** sütununu widget uzantınız +olarak ayarlayın ve **Add Package**'a tıklayın. + +**2. Modülü içe aktarın** + +Widget uzantısındaki bir Swift dosyasının en üstüne ekleyin: + +```swift +import WidgetDevelopmentKit +``` + +**3. Kullanın** + +Bu widget'ı uzantıya ekleyin. Paketin `CountdownWidget` görünümüyle bir tarihe geri sayım +yapar. Ardından `LaunchCountdownWidget()` satırını uzantının `WidgetBundle`'ına, yani +`@main` ile işaretli türe ekleyin. + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +Uygulamayı bir iPhone simülatöründe çalıştırın. Ana Ekran'da boş bir alana basılı tutun, +**Edit › Add Widget**'a dokunun, uygulamanızı bulun ve **Countdown**'ı ekleyin. Aynı kod +[`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift) +içinde de var. + +## Kurulum + +### Swift Package Manager + +Xcode'da **File › Add Package Dependencies…** menüsünü seçin ve depo adresini girin: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +Ya da `Package.swift` dosyanızdaki `dependencies` listesine ekleyin: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +Sürümler 2.0.0 ile başlar; daha önceki `v1.0.0` etiketi güncel koddan eskidir ve +derlenmez. Paketin üçüncü taraf bağımlılığı yoktur. + +## Kullanım + +Bu bölümdeki her Swift örneği, CI'ın derlediği +[`Examples/WidgetKitExample`](Examples/WidgetKitExample) projesinden ya da testlerle birlikte +derlenen +[`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift) +dosyasından alınmıştır. Buradaki bir blok o kodla artık eşleşmezse bir test başarısız olur. + +### Zaman çizelgesi sağlayıcısında paylaşılan veriyi okuyun + +`TimelineSchedule` giriş tarihlerini üretir, `TimelineBuilder` girişleri sıralar ve bir +`WidgetUpdatePolicy` değerini WidgetKit'in yeniden yükleme politikasına çevirir, +`WidgetSnapshotStore` ise uygulamanın paylaşılan App Group'a kaydettiklerini okur. + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### Uygulamada veriyi kaydedin + +Uygulama değeri bir geçerlilik süresiyle yazar ve WidgetKit'ten widget'ı yeniden yüklemesini +ister: + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +Uygulamaya ve uzantıya aynı tanımlayıcıyla **App Groups** yeteneğini ekleyin. Depo +şifrelenmeyen `UserDefaults` kullanır; gizli bilgileri burada tutmayın. + +### Bir girişi gösterin + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Zamana bağlı değer gösteren görünümler (`CountdownWidget`, `StandByCountdownWidget`, +`LockScreenCountdownWidget`) bir `referenceDate` alır; her giriş kendi anını göstersin diye +`entry.date` değerini verin. + +### Widget'a düğme ekleyin + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +Intent'ler widget uzantısında çalışır ve `WidgetStateStore.shared`'a yazar. Bu depo, +Info.plist'teki `WidgetDevelopmentKitAppGroup` anahtarında adı verilen App Group'u kullanır. +Anahtarı hem uygulamaya hem uzantıya ekleyin ve paketin intent'lerini kaydedin: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Sunucudan veri yükleyin + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader`, bağlantıyı beklemeyen ve kısa zaman aşımları olan geçici (ephemeral) bir +oturum kullanır. 5xx ve 408 yanıtlarını, zaman aşımlarını ve çevrimdışı hatalarını yeniden +dener (varsayılan olarak bir kez; değiştirmek için bir `RetryPolicy` verin), ancak 4xx +yanıtlarını ve çözümleme (decoding) hatalarını denemez. + +## Demo + + + + Paketten çizilen hava durumu, su, fitness, geri sayım, alıntı ve alışkanlık widget'ları + + + + İstatistik çizgisi, ilerleme halkası, çizgi grafik ve çubuk grafik görünümleri + + + + + Kilitli Ekran pil, aktivite halkaları, geri sayım, hava durumu ve istatistik widget'ları + + +Görseller paketin görünümlerinden +[`scripts/render-screenshots.sh`](scripts/render-screenshots.sh) ile `ImageRenderer` +kullanılarak çizilir. WidgetKit dışında çizildikleri için widget çerçevesi, Kilitli Ekran +canlılığı (vibrancy) ve StandBy gece modu sistem tarafından üretilmez, yaklaşık olarak +gösterilir. + +Widget'ları kendiniz denemek için +[`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample) projesini +açın ve **WidgetKitExample** şemasını bir iPhone simülatöründe çalıştırın; +[`Examples/README.md`](Examples/README.md) üç widget'ı ve bunları Ana Ekran'a nasıl +ekleyeceğinizi anlatır. + +## Dokümantasyon + +API başvuru belgeleri ve makaleler: + +- GitHub Pages'te + [API başvuru belgeleri](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/); + kaynak koddaki DocC yorumlarından üretilir. +- Xcode'da: **Product › Build Documentation** ya da + `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`. + +## Rehberler + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted): + widget uzantısını ve paketi ekleme, veriyi App Group üzerinden paylaşma, zaman çizelgesini + oluşturma ve bir girişi çizme. +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets): + App Group'u yapılandırma, intent'leri kaydetme, düğme ekleme ve bunların durumunu + sağlayıcıda ve uygulamada okuma. + +## Gereksinimler + +| Platform | En düşük sürüm | +| ------------ | -------------- | +| iOS | 17.0 | +| macOS | 14.0 | +| Araç zinciri | Swift 6.0 | + +Kilitli Ekran görünümleri ve Live Activities yalnızca iOS'ta vardır. macOS hedefi, platformdan +bağımsız kod ve testleri `swift test` ile çalışabilsin diye bulunur. CI, GitHub'ın `macos-26` +imajındaki varsayılan Xcode ile derler; yerel geliştirmede Xcode 27 kullanılır. Daha eski Xcode +sürümleri denenmedi. + +## Mimari + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +Görünümler ince tutulur: gerçek veriyle yanlış gidebilecek hesaplar (sıfır hedefler, `NaN`, +ters tarih aralıkları, yaz saati geçiş günleri) `Core` ve `Timeline` içinde birim testli saf +fonksiyonlar olarak durur. Depolamaya ya da ağa dokunan türler bağımlılıklarını +(`UserDefaults`, saat, taşıma katmanı, bekleme) parametre olarak alır; böylece App Group ya da +sunucu olmadan test edilebilirler. SwiftUI görünüm gövdeleri için snapshot testi yoktur. + +Testleri macOS'ta ve bir iOS simülatöründe çalıştırın: + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +Üç iOS testi bilerek atlanır: README görsellerini çizerler ve yalnızca +`scripts/render-screenshots.sh` ile çalışırlar. + +## Yol haritası + +- Paket sürüm numarasıyla eklenebilsin diye güncel koda karşılık gelen bir sürüm etiketlemek. +- Görünüm gövdeleri için snapshot testleri. +- Örnek uygulamaya bir Live Activity örneği. + +[CHANGELOG](CHANGELOG.md) şimdiye kadarki değişiklikleri kaydeder; `v1.0.0` kodunun +derlenmediği anlaşıldıktan sonra paketin Ekim 2026'da nasıl geri getirildiği de buna dahildir. + +## Katkıda bulunma + +Katkılarınızı bekliyoruz. Pull request açmadan önce [katkı rehberini](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md) ve [davranış kurallarını](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md) okuyun. Güvenlik açıklarını [güvenlik politikasında](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md) anlatıldığı gibi gizli olarak bildirin. + +## Lisans + +MIT lisansıyla yayımlanmıştır. Ayrıntılar için [LICENSE](LICENSE) dosyasına bakın. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..4320d60 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,314 @@ + + + +

iOS Widget Development Kit: 用于 WidgetKit 小组件和 Live Activities 的 SwiftUI 视图与时间线辅助工具。

+ + +

English · Türkçe · 简体中文 · 日本語 · 한국어 · Español · Deutsch · Français · Português (Brasil) · Русский

+ + +

+CI 状态 +支持的平台: iOS 17+ · macOS 14+ +语言版本: Swift 6.0+ +许可证: MIT +

+ +

+开始使用 +文档 +示例 +

+ +> 本文是英文 README 的译文。如两者内容不一致,以英文版为准。 + + + +WidgetDevelopmentKit 是一个 Swift 软件包,面向为 iOS 应用开发小组件的开发者。它包含用于主屏幕、锁定屏幕和 StandBy 小组件的现成 SwiftUI 视图,适合小组件尺寸的图表视图,用于时间线以及应用与小组件共享数据的辅助工具,用于小组件按钮的 App Intents,以及用于 Live Activities 的 ActivityKit 属性和视图。小组件扩展仍需你自己编写;这个软件包为你省去视图,以及每个小组件都需要的日期、存储和重试逻辑。 + +## 亮点 + +- **20 个主屏幕模板** —— 天气、统计、财务、日历、倒计时、时钟、备忘、名言、音乐、播客、新闻、健身圆环、习惯追踪、待办、饮水、电池等。 +- **锁定屏幕和 StandBy** —— 15 个配件视图(圆形、矩形、行内)和 7 个 StandBy 视图。 +- **时间线辅助工具** —— 条目日期、重新加载策略,以及带有效期的快照存储,让小组件读取应用通过 App Group 保存的数据。 +- **交互式小组件** —— 5 个 App Intent,以及 8 个带有调用这些 Intent 的按钮的视图。 +- **Live Activities** —— 10 个 `ActivityAttributes` 类型,带有锁定屏幕视图和紧凑型 Dynamic Island 视图,以及一个拒绝乱序更新的状态机。 +- **经过测试的逻辑** —— 日期、数值和重试代码都有 Swift Testing 测试覆盖,包括 `NaN`、目标为零、夏令时切换日等边界情况。 + +## 快速开始 + +**1. 添加软件包** + +你的应用需要一个小组件扩展。如果还没有,请在 Xcode 中选择 **File › New › Target…**,然后选择 **Widget Extension**。接着选择 **File › Add Package Dependencies…**,把下面的 URL 粘贴到右上角的搜索框中,然后点击 **Add Package**: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +在下一个对话框中,把 `WidgetDevelopmentKit` 的 **Add to Target** 设为你的小组件扩展,然后点击 **Add Package**。 + +**2. 导入模块** + +在小组件扩展中某个 Swift 文件的顶部加入: + +```swift +import WidgetDevelopmentKit +``` + +**3. 开始使用** + +把这个小组件添加到扩展中。它使用软件包中的 `CountdownWidget` 视图倒数到某个日期。然后在扩展的 `WidgetBundle`(即标有 `@main` 的类型)中列出 `LaunchCountdownWidget()`。 + +```swift +struct LaunchEntry: TimelineEntry { + let date: Date +} + +struct LaunchProvider: TimelineProvider { + func placeholder(in context: Context) -> LaunchEntry { + LaunchEntry(date: .now) + } + + func getSnapshot(in context: Context, completion: @escaping (LaunchEntry) -> Void) { + completion(LaunchEntry(date: .now)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + // One entry per hour for the next six hours, then WidgetKit asks again. + let entries = TimelineSchedule.dates(from: .now, every: 60 * 60, count: 6).map(LaunchEntry.init) + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +struct LaunchCountdownWidget: Widget { + let launchDay = DateComponents(calendar: .current, year: 2027, month: 1, day: 1).date ?? .now + + var body: some WidgetConfiguration { + StaticConfiguration(kind: "launch", provider: LaunchProvider()) { entry in + CountdownWidget(title: "Launch", targetDate: launchDay, referenceDate: entry.date) + } + .configurationDisplayName("Countdown") + .supportedFamilies([.systemSmall]) + } +} +``` + +在 iPhone 模拟器上运行应用。在主屏幕上长按空白处,轻点 **Edit › Add Widget**,找到你的应用并添加 **Countdown**。同样的代码也在 [`Examples/WidgetKitExample`](Examples/WidgetKitExample/Widgets/LaunchCountdownWidget.swift) 中。 + +## 安装 + +### Swift Package Manager + +在 Xcode 中选择 **File › Add Package Dependencies…**,然后输入仓库地址: + +```text +https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git +``` + +或者将其添加到 `Package.swift` 的 `dependencies` 中: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +], +targets: [ + .target(name: "YourApp", dependencies: [ + .product(name: "WidgetDevelopmentKit", package: "iOS-Widget-Development-Kit") + ]) +] +``` + +可用版本从 2.0.0 开始;更早的 `v1.0.0` 标签早于当前代码,无法编译。该软件包没有第三方依赖。 + +## 使用方法 + +本节中的每个 Swift 示例都取自 CI 会构建的 [`Examples/WidgetKitExample`](Examples/WidgetKitExample),或取自随测试一起编译的 [`ReadmeExamplesTests.swift`](Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift)。如果这里的代码块与那些代码不再一致,就会有测试失败。 + +### 在时间线提供者中读取共享数据 + +`TimelineSchedule` 生成条目日期,`TimelineBuilder` 对条目排序并把 `WidgetUpdatePolicy` 转换为 WidgetKit 的重新加载策略,`WidgetSnapshotStore` 读取应用保存到共享 App Group 中的数据。 + +```swift +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} +``` + +### 在应用中保存数据 + +应用写入带有效期的值,并请求 WidgetKit 重新加载小组件: + +```swift +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +为应用和扩展添加 **App Groups** 功能,并使用同一个标识符。存储使用未加密的 `UserDefaults`,因此不要在其中保存机密信息。 + +### 显示一个条目 + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +显示与时间相关数值的视图(`CountdownWidget`、`StandByCountdownWidget`、`LockScreenCountdownWidget`)接受一个 `referenceDate` 参数;传入 `entry.date`,让每个条目显示它自己的时刻。 + +### 为小组件添加按钮 + +```swift +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} +``` + +这些 Intent 在小组件扩展中运行,并写入 `WidgetStateStore.shared`。它使用 Info.plist 中 `WidgetDevelopmentKitAppGroup` 键所指定的 App Group。请在应用和扩展中都添加这个键,并注册软件包中的 Intent: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### 从服务器加载数据 + +```swift +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} +``` + +`WidgetDataLoader` 使用一个超时较短、不等待网络连接的临时(ephemeral)会话。它会重试 5xx 和 408 响应、超时以及离线错误(默认重试一次;可通过 `RetryPolicy` 修改),但不会重试 4xx 响应或解码错误。 + +## 演示 + + + + 由软件包渲染的天气、饮水、健身、倒计时、名言和习惯小组件 + + + + 统计迷你折线图、进度圆环、折线图和柱状图视图 + + + + + 锁定屏幕上的电池、活动圆环、倒计时、天气和统计小组件 + + +这些图片由 [`scripts/render-screenshots.sh`](scripts/render-screenshots.sh) 使用 `ImageRenderer` 从软件包的视图渲染而成。由于它们是在 WidgetKit 之外绘制的,小组件边框、锁定屏幕的鲜明效果(vibrancy)和 StandBy 夜间模式都是近似效果,而不是由系统生成的。 + +想亲自试用这些小组件,请打开 [`Examples/WidgetKitExample/WidgetKitExample.xcodeproj`](Examples/WidgetKitExample),在 iPhone 模拟器上运行 **WidgetKitExample** scheme;[`Examples/README.md`](Examples/README.md) 介绍了这三个小组件以及如何把它们添加到主屏幕。 + +## 文档 + +API 参考文档与文章: + +- GitHub Pages 上的 [API 参考](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/),由源代码中的 DocC 注释生成。 +- 在 Xcode 中:**Product › Build Documentation**,或运行 `xcodebuild docbuild -scheme WidgetDevelopmentKit -destination 'generic/platform=iOS Simulator'`。 + +## 指南 + +- [Getting Started](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/gettingstarted):添加小组件扩展和软件包,通过 App Group 共享数据,构建时间线并渲染条目。 +- [Interactive Widgets](https://muhittincamdali.github.io/iOS-Widget-Development-Kit/documentation/widgetdevelopmentkit/interactivewidgets):配置 App Group,注册 Intent,添加按钮,并在提供者和应用中读取按钮状态。 + +## 系统要求 + +| 平台 | 最低版本 | +| ------ | --------- | +| iOS | 17.0 | +| macOS | 14.0 | +| 工具链 | Swift 6.0 | + +锁定屏幕视图和 Live Activities 仅适用于 iOS。macOS 目标的存在是为了让与平台无关的代码及其测试可以通过 `swift test` 运行。CI 使用 GitHub `macos-26` 镜像的默认 Xcode 构建;本地开发使用 Xcode 27。尚未在更早的 Xcode 版本上尝试。 + +## 架构 + +```text +Sources/WidgetDevelopmentKit/ +├── Core/ Pure helpers: clamping, formatting, month grid, countdown math +├── Timeline/ Update policy, timeline builder and schedule, snapshot store +├── Networking/ Data loader with retry policy, endpoint builder +├── Configuration/ Deep links, hex colors, App Intents entities +├── Interactive/ App Intents, shared state store, interactive views +├── Templates/ Home Screen views +├── Charts/ Swift Charts views +├── LockScreen/ Accessory family views (iOS) +├── StandBy/ StandBy views +├── LiveActivities/ ActivityKit attributes and views (iOS), state machine +├── UI/ Timeline scrubber +└── WidgetDevelopmentKit.docc/ +``` + +视图保持精简:在真实数据下可能出错的计算(目标为零、`NaN`、颠倒的日期范围、夏令时切换日)以纯函数的形式放在 `Core` 和 `Timeline` 中,并有单元测试。涉及存储或网络的类型通过参数接收依赖(`UserDefaults`、时钟、传输层、等待),因此无需 App Group 或服务器即可测试。SwiftUI 视图的 body 没有快照测试。 + +在 macOS 和 iOS 模拟器上运行测试: + +```sh +swift test +xcodebuild test -scheme WidgetDevelopmentKit -destination 'platform=iOS Simulator,name=iPhone 17 Pro' +``` + +有三个 iOS 测试是有意跳过的:它们用于渲染 README 中的图片,只通过 `scripts/render-screenshots.sh` 运行。 + +## 路线图 + +- 为当前代码打一个发布标签,以便按版本添加软件包。 +- 为视图 body 添加快照测试。 +- 在示例应用中加入一个 Live Activity 示例。 + +[CHANGELOG](CHANGELOG.md) 记录了迄今为止的变更,包括在发现 `v1.0.0` 的代码无法编译之后,软件包于 2026 年 10 月是如何恢复的。 + +## 参与贡献 + +欢迎参与贡献。提交 pull request 之前,请先阅读[贡献指南](https://github.com/muhittincamdali/iOS-Widget-Development-Kit/blob/main/CONTRIBUTING.md)和[行为准则](https://github.com/muhittincamdali/.github/blob/main/CODE_OF_CONDUCT.md)。如发现安全问题,请按照[安全策略](https://github.com/muhittincamdali/.github/blob/main/SECURITY.md)私下报告。 + +## 许可证 + +基于 MIT 许可证发布。详见 [LICENSE](LICENSE)。 diff --git a/README_TR.md b/README_TR.md deleted file mode 100644 index 61d0a67..0000000 --- a/README_TR.md +++ /dev/null @@ -1,36 +0,0 @@ -# iOS-Widget-Development-Kit - -> 🇹🇷 Türkçe | [English](README.md) - -📲 WidgetKit development kit with Live Activities and dynamic widgets - -## 📦 Kurulum - -### Swift Package Manager - -```swift -dependencies: [ - .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "1.0.0") -] -``` - -## 🚀 Hızlı Başlangıç - -Detaylı kullanım ve API referansı için [İngilizce README](README.md) dosyasına bakın. - -## 📖 Dokümantasyon - -- [README (English)](README.md) -- [Contributing Guide](CONTRIBUTING.md) -- [Changelog](CHANGELOG.md) -- [Security Policy](SECURITY.md) - -## 📄 Lisans - -MIT License - detaylar için [LICENSE](LICENSE) dosyasına bakın. - -## 👨‍💻 Geliştirici - -**Muhittin Camdali** - -[![GitHub](https://img.shields.io/badge/GitHub-muhittincamdali-181717?style=flat&logo=github)](https://github.com/muhittincamdali) diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 63291de..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,45 +0,0 @@ -# Security Policy - -If you discover a security vulnerability, please report it responsibly. - -## Reporting a Vulnerability -- Do not open public issues for vulnerabilities -- Email the maintainer with steps to reproduce -- We will acknowledge receipt and work on a timely fix - -## Supported Versions -Security fixes are applied to the latest mainline branch. - -## Overview -This document belongs to the iOS Widget Development Kit repository. It explains goals, scope, and usage. - -## Architecture -Clean Architecture and SOLID are followed to ensure maintainability and scalability. - -## Installation (SPM) -```swift -.package(url: "https://github.com/owner/iOS-Widget-Development-Kit.git", from: "1.0.0") -``` - -## Quick Start -```swift -// Add a concise example usage here -``` - -## API Reference -Describe key types and methods exposed by this module. - -## Usage Examples -Provide several concrete end-to-end examples. - -## Performance -List relevant performance considerations. - -## Security -Document security-sensitive areas and mitigations. - -## Troubleshooting -Known issues and solutions. - -## FAQ -Answer common questions with clear, actionable guidance. diff --git a/SUPPORT.md b/SUPPORT.md deleted file mode 100644 index d918ade..0000000 --- a/SUPPORT.md +++ /dev/null @@ -1,25 +0,0 @@ -# Support - -## Getting Help - -- 📖 **Documentation**: Check the [README](README.md) and [Documentation](Documentation/) folder -- 💬 **Discussions**: Ask questions in [GitHub Discussions](../../discussions) -- 🐛 **Bug Reports**: Open an [issue](../../issues/new?template=bug_report.yml) -- 💡 **Feature Requests**: Submit a [feature request](../../issues/new?template=feature_request.yml) - -## Response Time - -| Channel | Expected Response | -|---------|-------------------| -| Bug (critical) | < 24 hours | -| Bug (normal) | < 48 hours | -| Feature Request | < 72 hours | -| Question | < 48 hours | - -## Commercial Support - -For commercial support or consulting, contact: muhittincamdali@gmail.com - -## Code of Conduct - -Please read our [Code of Conduct](CODE_OF_CONDUCT.md) before participating. diff --git a/Sources/Analytics/WidgetAnalytics.swift b/Sources/Analytics/WidgetAnalytics.swift deleted file mode 100644 index b62dba4..0000000 --- a/Sources/Analytics/WidgetAnalytics.swift +++ /dev/null @@ -1,515 +0,0 @@ -import Foundation -import Combine - -/// Widget Analytics Service -/// Provides comprehensive analytics and performance tracking for widgets -@available(iOS 16.0, *) -public class WidgetAnalyticsService: ObservableObject { - - // MARK: - Properties - - /// Analytics data storage - private var analyticsData = WidgetAnalytics() - - /// Performance metrics - private let performanceSubject = PassthroughSubject() - public var performancePublisher: AnyPublisher { - return performanceSubject.eraseToAnyPublisher() - } - - /// Event tracking - private var eventLog: [AnalyticsEvent] = [] - - /// Performance monitoring - private var performanceMetrics = WidgetPerformanceMetrics() - - /// Combine cancellables - private var cancellables = Set() - - /// Analytics configuration - private var configuration = AnalyticsConfiguration() - - // MARK: - Initialization - - public init() { - setupAnalytics() - } - - // MARK: - Public Methods - - /// Get current analytics data - /// - Returns: Analytics data - public func getAnalytics() -> WidgetAnalytics { - return analyticsData - } - - /// Log widget update - /// - Parameter widgetId: Widget identifier - public func logWidgetUpdate(widgetId: String) { - let event = AnalyticsEvent( - type: .widgetUpdate, - widgetId: widgetId, - timestamp: Date(), - metadata: ["widgetId": widgetId] - ) - - logEvent(event) - updateWidgetMetrics(widgetId: widgetId) - } - - /// Log template registration - /// - Parameter templateId: Template identifier - public func logTemplateRegistration(_ templateId: String) { - let event = AnalyticsEvent( - type: .templateRegistration, - widgetId: nil, - timestamp: Date(), - metadata: ["templateId": templateId] - ) - - logEvent(event) - analyticsData.templateRegistrations += 1 - } - - /// Log data source registration - /// - Parameter identifier: Data source identifier - public func logDataSourceRegistration(_ identifier: String) { - let event = AnalyticsEvent( - type: .dataSourceRegistration, - widgetId: nil, - timestamp: Date(), - metadata: ["dataSourceId": identifier] - ) - - logEvent(event) - analyticsData.dataSourceRegistrations += 1 - } - - /// Log WebSocket connection - /// - Parameters: - /// - identifier: Connection identifier - /// - url: Connection URL - public func logWebSocketConnection(identifier: String, url: String) { - let event = AnalyticsEvent( - type: .webSocketConnection, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "url": url] - ) - - logEvent(event) - analyticsData.webSocketConnections += 1 - } - - /// Log WebSocket disconnection - /// - Parameter identifier: Connection identifier - public func logWebSocketDisconnection(identifier: String) { - let event = AnalyticsEvent( - type: .webSocketDisconnection, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.webSocketDisconnections += 1 - } - - /// Log data sent - /// - Parameters: - /// - widgetId: Widget identifier - /// - dataType: Data type - public func logDataSent(widgetId: String, dataType: String) { - let event = AnalyticsEvent( - type: .dataSent, - widgetId: widgetId, - timestamp: Date(), - metadata: ["widgetId": widgetId, "dataType": dataType] - ) - - logEvent(event) - analyticsData.dataSentCount += 1 - } - - /// Log data source update - /// - Parameters: - /// - identifier: Data source identifier - /// - dataType: Data type - public func logDataSourceUpdate(identifier: String, dataType: String) { - let event = AnalyticsEvent( - type: .dataSourceUpdate, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "dataType": dataType] - ) - - logEvent(event) - analyticsData.dataSourceUpdates += 1 - } - - /// Log data source configuration - /// - Parameter identifier: Data source identifier - public func logDataSourceConfiguration(identifier: String) { - let event = AnalyticsEvent( - type: .dataSourceConfiguration, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.dataSourceConfigurations += 1 - } - - /// Log connection established - /// - Parameter identifier: Connection identifier - public func logConnectionEstablished(identifier: String) { - let event = AnalyticsEvent( - type: .connectionEstablished, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.connectionsEstablished += 1 - } - - /// Log connection failed - /// - Parameters: - /// - identifier: Connection identifier - /// - error: Error details - public func logConnectionFailed(identifier: String, error: Error) { - let event = AnalyticsEvent( - type: .connectionFailed, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.connectionFailures += 1 - } - - /// Log connection cancelled - /// - Parameter identifier: Connection identifier - public func logConnectionCancelled(identifier: String) { - let event = AnalyticsEvent( - type: .connectionCancelled, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.connectionsCancelled += 1 - } - - /// Log connection retry - /// - Parameter identifier: Connection identifier - public func logConnectionRetry(identifier: String) { - let event = AnalyticsEvent( - type: .connectionRetry, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.connectionRetries += 1 - } - - /// Log reconnection attempt - /// - Parameter identifier: Connection identifier - public func logReconnectionAttempt(identifier: String) { - let event = AnalyticsEvent( - type: .reconnectionAttempt, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier] - ) - - logEvent(event) - analyticsData.reconnectionAttempts += 1 - } - - /// Log data receive error - /// - Parameters: - /// - identifier: Connection identifier - /// - error: Error details - public func logDataReceiveError(identifier: String, error: Error) { - let event = AnalyticsEvent( - type: .dataReceiveError, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.dataReceiveErrors += 1 - } - - /// Log data decode error - /// - Parameters: - /// - identifier: Connection identifier - /// - error: Error details - public func logDataDecodeError(identifier: String, error: Error) { - let event = AnalyticsEvent( - type: .dataDecodeError, - widgetId: nil, - timestamp: Date(), - metadata: ["identifier": identifier, "error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.dataDecodeErrors += 1 - } - - /// Log network unavailable - public func logNetworkUnavailable() { - let event = AnalyticsEvent( - type: .networkUnavailable, - widgetId: nil, - timestamp: Date(), - metadata: [:] - ) - - logEvent(event) - analyticsData.networkUnavailableEvents += 1 - } - - /// Log memory cleanup - /// - Parameter removedEntries: Number of removed cache entries - public func logMemoryCleanup(removedEntries: Int) { - let event = AnalyticsEvent( - type: .memoryCleanup, - widgetId: nil, - timestamp: Date(), - metadata: ["removedEntries": removedEntries] - ) - - logEvent(event) - analyticsData.memoryCleanups += 1 - } - - /// Log battery optimization - /// - Parameter newInterval: New refresh interval - public func logBatteryOptimization(newInterval: TimeInterval) { - let event = AnalyticsEvent( - type: .batteryOptimization, - widgetId: nil, - timestamp: Date(), - metadata: ["newInterval": newInterval] - ) - - logEvent(event) - analyticsData.batteryOptimizations += 1 - } - - /// Log error - /// - Parameter error: Widget error - public func logError(_ error: WidgetError) { - let event = AnalyticsEvent( - type: .error, - widgetId: nil, - timestamp: Date(), - metadata: ["error": error.localizedDescription] - ) - - logEvent(event) - analyticsData.errors += 1 - } - - /// Log engine initialization - public func logEngineInitialization() { - let event = AnalyticsEvent( - type: .engineInitialization, - widgetId: nil, - timestamp: Date(), - metadata: [:] - ) - - logEvent(event) - analyticsData.engineInitializations += 1 - } - - /// Configure analytics - /// - Parameter config: Analytics configuration - public func configure(_ config: AnalyticsConfiguration) { - configuration = config - } - - /// Export analytics data - /// - Returns: Exported analytics data - public func exportAnalytics() -> Data? { - do { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - return try encoder.encode(analyticsData) - } catch { - return nil - } - } - - /// Clear analytics data - public func clearAnalytics() { - analyticsData = WidgetAnalytics() - eventLog.removeAll() - } - - // MARK: - Private Methods - - private func setupAnalytics() { - // Setup performance monitoring - Timer.publish(every: 60.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.updatePerformanceMetrics() - } - .store(in: &cancellables) - } - - private func logEvent(_ event: AnalyticsEvent) { - eventLog.append(event) - - // Limit event log size - if eventLog.count > configuration.maxEventLogSize { - eventLog.removeFirst(eventLog.count - configuration.maxEventLogSize) - } - - // Send to external analytics if configured - if configuration.enableExternalAnalytics { - sendToExternalAnalytics(event) - } - } - - private func updateWidgetMetrics(widgetId: String) { - analyticsData.widgetUpdates[widgetId, default: 0] += 1 - analyticsData.totalWidgetUpdates += 1 - } - - private func updatePerformanceMetrics() { - performanceSubject.send(performanceMetrics) - } - - private func sendToExternalAnalytics(_ event: AnalyticsEvent) { - // Implementation for external analytics service - // This could send data to Firebase, Mixpanel, etc. - } -} - -// MARK: - Supporting Types - -/// Analytics data structure -public struct WidgetAnalytics: Codable { - public var totalWidgetUpdates: Int = 0 - public var widgetUpdates: [String: Int] = [:] - public var templateRegistrations: Int = 0 - public var dataSourceRegistrations: Int = 0 - public var webSocketConnections: Int = 0 - public var webSocketDisconnections: Int = 0 - public var dataSentCount: Int = 0 - public var dataSourceUpdates: Int = 0 - public var dataSourceConfigurations: Int = 0 - public var connectionsEstablished: Int = 0 - public var connectionFailures: Int = 0 - public var connectionsCancelled: Int = 0 - public var connectionRetries: Int = 0 - public var reconnectionAttempts: Int = 0 - public var dataReceiveErrors: Int = 0 - public var dataDecodeErrors: Int = 0 - public var networkUnavailableEvents: Int = 0 - public var memoryCleanups: Int = 0 - public var batteryOptimizations: Int = 0 - public var errors: Int = 0 - public var engineInitializations: Int = 0 - public var sessionStartTime: Date = Date() - public var lastUpdateTime: Date = Date() - - public init() {} -} - -/// Analytics event -public struct AnalyticsEvent { - public let type: AnalyticsEventType - public let widgetId: String? - public let timestamp: Date - public let metadata: [String: Any] - - public init(type: AnalyticsEventType, widgetId: String?, timestamp: Date, metadata: [String: Any]) { - self.type = type - self.widgetId = widgetId - self.timestamp = timestamp - self.metadata = metadata - } -} - -/// Analytics event types -public enum AnalyticsEventType: String, CaseIterable { - case widgetUpdate = "widget_update" - case templateRegistration = "template_registration" - case dataSourceRegistration = "data_source_registration" - case webSocketConnection = "websocket_connection" - case webSocketDisconnection = "websocket_disconnection" - case dataSent = "data_sent" - case dataSourceUpdate = "data_source_update" - case dataSourceConfiguration = "data_source_configuration" - case connectionEstablished = "connection_established" - case connectionFailed = "connection_failed" - case connectionCancelled = "connection_cancelled" - case connectionRetry = "connection_retry" - case reconnectionAttempt = "reconnection_attempt" - case dataReceiveError = "data_receive_error" - case dataDecodeError = "data_decode_error" - case networkUnavailable = "network_unavailable" - case memoryCleanup = "memory_cleanup" - case batteryOptimization = "battery_optimization" - case error = "error" - case engineInitialization = "engine_initialization" -} - -/// Analytics configuration -public struct AnalyticsConfiguration { - public let enableExternalAnalytics: Bool - public let maxEventLogSize: Int - public let enablePerformanceTracking: Bool - public let enableErrorTracking: Bool - - public init(enableExternalAnalytics: Bool = false, maxEventLogSize: Int = 1000, enablePerformanceTracking: Bool = true, enableErrorTracking: Bool = true) { - self.enableExternalAnalytics = enableExternalAnalytics - self.maxEventLogSize = maxEventLogSize - self.enablePerformanceTracking = enablePerformanceTracking - self.enableErrorTracking = enableErrorTracking - } -} - -/// Widget error types -public enum WidgetError: Error, LocalizedError { - case templateNotFound(String) - case dataSourceNotFound(String) - case connectionFailed(String) - case dataDecodeFailed(String) - case networkUnavailable - case unknown - - public var errorDescription: String? { - switch self { - case .templateNotFound(let templateId): - return "Template not found: \(templateId)" - case .dataSourceNotFound(let dataSourceId): - return "Data source not found: \(dataSourceId)" - case .connectionFailed(let reason): - return "Connection failed: \(reason)" - case .dataDecodeFailed(let reason): - return "Data decode failed: \(reason)" - case .networkUnavailable: - return "Network unavailable" - case .unknown: - return "Unknown error occurred" - } - } - - public var localizedDescription: String { - return errorDescription ?? "Unknown error" - } -} \ No newline at end of file diff --git a/Sources/Analytics/WidgetErrorHandler.swift b/Sources/Analytics/WidgetErrorHandler.swift deleted file mode 100644 index 6c53c54..0000000 --- a/Sources/Analytics/WidgetErrorHandler.swift +++ /dev/null @@ -1,208 +0,0 @@ -import Foundation -import Combine - -/// Widget Error Handler -/// Provides comprehensive error handling and reporting for widgets -@available(iOS 16.0, *) -public class WidgetErrorHandler: ObservableObject { - - // MARK: - Properties - - /// Error publisher - private let errorSubject = PassthroughSubject() - public var errorPublisher: AnyPublisher { - return errorSubject.eraseToAnyPublisher() - } - - /// Error log - private var errorLog: [ErrorLogEntry] = [] - - /// Error configuration - private var configuration = ErrorHandlerConfiguration() - - /// Combine cancellables - private var cancellables = Set() - - // MARK: - Initialization - - public init() { - setupErrorHandling() - } - - // MARK: - Public Methods - - /// Handle widget error - /// - Parameter error: Widget error - public func handleError(_ error: WidgetError) { - let logEntry = ErrorLogEntry( - error: error, - timestamp: Date(), - context: getCurrentContext() - ) - - errorLog.append(logEntry) - - // Limit error log size - if errorLog.count > configuration.maxErrorLogSize { - errorLog.removeFirst(errorLog.count - configuration.maxErrorLogSize) - } - - // Send error to publisher - errorSubject.send(error) - - // Log error if enabled - if configuration.enableErrorLogging { - logError(logEntry) - } - - // Send to external error reporting if enabled - if configuration.enableExternalErrorReporting { - sendToExternalErrorReporting(logEntry) - } - } - - /// Get error log - /// - Returns: Array of error log entries - public func getErrorLog() -> [ErrorLogEntry] { - return errorLog - } - - /// Clear error log - public func clearErrorLog() { - errorLog.removeAll() - } - - /// Configure error handler - /// - Parameter config: Error handler configuration - public func configure(_ config: ErrorHandlerConfiguration) { - configuration = config - } - - /// Get error statistics - /// - Returns: Error statistics - public func getErrorStatistics() -> ErrorStatistics { - var statistics = ErrorStatistics() - - for entry in errorLog { - statistics.totalErrors += 1 - - switch entry.error { - case .templateNotFound: - statistics.templateNotFoundErrors += 1 - case .dataSourceNotFound: - statistics.dataSourceNotFoundErrors += 1 - case .connectionFailed: - statistics.connectionFailedErrors += 1 - case .dataDecodeFailed: - statistics.dataDecodeFailedErrors += 1 - case .networkUnavailable: - statistics.networkUnavailableErrors += 1 - case .unknown: - statistics.unknownErrors += 1 - } - } - - return statistics - } - - // MARK: - Private Methods - - private func setupErrorHandling() { - // Setup error monitoring - Timer.publish(every: 300.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - self?.cleanupOldErrors() - } - .store(in: &cancellables) - } - - private func getCurrentContext() -> [String: Any] { - return [ - "timestamp": Date(), - "memoryUsage": getCurrentMemoryUsage(), - "batteryLevel": getCurrentBatteryLevel() - ] - } - - private func getCurrentMemoryUsage() -> UInt64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size)/4 - - let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, - task_flavor_t(MACH_TASK_BASIC_INFO), - $0, - &count) - } - } - - return kerr == KERN_SUCCESS ? info.resident_size : 0 - } - - private func getCurrentBatteryLevel() -> Float { - UIDevice.current.isBatteryMonitoringEnabled = true - return UIDevice.current.batteryLevel - } - - private func logError(_ entry: ErrorLogEntry) { - // Implementation for local error logging - print("Widget Error: \(entry.error.localizedDescription) at \(entry.timestamp)") - } - - private func sendToExternalErrorReporting(_ entry: ErrorLogEntry) { - // Implementation for external error reporting - // This could send data to Crashlytics, Sentry, etc. - } - - private func cleanupOldErrors() { - let cutoffDate = Date().addingTimeInterval(-configuration.errorRetentionPeriod) - errorLog.removeAll { entry in - entry.timestamp < cutoffDate - } - } -} - -// MARK: - Supporting Types - -/// Error log entry -public struct ErrorLogEntry { - public let error: WidgetError - public let timestamp: Date - public let context: [String: Any] - - public init(error: WidgetError, timestamp: Date, context: [String: Any]) { - self.error = error - self.timestamp = timestamp - self.context = context - } -} - -/// Error statistics -public struct ErrorStatistics { - public var totalErrors: Int = 0 - public var templateNotFoundErrors: Int = 0 - public var dataSourceNotFoundErrors: Int = 0 - public var connectionFailedErrors: Int = 0 - public var dataDecodeFailedErrors: Int = 0 - public var networkUnavailableErrors: Int = 0 - public var unknownErrors: Int = 0 - - public init() {} -} - -/// Error handler configuration -public struct ErrorHandlerConfiguration { - public let enableErrorLogging: Bool - public let enableExternalErrorReporting: Bool - public let maxErrorLogSize: Int - public let errorRetentionPeriod: TimeInterval - - public init(enableErrorLogging: Bool = true, enableExternalErrorReporting: Bool = false, maxErrorLogSize: Int = 100, errorRetentionPeriod: TimeInterval = 86400) { - self.enableErrorLogging = enableErrorLogging - self.enableExternalErrorReporting = enableExternalErrorReporting - self.maxErrorLogSize = maxErrorLogSize - self.errorRetentionPeriod = errorRetentionPeriod - } -} \ No newline at end of file diff --git a/Sources/Charts/WidgetCharts.swift b/Sources/Charts/WidgetCharts.swift deleted file mode 100644 index 0c4ce13..0000000 --- a/Sources/Charts/WidgetCharts.swift +++ /dev/null @@ -1,506 +0,0 @@ -// WidgetCharts.swift -// iOS-Widget-Development-Kit -// -// SwiftUI Charts Support for Widgets -// Created by Muhittin Camdali - -import SwiftUI -import Charts - -// MARK: - Chart Data Types - -/// Generic data point for charts -public struct ChartDataPoint: Identifiable { - public let id = UUID() - public let label: String - public let value: Double - public let date: Date? - public let category: String? - - public init(label: String, value: Double, date: Date? = nil, category: String? = nil) { - self.label = label - self.value = value - self.date = date - self.category = category - } -} - -/// Time series data point -public struct TimeSeriesDataPoint: Identifiable { - public let id = UUID() - public let date: Date - public let value: Double - public let series: String? - - public init(date: Date, value: Double, series: String? = nil) { - self.date = date - self.value = value - self.series = series - } -} - -// MARK: - Widget Bar Chart - -/// Compact bar chart for widgets -@available(iOS 16.0, *) -public struct WidgetBarChart: View { - let data: [ChartDataPoint] - let accentColor: Color - let showLabels: Bool - let showValues: Bool - - public init( - data: [ChartDataPoint], - accentColor: Color = .blue, - showLabels: Bool = true, - showValues: Bool = false - ) { - self.data = data - self.accentColor = accentColor - self.showLabels = showLabels - self.showValues = showValues - } - - public var body: some View { - Chart(data) { point in - BarMark( - x: .value("Category", point.label), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor.gradient) - .cornerRadius(4) - - if showValues { - RuleMark(y: .value("Value", point.value)) - .lineStyle(StrokeStyle(lineWidth: 0)) - .annotation(position: .top) { - Text("\(Int(point.value))") - .font(.system(size: 8)) - .foregroundStyle(.secondary) - } - } - } - .chartXAxis(showLabels ? .automatic : .hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Widget Line Chart - -/// Compact line chart for widgets -@available(iOS 16.0, *) -public struct WidgetLineChart: View { - let data: [TimeSeriesDataPoint] - let accentColor: Color - let showArea: Bool - let showPoints: Bool - let showXAxis: Bool - - public init( - data: [TimeSeriesDataPoint], - accentColor: Color = .blue, - showArea: Bool = true, - showPoints: Bool = false, - showXAxis: Bool = false - ) { - self.data = data - self.accentColor = accentColor - self.showArea = showArea - self.showPoints = showPoints - self.showXAxis = showXAxis - } - - public var body: some View { - Chart(data) { point in - LineMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor) - .interpolationMethod(.catmullRom) - - if showArea { - AreaMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor.opacity(0.2).gradient) - .interpolationMethod(.catmullRom) - } - - if showPoints { - PointMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(accentColor) - } - } - .chartXAxis(showXAxis ? .automatic : .hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Widget Sparkline - -/// Minimal sparkline chart for widgets -@available(iOS 16.0, *) -public struct WidgetSparkline: View { - let values: [Double] - let color: Color - let lineWidth: CGFloat - let showGradient: Bool - - public init( - values: [Double], - color: Color = .blue, - lineWidth: CGFloat = 2, - showGradient: Bool = true - ) { - self.values = values - self.color = color - self.lineWidth = lineWidth - self.showGradient = showGradient - } - - private var dataPoints: [TimeSeriesDataPoint] { - values.enumerated().map { index, value in - TimeSeriesDataPoint( - date: Date().addingTimeInterval(Double(index) * 3600), - value: value - ) - } - } - - public var body: some View { - Chart(dataPoints) { point in - LineMark( - x: .value("Index", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(color) - .lineStyle(StrokeStyle(lineWidth: lineWidth, lineCap: .round)) - .interpolationMethod(.catmullRom) - - if showGradient { - AreaMark( - x: .value("Index", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle( - LinearGradient( - colors: [color.opacity(0.3), color.opacity(0)], - startPoint: .top, - endPoint: .bottom - ) - ) - .interpolationMethod(.catmullRom) - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Widget Pie Chart - -/// Compact pie chart for widgets -@available(iOS 16.0, *) -public struct WidgetPieChart: View { - let data: [ChartDataPoint] - let colors: [Color] - let showLabels: Bool - let innerRadiusRatio: Double - - public init( - data: [ChartDataPoint], - colors: [Color]? = nil, - showLabels: Bool = false, - innerRadiusRatio: Double = 0 - ) { - self.data = data - self.colors = colors ?? [.blue, .green, .orange, .red, .purple, .pink, .cyan, .yellow] - self.showLabels = showLabels - self.innerRadiusRatio = innerRadiusRatio - } - - public var body: some View { - Chart(data) { point in - SectorMark( - angle: .value("Value", point.value), - innerRadius: .ratio(innerRadiusRatio), - angularInset: 1 - ) - .foregroundStyle(by: .value("Category", point.label)) - .cornerRadius(4) - } - .chartLegend(showLabels ? .visible : .hidden) - .chartForegroundStyleScale(domain: data.map { $0.label }, range: colors) - } -} - -// MARK: - Widget Progress Ring Chart - -/// Circular progress chart -@available(iOS 16.0, *) -public struct WidgetProgressRing: View { - let progress: Double - let total: Double - let colors: [Color] - let lineWidth: CGFloat - let showValue: Bool - let valueFormat: String - - public init( - progress: Double, - total: Double = 1.0, - colors: [Color] = [.blue], - lineWidth: CGFloat = 12, - showValue: Bool = true, - valueFormat: String = "%.0f%%" - ) { - self.progress = progress - self.total = total - self.colors = colors - self.lineWidth = lineWidth - self.showValue = showValue - self.valueFormat = valueFormat - } - - private var normalizedProgress: Double { - min(max(progress / total, 0), 1) - } - - private var gradient: AngularGradient { - AngularGradient( - colors: colors + [colors.first!], - center: .center, - startAngle: .degrees(-90), - endAngle: .degrees(270) - ) - } - - public var body: some View { - ZStack { - Circle() - .stroke(colors.first!.opacity(0.2), lineWidth: lineWidth) - - Circle() - .trim(from: 0, to: normalizedProgress) - .stroke( - colors.count == 1 ? AnyShapeStyle(colors.first!) : AnyShapeStyle(gradient), - style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) - ) - .rotationEffect(.degrees(-90)) - - if showValue { - Text(String(format: valueFormat, normalizedProgress * 100)) - .font(.system(size: lineWidth * 1.5, weight: .bold, design: .rounded)) - } - } - } -} - -// MARK: - Multi-Line Chart - -/// Line chart with multiple series -@available(iOS 16.0, *) -public struct WidgetMultiLineChart: View { - let series: [String: [TimeSeriesDataPoint]] - let colors: [String: Color] - let showLegend: Bool - - public init( - series: [String: [TimeSeriesDataPoint]], - colors: [String: Color], - showLegend: Bool = true - ) { - self.series = series - self.colors = colors - self.showLegend = showLegend - } - - private var allDataPoints: [TimeSeriesDataPoint] { - series.flatMap { seriesName, points in - points.map { point in - TimeSeriesDataPoint(date: point.date, value: point.value, series: seriesName) - } - } - } - - public var body: some View { - Chart(allDataPoints) { point in - LineMark( - x: .value("Date", point.date), - y: .value("Value", point.value) - ) - .foregroundStyle(by: .value("Series", point.series ?? "")) - .interpolationMethod(.catmullRom) - } - .chartForegroundStyleScale(domain: Array(colors.keys), range: Array(colors.values)) - .chartLegend(showLegend ? .visible : .hidden) - .chartXAxis(.hidden) - .chartYAxis(.hidden) - } -} - -// MARK: - Stats Chart Widget - -/// Combined stats with chart -@available(iOS 16.0, *) -public struct WidgetStatsChart: View { - let title: String - let value: String - let change: Double - let sparklineData: [Double] - let accentColor: Color - - public init( - title: String, - value: String, - change: Double, - sparklineData: [Double], - accentColor: Color = .blue - ) { - self.title = title - self.value = value - self.change = change - self.sparklineData = sparklineData - self.accentColor = accentColor - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - - HStack(alignment: .firstTextBaseline) { - Text(value) - .font(.title2) - .fontWeight(.bold) - - HStack(spacing: 2) { - Image(systemName: change >= 0 ? "arrow.up.right" : "arrow.down.right") - Text(String(format: "%.1f%%", abs(change))) - } - .font(.caption) - .foregroundStyle(change >= 0 ? .green : .red) - } - - WidgetSparkline(values: sparklineData, color: accentColor) - .frame(height: 40) - } - } -} - -// MARK: - Horizontal Bar Chart - -/// Horizontal bar chart for comparisons -@available(iOS 16.0, *) -public struct WidgetHorizontalBarChart: View { - let data: [ChartDataPoint] - let accentColor: Color - let showValues: Bool - let maxValue: Double? - - public init( - data: [ChartDataPoint], - accentColor: Color = .blue, - showValues: Bool = true, - maxValue: Double? = nil - ) { - self.data = data - self.accentColor = accentColor - self.showValues = showValues - self.maxValue = maxValue - } - - private var normalizedMax: Double { - maxValue ?? (data.map { $0.value }.max() ?? 1) - } - - public var body: some View { - VStack(spacing: 8) { - ForEach(data) { point in - HStack(spacing: 8) { - Text(point.label) - .font(.caption) - .frame(width: 60, alignment: .leading) - .lineLimit(1) - - GeometryReader { geo in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 4) - .fill(accentColor.opacity(0.2)) - - RoundedRectangle(cornerRadius: 4) - .fill(accentColor.gradient) - .frame(width: geo.size.width * (point.value / normalizedMax)) - } - } - .frame(height: 16) - - if showValues { - Text("\(Int(point.value))") - .font(.caption) - .foregroundStyle(.secondary) - .frame(width: 30, alignment: .trailing) - } - } - } - } - } -} - -// MARK: - Activity Chart - -/// Activity/contribution chart (like GitHub) -@available(iOS 16.0, *) -public struct WidgetActivityChart: View { - let activities: [[Double]] // Weeks x Days - let colorRange: [Color] - let cellSize: CGFloat - let spacing: CGFloat - - public init( - activities: [[Double]], - colorRange: [Color] = [Color.gray.opacity(0.2), .green], - cellSize: CGFloat = 10, - spacing: CGFloat = 2 - ) { - self.activities = activities - self.colorRange = colorRange - self.cellSize = cellSize - self.spacing = spacing - } - - private func colorForValue(_ value: Double) -> Color { - if value <= 0 { return colorRange.first! } - let maxValue = activities.flatMap { $0 }.max() ?? 1 - let normalized = min(value / maxValue, 1) - - // Interpolate between colors - let index = normalized * Double(colorRange.count - 1) - let lowerIndex = Int(index) - let upperIndex = min(lowerIndex + 1, colorRange.count - 1) - let fraction = index - Double(lowerIndex) - - // Simple color interpolation - return colorRange[upperIndex].opacity(0.3 + normalized * 0.7) - } - - public var body: some View { - HStack(spacing: spacing) { - ForEach(activities.indices, id: \.self) { weekIndex in - VStack(spacing: spacing) { - ForEach(activities[weekIndex].indices, id: \.self) { dayIndex in - RoundedRectangle(cornerRadius: 2) - .fill(colorForValue(activities[weekIndex][dayIndex])) - .frame(width: cellSize, height: cellSize) - } - } - } - } - } -} diff --git a/Sources/Configuration/WidgetConfiguration.swift b/Sources/Configuration/WidgetConfiguration.swift deleted file mode 100644 index 9e1df62..0000000 --- a/Sources/Configuration/WidgetConfiguration.swift +++ /dev/null @@ -1,423 +0,0 @@ -// WidgetConfiguration.swift -// iOS-Widget-Development-Kit -// -// Widget Configuration and Deep Linking Support -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit -import AppIntents - -// MARK: - Deep Link Manager - -/// Handles widget deep links to main app -public final class WidgetDeepLinkManager { - public static let shared = WidgetDeepLinkManager() - - public typealias DeepLinkHandler = (URL) -> Void - private var handlers: [String: DeepLinkHandler] = [:] - - private init() {} - - /// Build a deep link URL - public func buildURL( - scheme: String = "widget", - host: String, - path: String = "", - parameters: [String: String] = [:] - ) -> URL? { - var components = URLComponents() - components.scheme = scheme - components.host = host - components.path = path.isEmpty ? "" : "/\(path)" - - if !parameters.isEmpty { - components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) } - } - - return components.url - } - - /// Build URL for specific widget action - public func buildActionURL( - action: String, - widgetKind: String, - parameters: [String: String] = [] - ) -> URL? { - var params = parameters - params["action"] = action - params["widgetKind"] = widgetKind - return buildURL(host: "action", parameters: params) - } - - /// Build URL to open specific content - public func buildContentURL( - contentType: String, - contentId: String, - parameters: [String: String] = [:] - ) -> URL? { - var params = parameters - params["type"] = contentType - params["id"] = contentId - return buildURL(host: "content", parameters: params) - } - - /// Register handler for deep link - public func registerHandler(for host: String, handler: @escaping DeepLinkHandler) { - handlers[host] = handler - } - - /// Handle incoming deep link - @discardableResult - public func handle(url: URL) -> Bool { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let host = components.host else { - return false - } - - if let handler = handlers[host] { - handler(url) - return true - } - - return false - } - - /// Parse parameters from URL - public func parseParameters(from url: URL) -> [String: String] { - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let queryItems = components.queryItems else { - return [:] - } - - var params: [String: String] = [:] - for item in queryItems { - if let value = item.value { - params[item.name] = value - } - } - return params - } -} - -// MARK: - Widget Link Builder - -/// Convenience builder for widget links -@available(iOS 17.0, *) -public struct WidgetLinkBuilder { - - /// Create a link that opens the app - public static func openApp( - @ViewBuilder content: () -> Content - ) -> some View { - Link(destination: URL(string: "widget://app")!) { - content() - } - } - - /// Create a link with specific action - public static func action( - _ action: String, - parameters: [String: String] = [:], - @ViewBuilder content: () -> Content - ) -> some View { - let url = WidgetDeepLinkManager.shared.buildActionURL( - action: action, - widgetKind: "", - parameters: parameters - ) ?? URL(string: "widget://app")! - - return Link(destination: url) { - content() - } - } - - /// Create a link to specific content - public static func content( - type: String, - id: String, - @ViewBuilder content: () -> Content - ) -> some View { - let url = WidgetDeepLinkManager.shared.buildContentURL( - contentType: type, - contentId: id - ) ?? URL(string: "widget://app")! - - return Link(destination: url) { - content() - } - } -} - -// MARK: - Widget Configuration Protocol - -/// Protocol for configurable widgets -public protocol ConfigurableWidgetProtocol { - associatedtype Configuration: WidgetConfigurationIntent - var configuration: Configuration { get } -} - -// MARK: - Configuration Options - -/// Common widget configuration options -public struct WidgetConfigurationOptions: Codable { - public var refreshInterval: Int - public var showTitle: Bool - public var accentColorHex: String - public var fontSize: FontSize - public var layoutStyle: LayoutStyle - - public enum FontSize: String, Codable, CaseIterable { - case small, medium, large - } - - public enum LayoutStyle: String, Codable, CaseIterable { - case compact, standard, expanded - } - - public init( - refreshInterval: Int = 15, - showTitle: Bool = true, - accentColorHex: String = "#007AFF", - fontSize: FontSize = .medium, - layoutStyle: LayoutStyle = .standard - ) { - self.refreshInterval = refreshInterval - self.showTitle = showTitle - self.accentColorHex = accentColorHex - self.fontSize = fontSize - self.layoutStyle = layoutStyle - } - - public static let `default` = WidgetConfigurationOptions() -} - -// MARK: - Configuration Storage - -/// Manages widget configuration persistence -public final class WidgetConfigurationStorage { - public static let shared = WidgetConfigurationStorage() - - private let defaults: UserDefaults - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private init() { - defaults = UserDefaults(suiteName: "group.widget.configuration") ?? .standard - } - - /// Save configuration - public func save(_ configuration: T, for widgetId: String) throws { - let data = try encoder.encode(configuration) - defaults.set(data, forKey: "config_\(widgetId)") - } - - /// Load configuration - public func load(_ type: T.Type, for widgetId: String) throws -> T? { - guard let data = defaults.data(forKey: "config_\(widgetId)") else { - return nil - } - return try decoder.decode(type, from: data) - } - - /// Remove configuration - public func remove(for widgetId: String) { - defaults.removeObject(forKey: "config_\(widgetId)") - } - - /// Get all configured widget IDs - public func allConfiguredWidgetIds() -> [String] { - defaults.dictionaryRepresentation().keys - .filter { $0.hasPrefix("config_") } - .map { String($0.dropFirst(7)) } - } -} - -// MARK: - App Intent Configurations - -/// Base configuration intent for widgets -@available(iOS 17.0, *) -public protocol WidgetConfigurationIntent: AppIntent { - static var intentIdentifier: String { get } -} - -/// Refresh interval configuration -@available(iOS 17.0, *) -public struct RefreshIntervalConfiguration: AppEntity { - public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Refresh Interval" - public static var defaultQuery = RefreshIntervalQuery() - - public var id: Int - public var displayName: String - - public var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: LocalizedStringResource(stringLiteral: displayName)) - } - - public static let intervals: [RefreshIntervalConfiguration] = [ - RefreshIntervalConfiguration(id: 5, displayName: "5 minutes"), - RefreshIntervalConfiguration(id: 15, displayName: "15 minutes"), - RefreshIntervalConfiguration(id: 30, displayName: "30 minutes"), - RefreshIntervalConfiguration(id: 60, displayName: "1 hour") - ] - - public init(id: Int, displayName: String) { - self.id = id - self.displayName = displayName - } -} - -@available(iOS 17.0, *) -public struct RefreshIntervalQuery: EntityQuery { - public init() {} - - public func entities(for identifiers: [Int]) async throws -> [RefreshIntervalConfiguration] { - RefreshIntervalConfiguration.intervals.filter { identifiers.contains($0.id) } - } - - public func suggestedEntities() async throws -> [RefreshIntervalConfiguration] { - RefreshIntervalConfiguration.intervals - } -} - -// MARK: - Color Configuration - -/// Color selection for widgets -@available(iOS 17.0, *) -public struct ColorConfiguration: AppEntity { - public static var typeDisplayRepresentation: TypeDisplayRepresentation = "Accent Color" - public static var defaultQuery = ColorConfigurationQuery() - - public var id: String - public var displayName: String - public var hexValue: String - - public var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: LocalizedStringResource(stringLiteral: displayName)) - } - - public var color: Color { - Color(hex: hexValue) ?? .blue - } - - public static let colors: [ColorConfiguration] = [ - ColorConfiguration(id: "blue", displayName: "Blue", hexValue: "#007AFF"), - ColorConfiguration(id: "green", displayName: "Green", hexValue: "#34C759"), - ColorConfiguration(id: "red", displayName: "Red", hexValue: "#FF3B30"), - ColorConfiguration(id: "orange", displayName: "Orange", hexValue: "#FF9500"), - ColorConfiguration(id: "purple", displayName: "Purple", hexValue: "#AF52DE"), - ColorConfiguration(id: "pink", displayName: "Pink", hexValue: "#FF2D55"), - ColorConfiguration(id: "teal", displayName: "Teal", hexValue: "#5AC8FA"), - ColorConfiguration(id: "indigo", displayName: "Indigo", hexValue: "#5856D6") - ] - - public init(id: String, displayName: String, hexValue: String) { - self.id = id - self.displayName = displayName - self.hexValue = hexValue - } -} - -@available(iOS 17.0, *) -public struct ColorConfigurationQuery: EntityQuery { - public init() {} - - public func entities(for identifiers: [String]) async throws -> [ColorConfiguration] { - ColorConfiguration.colors.filter { identifiers.contains($0.id) } - } - - public func suggestedEntities() async throws -> [ColorConfiguration] { - ColorConfiguration.colors - } -} - -// MARK: - Color Hex Extension - -extension Color { - /// Initialize Color from hex string - public init?(hex: String) { - var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) - hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") - - var rgb: UInt64 = 0 - - guard Scanner(string: hexSanitized).scanHexInt64(&rgb) else { - return nil - } - - let r = Double((rgb & 0xFF0000) >> 16) / 255.0 - let g = Double((rgb & 0x00FF00) >> 8) / 255.0 - let b = Double(rgb & 0x0000FF) / 255.0 - - self.init(red: r, green: g, blue: b) - } - - /// Convert Color to hex string - public var hexString: String? { - #if canImport(UIKit) - guard let components = UIColor(self).cgColor.components else { return nil } - #else - guard let components = NSColor(self).cgColor.components else { return nil } - #endif - - let r = Int(components[0] * 255) - let g = Int(components[1] * 255) - let b = Int(components[2] * 255) - - return String(format: "#%02X%02X%02X", r, g, b) - } -} - -// MARK: - Widget URL Handler View Modifier - -/// View modifier to handle widget URLs -@available(iOS 17.0, *) -public struct WidgetURLHandler: ViewModifier { - let handler: (URL) -> Void - - public func body(content: Content) -> some View { - content - .onOpenURL { url in - handler(url) - } - } -} - -@available(iOS 17.0, *) -extension View { - /// Handle widget deep links - public func onWidgetURL(_ handler: @escaping (URL) -> Void) -> some View { - modifier(WidgetURLHandler(handler: handler)) - } -} - -// MARK: - Configuration Migration - -/// Handles migration of widget configurations between versions -public final class ConfigurationMigrationManager { - public static let shared = ConfigurationMigrationManager() - - private let defaults = UserDefaults(suiteName: "group.widget.configuration") ?? .standard - private let versionKey = "config_version" - - private init() {} - - /// Current configuration version - public var currentVersion: Int { - get { defaults.integer(forKey: versionKey) } - set { defaults.set(newValue, forKey: versionKey) } - } - - /// Run migrations if needed - public func migrateIfNeeded(targetVersion: Int, migrations: [Int: () -> Void]) { - guard currentVersion < targetVersion else { return } - - for version in (currentVersion + 1)...targetVersion { - if let migration = migrations[version] { - migration() - } - } - - currentVersion = targetVersion - } -} diff --git a/Sources/Enterprise/AIManager.swift b/Sources/Enterprise/AIManager.swift deleted file mode 100644 index 203ddb2..0000000 --- a/Sources/Enterprise/AIManager.swift +++ /dev/null @@ -1,1013 +0,0 @@ -import Foundation -import CoreML -import Combine -import OSLog -import CreateML - -/// Enterprise-grade AI manager providing machine learning capabilities, -/// intelligent widget optimization, and predictive analytics for iOS widgets -@available(iOS 16.0, *) -public class AIManager: ObservableObject { - - // MARK: - Types - - public enum AIError: LocalizedError { - case modelNotLoaded - case predictionFailed(String) - case invalidInput - case modelTrainingFailed - case coreMLNotAvailable - case insufficientData - case learningDisabled - case configurationInvalid - - public var errorDescription: String? { - switch self { - case .modelNotLoaded: - return "AI model is not loaded" - case .predictionFailed(let reason): - return "Prediction failed: \(reason)" - case .invalidInput: - return "Invalid input data for AI processing" - case .modelTrainingFailed: - return "Model training failed" - case .coreMLNotAvailable: - return "Core ML is not available on this device" - case .insufficientData: - return "Insufficient data for AI operations" - case .learningDisabled: - return "Machine learning is disabled" - case .configurationInvalid: - return "AI configuration is invalid" - } - } - } - - public enum ModelType: String, CaseIterable, Codable { - case widgetUsagePrediction = "widget_usage_prediction" - case contentRecommendation = "content_recommendation" - case performanceOptimization = "performance_optimization" - case userBehaviorAnalysis = "user_behavior_analysis" - case anomalyDetection = "anomaly_detection" - case sentimentAnalysis = "sentiment_analysis" - case timeSeriesForecasting = "time_series_forecasting" - case clusteringAnalysis = "clustering_analysis" - - public var description: String { - switch self { - case .widgetUsagePrediction: - return "Predicts widget usage patterns and trends" - case .contentRecommendation: - return "Recommends personalized content for widgets" - case .performanceOptimization: - return "Optimizes widget performance based on usage data" - case .userBehaviorAnalysis: - return "Analyzes user behavior patterns and preferences" - case .anomalyDetection: - return "Detects unusual patterns and anomalies" - case .sentimentAnalysis: - return "Analyzes sentiment in user interactions" - case .timeSeriesForecasting: - return "Forecasts future trends and patterns" - case .clusteringAnalysis: - return "Groups similar users and behaviors" - } - } - } - - public struct AIConfiguration { - public var enableAI: Bool = true - public var enableLearning: Bool = true - public var enablePersonalization: Bool = true - public var enablePredictions: Bool = true - public var maxTrainingDataPoints: Int = 10000 - public var modelUpdateInterval: TimeInterval = 86400 // 24 hours - public var predictionConfidenceThreshold: Double = 0.7 - public var privacyLevel: PrivacyLevel = .balanced - public var enableFederatedLearning: Bool = false - public var enableEdgeComputing: Bool = true - - public enum PrivacyLevel: String, CaseIterable { - case strict = "strict" - case balanced = "balanced" - case minimal = "minimal" - - public var dataRetentionDays: Int { - switch self { - case .strict: return 7 - case .balanced: return 30 - case .minimal: return 90 - } - } - } - - public init() {} - } - - public struct PredictionResult: Codable { - public let modelType: ModelType - public let prediction: Double - public let confidence: Double - public let features: [String: Double] - public let metadata: [String: String] - public let timestamp: Date - public let explanation: String? - - public var isReliable: Bool { - return confidence >= 0.7 - } - - public init( - modelType: ModelType, - prediction: Double, - confidence: Double, - features: [String: Double], - metadata: [String: String] = [:], - explanation: String? = nil - ) { - self.modelType = modelType - self.prediction = prediction - self.confidence = confidence - self.features = features - self.metadata = metadata - self.timestamp = Date() - self.explanation = explanation - } - } - - public struct UserBehaviorProfile: Codable { - public let userId: String - public let preferredWidgetTypes: [String] - public let usagePatterns: [String: Double] - public let timePreferences: [String: Double] - public let interactionFrequency: Double - public let sessionDuration: Double - public let deviceUsageContext: [String: Double] - public let personalizedFeatures: [String: Double] - public let lastUpdated: Date - - public var engagementScore: Double { - let frequency = interactionFrequency - let duration = sessionDuration / 3600 // Convert to hours - let consistency = usagePatterns.values.reduce(0, +) / Double(usagePatterns.count) - - return (frequency * 0.4 + duration * 0.3 + consistency * 0.3).clamped(to: 0...1) - } - } - - public struct ContentRecommendation: Codable, Identifiable { - public let id: String - public let contentType: String - public let title: String - public let description: String - public let relevanceScore: Double - public let confidenceScore: Double - public let reasoning: [String] - public let metadata: [String: String] - public let expiresAt: Date - - public var isExpired: Bool { - return Date() > expiresAt - } - - public init( - contentType: String, - title: String, - description: String, - relevanceScore: Double, - confidenceScore: Double, - reasoning: [String] = [], - metadata: [String: String] = [:], - validityDuration: TimeInterval = 3600 - ) { - self.id = UUID().uuidString - self.contentType = contentType - self.title = title - self.description = description - self.relevanceScore = relevanceScore - self.confidenceScore = confidenceScore - self.reasoning = reasoning - self.metadata = metadata - self.expiresAt = Date().addingTimeInterval(validityDuration) - } - } - - public struct ModelMetrics: Codable { - public let modelType: ModelType - public let accuracy: Double - public let precision: Double - public let recall: Double - public let f1Score: Double - public let trainingDataSize: Int - public let lastTrainingDate: Date - public let predictionCount: Int - public let averageConfidence: Double - - public var qualityScore: Double { - return (accuracy * 0.3 + precision * 0.25 + recall * 0.25 + f1Score * 0.2).clamped(to: 0...1) - } - } - - // MARK: - Properties - - public static let shared = AIManager() - - private let modelRegistry: MLModelRegistry - private let featureExtractor: FeatureExtractor - private let predictionEngine: PredictionEngine - private let learningEngine: LearningEngine - private let recommendationEngine: RecommendationEngine - private let behaviorAnalyzer: BehaviorAnalyzer - private let modelTrainer: ModelTrainer - private let privacyManager: AIPrivacyManager - - @Published public private(set) var configuration: AIConfiguration = AIConfiguration() - @Published public private(set) var isAIEnabled: Bool = false - @Published public private(set) var loadedModels: Set = [] - @Published public private(set) var modelMetrics: [ModelType: ModelMetrics] = [:] - @Published public private(set) var userProfiles: [String: UserBehaviorProfile] = [:] - @Published public private(set) var recentPredictions: [PredictionResult] = [] - @Published public private(set) var recommendationCache: [ContentRecommendation] = [] - - private var trainingTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "ai.manager", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "AI") - - // MARK: - Initialization - - private init() { - self.modelRegistry = MLModelRegistry() - self.featureExtractor = FeatureExtractor() - self.predictionEngine = PredictionEngine() - self.learningEngine = LearningEngine() - self.recommendationEngine = RecommendationEngine() - self.behaviorAnalyzer = BehaviorAnalyzer() - self.modelTrainer = ModelTrainer() - self.privacyManager = AIPrivacyManager() - - setupAIManager() - } - - // MARK: - Public Methods - - /// Initializes and starts the AI system - public func startAI() async throws { - guard configuration.enableAI else { - throw AIError.learningDisabled - } - - guard await checkCoreMLAvailability() else { - throw AIError.coreMLNotAvailable - } - - await MainActor.run { - self.isAIEnabled = true - } - - // Load pre-trained models - await loadInitialModels() - - // Setup automatic training - if configuration.enableLearning { - setupAutomaticTraining() - } - - // Initialize privacy compliance - await privacyManager.initialize(configuration: configuration) - - logger.info("AI Manager started successfully") - } - - /// Stops the AI system - public func stopAI() async { - await MainActor.run { - self.isAIEnabled = false - } - - trainingTimer?.invalidate() - trainingTimer = nil - - await unloadAllModels() - - logger.info("AI Manager stopped") - } - - /// Makes a prediction using the specified model - public func predict( - modelType: ModelType, - features: [String: Double], - metadata: [String: String] = [:] - ) async throws -> PredictionResult { - guard isAIEnabled else { - throw AIError.learningDisabled - } - - guard loadedModels.contains(modelType) else { - // Try to load the model - try await loadModel(modelType) - } - - let result = try await predictionEngine.predict( - modelType: modelType, - features: features, - metadata: metadata - ) - - // Store prediction for learning - await storePredictionResult(result) - - // Update metrics - await updateModelMetrics(for: modelType, prediction: result) - - return result - } - - /// Trains a model with new data - public func trainModel( - modelType: ModelType, - trainingData: [[String: Double]], - labels: [Double] - ) async throws { - guard configuration.enableLearning else { - throw AIError.learningDisabled - } - - guard trainingData.count == labels.count else { - throw AIError.invalidInput - } - - // Privacy compliance check - let sanitizedData = await privacyManager.sanitizeTrainingData(trainingData) - - try await modelTrainer.trainModel( - type: modelType, - features: sanitizedData, - labels: labels, - configuration: configuration - ) - - // Update model metrics - await updateModelMetricsAfterTraining(modelType) - - logger.info("Model \(modelType.rawValue) training completed") - } - - /// Analyzes user behavior and updates profile - public func analyzeUserBehavior( - userId: String, - interactions: [UserInteraction], - context: [String: String] = [:] - ) async throws -> UserBehaviorProfile { - guard isAIEnabled else { - throw AIError.learningDisabled - } - - let profile = try await behaviorAnalyzer.analyzeUser( - userId: userId, - interactions: interactions, - context: context, - privacyLevel: configuration.privacyLevel - ) - - await MainActor.run { - self.userProfiles[userId] = profile - } - - // Generate personalized recommendations - if configuration.enablePersonalization { - await generatePersonalizedRecommendations(for: userId, profile: profile) - } - - return profile - } - - /// Generates content recommendations for a user - public func getContentRecommendations( - for userId: String, - contentTypes: [String] = [], - limit: Int = 10 - ) async throws -> [ContentRecommendation] { - guard isAIEnabled && configuration.enablePersonalization else { - throw AIError.learningDisabled - } - - let userProfile = userProfiles[userId] - let recommendations = try await recommendationEngine.generateRecommendations( - userId: userId, - userProfile: userProfile, - contentTypes: contentTypes, - limit: limit - ) - - // Cache recommendations - await MainActor.run { - self.recommendationCache.append(contentsOf: recommendations) - - // Keep cache size manageable - if self.recommendationCache.count > 1000 { - self.recommendationCache.removeFirst(self.recommendationCache.count - 1000) - } - } - - return recommendations - } - - /// Predicts widget usage patterns - public func predictWidgetUsage( - widgetId: String, - timeframe: TimeInterval, - context: [String: Double] = [:] - ) async throws -> PredictionResult { - let features = await featureExtractor.extractWidgetFeatures( - widgetId: widgetId, - timeframe: timeframe, - context: context - ) - - return try await predict( - modelType: .widgetUsagePrediction, - features: features, - metadata: ["widget_id": widgetId, "timeframe": String(timeframe)] - ) - } - - /// Detects anomalies in widget behavior - public func detectAnomalies( - widgetId: String, - metrics: [String: Double], - threshold: Double = 0.95 - ) async throws -> [AnomalyDetectionResult] { - let features = await featureExtractor.extractAnomalyFeatures( - widgetId: widgetId, - metrics: metrics - ) - - let prediction = try await predict( - modelType: .anomalyDetection, - features: features, - metadata: ["widget_id": widgetId, "threshold": String(threshold)] - ) - - return await interpretAnomalyPrediction(prediction, threshold: threshold) - } - - /// Optimizes widget performance using AI - public func optimizeWidgetPerformance( - widgetId: String, - currentMetrics: [String: Double], - constraints: [String: Double] = [:] - ) async throws -> PerformanceOptimizationSuggestion { - let features = await featureExtractor.extractPerformanceFeatures( - widgetId: widgetId, - metrics: currentMetrics, - constraints: constraints - ) - - let prediction = try await predict( - modelType: .performanceOptimization, - features: features, - metadata: ["widget_id": widgetId] - ) - - return await interpretPerformanceOptimization(prediction, currentMetrics: currentMetrics) - } - - /// Updates AI configuration - public func updateConfiguration(_ newConfiguration: AIConfiguration) async throws { - guard isValidConfiguration(newConfiguration) else { - throw AIError.configurationInvalid - } - - let wasEnabled = isAIEnabled - - if wasEnabled { - await stopAI() - } - - await MainActor.run { - self.configuration = newConfiguration - } - - if wasEnabled && newConfiguration.enableAI { - try await startAI() - } - - logger.info("AI configuration updated") - } - - /// Gets AI insights and analytics - public func getAIInsights() async -> AIInsights { - let totalPredictions = recentPredictions.count - let averageConfidence = recentPredictions.isEmpty ? 0 : - recentPredictions.map { $0.confidence }.reduce(0, +) / Double(totalPredictions) - - let modelQuality = modelMetrics.values.map { $0.qualityScore }.reduce(0, +) / Double(max(1, modelMetrics.count)) - - return AIInsights( - totalUsers: userProfiles.count, - totalPredictions: totalPredictions, - averageConfidence: averageConfidence, - loadedModels: Array(loadedModels), - modelQuality: modelQuality, - recommendationHitRate: await calculateRecommendationHitRate(), - learningEfficiency: await calculateLearningEfficiency(), - privacyCompliance: await privacyManager.getComplianceScore() - ) - } - - /// Exports AI model data for analysis - public func exportModelData(modelType: ModelType) async throws -> AIModelExport { - guard loadedModels.contains(modelType) else { - throw AIError.modelNotLoaded - } - - let metrics = modelMetrics[modelType] - let recentPredictionsForModel = recentPredictions.filter { $0.modelType == modelType } - - return AIModelExport( - modelType: modelType, - metrics: metrics, - recentPredictions: recentPredictionsForModel, - exportedAt: Date() - ) - } - - /// Clears all AI data and resets learning - public func resetAI() async throws { - await MainActor.run { - self.userProfiles.removeAll() - self.recentPredictions.removeAll() - self.recommendationCache.removeAll() - self.modelMetrics.removeAll() - } - - await unloadAllModels() - await learningEngine.resetLearning() - await privacyManager.clearData() - - logger.info("AI system reset completed") - } - - // MARK: - Private Methods - - private func setupAIManager() { - // Setup notification observers - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppWillEnterForeground() } - } - .store(in: &cancellables) - - logger.info("AI Manager initialized") - } - - private func checkCoreMLAvailability() async -> Bool { - // Check if Core ML is available on the device - return true // Simplified implementation - } - - private func loadInitialModels() async { - let priorityModels: [ModelType] = [ - .widgetUsagePrediction, - .contentRecommendation, - .performanceOptimization - ] - - for modelType in priorityModels { - do { - try await loadModel(modelType) - } catch { - logger.error("Failed to load model \(modelType.rawValue): \(error)") - } - } - } - - private func loadModel(_ modelType: ModelType) async throws { - try await modelRegistry.loadModel(modelType) - - await MainActor.run { - self.loadedModels.insert(modelType) - } - - logger.info("Loaded AI model: \(modelType.rawValue)") - } - - private func unloadAllModels() async { - await modelRegistry.unloadAllModels() - - await MainActor.run { - self.loadedModels.removeAll() - } - } - - private func setupAutomaticTraining() { - trainingTimer = Timer.scheduledTimer(withTimeInterval: configuration.modelUpdateInterval, repeats: true) { [weak self] _ in - Task { - await self?.performAutomaticTraining() - } - } - } - - private func performAutomaticTraining() async { - guard configuration.enableLearning else { return } - - for modelType in loadedModels { - do { - await learningEngine.performIncrementalLearning(for: modelType) - } catch { - logger.error("Automatic training failed for \(modelType.rawValue): \(error)") - } - } - } - - private func storePredictionResult(_ result: PredictionResult) async { - await MainActor.run { - self.recentPredictions.append(result) - - // Keep only recent predictions - if self.recentPredictions.count > 1000 { - self.recentPredictions.removeFirst(self.recentPredictions.count - 1000) - } - } - } - - private func updateModelMetrics(for modelType: ModelType, prediction: PredictionResult) async { - // Update prediction count and average confidence - await MainActor.run { - if var metrics = self.modelMetrics[modelType] { - metrics = ModelMetrics( - modelType: modelType, - accuracy: metrics.accuracy, - precision: metrics.precision, - recall: metrics.recall, - f1Score: metrics.f1Score, - trainingDataSize: metrics.trainingDataSize, - lastTrainingDate: metrics.lastTrainingDate, - predictionCount: metrics.predictionCount + 1, - averageConfidence: (metrics.averageConfidence * Double(metrics.predictionCount) + prediction.confidence) / Double(metrics.predictionCount + 1) - ) - self.modelMetrics[modelType] = metrics - } - } - } - - private func updateModelMetricsAfterTraining(_ modelType: ModelType) async { - // This would update metrics based on training results - let metrics = ModelMetrics( - modelType: modelType, - accuracy: 0.85, - precision: 0.82, - recall: 0.88, - f1Score: 0.85, - trainingDataSize: 1000, - lastTrainingDate: Date(), - predictionCount: 0, - averageConfidence: 0.0 - ) - - await MainActor.run { - self.modelMetrics[modelType] = metrics - } - } - - private func generatePersonalizedRecommendations(for userId: String, profile: UserBehaviorProfile) async { - do { - let recommendations = try await getContentRecommendations(for: userId) - // Recommendations are automatically cached in getContentRecommendations - } catch { - logger.error("Failed to generate personalized recommendations for user \(userId): \(error)") - } - } - - private func interpretAnomalyPrediction(_ prediction: PredictionResult, threshold: Double) async -> [AnomalyDetectionResult] { - guard prediction.confidence >= threshold else { - return [] - } - - return [ - AnomalyDetectionResult( - type: .performanceAnomaly, - severity: prediction.prediction > 0.8 ? .high : .medium, - description: "Unusual widget behavior detected", - confidence: prediction.confidence, - affectedMetrics: Array(prediction.features.keys), - timestamp: Date() - ) - ] - } - - private func interpretPerformanceOptimization(_ prediction: PredictionResult, currentMetrics: [String: Double]) async -> PerformanceOptimizationSuggestion { - return PerformanceOptimizationSuggestion( - widgetId: prediction.metadata["widget_id"] ?? "", - optimizationType: .memoryOptimization, - expectedImprovement: prediction.prediction, - confidence: prediction.confidence, - actionItems: [ - "Reduce memory allocation frequency", - "Optimize image caching strategy", - "Implement lazy loading for heavy components" - ], - estimatedImpact: prediction.prediction > 0.7 ? .high : .medium - ) - } - - private func isValidConfiguration(_ config: AIConfiguration) -> Bool { - return config.maxTrainingDataPoints > 0 && - config.modelUpdateInterval > 0 && - config.predictionConfidenceThreshold >= 0 && - config.predictionConfidenceThreshold <= 1 - } - - private func calculateRecommendationHitRate() async -> Double { - // Simplified implementation - return 0.75 - } - - private func calculateLearningEfficiency() async -> Double { - // Simplified implementation - return 0.82 - } - - private func handleAppDidEnterBackground() async { - // Save current state and optimize for background operation - if configuration.enableLearning { - await performAutomaticTraining() - } - } - - private func handleAppWillEnterForeground() async { - // Resume normal operation - await updateModelMetricsForAllModels() - } - - private func updateModelMetricsForAllModels() async { - for modelType in loadedModels { - await updateModelMetricsAfterTraining(modelType) - } - } -} - -// MARK: - Supporting Types - -public struct UserInteraction: Codable { - public let type: String - public let widgetId: String - public let timestamp: Date - public let duration: TimeInterval - public let context: [String: String] - - public init(type: String, widgetId: String, duration: TimeInterval, context: [String: String] = [:]) { - self.type = type - self.widgetId = widgetId - self.timestamp = Date() - self.duration = duration - self.context = context - } -} - -public struct AnomalyDetectionResult: Codable { - public let type: AnomalyType - public let severity: Severity - public let description: String - public let confidence: Double - public let affectedMetrics: [String] - public let timestamp: Date - - public enum AnomalyType: String, Codable { - case performanceAnomaly = "performance" - case usageAnomaly = "usage" - case dataAnomaly = "data" - } - - public enum Severity: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - } -} - -public struct PerformanceOptimizationSuggestion: Codable { - public let widgetId: String - public let optimizationType: OptimizationType - public let expectedImprovement: Double - public let confidence: Double - public let actionItems: [String] - public let estimatedImpact: Impact - - public enum OptimizationType: String, Codable { - case memoryOptimization = "memory" - case cpuOptimization = "cpu" - case batteryOptimization = "battery" - case networkOptimization = "network" - } - - public enum Impact: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - } -} - -public struct AIInsights { - public let totalUsers: Int - public let totalPredictions: Int - public let averageConfidence: Double - public let loadedModels: [AIManager.ModelType] - public let modelQuality: Double - public let recommendationHitRate: Double - public let learningEfficiency: Double - public let privacyCompliance: Double -} - -public struct AIModelExport: Codable { - public let modelType: AIManager.ModelType - public let metrics: AIManager.ModelMetrics? - public let recentPredictions: [AIManager.PredictionResult] - public let exportedAt: Date -} - -// MARK: - Private Supporting Classes - -private class MLModelRegistry { - private var models: [AIManager.ModelType: MLModel] = [:] - - func loadModel(_ type: AIManager.ModelType) async throws { - // Implementation would load actual Core ML models - // For now, we'll simulate model loading - models[type] = try await createMockModel(for: type) - } - - func unloadAllModels() async { - models.removeAll() - } - - private func createMockModel(for type: AIManager.ModelType) async throws -> MLModel { - // This would load actual Core ML models in a real implementation - // For now, return a mock model - throw AIManager.AIError.modelNotLoaded - } -} - -private class FeatureExtractor { - func extractWidgetFeatures(widgetId: String, timeframe: TimeInterval, context: [String: Double]) async -> [String: Double] { - var features: [String: Double] = [:] - features["timeframe"] = timeframe - features["hour_of_day"] = Double(Calendar.current.component(.hour, from: Date())) - features["day_of_week"] = Double(Calendar.current.component(.weekday, from: Date())) - features.merge(context) { $1 } - return features - } - - func extractAnomalyFeatures(widgetId: String, metrics: [String: Double]) async -> [String: Double] { - var features = metrics - features["timestamp"] = Date().timeIntervalSince1970 - features["widget_hash"] = Double(widgetId.hashValue) - return features - } - - func extractPerformanceFeatures(widgetId: String, metrics: [String: Double], constraints: [String: Double]) async -> [String: Double] { - var features = metrics - features.merge(constraints) { $1 } - features["optimization_timestamp"] = Date().timeIntervalSince1970 - return features - } -} - -private class PredictionEngine { - func predict(modelType: AIManager.ModelType, features: [String: Double], metadata: [String: String]) async throws -> AIManager.PredictionResult { - // Simulate AI prediction - let prediction = Double.random(in: 0...1) - let confidence = Double.random(in: 0.6...0.95) - - return AIManager.PredictionResult( - modelType: modelType, - prediction: prediction, - confidence: confidence, - features: features, - metadata: metadata, - explanation: "Simulated AI prediction based on \(features.count) features" - ) - } -} - -private class LearningEngine { - func performIncrementalLearning(for modelType: AIManager.ModelType) async { - // Implementation would perform incremental learning - } - - func resetLearning() async { - // Implementation would reset learning data - } -} - -private class RecommendationEngine { - func generateRecommendations( - userId: String, - userProfile: AIManager.UserBehaviorProfile?, - contentTypes: [String], - limit: Int - ) async throws -> [AIManager.ContentRecommendation] { - // Generate mock recommendations - var recommendations: [AIManager.ContentRecommendation] = [] - - for i in 0.. AIManager.UserBehaviorProfile { - // Analyze user interactions and generate profile - let widgetTypes = Set(interactions.map { $0.widgetId }).map { String($0) } - let totalDuration = interactions.reduce(0) { $0 + $1.duration } - let averageDuration = totalDuration / Double(max(1, interactions.count)) - - var usagePatterns: [String: Double] = [:] - var timePreferences: [String: Double] = [:] - - // Analyze usage patterns - for interaction in interactions { - let hour = Calendar.current.component(.hour, from: interaction.timestamp) - let hourKey = "hour_\(hour)" - timePreferences[hourKey, default: 0] += 1 - - usagePatterns[interaction.type, default: 0] += interaction.duration - } - - return AIManager.UserBehaviorProfile( - userId: userId, - preferredWidgetTypes: widgetTypes, - usagePatterns: usagePatterns, - timePreferences: timePreferences, - interactionFrequency: Double(interactions.count) / 7.0, // per week - sessionDuration: averageDuration, - deviceUsageContext: [:], - personalizedFeatures: [:], - lastUpdated: Date() - ) - } -} - -private class ModelTrainer { - func trainModel( - type: AIManager.ModelType, - features: [[String: Double]], - labels: [Double], - configuration: AIManager.AIConfiguration - ) async throws { - // Implementation would train actual models - // For now, simulate training - await Task.sleep(nanoseconds: UInt64(2 * 1_000_000_000)) // 2 seconds - } -} - -private class AIPrivacyManager { - func initialize(configuration: AIManager.AIConfiguration) async { - // Initialize privacy settings - } - - func sanitizeTrainingData(_ data: [[String: Double]]) async -> [[String: Double]] { - // Remove or anonymize sensitive data - return data - } - - func getComplianceScore() async -> Double { - return 0.95 // 95% compliance - } - - func clearData() async { - // Clear privacy-sensitive data - } -} - -// MARK: - Extensions - -extension Double { - func clamped(to range: ClosedRange) -> Double { - return Swift.min(Swift.max(self, range.lowerBound), range.upperBound) - } -} \ No newline at end of file diff --git a/Sources/Enterprise/AdvancedAnalytics.swift b/Sources/Enterprise/AdvancedAnalytics.swift deleted file mode 100644 index 99d6450..0000000 --- a/Sources/Enterprise/AdvancedAnalytics.swift +++ /dev/null @@ -1,1314 +0,0 @@ -import Foundation -import SwiftUI -import Combine -import OSLog -import Charts - -/// Enterprise-grade advanced analytics and business intelligence platform -/// providing comprehensive dashboards, KPI tracking, and strategic insights for iOS widgets -@available(iOS 16.0, *) -public class AdvancedAnalytics: ObservableObject { - - // MARK: - Types - - public enum AnalyticsError: LocalizedError { - case dashboardNotConfigured - case kpiCalculationFailed - case reportGenerationFailed - case dataVisualizationFailed - case alertConfigurationInvalid - case businessRuleViolation - case integrationFailed - case permissionDenied - - public var errorDescription: String? { - switch self { - case .dashboardNotConfigured: - return "Analytics dashboard is not properly configured" - case .kpiCalculationFailed: - return "Failed to calculate KPI metrics" - case .reportGenerationFailed: - return "Failed to generate analytics report" - case .dataVisualizationFailed: - return "Failed to create data visualization" - case .alertConfigurationInvalid: - return "Alert configuration is invalid" - case .businessRuleViolation: - return "Business rule validation failed" - case .integrationFailed: - return "External system integration failed" - case .permissionDenied: - return "Insufficient permissions for analytics operation" - } - } - } - - public enum DashboardType: String, CaseIterable, Codable { - case executive = "executive" - case operational = "operational" - case tactical = "tactical" - case technical = "technical" - case financial = "financial" - case marketing = "marketing" - case product = "product" - case userExperience = "user_experience" - - public var displayName: String { - switch self { - case .executive: return "Executive Dashboard" - case .operational: return "Operational Dashboard" - case .tactical: return "Tactical Dashboard" - case .technical: return "Technical Dashboard" - case .financial: return "Financial Dashboard" - case .marketing: return "Marketing Dashboard" - case .product: return "Product Dashboard" - case .userExperience: return "User Experience Dashboard" - } - } - - public var description: String { - switch self { - case .executive: - return "High-level strategic KPIs and business performance indicators" - case .operational: - return "Day-to-day operational metrics and system health" - case .tactical: - return "Mid-term tactical insights and performance trends" - case .technical: - return "Technical performance, system metrics, and engineering KPIs" - case .financial: - return "Revenue, cost, and financial performance analytics" - case .marketing: - return "User acquisition, engagement, and marketing effectiveness" - case .product: - return "Product usage, feature adoption, and user satisfaction" - case .userExperience: - return "User experience metrics, satisfaction, and usability insights" - } - } - } - - public enum KPICategory: String, CaseIterable, Codable { - case business = "business" - case technical = "technical" - case user = "user" - case financial = "financial" - case operational = "operational" - case quality = "quality" - - public var color: Color { - switch self { - case .business: return .blue - case .technical: return .green - case .user: return .purple - case .financial: return .orange - case .operational: return .red - case .quality: return .cyan - } - } - } - - public struct KPI: Identifiable, Codable { - public let id: String - public let name: String - public let description: String - public let category: KPICategory - public let currentValue: Double - public let targetValue: Double - public let previousValue: Double - public let unit: String - public let trend: Trend - public let status: Status - public let lastUpdated: Date - public let metadata: [String: String] - - public enum Trend: String, Codable { - case up = "up" - case down = "down" - case stable = "stable" - case volatile = "volatile" - - public var icon: String { - switch self { - case .up: return "arrow.up.circle.fill" - case .down: return "arrow.down.circle.fill" - case .stable: return "minus.circle.fill" - case .volatile: return "waveform.circle.fill" - } - } - - public var color: Color { - switch self { - case .up: return .green - case .down: return .red - case .stable: return .blue - case .volatile: return .orange - } - } - } - - public enum Status: String, Codable { - case excellent = "excellent" - case good = "good" - case warning = "warning" - case critical = "critical" - - public var color: Color { - switch self { - case .excellent: return .green - case .good: return .blue - case .warning: return .orange - case .critical: return .red - } - } - } - - public var achievementPercentage: Double { - guard targetValue != 0 else { return 0 } - return (currentValue / targetValue) * 100 - } - - public var changeFromPrevious: Double { - guard previousValue != 0 else { return 0 } - return ((currentValue - previousValue) / previousValue) * 100 - } - - public init( - name: String, - description: String, - category: KPICategory, - currentValue: Double, - targetValue: Double, - previousValue: Double = 0, - unit: String = "", - metadata: [String: String] = [:] - ) { - self.id = UUID().uuidString - self.name = name - self.description = description - self.category = category - self.currentValue = currentValue - self.targetValue = targetValue - self.previousValue = previousValue - self.unit = unit - self.lastUpdated = Date() - self.metadata = metadata - - // Calculate trend - if previousValue == 0 { - self.trend = .stable - } else { - let change = ((currentValue - previousValue) / previousValue) - if abs(change) < 0.05 { - self.trend = .stable - } else if change > 0.2 || change < -0.2 { - self.trend = .volatile - } else if change > 0 { - self.trend = .up - } else { - self.trend = .down - } - } - - // Calculate status - let achievement = (currentValue / targetValue) - if achievement >= 1.0 { - self.status = .excellent - } else if achievement >= 0.8 { - self.status = .good - } else if achievement >= 0.6 { - self.status = .warning - } else { - self.status = .critical - } - } - } - - public struct Dashboard: Identifiable, Codable { - public let id: String - public let name: String - public let type: DashboardType - public let description: String - public let widgets: [DashboardWidget] - public let layout: DashboardLayout - public let permissions: DashboardPermissions - public let refreshInterval: TimeInterval - public let createdAt: Date - public let lastUpdated: Date - - public struct DashboardWidget: Identifiable, Codable { - public let id: String - public let type: WidgetType - public let title: String - public let position: CGPoint - public let size: CGSize - public let configuration: WidgetConfiguration - public let dataSource: String - public let refreshInterval: TimeInterval - - public enum WidgetType: String, Codable { - case kpiCard = "kpi_card" - case chart = "chart" - case table = "table" - case gauge = "gauge" - case sparkline = "sparkline" - case heatmap = "heatmap" - case funnel = "funnel" - case scorecard = "scorecard" - } - - public struct WidgetConfiguration: Codable { - public let chartType: ChartType? - public let colorScheme: String - public let showTrend: Bool - public let showTarget: Bool - public let aggregationPeriod: String - public let filters: [String: String] - - public enum ChartType: String, Codable { - case line = "line" - case bar = "bar" - case pie = "pie" - case area = "area" - case scatter = "scatter" - case donut = "donut" - } - - public init( - chartType: ChartType? = nil, - colorScheme: String = "default", - showTrend: Bool = true, - showTarget: Bool = true, - aggregationPeriod: String = "daily", - filters: [String: String] = [:] - ) { - self.chartType = chartType - self.colorScheme = colorScheme - self.showTrend = showTrend - self.showTarget = showTarget - self.aggregationPeriod = aggregationPeriod - self.filters = filters - } - } - } - - public struct DashboardLayout: Codable { - public let columns: Int - public let rows: Int - public let gridSize: CGSize - public let spacing: CGFloat - } - - public struct DashboardPermissions: Codable { - public let viewUsers: [String] - public let editUsers: [String] - public let adminUsers: [String] - public let isPublic: Bool - } - - public init( - name: String, - type: DashboardType, - description: String, - widgets: [DashboardWidget] = [], - refreshInterval: TimeInterval = 300 - ) { - self.id = UUID().uuidString - self.name = name - self.type = type - self.description = description - self.widgets = widgets - self.layout = DashboardLayout( - columns: 12, - rows: 8, - gridSize: CGSize(width: 100, height: 100), - spacing: 16 - ) - self.permissions = DashboardPermissions( - viewUsers: [], - editUsers: [], - adminUsers: [], - isPublic: false - ) - self.refreshInterval = refreshInterval - self.createdAt = Date() - self.lastUpdated = Date() - } - } - - public struct AnalyticsReport: Codable { - public let id: String - public let title: String - public let type: ReportType - public let period: DateInterval - public let executiveSummary: String - public let keyFindings: [String] - public let recommendations: [Recommendation] - public let kpiSummary: [KPI] - public let charts: [ChartData] - public let appendices: [ReportSection] - public let generatedAt: Date - public let generatedBy: String - - public enum ReportType: String, Codable { - case daily = "daily" - case weekly = "weekly" - case monthly = "monthly" - case quarterly = "quarterly" - case annual = "annual" - case adhoc = "adhoc" - } - - public struct Recommendation: Codable { - public let priority: Priority - public let category: String - public let title: String - public let description: String - public let expectedImpact: String - public let timeframe: String - public let effort: String - public let stakeholders: [String] - - public enum Priority: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - case critical = "critical" - } - } - - public struct ChartData: Codable { - public let title: String - public let type: Dashboard.DashboardWidget.WidgetConfiguration.ChartType - public let data: [DataPoint] - public let insights: [String] - - public struct DataPoint: Codable { - public let x: Double - public let y: Double - public let label: String? - public let metadata: [String: String] - } - } - - public struct ReportSection: Codable { - public let title: String - public let content: String - public let charts: [ChartData] - public let tables: [TableData] - - public struct TableData: Codable { - public let title: String - public let headers: [String] - public let rows: [[String]] - public let summary: String? - } - } - } - - public struct Alert: Identifiable, Codable { - public let id: String - public let name: String - public let description: String - public let condition: AlertCondition - public let severity: Severity - public let isActive: Bool - public let triggeredAt: Date? - public let lastTriggered: Date? - public let triggerCount: Int - public let recipients: [String] - public let actions: [AlertAction] - - public struct AlertCondition: Codable { - public let kpiId: String - public let operator: ComparisonOperator - public let threshold: Double - public let duration: TimeInterval - public let aggregation: AggregationType - - public enum ComparisonOperator: String, Codable { - case greaterThan = "gt" - case lessThan = "lt" - case equals = "eq" - case greaterThanOrEquals = "gte" - case lessThanOrEquals = "lte" - case notEquals = "ne" - } - - public enum AggregationType: String, Codable { - case average = "avg" - case sum = "sum" - case count = "count" - case minimum = "min" - case maximum = "max" - } - } - - public enum Severity: String, Codable { - case info = "info" - case warning = "warning" - case critical = "critical" - case emergency = "emergency" - - public var color: Color { - switch self { - case .info: return .blue - case .warning: return .orange - case .critical: return .red - case .emergency: return .purple - } - } - } - - public struct AlertAction: Codable { - public let type: ActionType - public let configuration: [String: String] - - public enum ActionType: String, Codable { - case email = "email" - case push = "push" - case webhook = "webhook" - case sms = "sms" - case slack = "slack" - } - } - } - - public struct BusinessInsight: Identifiable, Codable { - public let id: String - public let title: String - public let description: String - public let category: InsightCategory - public let impact: Impact - public let confidence: Double - public let dataPoints: [String] - public let recommendations: [String] - public let generatedAt: Date - public let expiresAt: Date? - - public enum InsightCategory: String, Codable { - case performance = "performance" - case user = "user" - case revenue = "revenue" - case cost = "cost" - case quality = "quality" - case opportunity = "opportunity" - case risk = "risk" - } - - public enum Impact: String, Codable { - case low = "low" - case medium = "medium" - case high = "high" - case critical = "critical" - - public var color: Color { - switch self { - case .low: return .blue - case .medium: return .orange - case .high: return .red - case .critical: return .purple - } - } - } - } - - // MARK: - Properties - - public static let shared = AdvancedAnalytics() - - private let kpiEngine: KPIEngine - private let dashboardEngine: DashboardEngine - private let reportGenerator: ReportGenerator - private let alertManager: AlertManager - private let insightEngine: BusinessInsightEngine - private let visualizationEngine: DataVisualizationEngine - private let exportManager: AnalyticsExportManager - private let integrationManager: ExternalIntegrationManager - - @Published public private(set) var dashboards: [Dashboard] = [] - @Published public private(set) var kpis: [KPI] = [] - @Published public private(set) var alerts: [Alert] = [] - @Published public private(set) var insights: [BusinessInsight] = [] - @Published public private(set) var recentReports: [AnalyticsReport] = [] - @Published public private(set) var isProcessing: Bool = false - @Published public private(set) var lastRefreshTime: Date? - - private var refreshTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "advanced.analytics", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "AdvancedAnalytics") - - // MARK: - Initialization - - private init() { - self.kpiEngine = KPIEngine() - self.dashboardEngine = DashboardEngine() - self.reportGenerator = ReportGenerator() - self.alertManager = AlertManager() - self.insightEngine = BusinessInsightEngine() - self.visualizationEngine = DataVisualizationEngine() - self.exportManager = AnalyticsExportManager() - self.integrationManager = ExternalIntegrationManager() - - setupAdvancedAnalytics() - } - - // MARK: - Public Methods - - /// Initializes advanced analytics system - public func initialize() async throws { - await MainActor.run { - self.isProcessing = true - } - - // Load existing dashboards and KPIs - await loadDashboards() - await loadKPIs() - await loadAlerts() - await loadInsights() - - // Setup automatic refresh - setupAutoRefresh() - - await MainActor.run { - self.isProcessing = false - self.lastRefreshTime = Date() - } - - logger.info("Advanced Analytics initialized") - } - - /// Creates a new dashboard - public func createDashboard( - name: String, - type: DashboardType, - description: String, - widgets: [Dashboard.DashboardWidget] = [] - ) async throws -> Dashboard { - let dashboard = Dashboard( - name: name, - type: type, - description: description, - widgets: widgets - ) - - await MainActor.run { - self.dashboards.append(dashboard) - } - - await dashboardEngine.saveDashboard(dashboard) - - logger.info("Created dashboard: \(name)") - - return dashboard - } - - /// Updates an existing dashboard - public func updateDashboard(_ dashboard: Dashboard) async throws { - await MainActor.run { - if let index = self.dashboards.firstIndex(where: { $0.id == dashboard.id }) { - self.dashboards[index] = dashboard - } - } - - await dashboardEngine.saveDashboard(dashboard) - - logger.info("Updated dashboard: \(dashboard.name)") - } - - /// Deletes a dashboard - public func deleteDashboard(_ dashboardId: String) async throws { - await MainActor.run { - self.dashboards.removeAll { $0.id == dashboardId } - } - - await dashboardEngine.deleteDashboard(dashboardId) - - logger.info("Deleted dashboard: \(dashboardId)") - } - - /// Calculates and updates KPIs - public func refreshKPIs() async throws { - await MainActor.run { - self.isProcessing = true - } - - let updatedKPIs = try await kpiEngine.calculateKPIs() - - await MainActor.run { - self.kpis = updatedKPIs - self.lastRefreshTime = Date() - self.isProcessing = false - } - - // Check for alert conditions - await checkAlertConditions() - - logger.info("Refreshed \(updatedKPIs.count) KPIs") - } - - /// Generates comprehensive analytics report - public func generateReport( - type: AnalyticsReport.ReportType, - period: DateInterval, - includeCharts: Bool = true - ) async throws -> AnalyticsReport { - await MainActor.run { - self.isProcessing = true - } - - defer { - Task { - await MainActor.run { - self.isProcessing = false - } - } - } - - let report = try await reportGenerator.generateReport( - type: type, - period: period, - kpis: kpis, - includeCharts: includeCharts - ) - - await MainActor.run { - self.recentReports.append(report) - - // Keep only recent reports - if self.recentReports.count > 50 { - self.recentReports.removeFirst(self.recentReports.count - 50) - } - } - - logger.info("Generated \(type.rawValue) report for period \(period)") - - return report - } - - /// Creates or updates an alert - public func configureAlert( - name: String, - description: String, - condition: Alert.AlertCondition, - severity: Alert.Severity, - recipients: [String], - actions: [Alert.AlertAction] - ) async throws -> Alert { - let alert = Alert( - id: UUID().uuidString, - name: name, - description: description, - condition: condition, - severity: severity, - isActive: true, - triggeredAt: nil, - lastTriggered: nil, - triggerCount: 0, - recipients: recipients, - actions: actions - ) - - await MainActor.run { - self.alerts.append(alert) - } - - await alertManager.saveAlert(alert) - - logger.info("Configured alert: \(name)") - - return alert - } - - /// Gets dashboard data for visualization - public func getDashboardData(_ dashboardId: String) async throws -> DashboardData { - guard let dashboard = dashboards.first(where: { $0.id == dashboardId }) else { - throw AnalyticsError.dashboardNotConfigured - } - - var widgetData: [String: WidgetData] = [:] - - for widget in dashboard.widgets { - let data = try await dashboardEngine.getWidgetData( - widget: widget, - kpis: kpis - ) - widgetData[widget.id] = data - } - - return DashboardData( - dashboard: dashboard, - widgetData: widgetData, - lastUpdated: Date() - ) - } - - /// Generates business insights using AI - public func generateBusinessInsights() async throws -> [BusinessInsight] { - let newInsights = try await insightEngine.generateInsights( - kpis: kpis, - historicalData: await getHistoricalKPIData(), - externalFactors: await getExternalFactors() - ) - - await MainActor.run { - self.insights.append(contentsOf: newInsights) - - // Remove expired insights - self.insights.removeAll { insight in - if let expiresAt = insight.expiresAt { - return Date() > expiresAt - } - return false - } - - // Keep only recent insights - if self.insights.count > 100 { - self.insights = Array(self.insights.suffix(100)) - } - } - - logger.info("Generated \(newInsights.count) business insights") - - return newInsights - } - - /// Exports analytics data in various formats - public func exportData( - format: ExportFormat, - dashboardIds: [String] = [], - includeRawData: Bool = false - ) async throws -> ExportResult { - return try await exportManager.exportData( - format: format, - dashboards: dashboards.filter { dashboardIds.isEmpty || dashboardIds.contains($0.id) }, - kpis: kpis, - reports: recentReports, - includeRawData: includeRawData - ) - } - - /// Integrates with external analytics platforms - public func integrateWithPlatform( - platform: ExternalPlatform, - configuration: [String: String] - ) async throws { - try await integrationManager.setupIntegration( - platform: platform, - configuration: configuration, - dashboards: dashboards, - kpis: kpis - ) - - logger.info("Integrated with platform: \(platform.rawValue)") - } - - /// Gets real-time analytics summary - public func getAnalyticsSummary() async -> AnalyticsSummary { - let kpiSummary = await generateKPISummary() - let alertSummary = await generateAlertSummary() - let insightSummary = await generateInsightSummary() - - return AnalyticsSummary( - totalDashboards: dashboards.count, - totalKPIs: kpis.count, - activeAlerts: alerts.filter { $0.isActive }.count, - criticalAlerts: alerts.filter { $0.severity == .critical || $0.severity == .emergency }.count, - recentInsights: insights.filter { Date().timeIntervalSince($0.generatedAt) < 24 * 3600 }.count, - kpiSummary: kpiSummary, - alertSummary: alertSummary, - insightSummary: insightSummary, - lastRefresh: lastRefreshTime ?? Date() - ) - } - - // MARK: - Private Methods - - private func setupAdvancedAnalytics() { - // Setup notification observers - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - - logger.info("Advanced Analytics setup completed") - } - - private func loadDashboards() async { - do { - let loadedDashboards = try await dashboardEngine.loadDashboards() - await MainActor.run { - self.dashboards = loadedDashboards - } - } catch { - logger.error("Failed to load dashboards: \(error)") - } - } - - private func loadKPIs() async { - do { - let loadedKPIs = try await kpiEngine.loadKPIs() - await MainActor.run { - self.kpis = loadedKPIs - } - } catch { - logger.error("Failed to load KPIs: \(error)") - } - } - - private func loadAlerts() async { - do { - let loadedAlerts = try await alertManager.loadAlerts() - await MainActor.run { - self.alerts = loadedAlerts - } - } catch { - logger.error("Failed to load alerts: \(error)") - } - } - - private func loadInsights() async { - do { - let loadedInsights = try await insightEngine.loadInsights() - await MainActor.run { - self.insights = loadedInsights - } - } catch { - logger.error("Failed to load insights: \(error)") - } - } - - private func setupAutoRefresh() { - refreshTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in - Task { - try? await self?.refreshKPIs() - } - } - } - - private func checkAlertConditions() async { - for alert in alerts.filter({ $0.isActive }) { - let shouldTrigger = await alertManager.evaluateCondition( - condition: alert.condition, - kpis: kpis - ) - - if shouldTrigger { - await alertManager.triggerAlert(alert) - } - } - } - - private func getHistoricalKPIData() async -> [String: [(Date, Double)]] { - // Implementation would fetch historical KPI data - return [:] - } - - private func getExternalFactors() async -> [String: Double] { - // Implementation would fetch external factors (market data, seasonality, etc.) - return [:] - } - - private func generateKPISummary() async -> KPISummary { - let total = kpis.count - let excellent = kpis.filter { $0.status == .excellent }.count - let good = kpis.filter { $0.status == .good }.count - let warning = kpis.filter { $0.status == .warning }.count - let critical = kpis.filter { $0.status == .critical }.count - - return KPISummary( - total: total, - excellent: excellent, - good: good, - warning: warning, - critical: critical - ) - } - - private func generateAlertSummary() async -> AlertSummary { - let total = alerts.count - let active = alerts.filter { $0.isActive }.count - let triggered = alerts.filter { $0.triggeredAt != nil }.count - - return AlertSummary( - total: total, - active: active, - triggered: triggered - ) - } - - private func generateInsightSummary() async -> InsightSummary { - let total = insights.count - let highImpact = insights.filter { $0.impact == .high || $0.impact == .critical }.count - let recent = insights.filter { Date().timeIntervalSince($0.generatedAt) < 24 * 3600 }.count - - return InsightSummary( - total: total, - highImpact: highImpact, - recent: recent - ) - } - - private func handleAppDidEnterBackground() async { - // Save current state - refreshTimer?.invalidate() - - // Perform final data sync - try? await refreshKPIs() - } -} - -// MARK: - Supporting Types - -public struct DashboardData { - public let dashboard: AdvancedAnalytics.Dashboard - public let widgetData: [String: WidgetData] - public let lastUpdated: Date -} - -public struct WidgetData { - public let values: [Double] - public let labels: [String] - public let metadata: [String: String] - public let chartData: ChartDataPoints? - - public struct ChartDataPoints { - public let x: [Double] - public let y: [Double] - public let series: [Series] - - public struct Series { - public let name: String - public let data: [(Double, Double)] - public let color: Color - } - } -} - -public enum ExportFormat: String, CaseIterable { - case json = "json" - case csv = "csv" - case excel = "excel" - case pdf = "pdf" -} - -public enum ExternalPlatform: String, CaseIterable { - case googleAnalytics = "google_analytics" - case mixpanel = "mixpanel" - case amplitude = "amplitude" - case tableau = "tableau" - case powerBI = "power_bi" - case looker = "looker" -} - -public struct ExportResult { - public let format: ExportFormat - public let data: Data - public let filename: String - public let exportedAt: Date -} - -public struct AnalyticsSummary { - public let totalDashboards: Int - public let totalKPIs: Int - public let activeAlerts: Int - public let criticalAlerts: Int - public let recentInsights: Int - public let kpiSummary: KPISummary - public let alertSummary: AlertSummary - public let insightSummary: InsightSummary - public let lastRefresh: Date -} - -public struct KPISummary { - public let total: Int - public let excellent: Int - public let good: Int - public let warning: Int - public let critical: Int - - public var healthScore: Double { - guard total > 0 else { return 0 } - let score = (excellent * 4 + good * 3 + warning * 2 + critical * 1) - return Double(score) / Double(total * 4) - } -} - -public struct AlertSummary { - public let total: Int - public let active: Int - public let triggered: Int -} - -public struct InsightSummary { - public let total: Int - public let highImpact: Int - public let recent: Int -} - -// MARK: - Private Supporting Classes - -private class KPIEngine { - func calculateKPIs() async throws -> [AdvancedAnalytics.KPI] { - // Mock KPI calculation - let kpis = [ - AdvancedAnalytics.KPI( - name: "Daily Active Users", - description: "Number of unique users who engaged with widgets daily", - category: .user, - currentValue: 12500, - targetValue: 15000, - previousValue: 11800, - unit: "users" - ), - AdvancedAnalytics.KPI( - name: "Widget Load Time", - description: "Average time to load widget content", - category: .technical, - currentValue: 0.85, - targetValue: 1.0, - previousValue: 0.92, - unit: "seconds" - ), - AdvancedAnalytics.KPI( - name: "User Retention Rate", - description: "Percentage of users who return after 7 days", - category: .business, - currentValue: 78.5, - targetValue: 80.0, - previousValue: 76.2, - unit: "%" - ) - ] - - return kpis - } - - func loadKPIs() async throws -> [AdvancedAnalytics.KPI] { - // Load KPIs from storage - return [] - } -} - -private class DashboardEngine { - func saveDashboard(_ dashboard: AdvancedAnalytics.Dashboard) async { - // Save dashboard to storage - } - - func deleteDashboard(_ dashboardId: String) async { - // Delete dashboard from storage - } - - func loadDashboards() async throws -> [AdvancedAnalytics.Dashboard] { - // Load dashboards from storage - return [] - } - - func getWidgetData(widget: AdvancedAnalytics.Dashboard.DashboardWidget, kpis: [AdvancedAnalytics.KPI]) async throws -> WidgetData { - // Generate widget data based on KPIs - return WidgetData( - values: [100, 150, 200, 180, 220], - labels: ["Mon", "Tue", "Wed", "Thu", "Fri"], - metadata: [:], - chartData: nil - ) - } -} - -private class ReportGenerator { - func generateReport( - type: AdvancedAnalytics.AnalyticsReport.ReportType, - period: DateInterval, - kpis: [AdvancedAnalytics.KPI], - includeCharts: Bool - ) async throws -> AdvancedAnalytics.AnalyticsReport { - // Generate comprehensive report - return AdvancedAnalytics.AnalyticsReport( - id: UUID().uuidString, - title: "\(type.rawValue.capitalized) Analytics Report", - type: type, - period: period, - executiveSummary: "Overall performance shows positive trends with key metrics exceeding targets.", - keyFindings: [ - "User engagement increased by 15% compared to previous period", - "Widget load times improved by 8%", - "Revenue per user grew by 12%" - ], - recommendations: [ - AdvancedAnalytics.AnalyticsReport.Recommendation( - priority: .high, - category: "Performance", - title: "Optimize Widget Caching", - description: "Implement intelligent caching to further reduce load times", - expectedImpact: "15% improvement in load times", - timeframe: "2 weeks", - effort: "Medium", - stakeholders: ["Engineering", "Product"] - ) - ], - kpiSummary: kpis, - charts: [], - appendices: [], - generatedAt: Date(), - generatedBy: "Advanced Analytics Engine" - ) - } -} - -private class AlertManager { - func saveAlert(_ alert: AdvancedAnalytics.Alert) async { - // Save alert configuration - } - - func loadAlerts() async throws -> [AdvancedAnalytics.Alert] { - // Load alerts from storage - return [] - } - - func evaluateCondition(condition: AdvancedAnalytics.Alert.AlertCondition, kpis: [AdvancedAnalytics.KPI]) async -> Bool { - // Evaluate alert condition against current KPIs - guard let kpi = kpis.first(where: { $0.id == condition.kpiId }) else { - return false - } - - switch condition.operator { - case .greaterThan: - return kpi.currentValue > condition.threshold - case .lessThan: - return kpi.currentValue < condition.threshold - case .equals: - return abs(kpi.currentValue - condition.threshold) < 0.01 - case .greaterThanOrEquals: - return kpi.currentValue >= condition.threshold - case .lessThanOrEquals: - return kpi.currentValue <= condition.threshold - case .notEquals: - return abs(kpi.currentValue - condition.threshold) >= 0.01 - } - } - - func triggerAlert(_ alert: AdvancedAnalytics.Alert) async { - // Trigger alert actions (email, push, webhook, etc.) - for action in alert.actions { - await executeAlertAction(action, alert: alert) - } - } - - private func executeAlertAction(_ action: AdvancedAnalytics.Alert.AlertAction, alert: AdvancedAnalytics.Alert) async { - // Execute specific alert action - switch action.type { - case .email: - // Send email notification - break - case .push: - // Send push notification - break - case .webhook: - // Call webhook - break - case .sms: - // Send SMS - break - case .slack: - // Send Slack message - break - } - } -} - -private class BusinessInsightEngine { - func generateInsights( - kpis: [AdvancedAnalytics.KPI], - historicalData: [String: [(Date, Double)]], - externalFactors: [String: Double] - ) async throws -> [AdvancedAnalytics.BusinessInsight] { - // Generate AI-powered business insights - var insights: [AdvancedAnalytics.BusinessInsight] = [] - - // Example insight generation - let performanceKPIs = kpis.filter { $0.category == .technical } - if performanceKPIs.allSatisfy({ $0.trend == .up }) { - insights.append( - AdvancedAnalytics.BusinessInsight( - id: UUID().uuidString, - title: "Performance Optimization Success", - description: "All technical KPIs show positive trends indicating successful optimization efforts", - category: .performance, - impact: .high, - confidence: 0.85, - dataPoints: performanceKPIs.map { $0.id }, - recommendations: [ - "Continue current optimization strategies", - "Consider expanding optimization to other areas" - ], - generatedAt: Date(), - expiresAt: Date().addingTimeInterval(7 * 24 * 3600) - ) - ) - } - - return insights - } - - func loadInsights() async throws -> [AdvancedAnalytics.BusinessInsight] { - // Load insights from storage - return [] - } -} - -private class DataVisualizationEngine { - // Data visualization implementation -} - -private class AnalyticsExportManager { - func exportData( - format: ExportFormat, - dashboards: [AdvancedAnalytics.Dashboard], - kpis: [AdvancedAnalytics.KPI], - reports: [AdvancedAnalytics.AnalyticsReport], - includeRawData: Bool - ) async throws -> ExportResult { - // Export data in specified format - let exportData = Data() - - return ExportResult( - format: format, - data: exportData, - filename: "analytics_export_\(Date().timeIntervalSince1970).\(format.rawValue)", - exportedAt: Date() - ) - } -} - -private class ExternalIntegrationManager { - func setupIntegration( - platform: ExternalPlatform, - configuration: [String: String], - dashboards: [AdvancedAnalytics.Dashboard], - kpis: [AdvancedAnalytics.KPI] - ) async throws { - // Setup integration with external platform - switch platform { - case .googleAnalytics: - // Setup Google Analytics integration - break - case .mixpanel: - // Setup Mixpanel integration - break - case .amplitude: - // Setup Amplitude integration - break - case .tableau: - // Setup Tableau integration - break - case .powerBI: - // Setup Power BI integration - break - case .looker: - // Setup Looker integration - break - } - } -} \ No newline at end of file diff --git a/Sources/Enterprise/MLAnalyticsEngine.swift b/Sources/Enterprise/MLAnalyticsEngine.swift deleted file mode 100644 index cf006cc..0000000 --- a/Sources/Enterprise/MLAnalyticsEngine.swift +++ /dev/null @@ -1,1275 +0,0 @@ -import Foundation -import CoreML -import Combine -import OSLog -import Charts - -/// Enterprise-grade machine learning analytics engine providing advanced data analysis, -/// pattern recognition, and predictive insights for iOS widget ecosystems -@available(iOS 16.0, *) -public class MLAnalyticsEngine: ObservableObject { - - // MARK: - Types - - public enum AnalyticsError: LocalizedError { - case insufficientData - case analysisNotAvailable - case modelNotTrained - case invalidParameters - case dataProcessingFailed - case segmentationFailed - case forecastingFailed - case clusteringFailed - - public var errorDescription: String? { - switch self { - case .insufficientData: - return "Insufficient data for analytics processing" - case .analysisNotAvailable: - return "Analytics feature is not available" - case .modelNotTrained: - return "Analytics model is not trained" - case .invalidParameters: - return "Invalid parameters provided for analysis" - case .dataProcessingFailed: - return "Data processing failed" - case .segmentationFailed: - return "User segmentation analysis failed" - case .forecastingFailed: - return "Time series forecasting failed" - case .clusteringFailed: - return "Clustering analysis failed" - } - } - } - - public enum AnalyticsType: String, CaseIterable, Codable { - case userSegmentation = "user_segmentation" - case behaviorAnalysis = "behavior_analysis" - case timeSeriesForecasting = "time_series_forecasting" - case cohortAnalysis = "cohort_analysis" - case funnelAnalysis = "funnel_analysis" - case retentionAnalysis = "retention_analysis" - case sentimentAnalysis = "sentiment_analysis" - case anomalyDetection = "anomaly_detection" - case patternRecognition = "pattern_recognition" - case predictiveScoring = "predictive_scoring" - - public var description: String { - switch self { - case .userSegmentation: - return "Groups users based on behavior patterns and characteristics" - case .behaviorAnalysis: - return "Analyzes user behavior patterns and trends" - case .timeSeriesForecasting: - return "Predicts future trends based on historical data" - case .cohortAnalysis: - return "Analyzes user groups over time periods" - case .funnelAnalysis: - return "Analyzes user journey and conversion funnels" - case .retentionAnalysis: - return "Measures user retention and churn patterns" - case .sentimentAnalysis: - return "Analyzes sentiment in user interactions and feedback" - case .anomalyDetection: - return "Detects unusual patterns and outliers in data" - case .patternRecognition: - return "Identifies recurring patterns and trends" - case .predictiveScoring: - return "Calculates predictive scores for user actions" - } - } - } - - public struct AnalyticsConfiguration { - public var isEnabled: Bool = true - public var enableRealTimeAnalytics: Bool = true - public var enablePredictiveAnalytics: Bool = true - public var dataRetentionDays: Int = 90 - public var minDataPointsForAnalysis: Int = 100 - public var analysisUpdateInterval: TimeInterval = 3600 // 1 hour - public var confidenceThreshold: Double = 0.75 - public var enableAdvancedSegmentation: Bool = true - public var enableDeepLearning: Bool = false - public var maxConcurrentAnalyses: Int = 5 - - public init() {} - } - - public struct AnalyticsDataPoint: Codable { - public let id: String - public let userId: String? - public let widgetId: String? - public let eventType: String - public let timestamp: Date - public let properties: [String: AnyCodable] - public let metadata: [String: String] - public let sessionId: String? - - public init( - userId: String? = nil, - widgetId: String? = nil, - eventType: String, - properties: [String: AnyCodable] = [:], - metadata: [String: String] = [:], - sessionId: String? = nil - ) { - self.id = UUID().uuidString - self.userId = userId - self.widgetId = widgetId - self.eventType = eventType - self.timestamp = Date() - self.properties = properties - self.metadata = metadata - self.sessionId = sessionId - } - } - - public struct UserSegment: Codable, Identifiable { - public let id: String - public let name: String - public let description: String - public let criteria: SegmentCriteria - public let userCount: Int - public let characteristics: [String: Double] - public let createdAt: Date - public let lastUpdated: Date - public let confidence: Double - - public struct SegmentCriteria: Codable { - public let behaviorPatterns: [String: Double] - public let demographicFilters: [String: String] - public let usageThresholds: [String: Double] - public let timeBasedFilters: [String: String] - } - - public init( - name: String, - description: String, - criteria: SegmentCriteria, - userCount: Int, - characteristics: [String: Double], - confidence: Double - ) { - self.id = UUID().uuidString - self.name = name - self.description = description - self.criteria = criteria - self.userCount = userCount - self.characteristics = characteristics - self.createdAt = Date() - self.lastUpdated = Date() - self.confidence = confidence - } - } - - public struct TimeSeriesForecast: Codable { - public let metric: String - public let forecastPeriod: DateInterval - public let predictions: [ForecastPoint] - public let confidence: Double - public let methodology: String - public let seasonality: SeasonalityInfo? - public let trend: TrendInfo - - public struct ForecastPoint: Codable { - public let timestamp: Date - public let predictedValue: Double - public let confidenceInterval: ConfidenceInterval - public let trend: Double - public let seasonalComponent: Double? - } - - public struct ConfidenceInterval: Codable { - public let lower: Double - public let upper: Double - } - - public struct SeasonalityInfo: Codable { - public let period: TimeInterval - public let strength: Double - public let pattern: [Double] - } - - public struct TrendInfo: Codable { - public let direction: TrendDirection - public let strength: Double - public let changePoints: [Date] - - public enum TrendDirection: String, Codable { - case increasing = "increasing" - case decreasing = "decreasing" - case stable = "stable" - } - } - } - - public struct CohortAnalysisResult: Codable { - public let cohortDefinition: CohortDefinition - public let cohorts: [Cohort] - public let metrics: [String: [Double]] - public let insights: [String] - public let generatedAt: Date - - public struct CohortDefinition: Codable { - public let timeUnit: TimeUnit - public let metric: String - public let startDate: Date - public let endDate: Date - - public enum TimeUnit: String, Codable { - case day = "day" - case week = "week" - case month = "month" - } - } - - public struct Cohort: Codable { - public let name: String - public let startDate: Date - public let initialSize: Int - public let retentionRates: [Double] - public let averageValue: Double - public let churnRate: Double - } - } - - public struct BehaviorPattern: Codable, Identifiable { - public let id: String - public let name: String - public let description: String - public let frequency: Double - public let users: [String] - public let events: [String] - public let timePattern: TimePattern - public let strength: Double - public let significance: Double - - public struct TimePattern: Codable { - public let hourlyDistribution: [Double] - public let dailyDistribution: [Double] - public let weeklyDistribution: [Double] - public let seasonalityDetected: Bool - } - - public init( - name: String, - description: String, - frequency: Double, - users: [String], - events: [String], - timePattern: TimePattern, - strength: Double, - significance: Double - ) { - self.id = UUID().uuidString - self.name = name - self.description = description - self.frequency = frequency - self.users = users - self.events = events - self.timePattern = timePattern - self.strength = strength - self.significance = significance - } - } - - public struct FunnelAnalysisResult: Codable { - public let funnelName: String - public let steps: [FunnelStep] - public let conversionRate: Double - public let dropOffPoints: [DropOffAnalysis] - public let insights: [String] - public let timeframe: DateInterval - - public struct FunnelStep: Codable { - public let stepName: String - public let stepOrder: Int - public let userCount: Int - public let conversionFromPrevious: Double - public let averageTimeToComplete: TimeInterval - public let dropOffRate: Double - } - - public struct DropOffAnalysis: Codable { - public let stepName: String - public let dropOffRate: Double - public let commonReasons: [String] - public let recommendations: [String] - } - } - - // MARK: - Properties - - public static let shared = MLAnalyticsEngine() - - private let dataProcessor: AnalyticsDataProcessor - private let segmentationEngine: UserSegmentationEngine - private let forecastingEngine: TimeSeriesForecastingEngine - private let behaviorAnalyzer: BehaviorAnalysisEngine - private let cohortAnalyzer: CohortAnalysisEngine - private let patternRecognizer: PatternRecognitionEngine - private let anomalyDetector: AnomalyDetectionEngine - private let sentimentAnalyzer: SentimentAnalysisEngine - private let storageManager: AnalyticsStorageManager - - @Published public private(set) var configuration: AnalyticsConfiguration = AnalyticsConfiguration() - @Published public private(set) var isAnalyticsEnabled: Bool = false - @Published public private(set) var currentSegments: [UserSegment] = [] - @Published public private(set) var behaviorPatterns: [BehaviorPattern] = [] - @Published public private(set) var recentForecasts: [TimeSeriesForecast] = [] - @Published public private(set) var analyticsMetrics: AnalyticsMetrics = AnalyticsMetrics() - @Published public private(set) var processingQueue: [AnalyticsJob] = [] - - private var analysisTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "analytics.engine", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "Analytics") - - // MARK: - Initialization - - private init() { - self.dataProcessor = AnalyticsDataProcessor() - self.segmentationEngine = UserSegmentationEngine() - self.forecastingEngine = TimeSeriesForecastingEngine() - self.behaviorAnalyzer = BehaviorAnalysisEngine() - self.cohortAnalyzer = CohortAnalysisEngine() - self.patternRecognizer = PatternRecognitionEngine() - self.anomalyDetector = AnomalyDetectionEngine() - self.sentimentAnalyzer = SentimentAnalysisEngine() - self.storageManager = AnalyticsStorageManager() - - setupAnalyticsEngine() - } - - // MARK: - Public Methods - - /// Starts the analytics engine - public func startAnalytics() async throws { - guard configuration.isEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - await MainActor.run { - self.isAnalyticsEnabled = true - } - - // Load historical data - await loadHistoricalAnalytics() - - // Setup real-time analytics - if configuration.enableRealTimeAnalytics { - setupRealTimeAnalytics() - } - - // Setup periodic analysis - setupPeriodicAnalysis() - - logger.info("ML Analytics Engine started") - } - - /// Stops the analytics engine - public func stopAnalytics() async { - await MainActor.run { - self.isAnalyticsEnabled = false - } - - analysisTimer?.invalidate() - analysisTimer = nil - - // Save current state - await saveAnalyticsState() - - logger.info("ML Analytics Engine stopped") - } - - /// Records a new analytics data point - public func recordEvent( - userId: String? = nil, - widgetId: String? = nil, - eventType: String, - properties: [String: AnyCodable] = [:], - metadata: [String: String] = [:], - sessionId: String? = nil - ) async { - guard isAnalyticsEnabled else { return } - - let dataPoint = AnalyticsDataPoint( - userId: userId, - widgetId: widgetId, - eventType: eventType, - properties: properties, - metadata: metadata, - sessionId: sessionId - ) - - await dataProcessor.processDataPoint(dataPoint) - - // Trigger real-time analysis if enabled - if configuration.enableRealTimeAnalytics { - await triggerRealTimeAnalysis(for: dataPoint) - } - - await updateAnalyticsMetrics() - } - - /// Performs user segmentation analysis - public func performUserSegmentation( - criteria: [String: Any] = [:], - minSegmentSize: Int = 50 - ) async throws -> [UserSegment] { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let userData = try await dataProcessor.getUserData() - - guard userData.count >= configuration.minDataPointsForAnalysis else { - throw AnalyticsError.insufficientData - } - - let segments = try await segmentationEngine.segmentUsers( - userData: userData, - criteria: criteria, - minSegmentSize: minSegmentSize, - configuration: configuration - ) - - await MainActor.run { - self.currentSegments = segments - } - - await storageManager.saveSegments(segments) - - logger.info("User segmentation completed: \(segments.count) segments identified") - - return segments - } - - /// Performs time series forecasting - public func forecastMetric( - metric: String, - forecastPeriod: TimeInterval, - historicalPeriod: TimeInterval? = nil - ) async throws -> TimeSeriesForecast { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let timePeriod = historicalPeriod ?? (forecastPeriod * 4) // Default to 4x historical data - let historicalData = try await dataProcessor.getTimeSeriesData( - metric: metric, - period: timePeriod - ) - - guard historicalData.count >= configuration.minDataPointsForAnalysis else { - throw AnalyticsError.insufficientData - } - - let forecast = try await forecastingEngine.generateForecast( - metric: metric, - historicalData: historicalData, - forecastPeriod: forecastPeriod, - configuration: configuration - ) - - await MainActor.run { - self.recentForecasts.append(forecast) - - // Keep only recent forecasts - if self.recentForecasts.count > 100 { - self.recentForecasts.removeFirst(self.recentForecasts.count - 100) - } - } - - await storageManager.saveForecast(forecast) - - return forecast - } - - /// Analyzes user behavior patterns - public func analyzeBehaviorPatterns( - timeframe: DateInterval? = nil, - minPatternStrength: Double = 0.6 - ) async throws -> [BehaviorPattern] { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let period = timeframe ?? DateInterval( - start: Date().addingTimeInterval(-30 * 24 * 3600), // Last 30 days - end: Date() - ) - - let behaviorData = try await dataProcessor.getBehaviorData(for: period) - - let patterns = try await behaviorAnalyzer.identifyPatterns( - behaviorData: behaviorData, - minStrength: minPatternStrength, - configuration: configuration - ) - - await MainActor.run { - self.behaviorPatterns = patterns - } - - await storageManager.savePatterns(patterns) - - logger.info("Behavior analysis completed: \(patterns.count) patterns identified") - - return patterns - } - - /// Performs cohort analysis - public func performCohortAnalysis( - metric: String, - timeUnit: CohortAnalysisResult.CohortDefinition.TimeUnit, - startDate: Date, - endDate: Date - ) async throws -> CohortAnalysisResult { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let cohortDefinition = CohortAnalysisResult.CohortDefinition( - timeUnit: timeUnit, - metric: metric, - startDate: startDate, - endDate: endDate - ) - - let userData = try await dataProcessor.getCohortData(for: cohortDefinition) - - let result = try await cohortAnalyzer.analyzeCohorts( - definition: cohortDefinition, - userData: userData, - configuration: configuration - ) - - await storageManager.saveCohortAnalysis(result) - - return result - } - - /// Performs funnel analysis - public func performFunnelAnalysis( - funnelName: String, - steps: [String], - timeframe: DateInterval - ) async throws -> FunnelAnalysisResult { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let funnelData = try await dataProcessor.getFunnelData( - steps: steps, - timeframe: timeframe - ) - - let result = try await behaviorAnalyzer.analyzeFunnel( - name: funnelName, - steps: steps, - data: funnelData, - timeframe: timeframe, - configuration: configuration - ) - - await storageManager.saveFunnelAnalysis(result) - - return result - } - - /// Detects anomalies in metrics - public func detectAnomalies( - metrics: [String], - timeframe: DateInterval, - sensitivity: Double = 0.95 - ) async throws -> [AnomalyDetectionResult] { - guard isAnalyticsEnabled else { - throw AnalyticsError.analysisNotAvailable - } - - let metricsData = try await dataProcessor.getMetricsData( - metrics: metrics, - timeframe: timeframe - ) - - let anomalies = try await anomalyDetector.detectAnomalies( - data: metricsData, - sensitivity: sensitivity, - configuration: configuration - ) - - return anomalies - } - - /// Gets predictive scores for users - public func getPredictiveScores( - userIds: [String], - scoreType: String, - timeHorizon: TimeInterval = 7 * 24 * 3600 // 7 days - ) async throws -> [String: Double] { - guard isAnalyticsEnabled && configuration.enablePredictiveAnalytics else { - throw AnalyticsError.analysisNotAvailable - } - - let userData = try await dataProcessor.getUserData(for: userIds) - - let scores = try await behaviorAnalyzer.calculatePredictiveScores( - userData: userData, - scoreType: scoreType, - timeHorizon: timeHorizon, - configuration: configuration - ) - - return scores - } - - /// Gets analytics insights and recommendations - public func getAnalyticsInsights() async -> AnalyticsInsights { - let totalDataPoints = await dataProcessor.getTotalDataPoints() - let activeUsers = await dataProcessor.getActiveUserCount() - let processingSpeed = analyticsMetrics.averageProcessingTime - - return AnalyticsInsights( - totalDataPoints: totalDataPoints, - activeUsers: activeUsers, - segments: currentSegments.count, - patterns: behaviorPatterns.count, - forecasts: recentForecasts.count, - processingSpeed: processingSpeed, - dataQuality: await calculateDataQuality(), - insights: await generateInsights(), - recommendations: await generateRecommendations() - ) - } - - /// Exports analytics data for external analysis - public func exportAnalyticsData( - types: [AnalyticsType], - dateRange: DateInterval - ) async throws -> AnalyticsExport { - var exportData: [String: Any] = [:] - - for type in types { - switch type { - case .userSegmentation: - exportData["segments"] = currentSegments - case .behaviorAnalysis: - exportData["patterns"] = behaviorPatterns - case .timeSeriesForecasting: - exportData["forecasts"] = recentForecasts - case .cohortAnalysis: - let cohorts = try await storageManager.getCohortAnalyses(for: dateRange) - exportData["cohorts"] = cohorts - default: - // Add other analytics types as needed - break - } - } - - return AnalyticsExport( - exportedAt: Date(), - dateRange: dateRange, - types: types, - data: exportData, - metadata: [ - "total_users": String(await dataProcessor.getActiveUserCount()), - "data_points": String(await dataProcessor.getTotalDataPoints()), - "quality_score": String(await calculateDataQuality()) - ] - ) - } - - /// Updates analytics configuration - public func updateConfiguration(_ newConfiguration: AnalyticsConfiguration) async throws { - guard isValidConfiguration(newConfiguration) else { - throw AnalyticsError.invalidParameters - } - - let wasEnabled = isAnalyticsEnabled - - if wasEnabled { - await stopAnalytics() - } - - await MainActor.run { - self.configuration = newConfiguration - } - - if wasEnabled && newConfiguration.isEnabled { - try await startAnalytics() - } - - logger.info("Analytics configuration updated") - } - - /// Clears all analytics data - public func clearAnalyticsData() async throws { - await MainActor.run { - self.currentSegments.removeAll() - self.behaviorPatterns.removeAll() - self.recentForecasts.removeAll() - self.processingQueue.removeAll() - self.analyticsMetrics = AnalyticsMetrics() - } - - await storageManager.clearAllData() - - logger.info("Analytics data cleared") - } - - // MARK: - Private Methods - - private func setupAnalyticsEngine() { - // Setup notification observers - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - - logger.info("ML Analytics Engine initialized") - } - - private func loadHistoricalAnalytics() async { - do { - let segments = try await storageManager.loadSegments() - let patterns = try await storageManager.loadPatterns() - let forecasts = try await storageManager.loadForecasts() - - await MainActor.run { - self.currentSegments = segments - self.behaviorPatterns = patterns - self.recentForecasts = forecasts - } - } catch { - logger.error("Failed to load historical analytics: \(error)") - } - } - - private func setupRealTimeAnalytics() { - // Setup real-time analytics processing - Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in - Task { - await self?.processRealTimeAnalytics() - } - } - } - - private func setupPeriodicAnalysis() { - analysisTimer = Timer.scheduledTimer(withTimeInterval: configuration.analysisUpdateInterval, repeats: true) { [weak self] _ in - Task { - await self?.performPeriodicAnalysis() - } - } - } - - private func triggerRealTimeAnalysis(for dataPoint: AnalyticsDataPoint) async { - // Perform real-time analysis on new data point - await patternRecognizer.analyzeDataPoint(dataPoint) - - // Update any real-time metrics - await updateRealTimeMetrics(with: dataPoint) - } - - private func processRealTimeAnalytics() async { - // Process pending real-time analytics - await processAnalyticsQueue() - } - - private func performPeriodicAnalysis() async { - guard isAnalyticsEnabled else { return } - - do { - // Perform automated analytics - _ = try await performUserSegmentation() - _ = try await analyzeBehaviorPatterns() - - // Update forecasts - let commonMetrics = ["user_engagement", "widget_usage", "retention_rate"] - for metric in commonMetrics { - _ = try? await forecastMetric(metric: metric, forecastPeriod: 7 * 24 * 3600) // 7 days - } - - } catch { - logger.error("Periodic analysis failed: \(error)") - } - } - - private func processAnalyticsQueue() async { - // Process pending analytics jobs - let jobs = processingQueue - - await MainActor.run { - self.processingQueue.removeAll() - } - - for job in jobs { - await processAnalyticsJob(job) - } - } - - private func processAnalyticsJob(_ job: AnalyticsJob) async { - // Process individual analytics job - let startTime = CFAbsoluteTimeGetCurrent() - - defer { - let processingTime = CFAbsoluteTimeGetCurrent() - startTime - Task { - await self.updateProcessingMetrics(processingTime: processingTime) - } - } - - // Process job based on type - // Implementation would handle different job types - } - - private func updateAnalyticsMetrics() async { - await MainActor.run { - self.analyticsMetrics.totalEvents += 1 - self.analyticsMetrics.lastProcessedAt = Date() - } - } - - private func updateRealTimeMetrics(with dataPoint: AnalyticsDataPoint) async { - await MainActor.run { - self.analyticsMetrics.realTimeEvents += 1 - } - } - - private func updateProcessingMetrics(processingTime: TimeInterval) async { - await MainActor.run { - self.analyticsMetrics.totalProcessingTime += processingTime - self.analyticsMetrics.processedJobs += 1 - self.analyticsMetrics.averageProcessingTime = - self.analyticsMetrics.totalProcessingTime / Double(self.analyticsMetrics.processedJobs) - } - } - - private func saveAnalyticsState() async { - do { - try await storageManager.saveSegments(currentSegments) - try await storageManager.savePatterns(behaviorPatterns) - try await storageManager.saveForecasts(recentForecasts) - } catch { - logger.error("Failed to save analytics state: \(error)") - } - } - - private func calculateDataQuality() async -> Double { - // Calculate data quality score based on completeness, accuracy, consistency - let completeness = await dataProcessor.getDataCompleteness() - let accuracy = await dataProcessor.getDataAccuracy() - let consistency = await dataProcessor.getDataConsistency() - - return (completeness * 0.4 + accuracy * 0.4 + consistency * 0.2).clamped(to: 0...1) - } - - private func generateInsights() async -> [String] { - var insights: [String] = [] - - // Generate insights based on current analytics - if currentSegments.count > 0 { - insights.append("Identified \(currentSegments.count) distinct user segments") - } - - if behaviorPatterns.count > 0 { - let strongPatterns = behaviorPatterns.filter { $0.strength > 0.8 }.count - insights.append("Found \(strongPatterns) strong behavior patterns") - } - - if !recentForecasts.isEmpty { - let averageConfidence = recentForecasts.map { $0.confidence }.reduce(0, +) / Double(recentForecasts.count) - insights.append("Forecasting models showing \(Int(averageConfidence * 100))% average confidence") - } - - return insights - } - - private func generateRecommendations() async -> [String] { - var recommendations: [String] = [] - - // Generate recommendations based on analytics - let dataQuality = await calculateDataQuality() - if dataQuality < 0.8 { - recommendations.append("Improve data collection to enhance analytics accuracy") - } - - if currentSegments.isEmpty { - recommendations.append("Perform user segmentation to better understand your audience") - } - - if behaviorPatterns.count < 5 { - recommendations.append("Collect more behavioral data to identify usage patterns") - } - - return recommendations - } - - private func isValidConfiguration(_ config: AnalyticsConfiguration) -> Bool { - return config.dataRetentionDays > 0 && - config.minDataPointsForAnalysis > 0 && - config.analysisUpdateInterval > 0 && - config.confidenceThreshold >= 0 && - config.confidenceThreshold <= 1 && - config.maxConcurrentAnalyses > 0 - } - - private func handleAppDidEnterBackground() async { - // Save state and optimize for background operation - await saveAnalyticsState() - } -} - -// MARK: - Supporting Types - -public struct AnalyticsMetrics { - public var totalEvents: Int = 0 - public var realTimeEvents: Int = 0 - public var processedJobs: Int = 0 - public var totalProcessingTime: TimeInterval = 0 - public var averageProcessingTime: TimeInterval = 0 - public var lastProcessedAt: Date? -} - -public struct AnalyticsJob { - public let id: String - public let type: MLAnalyticsEngine.AnalyticsType - public let parameters: [String: Any] - public let priority: Int - public let createdAt: Date - - public init(type: MLAnalyticsEngine.AnalyticsType, parameters: [String: Any], priority: Int = 1) { - self.id = UUID().uuidString - self.type = type - self.parameters = parameters - self.priority = priority - self.createdAt = Date() - } -} - -public struct AnalyticsInsights { - public let totalDataPoints: Int - public let activeUsers: Int - public let segments: Int - public let patterns: Int - public let forecasts: Int - public let processingSpeed: TimeInterval - public let dataQuality: Double - public let insights: [String] - public let recommendations: [String] -} - -public struct AnalyticsExport: Codable { - public let exportedAt: Date - public let dateRange: DateInterval - public let types: [MLAnalyticsEngine.AnalyticsType] - public let data: [String: Any] - public let metadata: [String: String] - - enum CodingKeys: String, CodingKey { - case exportedAt, dateRange, types, metadata - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(exportedAt, forKey: .exportedAt) - try container.encode(dateRange, forKey: .dateRange) - try container.encode(types, forKey: .types) - try container.encode(metadata, forKey: .metadata) - // Note: data field would need custom encoding in real implementation - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - exportedAt = try container.decode(Date.self, forKey: .exportedAt) - dateRange = try container.decode(DateInterval.self, forKey: .dateRange) - types = try container.decode([MLAnalyticsEngine.AnalyticsType].self, forKey: .types) - metadata = try container.decode([String: String].self, forKey: .metadata) - data = [:] // Would be decoded from custom format - } - - init(exportedAt: Date, dateRange: DateInterval, types: [MLAnalyticsEngine.AnalyticsType], data: [String: Any], metadata: [String: String]) { - self.exportedAt = exportedAt - self.dateRange = dateRange - self.types = types - self.data = data - self.metadata = metadata - } -} - -public struct AnyCodable: Codable { - public let value: Any - - public init(_ value: T?) { - self.value = value ?? () - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch value { - case let value as String: - try container.encode(value) - case let value as Int: - try container.encode(value) - case let value as Double: - try container.encode(value) - case let value as Bool: - try container.encode(value) - default: - try container.encodeNil() - } - } - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if let value = try? container.decode(String.self) { - self.value = value - } else if let value = try? container.decode(Int.self) { - self.value = value - } else if let value = try? container.decode(Double.self) { - self.value = value - } else if let value = try? container.decode(Bool.self) { - self.value = value - } else { - self.value = () - } - } -} - -// MARK: - Private Supporting Classes - -private class AnalyticsDataProcessor { - func processDataPoint(_ dataPoint: MLAnalyticsEngine.AnalyticsDataPoint) async { - // Process incoming data point - } - - func getUserData() async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return user data grouped by user ID - return [:] - } - - func getUserData(for userIds: [String]) async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return user data for specific users - return [:] - } - - func getTimeSeriesData(metric: String, period: TimeInterval) async throws -> [(Date, Double)] { - // Return time series data for metric - return [] - } - - func getBehaviorData(for period: DateInterval) async throws -> [MLAnalyticsEngine.AnalyticsDataPoint] { - // Return behavior data for period - return [] - } - - func getCohortData(for definition: MLAnalyticsEngine.CohortAnalysisResult.CohortDefinition) async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return cohort data - return [:] - } - - func getFunnelData(steps: [String], timeframe: DateInterval) async throws -> [String: [MLAnalyticsEngine.AnalyticsDataPoint]] { - // Return funnel data - return [:] - } - - func getMetricsData(metrics: [String], timeframe: DateInterval) async throws -> [String: [(Date, Double)]] { - // Return metrics data - return [:] - } - - func getTotalDataPoints() async -> Int { - return 10000 // Mock value - } - - func getActiveUserCount() async -> Int { - return 500 // Mock value - } - - func getDataCompleteness() async -> Double { - return 0.85 // Mock value - } - - func getDataAccuracy() async -> Double { - return 0.92 // Mock value - } - - func getDataConsistency() async -> Double { - return 0.88 // Mock value - } -} - -private class UserSegmentationEngine { - func segmentUsers( - userData: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - criteria: [String: Any], - minSegmentSize: Int, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [MLAnalyticsEngine.UserSegment] { - // Perform user segmentation - return [] - } -} - -private class TimeSeriesForecastingEngine { - func generateForecast( - metric: String, - historicalData: [(Date, Double)], - forecastPeriod: TimeInterval, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> MLAnalyticsEngine.TimeSeriesForecast { - // Generate time series forecast - let predictions: [MLAnalyticsEngine.TimeSeriesForecast.ForecastPoint] = [] - - return MLAnalyticsEngine.TimeSeriesForecast( - metric: metric, - forecastPeriod: DateInterval(start: Date(), duration: forecastPeriod), - predictions: predictions, - confidence: 0.85, - methodology: "ARIMA", - seasonality: nil, - trend: MLAnalyticsEngine.TimeSeriesForecast.TrendInfo( - direction: .stable, - strength: 0.3, - changePoints: [] - ) - ) - } -} - -private class BehaviorAnalysisEngine { - func identifyPatterns( - behaviorData: [MLAnalyticsEngine.AnalyticsDataPoint], - minStrength: Double, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [MLAnalyticsEngine.BehaviorPattern] { - // Identify behavior patterns - return [] - } - - func analyzeFunnel( - name: String, - steps: [String], - data: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - timeframe: DateInterval, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> MLAnalyticsEngine.FunnelAnalysisResult { - // Analyze funnel - return MLAnalyticsEngine.FunnelAnalysisResult( - funnelName: name, - steps: [], - conversionRate: 0.0, - dropOffPoints: [], - insights: [], - timeframe: timeframe - ) - } - - func calculatePredictiveScores( - userData: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - scoreType: String, - timeHorizon: TimeInterval, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [String: Double] { - // Calculate predictive scores - return [:] - } -} - -private class CohortAnalysisEngine { - func analyzeCohorts( - definition: MLAnalyticsEngine.CohortAnalysisResult.CohortDefinition, - userData: [String: [MLAnalyticsEngine.AnalyticsDataPoint]], - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> MLAnalyticsEngine.CohortAnalysisResult { - // Perform cohort analysis - return MLAnalyticsEngine.CohortAnalysisResult( - cohortDefinition: definition, - cohorts: [], - metrics: [:], - insights: [], - generatedAt: Date() - ) - } -} - -private class PatternRecognitionEngine { - func analyzeDataPoint(_ dataPoint: MLAnalyticsEngine.AnalyticsDataPoint) async { - // Analyze data point for patterns - } -} - -private class AnomalyDetectionEngine { - func detectAnomalies( - data: [String: [(Date, Double)]], - sensitivity: Double, - configuration: MLAnalyticsEngine.AnalyticsConfiguration - ) async throws -> [AnomalyDetectionResult] { - // Detect anomalies in data - return [] - } -} - -private class SentimentAnalysisEngine { - // Sentiment analysis implementation -} - -private class AnalyticsStorageManager { - func saveSegments(_ segments: [MLAnalyticsEngine.UserSegment]) async throws { - // Save segments to storage - } - - func loadSegments() async throws -> [MLAnalyticsEngine.UserSegment] { - // Load segments from storage - return [] - } - - func savePatterns(_ patterns: [MLAnalyticsEngine.BehaviorPattern]) async throws { - // Save patterns to storage - } - - func loadPatterns() async throws -> [MLAnalyticsEngine.BehaviorPattern] { - // Load patterns from storage - return [] - } - - func saveForecasts(_ forecasts: [MLAnalyticsEngine.TimeSeriesForecast]) async throws { - // Save forecasts to storage - } - - func loadForecasts() async throws -> [MLAnalyticsEngine.TimeSeriesForecast] { - // Load forecasts from storage - return [] - } - - func saveForecast(_ forecast: MLAnalyticsEngine.TimeSeriesForecast) async throws { - // Save single forecast - } - - func saveCohortAnalysis(_ analysis: MLAnalyticsEngine.CohortAnalysisResult) async throws { - // Save cohort analysis - } - - func getCohortAnalyses(for dateRange: DateInterval) async throws -> [MLAnalyticsEngine.CohortAnalysisResult] { - // Get cohort analyses for date range - return [] - } - - func saveFunnelAnalysis(_ analysis: MLAnalyticsEngine.FunnelAnalysisResult) async throws { - // Save funnel analysis - } - - func clearAllData() async throws { - // Clear all stored data - } -} - -extension Double { - func clamped(to range: ClosedRange) -> Double { - return Swift.min(Swift.max(self, range.lowerBound), range.upperBound) - } -} \ No newline at end of file diff --git a/Sources/Integration/APIManager.swift b/Sources/Integration/APIManager.swift deleted file mode 100644 index 5753017..0000000 --- a/Sources/Integration/APIManager.swift +++ /dev/null @@ -1,1162 +0,0 @@ -import Foundation -import Network -import Combine -import OSLog - -/// Enterprise-grade API management system providing comprehensive RESTful, GraphQL, -/// and WebSocket integrations with advanced security, monitoring, and resilience features -@available(iOS 16.0, *) -public class APIManager: ObservableObject { - - // MARK: - Types - - public enum APIError: LocalizedError { - case invalidURL - case networkUnavailable - case authenticationFailed - case rateLimitExceeded - case serverError(Int, String) - case timeoutError - case serializationError - case validationError(String) - case circuitBreakerOpen - case retryLimitExceeded - case certificatePinningFailed - case requestTooLarge - case responseProcessingFailed - - public var errorDescription: String? { - switch self { - case .invalidURL: - return "Invalid URL provided" - case .networkUnavailable: - return "Network connection unavailable" - case .authenticationFailed: - return "Authentication failed" - case .rateLimitExceeded: - return "Rate limit exceeded" - case .serverError(let code, let message): - return "Server error \(code): \(message)" - case .timeoutError: - return "Request timeout" - case .serializationError: - return "Data serialization failed" - case .validationError(let details): - return "Validation error: \(details)" - case .circuitBreakerOpen: - return "Circuit breaker is open" - case .retryLimitExceeded: - return "Maximum retry attempts exceeded" - case .certificatePinningFailed: - return "Certificate pinning validation failed" - case .requestTooLarge: - return "Request payload too large" - case .responseProcessingFailed: - return "Response processing failed" - } - } - } - - public enum HTTPMethod: String, CaseIterable { - case GET = "GET" - case POST = "POST" - case PUT = "PUT" - case DELETE = "DELETE" - case PATCH = "PATCH" - case HEAD = "HEAD" - case OPTIONS = "OPTIONS" - } - - public enum ContentType: String { - case json = "application/json" - case formURLEncoded = "application/x-www-form-urlencoded" - case multipartFormData = "multipart/form-data" - case plainText = "text/plain" - case xml = "application/xml" - case protobuf = "application/x-protobuf" - } - - public struct APIConfiguration { - public var baseURL: String - public var timeout: TimeInterval = 30.0 - public var retryPolicy: RetryPolicy = RetryPolicy() - public var rateLimiting: RateLimitConfig = RateLimitConfig() - public var security: SecurityConfig = SecurityConfig() - public var caching: CacheConfig = CacheConfig() - public var monitoring: MonitoringConfig = MonitoringConfig() - public var circuitBreaker: CircuitBreakerConfig = CircuitBreakerConfig() - public var headers: [String: String] = [:] - public var enableCompression: Bool = true - public var enableLogging: Bool = true - - public struct RetryPolicy { - public var maxRetries: Int = 3 - public var backoffStrategy: BackoffStrategy = .exponential - public var retryableStatusCodes: Set = [408, 429, 500, 502, 503, 504] - public var retryableErrors: Set = [.timedOut, .networkConnectionLost, .notConnectedToInternet] - - public enum BackoffStrategy { - case linear(TimeInterval) - case exponential(base: TimeInterval = 1.0, multiplier: Double = 2.0) - case fixed(TimeInterval) - case custom((Int) -> TimeInterval) - } - } - - public struct RateLimitConfig { - public var requestsPerSecond: Int = 10 - public var burstLimit: Int = 50 - public var enableAdaptiveRateLimit: Bool = true - public var trackByEndpoint: Bool = true - } - - public struct SecurityConfig { - public var enableCertificatePinning: Bool = true - public var pinnedCertificates: [String] = [] - public var enableTLSValidation: Bool = true - public var allowInsecureConnections: Bool = false - public var customTrustEvaluator: ((SecTrust) -> Bool)? - public var enableRequestSigning: Bool = false - public var signingAlgorithm: String = "HMAC-SHA256" - } - - public struct CacheConfig { - public var enableCaching: Bool = true - public var cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad - public var maxCacheAge: TimeInterval = 3600 - public var cacheSizeLimit: Int = 100 * 1024 * 1024 // 100MB - public var enableETagSupport: Bool = true - } - - public struct MonitoringConfig { - public var enableMetrics: Bool = true - public var enableTracing: Bool = true - public var enableHealthChecks: Bool = true - public var metricsReportingInterval: TimeInterval = 60 - public var traceHeaders: [String] = ["X-Trace-ID", "X-Span-ID"] - } - - public struct CircuitBreakerConfig { - public var isEnabled: Bool = true - public var failureThreshold: Int = 5 - public var recoveryTimeout: TimeInterval = 30 - public var halfOpenMaxCalls: Int = 3 - public var monitoringPeriod: TimeInterval = 60 - } - - public init(baseURL: String) { - self.baseURL = baseURL - } - } - - public struct APIRequest { - public let method: HTTPMethod - public let endpoint: String - public let parameters: [String: Any]? - public let headers: [String: String] - public let body: Data? - public let contentType: ContentType - public let timeout: TimeInterval? - public let priority: Priority - public let tags: [String] - public let cachePolicy: URLRequest.CachePolicy? - public let retryPolicy: APIConfiguration.RetryPolicy? - - public enum Priority: Int, CaseIterable { - case low = 0 - case normal = 1 - case high = 2 - case critical = 3 - } - - public init( - method: HTTPMethod = .GET, - endpoint: String, - parameters: [String: Any]? = nil, - headers: [String: String] = [:], - body: Data? = nil, - contentType: ContentType = .json, - timeout: TimeInterval? = nil, - priority: Priority = .normal, - tags: [String] = [], - cachePolicy: URLRequest.CachePolicy? = nil, - retryPolicy: APIConfiguration.RetryPolicy? = nil - ) { - self.method = method - self.endpoint = endpoint - self.parameters = parameters - self.headers = headers - self.body = body - self.contentType = contentType - self.timeout = timeout - self.priority = priority - self.tags = tags - self.cachePolicy = cachePolicy - self.retryPolicy = retryPolicy - } - } - - public struct APIResponse { - public let data: T? - public let statusCode: Int - public let headers: [String: String] - public let responseTime: TimeInterval - public let fromCache: Bool - public let retryCount: Int - public let traceId: String? - public let rawData: Data? - - public var isSuccessful: Bool { - return 200...299 ~= statusCode - } - - public init( - data: T?, - statusCode: Int, - headers: [String: String] = [:], - responseTime: TimeInterval, - fromCache: Bool = false, - retryCount: Int = 0, - traceId: String? = nil, - rawData: Data? = nil - ) { - self.data = data - self.statusCode = statusCode - self.headers = headers - self.responseTime = responseTime - self.fromCache = fromCache - self.retryCount = retryCount - self.traceId = traceId - self.rawData = rawData - } - } - - public struct GraphQLRequest { - public let query: String - public let variables: [String: Any]? - public let operationName: String? - public let timeout: TimeInterval? - public let priority: APIRequest.Priority - - public init( - query: String, - variables: [String: Any]? = nil, - operationName: String? = nil, - timeout: TimeInterval? = nil, - priority: APIRequest.Priority = .normal - ) { - self.query = query - self.variables = variables - self.operationName = operationName - self.timeout = timeout - self.priority = priority - } - } - - public struct WebSocketConfig { - public let url: URL - public let protocols: [String] - public let headers: [String: String] - public let pingInterval: TimeInterval - public let reconnectPolicy: ReconnectPolicy - public let compression: Bool - - public struct ReconnectPolicy { - public let maxRetries: Int - public let backoffStrategy: APIConfiguration.RetryPolicy.BackoffStrategy - public let enableAutoReconnect: Bool - } - - public init( - url: URL, - protocols: [String] = [], - headers: [String: String] = [:], - pingInterval: TimeInterval = 30, - maxRetries: Int = 5, - enableAutoReconnect: Bool = true, - compression: Bool = true - ) { - self.url = url - self.protocols = protocols - self.headers = headers - self.pingInterval = pingInterval - self.reconnectPolicy = ReconnectPolicy( - maxRetries: maxRetries, - backoffStrategy: .exponential(), - enableAutoReconnect: enableAutoReconnect - ) - self.compression = compression - } - } - - public struct APIMetrics { - public var totalRequests: Int = 0 - public var successfulRequests: Int = 0 - public var failedRequests: Int = 0 - public var averageResponseTime: TimeInterval = 0 - public var cacheHitRate: Double = 0 - public var rateLimitHits: Int = 0 - public var circuitBreakerTrips: Int = 0 - public var bytesTransferred: Int = 0 - public var lastRequestTime: Date? - - public var successRate: Double { - guard totalRequests > 0 else { return 0 } - return Double(successfulRequests) / Double(totalRequests) - } - - public var errorRate: Double { - return 1.0 - successRate - } - } - - // MARK: - Properties - - public static let shared = APIManager() - - private let session: URLSession - private let configuration: APIConfiguration - private let rateLimiter: RateLimiter - private let circuitBreaker: CircuitBreaker - private let retryHandler: RetryHandler - private let securityHandler: SecurityHandler - private let metricsCollector: MetricsCollector - private let cacheManager: APICacheManager - private let requestQueue: RequestQueue - private let websocketManager: WebSocketManager - - @Published public private(set) var metrics: APIMetrics = APIMetrics() - @Published public private(set) var isNetworkAvailable: Bool = true - @Published public private(set) var activeConnections: Int = 0 - @Published public private(set) var circuitBreakerState: CircuitBreakerState = .closed - - private let networkMonitor = NWPathMonitor() - private var cancellables = Set() - private let queue = DispatchQueue(label: "api.manager", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "API") - - public enum CircuitBreakerState { - case closed - case open - case halfOpen - } - - // MARK: - Initialization - - public init(configuration: APIConfiguration? = nil) { - self.configuration = configuration ?? APIConfiguration(baseURL: "https://api.example.com") - - // Configure URLSession - let sessionConfig = URLSessionConfiguration.default - sessionConfig.timeoutIntervalForRequest = self.configuration.timeout - sessionConfig.timeoutIntervalForResource = self.configuration.timeout * 2 - sessionConfig.requestCachePolicy = self.configuration.caching.cachePolicy - sessionConfig.urlCache = URLCache( - memoryCapacity: self.configuration.caching.cacheSizeLimit / 2, - diskCapacity: self.configuration.caching.cacheSizeLimit, - diskPath: "APICache" - ) - - if self.configuration.enableCompression { - sessionConfig.httpAdditionalHeaders = ["Accept-Encoding": "gzip, deflate, br"] - } - - self.session = URLSession(configuration: sessionConfig) - - // Initialize components - self.rateLimiter = RateLimiter(config: self.configuration.rateLimiting) - self.circuitBreaker = CircuitBreaker(config: self.configuration.circuitBreaker) - self.retryHandler = RetryHandler(config: self.configuration.retryPolicy) - self.securityHandler = SecurityHandler(config: self.configuration.security) - self.metricsCollector = MetricsCollector() - self.cacheManager = APICacheManager(config: self.configuration.caching) - self.requestQueue = RequestQueue() - self.websocketManager = WebSocketManager() - - setupAPIManager() - } - - // MARK: - Public Methods - - /// Executes a REST API request with comprehensive error handling and monitoring - public func request( - _ request: APIRequest, - responseType: T.Type - ) async throws -> APIResponse { - let startTime = CFAbsoluteTimeGetCurrent() - let traceId = UUID().uuidString - - // Pre-flight checks - try await performPreflightChecks(request) - - // Rate limiting - try await rateLimiter.checkLimit(for: request.endpoint) - - // Circuit breaker check - try circuitBreaker.checkState() - - // Build URL request - let urlRequest = try buildURLRequest(from: request, traceId: traceId) - - // Check cache first - if let cachedResponse: APIResponse = await cacheManager.getCachedResponse(for: urlRequest) { - await updateMetrics(success: true, responseTime: 0, fromCache: true) - return cachedResponse - } - - var lastError: Error? - let maxRetries = request.retryPolicy?.maxRetries ?? configuration.retryPolicy.maxRetries - - for attempt in 0...maxRetries { - do { - // Execute request - let (data, response) = try await session.data(for: urlRequest) - let httpResponse = response as! HTTPURLResponse - - // Measure response time - let responseTime = CFAbsoluteTimeGetCurrent() - startTime - - // Validate response - try validateResponse(httpResponse, data: data) - - // Parse response - let parsedData = try parseResponse(data: data, type: responseType) - - let apiResponse = APIResponse( - data: parsedData, - statusCode: httpResponse.statusCode, - headers: extractHeaders(from: httpResponse), - responseTime: responseTime, - fromCache: false, - retryCount: attempt, - traceId: traceId, - rawData: data - ) - - // Cache successful response - if apiResponse.isSuccessful { - await cacheManager.cacheResponse(apiResponse, for: urlRequest) - } - - // Update metrics and circuit breaker - await updateMetrics(success: apiResponse.isSuccessful, responseTime: responseTime) - circuitBreaker.recordSuccess() - - logger.info("API request successful: \(request.endpoint) (\(responseTime)ms)") - - return apiResponse - - } catch { - lastError = error - - // Record failure - circuitBreaker.recordFailure() - - // Check if retry is appropriate - if attempt < maxRetries && shouldRetry(error: error, request: request) { - let delay = calculateRetryDelay(attempt: attempt, strategy: request.retryPolicy?.backoffStrategy ?? configuration.retryPolicy.backoffStrategy) - await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - continue - } - - break - } - } - - // All retries exhausted - await updateMetrics(success: false, responseTime: CFAbsoluteTimeGetCurrent() - startTime) - - if let error = lastError { - logger.error("API request failed: \(request.endpoint) - \(error)") - throw error - } else { - throw APIError.retryLimitExceeded - } - } - - /// Executes a GraphQL query with optimized batching and caching - public func graphQL( - _ request: GraphQLRequest, - responseType: T.Type - ) async throws -> APIResponse { - let graphQLBody: [String: Any] = [ - "query": request.query, - "variables": request.variables ?? [:], - "operationName": request.operationName as Any - ] - - let bodyData = try JSONSerialization.data(withJSONObject: graphQLBody) - - let apiRequest = APIRequest( - method: .POST, - endpoint: "/graphql", - body: bodyData, - contentType: .json, - timeout: request.timeout, - priority: request.priority - ) - - return try await self.request(apiRequest, responseType: responseType) - } - - /// Establishes and manages WebSocket connections - public func connectWebSocket( - config: WebSocketConfig, - onMessage: @escaping (Data) -> Void, - onConnect: @escaping () -> Void, - onDisconnect: @escaping (Error?) -> Void - ) async throws -> WebSocketConnection { - return try await websocketManager.connect( - config: config, - onMessage: onMessage, - onConnect: onConnect, - onDisconnect: onDisconnect - ) - } - - /// Uploads files with progress tracking and resumable uploads - public func uploadFile( - endpoint: String, - fileURL: URL, - parameters: [String: String] = [:], - progressHandler: @escaping (Double) -> Void - ) async throws -> APIResponse { - let boundary = UUID().uuidString - let contentType = "multipart/form-data; boundary=\(boundary)" - - // Create multipart body - let body = try createMultipartBody( - fileURL: fileURL, - parameters: parameters, - boundary: boundary - ) - - let request = APIRequest( - method: .POST, - endpoint: endpoint, - body: body, - contentType: ContentType(rawValue: contentType) ?? .multipartFormData, - priority: .high - ) - - // TODO: Implement progress tracking with URLSessionUploadTask - return try await self.request(request, responseType: Data.self) - } - - /// Downloads files with progress tracking and resumable downloads - public func downloadFile( - from url: URL, - to destinationURL: URL, - progressHandler: @escaping (Double) -> Void - ) async throws -> URL { - // TODO: Implement download with URLSessionDownloadTask - return destinationURL - } - - /// Executes batch requests with optimal parallelization - public func batchRequests( - _ requests: [APIRequest], - responseType: T.Type, - maxConcurrency: Int = 5 - ) async throws -> [Result, Error>] { - return await withTaskGroup(of: (Int, Result, Error>).self, returning: [Result, Error>].self) { group in - let semaphore = AsyncSemaphore(value: maxConcurrency) - - // Add tasks to group - for (index, request) in requests.enumerated() { - group.addTask { - await semaphore.wait() - defer { semaphore.signal() } - - do { - let response = try await self.request(request, responseType: responseType) - return (index, .success(response)) - } catch { - return (index, .failure(error)) - } - } - } - - // Collect results - var results: [(Int, Result, Error>)] = [] - for await result in group { - results.append(result) - } - - // Sort by original order - results.sort { $0.0 < $1.0 } - return results.map { $0.1 } - } - } - - /// Gets comprehensive API health status - public func getHealthStatus() async -> APIHealthStatus { - let latency = await measureLatency() - let networkStatus = await checkNetworkConnectivity() - - return APIHealthStatus( - isHealthy: circuitBreakerState == .closed && isNetworkAvailable, - latency: latency, - circuitBreakerState: circuitBreakerState, - networkAvailable: isNetworkAvailable, - activeConnections: activeConnections, - rateLimitStatus: await rateLimiter.getStatus(), - cacheStatus: await cacheManager.getStatus(), - metrics: metrics, - lastHealthCheck: Date() - ) - } - - /// Invalidates cache for specific endpoints or all cached data - public func invalidateCache(endpoints: [String] = []) async { - if endpoints.isEmpty { - await cacheManager.clearAll() - } else { - for endpoint in endpoints { - await cacheManager.invalidate(endpoint: endpoint) - } - } - - logger.info("Cache invalidated for \(endpoints.isEmpty ? "all endpoints" : endpoints.joined(separator: ", "))") - } - - /// Updates API configuration dynamically - public func updateConfiguration(_ newConfiguration: APIConfiguration) async { - // Update internal configuration - // Note: In a real implementation, this would update all components - logger.info("API configuration updated") - } - - /// Gets detailed API analytics and performance insights - public func getAnalytics(timeframe: TimeInterval = 3600) async -> APIAnalytics { - return await metricsCollector.generateAnalytics(for: timeframe) - } - - // MARK: - Private Methods - - private func setupAPIManager() { - // Setup network monitoring - networkMonitor.pathUpdateHandler = { [weak self] path in - DispatchQueue.main.async { - self?.isNetworkAvailable = path.status == .satisfied - } - } - networkMonitor.start(queue: queue) - - // Setup circuit breaker monitoring - circuitBreaker.onStateChange = { [weak self] state in - DispatchQueue.main.async { - self?.circuitBreakerState = state - } - } - - logger.info("API Manager initialized with base URL: \(configuration.baseURL)") - } - - private func performPreflightChecks(_ request: APIRequest) async throws { - // Network availability check - guard isNetworkAvailable else { - throw APIError.networkUnavailable - } - - // Request size check - if let body = request.body, body.count > 10 * 1024 * 1024 { // 10MB limit - throw APIError.requestTooLarge - } - - // Security validation - try securityHandler.validateRequest(request) - } - - private func buildURLRequest(from request: APIRequest, traceId: String) throws -> URLRequest { - // Build URL - guard let baseURL = URL(string: configuration.baseURL) else { - throw APIError.invalidURL - } - - let fullURL = baseURL.appendingPathComponent(request.endpoint) - - var urlRequest = URLRequest(url: fullURL) - urlRequest.httpMethod = request.method.rawValue - urlRequest.timeoutInterval = request.timeout ?? configuration.timeout - - // Add headers - var headers = configuration.headers - headers.merge(request.headers) { _, new in new } - headers["Content-Type"] = request.contentType.rawValue - headers["Accept"] = "application/json" - headers["User-Agent"] = "iOS-Widget-Development-Kit/1.0" - headers["X-Trace-ID"] = traceId - - for (key, value) in headers { - urlRequest.setValue(value, forHTTPHeaderField: key) - } - - // Add body - if let body = request.body { - urlRequest.httpBody = body - } else if let parameters = request.parameters, request.method != .GET { - urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters) - } - - // Add query parameters for GET requests - if request.method == .GET, let parameters = request.parameters { - let queryItems = parameters.map { URLQueryItem(name: $0.key, value: "\($0.value)") } - var components = URLComponents(url: fullURL, resolvingAgainstBaseURL: false) - components?.queryItems = queryItems - if let newURL = components?.url { - urlRequest.url = newURL - } - } - - // Apply cache policy - if let cachePolicy = request.cachePolicy { - urlRequest.cachePolicy = cachePolicy - } - - return urlRequest - } - - private func validateResponse(_ response: HTTPURLResponse, data: Data) throws { - let statusCode = response.statusCode - - switch statusCode { - case 200...299: - break // Success - case 401: - throw APIError.authenticationFailed - case 429: - throw APIError.rateLimitExceeded - case 500...599: - let message = String(data: data, encoding: .utf8) ?? "Unknown server error" - throw APIError.serverError(statusCode, message) - default: - let message = String(data: data, encoding: .utf8) ?? "Unknown error" - throw APIError.serverError(statusCode, message) - } - } - - private func parseResponse(data: Data, type: T.Type) throws -> T { - do { - if type == Data.self { - return data as! T - } else if type == String.self { - guard let string = String(data: data, encoding: .utf8) else { - throw APIError.serializationError - } - return string as! T - } else { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .iso8601 - return try decoder.decode(type, from: data) - } - } catch { - throw APIError.serializationError - } - } - - private func extractHeaders(from response: HTTPURLResponse) -> [String: String] { - var headers: [String: String] = [:] - for (key, value) in response.allHeaderFields { - if let stringKey = key as? String, let stringValue = value as? String { - headers[stringKey] = stringValue - } - } - return headers - } - - private func shouldRetry(error: Error, request: APIRequest) -> Bool { - let retryPolicy = request.retryPolicy ?? configuration.retryPolicy - - if let apiError = error as? APIError { - switch apiError { - case .timeoutError, .networkUnavailable, .serverError(let code, _): - return retryPolicy.retryableStatusCodes.contains(code) - default: - return false - } - } - - if let urlError = error as? URLError { - return retryPolicy.retryableErrors.contains(urlError.code) - } - - return false - } - - private func calculateRetryDelay(attempt: Int, strategy: APIConfiguration.RetryPolicy.BackoffStrategy) -> TimeInterval { - switch strategy { - case .linear(let interval): - return interval * TimeInterval(attempt + 1) - case .exponential(let base, let multiplier): - return base * pow(multiplier, Double(attempt)) - case .fixed(let interval): - return interval - case .custom(let calculator): - return calculator(attempt) - } - } - - private func createMultipartBody(fileURL: URL, parameters: [String: String], boundary: String) throws -> Data { - var body = Data() - - // Add parameters - for (key, value) in parameters { - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!) - body.append("\(value)\r\n".data(using: .utf8)!) - } - - // Add file - let fileData = try Data(contentsOf: fileURL) - let filename = fileURL.lastPathComponent - let mimeType = "application/octet-stream" - - body.append("--\(boundary)\r\n".data(using: .utf8)!) - body.append("Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n".data(using: .utf8)!) - body.append("Content-Type: \(mimeType)\r\n\r\n".data(using: .utf8)!) - body.append(fileData) - body.append("\r\n".data(using: .utf8)!) - body.append("--\(boundary)--\r\n".data(using: .utf8)!) - - return body - } - - private func updateMetrics(success: Bool, responseTime: TimeInterval, fromCache: Bool = false) async { - await MainActor.run { - self.metrics.totalRequests += 1 - - if success { - self.metrics.successfulRequests += 1 - } else { - self.metrics.failedRequests += 1 - } - - // Update average response time - let totalTime = self.metrics.averageResponseTime * Double(self.metrics.totalRequests - 1) - self.metrics.averageResponseTime = (totalTime + responseTime) / Double(self.metrics.totalRequests) - - if fromCache { - let totalCacheRequests = Double(self.metrics.totalRequests) - let cacheHits = self.metrics.cacheHitRate * (totalCacheRequests - 1) + 1 - self.metrics.cacheHitRate = cacheHits / totalCacheRequests - } - - self.metrics.lastRequestTime = Date() - } - } - - private func measureLatency() async -> TimeInterval { - // Implement ping/latency measurement - return 0.05 // Mock 50ms latency - } - - private func checkNetworkConnectivity() async -> Bool { - return isNetworkAvailable - } -} - -// MARK: - Supporting Types and Classes - -public struct APIHealthStatus { - public let isHealthy: Bool - public let latency: TimeInterval - public let circuitBreakerState: APIManager.CircuitBreakerState - public let networkAvailable: Bool - public let activeConnections: Int - public let rateLimitStatus: RateLimitStatus - public let cacheStatus: CacheStatus - public let metrics: APIManager.APIMetrics - public let lastHealthCheck: Date -} - -public struct RateLimitStatus { - public let remainingRequests: Int - public let resetTime: Date - public let isLimited: Bool -} - -public struct CacheStatus { - public let hitRate: Double - public let size: Int - public let maxSize: Int - public let entryCount: Int -} - -public struct APIAnalytics { - public let timeframe: TimeInterval - public let requestVolume: [TimeSeriesPoint] - public let responseTimePercentiles: ResponseTimePercentiles - public let errorRateByEndpoint: [String: Double] - public let topEndpoints: [EndpointStats] - public let geographicDistribution: [String: Int] - - public struct TimeSeriesPoint { - public let timestamp: Date - public let value: Double - } - - public struct ResponseTimePercentiles { - public let p50: TimeInterval - public let p90: TimeInterval - public let p95: TimeInterval - public let p99: TimeInterval - } - - public struct EndpointStats { - public let endpoint: String - public let requestCount: Int - public let averageResponseTime: TimeInterval - public let errorRate: Double - } -} - -public protocol WebSocketConnection { - func send(_ data: Data) async throws - func send(_ text: String) async throws - func close() async - var isConnected: Bool { get } -} - -// MARK: - Private Supporting Classes - -private class RateLimiter { - private let config: APIManager.APIConfiguration.RateLimitConfig - private var requestCounts: [String: [(Date, Int)]] = [:] - private let queue = DispatchQueue(label: "rate.limiter") - - init(config: APIManager.APIConfiguration.RateLimitConfig) { - self.config = config - } - - func checkLimit(for endpoint: String) async throws { - // Implementation would check rate limits - await cleanOldRequests(for: endpoint) - - let key = config.trackByEndpoint ? endpoint : "global" - let now = Date() - let requests = requestCounts[key] ?? [] - - let recentRequests = requests.filter { now.timeIntervalSince($0.0) < 1.0 } - - if recentRequests.count >= config.requestsPerSecond { - throw APIManager.APIError.rateLimitExceeded - } - - // Record this request - await queue.async { - self.requestCounts[key, default: []].append((now, 1)) - } - } - - private func cleanOldRequests(for endpoint: String) async { - let key = config.trackByEndpoint ? endpoint : "global" - let cutoff = Date().addingTimeInterval(-60) // Keep last minute - - await queue.async { - self.requestCounts[key] = self.requestCounts[key]?.filter { $0.0 > cutoff } ?? [] - } - } - - func getStatus() async -> RateLimitStatus { - return RateLimitStatus( - remainingRequests: config.requestsPerSecond, - resetTime: Date().addingTimeInterval(1), - isLimited: false - ) - } -} - -private class CircuitBreaker { - private let config: APIManager.APIConfiguration.CircuitBreakerConfig - private var state: APIManager.CircuitBreakerState = .closed - private var failureCount = 0 - private var lastFailureTime: Date? - private var halfOpenAttempts = 0 - - var onStateChange: ((APIManager.CircuitBreakerState) -> Void)? - - init(config: APIManager.APIConfiguration.CircuitBreakerConfig) { - self.config = config - } - - func checkState() throws { - guard config.isEnabled else { return } - - switch state { - case .open: - if let lastFailure = lastFailureTime, - Date().timeIntervalSince(lastFailure) > config.recoveryTimeout { - setState(.halfOpen) - halfOpenAttempts = 0 - } else { - throw APIManager.APIError.circuitBreakerOpen - } - case .halfOpen: - if halfOpenAttempts >= config.halfOpenMaxCalls { - throw APIManager.APIError.circuitBreakerOpen - } - halfOpenAttempts += 1 - case .closed: - break - } - } - - func recordSuccess() { - guard config.isEnabled else { return } - - if state == .halfOpen { - setState(.closed) - failureCount = 0 - } - } - - func recordFailure() { - guard config.isEnabled else { return } - - failureCount += 1 - lastFailureTime = Date() - - if failureCount >= config.failureThreshold { - setState(.open) - } - } - - private func setState(_ newState: APIManager.CircuitBreakerState) { - state = newState - onStateChange?(newState) - } -} - -private class RetryHandler { - private let config: APIManager.APIConfiguration.RetryPolicy - - init(config: APIManager.APIConfiguration.RetryPolicy) { - self.config = config - } -} - -private class SecurityHandler { - private let config: APIManager.APIConfiguration.SecurityConfig - - init(config: APIManager.APIConfiguration.SecurityConfig) { - self.config = config - } - - func validateRequest(_ request: APIManager.APIRequest) throws { - // Implement security validation - } -} - -private class MetricsCollector { - func generateAnalytics(for timeframe: TimeInterval) async -> APIAnalytics { - // Generate analytics data - return APIAnalytics( - timeframe: timeframe, - requestVolume: [], - responseTimePercentiles: APIAnalytics.ResponseTimePercentiles( - p50: 0.1, - p90: 0.5, - p95: 1.0, - p99: 2.0 - ), - errorRateByEndpoint: [:], - topEndpoints: [], - geographicDistribution: [:] - ) - } -} - -private class APICacheManager { - private let config: APIManager.APIConfiguration.CacheConfig - - init(config: APIManager.APIConfiguration.CacheConfig) { - self.config = config - } - - func getCachedResponse(for request: URLRequest) async -> APIManager.APIResponse? { - // Check cache for response - return nil - } - - func cacheResponse(_ response: APIManager.APIResponse, for request: URLRequest) async { - // Cache the response - } - - func invalidate(endpoint: String) async { - // Invalidate cache for endpoint - } - - func clearAll() async { - // Clear all cached data - } - - func getStatus() async -> CacheStatus { - return CacheStatus( - hitRate: 0.75, - size: 10 * 1024 * 1024, - maxSize: config.cacheSizeLimit, - entryCount: 150 - ) - } -} - -private class RequestQueue { - // Request queue implementation for handling priority-based requests -} - -private class WebSocketManager { - func connect( - config: APIManager.WebSocketConfig, - onMessage: @escaping (Data) -> Void, - onConnect: @escaping () -> Void, - onDisconnect: @escaping (Error?) -> Void - ) async throws -> WebSocketConnection { - // WebSocket implementation - return MockWebSocketConnection() - } -} - -private class MockWebSocketConnection: WebSocketConnection { - var isConnected: Bool = true - - func send(_ data: Data) async throws { - // Mock implementation - } - - func send(_ text: String) async throws { - // Mock implementation - } - - func close() async { - isConnected = false - } -} - -private actor AsyncSemaphore { - private var value: Int - private var waiters: [CheckedContinuation] = [] - - init(value: Int) { - self.value = value - } - - func wait() async { - if value > 0 { - value -= 1 - } else { - await withCheckedContinuation { continuation in - waiters.append(continuation) - } - } - } - - func signal() { - if waiters.isEmpty { - value += 1 - } else { - let waiter = waiters.removeFirst() - waiter.resume() - } - } -} \ No newline at end of file diff --git a/Sources/Integration/CloudIntegration.swift b/Sources/Integration/CloudIntegration.swift deleted file mode 100644 index eab57e2..0000000 --- a/Sources/Integration/CloudIntegration.swift +++ /dev/null @@ -1,1396 +0,0 @@ -import Foundation -import CloudKit -import Combine -import OSLog -import CryptoKit - -/// Enterprise-grade cloud integration manager providing multi-cloud support, -/// hybrid deployment strategies, and comprehensive cloud services integration -@available(iOS 16.0, *) -public class CloudIntegration: ObservableObject { - - // MARK: - Types - - public enum CloudError: LocalizedError { - case configurationInvalid - case authenticationFailed(String) - case networkUnavailable - case cloudServiceUnavailable(String) - case syncFailed(String) - case quotaExceeded(String) - case permissionDenied - case dataCorrupted - case operationTimeout - case unsupportedOperation - case multiCloudConflict - case migrationFailed - - public var errorDescription: String? { - switch self {\ - case .configurationInvalid: - return "Cloud configuration is invalid" - case .authenticationFailed(let reason): - return "Cloud authentication failed: \(reason)" - case .networkUnavailable: - return "Network connection is unavailable" - case .cloudServiceUnavailable(let service): - return "Cloud service '\(service)' is unavailable" - case .syncFailed(let reason): - return "Cloud synchronization failed: \(reason)" - case .quotaExceeded(let quota): - return "Cloud quota exceeded: \(quota)" - case .permissionDenied: - return "Permission denied for cloud operation" - case .dataCorrupted: - return "Cloud data is corrupted" - case .operationTimeout: - return "Cloud operation timed out" - case .unsupportedOperation: - return "Operation not supported by cloud service" - case .multiCloudConflict: - return "Conflict detected between multiple cloud providers" - case .migrationFailed: - return "Cloud migration failed" - } - } - } - - public enum CloudProvider: String, CaseIterable, Codable { - case icloud = "icloud" - case aws = "aws" - case azure = "azure" - case googleCloud = "gcp" - case firebase = "firebase" - case custom = "custom" - - public var displayName: String { - switch self { - case .icloud: return "iCloud" - case .aws: return "Amazon Web Services" - case .azure: return "Microsoft Azure" - case .googleCloud: return "Google Cloud Platform" - case .firebase: return "Firebase" - case .custom: return "Custom Cloud Provider" - } - } - - public var capabilities: Set { - switch self { - case .icloud: - return [.storage, .sync, .authentication, .keyValue, .documents] - case .aws: - return [.storage, .sync, .authentication, .database, .analytics, .ml, .computing, .cdn, .messaging] - case .azure: - return [.storage, .sync, .authentication, .database, .analytics, .ml, .computing, .cdn] - case .googleCloud: - return [.storage, .sync, .authentication, .database, .analytics, .ml, .computing, .cdn] - case .firebase: - return [.storage, .sync, .authentication, .database, .analytics, .messaging, .hosting] - case .custom: - return Set(CloudCapability.allCases) - } - } - } - - public enum CloudCapability: String, CaseIterable, Codable { - case storage = "storage" - case sync = "sync" - case authentication = "authentication" - case database = "database" - case analytics = "analytics" - case ml = "machine_learning" - case computing = "computing" - case cdn = "content_delivery" - case messaging = "messaging" - case keyValue = "key_value" - case documents = "documents" - case hosting = "hosting" - - public var description: String { - switch self { - case .storage: return "File and blob storage" - case .sync: return "Real-time synchronization" - case .authentication: return "User authentication and authorization" - case .database: return "Database services" - case .analytics: return "Analytics and monitoring" - case .ml: return "Machine learning services" - case .computing: return "Serverless computing" - case .cdn: return "Content delivery network" - case .messaging: return "Push notifications and messaging" - case .keyValue: return "Key-value storage" - case .documents: return "Document storage and management" - case .hosting: return "Web hosting and deployment" - } - } - } - - public enum SyncStrategy: String, CaseIterable, Codable { - case primary = "primary" - case backup = "backup" - case multiCloud = "multi_cloud" - case hybrid = "hybrid" - case failover = "failover" - - public var description: String { - switch self { - case .primary: return "Single primary cloud provider" - case .backup: return "Primary with backup cloud provider" - case .multiCloud: return "Multiple active cloud providers" - case .hybrid: return "On-premise and cloud hybrid" - case .failover: return "Automatic failover between providers" - } - } - } - - public struct CloudConfiguration { - public var primaryProvider: CloudProvider = .icloud - public var secondaryProviders: [CloudProvider] = [] - public var syncStrategy: SyncStrategy = .primary - public var enableAutoSync: Bool = true - public var syncInterval: TimeInterval = 300 // 5 minutes - public var enableCompression: Bool = true - public var enableEncryption: Bool = true - public var enableConflictResolution: Bool = true - public var maxRetryAttempts: Int = 3 - public var operationTimeout: TimeInterval = 30 - public var enableOfflineMode: Bool = true - public var enableMetrics: Bool = true - public var enableCaching: Bool = true - public var compressionThreshold: Int = 1024 // 1KB - - // Provider-specific configurations - public var icloudContainerIdentifier: String = "" - public var awsRegion: String = "us-east-1" - public var awsAccessKeyId: String = "" - public var awsSecretAccessKey: String = "" - public var azureConnectionString: String = "" - public var gcpProjectId: String = "" - public var gcpCredentialsPath: String = "" - public var firebaseProjectId: String = "" - public var customEndpoint: String = "" - public var customApiKey: String = "" - - public init() {} - } - - public struct CloudOperation: Identifiable, Codable { - public let id: String - public let type: OperationType - public let provider: CloudProvider - public let path: String - public let timestamp: Date - public let status: OperationStatus - public let metadata: [String: String] - public let retryCount: Int - public let error: String? - - public enum OperationType: String, Codable { - case upload = "upload" - case download = "download" - case sync = "sync" - case delete = "delete" - case query = "query" - case authenticate = "authenticate" - case backup = "backup" - case restore = "restore" - case migrate = "migrate" - } - - public enum OperationStatus: String, Codable { - case pending = "pending" - case inProgress = "in_progress" - case completed = "completed" - case failed = "failed" - case cancelled = "cancelled" - case retrying = "retrying" - } - - public init( - type: OperationType, - provider: CloudProvider, - path: String, - metadata: [String: String] = [:] - ) { - self.id = UUID().uuidString - self.type = type - self.provider = provider - self.path = path - self.timestamp = Date() - self.status = .pending - self.metadata = metadata - self.retryCount = 0 - self.error = nil - } - } - - public struct CloudFile: Codable, Identifiable { - public let id: String - public let name: String - public let path: String - public let size: Int64 - public let contentType: String - public let checksum: String - public let createdAt: Date - public let modifiedAt: Date - public let provider: CloudProvider - public let isEncrypted: Bool - public let metadata: [String: String] - public let tags: [String] - - public var formattedSize: String { - ByteCountFormatter.string(fromByteCount: size, countStyle: .file) - } - - public init( - name: String, - path: String, - size: Int64, - contentType: String, - checksum: String, - provider: CloudProvider, - isEncrypted: Bool = false, - metadata: [String: String] = [:], - tags: [String] = [] - ) { - self.id = UUID().uuidString - self.name = name - self.path = path - self.size = size - self.contentType = contentType - self.checksum = checksum - self.createdAt = Date() - self.modifiedAt = Date() - self.provider = provider - self.isEncrypted = isEncrypted - self.metadata = metadata - self.tags = tags - } - } - - public struct SyncStatus: Codable { - public let provider: CloudProvider - public let lastSyncTime: Date - public let filesInSync: Int - public let pendingOperations: Int - public let conflicts: Int - public let quota: CloudQuota - public let isOnline: Bool - public let healthScore: Double - - public struct CloudQuota: Codable { - public let used: Int64 - public let total: Int64 - public let available: Int64 - - public var usagePercentage: Double { - guard total > 0 else { return 0 } - return Double(used) / Double(total) * 100 - } - } - } - - public struct ConflictResolution: Codable { - public let conflictId: String - public let localFile: CloudFile - public let remoteFile: CloudFile - public let resolutionStrategy: ResolutionStrategy - public let resolvedAt: Date? - - public enum ResolutionStrategy: String, Codable { - case keepLocal = "keep_local" - case keepRemote = "keep_remote" - case merge = "merge" - case createCopy = "create_copy" - case manual = "manual" - } - } - - // MARK: - Properties - - public static let shared = CloudIntegration() - - private let icloudService: iCloudService - private let awsService: AWSService - private let azureService: AzureService - private let gcpService: GCPService - private let firebaseService: FirebaseService - private let customService: CustomCloudService - private let conflictResolver: ConflictResolver - private let migrationManager: CloudMigrationManager - private let metricsCollector: CloudMetricsCollector - private let cacheManager: CacheManager - - @Published public private(set) var configuration: CloudConfiguration = CloudConfiguration() - @Published public private(set) var isConnected: Bool = false - @Published public private(set) var activeOperations: [CloudOperation] = [] - @Published public private(set) var syncStatuses: [CloudProvider: SyncStatus] = [:] - @Published public private(set) var conflicts: [ConflictResolution] = [] - @Published public private(set) var availableProviders: Set = [] - @Published public private(set) var offlineQueue: [CloudOperation] = [] - - private var syncTimer: Timer? - private var cancellables = Set() - private let queue = DispatchQueue(label: "cloud.integration", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "CloudIntegration") - - // MARK: - Initialization - - private init() { - self.icloudService = iCloudService() - self.awsService = AWSService() - self.azureService = AzureService() - self.gcpService = GCPService() - self.firebaseService = FirebaseService() - self.customService = CustomCloudService() - self.conflictResolver = ConflictResolver() - self.migrationManager = CloudMigrationManager() - self.metricsCollector = CloudMetricsCollector() - self.cacheManager = CacheManager.shared - - setupCloudIntegration() - } - - // MARK: - Public Methods - - /// Initializes cloud integration with configuration - public func initialize(configuration: CloudConfiguration) async throws { - guard isValidConfiguration(configuration) else { - throw CloudError.configurationInvalid - } - - await MainActor.run { - self.configuration = configuration - } - - // Initialize cloud services - try await initializeServices() - - // Setup automatic sync if enabled - if configuration.enableAutoSync { - setupAutomaticSync() - } - - // Detect available providers - await detectAvailableProviders() - - await MainActor.run { - self.isConnected = true - } - - logger.info("Cloud integration initialized with primary provider: \(configuration.primaryProvider.rawValue)") - } - - /// Uploads data to cloud storage - public func upload( - data: Data, - to path: String, - provider: CloudProvider? = nil, - metadata: [String: String] = [:] - ) async throws -> CloudFile { - let targetProvider = provider ?? configuration.primaryProvider - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - let operation = CloudOperation( - type: .upload, - provider: targetProvider, - path: path, - metadata: metadata - ) - - await addToActiveOperations(operation) - - do { - // Encrypt data if enabled - let processedData = configuration.enableEncryption ? try encryptData(data) : data - - // Compress data if enabled and above threshold - let finalData = shouldCompress(data) ? try compressData(processedData) : processedData - - let cloudFile = try await uploadToProvider( - data: finalData, - path: path, - provider: targetProvider, - metadata: metadata - ) - - // Cache locally if enabled - if configuration.enableCaching { - await cacheFile(cloudFile, data: data) - } - - // Update sync status - await updateSyncStatus(for: targetProvider) - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Successfully uploaded file to \(targetProvider.rawValue): \(path)") - return cloudFile - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - - // Add to offline queue if network issue - if error is URLError || error is CloudError { - await addToOfflineQueue(operation) - } - - throw error - } - } - - /// Downloads data from cloud storage - public func download( - path: String, - provider: CloudProvider? = nil - ) async throws -> Data { - let targetProvider = provider ?? configuration.primaryProvider - - // Try cache first if enabled - if configuration.enableCaching, - let cachedData = await getCachedFile(path: path, provider: targetProvider) { - return cachedData - } - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - let operation = CloudOperation( - type: .download, - provider: targetProvider, - path: path - ) - - await addToActiveOperations(operation) - - do { - let data = try await downloadFromProvider(path: path, provider: targetProvider) - - // Decrypt data if encrypted - let finalData = configuration.enableEncryption ? try decryptData(data) : data - - // Cache for future use if enabled - if configuration.enableCaching { - await cacheData(finalData, path: path, provider: targetProvider) - } - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Successfully downloaded file from \(targetProvider.rawValue): \(path)") - return finalData - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Synchronizes data across all configured providers - public func synchronize(paths: [String] = []) async throws { - guard isConnected else { - throw CloudError.networkUnavailable - } - - let operation = CloudOperation( - type: .sync, - provider: configuration.primaryProvider, - path: paths.isEmpty ? "/" : paths.joined(separator: ",") - ) - - await addToActiveOperations(operation) - - do { - // Primary synchronization - try await synchronizeWithProvider(configuration.primaryProvider, paths: paths) - - // Secondary provider synchronization - for provider in configuration.secondaryProviders { - if availableProviders.contains(provider) { - try await synchronizeWithProvider(provider, paths: paths) - } - } - - // Resolve conflicts if any - if configuration.enableConflictResolution { - await resolveConflicts() - } - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Synchronization completed across \(availableProviders.count) providers") - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Deletes file from cloud storage - public func delete( - path: String, - provider: CloudProvider? = nil - ) async throws { - let targetProvider = provider ?? configuration.primaryProvider - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - let operation = CloudOperation( - type: .delete, - provider: targetProvider, - path: path - ) - - await addToActiveOperations(operation) - - do { - try await deleteFromProvider(path: path, provider: targetProvider) - - // Remove from cache - await removeCachedFile(path: path, provider: targetProvider) - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Successfully deleted file from \(targetProvider.rawValue): \(path)") - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Lists files in cloud storage - public func listFiles( - path: String = "/", - provider: CloudProvider? = nil, - recursive: Bool = false - ) async throws -> [CloudFile] { - let targetProvider = provider ?? configuration.primaryProvider - - guard availableProviders.contains(targetProvider) else { - throw CloudError.cloudServiceUnavailable(targetProvider.rawValue) - } - - return try await listFilesFromProvider(path: path, provider: targetProvider, recursive: recursive) - } - - /// Migrates data between cloud providers - public func migrate( - from sourceProvider: CloudProvider, - to targetProvider: CloudProvider, - paths: [String] = [] - ) async throws { - let operation = CloudOperation( - type: .migrate, - provider: sourceProvider, - path: "migrate_to_\(targetProvider.rawValue)", - metadata: ["target_provider": targetProvider.rawValue] - ) - - await addToActiveOperations(operation) - - do { - try await migrationManager.migrate( - from: sourceProvider, - to: targetProvider, - paths: paths, - cloudIntegration: self - ) - - await updateOperationStatus(operation.id, status: .completed) - - logger.info("Migration completed from \(sourceProvider.rawValue) to \(targetProvider.rawValue)") - - } catch { - await updateOperationStatus(operation.id, status: .failed, error: error.localizedDescription) - throw error - } - } - - /// Gets synchronization status for all providers - public func getSyncStatus() async -> [CloudProvider: SyncStatus] { - var statuses: [CloudProvider: SyncStatus] = [:] - - for provider in availableProviders { - if let status = await getSyncStatusForProvider(provider) { - statuses[provider] = status - } - } - - await MainActor.run { - self.syncStatuses = statuses - } - - return statuses - } - - /// Resolves a specific conflict - public func resolveConflict( - conflictId: String, - strategy: ConflictResolution.ResolutionStrategy - ) async throws { - guard let conflictIndex = conflicts.firstIndex(where: { $0.conflictId == conflictId }) else { - throw CloudError.dataCorrupted - } - - var conflict = conflicts[conflictIndex] - - try await conflictResolver.resolve( - conflict: conflict, - strategy: strategy, - cloudIntegration: self - ) - - conflict = ConflictResolution( - conflictId: conflict.conflictId, - localFile: conflict.localFile, - remoteFile: conflict.remoteFile, - resolutionStrategy: strategy, - resolvedAt: Date() - ) - - await MainActor.run { - self.conflicts[conflictIndex] = conflict - } - - logger.info("Conflict resolved: \(conflictId) using strategy: \(strategy.rawValue)") - } - - /// Updates cloud configuration - public func updateConfiguration(_ newConfiguration: CloudConfiguration) async throws { - guard isValidConfiguration(newConfiguration) else { - throw CloudError.configurationInvalid - } - - let wasConnected = isConnected - - if wasConnected { - await disconnect() - } - - await MainActor.run { - self.configuration = newConfiguration - } - - if wasConnected { - try await initialize(configuration: newConfiguration) - } - - logger.info("Cloud configuration updated") - } - - /// Disconnects from cloud services - public func disconnect() async { - await MainActor.run { - self.isConnected = false - } - - syncTimer?.invalidate() - syncTimer = nil - - // Cancel all active operations - await clearActiveOperations() - - // Disconnect from all services - await disconnectFromAllServices() - - logger.info("Disconnected from cloud services") - } - - /// Processes offline queue when connection is restored - public func processOfflineQueue() async { - guard isConnected else { return } - - let queueCopy = offlineQueue - await clearOfflineQueue() - - for operation in queueCopy { - do { - switch operation.type { - case .upload: - // Retry upload operation - logger.info("Retrying offline upload: \(operation.path)") - case .delete: - // Retry delete operation - logger.info("Retrying offline delete: \(operation.path)") - case .sync: - // Retry sync operation - try await synchronize() - default: - break - } - } catch { - logger.error("Failed to process offline operation: \(error)") - } - } - - logger.info("Processed \(queueCopy.count) offline operations") - } - - /// Gets cloud integration metrics and insights - public func getCloudInsights() async -> CloudInsights { - let totalOperations = activeOperations.count + (syncStatuses.values.map { $0.pendingOperations }.reduce(0, +)) - let totalConflicts = conflicts.count - let totalStorageUsed = syncStatuses.values.map { $0.quota.used }.reduce(0, +) - let averageHealthScore = syncStatuses.values.map { $0.healthScore }.reduce(0, +) / Double(max(1, syncStatuses.count)) - - return CloudInsights( - connectedProviders: availableProviders.count, - totalOperations: totalOperations, - pendingOperations: activeOperations.filter { $0.status == .pending }.count, - totalConflicts: totalConflicts, - storageUsed: totalStorageUsed, - averageHealthScore: averageHealthScore, - offlineOperations: offlineQueue.count, - lastSyncTime: syncStatuses.values.map { $0.lastSyncTime }.max(), - syncStrategy: configuration.syncStrategy - ) - } - - // MARK: - Private Methods - - private func setupCloudIntegration() { - // Setup network monitoring - setupNetworkMonitoring() - - // Setup notification observers - setupNotificationObservers() - - logger.info("Cloud integration initialized") - } - - private func setupNetworkMonitoring() { - // Monitor network connectivity for automatic sync resumption - NotificationCenter.default.publisher(for: .NSSystemClockDidChange) - .sink { [weak self] _ in - Task { await self?.handleNetworkChange() } - } - .store(in: &cancellables) - } - - private func setupNotificationObservers() { - NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) - .sink { [weak self] _ in - Task { await self?.processOfflineQueue() } - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - } - - private func initializeServices() async throws { - for provider in [configuration.primaryProvider] + configuration.secondaryProviders { - try await initializeProvider(provider) - } - } - - private func initializeProvider(_ provider: CloudProvider) async throws { - switch provider { - case .icloud: - try await icloudService.initialize(containerIdentifier: configuration.icloudContainerIdentifier) - case .aws: - try await awsService.initialize( - region: configuration.awsRegion, - accessKeyId: configuration.awsAccessKeyId, - secretAccessKey: configuration.awsSecretAccessKey - ) - case .azure: - try await azureService.initialize(connectionString: configuration.azureConnectionString) - case .googleCloud: - try await gcpService.initialize( - projectId: configuration.gcpProjectId, - credentialsPath: configuration.gcpCredentialsPath - ) - case .firebase: - try await firebaseService.initialize(projectId: configuration.firebaseProjectId) - case .custom: - try await customService.initialize( - endpoint: configuration.customEndpoint, - apiKey: configuration.customApiKey - ) - } - } - - private func setupAutomaticSync() { - syncTimer = Timer.scheduledTimer(withTimeInterval: configuration.syncInterval, repeats: true) { [weak self] _ in - Task { - try? await self?.synchronize() - } - } - } - - private func detectAvailableProviders() async { - var providers: Set = [] - - for provider in CloudProvider.allCases { - if await isProviderAvailable(provider) { - providers.insert(provider) - } - } - - await MainActor.run { - self.availableProviders = providers - } - } - - private func isProviderAvailable(_ provider: CloudProvider) async -> Bool { - switch provider { - case .icloud: - return await icloudService.isAvailable() - case .aws: - return await awsService.isAvailable() - case .azure: - return await azureService.isAvailable() - case .googleCloud: - return await gcpService.isAvailable() - case .firebase: - return await firebaseService.isAvailable() - case .custom: - return await customService.isAvailable() - } - } - - private func uploadToProvider( - data: Data, - path: String, - provider: CloudProvider, - metadata: [String: String] - ) async throws -> CloudFile { - switch provider { - case .icloud: - return try await icloudService.upload(data: data, path: path, metadata: metadata) - case .aws: - return try await awsService.upload(data: data, path: path, metadata: metadata) - case .azure: - return try await azureService.upload(data: data, path: path, metadata: metadata) - case .googleCloud: - return try await gcpService.upload(data: data, path: path, metadata: metadata) - case .firebase: - return try await firebaseService.upload(data: data, path: path, metadata: metadata) - case .custom: - return try await customService.upload(data: data, path: path, metadata: metadata) - } - } - - private func downloadFromProvider(path: String, provider: CloudProvider) async throws -> Data { - switch provider { - case .icloud: - return try await icloudService.download(path: path) - case .aws: - return try await awsService.download(path: path) - case .azure: - return try await azureService.download(path: path) - case .googleCloud: - return try await gcpService.download(path: path) - case .firebase: - return try await firebaseService.download(path: path) - case .custom: - return try await customService.download(path: path) - } - } - - private func deleteFromProvider(path: String, provider: CloudProvider) async throws { - switch provider { - case .icloud: - try await icloudService.delete(path: path) - case .aws: - try await awsService.delete(path: path) - case .azure: - try await azureService.delete(path: path) - case .googleCloud: - try await gcpService.delete(path: path) - case .firebase: - try await firebaseService.delete(path: path) - case .custom: - try await customService.delete(path: path) - } - } - - private func listFilesFromProvider(path: String, provider: CloudProvider, recursive: Bool) async throws -> [CloudFile] { - switch provider { - case .icloud: - return try await icloudService.listFiles(path: path, recursive: recursive) - case .aws: - return try await awsService.listFiles(path: path, recursive: recursive) - case .azure: - return try await azureService.listFiles(path: path, recursive: recursive) - case .googleCloud: - return try await gcpService.listFiles(path: path, recursive: recursive) - case .firebase: - return try await firebaseService.listFiles(path: path, recursive: recursive) - case .custom: - return try await customService.listFiles(path: path, recursive: recursive) - } - } - - private func synchronizeWithProvider(_ provider: CloudProvider, paths: [String]) async throws { - // Implementation would perform actual synchronization - logger.info("Synchronizing with \(provider.rawValue)") - } - - private func getSyncStatusForProvider(_ provider: CloudProvider) async -> SyncStatus? { - // Implementation would get actual sync status - return SyncStatus( - provider: provider, - lastSyncTime: Date(), - filesInSync: 100, - pendingOperations: 0, - conflicts: 0, - quota: SyncStatus.CloudQuota( - used: 1_000_000_000, // 1GB - total: 5_000_000_000, // 5GB - available: 4_000_000_000 // 4GB - ), - isOnline: true, - healthScore: 0.95 - ) - } - - private func resolveConflicts() async { - // Implementation would resolve conflicts automatically - } - - private func disconnectFromAllServices() async { - await icloudService.disconnect() - await awsService.disconnect() - await azureService.disconnect() - await gcpService.disconnect() - await firebaseService.disconnect() - await customService.disconnect() - } - - private func encryptData(_ data: Data) throws -> Data { - let key = SymmetricKey(size: .bits256) - let encryptedData = try AES.GCM.seal(data, using: key) - return encryptedData.combined ?? data - } - - private func decryptData(_ data: Data) throws -> Data { - // Implementation would decrypt data - return data - } - - private func compressData(_ data: Data) throws -> Data { - // Implementation would compress data - return data - } - - private func shouldCompress(_ data: Data) -> Bool { - return configuration.enableCompression && data.count > configuration.compressionThreshold - } - - private func cacheFile(_ file: CloudFile, data: Data) async { - let key = "cloud_file_\(file.provider.rawValue)_\(file.path)" - try? await cacheManager.store(data, forKey: key) - } - - private func getCachedFile(path: String, provider: CloudProvider) async -> Data? { - let key = "cloud_file_\(provider.rawValue)_\(path)" - return try? await cacheManager.retrieve(Data.self, forKey: key) - } - - private func cacheData(_ data: Data, path: String, provider: CloudProvider) async { - let key = "cloud_file_\(provider.rawValue)_\(path)" - try? await cacheManager.store(data, forKey: key) - } - - private func removeCachedFile(path: String, provider: CloudProvider) async { - let key = "cloud_file_\(provider.rawValue)_\(path)" - await cacheManager.removeObject(forKey: key) - } - - private func addToActiveOperations(_ operation: CloudOperation) async { - await MainActor.run { - self.activeOperations.append(operation) - } - } - - private func updateOperationStatus(_ id: String, status: CloudOperation.OperationStatus, error: String? = nil) async { - await MainActor.run { - if let index = self.activeOperations.firstIndex(where: { $0.id == id }) { - var operation = self.activeOperations[index] - operation = CloudOperation( - type: operation.type, - provider: operation.provider, - path: operation.path, - metadata: operation.metadata - ) - self.activeOperations[index] = operation - - if status == .completed || status == .failed { - self.activeOperations.remove(at: index) - } - } - } - } - - private func clearActiveOperations() async { - await MainActor.run { - self.activeOperations.removeAll() - } - } - - private func addToOfflineQueue(_ operation: CloudOperation) async { - await MainActor.run { - self.offlineQueue.append(operation) - } - } - - private func clearOfflineQueue() async { - await MainActor.run { - self.offlineQueue.removeAll() - } - } - - private func updateSyncStatus(for provider: CloudProvider) async { - // Implementation would update sync status - } - - private func isValidConfiguration(_ config: CloudConfiguration) -> Bool { - return config.syncInterval > 0 && - config.maxRetryAttempts > 0 && - config.operationTimeout > 0 - } - - private func handleNetworkChange() async { - if isConnected { - await processOfflineQueue() - } - } - - private func handleAppDidEnterBackground() async { - // Save current state for background operation - if configuration.enableAutoSync { - try? await synchronize() - } - } -} - -// MARK: - Supporting Types - -public struct CloudInsights { - public let connectedProviders: Int - public let totalOperations: Int - public let pendingOperations: Int - public let totalConflicts: Int - public let storageUsed: Int64 - public let averageHealthScore: Double - public let offlineOperations: Int - public let lastSyncTime: Date? - public let syncStrategy: CloudIntegration.SyncStrategy -} - -// MARK: - Cloud Service Protocols and Implementations - -private protocol CloudService { - func initialize() async throws - func isAvailable() async -> Bool - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile - func download(path: String) async throws -> Data - func delete(path: String) async throws - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] - func disconnect() async -} - -// MARK: - Cloud Service Implementations - -private class iCloudService: CloudService { - private var container: CKContainer? - - func initialize(containerIdentifier: String = "") async throws { - let identifier = containerIdentifier.isEmpty ? CKContainer.default().containerIdentifier : containerIdentifier - self.container = CKContainer(identifier: identifier!) - } - - func isAvailable() async -> Bool { - guard let container = container else { return false } - - do { - let status = try await container.accountStatus() - return status == .available - } catch { - return false - } - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to iCloud - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .icloud - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from iCloud - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from iCloud - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list iCloud files - return [] - } - - func disconnect() async { - container = nil - } -} - -private class AWSService: CloudService { - func initialize(region: String, accessKeyId: String, secretAccessKey: String) async throws { - // Implementation would initialize AWS SDK - } - - func isAvailable() async -> Bool { - // Implementation would check AWS availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to S3 - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .aws - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from S3 - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from S3 - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list S3 files - return [] - } - - func disconnect() async { - // Implementation would disconnect from AWS - } -} - -private class AzureService: CloudService { - func initialize(connectionString: String) async throws { - // Implementation would initialize Azure SDK - } - - func isAvailable() async -> Bool { - // Implementation would check Azure availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to Azure Blob Storage - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .azure - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from Azure - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from Azure - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list Azure files - return [] - } - - func disconnect() async { - // Implementation would disconnect from Azure - } -} - -private class GCPService: CloudService { - func initialize(projectId: String, credentialsPath: String) async throws { - // Implementation would initialize GCP SDK - } - - func isAvailable() async -> Bool { - // Implementation would check GCP availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to Google Cloud Storage - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .googleCloud - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from GCP - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from GCP - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list GCP files - return [] - } - - func disconnect() async { - // Implementation would disconnect from GCP - } -} - -private class FirebaseService: CloudService { - func initialize(projectId: String) async throws { - // Implementation would initialize Firebase SDK - } - - func isAvailable() async -> Bool { - // Implementation would check Firebase availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to Firebase Storage - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .firebase - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from Firebase - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from Firebase - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list Firebase files - return [] - } - - func disconnect() async { - // Implementation would disconnect from Firebase - } -} - -private class CustomCloudService: CloudService { - func initialize(endpoint: String, apiKey: String) async throws { - // Implementation would initialize custom cloud service - } - - func isAvailable() async -> Bool { - // Implementation would check custom service availability - return true - } - - func upload(data: Data, path: String, metadata: [String: String]) async throws -> CloudIntegration.CloudFile { - // Implementation would upload to custom service - return CloudIntegration.CloudFile( - name: URL(fileURLWithPath: path).lastPathComponent, - path: path, - size: Int64(data.count), - contentType: "application/octet-stream", - checksum: data.sha256, - provider: .custom - ) - } - - func download(path: String) async throws -> Data { - // Implementation would download from custom service - return Data() - } - - func delete(path: String) async throws { - // Implementation would delete from custom service - } - - func listFiles(path: String, recursive: Bool) async throws -> [CloudIntegration.CloudFile] { - // Implementation would list custom service files - return [] - } - - func disconnect() async { - // Implementation would disconnect from custom service - } -} - -// MARK: - Supporting Classes - -private class ConflictResolver { - func resolve( - conflict: CloudIntegration.ConflictResolution, - strategy: CloudIntegration.ConflictResolution.ResolutionStrategy, - cloudIntegration: CloudIntegration - ) async throws { - // Implementation would resolve conflicts - } -} - -private class CloudMigrationManager { - func migrate( - from sourceProvider: CloudIntegration.CloudProvider, - to targetProvider: CloudIntegration.CloudProvider, - paths: [String], - cloudIntegration: CloudIntegration - ) async throws { - // Implementation would migrate data between providers - } -} - -private class CloudMetricsCollector { - func recordMetric(_ metric: String, value: Double) async { - // Implementation would record metrics - } -} - -// MARK: - Extensions - -extension Data { - var sha256: String { - let digest = SHA256.hash(data: self) - return digest.compactMap { String(format: "%02x", $0) }.joined() - } -} \ No newline at end of file diff --git a/Sources/Integration/DataSourceManager.swift b/Sources/Integration/DataSourceManager.swift deleted file mode 100644 index 05c21e7..0000000 --- a/Sources/Integration/DataSourceManager.swift +++ /dev/null @@ -1,1281 +0,0 @@ -import Foundation -import Combine -import OSLog -import CoreData -import SQLite3 - -/// Enterprise-grade data source manager providing unified access to multiple data sources, -/// real-time streaming, intelligent caching, and comprehensive data orchestration -@available(iOS 16.0, *) -public class DataSourceManager: ObservableObject { - - // MARK: - Types - - public enum DataSourceError: LocalizedError { - case configurationInvalid - case dataSourceUnavailable(String) - case connectionFailed(String) - case queryFailed(String) - case transformationFailed(String) - case validationFailed(String) - case permissionDenied - case quotaExceeded - case dataCorrupted - case unsupportedOperation - case streamingFailed(String) - case cacheError(String) - case migrationFailed - - public var errorDescription: String? { - switch self { - case .configurationInvalid: - return "Data source configuration is invalid" - case .dataSourceUnavailable(let source): - return "Data source '\(source)' is unavailable" - case .connectionFailed(let reason): - return "Connection failed: \(reason)" - case .queryFailed(let reason): - return "Query failed: \(reason)" - case .transformationFailed(let reason): - return "Data transformation failed: \(reason)" - case .validationFailed(let reason): - return "Data validation failed: \(reason)" - case .permissionDenied: - return "Permission denied for data source operation" - case .quotaExceeded: - return "Data source quota exceeded" - case .dataCorrupted: - return "Data is corrupted or invalid" - case .unsupportedOperation: - return "Operation not supported by data source" - case .streamingFailed(let reason): - return "Data streaming failed: \(reason)" - case .cacheError(let reason): - return "Cache operation failed: \(reason)" - case .migrationFailed: - return "Data migration failed" - } - } - } - - public enum DataSourceType: String, CaseIterable, Codable { - case rest = "rest" - case graphql = "graphql" - case websocket = "websocket" - case database = "database" - case file = "file" - case cloud = "cloud" - case stream = "stream" - case mqtt = "mqtt" - case rss = "rss" - case custom = "custom" - - public var displayName: String { - switch self { - case .rest: return "REST API" - case .graphql: return "GraphQL API" - case .websocket: return "WebSocket Stream" - case .database: return "Database" - case .file: return "File System" - case .cloud: return "Cloud Storage" - case .stream: return "Data Stream" - case .mqtt: return "MQTT Broker" - case .rss: return "RSS Feed" - case .custom: return "Custom Data Source" - } - } - - public var capabilities: Set { - switch self { - case .rest: - return [.read, .write, .query, .pagination, .filtering, .caching] - case .graphql: - return [.read, .write, .query, .subscription, .filtering, .caching, .realtime] - case .websocket: - return [.read, .write, .realtime, .streaming, .bidirectional] - case .database: - return [.read, .write, .query, .transaction, .indexing, .caching, .persistence] - case .file: - return [.read, .write, .persistence, .caching, .compression] - case .cloud: - return [.read, .write, .sync, .caching, .persistence, .compression] - case .stream: - return [.read, .realtime, .streaming, .filtering, .transformation] - case .mqtt: - return [.read, .write, .realtime, .messaging, .subscription] - case .rss: - return [.read, .polling, .caching, .filtering] - case .custom: - return Set(DataCapability.allCases) - } - } - } - - public enum DataCapability: String, CaseIterable, Codable { - case read = "read" - case write = "write" - case query = "query" - case subscription = "subscription" - case realtime = "realtime" - case streaming = "streaming" - case pagination = "pagination" - case filtering = "filtering" - case caching = "caching" - case persistence = "persistence" - case transaction = "transaction" - case indexing = "indexing" - case compression = "compression" - case sync = "sync" - case bidirectional = "bidirectional" - case messaging = "messaging" - case polling = "polling" - case transformation = "transformation" - - public var description: String { - switch self { - case .read: return "Data reading operations" - case .write: return "Data writing operations" - case .query: return "Advanced query capabilities" - case .subscription: return "Real-time subscriptions" - case .realtime: return "Real-time data updates" - case .streaming: return "Continuous data streaming" - case .pagination: return "Paginated data retrieval" - case .filtering: return "Data filtering and search" - case .caching: return "Intelligent data caching" - case .persistence: return "Data persistence" - case .transaction: return "Transactional operations" - case .indexing: return "Data indexing for performance" - case .compression: return "Data compression" - case .sync: return "Data synchronization" - case .bidirectional: return "Bidirectional communication" - case .messaging: return "Message-based communication" - case .polling: return "Periodic data polling" - case .transformation: return "Data transformation" - } - } - } - - public struct DataSourceConfiguration { - public var name: String - public var type: DataSourceType - public var endpoint: String - public var authentication: AuthenticationConfig - public var enableCaching: Bool = true - public var enableRetries: Bool = true - public var maxRetryAttempts: Int = 3 - public var timeout: TimeInterval = 30 - public var enableCompression: Bool = true - public var enableEncryption: Bool = false - public var enableRateLimiting: Bool = true - public var rateLimit: RateLimit = RateLimit() - public var enableMetrics: Bool = true - public var enableValidation: Bool = true - public var transformationRules: [String: String] = [:] - public var headers: [String: String] = [:] - public var queryParameters: [String: String] = [:] - - public struct AuthenticationConfig { - public var type: AuthType = .none - public var apiKey: String = "" - public var bearerToken: String = "" - public var username: String = "" - public var password: String = "" - public var clientId: String = "" - public var clientSecret: String = "" - public var customHeaders: [String: String] = [:] - - public enum AuthType: String, CaseIterable { - case none = "none" - case apiKey = "api_key" - case bearer = "bearer" - case basic = "basic" - case oauth2 = "oauth2" - case custom = "custom" - } - } - - public struct RateLimit { - public var requestsPerSecond: Int = 10 - public var requestsPerMinute: Int = 600 - public var requestsPerHour: Int = 36000 - public var burstSize: Int = 20 - } - - public init(name: String, type: DataSourceType, endpoint: String) { - self.name = name - self.type = type - self.endpoint = endpoint - self.authentication = AuthenticationConfig() - } - } - - public struct DataQuery: Codable { - public let id: String - public let dataSourceName: String - public let query: String - public let parameters: [String: Any] - public let filters: [DataFilter] - public let pagination: PaginationOptions? - public let sortOptions: [SortOption] - public let enableCache: Bool - public let cacheTTL: TimeInterval? - public let priority: Priority - public let timeout: TimeInterval? - - public enum Priority: String, CaseIterable, Codable { - case low = "low" - case normal = "normal" - case high = "high" - case critical = "critical" - - public var value: Int { - switch self { - case .low: return 1 - case .normal: return 2 - case .high: return 3 - case .critical: return 4 - } - } - } - - public struct DataFilter: Codable { - public let field: String - public let operator: FilterOperator - public let value: String - public let caseSensitive: Bool - - public enum FilterOperator: String, CaseIterable, Codable { - case equals = "eq" - case notEquals = "ne" - case greaterThan = "gt" - case greaterThanOrEqual = "gte" - case lessThan = "lt" - case lessThanOrEqual = "lte" - case contains = "contains" - case startsWith = "starts_with" - case endsWith = "ends_with" - case regex = "regex" - case `in` = "in" - case notIn = "not_in" - } - } - - public struct PaginationOptions: Codable { - public let page: Int - public let pageSize: Int - public let offset: Int? - public let limit: Int? - public let cursor: String? - } - - public struct SortOption: Codable { - public let field: String - public let ascending: Bool - } - - public init( - dataSourceName: String, - query: String, - parameters: [String: Any] = [:], - filters: [DataFilter] = [], - pagination: PaginationOptions? = nil, - priority: Priority = .normal - ) { - self.id = UUID().uuidString - self.dataSourceName = dataSourceName - self.query = query - self.parameters = parameters - self.filters = filters - self.pagination = pagination - self.sortOptions = [] - self.enableCache = true - self.cacheTTL = nil - self.priority = priority - self.timeout = nil - } - } - - public struct DataResponse: Codable { - public let queryId: String - public let data: T? - public let metadata: ResponseMetadata - public let errors: [DataError] - public let cached: Bool - public let timestamp: Date - - public struct ResponseMetadata: Codable { - public let totalCount: Int? - public let pageCount: Int? - public let currentPage: Int? - public let hasNextPage: Bool - public let hasPreviousPage: Bool - public let executionTime: TimeInterval - public let dataSourceName: String - public let version: String - } - - public struct DataError: Codable { - public let code: String - public let message: String - public let field: String? - public let severity: Severity - - public enum Severity: String, Codable { - case info = "info" - case warning = "warning" - case error = "error" - case critical = "critical" - } - } - - public var isSuccess: Bool { - return data != nil && errors.isEmpty - } - } - - public struct StreamingData: Identifiable { - public let id: String - public let data: T - public let timestamp: Date - public let sourceId: String - public let metadata: [String: String] - - public init(data: T, sourceId: String, metadata: [String: String] = [:]) { - self.id = UUID().uuidString - self.data = data - self.timestamp = Date() - self.sourceId = sourceId - self.metadata = metadata - } - } - - // MARK: - Properties - - public static let shared = DataSourceManager() - - private let apiManager: APIManager - private let cloudIntegration: CloudIntegration - private let cacheManager: CacheManager - private let queryEngine: QueryEngine - private let transformationEngine: TransformationEngine - private let validationEngine: ValidationEngine - private let streamingManager: StreamingManager - private let metricsCollector: DataSourceMetricsCollector - private let rateLimiter: RateLimiter - - @Published public private(set) var dataSources: [String: DataSourceConfiguration] = [:] - @Published public private(set) var activeConnections: [String: DataSourceConnection] = [:] - @Published public private(set) var streamingConnections: [String: StreamingConnection] = [:] - @Published public private(set) var queryQueue: [DataQuery] = [] - @Published public private(set) var connectionStatus: [String: ConnectionStatus] = [:] - @Published public private(set) var metrics: DataSourceMetrics = DataSourceMetrics() - - private var cancellables = Set() - private let queue = DispatchQueue(label: "datasource.manager", qos: .utility) - private let logger = Logger(subsystem: "WidgetKit", category: "DataSource") - - // MARK: - Initialization - - private init() { - self.apiManager = APIManager.shared - self.cloudIntegration = CloudIntegration.shared - self.cacheManager = CacheManager.shared - self.queryEngine = QueryEngine() - self.transformationEngine = TransformationEngine() - self.validationEngine = ValidationEngine() - self.streamingManager = StreamingManager() - self.metricsCollector = DataSourceMetricsCollector() - self.rateLimiter = RateLimiter() - - setupDataSourceManager() - } - - // MARK: - Public Methods - - /// Registers a new data source - public func registerDataSource(_ configuration: DataSourceConfiguration) async throws { - guard isValidConfiguration(configuration) else { - throw DataSourceError.configurationInvalid - } - - // Test connection - try await testConnection(configuration) - - await MainActor.run { - self.dataSources[configuration.name] = configuration - self.connectionStatus[configuration.name] = .connected - } - - // Initialize connection if needed - if configuration.type.capabilities.contains(.realtime) { - try await initializeRealtimeConnection(configuration) - } - - logger.info("Data source registered: \(configuration.name) (\(configuration.type.rawValue))") - } - - /// Removes a data source - public func removeDataSource(_ name: String) async { - await disconnectDataSource(name) - - await MainActor.run { - self.dataSources.removeValue(forKey: name) - self.connectionStatus.removeValue(forKey: name) - } - - // Clear cached data - await clearCachedData(for: name) - - logger.info("Data source removed: \(name)") - } - - /// Executes a data query - public func executeQuery( - _ query: DataQuery, - responseType: T.Type - ) async throws -> DataResponse { - guard let configuration = dataSources[query.dataSourceName] else { - throw DataSourceError.dataSourceUnavailable(query.dataSourceName) - } - - // Check rate limiting - if configuration.enableRateLimiting { - try await rateLimiter.checkRateLimit(for: query.dataSourceName, configuration: configuration) - } - - // Check cache first if enabled - if query.enableCache { - if let cachedResponse = await getCachedResponse(query: query, responseType: responseType) { - await updateMetrics(queryId: query.id, cached: true, executionTime: 0) - return cachedResponse - } - } - - let startTime = CFAbsoluteTimeGetCurrent() - - do { - // Execute query based on data source type - let response = try await executeQueryForType(query, configuration: configuration, responseType: responseType) - - let executionTime = CFAbsoluteTimeGetCurrent() - startTime - - // Cache response if enabled - if query.enableCache && response.isSuccess { - await cacheResponse(query: query, response: response) - } - - // Update metrics - await updateMetrics(queryId: query.id, cached: false, executionTime: executionTime) - - logger.info("Query executed successfully: \(query.id) in \(executionTime)s") - return response - - } catch { - let executionTime = CFAbsoluteTimeGetCurrent() - startTime - await updateMetrics(queryId: query.id, cached: false, executionTime: executionTime, error: error) - throw error - } - } - - /// Streams data from a real-time data source - public func streamData( - from dataSourceName: String, - query: String? = nil, - responseType: T.Type - ) -> AsyncThrowingStream, Error> { - return AsyncThrowingStream { continuation in - Task { - do { - let stream = try await createDataStream( - dataSourceName: dataSourceName, - query: query, - responseType: responseType - ) - - for try await data in stream { - continuation.yield(data) - } - - continuation.finish() - - } catch { - continuation.finish(throwing: error) - } - } - } - } - - /// Writes data to a data source - public func writeData( - _ data: T, - to dataSourceName: String, - path: String? = nil, - options: WriteOptions = WriteOptions() - ) async throws -> WriteResult { - guard let configuration = dataSources[dataSourceName] else { - throw DataSourceError.dataSourceUnavailable(dataSourceName) - } - - guard configuration.type.capabilities.contains(.write) else { - throw DataSourceError.unsupportedOperation - } - - // Validate data if enabled - if configuration.enableValidation { - try await validationEngine.validate(data, for: configuration) - } - - // Transform data if rules are configured - let transformedData = try await transformationEngine.transform( - data, - using: configuration.transformationRules - ) - - return try await writeDataToSource( - transformedData, - configuration: configuration, - path: path, - options: options - ) - } - - /// Batch processes multiple queries - public func batchExecute( - queries: [DataQuery], - responseType: T.Type, - maxConcurrency: Int = 5 - ) async throws -> [DataResponse] { - let semaphore = AsyncSemaphore(value: maxConcurrency) - - return try await withThrowingTaskGroup(of: (Int, DataResponse).self) { group in - for (index, query) in queries.enumerated() { - group.addTask { - await semaphore.wait() - defer { semaphore.signal() } - - let response = try await self.executeQuery(query, responseType: responseType) - return (index, response) - } - } - - var results: [DataResponse] = Array(repeating: DataResponse( - queryId: "", - data: nil, - metadata: DataResponse.ResponseMetadata( - totalCount: nil, - pageCount: nil, - currentPage: nil, - hasNextPage: false, - hasPreviousPage: false, - executionTime: 0, - dataSourceName: "", - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ), count: queries.count) - - for try await (index, response) in group { - results[index] = response - } - - return results - } - } - - /// Synchronizes data between multiple data sources - public func synchronizeDataSources( - sourceNames: [String], - syncRules: [SyncRule] = [] - ) async throws { - for rule in syncRules { - try await applySyncRule(rule, dataSources: sourceNames) - } - - logger.info("Data source synchronization completed for \(sourceNames.count) sources") - } - - /// Migrates data from one source to another - public func migrateData( - from sourceDataSource: String, - to targetDataSource: String, - mapping: DataMapping, - batchSize: Int = 1000 - ) async throws { - guard let sourceConfig = dataSources[sourceDataSource], - let targetConfig = dataSources[targetDataSource] else { - throw DataSourceError.migrationFailed - } - - try await performDataMigration( - from: sourceConfig, - to: targetConfig, - mapping: mapping, - batchSize: batchSize - ) - - logger.info("Data migration completed from \(sourceDataSource) to \(targetDataSource)") - } - - /// Gets connection status for all data sources - public func getConnectionStatus() async -> [String: ConnectionStatus] { - var statuses: [String: ConnectionStatus] = [:] - - for (name, configuration) in dataSources { - statuses[name] = await checkConnectionStatus(name, configuration: configuration) - } - - await MainActor.run { - self.connectionStatus = statuses - } - - return statuses - } - - /// Gets data source metrics and insights - public func getDataSourceInsights() async -> DataSourceInsights { - let totalQueries = metrics.totalQueries - let successRate = metrics.successfulQueries > 0 ? - Double(metrics.successfulQueries) / Double(metrics.totalQueries) : 0 - let averageResponseTime = metrics.averageResponseTime - let cacheHitRate = metrics.cacheHits > 0 ? - Double(metrics.cacheHits) / Double(metrics.totalQueries) : 0 - - return DataSourceInsights( - totalDataSources: dataSources.count, - activeConnections: activeConnections.count, - totalQueries: totalQueries, - successRate: successRate, - averageResponseTime: averageResponseTime, - cacheHitRate: cacheHitRate, - streamingConnections: streamingConnections.count, - queuedQueries: queryQueue.count - ) - } - - /// Clears all cached data - public func clearAllCaches() async { - for dataSourceName in dataSources.keys { - await clearCachedData(for: dataSourceName) - } - - logger.info("All data source caches cleared") - } - - /// Resets data source manager - public func reset() async { - // Disconnect all connections - for name in dataSources.keys { - await disconnectDataSource(name) - } - - await MainActor.run { - self.dataSources.removeAll() - self.activeConnections.removeAll() - self.streamingConnections.removeAll() - self.queryQueue.removeAll() - self.connectionStatus.removeAll() - self.metrics = DataSourceMetrics() - } - - // Clear all caches - await clearAllCaches() - - logger.info("Data source manager reset completed") - } - - // MARK: - Private Methods - - private func setupDataSourceManager() { - // Setup background processing - setupBackgroundProcessing() - - // Setup notification observers - setupNotificationObservers() - - logger.info("Data source manager initialized") - } - - private func setupBackgroundProcessing() { - // Process query queue periodically - Timer.publish(every: 1.0, on: .main, in: .common) - .autoconnect() - .sink { [weak self] _ in - Task { await self?.processQueryQueue() } - } - .store(in: &cancellables) - } - - private func setupNotificationObservers() { - NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) - .sink { [weak self] _ in - Task { await self?.reconnectDisconnectedSources() } - } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification) - .sink { [weak self] _ in - Task { await self?.handleAppDidEnterBackground() } - } - .store(in: &cancellables) - } - - private func isValidConfiguration(_ configuration: DataSourceConfiguration) -> Bool { - return !configuration.name.isEmpty && - !configuration.endpoint.isEmpty && - configuration.timeout > 0 && - configuration.maxRetryAttempts > 0 - } - - private func testConnection(_ configuration: DataSourceConfiguration) async throws { - switch configuration.type { - case .rest, .graphql: - try await apiManager.testConnection(endpoint: configuration.endpoint) - case .websocket: - try await streamingManager.testWebSocketConnection(endpoint: configuration.endpoint) - case .database: - try await testDatabaseConnection(configuration) - case .file: - try await testFileSystemAccess(configuration) - case .cloud: - try await cloudIntegration.testConnection(configuration) - default: - // Custom testing implementation - break - } - } - - private func executeQueryForType( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - switch configuration.type { - case .rest: - return try await executeRESTQuery(query, configuration: configuration, responseType: responseType) - case .graphql: - return try await executeGraphQLQuery(query, configuration: configuration, responseType: responseType) - case .database: - return try await executeDatabaseQuery(query, configuration: configuration, responseType: responseType) - case .file: - return try await executeFileQuery(query, configuration: configuration, responseType: responseType) - case .cloud: - return try await executeCloudQuery(query, configuration: configuration, responseType: responseType) - case .rss: - return try await executeRSSQuery(query, configuration: configuration, responseType: responseType) - default: - throw DataSourceError.unsupportedOperation - } - } - - private func executeRESTQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Build REST request - var endpoint = configuration.endpoint - if !query.query.isEmpty { - endpoint = "\(endpoint)/\(query.query)" - } - - // Add query parameters - var urlComponents = URLComponents(string: endpoint) - var queryItems: [URLQueryItem] = [] - - for (key, value) in query.parameters { - queryItems.append(URLQueryItem(name: key, value: String(describing: value))) - } - - // Add filters as query parameters - for filter in query.filters { - let filterValue = "\(filter.operator.rawValue):\(filter.value)" - queryItems.append(URLQueryItem(name: filter.field, value: filterValue)) - } - - urlComponents?.queryItems = queryItems - - guard let finalURL = urlComponents?.url else { - throw DataSourceError.queryFailed("Invalid URL construction") - } - - // Execute via APIManager - let apiRequest = APIManager.APIRequest( - url: finalURL, - method: .GET, - headers: configuration.headers, - timeout: query.timeout ?? configuration.timeout - ) - - let apiResponse = try await apiManager.request(apiRequest, responseType: responseType) - - // Convert to DataResponse - return DataResponse( - queryId: query.id, - data: apiResponse.data, - metadata: DataResponse.ResponseMetadata( - totalCount: nil, - pageCount: nil, - currentPage: query.pagination?.page, - hasNextPage: false, - hasPreviousPage: false, - executionTime: apiResponse.responseTime, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeGraphQLQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Build GraphQL request - let graphQLRequest = APIManager.GraphQLRequest( - query: query.query, - variables: query.parameters, - operationName: nil - ) - - let apiResponse = try await apiManager.graphQL(graphQLRequest, responseType: responseType) - - return DataResponse( - queryId: query.id, - data: apiResponse.data, - metadata: DataResponse.ResponseMetadata( - totalCount: nil, - pageCount: nil, - currentPage: nil, - hasNextPage: false, - hasPreviousPage: false, - executionTime: apiResponse.responseTime, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeDatabaseQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute database query - // Implementation would use Core Data, SQLite, or other database framework - throw DataSourceError.unsupportedOperation - } - - private func executeFileQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute file system query - let filePath = "\(configuration.endpoint)/\(query.query)" - let fileURL = URL(fileURLWithPath: filePath) - - let data = try Data(contentsOf: fileURL) - let decodedData = try JSONDecoder().decode(responseType, from: data) - - return DataResponse( - queryId: query.id, - data: decodedData, - metadata: DataResponse.ResponseMetadata( - totalCount: 1, - pageCount: 1, - currentPage: 1, - hasNextPage: false, - hasPreviousPage: false, - executionTime: 0.001, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeCloudQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute cloud query via CloudIntegration - let data = try await cloudIntegration.download(path: query.query) - let decodedData = try JSONDecoder().decode(responseType, from: data) - - return DataResponse( - queryId: query.id, - data: decodedData, - metadata: DataResponse.ResponseMetadata( - totalCount: 1, - pageCount: 1, - currentPage: 1, - hasNextPage: false, - hasPreviousPage: false, - executionTime: 0.1, - dataSourceName: query.dataSourceName, - version: "1.0" - ), - errors: [], - cached: false, - timestamp: Date() - ) - } - - private func executeRSSQuery( - _ query: DataQuery, - configuration: DataSourceConfiguration, - responseType: T.Type - ) async throws -> DataResponse { - // Execute RSS feed query - // Implementation would parse RSS/XML data - throw DataSourceError.unsupportedOperation - } - - private func createDataStream( - dataSourceName: String, - query: String?, - responseType: T.Type - ) async throws -> AsyncThrowingStream, Error> { - guard let configuration = dataSources[dataSourceName] else { - throw DataSourceError.dataSourceUnavailable(dataSourceName) - } - - return try await streamingManager.createStream( - configuration: configuration, - query: query, - responseType: responseType - ) - } - - private func initializeRealtimeConnection(_ configuration: DataSourceConfiguration) async throws { - // Initialize real-time connection for WebSocket, etc. - let connection = try await streamingManager.connect(configuration: configuration) - - await MainActor.run { - self.streamingConnections[configuration.name] = connection - } - } - - private func writeDataToSource( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - switch configuration.type { - case .rest, .graphql: - return try await writeToAPI(data, configuration: configuration, path: path, options: options) - case .database: - return try await writeToDatabase(data, configuration: configuration, options: options) - case .file: - return try await writeToFile(data, configuration: configuration, path: path, options: options) - case .cloud: - return try await writeToCloud(data, configuration: configuration, path: path, options: options) - default: - throw DataSourceError.unsupportedOperation - } - } - - private func writeToAPI( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - // Implementation would write to API - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func writeToDatabase( - _ data: T, - configuration: DataSourceConfiguration, - options: WriteOptions - ) async throws -> WriteResult { - // Implementation would write to database - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func writeToFile( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - let encoder = JSONEncoder() - let jsonData = try encoder.encode(data) - - let filePath = path ?? "\(configuration.endpoint)/data.json" - let fileURL = URL(fileURLWithPath: filePath) - - try jsonData.write(to: fileURL) - - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func writeToCloud( - _ data: T, - configuration: DataSourceConfiguration, - path: String?, - options: WriteOptions - ) async throws -> WriteResult { - let encoder = JSONEncoder() - let jsonData = try encoder.encode(data) - - let cloudPath = path ?? "data.json" - _ = try await cloudIntegration.upload(data: jsonData, to: cloudPath) - - return WriteResult(success: true, recordsWritten: 1, errors: []) - } - - private func getCachedResponse( - query: DataQuery, - responseType: T.Type - ) async -> DataResponse? { - let cacheKey = "query_\(query.dataSourceName)_\(query.query.hashValue)" - return try? await cacheManager.retrieve(DataResponse.self, forKey: cacheKey) - } - - private func cacheResponse( - query: DataQuery, - response: DataResponse - ) async { - let cacheKey = "query_\(query.dataSourceName)_\(query.query.hashValue)" - let ttl = query.cacheTTL ?? 300 // 5 minutes default - try? await cacheManager.store(response, forKey: cacheKey, ttl: ttl) - } - - private func clearCachedData(for dataSourceName: String) async { - // Implementation would clear cache for specific data source - } - - private func disconnectDataSource(_ name: String) async { - // Disconnect from data source - await MainActor.run { - self.activeConnections.removeValue(forKey: name) - self.streamingConnections.removeValue(forKey: name) - } - } - - private func checkConnectionStatus(_ name: String, configuration: DataSourceConfiguration) async -> ConnectionStatus { - // Check actual connection status - return .connected - } - - private func updateMetrics(queryId: String, cached: Bool, executionTime: TimeInterval, error: Error? = nil) async { - await MainActor.run { - self.metrics.totalQueries += 1 - - if error == nil { - self.metrics.successfulQueries += 1 - } else { - self.metrics.failedQueries += 1 - } - - if cached { - self.metrics.cacheHits += 1 - } else { - self.metrics.cacheMisses += 1 - } - - self.metrics.totalResponseTime += executionTime - self.metrics.averageResponseTime = self.metrics.totalResponseTime / Double(self.metrics.totalQueries) - } - } - - private func processQueryQueue() async { - // Process queued queries - } - - private func reconnectDisconnectedSources() async { - // Reconnect to disconnected sources - } - - private func handleAppDidEnterBackground() async { - // Handle background operation - } - - private func testDatabaseConnection(_ configuration: DataSourceConfiguration) async throws { - // Test database connection - } - - private func testFileSystemAccess(_ configuration: DataSourceConfiguration) async throws { - // Test file system access - } - - private func applySyncRule(_ rule: SyncRule, dataSources: [String]) async throws { - // Apply synchronization rule - } - - private func performDataMigration( - from sourceConfig: DataSourceConfiguration, - to targetConfig: DataSourceConfiguration, - mapping: DataMapping, - batchSize: Int - ) async throws { - // Perform data migration - } -} - -// MARK: - Supporting Types - -public enum ConnectionStatus: String, CaseIterable { - case connected = "connected" - case disconnected = "disconnected" - case connecting = "connecting" - case error = "error" -} - -public struct DataSourceConnection { - public let name: String - public let type: DataSourceManager.DataSourceType - public let connectedAt: Date - public let lastActivity: Date -} - -public struct StreamingConnection { - public let name: String - public let endpoint: String - public let connectedAt: Date - public let messageCount: Int -} - -public struct DataSourceMetrics { - public var totalQueries: Int = 0 - public var successfulQueries: Int = 0 - public var failedQueries: Int = 0 - public var cacheHits: Int = 0 - public var cacheMisses: Int = 0 - public var totalResponseTime: TimeInterval = 0 - public var averageResponseTime: TimeInterval = 0 -} - -public struct DataSourceInsights { - public let totalDataSources: Int - public let activeConnections: Int - public let totalQueries: Int - public let successRate: Double - public let averageResponseTime: TimeInterval - public let cacheHitRate: Double - public let streamingConnections: Int - public let queuedQueries: Int -} - -public struct WriteOptions { - public var upsert: Bool = false - public var batch: Bool = false - public var transaction: Bool = false - public var validateBeforeWrite: Bool = true - - public init() {} -} - -public struct WriteResult { - public let success: Bool - public let recordsWritten: Int - public let errors: [String] -} - -public struct SyncRule { - public let sourceField: String - public let targetField: String - public let transformation: String? - public let bidirectional: Bool -} - -public struct DataMapping { - public let fieldMappings: [String: String] - public let transformations: [String: String] - public let filters: [DataSourceManager.DataQuery.DataFilter] -} - -// MARK: - Supporting Classes - -private class QueryEngine { - func optimizeQuery(_ query: DataSourceManager.DataQuery) -> DataSourceManager.DataQuery { - // Implementation would optimize queries - return query - } -} - -private class TransformationEngine { - func transform(_ data: T, using rules: [String: String]) async throws -> T { - // Implementation would transform data - return data - } -} - -private class ValidationEngine { - func validate(_ data: T, for configuration: DataSourceManager.DataSourceConfiguration) async throws { - // Implementation would validate data - } -} - -private class StreamingManager { - func connect(configuration: DataSourceManager.DataSourceConfiguration) async throws -> StreamingConnection { - return StreamingConnection( - name: configuration.name, - endpoint: configuration.endpoint, - connectedAt: Date(), - messageCount: 0 - ) - } - - func createStream( - configuration: DataSourceManager.DataSourceConfiguration, - query: String?, - responseType: T.Type - ) async throws -> AsyncThrowingStream, Error> { - return AsyncThrowingStream { continuation in - // Implementation would create actual stream - continuation.finish() - } - } - - func testWebSocketConnection(endpoint: String) async throws { - // Implementation would test WebSocket connection - } -} - -private class DataSourceMetricsCollector { - func recordMetric(_ metric: String, value: Double) async { - // Implementation would record metrics - } -} - -private class RateLimiter { - func checkRateLimit(for dataSourceName: String, configuration: DataSourceManager.DataSourceConfiguration) async throws { - // Implementation would check rate limits - } -} - -private class AsyncSemaphore { - private let semaphore: DispatchSemaphore - - init(value: Int) { - self.semaphore = DispatchSemaphore(value: value) - } - - func wait() async { - await withCheckedContinuation { continuation in - DispatchQueue.global().async { - self.semaphore.wait() - continuation.resume() - } - } - } - - func signal() { - semaphore.signal() - } -} \ No newline at end of file diff --git a/Sources/Interactive/InteractiveWidgets.swift b/Sources/Interactive/InteractiveWidgets.swift deleted file mode 100644 index 7019380..0000000 --- a/Sources/Interactive/InteractiveWidgets.swift +++ /dev/null @@ -1,715 +0,0 @@ -// InteractiveWidgets.swift -// iOS-Widget-Development-Kit -// -// iOS 17+ Interactive Widget support with App Intents -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit -import AppIntents - -// MARK: - Interactive Widget Protocol - -/// Protocol for interactive widget configurations -@available(iOS 17.0, *) -public protocol InteractiveWidgetProtocol { - associatedtype IntentType: AppIntent - var supportedFamilies: [WidgetFamily] { get } -} - -// MARK: - Base Intent Protocol - -/// Base protocol for widget intents -@available(iOS 17.0, *) -public protocol WidgetIntentProtocol: AppIntent { - static var intentIdentifier: String { get } -} - -// MARK: - Toggle Intent - -/// Generic toggle intent for widgets -@available(iOS 17.0, *) -public struct ToggleIntent: AppIntent { - public static var title: LocalizedStringResource = "Toggle" - public static var description = IntentDescription("Toggle a boolean value") - - @Parameter(title: "Item ID") - public var itemId: String - - @Parameter(title: "New State") - public var newState: Bool - - public init() {} - - public init(itemId: String, newState: Bool) { - self.itemId = itemId - self.newState = newState - } - - public func perform() async throws -> some IntentResult { - // Store the state change - UserDefaults(suiteName: "group.widget.interactive")?.set(newState, forKey: "toggle_\(itemId)") - - // Reload widgets - WidgetCenter.shared.reloadAllTimelines() - - return .result() - } -} - -// MARK: - Increment Intent - -/// Intent to increment a counter -@available(iOS 17.0, *) -public struct IncrementIntent: AppIntent { - public static var title: LocalizedStringResource = "Increment" - public static var description = IntentDescription("Increment a counter value") - - @Parameter(title: "Counter ID") - public var counterId: String - - @Parameter(title: "Amount") - public var amount: Int - - public init() {} - - public init(counterId: String, amount: Int = 1) { - self.counterId = counterId - self.amount = amount - } - - public func perform() async throws -> some IntentResult { - let defaults = UserDefaults(suiteName: "group.widget.interactive") - let currentValue = defaults?.integer(forKey: "counter_\(counterId)") ?? 0 - defaults?.set(currentValue + amount, forKey: "counter_\(counterId)") - - WidgetCenter.shared.reloadAllTimelines() - - return .result() - } -} - -// MARK: - Refresh Intent - -/// Intent to manually refresh widget data -@available(iOS 17.0, *) -public struct RefreshWidgetIntent: AppIntent { - public static var title: LocalizedStringResource = "Refresh Widget" - public static var description = IntentDescription("Manually refresh widget data") - - @Parameter(title: "Widget ID") - public var widgetId: String? - - public init() {} - - public init(widgetId: String? = nil) { - self.widgetId = widgetId - } - - public func perform() async throws -> some IntentResult { - if let widgetId = widgetId { - WidgetCenter.shared.reloadTimelines(ofKind: widgetId) - } else { - WidgetCenter.shared.reloadAllTimelines() - } - - return .result() - } -} - -// MARK: - Action Intent - -/// Generic action intent for custom actions -@available(iOS 17.0, *) -public struct ActionIntent: AppIntent { - public static var title: LocalizedStringResource = "Perform Action" - public static var description = IntentDescription("Perform a custom widget action") - - @Parameter(title: "Action Type") - public var actionType: String - - @Parameter(title: "Parameters") - public var parameters: String? - - public init() {} - - public init(actionType: String, parameters: String? = nil) { - self.actionType = actionType - self.parameters = parameters - } - - public func perform() async throws -> some IntentResult { - // Post notification for app to handle - NotificationCenter.default.post( - name: Notification.Name("WidgetAction"), - object: nil, - userInfo: ["actionType": actionType, "parameters": parameters as Any] - ) - - return .result() - } -} - -// MARK: - Interactive Button Styles - -@available(iOS 17.0, *) -public struct InteractiveWidgetButtonStyle: ButtonStyle { - let backgroundColor: Color - let foregroundColor: Color - let cornerRadius: CGFloat - - public init( - backgroundColor: Color = .blue, - foregroundColor: Color = .white, - cornerRadius: CGFloat = 8 - ) { - self.backgroundColor = backgroundColor - self.foregroundColor = foregroundColor - self.cornerRadius = cornerRadius - } - - public func makeBody(configuration: Configuration) -> some View { - configuration.label - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(backgroundColor.opacity(configuration.isPressed ? 0.8 : 1)) - .foregroundStyle(foregroundColor) - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) - .scaleEffect(configuration.isPressed ? 0.95 : 1) - .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) - } -} - -// MARK: - Interactive Toggle Widget - -/// Interactive toggle widget template -@available(iOS 17.0, *) -public struct InteractiveToggleWidget: View { - let title: String - let itemId: String - let isOn: Bool - let onIcon: String - let offIcon: String - let onColor: Color - let offColor: Color - - public init( - title: String, - itemId: String, - isOn: Bool, - onIcon: String = "checkmark.circle.fill", - offIcon: String = "circle", - onColor: Color = .green, - offColor: Color = .gray - ) { - self.title = title - self.itemId = itemId - self.isOn = isOn - self.onIcon = onIcon - self.offIcon = offIcon - self.onColor = onColor - self.offColor = offColor - } - - public var body: some View { - Button(intent: ToggleIntent(itemId: itemId, newState: !isOn)) { - HStack { - Image(systemName: isOn ? onIcon : offIcon) - .font(.title2) - .foregroundStyle(isOn ? onColor : offColor) - - Text(title) - .font(.caption) - .lineLimit(1) - - Spacer() - } - .padding() - .background(Color.secondary.opacity(0.1)) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - .buttonStyle(.plain) - } -} - -// MARK: - Interactive Counter Widget - -/// Interactive counter widget template -@available(iOS 17.0, *) -public struct InteractiveCounterWidget: View { - let title: String - let counterId: String - let currentValue: Int - let accentColor: Color - - public init( - title: String, - counterId: String, - currentValue: Int, - accentColor: Color = .blue - ) { - self.title = title - self.counterId = counterId - self.currentValue = currentValue - self.accentColor = accentColor - } - - public var body: some View { - VStack(spacing: 12) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - - Text("\(currentValue)") - .font(.system(size: 48, weight: .bold, design: .rounded)) - .foregroundStyle(accentColor) - - HStack(spacing: 16) { - Button(intent: IncrementIntent(counterId: counterId, amount: -1)) { - Image(systemName: "minus.circle.fill") - .font(.title) - .foregroundStyle(.red) - } - .buttonStyle(.plain) - - Button(intent: IncrementIntent(counterId: counterId, amount: 1)) { - Image(systemName: "plus.circle.fill") - .font(.title) - .foregroundStyle(.green) - } - .buttonStyle(.plain) - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Task List Widget - -/// Interactive task list widget with completion toggles -@available(iOS 17.0, *) -public struct InteractiveTaskListWidget: View { - public struct Task: Identifiable { - public let id: String - public let title: String - public var isCompleted: Bool - public let priority: Priority - - public enum Priority { - case low, medium, high - - var color: Color { - switch self { - case .low: return .blue - case .medium: return .orange - case .high: return .red - } - } - } - - public init(id: String, title: String, isCompleted: Bool, priority: Priority = .medium) { - self.id = id - self.title = title - self.isCompleted = isCompleted - self.priority = priority - } - } - - let tasks: [Task] - let listTitle: String - - public init(tasks: [Task], listTitle: String = "Tasks") { - self.tasks = tasks - self.listTitle = listTitle - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text(listTitle) - .font(.headline) - - Spacer() - - let completed = tasks.filter { $0.isCompleted }.count - Text("\(completed)/\(tasks.count)") - .font(.caption) - .foregroundStyle(.secondary) - } - - ForEach(tasks.prefix(4)) { task in - Button(intent: ToggleIntent(itemId: task.id, newState: !task.isCompleted)) { - HStack(spacing: 10) { - Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") - .foregroundStyle(task.isCompleted ? .green : task.priority.color) - .font(.body) - - Text(task.title) - .font(.caption) - .strikethrough(task.isCompleted) - .foregroundStyle(task.isCompleted ? .secondary : .primary) - .lineLimit(1) - - Spacer() - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Quick Actions Widget - -/// Quick action buttons widget -@available(iOS 17.0, *) -public struct InteractiveQuickActionsWidget: View { - public struct QuickAction: Identifiable { - public let id: String - public let title: String - public let icon: String - public let color: Color - public let url: URL? - - public init(id: String, title: String, icon: String, color: Color, url: URL? = nil) { - self.id = id - self.title = title - self.icon = icon - self.color = color - self.url = url - } - } - - let actions: [QuickAction] - let columns: Int - - public init(actions: [QuickAction], columns: Int = 2) { - self.actions = actions - self.columns = columns - } - - public var body: some View { - LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns), spacing: 8) { - ForEach(actions.prefix(4)) { action in - if let url = action.url { - Link(destination: url) { - ActionButtonView(action: action) - } - } else { - Button(intent: ActionIntent(actionType: action.id)) { - ActionButtonView(action: action) - } - .buttonStyle(.plain) - } - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } - - struct ActionButtonView: View { - let action: QuickAction - - var body: some View { - VStack(spacing: 6) { - Image(systemName: action.icon) - .font(.title2) - .foregroundStyle(.white) - .frame(width: 44, height: 44) - .background(action.color) - .clipShape(RoundedRectangle(cornerRadius: 10)) - - Text(action.title) - .font(.caption2) - .lineLimit(1) - } - } - } -} - -// MARK: - Interactive Timer Control Widget - -/// Timer control widget with start/pause/reset -@available(iOS 17.0, *) -public struct InteractiveTimerWidget: View { - let timerId: String - let timeRemaining: TimeInterval - let isRunning: Bool - let title: String - - public init( - timerId: String, - timeRemaining: TimeInterval, - isRunning: Bool, - title: String = "Timer" - ) { - self.timerId = timerId - self.timeRemaining = timeRemaining - self.isRunning = isRunning - self.title = title - } - - private func formatTime(_ interval: TimeInterval) -> String { - let minutes = Int(interval) / 60 - let seconds = Int(interval) % 60 - return String(format: "%02d:%02d", minutes, seconds) - } - - public var body: some View { - VStack(spacing: 12) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - - Text(formatTime(timeRemaining)) - .font(.system(size: 36, weight: .thin, design: .rounded)) - .monospacedDigit() - - HStack(spacing: 20) { - Button(intent: ActionIntent(actionType: "timer_reset", parameters: timerId)) { - Image(systemName: "arrow.counterclockwise") - .font(.title3) - .foregroundStyle(.orange) - } - .buttonStyle(.plain) - - Button(intent: ActionIntent(actionType: isRunning ? "timer_pause" : "timer_start", parameters: timerId)) { - Image(systemName: isRunning ? "pause.circle.fill" : "play.circle.fill") - .font(.title) - .foregroundStyle(isRunning ? .yellow : .green) - } - .buttonStyle(.plain) - - Button(intent: ActionIntent(actionType: "timer_stop", parameters: timerId)) { - Image(systemName: "stop.circle") - .font(.title3) - .foregroundStyle(.red) - } - .buttonStyle(.plain) - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Habit Tracker Widget - -/// Habit tracker with interactive checkmarks -@available(iOS 17.0, *) -public struct InteractiveHabitWidget: View { - public struct Habit: Identifiable { - public let id: String - public let name: String - public let icon: String - public let color: Color - public var weekProgress: [Bool] // 7 days - - public init(id: String, name: String, icon: String, color: Color, weekProgress: [Bool]) { - self.id = id - self.name = name - self.icon = icon - self.color = color - self.weekProgress = weekProgress - } - } - - let habits: [Habit] - let todayIndex: Int - - public init(habits: [Habit], todayIndex: Int = 6) { - self.habits = habits - self.todayIndex = todayIndex - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Habits") - .font(.headline) - Spacer() - Text("Today") - .font(.caption) - .foregroundStyle(.secondary) - } - - ForEach(habits.prefix(3)) { habit in - HStack(spacing: 8) { - Image(systemName: habit.icon) - .font(.caption) - .foregroundStyle(habit.color) - .frame(width: 20) - - Text(habit.name) - .font(.caption) - .lineLimit(1) - - Spacer() - - // Week progress (non-interactive) - HStack(spacing: 2) { - ForEach(0..<6, id: \.self) { day in - Circle() - .fill(habit.weekProgress.indices.contains(day) && habit.weekProgress[day] - ? habit.color - : Color.secondary.opacity(0.3)) - .frame(width: 6, height: 6) - } - } - - // Today's checkbox (interactive) - Button(intent: ToggleIntent( - itemId: "\(habit.id)_\(todayIndex)", - newState: !(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex]) - )) { - Image(systemName: habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] - ? "checkmark.circle.fill" - : "circle") - .font(.body) - .foregroundStyle(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] - ? habit.color - : Color.secondary) - } - .buttonStyle(.plain) - } - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Rating Widget - -/// Star rating widget -@available(iOS 17.0, *) -public struct InteractiveRatingWidget: View { - let ratingId: String - let currentRating: Int - let maxRating: Int - let title: String - let subtitle: String? - - public init( - ratingId: String, - currentRating: Int, - maxRating: Int = 5, - title: String, - subtitle: String? = nil - ) { - self.ratingId = ratingId - self.currentRating = currentRating - self.maxRating = maxRating - self.title = title - self.subtitle = subtitle - } - - public var body: some View { - VStack(spacing: 12) { - Text(title) - .font(.headline) - - if let subtitle = subtitle { - Text(subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - - HStack(spacing: 8) { - ForEach(1...maxRating, id: \.self) { star in - Button(intent: ActionIntent(actionType: "rate", parameters: "\(ratingId):\(star)")) { - Image(systemName: star <= currentRating ? "star.fill" : "star") - .font(.title2) - .foregroundStyle(star <= currentRating ? .yellow : .secondary) - } - .buttonStyle(.plain) - } - } - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} - -// MARK: - Interactive Water Tracker Widget - -/// Water intake tracker with increment buttons -@available(iOS 17.0, *) -public struct InteractiveWaterWidget: View { - let trackerId: String - let currentIntake: Double // in ml - let dailyGoal: Double - let glassSize: Double - - public init( - trackerId: String, - currentIntake: Double, - dailyGoal: Double = 2000, - glassSize: Double = 250 - ) { - self.trackerId = trackerId - self.currentIntake = currentIntake - self.dailyGoal = dailyGoal - self.glassSize = glassSize - } - - var progress: Double { - min(currentIntake / dailyGoal, 1.0) - } - - var glasses: Int { - Int(currentIntake / glassSize) - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Image(systemName: "drop.fill") - .foregroundStyle(.cyan) - Text("Water") - .font(.headline) - Spacer() - } - - ZStack { - Circle() - .stroke(Color.cyan.opacity(0.2), lineWidth: 8) - - Circle() - .trim(from: 0, to: progress) - .stroke( - LinearGradient(colors: [.cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing), - style: StrokeStyle(lineWidth: 8, lineCap: .round) - ) - .rotationEffect(.degrees(-90)) - - VStack(spacing: 2) { - Text("\(Int(currentIntake))") - .font(.title3) - .fontWeight(.bold) - Text("/ \(Int(dailyGoal))ml") - .font(.system(size: 9)) - .foregroundStyle(.secondary) - } - } - .frame(width: 70, height: 70) - - Button(intent: ActionIntent(actionType: "add_water", parameters: "\(trackerId):\(Int(glassSize))")) { - Label("+\(Int(glassSize))ml", systemImage: "plus.circle.fill") - .font(.caption) - .foregroundStyle(.white) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(.cyan) - .clipShape(Capsule()) - } - .buttonStyle(.plain) - } - .padding() - .containerBackground(.fill.tertiary, for: .widget) - } -} diff --git a/Sources/LiveActivities/LiveActivityTemplates.swift b/Sources/LiveActivities/LiveActivityTemplates.swift deleted file mode 100644 index 6129405..0000000 --- a/Sources/LiveActivities/LiveActivityTemplates.swift +++ /dev/null @@ -1,1303 +0,0 @@ -// LiveActivityTemplates.swift -// iOS-Widget-Development-Kit -// -// Complete collection of 15+ Live Activity templates with Dynamic Island support -// Created by Muhittin Camdali - -import SwiftUI -import ActivityKit -import WidgetKit - -// MARK: - Live Activity Protocol - -/// Protocol for all Live Activity templates -@available(iOS 16.1, *) -public protocol LiveActivityTemplate { - associatedtype Attributes: ActivityAttributes - associatedtype CompactLeadingView: View - associatedtype CompactTrailingView: View - associatedtype ExpandedView: View - associatedtype MinimalView: View - associatedtype LockScreenView: View - - @ViewBuilder func makeCompactLeading(context: ActivityViewContext) -> CompactLeadingView - @ViewBuilder func makeCompactTrailing(context: ActivityViewContext) -> CompactTrailingView - @ViewBuilder func makeExpanded(context: ActivityViewContext) -> ExpandedView - @ViewBuilder func makeMinimal(context: ActivityViewContext) -> MinimalView - @ViewBuilder func makeLockScreen(context: ActivityViewContext) -> LockScreenView -} - -// MARK: - 1. Delivery Tracking Activity - -/// Food/Package delivery tracking Live Activity -@available(iOS 16.1, *) -public struct DeliveryAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: DeliveryStatus - public var estimatedArrival: Date - public var driverName: String - public var currentStep: Int - - public init(status: DeliveryStatus, estimatedArrival: Date, driverName: String, currentStep: Int) { - self.status = status - self.estimatedArrival = estimatedArrival - self.driverName = driverName - self.currentStep = currentStep - } - } - - public enum DeliveryStatus: String, Codable { - case preparing = "Preparing" - case pickedUp = "Picked Up" - case onTheWay = "On the Way" - case arriving = "Arriving" - case delivered = "Delivered" - - public var icon: String { - switch self { - case .preparing: return "takeoutbag.and.cup.and.straw" - case .pickedUp: return "bag.fill" - case .onTheWay: return "car.fill" - case .arriving: return "location.fill" - case .delivered: return "checkmark.circle.fill" - } - } - - public var color: Color { - switch self { - case .preparing: return .orange - case .pickedUp: return .blue - case .onTheWay: return .purple - case .arriving: return .green - case .delivered: return .green - } - } - } - - public let orderNumber: String - public let restaurantName: String - public let totalSteps: Int - - public init(orderNumber: String, restaurantName: String, totalSteps: Int = 4) { - self.orderNumber = orderNumber - self.restaurantName = restaurantName - self.totalSteps = totalSteps - } -} - -@available(iOS 16.2, *) -public struct DeliveryActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(context.attributes.restaurantName) - .font(.headline) - Text("Order #\(context.attributes.orderNumber)") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: context.state.status.icon) - .font(.title2) - .foregroundStyle(context.state.status.color) - } - - // Progress bar - ProgressView(value: Double(context.state.currentStep), total: Double(context.attributes.totalSteps)) - .tint(context.state.status.color) - - HStack { - Text(context.state.status.rawValue) - .font(.caption) - .fontWeight(.medium) - - Spacer() - - Label { - Text(context.state.estimatedArrival, style: .relative) - } icon: { - Image(systemName: "clock") - } - .font(.caption) - .foregroundStyle(.secondary) - } - } - .padding() - } - - // Dynamic Island Views - public var compactLeading: some View { - Image(systemName: context.state.status.icon) - .foregroundStyle(context.state.status.color) - } - - public var compactTrailing: some View { - Text(context.state.estimatedArrival, style: .timer) - .font(.caption2) - .monospacedDigit() - } - - public var minimal: some View { - Image(systemName: context.state.status.icon) - .foregroundStyle(context.state.status.color) - } -} - -// MARK: - 2. Ride Sharing Activity - -/// Uber/Lyft style ride tracking -@available(iOS 16.1, *) -public struct RideAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: RideStatus - public var driverName: String - public var vehicleInfo: String - public var licensePlate: String - public var eta: Date - public var driverRating: Double - - public init(status: RideStatus, driverName: String, vehicleInfo: String, licensePlate: String, eta: Date, driverRating: Double) { - self.status = status - self.driverName = driverName - self.vehicleInfo = vehicleInfo - self.licensePlate = licensePlate - self.eta = eta - self.driverRating = driverRating - } - } - - public enum RideStatus: String, Codable { - case searching = "Finding Driver" - case driverAssigned = "Driver Assigned" - case arriving = "Driver Arriving" - case inProgress = "In Progress" - case completed = "Completed" - - public var icon: String { - switch self { - case .searching: return "magnifyingglass" - case .driverAssigned: return "person.fill.checkmark" - case .arriving: return "car.fill" - case .inProgress: return "location.fill" - case .completed: return "checkmark.circle.fill" - } - } - } - - public let pickupLocation: String - public let dropoffLocation: String - public let rideType: String - - public init(pickupLocation: String, dropoffLocation: String, rideType: String = "UberX") { - self.pickupLocation = pickupLocation - self.dropoffLocation = dropoffLocation - self.rideType = rideType - } -} - -@available(iOS 16.2, *) -public struct RideActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(context.state.driverName) - .font(.headline) - - HStack(spacing: 4) { - Image(systemName: "star.fill") - .font(.caption2) - .foregroundStyle(.yellow) - Text(String(format: "%.1f", context.state.driverRating)) - .font(.caption) - } - } - - Spacer() - - VStack(alignment: .trailing, spacing: 2) { - Text(context.state.vehicleInfo) - .font(.caption) - Text(context.state.licensePlate) - .font(.caption) - .fontWeight(.bold) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.secondary.opacity(0.2)) - .clipShape(RoundedRectangle(cornerRadius: 4)) - } - } - - Divider() - - HStack { - VStack(alignment: .leading) { - Label(context.attributes.pickupLocation, systemImage: "circle.fill") - .font(.caption2) - .foregroundStyle(.green) - - Label(context.attributes.dropoffLocation, systemImage: "mappin.circle.fill") - .font(.caption2) - .foregroundStyle(.red) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("ETA") - .font(.caption2) - .foregroundStyle(.secondary) - Text(context.state.eta, style: .relative) - .font(.headline) - } - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "car.fill") - .foregroundStyle(.blue) - } - - public var compactTrailing: some View { - Text(context.state.eta, style: .relative) - .font(.caption2) - } -} - -// MARK: - 3. Sports Score Activity - -/// Live sports score tracking -@available(iOS 16.1, *) -public struct SportsScoreAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var homeScore: Int - public var awayScore: Int - public var gameTime: String - public var period: String - public var isLive: Bool - public var lastEvent: String? - - public init(homeScore: Int, awayScore: Int, gameTime: String, period: String, isLive: Bool, lastEvent: String? = nil) { - self.homeScore = homeScore - self.awayScore = awayScore - self.gameTime = gameTime - self.period = period - self.isLive = isLive - self.lastEvent = lastEvent - } - } - - public let homeTeam: String - public let awayTeam: String - public let homeTeamAbbr: String - public let awayTeamAbbr: String - public let league: String - - public init(homeTeam: String, awayTeam: String, homeTeamAbbr: String, awayTeamAbbr: String, league: String) { - self.homeTeam = homeTeam - self.awayTeam = awayTeam - self.homeTeamAbbr = homeTeamAbbr - self.awayTeamAbbr = awayTeamAbbr - self.league = league - } -} - -@available(iOS 16.2, *) -public struct SportsActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 8) { - HStack { - Text(context.attributes.league) - .font(.caption2) - .foregroundStyle(.secondary) - - Spacer() - - if context.state.isLive { - HStack(spacing: 4) { - Circle() - .fill(.red) - .frame(width: 6, height: 6) - Text("LIVE") - .font(.caption2) - .foregroundStyle(.red) - } - } - - Text(context.state.period) - .font(.caption) - .fontWeight(.medium) - } - - HStack { - TeamScoreView( - name: context.attributes.homeTeamAbbr, - score: context.state.homeScore, - isWinning: context.state.homeScore > context.state.awayScore - ) - - Spacer() - - VStack { - Text(context.state.gameTime) - .font(.headline) - .monospacedDigit() - } - - Spacer() - - TeamScoreView( - name: context.attributes.awayTeamAbbr, - score: context.state.awayScore, - isWinning: context.state.awayScore > context.state.homeScore - ) - } - - if let event = context.state.lastEvent { - Text(event) - .font(.caption2) - .foregroundStyle(.secondary) - .lineLimit(1) - } - } - .padding() - } - - public var compactLeading: some View { - HStack(spacing: 4) { - Text(context.attributes.homeTeamAbbr) - .font(.caption2) - .fontWeight(.bold) - Text("\(context.state.homeScore)") - .font(.caption) - } - } - - public var compactTrailing: some View { - HStack(spacing: 4) { - Text("\(context.state.awayScore)") - .font(.caption) - Text(context.attributes.awayTeamAbbr) - .font(.caption2) - .fontWeight(.bold) - } - } - - struct TeamScoreView: View { - let name: String - let score: Int - let isWinning: Bool - - var body: some View { - VStack(spacing: 4) { - Text(name) - .font(.caption) - .fontWeight(.medium) - Text("\(score)") - .font(.title) - .fontWeight(isWinning ? .bold : .regular) - .foregroundStyle(isWinning ? .primary : .secondary) - } - } - } -} - -// MARK: - 4. Timer Activity - -/// General purpose timer Live Activity -@available(iOS 16.1, *) -public struct TimerAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var endTime: Date - public var isPaused: Bool - public var pausedTimeRemaining: TimeInterval? - - public init(endTime: Date, isPaused: Bool = false, pausedTimeRemaining: TimeInterval? = nil) { - self.endTime = endTime - self.isPaused = isPaused - self.pausedTimeRemaining = pausedTimeRemaining - } - } - - public let timerName: String - public let category: String - public let accentColor: String - - public init(timerName: String, category: String = "Timer", accentColor: String = "blue") { - self.timerName = timerName - self.category = category - self.accentColor = accentColor - } -} - -@available(iOS 16.2, *) -public struct TimerActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - var color: Color { - switch context.attributes.accentColor { - case "red": return .red - case "green": return .green - case "orange": return .orange - case "purple": return .purple - default: return .blue - } - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Label(context.attributes.category, systemImage: "timer") - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - if context.state.isPaused { - Text("PAUSED") - .font(.caption2) - .fontWeight(.bold) - .foregroundStyle(.orange) - } - } - - Text(context.state.endTime, style: .timer) - .font(.system(size: 48, weight: .thin, design: .rounded)) - .monospacedDigit() - .foregroundStyle(color) - - Text(context.attributes.timerName) - .font(.headline) - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "timer") - .foregroundStyle(color) - } - - public var compactTrailing: some View { - Text(context.state.endTime, style: .timer) - .font(.caption) - .monospacedDigit() - } - - public var minimal: some View { - Text(context.state.endTime, style: .timer) - .font(.caption2) - .monospacedDigit() - } -} - -// MARK: - 5. Workout Activity - -/// Fitness workout tracking -@available(iOS 16.1, *) -public struct WorkoutAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var elapsedTime: TimeInterval - public var calories: Int - public var heartRate: Int - public var distance: Double - public var pace: String - public var isActive: Bool - - public init(elapsedTime: TimeInterval, calories: Int, heartRate: Int, distance: Double, pace: String, isActive: Bool) { - self.elapsedTime = elapsedTime - self.calories = calories - self.heartRate = heartRate - self.distance = distance - self.pace = pace - self.isActive = isActive - } - } - - public enum WorkoutType: String, Codable { - case running = "Running" - case cycling = "Cycling" - case swimming = "Swimming" - case walking = "Walking" - case hiit = "HIIT" - - public var icon: String { - switch self { - case .running: return "figure.run" - case .cycling: return "figure.outdoor.cycle" - case .swimming: return "figure.pool.swim" - case .walking: return "figure.walk" - case .hiit: return "figure.highintensity.intervaltraining" - } - } - } - - public let workoutType: WorkoutType - public let goalType: String - public let goalValue: Double - - public init(workoutType: WorkoutType, goalType: String = "Distance", goalValue: Double = 5.0) { - self.workoutType = workoutType - self.goalType = goalType - self.goalValue = goalValue - } -} - -@available(iOS 16.2, *) -public struct WorkoutActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - private func formatTime(_ interval: TimeInterval) -> String { - let hours = Int(interval) / 3600 - let minutes = (Int(interval) % 3600) / 60 - let seconds = Int(interval) % 60 - - if hours > 0 { - return String(format: "%d:%02d:%02d", hours, minutes, seconds) - } - return String(format: "%d:%02d", minutes, seconds) - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Label(context.attributes.workoutType.rawValue, systemImage: context.attributes.workoutType.icon) - .font(.headline) - - Spacer() - - if context.state.isActive { - HStack(spacing: 4) { - Circle() - .fill(.green) - .frame(width: 8, height: 8) - Text("Active") - .font(.caption) - .foregroundStyle(.green) - } - } - } - - HStack(spacing: 20) { - MetricView(value: formatTime(context.state.elapsedTime), label: "Time", icon: "clock") - MetricView(value: String(format: "%.2f", context.state.distance), label: "km", icon: "location") - MetricView(value: "\(context.state.calories)", label: "Cal", icon: "flame.fill") - MetricView(value: "\(context.state.heartRate)", label: "BPM", icon: "heart.fill") - } - - HStack { - Text("Pace: \(context.state.pace)") - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - Text("Goal: \(String(format: "%.1f", context.attributes.goalValue)) km") - .font(.caption) - .foregroundStyle(.secondary) - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: context.attributes.workoutType.icon) - .foregroundStyle(.green) - } - - public var compactTrailing: some View { - Text(formatTime(context.state.elapsedTime)) - .font(.caption) - .monospacedDigit() - } - - struct MetricView: View { - let value: String - let label: String - let icon: String - - var body: some View { - VStack(spacing: 2) { - Image(systemName: icon) - .font(.caption2) - .foregroundStyle(.secondary) - Text(value) - .font(.caption) - .fontWeight(.semibold) - Text(label) - .font(.system(size: 9)) - .foregroundStyle(.secondary) - } - } - } -} - -// MARK: - 6. Music Now Playing Activity - -/// Music player Live Activity -@available(iOS 16.1, *) -public struct NowPlayingAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var trackName: String - public var artistName: String - public var albumName: String - public var progress: Double - public var duration: TimeInterval - public var isPlaying: Bool - - public init(trackName: String, artistName: String, albumName: String, progress: Double, duration: TimeInterval, isPlaying: Bool) { - self.trackName = trackName - self.artistName = artistName - self.albumName = albumName - self.progress = progress - self.duration = duration - self.isPlaying = isPlaying - } - } - - public let appName: String - - public init(appName: String = "Music") { - self.appName = appName - } -} - -@available(iOS 16.2, *) -public struct NowPlayingActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - HStack(spacing: 12) { - RoundedRectangle(cornerRadius: 8) - .fill(LinearGradient(colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing)) - .frame(width: 60, height: 60) - .overlay { - Image(systemName: "music.note") - .font(.title2) - .foregroundStyle(.white) - } - - VStack(alignment: .leading, spacing: 4) { - Text(context.state.trackName) - .font(.headline) - .lineLimit(1) - - Text(context.state.artistName) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - - GeometryReader { geo in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 2) - .fill(Color.secondary.opacity(0.3)) - - RoundedRectangle(cornerRadius: 2) - .fill(Color.pink) - .frame(width: geo.size.width * context.state.progress) - } - } - .frame(height: 4) - } - - VStack(spacing: 8) { - Image(systemName: context.state.isPlaying ? "pause.fill" : "play.fill") - .font(.title3) - - Image(systemName: "forward.fill") - .font(.caption) - .foregroundStyle(.secondary) - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: context.state.isPlaying ? "waveform" : "pause.fill") - .foregroundStyle(.pink) - } - - public var compactTrailing: some View { - Text(context.state.trackName) - .font(.caption2) - .lineLimit(1) - } -} - -// MARK: - 7. Flight Tracking Activity - -/// Flight status tracking -@available(iOS 16.1, *) -public struct FlightAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: FlightStatus - public var gate: String - public var departureTime: Date - public var arrivalTime: Date - public var delay: Int? // minutes - public var progress: Double - - public init(status: FlightStatus, gate: String, departureTime: Date, arrivalTime: Date, delay: Int? = nil, progress: Double = 0) { - self.status = status - self.gate = gate - self.departureTime = departureTime - self.arrivalTime = arrivalTime - self.delay = delay - self.progress = progress - } - } - - public enum FlightStatus: String, Codable { - case scheduled = "Scheduled" - case boarding = "Boarding" - case departed = "Departed" - case inFlight = "In Flight" - case landing = "Landing" - case arrived = "Arrived" - case delayed = "Delayed" - case cancelled = "Cancelled" - - public var color: Color { - switch self { - case .scheduled: return .blue - case .boarding: return .orange - case .departed, .inFlight: return .green - case .landing: return .purple - case .arrived: return .green - case .delayed: return .orange - case .cancelled: return .red - } - } - } - - public let flightNumber: String - public let airline: String - public let departure: String - public let arrival: String - public let departureCode: String - public let arrivalCode: String - - public init(flightNumber: String, airline: String, departure: String, arrival: String, departureCode: String, arrivalCode: String) { - self.flightNumber = flightNumber - self.airline = airline - self.departure = departure - self.arrival = arrival - self.departureCode = departureCode - self.arrivalCode = arrivalCode - } -} - -@available(iOS 16.2, *) -public struct FlightActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Text("\(context.attributes.airline) \(context.attributes.flightNumber)") - .font(.headline) - - Spacer() - - Text(context.state.status.rawValue) - .font(.caption) - .fontWeight(.semibold) - .foregroundStyle(context.state.status.color) - } - - HStack { - VStack(alignment: .leading) { - Text(context.attributes.departureCode) - .font(.title2) - .fontWeight(.bold) - Text(context.state.departureTime, format: .dateTime.hour().minute()) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - VStack { - Image(systemName: "airplane") - .rotationEffect(.degrees(90)) - - GeometryReader { geo in - ZStack(alignment: .leading) { - Rectangle() - .fill(Color.secondary.opacity(0.3)) - .frame(height: 2) - - Rectangle() - .fill(context.state.status.color) - .frame(width: geo.size.width * context.state.progress, height: 2) - } - } - .frame(height: 2) - } - .frame(maxWidth: 100) - - Spacer() - - VStack(alignment: .trailing) { - Text(context.attributes.arrivalCode) - .font(.title2) - .fontWeight(.bold) - Text(context.state.arrivalTime, format: .dateTime.hour().minute()) - .font(.caption) - .foregroundStyle(.secondary) - } - } - - HStack { - Label("Gate \(context.state.gate)", systemImage: "door.right.hand.open") - .font(.caption) - - Spacer() - - if let delay = context.state.delay, delay > 0 { - Text("+\(delay) min delay") - .font(.caption) - .foregroundStyle(.orange) - } - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "airplane") - .foregroundStyle(.blue) - } - - public var compactTrailing: some View { - Text(context.state.status.rawValue) - .font(.caption2) - } -} - -// MARK: - 8. Parking Timer Activity - -/// Parking meter countdown -@available(iOS 16.1, *) -public struct ParkingAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var expiresAt: Date - public var isExpired: Bool - public var remainingTime: TimeInterval - - public init(expiresAt: Date, isExpired: Bool = false, remainingTime: TimeInterval) { - self.expiresAt = expiresAt - self.isExpired = isExpired - self.remainingTime = remainingTime - } - } - - public let location: String - public let spotNumber: String - public let rate: String - - public init(location: String, spotNumber: String, rate: String) { - self.location = location - self.spotNumber = spotNumber - self.rate = rate - } -} - -@available(iOS 16.2, *) -public struct ParkingActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - var urgencyColor: Color { - let remaining = context.state.remainingTime - if remaining <= 0 || context.state.isExpired { return .red } - if remaining < 600 { return .orange } // Less than 10 min - return .green - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - Label(context.attributes.location, systemImage: "car.fill") - .font(.headline) - - Spacer() - - Text("Spot \(context.attributes.spotNumber)") - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.secondary.opacity(0.2)) - .clipShape(Capsule()) - } - - if context.state.isExpired { - Text("EXPIRED") - .font(.title) - .fontWeight(.bold) - .foregroundStyle(.red) - } else { - Text(context.state.expiresAt, style: .timer) - .font(.system(size: 36, weight: .thin, design: .rounded)) - .monospacedDigit() - .foregroundStyle(urgencyColor) - } - - HStack { - Text(context.attributes.rate) - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - if !context.state.isExpired { - Text("Expires at \(context.state.expiresAt, format: .dateTime.hour().minute())") - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "car.fill") - .foregroundStyle(urgencyColor) - } - - public var compactTrailing: some View { - Text(context.state.expiresAt, style: .timer) - .font(.caption2) - .monospacedDigit() - } -} - -// MARK: - 9. Order Status Activity - -/// E-commerce order tracking -@available(iOS 16.1, *) -public struct OrderAttributes: ActivityAttributes { - public struct ContentState: Codable, Hashable { - public var status: OrderStatus - public var currentStep: Int - public var estimatedDelivery: Date - public var trackingNumber: String? - - public init(status: OrderStatus, currentStep: Int, estimatedDelivery: Date, trackingNumber: String? = nil) { - self.status = status - self.currentStep = currentStep - self.estimatedDelivery = estimatedDelivery - self.trackingNumber = trackingNumber - } - } - - public enum OrderStatus: String, Codable { - case confirmed = "Confirmed" - case processing = "Processing" - case shipped = "Shipped" - case outForDelivery = "Out for Delivery" - case delivered = "Delivered" - - public var icon: String { - switch self { - case .confirmed: return "checkmark.circle" - case .processing: return "box.truck.badge.clock" - case .shipped: return "shippingbox" - case .outForDelivery: return "truck.box" - case .delivered: return "house.fill" - } - } - } - - public let orderNumber: String - public let storeName: String - public let totalSteps: Int - - public init(orderNumber: String, storeName: String, totalSteps: Int = 5) { - self.orderNumber = orderNumber - self.storeName = storeName - self.totalSteps = totalSteps - } -} - -@available(iOS 16.2, *) -public struct OrderActivityView: View { - let context: ActivityViewContext - - public init(context: ActivityViewContext) { - self.context = context - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading) { - Text(context.attributes.storeName) - .font(.headline) - Text("Order #\(context.attributes.orderNumber)") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: context.state.status.icon) - .font(.title2) - .foregroundStyle(.blue) - } - - // Progress steps - HStack(spacing: 0) { - ForEach(0.. - - public init(context: ActivityViewContext) { - self.context = context - } - - private func formatDuration(_ interval: TimeInterval) -> String { - let minutes = Int(interval) / 60 - let seconds = Int(interval) % 60 - return String(format: "%d:%02d", minutes, seconds) - } - - public var body: some View { - VStack(spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(context.attributes.title) - .font(.headline) - .lineLimit(1) - - Text("by \(context.attributes.organizer)") - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - VStack(alignment: .trailing) { - if context.state.hasRecording { - Label("Recording", systemImage: "record.circle") - .font(.caption2) - .foregroundStyle(.red) - } - - HStack { - Image(systemName: "person.2.fill") - Text("\(context.state.participantCount)") - } - .font(.caption) - .foregroundStyle(.secondary) - } - } - - HStack { - Label(context.state.status.rawValue, systemImage: "video.fill") - .font(.caption) - .foregroundStyle(.green) - - Spacer() - - Text(formatDuration(context.state.elapsedTime)) - .font(.headline) - .monospacedDigit() - } - } - .padding() - } - - public var compactLeading: some View { - Image(systemName: "video.fill") - .foregroundStyle(.green) - } - - public var compactTrailing: some View { - Text(formatDuration(context.state.elapsedTime)) - .font(.caption2) - .monospacedDigit() - } -} - -// MARK: - Live Activity Manager - -/// Comprehensive Live Activity manager -@available(iOS 16.1, *) -public final class LiveActivityManager { - public static let shared = LiveActivityManager() - - private init() {} - - /// Check if Live Activities are enabled - public var areActivitiesEnabled: Bool { - ActivityAuthorizationInfo().areActivitiesEnabled - } - - /// Start a new Live Activity - public func startActivity( - attributes: T, - contentState: T.ContentState, - pushType: PushType? = nil - ) throws -> Activity { - let content = ActivityContent(state: contentState, staleDate: nil) - return try Activity.request(attributes: attributes, content: content, pushType: pushType) - } - - /// Update an existing Live Activity - public func updateActivity( - _ activity: Activity, - with state: T.ContentState, - alertConfiguration: AlertConfiguration? = nil - ) async { - let content = ActivityContent(state: state, staleDate: nil) - await activity.update(content, alertConfiguration: alertConfiguration) - } - - /// End a Live Activity - public func endActivity( - _ activity: Activity, - with state: T.ContentState? = nil, - dismissalPolicy: ActivityUIDismissalPolicy = .default - ) async { - if let state = state { - let content = ActivityContent(state: state, staleDate: nil) - await activity.end(content, dismissalPolicy: dismissalPolicy) - } else { - await activity.end(nil, dismissalPolicy: dismissalPolicy) - } - } - - /// Get all active activities of a specific type - public func getActivities(of type: T.Type) -> [Activity] { - Activity.activities - } - - /// End all activities of a specific type - public func endAllActivities(of type: T.Type) async { - for activity in Activity.activities { - await activity.end(nil, dismissalPolicy: .immediate) - } - } -} diff --git a/Sources/LiveData/LiveDataIntegration.swift b/Sources/LiveData/LiveDataIntegration.swift deleted file mode 100644 index 1433873..0000000 --- a/Sources/LiveData/LiveDataIntegration.swift +++ /dev/null @@ -1,590 +0,0 @@ -import Foundation -import Combine -import Network - -/// Live Data Integration Framework -/// Provides real-time data updates for widgets with WebSocket support -@available(iOS 16.0, *) -public class LiveDataIntegration: ObservableObject { - - // MARK: - Properties - - /// Shared instance for singleton access - public static let shared = LiveDataIntegration() - - /// Active data sources - private var dataSources: [String: WidgetDataSource] = [:] - - /// WebSocket connections - private var webSocketConnections: [String: NWConnection] = [:] - - /// Data publishers - private var dataPublishers: [String: PassthroughSubject] = [:] - - /// Combine cancellables - private var cancellables = Set() - - /// Network monitor - private let networkMonitor = NWPathMonitor() - - /// Connection queue - private let connectionQueue = DispatchQueue(label: "com.widgetkit.connection") - - /// Analytics service - private let analyticsService = WidgetAnalyticsService() - - // MARK: - Initialization - - private init() { - setupNetworkMonitoring() - setupDataSources() - } - - // MARK: - Public Methods - - /// Register a data source for live updates - /// - Parameters: - /// - identifier: Data source identifier - /// - dataSource: Data source implementation - public func registerDataSource(_ identifier: String, dataSource: WidgetDataSource) { - dataSources[identifier] = dataSource - dataPublishers[identifier] = PassthroughSubject() - - setupDataSourceUpdates(for: identifier, dataSource: dataSource) - analyticsService.logDataSourceRegistration(identifier) - } - - /// Connect to WebSocket for real-time data - /// - Parameters: - /// - identifier: Connection identifier - /// - url: WebSocket URL - /// - headers: Optional headers - public func connectWebSocket(identifier: String, url: URL, headers: [String: String] = [:]) { - let endpoint = NWEndpoint.url(url) - let connection = NWConnection(to: endpoint, using: .ws) - - connection.stateUpdateHandler = { [weak self] state in - self?.handleConnectionStateChange(identifier: identifier, state: state) - } - - webSocketConnections[identifier] = connection - connection.start(queue: connectionQueue) - - analyticsService.logWebSocketConnection(identifier: identifier, url: url.absoluteString) - } - - /// Disconnect WebSocket - /// - Parameter identifier: Connection identifier - public func disconnectWebSocket(identifier: String) { - webSocketConnections[identifier]?.cancel() - webSocketConnections.removeValue(forKey: identifier) - - analyticsService.logWebSocketDisconnection(identifier: identifier) - } - - /// Get data publisher for specific source - /// - Parameter identifier: Data source identifier - /// - Returns: Publisher for data updates - public func getDataPublisher(for identifier: String) -> AnyPublisher? { - return dataPublishers[identifier]?.eraseToAnyPublisher() - } - - /// Send data to specific widget - /// - Parameters: - /// - widgetId: Widget identifier - /// - data: Data to send - public func sendData(to widgetId: String, data: WidgetData) { - NotificationCenter.default.post( - name: .widgetDataReceived, - object: nil, - userInfo: ["widgetId": widgetId, "data": data] - ) - - analyticsService.logDataSent(widgetId: widgetId, dataType: data.type.rawValue) - } - - /// Configure data source settings - /// - Parameters: - /// - identifier: Data source identifier - /// - settings: Data source settings - public func configureDataSource(_ identifier: String, settings: DataSourceSettings) { - dataSources[identifier]?.configure(settings) - analyticsService.logDataSourceConfiguration(identifier: identifier) - } - - /// Get connection status - /// - Parameter identifier: Connection identifier - /// - Returns: Connection status - public func getConnectionStatus(for identifier: String) -> ConnectionStatus { - guard let connection = webSocketConnections[identifier] else { - return .disconnected - } - - switch connection.state { - case .ready: - return .connected - case .failed, .cancelled: - return .disconnected - case .preparing, .setup: - return .connecting - default: - return .unknown - } - } - - // MARK: - Private Methods - - private func setupNetworkMonitoring() { - networkMonitor.pathUpdateHandler = { [weak self] path in - self?.handleNetworkPathUpdate(path) - } - networkMonitor.start(queue: DispatchQueue.global()) - } - - private func setupDataSources() { - // Register default data sources - registerDefaultDataSources() - } - - private func registerDefaultDataSources() { - let defaultSources: [WidgetDataSource] = [ - WeatherDataSource(), - CalendarDataSource(), - FitnessDataSource(), - NewsDataSource(), - SocialMediaDataSource(), - ProductivityDataSource(), - EntertainmentDataSource(), - FinanceDataSource(), - HealthDataSource(), - TravelDataSource() - ] - - defaultSources.forEach { dataSource in - registerDataSource(dataSource.identifier, dataSource: dataSource) - } - } - - private func setupDataSourceUpdates(for identifier: String, dataSource: WidgetDataSource) { - dataSource.dataPublisher - .sink { [weak self] data in - self?.handleDataSourceUpdate(identifier: identifier, data: data) - } - .store(in: &cancellables) - } - - private func handleDataSourceUpdate(identifier: String, data: WidgetData) { - dataPublishers[identifier]?.send(data) - - // Notify widgets using this data source - NotificationCenter.default.post( - name: .dataSourceUpdated, - object: nil, - userInfo: ["identifier": identifier, "data": data] - ) - - analyticsService.logDataSourceUpdate(identifier: identifier, dataType: data.type.rawValue) - } - - private func handleConnectionStateChange(identifier: String, state: NWConnection.State) { - DispatchQueue.main.async { [weak self] in - switch state { - case .ready: - self?.analyticsService.logConnectionEstablished(identifier: identifier) - self?.startReceivingData(for: identifier) - case .failed(let error): - self?.analyticsService.logConnectionFailed(identifier: identifier, error: error) - self?.handleConnectionFailure(identifier: identifier, error: error) - case .cancelled: - self?.analyticsService.logConnectionCancelled(identifier: identifier) - default: - break - } - } - } - - private func handleNetworkPathUpdate(_ path: NWPath) { - if path.status == .satisfied { - // Network is available, reconnect if needed - reconnectIfNeeded() - } else { - // Network is unavailable - analyticsService.logNetworkUnavailable() - } - } - - private func startReceivingData(for identifier: String) { - guard let connection = webSocketConnections[identifier] else { return } - - connection.receiveMessage { [weak self] content, context, isComplete, error in - if let error = error { - self?.analyticsService.logDataReceiveError(identifier: identifier, error: error) - return - } - - if let content = content { - self?.handleReceivedData(identifier: identifier, data: content) - } - - // Continue receiving - if !isComplete { - self?.startReceivingData(for: identifier) - } - } - } - - private func handleReceivedData(identifier: String, data: Data) { - do { - let widgetData = try JSONDecoder().decode(WidgetData.self, from: data) - handleDataSourceUpdate(identifier: identifier, data: widgetData) - } catch { - analyticsService.logDataDecodeError(identifier: identifier, error: error) - } - } - - private func handleConnectionFailure(identifier: String, error: NWError) { - // Implement retry logic - DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in - self?.retryConnection(identifier: identifier) - } - } - - private func retryConnection(identifier: String) { - // Implement retry logic for failed connections - analyticsService.logConnectionRetry(identifier: identifier) - } - - private func reconnectIfNeeded() { - // Reconnect any disconnected WebSocket connections - for (identifier, connection) in webSocketConnections { - if connection.state == .failed || connection.state == .cancelled { - // Reconnect logic - analyticsService.logReconnectionAttempt(identifier: identifier) - } - } - } -} - -// MARK: - Supporting Types - -/// Data source protocol -@available(iOS 16.0, *) -public protocol WidgetDataSource { - var identifier: String { get } - var dataPublisher: AnyPublisher { get } - - func refresh() - func configure(_ settings: DataSourceSettings) -} - -/// Data source settings -public struct DataSourceSettings { - public let refreshInterval: TimeInterval - public let enableCaching: Bool - public let maxRetries: Int - public let timeout: TimeInterval - - public init(refreshInterval: TimeInterval = 30.0, enableCaching: Bool = true, maxRetries: Int = 3, timeout: TimeInterval = 10.0) { - self.refreshInterval = refreshInterval - self.enableCaching = enableCaching - self.maxRetries = maxRetries - self.timeout = timeout - } -} - -/// Connection status -public enum ConnectionStatus { - case connected - case connecting - case disconnected - case unknown -} - -// MARK: - Default Data Sources - -@available(iOS 16.0, *) -public class WeatherDataSource: WidgetDataSource { - public let identifier = "weather_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate weather API call - let weatherData = WidgetData( - id: "weather_current", - type: .weather, - content: [ - "temperature": 22, - "condition": "sunny", - "humidity": 65, - "windSpeed": 12 - ] - ) - dataSubject.send(weatherData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class CalendarDataSource: WidgetDataSource { - public let identifier = "calendar_events" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate calendar API call - let calendarData = WidgetData( - id: "calendar_events", - type: .calendar, - content: [ - "events": [ - ["title": "Team Meeting", "time": "10:00 AM", "location": "Conference Room"], - ["title": "Lunch", "time": "12:30 PM", "location": "Cafeteria"] - ] - ] - ) - dataSubject.send(calendarData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class FitnessDataSource: WidgetDataSource { - public let identifier = "health_kit" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate HealthKit data - let fitnessData = WidgetData( - id: "fitness_current", - type: .fitness, - content: [ - "steps": 8420, - "calories": 320, - "activeMinutes": 45, - "workouts": 2 - ] - ) - dataSubject.send(fitnessData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class NewsDataSource: WidgetDataSource { - public let identifier = "news_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate news API call - let newsData = WidgetData( - id: "news_latest", - type: .news, - content: [ - "articles": [ - ["title": "Tech Innovation", "summary": "Latest developments in AI technology", "category": "Technology"], - ["title": "Market Update", "summary": "Stock market trends and analysis", "category": "Finance"] - ] - ] - ) - dataSubject.send(newsData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class SocialMediaDataSource: WidgetDataSource { - public let identifier = "social_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate social media API call - let socialData = WidgetData( - id: "social_updates", - type: .social, - content: [ - "updates": [ - ["platform": "Twitter", "content": "New product launch!", "likes": 150], - ["platform": "Instagram", "content": "Beautiful sunset", "likes": 89] - ] - ] - ) - dataSubject.send(socialData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class ProductivityDataSource: WidgetDataSource { - public let identifier = "productivity_data" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate productivity data - let productivityData = WidgetData( - id: "productivity_current", - type: .productivity, - content: [ - "tasksCompleted": 8, - "tasksRemaining": 3, - "focusTime": 240, - "productivityScore": 85 - ] - ) - dataSubject.send(productivityData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class EntertainmentDataSource: WidgetDataSource { - public let identifier = "entertainment_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate entertainment API call - let entertainmentData = WidgetData( - id: "entertainment_latest", - type: .entertainment, - content: [ - "items": [ - ["title": "New Movie", "type": "Movie", "rating": 4.5], - ["title": "Popular Series", "type": "TV Show", "rating": 4.8] - ] - ] - ) - dataSubject.send(entertainmentData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class FinanceDataSource: WidgetDataSource { - public let identifier = "finance_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate finance API call - let financeData = WidgetData( - id: "finance_current", - type: .finance, - content: [ - "portfolioValue": 125000, - "dailyChange": 1250, - "topStock": "AAPL", - "marketTrend": "Bullish" - ] - ) - dataSubject.send(financeData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class HealthDataSource: WidgetDataSource { - public let identifier = "health_kit" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate health data - let healthData = WidgetData( - id: "health_current", - type: .health, - content: [ - "heartRate": 72, - "sleepHours": 7.5, - "waterIntake": 8, - "mood": "Good" - ] - ) - dataSubject.send(healthData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -@available(iOS 16.0, *) -public class TravelDataSource: WidgetDataSource { - public let identifier = "travel_api" - private let dataSubject = PassthroughSubject() - public var dataPublisher: AnyPublisher { - return dataSubject.eraseToAnyPublisher() - } - - public func refresh() { - // Simulate travel API call - let travelData = WidgetData( - id: "travel_current", - type: .travel, - content: [ - "destination": "Paris", - "departureTime": "10:30 AM", - "flightNumber": "AF123", - "weather": "Sunny" - ] - ) - dataSubject.send(travelData) - } - - public func configure(_ settings: DataSourceSettings) { - // Configure data source settings - } -} - -// MARK: - Notification Names - -extension Notification.Name { - static let widgetDataReceived = Notification.Name("widgetDataReceived") - static let dataSourceUpdated = Notification.Name("dataSourceUpdated") -} \ No newline at end of file diff --git a/Sources/LockScreen/LockScreenWidgets.swift b/Sources/LockScreen/LockScreenWidgets.swift deleted file mode 100644 index 1abcbbf..0000000 --- a/Sources/LockScreen/LockScreenWidgets.swift +++ /dev/null @@ -1,796 +0,0 @@ -// LockScreenWidgets.swift -// iOS-Widget-Development-Kit -// -// iOS 16+ Lock Screen Widget Templates -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit - -// MARK: - Lock Screen Widget Families - -/// Supported Lock Screen widget families -@available(iOS 16.0, *) -public enum LockScreenWidgetFamily: CaseIterable { - case accessoryCircular - case accessoryRectangular - case accessoryInline - - public var widgetFamily: WidgetFamily { - switch self { - case .accessoryCircular: - return .accessoryCircular - case .accessoryRectangular: - return .accessoryRectangular - case .accessoryInline: - return .accessoryInline - } - } -} - -// MARK: - Lock Screen Widget Protocol - -/// Protocol for Lock Screen widgets -@available(iOS 16.0, *) -public protocol LockScreenWidgetProtocol { - associatedtype CircularView: View - associatedtype RectangularView: View - associatedtype InlineView: View - - @ViewBuilder func makeCircular() -> CircularView - @ViewBuilder func makeRectangular() -> RectangularView - @ViewBuilder func makeInline() -> InlineView -} - -// MARK: - 1. Circular Progress Widget - -/// Circular progress indicator widget -@available(iOS 16.0, *) -public struct CircularProgressWidget: View { - let progress: Double - let icon: String - let title: String - let accentColor: Color - - public init( - progress: Double, - icon: String = "star.fill", - title: String = "", - accentColor: Color = .blue - ) { - self.progress = progress - self.icon = icon - self.title = title - self.accentColor = accentColor - } - - public var body: some View { - Gauge(value: progress, in: 0...1) { - Image(systemName: icon) - } currentValueLabel: { - Text("\(Int(progress * 100))%") - .font(.system(size: 12, weight: .semibold)) - } - .gaugeStyle(.accessoryCircularCapacity) - .tint(accentColor) - } -} - -// MARK: - 2. Circular Icon Widget - -/// Simple icon display widget -@available(iOS 16.0, *) -public struct CircularIconWidget: View { - let icon: String - let value: String? - let backgroundColor: Color - - public init( - icon: String, - value: String? = nil, - backgroundColor: Color = .blue - ) { - self.icon = icon - self.value = value - self.backgroundColor = backgroundColor - } - - public var body: some View { - ZStack { - AccessoryWidgetBackground() - - VStack(spacing: 2) { - Image(systemName: icon) - .font(.title3) - - if let value = value { - Text(value) - .font(.system(size: 10, weight: .semibold)) - } - } - } - } -} - -// MARK: - 3. Circular Ring Widget - -/// Activity ring style widget -@available(iOS 16.0, *) -public struct CircularRingWidget: View { - let innerProgress: Double - let outerProgress: Double - let innerColor: Color - let outerColor: Color - let centerIcon: String - - public init( - innerProgress: Double, - outerProgress: Double, - innerColor: Color = .cyan, - outerColor: Color = .green, - centerIcon: String = "figure.walk" - ) { - self.innerProgress = innerProgress - self.outerProgress = outerProgress - self.innerColor = innerColor - self.outerColor = outerColor - self.centerIcon = centerIcon - } - - public var body: some View { - ZStack { - // Outer ring - Circle() - .stroke(outerColor.opacity(0.3), lineWidth: 4) - Circle() - .trim(from: 0, to: outerProgress) - .stroke(outerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - - // Inner ring - Circle() - .stroke(innerColor.opacity(0.3), lineWidth: 4) - .padding(6) - Circle() - .trim(from: 0, to: innerProgress) - .stroke(innerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .padding(6) - - // Center icon - Image(systemName: centerIcon) - .font(.body) - } - } -} - -// MARK: - 4. Rectangular Text Widget - -/// Multi-line text widget -@available(iOS 16.0, *) -public struct RectangularTextWidget: View { - let title: String - let subtitle: String - let detail: String? - let icon: String - - public init( - title: String, - subtitle: String, - detail: String? = nil, - icon: String = "star" - ) { - self.title = title - self.subtitle = subtitle - self.detail = detail - self.icon = icon - } - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.headline) - .widgetAccentable() - - Text(subtitle) - .font(.caption) - - if let detail = detail { - Text(detail) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - - Spacer() - - Image(systemName: icon) - .font(.title2) - .widgetAccentable() - } - } -} - -// MARK: - 5. Rectangular Stats Widget - -/// Statistics display widget -@available(iOS 16.0, *) -public struct RectangularStatsWidget: View { - let stats: [(String, String)] - let title: String? - - public init(stats: [(String, String)], title: String? = nil) { - self.stats = stats - self.title = title - } - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - if let title = title { - Text(title) - .font(.caption2) - .foregroundStyle(.secondary) - } - - HStack { - ForEach(stats.prefix(3), id: \.0) { stat in - VStack(spacing: 2) { - Text(stat.1) - .font(.headline) - .widgetAccentable() - Text(stat.0) - .font(.system(size: 8)) - .foregroundStyle(.secondary) - } - .frame(maxWidth: .infinity) - } - } - } - } -} - -// MARK: - 6. Rectangular Progress Widget - -/// Progress bar widget -@available(iOS 16.0, *) -public struct RectangularProgressWidget: View { - let title: String - let progress: Double - let currentValue: String - let maxValue: String - let icon: String - - public init( - title: String, - progress: Double, - currentValue: String, - maxValue: String, - icon: String = "chart.bar.fill" - ) { - self.title = title - self.progress = progress - self.currentValue = currentValue - self.maxValue = maxValue - self.icon = icon - } - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Image(systemName: icon) - .widgetAccentable() - - Text(title) - .font(.caption) - - Spacer() - - Text("\(currentValue)/\(maxValue)") - .font(.caption2) - .foregroundStyle(.secondary) - } - - ProgressView(value: progress, total: 1.0) - .tint(.primary) - } - } -} - -// MARK: - 7. Rectangular Calendar Widget - -/// Upcoming events widget -@available(iOS 16.0, *) -public struct RectangularCalendarWidget: View { - let events: [(String, Date)] - - public init(events: [(String, Date)]) { - self.events = events - } - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - ForEach(events.prefix(2), id: \.0) { event in - HStack { - Circle() - .fill(Color.primary) - .frame(width: 6, height: 6) - - Text(event.0) - .font(.caption) - .lineLimit(1) - - Spacer() - - Text(event.1, style: .time) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - } - } -} - -// MARK: - 8. Rectangular Weather Widget - -/// Weather conditions widget -@available(iOS 16.0, *) -public struct RectangularWeatherWidget: View { - let temperature: Int - let high: Int - let low: Int - let condition: String - let location: String - - public init( - temperature: Int, - high: Int, - low: Int, - condition: String = "sun.max.fill", - location: String - ) { - self.temperature = temperature - self.high = high - self.low = low - self.condition = condition - self.location = location - } - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(location) - .font(.caption2) - .foregroundStyle(.secondary) - - Text("\(temperature)°") - .font(.title) - .fontWeight(.medium) - .widgetAccentable() - - HStack(spacing: 4) { - Text("H:\(high)°") - Text("L:\(low)°") - } - .font(.caption2) - .foregroundStyle(.secondary) - } - - Spacer() - - Image(systemName: condition) - .font(.largeTitle) - .widgetAccentable() - } - } -} - -// MARK: - 9. Inline Text Widget - -/// Simple inline text widget -@available(iOS 16.0, *) -public struct InlineTextWidget: View { - let icon: String? - let text: String - - public init(icon: String? = nil, text: String) { - self.icon = icon - self.text = text - } - - public var body: some View { - if let icon = icon { - Label(text, systemImage: icon) - } else { - Text(text) - } - } -} - -// MARK: - 10. Inline Stats Widget - -/// Inline statistics widget -@available(iOS 16.0, *) -public struct InlineStatsWidget: View { - let label: String - let value: String - let icon: String - - public init(label: String, value: String, icon: String) { - self.label = label - self.value = value - self.icon = icon - } - - public var body: some View { - Label { - Text("\(label): \(value)") - } icon: { - Image(systemName: icon) - } - } -} - -// MARK: - 11. Battery Widget (All Families) - -/// Battery level widget supporting all Lock Screen families -@available(iOS 16.0, *) -public struct LockScreenBatteryWidget: View { - let level: Double - let isCharging: Bool - let deviceName: String? - let family: WidgetFamily - - public init( - level: Double, - isCharging: Bool = false, - deviceName: String? = nil, - family: WidgetFamily = .accessoryCircular - ) { - self.level = level - self.isCharging = isCharging - self.deviceName = deviceName - self.family = family - } - - private var batteryIcon: String { - if isCharging { return "battery.100.bolt" } - if level < 0.1 { return "battery.0" } - if level < 0.25 { return "battery.25" } - if level < 0.5 { return "battery.50" } - if level < 0.75 { return "battery.75" } - return "battery.100" - } - - public var body: some View { - switch family { - case .accessoryCircular: - Gauge(value: level, in: 0...1) { - Image(systemName: batteryIcon) - } currentValueLabel: { - Text("\(Int(level * 100))") - .font(.system(size: 14, weight: .semibold)) - } - .gaugeStyle(.accessoryCircularCapacity) - .tint(level < 0.2 ? .red : (level < 0.5 ? .orange : .green)) - - case .accessoryRectangular: - HStack { - VStack(alignment: .leading, spacing: 2) { - if let name = deviceName { - Text(name) - .font(.caption2) - .foregroundStyle(.secondary) - } - - Text("\(Int(level * 100))%") - .font(.title2) - .fontWeight(.semibold) - .widgetAccentable() - - if isCharging { - Label("Charging", systemImage: "bolt.fill") - .font(.caption2) - .foregroundStyle(.green) - } - } - - Spacer() - - Image(systemName: batteryIcon) - .font(.title) - .widgetAccentable() - } - - case .accessoryInline: - Label("\(Int(level * 100))%\(isCharging ? " ⚡" : "")", systemImage: batteryIcon) - - default: - EmptyView() - } - } -} - -// MARK: - 12. Timer Widget (All Families) - -/// Countdown timer widget supporting all families -@available(iOS 16.0, *) -public struct LockScreenTimerWidget: View { - let endTime: Date - let title: String? - let family: WidgetFamily - - public init( - endTime: Date, - title: String? = nil, - family: WidgetFamily = .accessoryCircular - ) { - self.endTime = endTime - self.title = title - self.family = family - } - - private var progress: Double { - // Calculate progress (assuming 1 hour max for visualization) - let remaining = endTime.timeIntervalSince(Date()) - return max(0, min(1, remaining / 3600)) - } - - public var body: some View { - switch family { - case .accessoryCircular: - ZStack { - ProgressView(timerInterval: Date()...endTime) { - Image(systemName: "timer") - } - .progressViewStyle(.circular) - } - - case .accessoryRectangular: - VStack(alignment: .leading, spacing: 2) { - if let title = title { - Text(title) - .font(.caption2) - .foregroundStyle(.secondary) - } - - Text(endTime, style: .timer) - .font(.title) - .fontWeight(.medium) - .monospacedDigit() - .widgetAccentable() - - ProgressView(value: progress) - .tint(.primary) - } - - case .accessoryInline: - Label { - Text(endTime, style: .timer) - } icon: { - Image(systemName: "timer") - } - - default: - EmptyView() - } - } -} - -// MARK: - 13. Fitness Widget (All Families) - -/// Fitness activity widget -@available(iOS 16.0, *) -public struct LockScreenFitnessWidget: View { - let moveProgress: Double - let exerciseProgress: Double - let standProgress: Double - let family: WidgetFamily - - public init( - moveProgress: Double, - exerciseProgress: Double, - standProgress: Double, - family: WidgetFamily = .accessoryCircular - ) { - self.moveProgress = moveProgress - self.exerciseProgress = exerciseProgress - self.standProgress = standProgress - self.family = family - } - - public var body: some View { - switch family { - case .accessoryCircular: - ZStack { - // Move ring (outer) - Circle() - .stroke(Color.red.opacity(0.3), lineWidth: 4) - Circle() - .trim(from: 0, to: min(moveProgress, 1)) - .stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - - // Exercise ring (middle) - Circle() - .stroke(Color.green.opacity(0.3), lineWidth: 4) - .padding(5) - Circle() - .trim(from: 0, to: min(exerciseProgress, 1)) - .stroke(Color.green, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .padding(5) - - // Stand ring (inner) - Circle() - .stroke(Color.cyan.opacity(0.3), lineWidth: 4) - .padding(10) - Circle() - .trim(from: 0, to: min(standProgress, 1)) - .stroke(Color.cyan, style: StrokeStyle(lineWidth: 4, lineCap: .round)) - .rotationEffect(.degrees(-90)) - .padding(10) - } - - case .accessoryRectangular: - HStack { - VStack(alignment: .leading, spacing: 4) { - FitnessRow(label: "Move", progress: moveProgress, color: .red) - FitnessRow(label: "Exercise", progress: exerciseProgress, color: .green) - FitnessRow(label: "Stand", progress: standProgress, color: .cyan) - } - } - - case .accessoryInline: - Label("\(Int(moveProgress * 100))% · \(Int(exerciseProgress * 100))% · \(Int(standProgress * 100))%", systemImage: "figure.walk") - - default: - EmptyView() - } - } - - struct FitnessRow: View { - let label: String - let progress: Double - let color: Color - - var body: some View { - HStack(spacing: 4) { - Circle() - .fill(color) - .frame(width: 6, height: 6) - - Text(label) - .font(.system(size: 9)) - - Gauge(value: progress, in: 0...1) { - EmptyView() - } - .gaugeStyle(.linearCapacity) - .tint(color) - - Text("\(Int(progress * 100))%") - .font(.system(size: 9)) - .foregroundStyle(.secondary) - } - } - } -} - -// MARK: - 14. Countdown Widget (All Families) - -/// Event countdown widget -@available(iOS 16.0, *) -public struct LockScreenCountdownWidget: View { - let targetDate: Date - let eventName: String - let family: WidgetFamily - - public init( - targetDate: Date, - eventName: String, - family: WidgetFamily = .accessoryCircular - ) { - self.targetDate = targetDate - self.eventName = eventName - self.family = family - } - - private var daysRemaining: Int { - let interval = targetDate.timeIntervalSince(Date()) - return max(0, Int(interval / 86400)) - } - - public var body: some View { - switch family { - case .accessoryCircular: - VStack(spacing: 2) { - Text("\(daysRemaining)") - .font(.system(size: 24, weight: .bold)) - Text("DAYS") - .font(.system(size: 8, weight: .medium)) - } - .background(AccessoryWidgetBackground()) - - case .accessoryRectangular: - VStack(alignment: .leading, spacing: 2) { - Text(eventName) - .font(.caption) - .foregroundStyle(.secondary) - - HStack(alignment: .firstTextBaseline, spacing: 4) { - Text("\(daysRemaining)") - .font(.title) - .fontWeight(.bold) - .widgetAccentable() - Text("days remaining") - .font(.caption) - } - - Text(targetDate, format: .dateTime.month().day().year()) - .font(.caption2) - .foregroundStyle(.secondary) - } - - case .accessoryInline: - Text("\(daysRemaining) days until \(eventName)") - - default: - EmptyView() - } - } -} - -// MARK: - 15. Quick Action Widget - -/// Quick action shortcut widget -@available(iOS 16.0, *) -public struct LockScreenQuickActionWidget: View { - let icon: String - let label: String - let family: WidgetFamily - - public init( - icon: String, - label: String, - family: WidgetFamily = .accessoryCircular - ) { - self.icon = icon - self.label = label - self.family = family - } - - public var body: some View { - switch family { - case .accessoryCircular: - ZStack { - AccessoryWidgetBackground() - - Image(systemName: icon) - .font(.title2) - } - - case .accessoryRectangular: - HStack { - Image(systemName: icon) - .font(.title) - .widgetAccentable() - - Text(label) - .font(.headline) - - Spacer() - - Image(systemName: "chevron.right") - .font(.caption) - .foregroundStyle(.secondary) - } - - case .accessoryInline: - Label(label, systemImage: icon) - - default: - EmptyView() - } - } -} diff --git a/Sources/Network/NetworkDataProvider.swift b/Sources/Network/NetworkDataProvider.swift deleted file mode 100644 index 393ae4e..0000000 --- a/Sources/Network/NetworkDataProvider.swift +++ /dev/null @@ -1,473 +0,0 @@ -// NetworkDataProvider.swift -// iOS-Widget-Development-Kit -// -// Network Data Support for Widgets -// Created by Muhittin Camdali - -import Foundation -import Combine - -// MARK: - Network Configuration - -/// Network request configuration -public struct NetworkConfiguration { - public let timeout: TimeInterval - public let cachePolicy: URLRequest.CachePolicy - public let retryCount: Int - public let retryDelay: TimeInterval - - public init( - timeout: TimeInterval = 30, - cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad, - retryCount: Int = 2, - retryDelay: TimeInterval = 1 - ) { - self.timeout = timeout - self.cachePolicy = cachePolicy - self.retryCount = retryCount - self.retryDelay = retryDelay - } - - public static let `default` = NetworkConfiguration() - public static let fastRefresh = NetworkConfiguration(timeout: 10, cachePolicy: .reloadIgnoringLocalCacheData, retryCount: 1) - public static let cachePriority = NetworkConfiguration(timeout: 60, cachePolicy: .returnCacheDataDontLoad, retryCount: 0) -} - -// MARK: - Network Error - -/// Widget-specific network errors -public enum WidgetNetworkError: Error, LocalizedError { - case invalidURL - case noData - case decodingFailed(Error) - case networkError(Error) - case timeout - case serverError(Int) - case rateLimited - case cancelled - - public var errorDescription: String? { - switch self { - case .invalidURL: - return "Invalid URL provided" - case .noData: - return "No data received from server" - case .decodingFailed(let error): - return "Failed to decode data: \(error.localizedDescription)" - case .networkError(let error): - return "Network error: \(error.localizedDescription)" - case .timeout: - return "Request timed out" - case .serverError(let code): - return "Server returned error code: \(code)" - case .rateLimited: - return "Rate limited - try again later" - case .cancelled: - return "Request was cancelled" - } - } -} - -// MARK: - Network Data Provider - -/// Primary network data provider for widgets -public final class NetworkDataProvider { - public static let shared = NetworkDataProvider() - - private let session: URLSession - private let cache = URLCache( - memoryCapacity: 10 * 1024 * 1024, // 10 MB - diskCapacity: 50 * 1024 * 1024, // 50 MB - directory: nil - ) - - private init() { - let configuration = URLSessionConfiguration.default - configuration.timeoutIntervalForRequest = 30 - configuration.timeoutIntervalForResource = 60 - configuration.waitsForConnectivity = true - configuration.urlCache = cache - - session = URLSession(configuration: configuration) - } - - /// Fetch decodable data from URL - public func fetch( - _ type: T.Type, - from url: URL, - configuration: NetworkConfiguration = .default - ) async throws -> T { - var lastError: Error = WidgetNetworkError.networkError(NSError(domain: "", code: -1)) - - for attempt in 0...configuration.retryCount { - if attempt > 0 { - try await Task.sleep(nanoseconds: UInt64(configuration.retryDelay * 1_000_000_000)) - } - - do { - var request = URLRequest(url: url) - request.timeoutInterval = configuration.timeout - request.cachePolicy = configuration.cachePolicy - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse else { - throw WidgetNetworkError.networkError(NSError(domain: "Invalid response", code: -1)) - } - - switch httpResponse.statusCode { - case 200...299: - do { - let decoded = try JSONDecoder().decode(type, from: data) - return decoded - } catch { - throw WidgetNetworkError.decodingFailed(error) - } - - case 429: - throw WidgetNetworkError.rateLimited - - case 500...599: - throw WidgetNetworkError.serverError(httpResponse.statusCode) - - default: - throw WidgetNetworkError.serverError(httpResponse.statusCode) - } - - } catch is CancellationError { - throw WidgetNetworkError.cancelled - } catch let error as WidgetNetworkError { - lastError = error - // Don't retry certain errors - if case .decodingFailed = error { throw error } - if case .rateLimited = error { throw error } - } catch { - lastError = WidgetNetworkError.networkError(error) - } - } - - throw lastError - } - - /// Fetch with custom decoder - public func fetch( - _ type: T.Type, - from url: URL, - decoder: JSONDecoder, - configuration: NetworkConfiguration = .default - ) async throws -> T { - var request = URLRequest(url: url) - request.timeoutInterval = configuration.timeout - request.cachePolicy = configuration.cachePolicy - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw WidgetNetworkError.serverError((response as? HTTPURLResponse)?.statusCode ?? -1) - } - - return try decoder.decode(type, from: data) - } - - /// Fetch raw data - public func fetchData(from url: URL, configuration: NetworkConfiguration = .default) async throws -> Data { - var request = URLRequest(url: url) - request.timeoutInterval = configuration.timeout - request.cachePolicy = configuration.cachePolicy - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw WidgetNetworkError.serverError((response as? HTTPURLResponse)?.statusCode ?? -1) - } - - return data - } - - /// Fetch with POST request - public func post( - _ type: T.Type, - to url: URL, - body: U, - configuration: NetworkConfiguration = .default - ) async throws -> T { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.timeoutInterval = configuration.timeout - request.httpBody = try JSONEncoder().encode(body) - - let (data, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw WidgetNetworkError.serverError((response as? HTTPURLResponse)?.statusCode ?? -1) - } - - return try JSONDecoder().decode(type, from: data) - } - - /// Clear URL cache - public func clearCache() { - cache.removeAllCachedResponses() - } -} - -// MARK: - Cached Network Provider - -/// Network provider with built-in caching layer -public final class CachedNetworkProvider { - private let networkProvider = NetworkDataProvider.shared - private let cacheManager = TimelineCacheManager.shared - private let cacheKey: String - private let cacheDuration: TimeInterval - - public init(cacheKey: String, cacheDuration: TimeInterval = 300) { - self.cacheKey = cacheKey - self.cacheDuration = cacheDuration - } - - /// Fetch data with cache fallback - public func fetch(from url: URL, forceRefresh: Bool = false) async throws -> T { - // Check cache first (unless force refresh) - if !forceRefresh, let cached: T = cacheManager.retrieve(T.self, forKey: cacheKey) { - return cached - } - - // Fetch from network - let data = try await networkProvider.fetch(T.self, from: url) - - // Store in cache - cacheManager.store(data, forKey: cacheKey, expiration: cacheDuration) - - return data - } - - /// Get cached data only - public func getCached() -> T? { - cacheManager.retrieve(T.self, forKey: cacheKey) - } - - /// Clear this provider's cache - public func clearCache() { - cacheManager.clear(forKey: cacheKey) - } -} - -// MARK: - API Endpoint Builder - -/// Builder for constructing API endpoints -public final class APIEndpointBuilder { - private var baseURL: String - private var path: String = "" - private var queryItems: [URLQueryItem] = [] - private var headers: [String: String] = [:] - - public init(baseURL: String) { - self.baseURL = baseURL - } - - @discardableResult - public func path(_ path: String) -> Self { - self.path = path - return self - } - - @discardableResult - public func appendPath(_ segment: String) -> Self { - self.path += "/\(segment)" - return self - } - - @discardableResult - public func query(_ name: String, value: String) -> Self { - queryItems.append(URLQueryItem(name: name, value: value)) - return self - } - - @discardableResult - public func query(_ items: [String: String]) -> Self { - for (name, value) in items { - queryItems.append(URLQueryItem(name: name, value: value)) - } - return self - } - - @discardableResult - public func header(_ name: String, value: String) -> Self { - headers[name] = value - return self - } - - public func build() -> URL? { - var components = URLComponents(string: baseURL + path) - if !queryItems.isEmpty { - components?.queryItems = queryItems - } - return components?.url - } - - public func buildRequest() -> URLRequest? { - guard let url = build() else { return nil } - var request = URLRequest(url: url) - for (name, value) in headers { - request.setValue(value, forHTTPHeaderField: name) - } - return request - } -} - -// MARK: - Background Refresh Support - -/// Background refresh coordinator for widgets -public final class BackgroundRefreshCoordinator { - public static let shared = BackgroundRefreshCoordinator() - - private let userDefaults = UserDefaults(suiteName: "group.widget.background") ?? .standard - - private init() {} - - /// Schedule background refresh - public func scheduleBackgroundRefresh(identifier: String, afterInterval: TimeInterval) { - let nextRefresh = Date().addingTimeInterval(afterInterval) - userDefaults.set(nextRefresh, forKey: "bg_refresh_\(identifier)") - } - - /// Check if background refresh is due - public func isRefreshDue(identifier: String) -> Bool { - guard let scheduledDate = userDefaults.object(forKey: "bg_refresh_\(identifier)") as? Date else { - return true - } - return Date() >= scheduledDate - } - - /// Mark refresh as completed - public func markRefreshCompleted(identifier: String) { - userDefaults.removeObject(forKey: "bg_refresh_\(identifier)") - } - - /// Get last refresh date - public func lastRefreshDate(identifier: String) -> Date? { - userDefaults.object(forKey: "bg_last_refresh_\(identifier)") as? Date - } - - /// Record refresh timestamp - public func recordRefresh(identifier: String) { - userDefaults.set(Date(), forKey: "bg_last_refresh_\(identifier)") - } -} - -// MARK: - Data Sync Manager - -/// Manages data synchronization between app and widget -public final class WidgetDataSyncManager { - public static let shared = WidgetDataSyncManager() - - private let sharedDefaults: UserDefaults - private let encoder = JSONEncoder() - private let decoder = JSONDecoder() - - private init() { - sharedDefaults = UserDefaults(suiteName: "group.widget.data") ?? .standard - } - - /// Share data with widget - public func shareData(_ data: T, key: String) throws { - let encoded = try encoder.encode(data) - sharedDefaults.set(encoded, forKey: key) - - // Notify widgets of data change - WidgetReloadManager.shared.reloadAllWidgets() - } - - /// Retrieve shared data - public func retrieveData(_ type: T.Type, key: String) throws -> T? { - guard let data = sharedDefaults.data(forKey: key) else { - return nil - } - return try decoder.decode(type, from: data) - } - - /// Remove shared data - public func removeData(key: String) { - sharedDefaults.removeObject(forKey: key) - } - - /// Observe data changes - public func observeChanges( - _ type: T.Type, - key: String, - handler: @escaping (T?) -> Void - ) -> NSObjectProtocol { - NotificationCenter.default.addObserver( - forName: UserDefaults.didChangeNotification, - object: sharedDefaults, - queue: .main - ) { [weak self] _ in - let data = try? self?.retrieveData(type, key: key) - handler(data) - } - } -} - -// MARK: - Polling Manager - -/// Manages periodic data polling for widgets -public final class PollingManager { - public static let shared = PollingManager() - - private var pollingTasks: [String: Task] = [:] - private let queue = DispatchQueue(label: "polling.manager.queue") - - private init() {} - - /// Start polling for data - public func startPolling( - identifier: String, - url: URL, - type: T.Type, - interval: TimeInterval, - handler: @escaping (Result) -> Void - ) { - stopPolling(identifier: identifier) - - let task = Task { - while !Task.isCancelled { - do { - let data = try await NetworkDataProvider.shared.fetch(type, from: url) - await MainActor.run { - handler(.success(data)) - } - } catch { - await MainActor.run { - handler(.failure(error)) - } - } - - try? await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) - } - } - - queue.sync { - pollingTasks[identifier] = task - } - } - - /// Stop polling - public func stopPolling(identifier: String) { - queue.sync { - pollingTasks[identifier]?.cancel() - pollingTasks.removeValue(forKey: identifier) - } - } - - /// Stop all polling - public func stopAllPolling() { - queue.sync { - pollingTasks.values.forEach { $0.cancel() } - pollingTasks.removeAll() - } - } -} diff --git a/Sources/StandBy/StandByWidgets.swift b/Sources/StandBy/StandByWidgets.swift deleted file mode 100644 index 496c80b..0000000 --- a/Sources/StandBy/StandByWidgets.swift +++ /dev/null @@ -1,851 +0,0 @@ -// StandByWidgets.swift -// iOS-Widget-Development-Kit -// -// iOS 17+ StandBy Mode Widget Support -// Created by Muhittin Camdali - -import SwiftUI -import WidgetKit - -// MARK: - StandBy Mode Configuration - -/// Configuration for StandBy mode widgets -@available(iOS 17.0, *) -public struct StandByConfiguration { - public let supportsStandBy: Bool - public let fullColorMode: Bool - public let nightModeColor: Color - public let alwaysOnDisplay: Bool - - public init( - supportsStandBy: Bool = true, - fullColorMode: Bool = true, - nightModeColor: Color = .red, - alwaysOnDisplay: Bool = true - ) { - self.supportsStandBy = supportsStandBy - self.fullColorMode = fullColorMode - self.nightModeColor = nightModeColor - self.alwaysOnDisplay = alwaysOnDisplay - } -} - -// MARK: - StandBy View Modifier - -/// View modifier for StandBy mode optimizations -@available(iOS 17.0, *) -public struct StandByOptimizedModifier: ViewModifier { - @Environment(\.showsWidgetContainerBackground) var showsBackground - @Environment(\.widgetRenderingMode) var renderingMode - - let nightModeColor: Color - - public init(nightModeColor: Color = .red) { - self.nightModeColor = nightModeColor - } - - public func body(content: Content) -> some View { - content - .foregroundStyle(renderingMode == .fullColor ? .primary : nightModeColor) - .containerBackground(for: .widget) { - if showsBackground { - Color.clear - } - } - } -} - -@available(iOS 17.0, *) -extension View { - public func standByOptimized(nightModeColor: Color = .red) -> some View { - modifier(StandByOptimizedModifier(nightModeColor: nightModeColor)) - } -} - -// MARK: - StandBy Clock Widget - -/// Large clock widget optimized for StandBy mode -@available(iOS 17.0, *) -public struct StandByClockWidget: View { - let date: Date - let style: ClockStyle - let showDate: Bool - let accentColor: Color - - public enum ClockStyle { - case digital - case analog - case flipClock - case minimal - } - - public init( - date: Date = Date(), - style: ClockStyle = .digital, - showDate: Bool = true, - accentColor: Color = .white - ) { - self.date = date - self.style = style - self.showDate = showDate - self.accentColor = accentColor - } - - public var body: some View { - VStack(spacing: 16) { - switch style { - case .digital: - DigitalClockView(date: date, accentColor: accentColor) - case .analog: - AnalogClockView(date: date, accentColor: accentColor) - case .flipClock: - FlipClockView(date: date, accentColor: accentColor) - case .minimal: - MinimalClockView(date: date, accentColor: accentColor) - } - - if showDate { - Text(date, format: .dateTime.weekday(.wide).month().day()) - .font(.title3) - .foregroundStyle(accentColor.opacity(0.8)) - } - } - .containerBackground(.black.gradient, for: .widget) - } - - struct DigitalClockView: View { - let date: Date - let accentColor: Color - - var body: some View { - Text(date, format: .dateTime.hour().minute()) - .font(.system(size: 96, weight: .thin, design: .rounded)) - .monospacedDigit() - .foregroundStyle(accentColor) - } - } - - struct AnalogClockView: View { - let date: Date - let accentColor: Color - - var hourAngle: Double { - let hour = Calendar.current.component(.hour, from: date) % 12 - let minute = Calendar.current.component(.minute, from: date) - return Double(hour) * 30 + Double(minute) * 0.5 - } - - var minuteAngle: Double { - let minute = Calendar.current.component(.minute, from: date) - return Double(minute) * 6 - } - - var body: some View { - GeometryReader { geo in - let size = min(geo.size.width, geo.size.height) - - ZStack { - // Clock face - Circle() - .stroke(accentColor.opacity(0.3), lineWidth: 3) - - // Hour markers - ForEach(0..<12) { hour in - Rectangle() - .fill(accentColor) - .frame(width: hour % 3 == 0 ? 3 : 1, height: hour % 3 == 0 ? 15 : 8) - .offset(y: -size/2 + 20) - .rotationEffect(.degrees(Double(hour) * 30)) - } - - // Hour hand - RoundedRectangle(cornerRadius: 3) - .fill(accentColor) - .frame(width: 6, height: size * 0.25) - .offset(y: -size * 0.125) - .rotationEffect(.degrees(hourAngle)) - - // Minute hand - RoundedRectangle(cornerRadius: 2) - .fill(accentColor) - .frame(width: 4, height: size * 0.35) - .offset(y: -size * 0.175) - .rotationEffect(.degrees(minuteAngle)) - - // Center dot - Circle() - .fill(accentColor) - .frame(width: 12, height: 12) - } - .frame(width: size, height: size) - .position(x: geo.size.width / 2, y: geo.size.height / 2) - } - .aspectRatio(1, contentMode: .fit) - } - } - - struct FlipClockView: View { - let date: Date - let accentColor: Color - - var hour: String { - let formatter = DateFormatter() - formatter.dateFormat = "HH" - return formatter.string(from: date) - } - - var minute: String { - let formatter = DateFormatter() - formatter.dateFormat = "mm" - return formatter.string(from: date) - } - - var body: some View { - HStack(spacing: 8) { - FlipDigitView(digit: hour, accentColor: accentColor) - - VStack(spacing: 12) { - Circle() - .fill(accentColor) - .frame(width: 10, height: 10) - Circle() - .fill(accentColor) - .frame(width: 10, height: 10) - } - - FlipDigitView(digit: minute, accentColor: accentColor) - } - } - } - - struct FlipDigitView: View { - let digit: String - let accentColor: Color - - var body: some View { - Text(digit) - .font(.system(size: 80, weight: .bold, design: .rounded)) - .monospacedDigit() - .foregroundStyle(accentColor) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(accentColor.opacity(0.15)) - ) - } - } - - struct MinimalClockView: View { - let date: Date - let accentColor: Color - - var body: some View { - Text(date, format: .dateTime.hour().minute()) - .font(.system(size: 120, weight: .ultraLight, design: .rounded)) - .monospacedDigit() - .foregroundStyle(accentColor) - } - } -} - -// MARK: - StandBy Photo Widget - -/// Photo display widget for StandBy mode -@available(iOS 17.0, *) -public struct StandByPhotoWidget: View { - let caption: String? - let date: Date? - let style: PhotoStyle - - public enum PhotoStyle { - case full - case polaroid - case rounded - case minimal - } - - public init( - caption: String? = nil, - date: Date? = nil, - style: PhotoStyle = .full - ) { - self.caption = caption - self.date = date - self.style = style - } - - public var body: some View { - GeometryReader { geo in - VStack(spacing: 0) { - // Photo placeholder - Rectangle() - .fill( - LinearGradient( - colors: [.gray.opacity(0.3), .gray.opacity(0.5)], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .overlay { - Image(systemName: "photo.fill") - .font(.system(size: 48)) - .foregroundStyle(.white.opacity(0.5)) - } - .frame(maxHeight: style == .polaroid ? geo.size.height * 0.8 : .infinity) - - if style == .polaroid { - VStack(spacing: 4) { - if let caption = caption { - Text(caption) - .font(.caption) - .foregroundStyle(.black) - } - if let date = date { - Text(date, format: .dateTime.month().day().year()) - .font(.caption2) - .foregroundStyle(.gray) - } - } - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(.white) - } - } - .clipShape(RoundedRectangle(cornerRadius: style == .rounded ? 20 : (style == .minimal ? 0 : 8))) - .shadow(radius: style == .polaroid ? 10 : 0) - } - .containerBackground(.black, for: .widget) - } -} - -// MARK: - StandBy Weather Widget - -/// Weather widget optimized for StandBy mode -@available(iOS 17.0, *) -public struct StandByWeatherWidget: View { - let temperature: Int - let condition: WeatherCondition - let location: String - let high: Int - let low: Int - let hourlyForecast: [HourlyForecast] - - public enum WeatherCondition: String { - case sunny = "sun.max.fill" - case cloudy = "cloud.fill" - case rainy = "cloud.rain.fill" - case stormy = "cloud.bolt.rain.fill" - case snowy = "cloud.snow.fill" - case partlyCloudy = "cloud.sun.fill" - - var color: Color { - switch self { - case .sunny: return .yellow - case .cloudy: return .gray - case .rainy: return .blue - case .stormy: return .purple - case .snowy: return .cyan - case .partlyCloudy: return .orange - } - } - } - - public struct HourlyForecast: Identifiable { - public let id = UUID() - public let hour: String - public let temp: Int - public let condition: WeatherCondition - - public init(hour: String, temp: Int, condition: WeatherCondition) { - self.hour = hour - self.temp = temp - self.condition = condition - } - } - - public init( - temperature: Int, - condition: WeatherCondition, - location: String, - high: Int, - low: Int, - hourlyForecast: [HourlyForecast] = [] - ) { - self.temperature = temperature - self.condition = condition - self.location = location - self.high = high - self.low = low - self.hourlyForecast = hourlyForecast - } - - public var body: some View { - VStack(spacing: 20) { - // Location - Text(location) - .font(.title3) - .foregroundStyle(.white.opacity(0.8)) - - // Main temperature - HStack(alignment: .top, spacing: 16) { - Image(systemName: condition.rawValue) - .font(.system(size: 64)) - .foregroundStyle(condition.color) - - VStack(alignment: .leading) { - Text("\(temperature)°") - .font(.system(size: 72, weight: .thin)) - .foregroundStyle(.white) - - HStack { - Text("H:\(high)°") - Text("L:\(low)°") - } - .font(.title3) - .foregroundStyle(.white.opacity(0.7)) - } - } - - // Hourly forecast - if !hourlyForecast.isEmpty { - HStack(spacing: 16) { - ForEach(hourlyForecast.prefix(5)) { forecast in - VStack(spacing: 8) { - Text(forecast.hour) - .font(.caption) - Image(systemName: forecast.condition.rawValue) - .font(.title3) - .foregroundStyle(forecast.condition.color) - Text("\(forecast.temp)°") - .font(.caption) - .fontWeight(.medium) - } - .foregroundStyle(.white.opacity(0.8)) - } - } - } - } - .padding() - .containerBackground(.black.gradient, for: .widget) - } -} - -// MARK: - StandBy Calendar Widget - -/// Calendar widget for StandBy mode -@available(iOS 17.0, *) -public struct StandByCalendarWidget: View { - let date: Date - let events: [CalendarEvent] - let accentColor: Color - - public struct CalendarEvent: Identifiable { - public let id = UUID() - public let title: String - public let time: Date - public let color: Color - - public init(title: String, time: Date, color: Color = .blue) { - self.title = title - self.time = time - self.color = color - } - } - - public init( - date: Date = Date(), - events: [CalendarEvent] = [], - accentColor: Color = .blue - ) { - self.date = date - self.events = events - self.accentColor = accentColor - } - - private var calendar: Calendar { Calendar.current } - - private var monthDays: [[Int?]] { - let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: date))! - let range = calendar.range(of: .day, in: .month, for: startOfMonth)! - let firstWeekday = calendar.component(.weekday, from: startOfMonth) - - var days: [[Int?]] = [] - var week: [Int?] = [] - - // Leading empty days - for _ in 1.. 0 else { return (0, 0, 0, 0) } - - let days = Int(interval) / 86400 - let hours = (Int(interval) % 86400) / 3600 - let minutes = (Int(interval) % 3600) / 60 - let seconds = Int(interval) % 60 - return (days, hours, minutes, seconds) - } - - public var body: some View { - VStack(spacing: 24) { - Text(eventName) - .font(.title2) - .foregroundStyle(.white.opacity(0.8)) - - HStack(spacing: 16) { - CountdownUnit(value: timeComponents.days, label: "DAYS", color: accentColor) - CountdownUnit(value: timeComponents.hours, label: "HOURS", color: accentColor) - CountdownUnit(value: timeComponents.minutes, label: "MIN", color: accentColor) - CountdownUnit(value: timeComponents.seconds, label: "SEC", color: accentColor) - } - - Text(targetDate, format: .dateTime.month().day().year()) - .font(.caption) - .foregroundStyle(.white.opacity(0.5)) - } - .padding() - .containerBackground(.black.gradient, for: .widget) - } - - struct CountdownUnit: View { - let value: Int - let label: String - let color: Color - - var body: some View { - VStack(spacing: 8) { - Text(String(format: "%02d", value)) - .font(.system(size: 48, weight: .bold, design: .rounded)) - .monospacedDigit() - .foregroundStyle(color) - - Text(label) - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.white.opacity(0.6)) - } - } - } -} diff --git a/Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift b/Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift new file mode 100644 index 0000000..725622e --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Charts/ChartCategorical.swift @@ -0,0 +1,257 @@ +import SwiftUI +import Charts + +// MARK: - Widget Bar Chart + +/// Compact bar chart for widgets +public struct WidgetBarChart: View { + let data: [ChartDataPoint] + let accentColor: Color + let showLabels: Bool + let showValues: Bool + + public init( + data: [ChartDataPoint], + accentColor: Color = .blue, + showLabels: Bool = true, + showValues: Bool = false + ) { + self.data = data + self.accentColor = accentColor + self.showLabels = showLabels + self.showValues = showValues + } + + public var body: some View { + Chart(data) { point in + BarMark( + x: .value("Category", point.label), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor.gradient) + .cornerRadius(4) + + if showValues { + RuleMark(y: .value("Value", point.value)) + .lineStyle(StrokeStyle(lineWidth: 0)) + .annotation(position: .top) { + Text("\(Int(point.value))") + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + } + } + .chartXAxis(showLabels ? .automatic : .hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Widget Pie Chart + +/// Compact pie chart for widgets +public struct WidgetPieChart: View { + let data: [ChartDataPoint] + let colors: [Color] + let showLabels: Bool + let innerRadiusRatio: Double + + public init( + data: [ChartDataPoint], + colors: [Color]? = nil, + showLabels: Bool = false, + innerRadiusRatio: Double = 0 + ) { + self.data = data + let palette: [Color] = [.blue, .green, .orange, .red, .purple, .pink, .cyan, .yellow] + self.colors = (colors?.isEmpty == false ? colors : nil) ?? palette + self.showLabels = showLabels + self.innerRadiusRatio = innerRadiusRatio + } + + public var body: some View { + Chart(data) { point in + SectorMark( + angle: .value("Value", point.value), + innerRadius: .ratio(innerRadiusRatio), + angularInset: 1 + ) + .foregroundStyle(by: .value("Category", point.label)) + .cornerRadius(4) + } + .chartLegend(showLabels ? .visible : .hidden) + .chartForegroundStyleScale(domain: data.map(\.label), range: colors) + } +} + +// MARK: - Widget Progress Ring Chart + +/// Circular progress chart +public struct WidgetProgressRing: View { + let progress: Double + let total: Double + let colors: [Color] + let lineWidth: CGFloat + let showValue: Bool + let valueFormat: String + + public init( + progress: Double, + total: Double = 1.0, + colors: [Color] = [.blue], + lineWidth: CGFloat = 12, + showValue: Bool = true, + valueFormat: String = "%.0f%%" + ) { + self.progress = progress + self.total = total + self.colors = colors.isEmpty ? [.blue] : colors + self.lineWidth = lineWidth + self.showValue = showValue + self.valueFormat = valueFormat + } + + private var normalizedProgress: Double { + WidgetMath.fraction(progress, of: total) + } + + private var baseColor: Color { colors[0] } + + private var gradient: AngularGradient { + AngularGradient( + colors: colors + [baseColor], + center: .center, + startAngle: .degrees(-90), + endAngle: .degrees(270) + ) + } + + public var body: some View { + ZStack { + Circle() + .stroke(baseColor.opacity(0.2), lineWidth: lineWidth) + + Circle() + .trim(from: 0, to: normalizedProgress) + .stroke( + colors.count == 1 ? AnyShapeStyle(baseColor) : AnyShapeStyle(gradient), + style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + + if showValue { + Text(String(format: valueFormat, normalizedProgress * 100)) + .font(.system(size: lineWidth * 1.5, weight: .bold, design: .rounded)) + } + } + } +} + +// MARK: - Horizontal Bar Chart + +/// Horizontal bar chart for comparisons +public struct WidgetHorizontalBarChart: View { + let data: [ChartDataPoint] + let accentColor: Color + let showValues: Bool + let maxValue: Double? + + public init( + data: [ChartDataPoint], + accentColor: Color = .blue, + showValues: Bool = true, + maxValue: Double? = nil + ) { + self.data = data + self.accentColor = accentColor + self.showValues = showValues + self.maxValue = maxValue + } + + private var normalizedMax: Double { + maxValue ?? data.map(\.value).max() ?? 1 + } + + public var body: some View { + VStack(spacing: 8) { + ForEach(data) { point in + HStack(spacing: 8) { + Text(point.label) + .font(.caption) + .frame(width: 60, alignment: .leading) + .lineLimit(1) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4) + .fill(accentColor.opacity(0.2)) + + RoundedRectangle(cornerRadius: 4) + .fill(accentColor.gradient) + .frame(width: geo.size.width * WidgetMath.fraction(point.value, of: normalizedMax)) + } + } + .frame(height: 16) + + if showValues { + Text("\(Int(point.value))") + .font(.caption) + .foregroundStyle(.secondary) + .frame(width: 30, alignment: .trailing) + } + } + } + } + } +} + +// MARK: - Activity Chart + +/// Contribution-style heat map (weeks as columns, days as rows). +public struct WidgetActivityChart: View { + let activities: [[Double]] + let color: Color + let levels: Int + let cellSize: CGFloat + let spacing: CGFloat + + /// - Parameters: + /// - activities: One array per week, each holding the values for its days. + /// - color: Color of the most active cells; lower levels use lower opacity. + /// - levels: Number of discrete intensity steps, including the empty level. + /// - cellSize: Side length of each square cell, in points. + /// - spacing: Gap between cells, in points. + public init( + activities: [[Double]], + color: Color = .green, + levels: Int = 5, + cellSize: CGFloat = 10, + spacing: CGFloat = 2 + ) { + self.activities = activities + self.color = color + self.levels = max(levels, 2) + self.cellSize = cellSize + self.spacing = spacing + } + + public var body: some View { + let maximum = activities.flatMap { $0 }.max() ?? 0 + HStack(spacing: spacing) { + ForEach(activities.indices, id: \.self) { weekIndex in + VStack(spacing: spacing) { + ForEach(activities[weekIndex].indices, id: \.self) { dayIndex in + RoundedRectangle(cornerRadius: 2) + .fill(cellColor(activities[weekIndex][dayIndex], maximum: maximum)) + .frame(width: cellSize, height: cellSize) + } + } + } + } + } + + private func cellColor(_ value: Double, maximum: Double) -> Color { + let level = WidgetMath.intensityLevel(value, maximum: maximum, levels: levels) + guard level > 0 else { return Color.gray.opacity(0.2) } + return color.opacity(Double(level) / Double(levels - 1)) + } +} diff --git a/Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift b/Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift new file mode 100644 index 0000000..5c949ce --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Charts/ChartTimeSeries.swift @@ -0,0 +1,233 @@ +import SwiftUI +import Charts + +// MARK: - Chart Data Types + +/// Generic data point for charts +public struct ChartDataPoint: Identifiable, Hashable, Sendable { + public let id = UUID() + public let label: String + public let value: Double + public let date: Date? + public let category: String? + + public init(label: String, value: Double, date: Date? = nil, category: String? = nil) { + self.label = label + self.value = value + self.date = date + self.category = category + } +} + +/// Time series data point +public struct TimeSeriesDataPoint: Identifiable, Hashable, Sendable { + public let id = UUID() + public let date: Date + public let value: Double + public let series: String? + + public init(date: Date, value: Double, series: String? = nil) { + self.date = date + self.value = value + self.series = series + } +} + +// MARK: - Widget Line Chart + +/// Compact line chart for widgets +public struct WidgetLineChart: View { + let data: [TimeSeriesDataPoint] + let accentColor: Color + let showArea: Bool + let showPoints: Bool + let showXAxis: Bool + + public init( + data: [TimeSeriesDataPoint], + accentColor: Color = .blue, + showArea: Bool = true, + showPoints: Bool = false, + showXAxis: Bool = false + ) { + self.data = data + self.accentColor = accentColor + self.showArea = showArea + self.showPoints = showPoints + self.showXAxis = showXAxis + } + + public var body: some View { + Chart(data) { point in + LineMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor) + .interpolationMethod(.catmullRom) + + if showArea { + AreaMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor.opacity(0.2).gradient) + .interpolationMethod(.catmullRom) + } + + if showPoints { + PointMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(accentColor) + } + } + .chartXAxis(showXAxis ? .automatic : .hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Widget Sparkline + +/// Minimal sparkline chart for widgets +public struct WidgetSparkline: View { + let values: [Double] + let color: Color + let lineWidth: CGFloat + let showGradient: Bool + + public init( + values: [Double], + color: Color = .blue, + lineWidth: CGFloat = 2, + showGradient: Bool = true + ) { + self.values = values + self.color = color + self.lineWidth = lineWidth + self.showGradient = showGradient + } + + public var body: some View { + Chart(Array(values.enumerated()), id: \.offset) { index, value in + LineMark( + x: .value("Index", index), + y: .value("Value", value) + ) + .foregroundStyle(color) + .lineStyle(StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .interpolationMethod(.catmullRom) + + if showGradient { + AreaMark( + x: .value("Index", index), + y: .value("Value", value) + ) + .foregroundStyle( + LinearGradient( + colors: [color.opacity(0.3), color.opacity(0)], + startPoint: .top, + endPoint: .bottom + ) + ) + .interpolationMethod(.catmullRom) + } + } + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Multi-Line Chart + +/// Line chart with multiple series +public struct WidgetMultiLineChart: View { + let series: [String: [TimeSeriesDataPoint]] + let colors: [String: Color] + let showLegend: Bool + + public init( + series: [String: [TimeSeriesDataPoint]], + colors: [String: Color], + showLegend: Bool = true + ) { + self.series = series + self.colors = colors + self.showLegend = showLegend + } + + private var seriesNames: [String] { series.keys.sorted() } + + private var allDataPoints: [TimeSeriesDataPoint] { + seriesNames.flatMap { seriesName in + (series[seriesName] ?? []).map { point in + TimeSeriesDataPoint(date: point.date, value: point.value, series: seriesName) + } + } + } + + public var body: some View { + Chart(allDataPoints) { point in + LineMark( + x: .value("Date", point.date), + y: .value("Value", point.value) + ) + .foregroundStyle(by: .value("Series", point.series ?? "")) + .interpolationMethod(.catmullRom) + } + .chartForegroundStyleScale(domain: seriesNames, range: seriesNames.map { colors[$0] ?? .gray }) + .chartLegend(showLegend ? .visible : .hidden) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } +} + +// MARK: - Stats Chart Widget + +/// Combined stats with chart +public struct WidgetStatsChart: View { + let title: String + let value: String + let change: Double + let sparklineData: [Double] + let accentColor: Color + + public init( + title: String, + value: String, + change: Double, + sparklineData: [Double], + accentColor: Color = .blue + ) { + self.title = title + self.value = value + self.change = change + self.sparklineData = sparklineData + self.accentColor = accentColor + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + + HStack(alignment: .firstTextBaseline) { + Text(value) + .font(.title2) + .fontWeight(.bold) + + HStack(spacing: 2) { + Image(systemName: change >= 0 ? "arrow.up.right" : "arrow.down.right") + Text(String(format: "%.1f%%", abs(change))) + } + .font(.caption) + .foregroundStyle(change >= 0 ? .green : .red) + } + + WidgetSparkline(values: sparklineData, color: accentColor) + .frame(height: 40) + } + } +} diff --git a/Sources/WidgetDevelopmentKit/Configuration/HexColor.swift b/Sources/WidgetDevelopmentKit/Configuration/HexColor.swift new file mode 100644 index 0000000..e4a3c6d --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Configuration/HexColor.swift @@ -0,0 +1,55 @@ +import SwiftUI + +/// An sRGB color parsed from a hex string such as `"#34C759"`, `"0af"` or `"#FF3B3080"`. +public struct HexColor: Hashable, Sendable { + public let red: Double + public let green: Double + public let blue: Double + public let opacity: Double + + /// Accepts 3, 6 or 8 hex digits (`RGB`, `RRGGBB`, `RRGGBBAA`), with or without a + /// leading `#`. Returns `nil` for any other length or a non-hex character. + public init?(_ string: String) { + var hex = string.trimmingCharacters(in: .whitespacesAndNewlines) + if hex.hasPrefix("#") { hex.removeFirst() } + guard hex.allSatisfy(\.isHexDigit) else { return nil } + + switch hex.count { + case 3: + hex = hex.map { "\($0)\($0)" }.joined() + "FF" + case 6: + hex += "FF" + case 8: + break + default: + return nil + } + + guard let value = UInt32(hex, radix: 16) else { return nil } + red = Double((value >> 24) & 0xFF) / 255 + green = Double((value >> 16) & 0xFF) / 255 + blue = Double((value >> 8) & 0xFF) / 255 + opacity = Double(value & 0xFF) / 255 + } + + /// Uppercase `#RRGGBB`, or `#RRGGBBAA` when not fully opaque. + public var hexString: String { + func byte(_ component: Double) -> String { + String(format: "%02X", Int((component * 255).rounded())) + } + let rgb = "#" + byte(red) + byte(green) + byte(blue) + return opacity < 1 ? rgb + byte(opacity) : rgb + } + + public var color: Color { + Color(.sRGB, red: red, green: green, blue: blue, opacity: opacity) + } +} + +extension Color { + /// Creates a color from a hex string; see ``HexColor`` for the accepted formats. + public init?(hex: String) { + guard let parsed = HexColor(hex) else { return nil } + self = parsed.color + } +} diff --git a/Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift b/Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift new file mode 100644 index 0000000..77a7d0c --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Configuration/WidgetConfigurationEntities.swift @@ -0,0 +1,114 @@ +import AppIntents +import SwiftUI + +/// A refresh interval the user can pick when editing a configurable widget. +/// +/// Use it as a parameter of your `WidgetConfigurationIntent`: +/// +/// ```swift +/// struct ConfigurationIntent: WidgetConfigurationIntent { +/// static let title: LocalizedStringResource = "Configure" +/// @Parameter(title: "Refresh") var refresh: RefreshIntervalOption? +/// } +/// ``` +public struct RefreshIntervalOption: AppEntity, Hashable, Sendable { + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Refresh Interval" + public static let defaultQuery = RefreshIntervalQuery() + + /// Interval in minutes. + public let id: Int + public let displayName: String + + public init(minutes: Int, displayName: String) { + self.id = minutes + self.displayName = displayName + } + + public var minutes: Int { id } + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(displayName)") + } + + /// The update policy matching this interval. + public var updatePolicy: WidgetUpdatePolicy { .every(minutes: minutes) } + + public static let all: [RefreshIntervalOption] = [ + RefreshIntervalOption(minutes: 15, displayName: "15 minutes"), + RefreshIntervalOption(minutes: 30, displayName: "30 minutes"), + RefreshIntervalOption(minutes: 60, displayName: "1 hour"), + RefreshIntervalOption(minutes: 240, displayName: "4 hours") + ] +} + +public struct RefreshIntervalQuery: EntityQuery { + public init() {} + + public func entities(for identifiers: [Int]) async throws -> [RefreshIntervalOption] { + RefreshIntervalOption.all.filter { identifiers.contains($0.id) } + } + + public func suggestedEntities() async throws -> [RefreshIntervalOption] { + RefreshIntervalOption.all + } +} + +/// An accent color the user can pick when editing a configurable widget. +public struct AccentColorOption: AppEntity, Hashable, Sendable { + public static let typeDisplayRepresentation: TypeDisplayRepresentation = "Accent Color" + public static let defaultQuery = AccentColorQuery() + + public let id: String + public let displayName: String + public let hex: HexColor + + public init(id: String, displayName: String, hex: HexColor) { + self.id = id + self.displayName = displayName + self.hex = hex + } + + public var color: Color { hex.color } + + public var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(displayName)") + } + + /// Values match the iOS system colors in light mode. + public static let all: [AccentColorOption] = [ + ("blue", "Blue", "#007AFF"), + ("green", "Green", "#34C759"), + ("red", "Red", "#FF3B30"), + ("orange", "Orange", "#FF9500"), + ("purple", "Purple", "#AF52DE"), + ("pink", "Pink", "#FF2D55"), + ("teal", "Teal", "#5AC8FA"), + ("indigo", "Indigo", "#5856D6") + ].compactMap { id, name, hex in + HexColor(hex).map { AccentColorOption(id: id, displayName: name, hex: $0) } + } +} + +public struct AccentColorQuery: EntityQuery { + public init() {} + + public func entities(for identifiers: [String]) async throws -> [AccentColorOption] { + AccentColorOption.all.filter { identifiers.contains($0.id) } + } + + public func suggestedEntities() async throws -> [AccentColorOption] { + AccentColorOption.all + } +} + +/// Lets the App Intents metadata processor find the entities and intents defined in +/// this package. List it in your app's and widget extension's own `AppIntentsPackage`: +/// +/// ```swift +/// struct AppIntentsRoot: AppIntentsPackage { +/// static var includedPackages: [any AppIntentsPackage.Type] { +/// [WidgetDevelopmentKitIntents.self] +/// } +/// } +/// ``` +public struct WidgetDevelopmentKitIntents: AppIntentsPackage {} diff --git a/Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift b/Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift new file mode 100644 index 0000000..a039bd7 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Configuration/WidgetDeepLink.swift @@ -0,0 +1,78 @@ +import Foundation +import SwiftUI + +/// A deep link from a widget into the host app, e.g. `myapp://content/article?id=42`. +/// +/// Widgets can only open the app through URLs (`widgetURL` or `Link`). Building and +/// parsing them with one type keeps both sides in agreement. +/// +/// ```swift +/// let link = WidgetDeepLink(scheme: "myapp", host: "content", path: "article", parameters: ["id": "42"]) +/// Text(title).widgetURL(link.url) +/// +/// // In the app: +/// .onOpenURL { url in +/// guard let link = WidgetDeepLink(url: url, expectedScheme: "myapp") else { return } +/// router.open(link) +/// } +/// ``` +public struct WidgetDeepLink: Hashable, Sendable { + public let scheme: String + public let host: String + /// Path components without slashes, e.g. `["article"]`. + public let pathComponents: [String] + public let parameters: [String: String] + + public init(scheme: String, host: String, path: String = "", parameters: [String: String] = [:]) { + self.scheme = scheme.lowercased() + self.host = host + self.pathComponents = path.split(separator: "/").map(String.init) + self.parameters = parameters + } + + /// Parses `url`. Returns `nil` if it has no host, or if `expectedScheme` is given and differs. + public init?(url: URL, expectedScheme: String? = nil) { + guard + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let scheme = components.scheme?.lowercased(), + let host = components.host, !host.isEmpty + else { return nil } + if let expectedScheme, expectedScheme.lowercased() != scheme { return nil } + + var parameters: [String: String] = [:] + for item in components.queryItems ?? [] { + guard let value = item.value else { continue } + // First occurrence wins, so a repeated key cannot override an earlier one. + if parameters[item.name] == nil { parameters[item.name] = value } + } + + self.scheme = scheme + self.host = host + self.pathComponents = components.path.split(separator: "/").map(String.init) + self.parameters = parameters + } + + /// The path joined with slashes, without a leading slash. + public var path: String { pathComponents.joined(separator: "/") } + + /// The URL, with query items sorted by name so equal links produce identical URLs. + public var url: URL? { + var components = URLComponents() + components.scheme = scheme + components.host = host + components.path = pathComponents.isEmpty ? "" : "/" + path + if !parameters.isEmpty { + components.queryItems = parameters + .sorted { $0.key < $1.key } + .map { URLQueryItem(name: $0.key, value: $0.value) } + } + return components.url + } +} + +extension View { + /// Sets the widget's tap target to `link`; does nothing if the link cannot form a URL. + public func widgetURL(_ link: WidgetDeepLink) -> some View { + widgetURL(link.url) + } +} diff --git a/Sources/WidgetDevelopmentKit/Core/CalendarMath.swift b/Sources/WidgetDevelopmentKit/Core/CalendarMath.swift new file mode 100644 index 0000000..4fb75e7 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Core/CalendarMath.swift @@ -0,0 +1,53 @@ +import Foundation + +/// A month laid out as rows of seven cells, respecting the calendar's first weekday. +public struct MonthGrid: Equatable, Sendable { + /// Each row has exactly seven entries; `nil` marks a cell outside the month. + public let weeks: [[Int?]] + /// Very short weekday symbols ordered to match the grid columns. + public let weekdaySymbols: [String] + + public init(month date: Date, calendar: Calendar = .current) { + let daysPerWeek = 7 + let symbols = calendar.veryShortWeekdaySymbols + let firstIndex = calendar.firstWeekday - 1 + weekdaySymbols = Array(symbols[firstIndex...] + symbols[.. DataType -} - -/// A standard configuration for widgets -public struct WidgetConfiguration: Codable, Equatable, Sendable { - public let identifier: String - public let refreshInterval: TimeInterval - - public init(identifier: String, refreshInterval: TimeInterval = 3600) { - self.identifier = identifier - self.refreshInterval = refreshInterval - } -} diff --git a/Sources/WidgetDevelopmentKit/Core/WidgetFormat.swift b/Sources/WidgetDevelopmentKit/Core/WidgetFormat.swift new file mode 100644 index 0000000..f09aa30 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Core/WidgetFormat.swift @@ -0,0 +1,31 @@ +import Foundation + +/// Locale-independent formatting helpers for small widget labels. +public enum WidgetFormat { + /// Short count such as `"950"`, `"12.3K"` or `"4.5M"`. Negative values keep their sign. + public static func compactNumber(_ number: Int) -> String { + let magnitude = number.magnitude + let sign = number < 0 ? "-" : "" + switch magnitude { + case 1_000_000...: + return sign + String(format: "%.1fM", Double(magnitude) / 1_000_000) + case 1_000...: + return sign + String(format: "%.1fK", Double(magnitude) / 1_000) + default: + return "\(number)" + } + } + + /// Playback position such as `"3:07"` or `"1:02:05"`. Invalid or negative input reads `"0:00"`. + public static func playbackTime(_ interval: TimeInterval) -> String { + guard interval.isFinite, interval > 0 else { return "0:00" } + let totalSeconds = Int(interval) + let hours = totalSeconds / 3_600 + let minutes = totalSeconds % 3_600 / 60 + let seconds = totalSeconds % 60 + if hours > 0 { + return String(format: "%d:%02d:%02d", hours, minutes, seconds) + } + return String(format: "%d:%02d", minutes, seconds) + } +} diff --git a/Sources/WidgetDevelopmentKit/Core/WidgetMath.swift b/Sources/WidgetDevelopmentKit/Core/WidgetMath.swift new file mode 100644 index 0000000..4fbea3a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Core/WidgetMath.swift @@ -0,0 +1,69 @@ +import Foundation + +/// Pure helpers shared by the widget views. +/// +/// Widget views receive values straight from app data, so progress values can be +/// negative, larger than one, `NaN`, or come with a zero total. Keeping the +/// arithmetic here makes those cases testable on every platform. +public enum WidgetMath { + /// Clamps a progress value into `0...1`. `NaN` and infinities map to `0`. + public static func clampedFraction(_ value: Double) -> Double { + guard value.isFinite else { return 0 } + return min(max(value, 0), 1) + } + + /// Returns `value / total` clamped into `0...1`, or `0` when `total` is not positive. + public static func fraction(_ value: Double, of total: Double) -> Double { + guard total > 0, total.isFinite else { return 0 } + return clampedFraction(value / total) + } + + /// Whole-number percentage (`0...100`) of a fraction, rounded down. + public static func percent(_ fraction: Double) -> Int { + Int(clampedFraction(fraction) * 100) + } + + /// Percentage string such as `"42%"`. + public static func percentString(_ fraction: Double) -> String { + "\(percent(fraction))%" + } + + /// SF Symbol name for a battery level in `0...1`. + public static func batterySymbolName(level: Double, isCharging: Bool) -> String { + if isCharging { return "battery.100.bolt" } + switch clampedFraction(level) { + case ..<0.1: return "battery.0" + case ..<0.25: return "battery.25" + case ..<0.5: return "battery.50" + case ..<0.75: return "battery.75" + default: return "battery.100" + } + } + + /// Builds a date range for timer views. `ClosedRange` traps when the bounds are + /// reversed, so an `end` earlier than `start` collapses to an empty interval. + public static func timerInterval(start: Date, end: Date) -> ClosedRange { + start...max(start, end) + } + + /// Number of calendar days from `date` until `target`; `0` once the target has passed. + public static func daysRemaining( + until target: Date, + from date: Date, + calendar: Calendar = .current + ) -> Int { + let start = calendar.startOfDay(for: date) + let end = calendar.startOfDay(for: target) + let days = calendar.dateComponents([.day], from: start, to: end).day ?? 0 + return max(0, days) + } + + /// Discrete intensity level (`0.. Int { + guard levels > 1, value > 0, maximum > 0, value.isFinite else { return 0 } + let fraction = min(value / maximum, 1) + let level = Int((fraction * Double(levels - 1)).rounded(.up)) + return min(max(level, 1), levels - 1) + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift b/Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift new file mode 100644 index 0000000..813dfb2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/InteractiveControlWidgets.swift @@ -0,0 +1,212 @@ +import AppIntents +import SwiftUI +import WidgetKit + +// MARK: - Interactive Button Styles + +public struct InteractiveWidgetButtonStyle: ButtonStyle { + let backgroundColor: Color + let foregroundColor: Color + let cornerRadius: CGFloat + + public init( + backgroundColor: Color = .blue, + foregroundColor: Color = .white, + cornerRadius: CGFloat = 8 + ) { + self.backgroundColor = backgroundColor + self.foregroundColor = foregroundColor + self.cornerRadius = cornerRadius + } + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding(.horizontal, 12) + .padding(.vertical, 8) + .background(backgroundColor.opacity(configuration.isPressed ? 0.8 : 1)) + .foregroundStyle(foregroundColor) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .scaleEffect(configuration.isPressed ? 0.95 : 1) + .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) + } +} + +// MARK: - Interactive Toggle Widget + +/// Interactive toggle widget template +public struct InteractiveToggleWidget: View { + let title: String + let itemId: String + let isOn: Bool + let onIcon: String + let offIcon: String + let onColor: Color + let offColor: Color + + public init( + title: String, + itemId: String, + isOn: Bool, + onIcon: String = "checkmark.circle.fill", + offIcon: String = "circle", + onColor: Color = .green, + offColor: Color = .gray + ) { + self.title = title + self.itemId = itemId + self.isOn = isOn + self.onIcon = onIcon + self.offIcon = offIcon + self.onColor = onColor + self.offColor = offColor + } + + public var body: some View { + Button(intent: ToggleIntent(itemId: itemId, newState: !isOn)) { + HStack { + Image(systemName: isOn ? onIcon : offIcon) + .font(.title2) + .foregroundStyle(isOn ? onColor : offColor) + + Text(title) + .font(.caption) + .lineLimit(1) + + Spacer() + } + .padding() + .background(Color.secondary.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + .buttonStyle(.plain) + } +} + +// MARK: - Interactive Counter Widget + +/// Interactive counter widget template +public struct InteractiveCounterWidget: View { + let title: String + let counterId: String + let currentValue: Int + let accentColor: Color + + public init( + title: String, + counterId: String, + currentValue: Int, + accentColor: Color = .blue + ) { + self.title = title + self.counterId = counterId + self.currentValue = currentValue + self.accentColor = accentColor + } + + public var body: some View { + VStack(spacing: 12) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + + Text("\(currentValue)") + .font(.system(size: 48, weight: .bold, design: .rounded)) + .foregroundStyle(accentColor) + + HStack(spacing: 16) { + Button(intent: IncrementIntent(counterId: counterId, amount: -1)) { + Image(systemName: "minus.circle.fill") + .font(.title) + .foregroundStyle(.red) + } + .buttonStyle(.plain) + + Button(intent: IncrementIntent(counterId: counterId, amount: 1)) { + Image(systemName: "plus.circle.fill") + .font(.title) + .foregroundStyle(.green) + } + .buttonStyle(.plain) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Task List Widget + +/// Interactive task list widget with completion toggles +public struct InteractiveTaskListWidget: View { + public struct Task: Identifiable { + public let id: String + public let title: String + public var isCompleted: Bool + public let priority: Priority + + public enum Priority { + case low, medium, high + + var color: Color { + switch self { + case .low: return .blue + case .medium: return .orange + case .high: return .red + } + } + } + + public init(id: String, title: String, isCompleted: Bool, priority: Priority = .medium) { + self.id = id + self.title = title + self.isCompleted = isCompleted + self.priority = priority + } + } + + let tasks: [Task] + let listTitle: String + + public init(tasks: [Task], listTitle: String = "Tasks") { + self.tasks = tasks + self.listTitle = listTitle + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(listTitle) + .font(.headline) + + Spacer() + + let completed = tasks.filter { $0.isCompleted }.count + Text("\(completed)/\(tasks.count)") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(tasks.prefix(4)) { task in + Button(intent: ToggleIntent(itemId: task.id, newState: !task.isCompleted)) { + HStack(spacing: 10) { + Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundStyle(task.isCompleted ? .green : task.priority.color) + .font(.body) + + Text(task.title) + .font(.caption) + .strikethrough(task.isCompleted) + .foregroundStyle(task.isCompleted ? .secondary : .primary) + .lineLimit(1) + + Spacer() + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift b/Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift new file mode 100644 index 0000000..78303f6 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/InteractiveTrackerWidgets.swift @@ -0,0 +1,344 @@ +import AppIntents +import SwiftUI +import WidgetKit + +// MARK: - Interactive Quick Actions Widget + +/// Quick action buttons widget +public struct InteractiveQuickActionsWidget: View { + public struct QuickAction: Identifiable { + public let id: String + public let title: String + public let icon: String + public let color: Color + public let url: URL? + + public init(id: String, title: String, icon: String, color: Color, url: URL? = nil) { + self.id = id + self.title = title + self.icon = icon + self.color = color + self.url = url + } + } + + let actions: [QuickAction] + let columns: Int + + public init(actions: [QuickAction], columns: Int = 2) { + self.actions = actions + self.columns = columns + } + + public var body: some View { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columns), spacing: 8) { + ForEach(actions.prefix(4)) { action in + if let url = action.url { + Link(destination: url) { + ActionButtonView(action: action) + } + } else { + Button(intent: RecordActionIntent(action: action.id)) { + ActionButtonView(action: action) + } + .buttonStyle(.plain) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + struct ActionButtonView: View { + let action: QuickAction + + var body: some View { + VStack(spacing: 6) { + Image(systemName: action.icon) + .font(.title2) + .foregroundStyle(.white) + .frame(width: 44, height: 44) + .background(action.color) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + Text(action.title) + .font(.caption2) + .lineLimit(1) + } + } + } +} + +// MARK: - Interactive Timer Control Widget + +/// Timer control widget with start/pause/reset +public struct InteractiveTimerWidget: View { + let timerId: String + let timeRemaining: TimeInterval + let isRunning: Bool + let title: String + + public init( + timerId: String, + timeRemaining: TimeInterval, + isRunning: Bool, + title: String = "Timer" + ) { + self.timerId = timerId + self.timeRemaining = timeRemaining + self.isRunning = isRunning + self.title = title + } + + private func formatTime(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } + + public var body: some View { + VStack(spacing: 12) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + + Text(formatTime(timeRemaining)) + .font(.system(size: 36, weight: .thin, design: .rounded)) + .monospacedDigit() + + HStack(spacing: 20) { + Button(intent: RecordActionIntent(action: "timer.reset.\(timerId)")) { + Image(systemName: "arrow.counterclockwise") + .font(.title3) + .foregroundStyle(.orange) + } + .buttonStyle(.plain) + + Button(intent: RecordActionIntent(action: "timer.\(isRunning ? "pause" : "start").\(timerId)")) { + Image(systemName: isRunning ? "pause.circle.fill" : "play.circle.fill") + .font(.title) + .foregroundStyle(isRunning ? .yellow : .green) + } + .buttonStyle(.plain) + + Button(intent: RecordActionIntent(action: "timer.stop.\(timerId)")) { + Image(systemName: "stop.circle") + .font(.title3) + .foregroundStyle(.red) + } + .buttonStyle(.plain) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Habit Tracker Widget + +/// Habit tracker with interactive checkmarks +public struct InteractiveHabitWidget: View { + public struct Habit: Identifiable { + public let id: String + public let name: String + public let icon: String + public let color: Color + public var weekProgress: [Bool] // 7 days + + public init(id: String, name: String, icon: String, color: Color, weekProgress: [Bool]) { + self.id = id + self.name = name + self.icon = icon + self.color = color + self.weekProgress = weekProgress + } + } + + let habits: [Habit] + let todayIndex: Int + + public init(habits: [Habit], todayIndex: Int = 6) { + self.habits = habits + self.todayIndex = todayIndex + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Habits") + .font(.headline) + Spacer() + Text("Today") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(habits.prefix(3)) { habit in + HStack(spacing: 8) { + Image(systemName: habit.icon) + .font(.caption) + .foregroundStyle(habit.color) + .frame(width: 20) + + Text(habit.name) + .font(.caption) + .lineLimit(1) + + Spacer() + + // Week progress (non-interactive) + HStack(spacing: 2) { + ForEach(0..<6, id: \.self) { day in + Circle() + .fill(habit.weekProgress.indices.contains(day) && habit.weekProgress[day] + ? habit.color + : Color.secondary.opacity(0.3)) + .frame(width: 6, height: 6) + } + } + + // Today's checkbox (interactive) + Button(intent: ToggleIntent( + itemId: "\(habit.id)_\(todayIndex)", + newState: !(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex]) + )) { + Image(systemName: habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] + ? "checkmark.circle.fill" + : "circle") + .font(.body) + .foregroundStyle(habit.weekProgress.indices.contains(todayIndex) && habit.weekProgress[todayIndex] + ? habit.color + : Color.secondary) + } + .buttonStyle(.plain) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Rating Widget + +/// Star rating widget +public struct InteractiveRatingWidget: View { + let ratingId: String + let currentRating: Int + let maxRating: Int + let title: String + let subtitle: String? + + public init( + ratingId: String, + currentRating: Int, + maxRating: Int = 5, + title: String, + subtitle: String? = nil + ) { + self.ratingId = ratingId + self.currentRating = currentRating + self.maxRating = maxRating + self.title = title + self.subtitle = subtitle + } + + public var body: some View { + VStack(spacing: 12) { + Text(title) + .font(.headline) + + if let subtitle = subtitle { + Text(subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 8) { + ForEach(1...max(maxRating, 1), id: \.self) { star in + Button(intent: SetValueIntent(key: ratingId, value: star)) { + Image(systemName: star <= currentRating ? "star.fill" : "star") + .font(.title2) + .foregroundStyle(star <= currentRating ? .yellow : .secondary) + } + .buttonStyle(.plain) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Interactive Water Tracker Widget + +/// Water intake tracker with increment buttons +public struct InteractiveWaterWidget: View { + let trackerId: String + let currentIntake: Double // in ml + let dailyGoal: Double + let glassSize: Double + + public init( + trackerId: String, + currentIntake: Double, + dailyGoal: Double = 2000, + glassSize: Double = 250 + ) { + self.trackerId = trackerId + self.currentIntake = currentIntake.isFinite ? max(currentIntake, 0) : 0 + self.dailyGoal = dailyGoal + // The button converts the glass size to whole millilitres, so keep it finite. + self.glassSize = glassSize.isFinite ? min(max(glassSize, 0), 10_000) : 0 + } + + var progress: Double { + WidgetMath.fraction(currentIntake, of: dailyGoal) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: "drop.fill") + .foregroundStyle(.cyan) + Text("Water") + .font(.headline) + Spacer() + } + + ZStack { + Circle() + .stroke(Color.cyan.opacity(0.2), lineWidth: 8) + + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(progress)) + .stroke( + LinearGradient(colors: [.cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing), + style: StrokeStyle(lineWidth: 8, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + + VStack(spacing: 2) { + Text("\(Int(currentIntake))") + .font(.title3) + .fontWeight(.bold) + Text("/ \(dailyGoal, format: .number.precision(.fractionLength(0)))ml") + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + .frame(width: 70, height: 70) + + Button(intent: IncrementIntent(counterId: trackerId, amount: Int(glassSize))) { + Label("+\(Int(glassSize))ml", systemImage: "plus.circle.fill") + .font(.caption) + .foregroundStyle(.white) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(.cyan) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift b/Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift new file mode 100644 index 0000000..fe19b40 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/WidgetIntents.swift @@ -0,0 +1,118 @@ +import AppIntents +import WidgetKit + +/// Sets a boolean flag in ``WidgetStateStore/shared`` and reloads widget timelines. +public struct ToggleIntent: AppIntent { + public static let title: LocalizedStringResource = "Toggle" + public static let description = IntentDescription("Turns a widget item on or off.") + public static let isDiscoverable = false + + @Parameter(title: "Item ID") public var itemId: String + @Parameter(title: "New State") public var newState: Bool + + public init() {} + + public init(itemId: String, newState: Bool) { + self.itemId = itemId + self.newState = newState + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.setOn(newState, for: itemId) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Adds `amount` (which may be negative) to a counter, never going below zero. +public struct IncrementIntent: AppIntent { + public static let title: LocalizedStringResource = "Increment" + public static let description = IntentDescription("Changes a widget counter.") + public static let isDiscoverable = false + + @Parameter(title: "Counter ID") public var counterId: String + @Parameter(title: "Amount") public var amount: Int + + public init() {} + + public init(counterId: String, amount: Int = 1) { + self.counterId = counterId + self.amount = amount + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.increment(counterId, by: amount, minimum: 0) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Stores an integer, e.g. a star rating. +public struct SetValueIntent: AppIntent { + public static let title: LocalizedStringResource = "Set Value" + public static let description = IntentDescription("Stores a value chosen in a widget.") + public static let isDiscoverable = false + + @Parameter(title: "Key") public var key: String + @Parameter(title: "Value") public var value: Int + + public init() {} + + public init(key: String, value: Int) { + self.key = key + self.value = value + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.setValue(value, for: key) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Queues an action identifier for the app to process later with +/// ``WidgetStateStore/drainPendingActions()``. Use it for work the extension +/// cannot do itself, such as starting a timer owned by the app. +public struct RecordActionIntent: AppIntent { + public static let title: LocalizedStringResource = "Record Action" + public static let description = IntentDescription("Queues a widget action for the app.") + public static let isDiscoverable = false + + @Parameter(title: "Action") public var action: String + + public init() {} + + public init(action: String) { + self.action = action + } + + public func perform() async throws -> some IntentResult { + WidgetStateStore.shared.enqueueAction(action) + WidgetCenter.shared.reloadAllTimelines() + return .result() + } +} + +/// Reloads one widget kind, or all widgets when `kind` is empty. +public struct RefreshWidgetIntent: AppIntent { + public static let title: LocalizedStringResource = "Refresh Widget" + public static let description = IntentDescription("Reloads widget data.") + public static let isDiscoverable = false + + @Parameter(title: "Widget Kind") public var kind: String? + + public init() {} + + public init(kind: String? = nil) { + self.kind = kind + } + + public func perform() async throws -> some IntentResult { + if let kind, !kind.isEmpty { + WidgetCenter.shared.reloadTimelines(ofKind: kind) + } else { + WidgetCenter.shared.reloadAllTimelines() + } + return .result() + } +} diff --git a/Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift b/Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift new file mode 100644 index 0000000..2de8295 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Interactive/WidgetStateStore.swift @@ -0,0 +1,90 @@ +import Foundation + +/// Small key-value state shared by interactive widget intents and the host app. +/// +/// Interactive widget buttons run their `AppIntent` in the widget extension +/// process, so any state they change must live somewhere the app can also read: +/// usually `UserDefaults` for a shared App Group. +/// +/// The intents in this package use ``shared``, which reads the App Group from +/// the `WidgetDevelopmentKitAppGroup` key in the running bundle's Info.plist. +/// Add that key with the same value to both the app and the widget extension. +public struct WidgetStateStore: Sendable { + /// Info.plist key holding the App Group identifier used by ``shared``. + public static let appGroupInfoKey = "WidgetDevelopmentKitAppGroup" + + private static let pendingActionsKey = "actions.pending" + private static let maximumPendingActions = 50 + + // UserDefaults is documented as thread-safe but is not marked Sendable in the SDK. + nonisolated(unsafe) private let defaults: UserDefaults + private let keyPrefix: String + + public init(defaults: UserDefaults, keyPrefix: String = "widget.state.") { + self.defaults = defaults + self.keyPrefix = keyPrefix + } + + /// The store for the App Group named in Info.plist, or standard defaults if the key + /// is missing (state then stays inside the current process's container). + public static var shared: WidgetStateStore { + let group = Bundle.main.object(forInfoDictionaryKey: appGroupInfoKey) as? String + let defaults = group.flatMap { UserDefaults(suiteName: $0) } ?? .standard + return WidgetStateStore(defaults: defaults) + } + + // MARK: Flags + + public func isOn(_ id: String) -> Bool { + defaults.bool(forKey: key("flag", id)) + } + + public func setOn(_ isOn: Bool, for id: String) { + defaults.set(isOn, forKey: key("flag", id)) + } + + // MARK: Integers + + public func value(_ id: String) -> Int { + defaults.integer(forKey: key("value", id)) + } + + public func setValue(_ value: Int, for id: String) { + defaults.set(value, forKey: key("value", id)) + } + + /// Adds `amount` and returns the new value. Saturates instead of overflowing. + @discardableResult + public func increment(_ id: String, by amount: Int = 1, minimum: Int? = nil) -> Int { + let (sum, overflow) = value(id).addingReportingOverflow(amount) + var newValue = overflow ? (amount > 0 ? Int.max : Int.min) : sum + if let minimum { newValue = max(newValue, minimum) } + setValue(newValue, for: id) + return newValue + } + + // MARK: Pending actions + + /// Queues an action for the app to handle on its next launch or foreground. + /// The queue keeps the most recent 50 actions. + public func enqueueAction(_ action: String) { + var actions = pendingActions() + actions.append(action) + defaults.set(Array(actions.suffix(Self.maximumPendingActions)), forKey: keyPrefix + Self.pendingActionsKey) + } + + public func pendingActions() -> [String] { + defaults.stringArray(forKey: keyPrefix + Self.pendingActionsKey) ?? [] + } + + /// Returns queued actions in order and clears the queue. + public func drainPendingActions() -> [String] { + let actions = pendingActions() + defaults.removeObject(forKey: keyPrefix + Self.pendingActionsKey) + return actions + } + + private func key(_ kind: String, _ id: String) -> String { + "\(keyPrefix)\(kind).\(id)" + } +} diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift new file mode 100644 index 0000000..220be12 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/DeliveryActivities.swift @@ -0,0 +1,245 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Delivery Tracking Activity + +/// Food/Package delivery tracking Live Activity +public struct DeliveryAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: DeliveryStatus + public var estimatedArrival: Date + public var driverName: String + public var currentStep: Int + + public init(status: DeliveryStatus, estimatedArrival: Date, driverName: String, currentStep: Int) { + self.status = status + self.estimatedArrival = estimatedArrival + self.driverName = driverName + self.currentStep = currentStep + } + } + + public enum DeliveryStatus: String, Codable { + case preparing = "Preparing" + case pickedUp = "Picked Up" + case onTheWay = "On the Way" + case arriving = "Arriving" + case delivered = "Delivered" + + public var icon: String { + switch self { + case .preparing: return "takeoutbag.and.cup.and.straw" + case .pickedUp: return "bag.fill" + case .onTheWay: return "car.fill" + case .arriving: return "location.fill" + case .delivered: return "checkmark.circle.fill" + } + } + + public var color: Color { + switch self { + case .preparing: return .orange + case .pickedUp: return .blue + case .onTheWay: return .purple + case .arriving: return .green + case .delivered: return .green + } + } + } + + public let orderNumber: String + public let restaurantName: String + public let totalSteps: Int + + public init(orderNumber: String, restaurantName: String, totalSteps: Int = 4) { + self.orderNumber = orderNumber + self.restaurantName = restaurantName + self.totalSteps = totalSteps + } +} + +public struct DeliveryActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(context.attributes.restaurantName) + .font(.headline) + Text("Order #\(context.attributes.orderNumber)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: context.state.status.icon) + .font(.title2) + .foregroundStyle(context.state.status.color) + } + + // Progress bar + ProgressView( + value: WidgetMath.fraction(Double(context.state.currentStep), of: Double(context.attributes.totalSteps)) + ) + .tint(context.state.status.color) + + HStack { + Text(context.state.status.rawValue) + .font(.caption) + .fontWeight(.medium) + + Spacer() + + Label { + Text(context.state.estimatedArrival, style: .relative) + } icon: { + Image(systemName: "clock") + } + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + } + + // Dynamic Island Views + public var compactLeading: some View { + Image(systemName: context.state.status.icon) + .foregroundStyle(context.state.status.color) + } + + public var compactTrailing: some View { + Text(context.state.estimatedArrival, style: .timer) + .font(.caption2) + .monospacedDigit() + } + + public var minimal: some View { + Image(systemName: context.state.status.icon) + .foregroundStyle(context.state.status.color) + } +} + +// MARK: - Order Status Activity + +/// E-commerce order tracking +public struct OrderAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: OrderStatus + public var currentStep: Int + public var estimatedDelivery: Date + public var trackingNumber: String? + + public init(status: OrderStatus, currentStep: Int, estimatedDelivery: Date, trackingNumber: String? = nil) { + self.status = status + self.currentStep = currentStep + self.estimatedDelivery = estimatedDelivery + self.trackingNumber = trackingNumber + } + } + + public enum OrderStatus: String, Codable { + case confirmed = "Confirmed" + case processing = "Processing" + case shipped = "Shipped" + case outForDelivery = "Out for Delivery" + case delivered = "Delivered" + + public var icon: String { + switch self { + case .confirmed: return "checkmark.circle" + case .processing: return "box.truck.badge.clock" + case .shipped: return "shippingbox" + case .outForDelivery: return "truck.box" + case .delivered: return "house.fill" + } + } + } + + public let orderNumber: String + public let storeName: String + public let totalSteps: Int + + public init(orderNumber: String, storeName: String, totalSteps: Int = 5) { + self.orderNumber = orderNumber + self.storeName = storeName + self.totalSteps = totalSteps + } +} + +public struct OrderActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading) { + Text(context.attributes.storeName) + .font(.headline) + Text("Order #\(context.attributes.orderNumber)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: context.state.status.icon) + .font(.title2) + .foregroundStyle(.blue) + } + + // Progress steps + HStack(spacing: 0) { + ForEach(0..( + _ attributes: Attributes, + state: Attributes.ContentState, + staleAfter: TimeInterval? = nil, + pushType: PushType? = nil + ) throws -> Activity { + let content = ActivityContent(state: state, staleDate: staleAfter.map { Date().addingTimeInterval($0) }) + return try Activity.request(attributes: attributes, content: content, pushType: pushType) + } + + /// Updates an activity's content. + public static func update( + _ activity: Activity, + state: Attributes.ContentState, + staleAfter: TimeInterval? = nil, + alert: AlertConfiguration? = nil + ) async { + let content = ActivityContent(state: state, staleDate: staleAfter.map { Date().addingTimeInterval($0) }) + await activity.update(content, alertConfiguration: alert) + } + + /// Ends every running activity of the given type. + public static func endAll( + _ type: Attributes.Type, + dismissalPolicy: ActivityUIDismissalPolicy = .immediate + ) async { + for activity in Activity.activities { + await activity.end(nil, dismissalPolicy: dismissalPolicy) + } + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift b/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift index 2c9c403..d036e11 100644 --- a/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift +++ b/Sources/WidgetDevelopmentKit/LiveActivities/LiveActivityStateMachine.swift @@ -1,34 +1,48 @@ import Foundation -/// iOS-Widget-Development-Kit: Live Activity State-Machine. -/// -/// Manages complex state transitions for Live Activities using a -/// strictly-typed, high-integrity state machine. +/// A small state machine for driving Live Activity content through allowed phases. +/// +/// Live Activity updates often arrive out of order (push, background refresh, app +/// launch). Declaring the allowed transitions up front lets the app reject an update +/// that would move an activity backwards, for example from `.delivered` to `.preparing`. +/// +/// ```swift +/// enum Phase: Hashable, Sendable { case preparing, onTheWay, delivered } +/// +/// let machine = LiveActivityStateMachine( +/// initialState: Phase.preparing, +/// transitions: [.preparing: [.onTheWay], .onTheWay: [.delivered]] +/// ) +/// try await machine.transition(to: .onTheWay) +/// ``` public actor LiveActivityStateMachine { - private var currentState: State - private let transitions: [State: [State]] - - public init(initialState: State, transitions: [State: [State]]) { + public private(set) var currentState: State + private let transitions: [State: Set] + + public init(initialState: State, transitions: [State: Set]) { self.currentState = initialState self.transitions = transitions } - - /// Attempts to transition to a new state. - /// Validates the transition against the defined graph. - public func transition(to newState: State) throws { - guard let allowed = transitions[currentState], allowed.contains(newState) else { - throw StateMachineError.invalidTransition + + /// Whether moving from the current state to `newState` is allowed. + public func canTransition(to newState: State) -> Bool { + transitions[currentState]?.contains(newState) ?? false + } + + /// Moves to `newState`, or throws without changing state if the transition is not allowed. + public func transition(to newState: State) throws(StateMachineError) { + guard canTransition(to: newState) else { + throw .invalidTransition(from: currentState, to: newState) } - currentState = newState - print("⚡ [WidgetKit] Live Activity Transitioned to: \\(newState)") } - - public func getCurrentState() -> State { - return currentState + + /// States reachable in one step from the current state. + public func allowedNextStates() -> Set { + transitions[currentState] ?? [] } } -public enum StateMachineError: Error { - case invalidTransition +public enum StateMachineError: Error, Equatable { + case invalidTransition(from: State, to: State) } diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift new file mode 100644 index 0000000..5cc2aa2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/MediaActivities.swift @@ -0,0 +1,243 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Sports Score Activity + +/// Live sports score tracking +public struct SportsScoreAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var homeScore: Int + public var awayScore: Int + public var gameTime: String + public var period: String + public var isLive: Bool + public var lastEvent: String? + + public init(homeScore: Int, awayScore: Int, gameTime: String, period: String, isLive: Bool, lastEvent: String? = nil) { + self.homeScore = homeScore + self.awayScore = awayScore + self.gameTime = gameTime + self.period = period + self.isLive = isLive + self.lastEvent = lastEvent + } + } + + public let homeTeam: String + public let awayTeam: String + public let homeTeamAbbr: String + public let awayTeamAbbr: String + public let league: String + + public init(homeTeam: String, awayTeam: String, homeTeamAbbr: String, awayTeamAbbr: String, league: String) { + self.homeTeam = homeTeam + self.awayTeam = awayTeam + self.homeTeamAbbr = homeTeamAbbr + self.awayTeamAbbr = awayTeamAbbr + self.league = league + } +} + +public struct SportsActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 8) { + HStack { + Text(context.attributes.league) + .font(.caption2) + .foregroundStyle(.secondary) + + Spacer() + + if context.state.isLive { + HStack(spacing: 4) { + Circle() + .fill(.red) + .frame(width: 6, height: 6) + Text("LIVE") + .font(.caption2) + .foregroundStyle(.red) + } + } + + Text(context.state.period) + .font(.caption) + .fontWeight(.medium) + } + + HStack { + TeamScoreView( + name: context.attributes.homeTeamAbbr, + score: context.state.homeScore, + isWinning: context.state.homeScore > context.state.awayScore + ) + + Spacer() + + VStack { + Text(context.state.gameTime) + .font(.headline) + .monospacedDigit() + } + + Spacer() + + TeamScoreView( + name: context.attributes.awayTeamAbbr, + score: context.state.awayScore, + isWinning: context.state.awayScore > context.state.homeScore + ) + } + + if let event = context.state.lastEvent { + Text(event) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } + .padding() + } + + public var compactLeading: some View { + HStack(spacing: 4) { + Text(context.attributes.homeTeamAbbr) + .font(.caption2) + .fontWeight(.bold) + Text("\(context.state.homeScore)") + .font(.caption) + } + } + + public var compactTrailing: some View { + HStack(spacing: 4) { + Text("\(context.state.awayScore)") + .font(.caption) + Text(context.attributes.awayTeamAbbr) + .font(.caption2) + .fontWeight(.bold) + } + } + + struct TeamScoreView: View { + let name: String + let score: Int + let isWinning: Bool + + var body: some View { + VStack(spacing: 4) { + Text(name) + .font(.caption) + .fontWeight(.medium) + Text("\(score)") + .font(.title) + .fontWeight(isWinning ? .bold : .regular) + .foregroundStyle(isWinning ? .primary : .secondary) + } + } + } +} + +// MARK: - Music Now Playing Activity + +/// Music player Live Activity +public struct NowPlayingAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var trackName: String + public var artistName: String + public var albumName: String + public var progress: Double + public var duration: TimeInterval + public var isPlaying: Bool + + public init(trackName: String, artistName: String, albumName: String, progress: Double, duration: TimeInterval, isPlaying: Bool) { + self.trackName = trackName + self.artistName = artistName + self.albumName = albumName + self.progress = progress + self.duration = duration + self.isPlaying = isPlaying + } + } + + public let appName: String + + public init(appName: String = "Music") { + self.appName = appName + } +} + +public struct NowPlayingActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 8) + .fill(LinearGradient(colors: [.purple, .pink], startPoint: .topLeading, endPoint: .bottomTrailing)) + .frame(width: 60, height: 60) + .overlay { + Image(systemName: "music.note") + .font(.title2) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(context.state.trackName) + .font(.headline) + .lineLimit(1) + + Text(context.state.artistName) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(Color.pink) + .frame(width: geo.size.width * context.state.progress) + } + } + .frame(height: 4) + } + + VStack(spacing: 8) { + Image(systemName: context.state.isPlaying ? "pause.fill" : "play.fill") + .font(.title3) + + Image(systemName: "forward.fill") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: context.state.isPlaying ? "waveform" : "pause.fill") + .foregroundStyle(.pink) + } + + public var compactTrailing: some View { + Text(context.state.trackName) + .font(.caption2) + .lineLimit(1) + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift b/Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift new file mode 100644 index 0000000..44cbbab --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/MeetingActivity.swift @@ -0,0 +1,116 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Meeting Activity + +/// Calendar meeting Live Activity +public struct MeetingAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: MeetingStatus + public var participantCount: Int + public var elapsedTime: TimeInterval + public var hasRecording: Bool + + public init(status: MeetingStatus, participantCount: Int, elapsedTime: TimeInterval, hasRecording: Bool = false) { + self.status = status + self.participantCount = participantCount + self.elapsedTime = elapsedTime + self.hasRecording = hasRecording + } + } + + public enum MeetingStatus: String, Codable { + case upcoming = "Starting Soon" + case inProgress = "In Progress" + case ending = "Ending Soon" + case ended = "Ended" + } + + public let title: String + public let startTime: Date + public let endTime: Date + public let organizer: String + public let meetingLink: String? + + public init(title: String, startTime: Date, endTime: Date, organizer: String, meetingLink: String? = nil) { + self.title = title + self.startTime = startTime + self.endTime = endTime + self.organizer = organizer + self.meetingLink = meetingLink + } +} + +public struct MeetingActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + private func formatDuration(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(context.attributes.title) + .font(.headline) + .lineLimit(1) + + Text("by \(context.attributes.organizer)") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + VStack(alignment: .trailing) { + if context.state.hasRecording { + Label("Recording", systemImage: "record.circle") + .font(.caption2) + .foregroundStyle(.red) + } + + HStack { + Image(systemName: "person.2.fill") + Text("\(context.state.participantCount)") + } + .font(.caption) + .foregroundStyle(.secondary) + } + } + + HStack { + Label(context.state.status.rawValue, systemImage: "video.fill") + .font(.caption) + .foregroundStyle(.green) + + Spacer() + + Text(formatDuration(context.state.elapsedTime)) + .font(.headline) + .monospacedDigit() + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "video.fill") + .foregroundStyle(.green) + } + + public var compactTrailing: some View { + Text(formatDuration(context.state.elapsedTime)) + .font(.caption2) + .monospacedDigit() + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift new file mode 100644 index 0000000..eba7367 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/TimingActivities.swift @@ -0,0 +1,347 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Timer Activity + +/// General purpose timer Live Activity +public struct TimerAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var endTime: Date + public var isPaused: Bool + /// Time left when the timer was paused. Shown instead of the running timer while + /// `isPaused` is `true`; `nil` while paused reads `0:00`. + public var pausedTimeRemaining: TimeInterval? + + public init(endTime: Date, isPaused: Bool = false, pausedTimeRemaining: TimeInterval? = nil) { + self.endTime = endTime + self.isPaused = isPaused + self.pausedTimeRemaining = pausedTimeRemaining + } + + /// Fixed label for a paused timer, or `nil` while the timer runs. + public var pausedLabel: String? { + guard isPaused else { return nil } + return WidgetFormat.playbackTime(pausedTimeRemaining ?? 0) + } + } + + public let timerName: String + public let category: String + public let accentColor: String + + public init(timerName: String, category: String = "Timer", accentColor: String = "blue") { + self.timerName = timerName + self.category = category + self.accentColor = accentColor + } +} + +public struct TimerActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + var color: Color { + switch context.attributes.accentColor { + case "red": return .red + case "green": return .green + case "orange": return .orange + case "purple": return .purple + default: return .blue + } + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Label(context.attributes.category, systemImage: "timer") + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + if context.state.isPaused { + Text("PAUSED") + .font(.caption2) + .fontWeight(.bold) + .foregroundStyle(.orange) + } + } + + remainingText + .font(.system(size: 48, weight: .thin, design: .rounded)) + .monospacedDigit() + .foregroundStyle(color) + + Text(context.attributes.timerName) + .font(.headline) + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "timer") + .foregroundStyle(color) + } + + public var compactTrailing: some View { + remainingText + .font(.caption) + .monospacedDigit() + } + + public var minimal: some View { + remainingText + .font(.caption2) + .monospacedDigit() + } + + /// `Text(_:style: .timer)` keeps counting, so a paused timer shows a fixed value. + private var remainingText: Text { + if let pausedLabel = context.state.pausedLabel { + return Text(pausedLabel) + } + return Text(context.state.endTime, style: .timer) + } +} + +// MARK: - Workout Activity + +/// Fitness workout tracking +public struct WorkoutAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var elapsedTime: TimeInterval + public var calories: Int + public var heartRate: Int + public var distance: Double + public var pace: String + public var isActive: Bool + + public init(elapsedTime: TimeInterval, calories: Int, heartRate: Int, distance: Double, pace: String, isActive: Bool) { + self.elapsedTime = elapsedTime + self.calories = calories + self.heartRate = heartRate + self.distance = distance + self.pace = pace + self.isActive = isActive + } + } + + public enum WorkoutType: String, Codable { + case running = "Running" + case cycling = "Cycling" + case swimming = "Swimming" + case walking = "Walking" + case hiit = "HIIT" + + public var icon: String { + switch self { + case .running: return "figure.run" + case .cycling: return "figure.outdoor.cycle" + case .swimming: return "figure.pool.swim" + case .walking: return "figure.walk" + case .hiit: return "figure.highintensity.intervaltraining" + } + } + } + + public let workoutType: WorkoutType + public let goalType: String + public let goalValue: Double + + public init(workoutType: WorkoutType, goalType: String = "Distance", goalValue: Double = 5.0) { + self.workoutType = workoutType + self.goalType = goalType + self.goalValue = goalValue + } +} + +public struct WorkoutActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + private func formatTime(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Label(context.attributes.workoutType.rawValue, systemImage: context.attributes.workoutType.icon) + .font(.headline) + + Spacer() + + if context.state.isActive { + HStack(spacing: 4) { + Circle() + .fill(.green) + .frame(width: 8, height: 8) + Text("Active") + .font(.caption) + .foregroundStyle(.green) + } + } + } + + HStack(spacing: 20) { + MetricView(value: formatTime(context.state.elapsedTime), label: "Time", icon: "clock") + MetricView(value: String(format: "%.2f", context.state.distance), label: "km", icon: "location") + MetricView(value: "\(context.state.calories)", label: "Cal", icon: "flame.fill") + MetricView(value: "\(context.state.heartRate)", label: "BPM", icon: "heart.fill") + } + + HStack { + Text("Pace: \(context.state.pace)") + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + Text("Goal: \(String(format: "%.1f", context.attributes.goalValue)) km") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: context.attributes.workoutType.icon) + .foregroundStyle(.green) + } + + public var compactTrailing: some View { + Text(formatTime(context.state.elapsedTime)) + .font(.caption) + .monospacedDigit() + } + + struct MetricView: View { + let value: String + let label: String + let icon: String + + var body: some View { + VStack(spacing: 2) { + Image(systemName: icon) + .font(.caption2) + .foregroundStyle(.secondary) + Text(value) + .font(.caption) + .fontWeight(.semibold) + Text(label) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Parking Timer Activity + +/// Parking meter countdown +public struct ParkingAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var expiresAt: Date + public var isExpired: Bool + public var remainingTime: TimeInterval + + public init(expiresAt: Date, isExpired: Bool = false, remainingTime: TimeInterval) { + self.expiresAt = expiresAt + self.isExpired = isExpired + self.remainingTime = remainingTime + } + } + + public let location: String + public let spotNumber: String + public let rate: String + + public init(location: String, spotNumber: String, rate: String) { + self.location = location + self.spotNumber = spotNumber + self.rate = rate + } +} + +public struct ParkingActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + var urgencyColor: Color { + let remaining = context.state.remainingTime + if remaining <= 0 || context.state.isExpired { return .red } + if remaining < 600 { return .orange } // Less than 10 min + return .green + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Label(context.attributes.location, systemImage: "car.fill") + .font(.headline) + + Spacer() + + Text("Spot \(context.attributes.spotNumber)") + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.secondary.opacity(0.2)) + .clipShape(Capsule()) + } + + if context.state.isExpired { + Text("EXPIRED") + .font(.title) + .fontWeight(.bold) + .foregroundStyle(.red) + } else { + Text(context.state.expiresAt, style: .timer) + .font(.system(size: 36, weight: .thin, design: .rounded)) + .monospacedDigit() + .foregroundStyle(urgencyColor) + } + + HStack { + Text(context.attributes.rate) + .font(.caption) + .foregroundStyle(.secondary) + + Spacer() + + if !context.state.isExpired { + Text("Expires at \(context.state.expiresAt, format: .dateTime.hour().minute())") + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "car.fill") + .foregroundStyle(urgencyColor) + } + + public var compactTrailing: some View { + Text(context.state.expiresAt, style: .timer) + .font(.caption2) + .monospacedDigit() + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift b/Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift new file mode 100644 index 0000000..b167bb0 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LiveActivities/TravelActivities.swift @@ -0,0 +1,287 @@ +// Live Activity attributes and views. ActivityKit exists only on iOS. + +#if canImport(ActivityKit) && os(iOS) +import ActivityKit +import SwiftUI +import WidgetKit + +// MARK: - Ride Sharing Activity + +/// Uber/Lyft style ride tracking +public struct RideAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: RideStatus + public var driverName: String + public var vehicleInfo: String + public var licensePlate: String + public var eta: Date + public var driverRating: Double + + public init(status: RideStatus, driverName: String, vehicleInfo: String, licensePlate: String, eta: Date, driverRating: Double) { + self.status = status + self.driverName = driverName + self.vehicleInfo = vehicleInfo + self.licensePlate = licensePlate + self.eta = eta + self.driverRating = driverRating + } + } + + public enum RideStatus: String, Codable { + case searching = "Finding Driver" + case driverAssigned = "Driver Assigned" + case arriving = "Driver Arriving" + case inProgress = "In Progress" + case completed = "Completed" + + public var icon: String { + switch self { + case .searching: return "magnifyingglass" + case .driverAssigned: return "person.fill.checkmark" + case .arriving: return "car.fill" + case .inProgress: return "location.fill" + case .completed: return "checkmark.circle.fill" + } + } + } + + public let pickupLocation: String + public let dropoffLocation: String + public let rideType: String + + public init(pickupLocation: String, dropoffLocation: String, rideType: String = "UberX") { + self.pickupLocation = pickupLocation + self.dropoffLocation = dropoffLocation + self.rideType = rideType + } +} + +public struct RideActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(context.state.driverName) + .font(.headline) + + HStack(spacing: 4) { + Image(systemName: "star.fill") + .font(.caption2) + .foregroundStyle(.yellow) + Text(String(format: "%.1f", context.state.driverRating)) + .font(.caption) + } + } + + Spacer() + + VStack(alignment: .trailing, spacing: 2) { + Text(context.state.vehicleInfo) + .font(.caption) + Text(context.state.licensePlate) + .font(.caption) + .fontWeight(.bold) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background(Color.secondary.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + } + + Divider() + + HStack { + VStack(alignment: .leading) { + Label(context.attributes.pickupLocation, systemImage: "circle.fill") + .font(.caption2) + .foregroundStyle(.green) + + Label(context.attributes.dropoffLocation, systemImage: "mappin.circle.fill") + .font(.caption2) + .foregroundStyle(.red) + } + + Spacer() + + VStack(alignment: .trailing) { + Text("ETA") + .font(.caption2) + .foregroundStyle(.secondary) + Text(context.state.eta, style: .relative) + .font(.headline) + } + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "car.fill") + .foregroundStyle(.blue) + } + + public var compactTrailing: some View { + Text(context.state.eta, style: .relative) + .font(.caption2) + } +} + +// MARK: - Flight Tracking Activity + +/// Flight status tracking +public struct FlightAttributes: ActivityAttributes { + public struct ContentState: Codable, Hashable { + public var status: FlightStatus + public var gate: String + public var departureTime: Date + public var arrivalTime: Date + public var delay: Int? // minutes + public var progress: Double + + public init(status: FlightStatus, gate: String, departureTime: Date, arrivalTime: Date, delay: Int? = nil, progress: Double = 0) { + self.status = status + self.gate = gate + self.departureTime = departureTime + self.arrivalTime = arrivalTime + self.delay = delay + self.progress = progress + } + } + + public enum FlightStatus: String, Codable { + case scheduled = "Scheduled" + case boarding = "Boarding" + case departed = "Departed" + case inFlight = "In Flight" + case landing = "Landing" + case arrived = "Arrived" + case delayed = "Delayed" + case cancelled = "Cancelled" + + public var color: Color { + switch self { + case .scheduled: return .blue + case .boarding: return .orange + case .departed, .inFlight: return .green + case .landing: return .purple + case .arrived: return .green + case .delayed: return .orange + case .cancelled: return .red + } + } + } + + public let flightNumber: String + public let airline: String + public let departure: String + public let arrival: String + public let departureCode: String + public let arrivalCode: String + + public init(flightNumber: String, airline: String, departure: String, arrival: String, departureCode: String, arrivalCode: String) { + self.flightNumber = flightNumber + self.airline = airline + self.departure = departure + self.arrival = arrival + self.departureCode = departureCode + self.arrivalCode = arrivalCode + } +} + +public struct FlightActivityView: View { + let context: ActivityViewContext + + public init(context: ActivityViewContext) { + self.context = context + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Text("\(context.attributes.airline) \(context.attributes.flightNumber)") + .font(.headline) + + Spacer() + + Text(context.state.status.rawValue) + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(context.state.status.color) + } + + HStack { + VStack(alignment: .leading) { + Text(context.attributes.departureCode) + .font(.title2) + .fontWeight(.bold) + Text(context.state.departureTime, format: .dateTime.hour().minute()) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + VStack { + Image(systemName: "airplane") + .rotationEffect(.degrees(90)) + + GeometryReader { geo in + ZStack(alignment: .leading) { + Rectangle() + .fill(Color.secondary.opacity(0.3)) + .frame(height: 2) + + Rectangle() + .fill(context.state.status.color) + .frame(width: geo.size.width * context.state.progress, height: 2) + } + } + .frame(height: 2) + } + .frame(maxWidth: 100) + + Spacer() + + VStack(alignment: .trailing) { + Text(context.attributes.arrivalCode) + .font(.title2) + .fontWeight(.bold) + Text(context.state.arrivalTime, format: .dateTime.hour().minute()) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + HStack { + Label("Gate \(context.state.gate)", systemImage: "door.right.hand.open") + .font(.caption) + + Spacer() + + if let delay = context.state.delay, delay > 0 { + Text("+\(delay) min delay") + .font(.caption) + .foregroundStyle(.orange) + } + } + } + .padding() + } + + public var compactLeading: some View { + Image(systemName: "airplane") + .foregroundStyle(.blue) + } + + public var compactTrailing: some View { + Text(context.state.status.rawValue) + .font(.caption2) + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift new file mode 100644 index 0000000..bc91600 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenMultiFamilyWidgets.swift @@ -0,0 +1,370 @@ +// Lock Screen (accessory family) widget views. +// Accessory widget families do not exist on macOS, so this file is iOS/watchOS only. + +#if os(iOS) || os(watchOS) +import SwiftUI +import WidgetKit + +// MARK: - Battery Widget (All Families) + +/// Battery level widget supporting all Lock Screen families +public struct LockScreenBatteryWidget: View { + let level: Double + let isCharging: Bool + let deviceName: String? + let family: WidgetFamily + + public init( + level: Double, + isCharging: Bool = false, + deviceName: String? = nil, + family: WidgetFamily = .accessoryCircular + ) { + self.level = level + self.isCharging = isCharging + self.deviceName = deviceName + self.family = family + } + + private var batteryIcon: String { + WidgetMath.batterySymbolName(level: level, isCharging: isCharging) + } + + public var body: some View { + switch family { + case .accessoryCircular: + Gauge(value: WidgetMath.clampedFraction(level), in: 0...1) { + Image(systemName: batteryIcon) + } currentValueLabel: { + Text("\(WidgetMath.percent(level))") + .font(.system(size: 14, weight: .semibold)) + } + .gaugeStyle(.accessoryCircularCapacity) + .tint(level < 0.2 ? .red : (level < 0.5 ? .orange : .green)) + + case .accessoryRectangular: + HStack { + VStack(alignment: .leading, spacing: 2) { + if let name = deviceName { + Text(name) + .font(.caption2) + .foregroundStyle(.secondary) + } + + Text(WidgetMath.percentString(level)) + .font(.title2) + .fontWeight(.semibold) + .widgetAccentable() + + if isCharging { + Label("Charging", systemImage: "bolt.fill") + .font(.caption2) + .foregroundStyle(.green) + } + } + + Spacer() + + Image(systemName: batteryIcon) + .font(.title) + .widgetAccentable() + } + + case .accessoryInline: + Label(WidgetMath.percentString(level), systemImage: batteryIcon) + + default: + EmptyView() + } + } +} + +// MARK: - Timer Widget (All Families) + +/// Countdown timer widget supporting all accessory families. +/// +/// The system animates the timer text and progress between `startTime` and +/// `endTime`, so the widget does not need extra timeline entries to tick. +public struct LockScreenTimerWidget: View { + let interval: ClosedRange + let title: String? + let family: WidgetFamily + + public init( + startTime: Date, + endTime: Date, + title: String? = nil, + family: WidgetFamily = .accessoryCircular + ) { + self.interval = WidgetMath.timerInterval(start: startTime, end: endTime) + self.title = title + self.family = family + } + + private var endTime: Date { interval.upperBound } + + public var body: some View { + switch family { + case .accessoryCircular: + ProgressView(timerInterval: interval) { + Image(systemName: "timer") + } + .progressViewStyle(.circular) + + case .accessoryRectangular: + VStack(alignment: .leading, spacing: 2) { + if let title = title { + Text(title) + .font(.caption2) + .foregroundStyle(.secondary) + } + + Text(endTime, style: .timer) + .font(.title) + .fontWeight(.medium) + .monospacedDigit() + .widgetAccentable() + + ProgressView(timerInterval: interval, countsDown: true) { + EmptyView() + } currentValueLabel: { + EmptyView() + } + .tint(.primary) + } + + case .accessoryInline: + Label { + Text(endTime, style: .timer) + } icon: { + Image(systemName: "timer") + } + + default: + EmptyView() + } + } +} + +// MARK: - Fitness Widget (All Families) + +/// Fitness activity widget +public struct LockScreenFitnessWidget: View { + let moveProgress: Double + let exerciseProgress: Double + let standProgress: Double + let family: WidgetFamily + + public init( + moveProgress: Double, + exerciseProgress: Double, + standProgress: Double, + family: WidgetFamily = .accessoryCircular + ) { + self.moveProgress = moveProgress + self.exerciseProgress = exerciseProgress + self.standProgress = standProgress + self.family = family + } + + public var body: some View { + switch family { + case .accessoryCircular: + ZStack { + // Move ring (outer) + Circle() + .stroke(Color.red.opacity(0.3), lineWidth: 4) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(moveProgress)) + .stroke(Color.red, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + + // Exercise ring (middle) + Circle() + .stroke(Color.green.opacity(0.3), lineWidth: 4) + .padding(5) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(exerciseProgress)) + .stroke(Color.green, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(5) + + // Stand ring (inner) + Circle() + .stroke(Color.cyan.opacity(0.3), lineWidth: 4) + .padding(10) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(standProgress)) + .stroke(Color.cyan, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(10) + } + + case .accessoryRectangular: + HStack { + VStack(alignment: .leading, spacing: 4) { + FitnessRow(label: "Move", progress: moveProgress, color: .red) + FitnessRow(label: "Exercise", progress: exerciseProgress, color: .green) + FitnessRow(label: "Stand", progress: standProgress, color: .cyan) + } + } + + case .accessoryInline: + Label("\(WidgetMath.percentString(moveProgress)) · \(WidgetMath.percentString(exerciseProgress)) · \(WidgetMath.percentString(standProgress))", systemImage: "figure.walk") + + default: + EmptyView() + } + } + + struct FitnessRow: View { + let label: String + let progress: Double + let color: Color + + var body: some View { + HStack(spacing: 4) { + Circle() + .fill(color) + .frame(width: 6, height: 6) + + Text(label) + .font(.system(size: 9)) + + Gauge(value: WidgetMath.clampedFraction(progress), in: 0...1) { + EmptyView() + } + .gaugeStyle(.linearCapacity) + .tint(color) + + Text(WidgetMath.percentString(progress)) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Countdown Widget (All Families) + +/// Event countdown widget +public struct LockScreenCountdownWidget: View { + let targetDate: Date + let eventName: String + let referenceDate: Date + let family: WidgetFamily + + /// - Parameters: + /// - targetDate: The day being counted down to. + /// - eventName: Shown in the rectangular and inline families. + /// - referenceDate: The timeline entry date; pass `entry.date` from your provider. + /// - family: The accessory family to lay out for. + public init( + targetDate: Date, + eventName: String, + referenceDate: Date = Date(), + family: WidgetFamily = .accessoryCircular + ) { + self.targetDate = targetDate + self.eventName = eventName + self.referenceDate = referenceDate + self.family = family + } + + private var daysRemaining: Int { + WidgetMath.daysRemaining(until: targetDate, from: referenceDate) + } + + public var body: some View { + switch family { + case .accessoryCircular: + VStack(spacing: 2) { + Text("\(daysRemaining)") + .font(.system(size: 24, weight: .bold)) + Text("DAYS") + .font(.system(size: 8, weight: .medium)) + } + .background(AccessoryWidgetBackground()) + + case .accessoryRectangular: + VStack(alignment: .leading, spacing: 2) { + Text(eventName) + .font(.caption) + .foregroundStyle(.secondary) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text("\(daysRemaining)") + .font(.title) + .fontWeight(.bold) + .widgetAccentable() + Text(daysRemaining == 1 ? "day remaining" : "days remaining") + .font(.caption) + } + + Text(targetDate, format: .dateTime.month().day().year()) + .font(.caption2) + .foregroundStyle(.secondary) + } + + case .accessoryInline: + Text("\(daysRemaining) \(daysRemaining == 1 ? "day" : "days") until \(eventName)") + + default: + EmptyView() + } + } +} + +// MARK: - Quick Action Widget + +/// Quick action shortcut widget +public struct LockScreenQuickActionWidget: View { + let icon: String + let label: String + let family: WidgetFamily + + public init( + icon: String, + label: String, + family: WidgetFamily = .accessoryCircular + ) { + self.icon = icon + self.label = label + self.family = family + } + + public var body: some View { + switch family { + case .accessoryCircular: + ZStack { + AccessoryWidgetBackground() + + Image(systemName: icon) + .font(.title2) + } + + case .accessoryRectangular: + HStack { + Image(systemName: icon) + .font(.title) + .widgetAccentable() + + Text(label) + .font(.headline) + + Spacer() + + Image(systemName: "chevron.right") + .font(.caption) + .foregroundStyle(.secondary) + } + + case .accessoryInline: + Label(label, systemImage: icon) + + default: + EmptyView() + } + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift new file mode 100644 index 0000000..a9118f9 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/LockScreen/LockScreenSingleFamilyWidgets.swift @@ -0,0 +1,408 @@ +// Lock Screen (accessory family) widget views. +// Accessory widget families do not exist on macOS, so this file is iOS/watchOS only. + +#if os(iOS) || os(watchOS) +import SwiftUI +import WidgetKit + +// MARK: - Lock Screen Widget Families + +/// Supported Lock Screen widget families +public enum LockScreenWidgetFamily: CaseIterable, Sendable { + case accessoryCircular + case accessoryRectangular + case accessoryInline + + public var widgetFamily: WidgetFamily { + switch self { + case .accessoryCircular: + return .accessoryCircular + case .accessoryRectangular: + return .accessoryRectangular + case .accessoryInline: + return .accessoryInline + } + } +} + +// MARK: - Circular Progress Widget + +/// Circular progress indicator widget +public struct CircularProgressWidget: View { + let progress: Double + let icon: String + let title: String + let accentColor: Color + + public init( + progress: Double, + icon: String = "star.fill", + title: String = "", + accentColor: Color = .blue + ) { + self.progress = progress + self.icon = icon + self.title = title + self.accentColor = accentColor + } + + public var body: some View { + Gauge(value: WidgetMath.clampedFraction(progress), in: 0...1) { + Image(systemName: icon) + } currentValueLabel: { + Text(WidgetMath.percentString(progress)) + .font(.system(size: 12, weight: .semibold)) + } + .gaugeStyle(.accessoryCircularCapacity) + .tint(accentColor) + } +} + +// MARK: - Circular Icon Widget + +/// Simple icon display widget +public struct CircularIconWidget: View { + let icon: String + let value: String? + let backgroundColor: Color + + public init( + icon: String, + value: String? = nil, + backgroundColor: Color = .blue + ) { + self.icon = icon + self.value = value + self.backgroundColor = backgroundColor + } + + public var body: some View { + ZStack { + AccessoryWidgetBackground() + + VStack(spacing: 2) { + Image(systemName: icon) + .font(.title3) + + if let value = value { + Text(value) + .font(.system(size: 10, weight: .semibold)) + } + } + } + } +} + +// MARK: - Circular Ring Widget + +/// Activity ring style widget +public struct CircularRingWidget: View { + let innerProgress: Double + let outerProgress: Double + let innerColor: Color + let outerColor: Color + let centerIcon: String + + public init( + innerProgress: Double, + outerProgress: Double, + innerColor: Color = .cyan, + outerColor: Color = .green, + centerIcon: String = "figure.walk" + ) { + self.innerProgress = innerProgress + self.outerProgress = outerProgress + self.innerColor = innerColor + self.outerColor = outerColor + self.centerIcon = centerIcon + } + + public var body: some View { + ZStack { + // Outer ring + Circle() + .stroke(outerColor.opacity(0.3), lineWidth: 4) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(outerProgress)) + .stroke(outerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + + // Inner ring + Circle() + .stroke(innerColor.opacity(0.3), lineWidth: 4) + .padding(6) + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(innerProgress)) + .stroke(innerColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .padding(6) + + // Center icon + Image(systemName: centerIcon) + .font(.body) + } + } +} + +// MARK: - Rectangular Text Widget + +/// Multi-line text widget +public struct RectangularTextWidget: View { + let title: String + let subtitle: String + let detail: String? + let icon: String + + public init( + title: String, + subtitle: String, + detail: String? = nil, + icon: String = "star" + ) { + self.title = title + self.subtitle = subtitle + self.detail = detail + self.icon = icon + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.headline) + .widgetAccentable() + + Text(subtitle) + .font(.caption) + + if let detail = detail { + Text(detail) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + + Spacer() + + Image(systemName: icon) + .font(.title2) + .widgetAccentable() + } + } +} + +// MARK: - Rectangular Stats Widget + +/// Statistics display widget +public struct RectangularStatsWidget: View { + let stats: [(String, String)] + let title: String? + + public init(stats: [(String, String)], title: String? = nil) { + self.stats = stats + self.title = title + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + if let title = title { + Text(title) + .font(.caption2) + .foregroundStyle(.secondary) + } + + HStack { + ForEach(Array(stats.prefix(3).enumerated()), id: \.offset) { _, stat in + VStack(spacing: 2) { + Text(stat.1) + .font(.headline) + .widgetAccentable() + Text(stat.0) + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity) + } + } + } + } +} + +// MARK: - Rectangular Progress Widget + +/// Progress bar widget +public struct RectangularProgressWidget: View { + let title: String + let progress: Double + let currentValue: String + let maxValue: String + let icon: String + + public init( + title: String, + progress: Double, + currentValue: String, + maxValue: String, + icon: String = "chart.bar.fill" + ) { + self.title = title + self.progress = progress + self.currentValue = currentValue + self.maxValue = maxValue + self.icon = icon + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: icon) + .widgetAccentable() + + Text(title) + .font(.caption) + + Spacer() + + Text("\(currentValue)/\(maxValue)") + .font(.caption2) + .foregroundStyle(.secondary) + } + + Gauge(value: WidgetMath.clampedFraction(progress)) { + EmptyView() + } + .gaugeStyle(.accessoryLinearCapacity) + } + } +} + +// MARK: - Rectangular Calendar Widget + +/// Upcoming events widget +public struct RectangularCalendarWidget: View { + let events: [(String, Date)] + + public init(events: [(String, Date)]) { + self.events = events + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + ForEach(Array(events.prefix(2).enumerated()), id: \.offset) { _, event in + HStack { + Circle() + .fill(Color.primary) + .frame(width: 6, height: 6) + + Text(event.0) + .font(.caption) + .lineLimit(1) + + Spacer() + + Text(event.1, style: .time) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + } +} + +// MARK: - Rectangular Weather Widget + +/// Weather conditions widget +public struct RectangularWeatherWidget: View { + let temperature: Int + let high: Int + let low: Int + let condition: String + let location: String + + public init( + temperature: Int, + high: Int, + low: Int, + condition: String = "sun.max.fill", + location: String + ) { + self.temperature = temperature + self.high = high + self.low = low + self.condition = condition + self.location = location + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(location) + .font(.caption2) + .foregroundStyle(.secondary) + + Text("\(temperature)°") + .font(.title) + .fontWeight(.medium) + .widgetAccentable() + + HStack(spacing: 4) { + Text("H:\(high)°") + Text("L:\(low)°") + } + .font(.caption2) + .foregroundStyle(.secondary) + } + + Spacer() + + Image(systemName: condition) + .font(.largeTitle) + .widgetAccentable() + } + } +} + +// MARK: - Inline Text Widget + +/// Simple inline text widget +public struct InlineTextWidget: View { + let icon: String? + let text: String + + public init(icon: String? = nil, text: String) { + self.icon = icon + self.text = text + } + + public var body: some View { + if let icon = icon { + Label(text, systemImage: icon) + } else { + Text(text) + } + } +} + +// MARK: - Inline Stats Widget + +/// Inline statistics widget +public struct InlineStatsWidget: View { + let label: String + let value: String + let icon: String + + public init(label: String, value: String, icon: String) { + self.label = label + self.value = value + self.icon = icon + } + + public var body: some View { + Label { + Text("\(label): \(value)") + } icon: { + Image(systemName: icon) + } + } +} +#endif diff --git a/Sources/WidgetDevelopmentKit/Networking/Endpoint.swift b/Sources/WidgetDevelopmentKit/Networking/Endpoint.swift new file mode 100644 index 0000000..2dce716 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Networking/Endpoint.swift @@ -0,0 +1,65 @@ +import Foundation + +/// A value-type description of an HTTP endpoint that produces a `URLRequest`. +/// +/// ```swift +/// let request = Endpoint(base: apiURL) +/// .appendingPath("v1/quotes/random") +/// .adding(query: "lang", "en") +/// .adding(header: "Accept", "application/json") +/// .request +/// ``` +public struct Endpoint: Hashable, Sendable { + public let base: URL + public private(set) var pathComponents: [String] = [] + public private(set) var queryItems: [URLQueryItem] = [] + public private(set) var headers: [String: String] = [:] + public var method = "GET" + + public init(base: URL) { + self.base = base + } + + /// Appends one or more slash-separated path segments; empty segments are ignored. + public func appendingPath(_ path: String) -> Endpoint { + var copy = self + copy.pathComponents += path.split(separator: "/").map(String.init) + return copy + } + + /// Adds a query item; repeated names are kept in order. + public func adding(query name: String, _ value: String?) -> Endpoint { + var copy = self + copy.queryItems.append(URLQueryItem(name: name, value: value)) + return copy + } + + public func adding(header name: String, _ value: String) -> Endpoint { + var copy = self + copy.headers[name] = value + return copy + } + + /// The full URL, or `nil` if the components do not form a valid URL. + public var url: URL? { + let fullURL = pathComponents.reduce(base) { $0.appending(path: $1) } + guard var components = URLComponents(url: fullURL, resolvingAgainstBaseURL: false) else { + return nil + } + if !queryItems.isEmpty { + components.queryItems = (components.queryItems ?? []) + queryItems + } + return components.url + } + + /// A request for this endpoint, or `nil` if ``url`` is `nil`. + public var request: URLRequest? { + guard let url else { return nil } + var request = URLRequest(url: url) + request.httpMethod = method + for (name, value) in headers { + request.setValue(value, forHTTPHeaderField: name) + } + return request + } +} diff --git a/Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift b/Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift new file mode 100644 index 0000000..379dea5 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Networking/WidgetDataLoader.swift @@ -0,0 +1,137 @@ +import Foundation + +/// Errors thrown by ``WidgetDataLoader``. +public enum WidgetNetworkError: Error, Sendable, Equatable { + /// The response was not an HTTP response. + case invalidResponse + /// The server answered with a non-2xx status code. + case httpStatus(Int) + /// The body could not be decoded into the requested type. + case decodingFailed(String) + /// A transport error, such as no connection or a timeout. + case transport(URLError.Code) + + /// Whether retrying the same request could reasonably succeed. + public var isTransient: Bool { + switch self { + case .httpStatus(let code): + return code == 408 || (500...599).contains(code) + case .transport(let code): + return [.timedOut, .networkConnectionLost, .notConnectedToInternet, .cannotConnectToHost] + .contains(code) + case .invalidResponse, .decodingFailed: + return false + } + } +} + +/// Retry settings sized for a widget extension, which only gets a few seconds of +/// runtime per timeline request. +public struct RetryPolicy: Sendable, Equatable { + /// Total attempts, including the first one. + public let maxAttempts: Int + /// Delay before the second attempt; each later attempt doubles it. + public let initialDelay: Duration + + public init(maxAttempts: Int = 2, initialDelay: Duration = .milliseconds(500)) { + self.maxAttempts = max(maxAttempts, 1) + self.initialDelay = initialDelay + } + + public static let none = RetryPolicy(maxAttempts: 1) + + /// Delay before attempt number `attempt` (1-based); zero for the first attempt. + public func delay(beforeAttempt attempt: Int) -> Duration { + guard attempt > 1 else { return .zero } + return initialDelay * (1 << (attempt - 2)) + } +} + +/// Fetches and decodes JSON for a timeline provider, retrying only transient failures. +/// +/// ```swift +/// let loader = WidgetDataLoader() +/// let quote = try await loader.load(Quote.self, from: URLRequest(url: quoteURL)) +/// ``` +public struct WidgetDataLoader: Sendable { + public typealias Transport = @Sendable (URLRequest) async throws -> (Data, URLResponse) + public typealias Sleep = @Sendable (Duration) async throws -> Void + + private let transport: Transport + private let sleep: Sleep + private let retryPolicy: RetryPolicy + + /// - Parameters: + /// - session: Session used for requests. The default does not wait for connectivity, + /// so a widget without network fails fast and can fall back to cached data. + /// - retryPolicy: How many times to retry transient errors. + public init(session: URLSession = .widgetDefault, retryPolicy: RetryPolicy = RetryPolicy()) { + self.init( + retryPolicy: retryPolicy, + transport: { try await session.data(for: $0) }, + sleep: { try await Task.sleep(for: $0) } + ) + } + + /// Creates a loader with a custom transport, for tests or non-URLSession clients. + public init(retryPolicy: RetryPolicy = RetryPolicy(), transport: @escaping Transport, sleep: @escaping Sleep) { + self.transport = transport + self.sleep = sleep + self.retryPolicy = retryPolicy + } + + /// Loads `request` and decodes the body as `type`. + public func load( + _ type: Value.Type, + from request: URLRequest, + decoder: JSONDecoder = JSONDecoder() + ) async throws -> Value { + let data = try await loadData(from: request) + do { + return try decoder.decode(type, from: data) + } catch { + throw WidgetNetworkError.decodingFailed(String(describing: error)) + } + } + + /// Loads `request` and returns the body of a 2xx response. + public func loadData(from request: URLRequest) async throws -> Data { + var attempt = 1 + while true { + do { + return try await performOnce(request) + } catch let error as WidgetNetworkError where error.isTransient && attempt < retryPolicy.maxAttempts { + attempt += 1 + try await sleep(retryPolicy.delay(beforeAttempt: attempt)) + } + } + } + + private func performOnce(_ request: URLRequest) async throws -> Data { + let data: Data + let response: URLResponse + do { + (data, response) = try await transport(request) + } catch let error as URLError { + throw WidgetNetworkError.transport(error.code) + } + guard let http = response as? HTTPURLResponse else { + throw WidgetNetworkError.invalidResponse + } + guard (200...299).contains(http.statusCode) else { + throw WidgetNetworkError.httpStatus(http.statusCode) + } + return data + } +} + +extension URLSession { + /// An ephemeral session with short timeouts and no waiting for connectivity. + public static let widgetDefault: URLSession = { + let configuration = URLSessionConfiguration.ephemeral + configuration.timeoutIntervalForRequest = 10 + configuration.timeoutIntervalForResource = 15 + configuration.waitsForConnectivity = false + return URLSession(configuration: configuration) + }() +} diff --git a/Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift b/Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift new file mode 100644 index 0000000..90de22e --- /dev/null +++ b/Sources/WidgetDevelopmentKit/StandBy/StandByActivityWidgets.swift @@ -0,0 +1,272 @@ +import SwiftUI +import WidgetKit + +// MARK: - StandBy Fitness Widget + +/// Fitness activity rings for StandBy mode +public struct StandByFitnessWidget: View { + let calories: Double + let caloriesGoal: Double + let exercise: Double + let exerciseGoal: Double + let standing: Double + let standingGoal: Double + + public init( + calories: Double, + caloriesGoal: Double = 500, + exercise: Double, + exerciseGoal: Double = 30, + standing: Double, + standingGoal: Double = 12 + ) { + self.calories = calories + self.caloriesGoal = caloriesGoal + self.exercise = exercise + self.exerciseGoal = exerciseGoal + self.standing = standing + self.standingGoal = standingGoal + } + + public var body: some View { + HStack(spacing: 40) { + // Activity rings + ZStack { + ActivityRing(progress: WidgetMath.fraction(calories, of: caloriesGoal), color: .red, lineWidth: 16) + .frame(width: 140, height: 140) + + ActivityRing(progress: WidgetMath.fraction(exercise, of: exerciseGoal), color: .green, lineWidth: 14) + .frame(width: 108, height: 108) + + ActivityRing(progress: WidgetMath.fraction(standing, of: standingGoal), color: .cyan, lineWidth: 12) + .frame(width: 80, height: 80) + } + + // Stats + VStack(alignment: .leading, spacing: 20) { + StatRow(label: "Move", value: "\(Int(calories))", unit: "CAL", color: .red, progress: WidgetMath.fraction(calories, of: caloriesGoal)) + StatRow(label: "Exercise", value: "\(Int(exercise))", unit: "MIN", color: .green, progress: WidgetMath.fraction(exercise, of: exerciseGoal)) + StatRow(label: "Stand", value: "\(Int(standing))", unit: "HRS", color: .cyan, progress: WidgetMath.fraction(standing, of: standingGoal)) + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } + + struct ActivityRing: View { + let progress: Double + let color: Color + let lineWidth: CGFloat + + var body: some View { + Circle() + .stroke(color.opacity(0.3), lineWidth: lineWidth) + .overlay { + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(progress)) + .stroke( + color, + style: StrokeStyle(lineWidth: lineWidth, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + } + } + } + + struct StatRow: View { + let label: String + let value: String + let unit: String + let color: Color + let progress: Double + + var body: some View { + HStack(spacing: 8) { + Circle() + .fill(color) + .frame(width: 10, height: 10) + + VStack(alignment: .leading, spacing: 2) { + Text(label) + .font(.caption) + .foregroundStyle(.white.opacity(0.7)) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + Text(value) + .font(.title2) + .fontWeight(.bold) + Text(unit) + .font(.caption) + } + .foregroundStyle(.white) + } + } + } + } +} + +// MARK: - StandBy Music Widget + +/// Now playing widget for StandBy mode +public struct StandByMusicWidget: View { + let trackName: String + let artistName: String + let albumName: String + let isPlaying: Bool + let progress: Double + let accentColor: Color + + public init( + trackName: String, + artistName: String, + albumName: String, + isPlaying: Bool = true, + progress: Double = 0.5, + accentColor: Color = .purple + ) { + self.trackName = trackName + self.artistName = artistName + self.albumName = albumName + self.isPlaying = isPlaying + self.progress = progress + self.accentColor = accentColor + } + + public var body: some View { + HStack(spacing: 24) { + // Album art placeholder + RoundedRectangle(cornerRadius: 16) + .fill( + LinearGradient( + colors: [accentColor, accentColor.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .frame(width: 140, height: 140) + .overlay { + Image(systemName: "music.note") + .font(.system(size: 48)) + .foregroundStyle(.white.opacity(0.5)) + } + .shadow(color: accentColor.opacity(0.5), radius: 20) + + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(trackName) + .font(.title2) + .fontWeight(.semibold) + .lineLimit(1) + + Text(artistName) + .font(.title3) + .foregroundStyle(.white.opacity(0.7)) + .lineLimit(1) + + Text(albumName) + .font(.caption) + .foregroundStyle(.white.opacity(0.5)) + .lineLimit(1) + } + .foregroundStyle(.white) + + // Progress bar + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.white.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(accentColor) + .frame(width: geo.size.width * WidgetMath.clampedFraction(progress)) + } + } + .frame(height: 4) + + // Controls + HStack(spacing: 32) { + Image(systemName: "backward.fill") + .font(.title2) + + Image(systemName: isPlaying ? "pause.fill" : "play.fill") + .font(.system(size: 36)) + + Image(systemName: "forward.fill") + .font(.title2) + } + .foregroundStyle(.white) + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } +} + +// MARK: - StandBy Countdown Widget + +/// Event countdown for StandBy mode +public struct StandByCountdownWidget: View { + let eventName: String + let targetDate: Date + let referenceDate: Date + let accentColor: Color + + /// - Parameters: + /// - eventName: Title shown above the countdown. + /// - targetDate: The moment being counted down to. + /// - referenceDate: The timeline entry date the countdown is computed from. + /// Widgets render a static snapshot per entry, so pass `entry.date` here. + /// - accentColor: Color of the digits. + public init( + eventName: String, + targetDate: Date, + referenceDate: Date = Date(), + accentColor: Color = .orange + ) { + self.eventName = eventName + self.targetDate = targetDate + self.referenceDate = referenceDate + self.accentColor = accentColor + } + + public var body: some View { + let remaining = CountdownComponents(from: referenceDate, to: targetDate) + VStack(spacing: 24) { + Text(eventName) + .font(.title2) + .foregroundStyle(.white.opacity(0.8)) + + HStack(spacing: 16) { + CountdownUnit(value: remaining.days, label: "DAYS", color: accentColor) + CountdownUnit(value: remaining.hours, label: "HOURS", color: accentColor) + CountdownUnit(value: remaining.minutes, label: "MIN", color: accentColor) + } + + Text(targetDate, format: .dateTime.month().day().year()) + .font(.caption) + .foregroundStyle(.white.opacity(0.5)) + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } + + struct CountdownUnit: View { + let value: Int + let label: String + let color: Color + + var body: some View { + VStack(spacing: 8) { + Text(String(format: "%02d", value)) + .font(.system(size: 48, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(color) + + Text(label) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.white.opacity(0.6)) + } + } + } +} diff --git a/Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift b/Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift new file mode 100644 index 0000000..b1e38a7 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/StandBy/StandByClock.swift @@ -0,0 +1,218 @@ +import SwiftUI +import WidgetKit + +// MARK: - StandBy View Modifier + +/// View modifier for StandBy mode optimizations +public struct StandByOptimizedModifier: ViewModifier { + @Environment(\.showsWidgetContainerBackground) var showsBackground + @Environment(\.widgetRenderingMode) var renderingMode + + let nightModeColor: Color + + public init(nightModeColor: Color = .red) { + self.nightModeColor = nightModeColor + } + + public func body(content: Content) -> some View { + content + .foregroundStyle(renderingMode == .fullColor ? .primary : nightModeColor) + .containerBackground(for: .widget) { + if showsBackground { + Color.clear + } + } + } +} + +extension View { + public func standByOptimized(nightModeColor: Color = .red) -> some View { + modifier(StandByOptimizedModifier(nightModeColor: nightModeColor)) + } +} + +// MARK: - StandBy Clock Widget + +/// Large clock widget optimized for StandBy mode +public struct StandByClockWidget: View { + let date: Date + let style: ClockStyle + let showDate: Bool + let accentColor: Color + + public enum ClockStyle { + case digital + case analog + case flipClock + case minimal + } + + public init( + date: Date = Date(), + style: ClockStyle = .digital, + showDate: Bool = true, + accentColor: Color = .white + ) { + self.date = date + self.style = style + self.showDate = showDate + self.accentColor = accentColor + } + + public var body: some View { + VStack(spacing: 16) { + switch style { + case .digital: + DigitalClockView(date: date, accentColor: accentColor) + case .analog: + AnalogClockView(date: date, accentColor: accentColor) + case .flipClock: + FlipClockView(date: date, accentColor: accentColor) + case .minimal: + MinimalClockView(date: date, accentColor: accentColor) + } + + if showDate { + Text(date, format: .dateTime.weekday(.wide).month().day()) + .font(.title3) + .foregroundStyle(accentColor.opacity(0.8)) + } + } + .containerBackground(.black.gradient, for: .widget) + } + + struct DigitalClockView: View { + let date: Date + let accentColor: Color + + var body: some View { + Text(date, format: .dateTime.hour().minute()) + .font(.system(size: 96, weight: .thin, design: .rounded)) + .monospacedDigit() + .foregroundStyle(accentColor) + } + } + + struct AnalogClockView: View { + let date: Date + let accentColor: Color + + var hourAngle: Double { + let hour = Calendar.current.component(.hour, from: date) % 12 + let minute = Calendar.current.component(.minute, from: date) + return Double(hour) * 30 + Double(minute) * 0.5 + } + + var minuteAngle: Double { + let minute = Calendar.current.component(.minute, from: date) + return Double(minute) * 6 + } + + var body: some View { + GeometryReader { geo in + let size = min(geo.size.width, geo.size.height) + + ZStack { + // Clock face + Circle() + .stroke(accentColor.opacity(0.3), lineWidth: 3) + + // Hour markers + ForEach(0..<12) { hour in + Rectangle() + .fill(accentColor) + .frame(width: hour % 3 == 0 ? 3 : 1, height: hour % 3 == 0 ? 15 : 8) + .offset(y: -size/2 + 20) + .rotationEffect(.degrees(Double(hour) * 30)) + } + + // Hour hand + RoundedRectangle(cornerRadius: 3) + .fill(accentColor) + .frame(width: 6, height: size * 0.25) + .offset(y: -size * 0.125) + .rotationEffect(.degrees(hourAngle)) + + // Minute hand + RoundedRectangle(cornerRadius: 2) + .fill(accentColor) + .frame(width: 4, height: size * 0.35) + .offset(y: -size * 0.175) + .rotationEffect(.degrees(minuteAngle)) + + // Center dot + Circle() + .fill(accentColor) + .frame(width: 12, height: 12) + } + .frame(width: size, height: size) + .position(x: geo.size.width / 2, y: geo.size.height / 2) + } + .aspectRatio(1, contentMode: .fit) + } + } + + struct FlipClockView: View { + let date: Date + let accentColor: Color + + var hour: String { + let formatter = DateFormatter() + formatter.dateFormat = "HH" + return formatter.string(from: date) + } + + var minute: String { + let formatter = DateFormatter() + formatter.dateFormat = "mm" + return formatter.string(from: date) + } + + var body: some View { + HStack(spacing: 8) { + FlipDigitView(digit: hour, accentColor: accentColor) + + VStack(spacing: 12) { + Circle() + .fill(accentColor) + .frame(width: 10, height: 10) + Circle() + .fill(accentColor) + .frame(width: 10, height: 10) + } + + FlipDigitView(digit: minute, accentColor: accentColor) + } + } + } + + struct FlipDigitView: View { + let digit: String + let accentColor: Color + + var body: some View { + Text(digit) + .font(.system(size: 80, weight: .bold, design: .rounded)) + .monospacedDigit() + .foregroundStyle(accentColor) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(accentColor.opacity(0.15)) + ) + } + } + + struct MinimalClockView: View { + let date: Date + let accentColor: Color + + var body: some View { + Text(date, format: .dateTime.hour().minute()) + .font(.system(size: 120, weight: .ultraLight, design: .rounded)) + .monospacedDigit() + .foregroundStyle(accentColor) + } + } +} diff --git a/Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift b/Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift new file mode 100644 index 0000000..760221a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/StandBy/StandByInfoWidgets.swift @@ -0,0 +1,295 @@ +import SwiftUI +import WidgetKit + +// MARK: - StandBy Photo Widget + +/// Photo display widget for StandBy mode +public struct StandByPhotoWidget: View { + let caption: String? + let date: Date? + let style: PhotoStyle + + public enum PhotoStyle { + case full + case polaroid + case rounded + case minimal + } + + public init( + caption: String? = nil, + date: Date? = nil, + style: PhotoStyle = .full + ) { + self.caption = caption + self.date = date + self.style = style + } + + public var body: some View { + GeometryReader { geo in + VStack(spacing: 0) { + // Photo placeholder + Rectangle() + .fill( + LinearGradient( + colors: [.gray.opacity(0.3), .gray.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .overlay { + Image(systemName: "photo.fill") + .font(.system(size: 48)) + .foregroundStyle(.white.opacity(0.5)) + } + .frame(maxHeight: style == .polaroid ? geo.size.height * 0.8 : .infinity) + + if style == .polaroid { + VStack(spacing: 4) { + if let caption = caption { + Text(caption) + .font(.caption) + .foregroundStyle(.black) + } + if let date = date { + Text(date, format: .dateTime.month().day().year()) + .font(.caption2) + .foregroundStyle(.gray) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(.white) + } + } + .clipShape(RoundedRectangle(cornerRadius: style == .rounded ? 20 : (style == .minimal ? 0 : 8))) + .shadow(radius: style == .polaroid ? 10 : 0) + } + .containerBackground(.black, for: .widget) + } +} + +// MARK: - StandBy Weather Widget + +/// Weather widget optimized for StandBy mode +public struct StandByWeatherWidget: View { + let temperature: Int + let condition: WeatherCondition + let location: String + let high: Int + let low: Int + let hourlyForecast: [HourlyForecast] + + public enum WeatherCondition: String { + case sunny = "sun.max.fill" + case cloudy = "cloud.fill" + case rainy = "cloud.rain.fill" + case stormy = "cloud.bolt.rain.fill" + case snowy = "cloud.snow.fill" + case partlyCloudy = "cloud.sun.fill" + + var color: Color { + switch self { + case .sunny: return .yellow + case .cloudy: return .gray + case .rainy: return .blue + case .stormy: return .purple + case .snowy: return .cyan + case .partlyCloudy: return .orange + } + } + } + + public struct HourlyForecast: Identifiable { + public let id = UUID() + public let hour: String + public let temp: Int + public let condition: WeatherCondition + + public init(hour: String, temp: Int, condition: WeatherCondition) { + self.hour = hour + self.temp = temp + self.condition = condition + } + } + + public init( + temperature: Int, + condition: WeatherCondition, + location: String, + high: Int, + low: Int, + hourlyForecast: [HourlyForecast] = [] + ) { + self.temperature = temperature + self.condition = condition + self.location = location + self.high = high + self.low = low + self.hourlyForecast = hourlyForecast + } + + public var body: some View { + VStack(spacing: 20) { + // Location + Text(location) + .font(.title3) + .foregroundStyle(.white.opacity(0.8)) + + // Main temperature + HStack(alignment: .top, spacing: 16) { + Image(systemName: condition.rawValue) + .font(.system(size: 64)) + .foregroundStyle(condition.color) + + VStack(alignment: .leading) { + Text("\(temperature)°") + .font(.system(size: 72, weight: .thin)) + .foregroundStyle(.white) + + HStack { + Text("H:\(high)°") + Text("L:\(low)°") + } + .font(.title3) + .foregroundStyle(.white.opacity(0.7)) + } + } + + // Hourly forecast + if !hourlyForecast.isEmpty { + HStack(spacing: 16) { + ForEach(hourlyForecast.prefix(5)) { forecast in + VStack(spacing: 8) { + Text(forecast.hour) + .font(.caption) + Image(systemName: forecast.condition.rawValue) + .font(.title3) + .foregroundStyle(forecast.condition.color) + Text("\(forecast.temp)°") + .font(.caption) + .fontWeight(.medium) + } + .foregroundStyle(.white.opacity(0.8)) + } + } + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } +} + +// MARK: - StandBy Calendar Widget + +/// Calendar widget for StandBy mode +public struct StandByCalendarWidget: View { + let date: Date + let events: [CalendarEvent] + let accentColor: Color + + public struct CalendarEvent: Identifiable { + public let id = UUID() + public let title: String + public let time: Date + public let color: Color + + public init(title: String, time: Date, color: Color = .blue) { + self.title = title + self.time = time + self.color = color + } + } + + public init( + date: Date = Date(), + events: [CalendarEvent] = [], + accentColor: Color = .blue + ) { + self.date = date + self.events = events + self.accentColor = accentColor + } + + private var calendar: Calendar { Calendar.current } + + private var grid: MonthGrid { MonthGrid(month: date, calendar: calendar) } + + private var currentDay: Int { + calendar.component(.day, from: date) + } + + public var body: some View { + VStack(spacing: 16) { + // Month header + Text(date, format: .dateTime.month(.wide).year()) + .font(.title2) + .fontWeight(.medium) + .foregroundStyle(.white) + + // Weekday headers + HStack { + ForEach(Array(grid.weekdaySymbols.enumerated()), id: \.offset) { _, day in + Text(day) + .font(.caption) + .foregroundStyle(.white.opacity(0.5)) + .frame(maxWidth: .infinity) + } + } + + // Calendar grid + let weeks = grid.weeks + VStack(spacing: 8) { + ForEach(weeks.indices, id: \.self) { weekIndex in + HStack { + ForEach(0..<7, id: \.self) { dayIndex in + if let day = weeks[weekIndex][dayIndex] { + Text("\(day)") + .font(.caption) + .fontWeight(day == currentDay ? .bold : .regular) + .foregroundStyle(day == currentDay ? .white : .white.opacity(0.8)) + .frame(maxWidth: .infinity) + .padding(4) + .background( + day == currentDay ? + Circle().fill(accentColor) : + Circle().fill(Color.clear) + ) + } else { + Text("") + .frame(maxWidth: .infinity) + } + } + } + } + } + + // Upcoming events + if !events.isEmpty { + VStack(alignment: .leading, spacing: 8) { + ForEach(events.prefix(2)) { event in + HStack(spacing: 8) { + Circle() + .fill(event.color) + .frame(width: 6, height: 6) + + Text(event.title) + .font(.caption) + .lineLimit(1) + + Spacer() + + Text(event.time, format: .dateTime.hour().minute()) + .font(.caption2) + .foregroundStyle(.white.opacity(0.6)) + } + .foregroundStyle(.white) + } + } + } + } + .padding() + .containerBackground(.black.gradient, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift new file mode 100644 index 0000000..8ab11f2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/InfoTemplates.swift @@ -0,0 +1,353 @@ +import SwiftUI +import WidgetKit + +// MARK: - Gradient Card Widget + +/// Beautiful gradient card widget with customizable colors +public struct GradientCardWidget: View { + let title: String + let subtitle: String + let gradient: LinearGradient + let icon: String + let content: () -> Content + + public init( + title: String, + subtitle: String, + gradient: LinearGradient = LinearGradient( + colors: [.blue, .purple], + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + icon: String = "star.fill", + @ViewBuilder content: @escaping () -> Content = { EmptyView() } + ) { + self.title = title + self.subtitle = subtitle + self.gradient = gradient + self.icon = icon + self.content = content + } + + public var body: some View { + ZStack { + gradient + + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: icon) + .font(.title2) + .foregroundStyle(.white.opacity(0.9)) + Spacer() + } + + Spacer() + + Text(title) + .font(.headline) + .foregroundStyle(.white) + + Text(subtitle) + .font(.caption) + .foregroundStyle(.white.opacity(0.8)) + + content() + } + .padding() + } + } +} + +// MARK: - Stats Widget + +/// Statistics widget with progress indicators +public struct StatsWidget: View { + public struct StatItem: Identifiable { + public let id = UUID() + public let title: String + public let value: String + public let progress: Double + public let color: Color + + public init(title: String, value: String, progress: Double, color: Color = .blue) { + self.title = title + self.value = value + self.progress = progress + self.color = color + } + } + + let title: String + let stats: [StatItem] + + public init(title: String, stats: [StatItem]) { + self.title = title + self.stats = stats + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text(title) + .font(.headline) + .foregroundStyle(.primary) + + ForEach(stats) { stat in + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(stat.title) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text(stat.value) + .font(.caption) + .fontWeight(.semibold) + } + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 4) + .fill(stat.color.opacity(0.2)) + + RoundedRectangle(cornerRadius: 4) + .fill(stat.color) + .frame(width: geo.size.width * WidgetMath.clampedFraction(stat.progress)) + } + } + .frame(height: 6) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Weather Widget + +/// Beautiful weather widget with conditions +public struct WeatherWidget: View { + public enum Condition: String, CaseIterable { + case sunny = "sun.max.fill" + case cloudy = "cloud.fill" + case rainy = "cloud.rain.fill" + case stormy = "cloud.bolt.rain.fill" + case snowy = "cloud.snow.fill" + case partlyCloudy = "cloud.sun.fill" + case foggy = "cloud.fog.fill" + case windy = "wind" + + var color: Color { + switch self { + case .sunny: return .yellow + case .cloudy: return .gray + case .rainy: return .blue + case .stormy: return .purple + case .snowy: return .cyan + case .partlyCloudy: return .orange + case .foggy: return .gray + case .windy: return .teal + } + } + } + + let temperature: Int + let condition: Condition + let location: String + let high: Int + let low: Int + + public init( + temperature: Int, + condition: Condition, + location: String, + high: Int, + low: Int + ) { + self.temperature = temperature + self.condition = condition + self.location = location + self.high = high + self.low = low + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(location) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Image(systemName: condition.rawValue) + .font(.title) + .foregroundStyle(condition.color) + } + + Spacer() + + Text("\(temperature)°") + .font(.system(size: 48, weight: .thin)) + + HStack { + Text("H:\(high)°") + Text("L:\(low)°") + } + .font(.caption) + .foregroundStyle(.secondary) + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Finance Widget + +/// Financial overview widget +public struct FinanceWidget: View { + public struct Account: Identifiable { + public let id = UUID() + public let name: String + public let balance: Double + public let change: Double + public let icon: String + + public init(name: String, balance: Double, change: Double, icon: String = "creditcard") { + self.name = name + self.balance = balance + self.change = change + self.icon = icon + } + } + + let totalBalance: Double + let accounts: [Account] + let currency: String + + public init(totalBalance: Double, accounts: [Account], currency: String = "$") { + self.totalBalance = totalBalance + self.accounts = accounts + self.currency = currency + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text("Total Balance") + .font(.caption) + .foregroundStyle(.secondary) + + Text("\(currency)\(totalBalance, specifier: "%.2f")") + .font(.title2) + .fontWeight(.bold) + } + + Divider() + + ForEach(accounts.prefix(2)) { account in + HStack { + Image(systemName: account.icon) + .font(.caption) + .foregroundStyle(.blue) + .frame(width: 24, height: 24) + .background(Color.blue.opacity(0.1)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + VStack(alignment: .leading) { + Text(account.name) + .font(.caption) + Text("\(currency)\(account.balance, specifier: "%.2f")") + .font(.caption2) + .foregroundStyle(.secondary) + } + + Spacer() + + Text("\(account.change >= 0 ? "+" : "")\(account.change, specifier: "%.1f")%") + .font(.caption) + .foregroundStyle(account.change >= 0 ? .green : .red) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Social Stats Widget + +/// Social media statistics widget +public struct SocialStatsWidget: View { + let platform: String + let platformIcon: String + let platformColor: Color + let followers: Int + let following: Int + let posts: Int + let engagement: Double + + public init( + platform: String, + platformIcon: String, + platformColor: Color, + followers: Int, + following: Int, + posts: Int, + engagement: Double + ) { + self.platform = platform + self.platformIcon = platformIcon + self.platformColor = platformColor + self.followers = followers + self.following = following + self.posts = posts + self.engagement = engagement + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: platformIcon) + .foregroundStyle(platformColor) + Text(platform) + .font(.headline) + } + + HStack(spacing: 16) { + StatBox(value: formatNumber(followers), label: "Followers") + StatBox(value: formatNumber(following), label: "Following") + StatBox(value: formatNumber(posts), label: "Posts") + } + + HStack { + Text("Engagement") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text("\(engagement, specifier: "%.1f")%") + .font(.caption) + .fontWeight(.semibold) + .foregroundStyle(.green) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + private func formatNumber(_ num: Int) -> String { + WidgetFormat.compactNumber(num) + } + + struct StatBox: View { + let value: String + let label: String + + var body: some View { + VStack(spacing: 2) { + Text(value) + .font(.headline) + Text(label) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift new file mode 100644 index 0000000..dcebd9a --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/MediaTemplates.swift @@ -0,0 +1,326 @@ +import SwiftUI +import WidgetKit + +// MARK: - Quote Widget + +/// Inspirational quote widget +public struct QuoteWidget: View { + let quote: String + let author: String + let category: String? + + public init(quote: String, author: String, category: String? = nil) { + self.quote = quote + self.author = author + self.category = category + } + + public var body: some View { + VStack(alignment: .leading, spacing: 12) { + if let category = category { + Text(category.uppercased()) + .font(.system(size: 10, weight: .semibold)) + .foregroundStyle(.secondary) + } + + Text("\u{201C}\(quote)\u{201D}") + .font(.system(.body, design: .serif)) + .italic() + .lineLimit(4) + + Spacer() + + HStack { + Text("— \(author)") + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Image(systemName: "quote.closing") + .foregroundStyle(.tertiary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Music Widget + +/// Music player widget with album art placeholder +public struct MusicWidget: View { + let trackName: String + let artistName: String + let albumName: String + let isPlaying: Bool + let progress: Double + + public init( + trackName: String, + artistName: String, + albumName: String, + isPlaying: Bool = true, + progress: Double = 0.5 + ) { + self.trackName = trackName + self.artistName = artistName + self.albumName = albumName + self.isPlaying = isPlaying + self.progress = progress + } + + public var body: some View { + HStack(spacing: 12) { + // Album art placeholder + RoundedRectangle(cornerRadius: 8) + .fill(LinearGradient( + colors: [.purple, .blue], + startPoint: .topLeading, + endPoint: .bottomTrailing + )) + .frame(width: 60, height: 60) + .overlay { + Image(systemName: "music.note") + .font(.title2) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(trackName) + .font(.headline) + .lineLimit(1) + + Text(artistName) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(Color.purple) + .frame(width: geo.size.width * WidgetMath.clampedFraction(progress)) + } + } + .frame(height: 4) + + HStack { + Image(systemName: isPlaying ? "pause.fill" : "play.fill") + Spacer() + Image(systemName: "forward.fill") + } + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Photo Widget + +/// Photo frame widget with customizable styling +public struct PhotoFrameWidget: View { + let imageName: String? + let caption: String? + let date: Date? + let style: FrameStyle + + public enum FrameStyle { + case minimal + case polaroid + case modern + case vintage + } + + public init( + imageName: String? = nil, + caption: String? = nil, + date: Date? = nil, + style: FrameStyle = .modern + ) { + self.imageName = imageName + self.caption = caption + self.date = date + self.style = style + } + + public var body: some View { + VStack(spacing: 0) { + // Image placeholder + Rectangle() + .fill( + LinearGradient( + colors: [.gray.opacity(0.3), .gray.opacity(0.5)], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .overlay { + Image(systemName: "photo") + .font(.largeTitle) + .foregroundStyle(.secondary) + } + + if style == .polaroid, let caption = caption { + VStack(spacing: 4) { + Text(caption) + .font(.caption) + .lineLimit(1) + + if let date = date { + Text(date, style: .date) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + .background(.white) + } + } + .clipShape(RoundedRectangle(cornerRadius: style == .minimal ? 0 : 12)) + .shadow(radius: style == .modern ? 8 : 0) + .containerBackground(.clear, for: .widget) + } +} + +// MARK: - Podcast Widget + +/// Podcast player widget +public struct PodcastWidget: View { + let podcastName: String + let episodeTitle: String + let currentTime: TimeInterval + let duration: TimeInterval + let isPlaying: Bool + + public init( + podcastName: String, + episodeTitle: String, + currentTime: TimeInterval, + duration: TimeInterval, + isPlaying: Bool = false + ) { + self.podcastName = podcastName + self.episodeTitle = episodeTitle + self.currentTime = currentTime + self.duration = duration + self.isPlaying = isPlaying + } + + public var body: some View { + HStack(spacing: 12) { + RoundedRectangle(cornerRadius: 8) + .fill(LinearGradient(colors: [.orange, .pink], startPoint: .top, endPoint: .bottom)) + .frame(width: 50, height: 50) + .overlay { + Image(systemName: "mic.fill") + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + Text(podcastName) + .font(.caption2) + .foregroundStyle(.secondary) + + Text(episodeTitle) + .font(.caption) + .fontWeight(.medium) + .lineLimit(2) + + HStack { + Text(formatTime(currentTime)) + + GeometryReader { geo in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 2) + .fill(Color.secondary.opacity(0.3)) + + RoundedRectangle(cornerRadius: 2) + .fill(Color.orange) + .frame(width: geo.size.width * WidgetMath.fraction(currentTime, of: duration)) + } + } + .frame(height: 3) + + Text(formatTime(duration)) + } + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + private func formatTime(_ interval: TimeInterval) -> String { + WidgetFormat.playbackTime(interval) + } +} + +// MARK: - News Headlines Widget + +/// News headlines widget +public struct NewsWidget: View { + public struct Article: Identifiable { + public let id = UUID() + public let headline: String + public let source: String + public let timeAgo: String + public let category: String + + public init(headline: String, source: String, timeAgo: String, category: String) { + self.headline = headline + self.source = source + self.timeAgo = timeAgo + self.category = category + } + } + + let articles: [Article] + let category: String + + public init(articles: [Article], category: String = "Top Stories") { + self.articles = articles + self.category = category + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(category) + .font(.headline) + Spacer() + Image(systemName: "newspaper.fill") + .foregroundStyle(.red) + } + + ForEach(articles.prefix(3)) { article in + VStack(alignment: .leading, spacing: 4) { + Text(article.headline) + .font(.caption) + .fontWeight(.medium) + .lineLimit(2) + + HStack { + Text(article.source) + Text("•") + Text(article.timeAgo) + } + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + + if article.id != articles.prefix(3).last?.id { + Divider() + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift new file mode 100644 index 0000000..cd53503 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/TimeTemplates.swift @@ -0,0 +1,346 @@ +import SwiftUI +import WidgetKit + +// MARK: - Calendar Widget + +/// Calendar widget showing upcoming events +public struct CalendarWidget: View { + public struct Event: Identifiable { + public let id = UUID() + public let title: String + public let time: Date + public let color: Color + + public init(title: String, time: Date, color: Color = .blue) { + self.title = title + self.time = time + self.color = color + } + } + + let date: Date + let events: [Event] + + public init(date: Date = Date(), events: [Event]) { + self.date = date + self.events = events + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + VStack(alignment: .leading) { + Text(date.formatted(.dateTime.weekday(.wide))) + .font(.caption) + .foregroundStyle(.secondary) + Text(date.formatted(.dateTime.day().month())) + .font(.title2) + .fontWeight(.bold) + } + Spacer() + Image(systemName: "calendar") + .font(.title2) + .foregroundStyle(.blue) + } + + Divider() + + ForEach(events.prefix(3)) { event in + HStack(spacing: 8) { + Circle() + .fill(event.color) + .frame(width: 8, height: 8) + + VStack(alignment: .leading, spacing: 2) { + Text(event.title) + .font(.caption) + .lineLimit(1) + Text(event.time, style: .time) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + + if events.count > 3 { + Text("+\(events.count - 3) more events") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Countdown Widget + +/// Countdown timer widget for events +public struct CountdownWidget: View { + let title: String + let targetDate: Date + let referenceDate: Date + let icon: String + let accentColor: Color + + /// - Parameters: + /// - title: Label next to the icon. + /// - targetDate: The moment being counted down to. + /// - referenceDate: The timeline entry date; pass `entry.date` from your provider. + /// - icon: SF Symbol name. + /// - accentColor: Color of the icon and digits. + public init( + title: String, + targetDate: Date, + referenceDate: Date = Date(), + icon: String = "clock.fill", + accentColor: Color = .orange + ) { + self.title = title + self.targetDate = targetDate + self.referenceDate = referenceDate + self.icon = icon + self.accentColor = accentColor + } + + private var timeRemaining: CountdownComponents { + CountdownComponents(from: referenceDate, to: targetDate) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: icon) + .foregroundStyle(accentColor) + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + } + + HStack(spacing: 16) { + TimeUnitView(value: timeRemaining.days, unit: "DAYS", color: accentColor) + TimeUnitView(value: timeRemaining.hours, unit: "HRS", color: accentColor) + TimeUnitView(value: timeRemaining.minutes, unit: "MIN", color: accentColor) + } + + Text(targetDate, style: .date) + .font(.caption2) + .foregroundStyle(.secondary) + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + struct TimeUnitView: View { + let value: Int + let unit: String + let color: Color + + var body: some View { + VStack(spacing: 2) { + Text("\(value)") + .font(.title2) + .fontWeight(.bold) + .foregroundStyle(color) + Text(unit) + .font(.system(size: 8, weight: .medium)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Clock Widget + +/// Analog/Digital clock widget +public struct ClockWidget: View { + let date: Date + let style: ClockStyle + let showSeconds: Bool + + public enum ClockStyle { + case analog + case digital + case minimal + } + + public init(date: Date = Date(), style: ClockStyle = .digital, showSeconds: Bool = false) { + self.date = date + self.style = style + self.showSeconds = showSeconds + } + + public var body: some View { + VStack { + switch style { + case .analog: + AnalogClockView(date: date) + case .digital: + DigitalClockView(date: date, showSeconds: showSeconds) + case .minimal: + MinimalClockView(date: date) + } + } + .containerBackground(.fill.tertiary, for: .widget) + } + + struct AnalogClockView: View { + let date: Date + + var body: some View { + GeometryReader { geo in + let size = min(geo.size.width, geo.size.height) - 20 + + ZStack { + Circle() + .stroke(Color.secondary.opacity(0.3), lineWidth: 2) + + // Hour markers + ForEach(0..<12) { hour in + Rectangle() + .fill(Color.primary) + .frame(width: 2, height: hour % 3 == 0 ? 10 : 5) + .offset(y: -size/2 + 15) + .rotationEffect(.degrees(Double(hour) * 30)) + } + + // Hour hand + ClockHand(length: size * 0.25, width: 4) + .rotationEffect(.degrees(hourAngle)) + + // Minute hand + ClockHand(length: size * 0.35, width: 3) + .rotationEffect(.degrees(minuteAngle)) + + Circle() + .fill(Color.primary) + .frame(width: 8, height: 8) + } + .frame(width: size, height: size) + .position(x: geo.size.width / 2, y: geo.size.height / 2) + } + .padding() + } + + var hourAngle: Double { + let hour = Calendar.current.component(.hour, from: date) % 12 + let minute = Calendar.current.component(.minute, from: date) + return Double(hour) * 30 + Double(minute) * 0.5 + } + + var minuteAngle: Double { + let minute = Calendar.current.component(.minute, from: date) + return Double(minute) * 6 + } + } + + struct ClockHand: View { + let length: CGFloat + let width: CGFloat + + var body: some View { + RoundedRectangle(cornerRadius: width / 2) + .fill(Color.primary) + .frame(width: width, height: length) + .offset(y: -length / 2) + } + } + + struct DigitalClockView: View { + let date: Date + let showSeconds: Bool + + var body: some View { + VStack(spacing: 4) { + Text(date, format: showSeconds ? .dateTime.hour().minute().second() : .dateTime.hour().minute()) + .font(.system(size: 36, weight: .thin, design: .rounded)) + .monospacedDigit() + + Text(date, format: .dateTime.weekday(.wide).month().day()) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding() + } + } + + struct MinimalClockView: View { + let date: Date + + var body: some View { + Text(date, format: .dateTime.hour().minute()) + .font(.system(size: 48, weight: .ultraLight, design: .rounded)) + .monospacedDigit() + .padding() + } + } +} + +// MARK: - Notes Widget + +/// Quick notes widget with rich formatting +public struct NotesWidget: View { + public struct Note: Identifiable { + public let id = UUID() + public let title: String + public let preview: String + public let date: Date + public let color: Color + + public init(title: String, preview: String, date: Date = Date(), color: Color = .yellow) { + self.title = title + self.preview = preview + self.date = date + self.color = color + } + } + + let notes: [Note] + + public init(notes: [Note]) { + self.notes = notes + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "note.text") + .foregroundStyle(.yellow) + Text("Notes") + .font(.headline) + Spacer() + Text("\(notes.count)") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(notes.prefix(3)) { note in + HStack(spacing: 8) { + RoundedRectangle(cornerRadius: 2) + .fill(note.color) + .frame(width: 3) + + VStack(alignment: .leading, spacing: 2) { + Text(note.title) + .font(.caption) + .fontWeight(.medium) + .lineLimit(1) + + Text(note.preview) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + } + + Spacer() + + Text(note.date, style: .relative) + .font(.system(size: 9)) + .foregroundStyle(.tertiary) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift b/Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift new file mode 100644 index 0000000..2a72616 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Templates/TrackerTemplates.swift @@ -0,0 +1,337 @@ +import SwiftUI +import WidgetKit + +// MARK: - Fitness Widget + +/// Fitness tracking widget with activity rings +public struct FitnessWidget: View { + let calories: Double + let caloriesGoal: Double + let exercise: Double + let exerciseGoal: Double + let standing: Double + let standingGoal: Double + + public init( + calories: Double, + caloriesGoal: Double = 500, + exercise: Double, + exerciseGoal: Double = 30, + standing: Double, + standingGoal: Double = 12 + ) { + self.calories = calories + self.caloriesGoal = caloriesGoal + self.exercise = exercise + self.exerciseGoal = exerciseGoal + self.standing = standing + self.standingGoal = standingGoal + } + + public var body: some View { + HStack { + ZStack { + ActivityRing(progress: WidgetMath.fraction(calories, of: caloriesGoal), color: .red, lineWidth: 12) + .frame(width: 80, height: 80) + + ActivityRing(progress: WidgetMath.fraction(exercise, of: exerciseGoal), color: .green, lineWidth: 10) + .frame(width: 60, height: 60) + + ActivityRing(progress: WidgetMath.fraction(standing, of: standingGoal), color: .cyan, lineWidth: 8) + .frame(width: 44, height: 44) + } + + VStack(alignment: .leading, spacing: 8) { + ActivityStat(label: "Move", value: "\(Int(calories))/\(Int(caloriesGoal))", unit: "CAL", color: .red) + ActivityStat(label: "Exercise", value: "\(Int(exercise))/\(Int(exerciseGoal))", unit: "MIN", color: .green) + ActivityStat(label: "Stand", value: "\(Int(standing))/\(Int(standingGoal))", unit: "HRS", color: .cyan) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } + + struct ActivityRing: View { + let progress: Double + let color: Color + let lineWidth: CGFloat + + var body: some View { + Circle() + .stroke(color.opacity(0.3), lineWidth: lineWidth) + .overlay { + Circle() + .trim(from: 0, to: WidgetMath.clampedFraction(progress)) + .stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) + .rotationEffect(.degrees(-90)) + } + } + } + + struct ActivityStat: View { + let label: String + let value: String + let unit: String + let color: Color + + var body: some View { + HStack(spacing: 4) { + Circle() + .fill(color) + .frame(width: 6, height: 6) + + Text(label) + .font(.system(size: 10)) + .foregroundStyle(.secondary) + + Spacer() + + Text(value) + .font(.system(size: 10, weight: .semibold)) + + Text(unit) + .font(.system(size: 8)) + .foregroundStyle(.secondary) + } + } + } +} + +// MARK: - Habit Tracker Widget + +/// Habit tracking widget with streak display +public struct HabitTrackerWidget: View { + public struct Habit: Identifiable { + public let id = UUID() + public let name: String + public let icon: String + public let color: Color + public let completedDays: [Bool] // Last 7 days + + public init(name: String, icon: String, color: Color, completedDays: [Bool]) { + self.name = name + self.icon = icon + self.color = color + self.completedDays = completedDays + } + + public var streak: Int { + var count = 0 + for completed in completedDays.reversed() { + if completed { count += 1 } else { break } + } + return count + } + } + + let habits: [Habit] + + public init(habits: [Habit]) { + self.habits = habits + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Habits") + .font(.headline) + Spacer() + Text("Last 7 days") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(habits.prefix(3)) { habit in + HStack(spacing: 8) { + Image(systemName: habit.icon) + .font(.caption) + .foregroundStyle(habit.color) + .frame(width: 20) + + Text(habit.name) + .font(.caption) + .lineLimit(1) + + Spacer() + + HStack(spacing: 2) { + ForEach(0..<7, id: \.self) { day in + Circle() + .fill(habit.completedDays.indices.contains(day) && habit.completedDays[day] + ? habit.color + : Color.secondary.opacity(0.3)) + .frame(width: 8, height: 8) + } + } + + Text("🔥\(habit.streak)") + .font(.system(size: 10)) + } + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - To-Do Widget + +/// Task list widget +public struct TodoWidget: View { + public struct Task: Identifiable { + public let id = UUID() + public let title: String + public let isCompleted: Bool + public let priority: Priority + public let dueDate: Date? + + public enum Priority { + case low, medium, high + + var color: Color { + switch self { + case .low: return .blue + case .medium: return .orange + case .high: return .red + } + } + } + + public init(title: String, isCompleted: Bool = false, priority: Priority = .medium, dueDate: Date? = nil) { + self.title = title + self.isCompleted = isCompleted + self.priority = priority + self.dueDate = dueDate + } + } + + let tasks: [Task] + let listName: String + + public init(tasks: [Task], listName: String = "Tasks") { + self.tasks = tasks + self.listName = listName + } + + public var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(listName) + .font(.headline) + Spacer() + let completed = tasks.filter { $0.isCompleted }.count + Text("\(completed)/\(tasks.count)") + .font(.caption) + .foregroundStyle(.secondary) + } + + ForEach(tasks.prefix(4)) { task in + HStack(spacing: 8) { + Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") + .foregroundStyle(task.isCompleted ? .green : task.priority.color) + .font(.body) + + Text(task.title) + .font(.caption) + .strikethrough(task.isCompleted) + .foregroundStyle(task.isCompleted ? .secondary : .primary) + .lineLimit(1) + + Spacer() + + if let dueDate = task.dueDate { + Text(dueDate, style: .relative) + .font(.system(size: 9)) + .foregroundStyle(.secondary) + } + } + } + + if tasks.count > 4 { + Text("+\(tasks.count - 4) more tasks") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .padding() + .containerBackground(.fill.tertiary, for: .widget) + } +} + +// MARK: - Water Tracker Widget + +/// Water intake tracking widget +public struct WaterTrackerWidget: View { + let currentIntake: Double // in ml + let dailyGoal: Double + let glassSize: Double + + public init(currentIntake: Double, dailyGoal: Double = 2000, glassSize: Double = 250) { + self.currentIntake = currentIntake + self.dailyGoal = dailyGoal + self.glassSize = glassSize + } + + private static let maximumGlassIcons = 12 + + var progress: Double { + WidgetMath.fraction(currentIntake, of: dailyGoal) + } + + /// Number of full glasses drunk so far. + var glasses: Int { + guard glassSize > 0, currentIntake.isFinite, currentIntake > 0 else { return 0 } + return Int(currentIntake / glassSize) + } + + /// Number of glasses needed to reach the daily goal, capped so the row fits. + var glassGoal: Int { + guard glassSize > 0, dailyGoal.isFinite, dailyGoal > 0 else { return 0 } + return min(Int((dailyGoal / glassSize).rounded(.up)), Self.maximumGlassIcons) + } + + public var body: some View { + VStack(spacing: 12) { + HStack { + Image(systemName: "drop.fill") + .foregroundStyle(.cyan) + Text("Water") + .font(.headline) + Spacer() + } + + ZStack { + Circle() + .stroke(Color.cyan.opacity(0.2), lineWidth: 8) + + Circle() + .trim(from: 0, to: progress) + .stroke( + LinearGradient(colors: [.cyan, .blue], startPoint: .topLeading, endPoint: .bottomTrailing), + style: StrokeStyle(lineWidth: 8, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + + VStack(spacing: 2) { + Text("\(Int(currentIntake))") + .font(.title2) + .fontWeight(.bold) + Text("/ \(Int(dailyGoal)) ml") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .frame(width: 80, height: 80) + + HStack { + ForEach(0..: View { + let url: URL? + @ViewBuilder let content: () -> Content + + var body: some View { + if let url { + Link(destination: url, label: content) + } else { + content() + } + } +} diff --git a/Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift b/Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift deleted file mode 100644 index c83789b..0000000 --- a/Sources/WidgetDevelopmentKit/Templates/WidgetGallery.swift +++ /dev/null @@ -1,94 +0,0 @@ -import SwiftUI -import WidgetKit - -/// A world-class gallery of pre-built, copy-paste ready Widget Templates. -/// -/// These templates are designed to dominate the iOS 17/18 Widget space with -/// Interactive Widgets and StandBy mode support. -@available(iOS 17.0, macOS 14.0, watchOS 10.0, tvOS 17.0, *) -public enum WidgetGallery { - - /// A high-conversion Crypto Tracker Widget. - public struct CryptoWidget: View { - public let coinName: String - public let price: String - public let trend: String - public let isPositive: Bool - - public init(coinName: String, price: String, trend: String, isPositive: Bool) { - self.coinName = coinName - self.price = price - self.trend = trend - self.isPositive = isPositive - } - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "bitcoinsign.circle.fill") - .foregroundColor(.orange) - Text(coinName) - .font(.headline) - .foregroundColor(.primary) - } - - Spacer() - - Text(price) - .font(.system(.title, design: .rounded).bold()) - .foregroundColor(.primary) - - Text(trend) - .font(.subheadline.bold()) - .foregroundColor(isPositive ? .green : .red) - } - .padding() - .containerBackground(for: .widget) { - #if canImport(UIKit) - Color(uiColor: .systemBackground) - #else - Color.white - #endif - } - } - } - - /// An interactive Health & Fitness Goal Widget. - public struct FitnessWidget: View { - public let steps: Int - public let goal: Int - - public init(steps: Int, goal: Int) { - self.steps = steps - self.goal = goal - } - - public var body: some View { - VStack { - HStack { - Image(systemName: "figure.walk") - .foregroundColor(.blue) - Text("Daily Steps") - .font(.subheadline.bold()) - Spacer() - } - - Spacer() - - Gauge(value: Double(steps), in: 0...Double(goal)) { - Text("\(steps)") - } - .gaugeStyle(.accessoryCircular) - .tint(Gradient(colors: [.blue, .purple])) - } - .padding() - .containerBackground(for: .widget) { - #if canImport(UIKit) - Color(uiColor: .secondarySystemBackground) - #else - Color.gray.opacity(0.1) - #endif - } - } - } -} diff --git a/Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift b/Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift new file mode 100644 index 0000000..3185ce2 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Timeline/TimelineBuilder.swift @@ -0,0 +1,101 @@ +import Foundation +import WidgetKit + +/// Assembles a WidgetKit `Timeline` from entries and a reload policy. +/// +/// Entries are sorted by date when the timeline is built, since WidgetKit expects +/// them in chronological order. +/// +/// ```swift +/// let timeline = TimelineBuilder() +/// .adding(entries) +/// .reloading(.every(minutes: 30), from: now) +/// .build() +/// ``` +public struct TimelineBuilder { + public private(set) var entries: [Entry] + public private(set) var policy: TimelineReloadPolicy + + public init(entries: [Entry] = [], policy: TimelineReloadPolicy = .atEnd) { + self.entries = entries + self.policy = policy + } + + /// Returns a copy with `entry` appended. + public func adding(_ entry: Entry) -> Self { + var copy = self + copy.entries.append(entry) + return copy + } + + /// Returns a copy with `newEntries` appended. + public func adding(_ newEntries: [Entry]) -> Self { + var copy = self + copy.entries.append(contentsOf: newEntries) + return copy + } + + /// Returns a copy using a WidgetKit reload policy. + public func reloading(_ policy: TimelineReloadPolicy) -> Self { + var copy = self + copy.policy = policy + return copy + } + + /// Returns a copy whose reload policy is derived from a `WidgetUpdatePolicy`. + public func reloading( + _ policy: WidgetUpdatePolicy, + from date: Date, + calendar: Calendar = .current + ) -> Self { + reloading(policy.reloadPolicy(from: date, calendar: calendar)) + } + + /// The timeline with entries in chronological order. + public func build() -> Timeline { + Timeline(entries: entries.sorted { $0.date < $1.date }, policy: policy) + } +} + +/// Generates evenly spaced dates for timeline entries. +public enum TimelineSchedule { + /// `count` dates starting at `start`, `interval` seconds apart. + /// Returns an empty array when `count` is not positive or `interval` is not positive. + public static func dates(from start: Date, every interval: TimeInterval, count: Int) -> [Date] { + guard count > 0, interval > 0 else { return [] } + return (0.. [Date] { + guard minutes > 0, start < end else { return [] } + var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: start) + let minute = components.minute ?? 0 + components.minute = minute - minute % minutes + guard var current = calendar.date(from: components) else { return [] } + + var dates: [Date] = [] + while current < end { + if current >= start || dates.isEmpty { + dates.append(max(current, start)) + } + guard let next = calendar.date(byAdding: .minute, value: minutes, to: current) else { break } + current = next + } + return dates + } +} + +extension Timeline { + /// A timeline with one entry. + public static func single(_ entry: EntryType, policy: TimelineReloadPolicy = .atEnd) -> Timeline { + Timeline(entries: [entry], policy: policy) + } +} diff --git a/Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift b/Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift deleted file mode 100644 index 6a873cc..0000000 --- a/Sources/WidgetDevelopmentKit/Timeline/TimelinePrewarmer.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation -import WidgetKit - -/// iOS-Widget-Development-Kit: Smart Timeline Pre-warmer. -/// -/// Intelligently schedules background updates to ensure data is fresh -/// exactly when the user is most likely to check their home screen. -public actor TimelinePrewarmer { - public static let shared = TimelinePrewarmer() - - private init() {} - - /// Schedules a pre-emptive refresh based on usage patterns. - public func scheduleOptimalRefresh(for kind: String) { - print("⏱️ [WidgetKit] Pre-warming timeline for: \\(kind)") - // Logic to calculate usage peak and trigger WidgetCenter.reloadTimelines - WidgetCenter.shared.reloadTimelines(ofKind: kind) - } -} diff --git a/Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift b/Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift new file mode 100644 index 0000000..ddda948 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Timeline/WidgetSnapshotStore.swift @@ -0,0 +1,70 @@ +import Foundation + +/// Stores `Codable` values with an expiry date in `UserDefaults`. +/// +/// The usual setup is an App Group suite shared by the app and its widget +/// extension: the app writes fresh data, and the timeline provider reads it +/// without a network call. +/// +/// ```swift +/// let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.app")!) +/// try store.save(weather, forKey: "weather", lifetime: 15 * 60) +/// let cached: Weather? = store.value(Weather.self, forKey: "weather") +/// ``` +public struct WidgetSnapshotStore: Sendable { + private struct Envelope: Codable { + let value: Value + let expiresAt: Date + } + + // UserDefaults is documented as thread-safe but is not marked Sendable in the SDK. + nonisolated(unsafe) private let defaults: UserDefaults + private let keyPrefix: String + private let now: @Sendable () -> Date + + /// - Parameters: + /// - defaults: Usually `UserDefaults(suiteName:)` for your App Group. + /// - keyPrefix: Namespace for keys written by this store. + /// - now: Clock used for expiry checks; injectable for tests. + public init( + defaults: UserDefaults, + keyPrefix: String = "widget.snapshot.", + now: @escaping @Sendable () -> Date = { Date() } + ) { + self.defaults = defaults + self.keyPrefix = keyPrefix + self.now = now + } + + /// Encodes and stores `value` until `lifetime` seconds from now. + public func save(_ value: Value, forKey key: String, lifetime: TimeInterval) throws { + let envelope = Envelope(value: value, expiresAt: now().addingTimeInterval(lifetime)) + let data = try JSONEncoder().encode(envelope) + defaults.set(data, forKey: keyPrefix + key) + } + + /// The stored value if present, decodable as `Value`, and not expired. + /// Expired entries are removed. + public func value(_ type: Value.Type, forKey key: String) -> Value? { + guard let envelope = envelope(type, forKey: key) else { return nil } + guard envelope.expiresAt > now() else { + removeValue(forKey: key) + return nil + } + return envelope.value + } + + /// The stored value even if it has expired, for showing stale data while a refresh runs. + public func staleValue(_ type: Value.Type, forKey key: String) -> Value? { + envelope(type, forKey: key)?.value + } + + public func removeValue(forKey key: String) { + defaults.removeObject(forKey: keyPrefix + key) + } + + private func envelope(_ type: Value.Type, forKey key: String) -> Envelope? { + guard let data = defaults.data(forKey: keyPrefix + key) else { return nil } + return try? JSONDecoder().decode(Envelope.self, from: data) + } +} diff --git a/Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift b/Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift new file mode 100644 index 0000000..793ca18 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/Timeline/WidgetUpdatePolicy.swift @@ -0,0 +1,53 @@ +import Foundation +import WidgetKit + +/// Describes when a widget's timeline should be reloaded, and converts that into +/// a WidgetKit `TimelineReloadPolicy`. +/// +/// Calendar-based cases (`.nextHour`, `.startOfNextDay`) use calendar arithmetic, so +/// they stay correct across daylight-saving changes where adding 3600 or 86400 +/// seconds would not. +public enum WidgetUpdatePolicy: Sendable { + /// Reload at a fixed date. + case atDate(Date) + /// Reload after a time interval in seconds. + case afterInterval(TimeInterval) + /// Reload at the start of the next clock hour. + case nextHour + /// Reload at midnight of the following day. + case startOfNextDay + /// Reload every `minutes` minutes. + case every(minutes: Int) + /// Never reload automatically; the app calls `WidgetCenter` itself. + case never + /// Compute the date yourself. + case custom(@Sendable (Date) -> Date?) + + /// The next reload date after `date`, or `nil` for `.never`. + public func nextUpdateDate(from date: Date, calendar: Calendar = .current) -> Date? { + switch self { + case .atDate(let target): + return target + case .afterInterval(let interval): + return date.addingTimeInterval(max(interval, 0)) + case .nextHour: + guard let hour = calendar.dateInterval(of: .hour, for: date) else { return nil } + return hour.end + case .startOfNextDay: + let startOfDay = calendar.startOfDay(for: date) + return calendar.date(byAdding: .day, value: 1, to: startOfDay) + case .every(let minutes): + return calendar.date(byAdding: .minute, value: max(minutes, 1), to: date) + case .never: + return nil + case .custom(let compute): + return compute(date) + } + } + + /// The WidgetKit reload policy for a timeline generated at `date`. + public func reloadPolicy(from date: Date, calendar: Calendar = .current) -> TimelineReloadPolicy { + guard let next = nextUpdateDate(from: date, calendar: calendar) else { return .never } + return .after(next) + } +} diff --git a/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift b/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift index df29cb9..c8bcc8b 100644 --- a/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift +++ b/Sources/WidgetDevelopmentKit/UI/WidgetTimelineScrubber.swift @@ -1,52 +1,79 @@ import SwiftUI import WidgetKit -/// A SwiftUI view for scrubbing through a widget's timeline. -/// -/// This tool allows developers to visualize how their widget changes over time. -public struct WidgetTimelineScrubber: View { - public let entries: [T] - public let content: (T) -> Content - - @State private var currentIndex: Int = 0 - - public init(entries: [T], @ViewBuilder content: @escaping (T) -> Content) { +/// A debugging view that steps through a widget's timeline entries. +/// +/// Embed it in an app target or a SwiftUI preview to check how a widget looks at +/// each entry without waiting for WidgetKit to advance the timeline. +/// +/// ```swift +/// WidgetTimelineScrubber(entries: timeline.entries) { entry in +/// MyWidgetView(entry: entry) +/// } +/// ``` +public struct WidgetTimelineScrubber: View { + public let entries: [Entry] + public let widgetSize: CGSize + public let content: (Entry) -> Content + + @State private var selectedIndex = 0 + + /// - Parameters: + /// - entries: The timeline entries to step through. + /// - widgetSize: Frame used for the preview; defaults to a small widget on a 6.1" iPhone. + /// - content: Builds the widget view for one entry. + public init( + entries: [Entry], + widgetSize: CGSize = CGSize(width: 158, height: 158), + @ViewBuilder content: @escaping (Entry) -> Content + ) { self.entries = entries + self.widgetSize = widgetSize self.content = content } - + public var body: some View { VStack(spacing: 20) { if entries.isEmpty { Text("No timeline entries provided") - .foregroundColor(.secondary) + .foregroundStyle(.secondary) } else { - let currentEntry = entries[currentIndex] - + let index = Self.clampedIndex(selectedIndex, count: entries.count) + let entry = entries[index] + VStack(alignment: .leading, spacing: 4) { - Text("Timeline Entry \(currentIndex + 1) of \(entries.count)") + Text("Entry \(index + 1) of \(entries.count)") .font(.caption.bold()) - .foregroundColor(.secondary) - - Text("Date: \(currentEntry.date.description)") + Text(entry.date, format: .dateTime.month().day().hour().minute()) .font(.caption2) - .foregroundColor(.secondary) } - - content(currentEntry) - .frame(width: 158, height: 158) // Standard small widget size + .foregroundStyle(.secondary) + + content(entry) + .frame(width: widgetSize.width, height: widgetSize.height) .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) .shadow(radius: 10) - - Slider(value: Binding( - get: { Double(currentIndex) }, - set: { currentIndex = Int($0) } - ), in: 0...Double(max(0, entries.count - 1)), step: 1) - .padding(.horizontal) + + if entries.count > 1 { + Slider( + value: Binding( + get: { Double(index) }, + set: { selectedIndex = Int($0.rounded()) } + ), + in: 0...Double(entries.count - 1), + step: 1 + ) + .padding(.horizontal) + } } } .padding() - .background(.ultraThinMaterial) - .cornerRadius(20) + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20)) + } + + /// Keeps a stored index valid when the entries array shrinks. + static func clampedIndex(_ index: Int, count: Int) -> Int { + guard count > 0 else { return 0 } + return min(max(index, 0), count - 1) } } diff --git a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/GettingStarted.md b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/GettingStarted.md new file mode 100644 index 0000000..bbe6a76 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/GettingStarted.md @@ -0,0 +1,103 @@ +# Getting Started + +Add the package to a widget extension and build a timeline-driven widget. + +## Overview + +A widget has three parts: a timeline entry, a provider that produces a timeline of +entries, and a SwiftUI view that renders one entry. WidgetDevelopmentKit helps with +the provider (scheduling and reload policy), the data it reads, and the view. + +The `Examples/WidgetKitExample` project in the repository contains the code from this +article as a running app with a widget extension. + +### Add a widget extension + +Widgets live in a widget extension, not in the app target. If your app does not have one +yet, choose **File > New > Target…** in Xcode, pick **Widget Extension**, and give it a +name. Xcode adds the extension and embeds it in the app. + +### Add the package + +1. In Xcode, choose **File > Add Package Dependencies…**. +2. Paste `https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git` into the search + field at the top right and choose **Add Package**. +3. In the **Add to Target** column, choose your widget extension for the + `WidgetDevelopmentKit` library, then choose **Add Package**. Add the library to the app + target as well (**General > Frameworks, Libraries, and Embedded Content**) if the app + writes data for the widget. + +In a `Package.swift`: + +```swift +dependencies: [ + .package(url: "https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git", from: "2.0.0") +] +``` + +### Share data from the app + +The app and the extension run in separate processes. Add the **App Groups** capability to +both targets with the same group identifier, then write data from the app with +``WidgetSnapshotStore``: + +```swift +let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) +try store.save(steps, forKey: "steps", lifetime: 30 * 60) +WidgetCenter.shared.reloadTimelines(ofKind: "steps") +``` + +The store keeps values in `UserDefaults`, which is not encrypted. Do not put credentials +or other secrets in ``WidgetSnapshotStore`` or ``WidgetStateStore``. + +### Build the timeline + +``TimelineSchedule`` produces entry dates and ``TimelineBuilder`` assembles the timeline, +sorting entries and converting a ``WidgetUpdatePolicy`` into WidgetKit's reload policy: + +```swift +func makeTimeline(now: Date) -> Timeline { + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() +} +``` + +`staleValue(_:forKey:)` returns expired data too, which is usually better than an empty +widget; use `value(_:forKey:)` when expired data must not be shown. + +### Render the entry + +Use one of the template views, or compose your own from the chart views: + +```swift +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} +``` + +Views that show time-dependent values, such as ``CountdownWidget`` and +``StandByCountdownWidget``, take a `referenceDate`. Pass the entry's date so each entry +renders the value for its own moment rather than the time the timeline was generated. + +### Preview a timeline + +``WidgetTimelineScrubber`` steps through a timeline's entries in a SwiftUI preview or a +debug screen in the app. The example app uses it to show the steps timeline: + +```swift +WidgetTimelineScrubber(entries: timeline.entries) { entry in + StepsWidgetView(entry: entry) +} +``` diff --git a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/InteractiveWidgets.md b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/InteractiveWidgets.md new file mode 100644 index 0000000..165edb0 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/InteractiveWidgets.md @@ -0,0 +1,66 @@ +# Interactive Widgets + +Handle widget button taps with App Intents and share the resulting state with the app. + +## Overview + +Since iOS 17, `Button` and `Toggle` in a widget can run an `AppIntent`. The intent runs in +the widget extension's process, not in the app, so any state it changes must be stored +where both processes can read it. + +### Configure the App Group + +The intents in this package write to ``WidgetStateStore/shared``. It reads the App Group +identifier from the `WidgetDevelopmentKitAppGroup` key in the running bundle's Info.plist. +Add the key, with the same App Group identifier, to both the app's and the widget +extension's Info.plist. Without the key the store falls back to standard user defaults, +which the app cannot see. + +### Register the package's intents + +The App Intents metadata processor only finds intents and entities in packages that are +listed in an `AppIntentsPackage`: + +```swift +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} +``` + +### Add buttons + +```swift +Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") +} +``` + +| Intent | Effect | +| --------------------- | --------------------------------------- | +| ``ToggleIntent`` | Sets a boolean flag. | +| ``IncrementIntent`` | Adds to a counter, never below zero. | +| ``SetValueIntent`` | Stores an integer, such as a rating. | +| ``RecordActionIntent`` | Queues an action string for the app. | +| ``RefreshWidgetIntent`` | Reloads one widget kind or all widgets. | + +Each intent except ``RefreshWidgetIntent`` reloads all widget timelines after writing, so +the provider can read the new value from the store. + +### Read state in the provider and the app + +```swift +let intake = WidgetStateStore.shared.value("water") +``` + +Work the extension cannot do itself, such as starting a timer the app owns, goes through +``RecordActionIntent``. The app processes the queue when it becomes active: + +```swift +for action in WidgetStateStore.shared.drainPendingActions() { + handle(action) +} +``` + +The queue keeps the 50 most recent actions. diff --git a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/WidgetDevelopmentKit.md b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/WidgetDevelopmentKit.md new file mode 100644 index 0000000..93047d3 --- /dev/null +++ b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.docc/WidgetDevelopmentKit.md @@ -0,0 +1,121 @@ +# ``WidgetDevelopmentKit`` + +SwiftUI views and helpers for building WidgetKit widgets, interactive widgets and Live Activities. + +## Overview + +WidgetDevelopmentKit is a source library for a widget extension. It provides: + +- Ready-made SwiftUI views for Home Screen, Lock Screen and StandBy widgets, and chart views built on Swift Charts. +- Timeline helpers: reload policies based on calendar arithmetic, a value-type timeline builder, and evenly spaced or boundary-aligned entry dates. +- A `Codable` snapshot store with expiry, so the app can hand data to the widget through an App Group. +- App Intents for interactive widget buttons and a shared state store they write to. +- A JSON loader that retries only transient network errors, sized for a widget extension's short runtime. +- ActivityKit attributes and views for ten common Live Activity scenarios (iOS only). + +Lock Screen views and Live Activities compile only on iOS, where accessory widget families and ActivityKit exist. Everything else also builds on macOS, which is where the unit tests run. + +## Topics + +### Essentials + +- +- + +### Timelines and data + +- ``TimelineBuilder`` +- ``TimelineSchedule`` +- ``WidgetUpdatePolicy`` +- ``WidgetSnapshotStore`` +- ``WidgetDataLoader`` +- ``Endpoint`` +- ``RetryPolicy`` +- ``WidgetNetworkError`` + +### Interactive widgets + +- ``WidgetStateStore`` +- ``ToggleIntent`` +- ``IncrementIntent`` +- ``SetValueIntent`` +- ``RecordActionIntent`` +- ``RefreshWidgetIntent`` +- ``InteractiveToggleWidget`` +- ``InteractiveCounterWidget`` +- ``InteractiveTaskListWidget`` +- ``InteractiveQuickActionsWidget`` +- ``InteractiveTimerWidget`` +- ``InteractiveHabitWidget`` +- ``InteractiveRatingWidget`` +- ``InteractiveWaterWidget`` +- ``InteractiveWidgetButtonStyle`` + +### Configuration and deep links + +- ``WidgetDeepLink`` +- ``HexColor`` +- ``RefreshIntervalOption`` +- ``AccentColorOption`` +- ``WidgetDevelopmentKitIntents`` + +### Charts + +- ``WidgetLineChart`` +- ``WidgetSparkline`` +- ``WidgetMultiLineChart`` +- ``WidgetStatsChart`` +- ``WidgetBarChart`` +- ``WidgetHorizontalBarChart`` +- ``WidgetPieChart`` +- ``WidgetProgressRing`` +- ``WidgetActivityChart`` +- ``ChartDataPoint`` +- ``TimeSeriesDataPoint`` + +### Home Screen templates + +- ``GradientCardWidget`` +- ``StatsWidget`` +- ``WeatherWidget`` +- ``FinanceWidget`` +- ``SocialStatsWidget`` +- ``CalendarWidget`` +- ``CountdownWidget`` +- ``ClockWidget`` +- ``NotesWidget`` +- ``QuoteWidget`` +- ``MusicWidget`` +- ``PhotoFrameWidget`` +- ``PodcastWidget`` +- ``NewsWidget`` +- ``FitnessWidget`` +- ``HabitTrackerWidget`` +- ``TodoWidget`` +- ``WaterTrackerWidget`` +- ``BatteryWidget`` +- ``ShortcutsWidget`` + +### StandBy + +- ``StandByClockWidget`` +- ``StandByPhotoWidget`` +- ``StandByWeatherWidget`` +- ``StandByCalendarWidget`` +- ``StandByFitnessWidget`` +- ``StandByMusicWidget`` +- ``StandByCountdownWidget`` +- ``StandByOptimizedModifier`` + +### Live Activities + +- ``LiveActivityStateMachine`` +- ``StateMachineError`` + +### Utilities + +- ``WidgetMath`` +- ``WidgetFormat`` +- ``MonthGrid`` +- ``CountdownComponents`` +- ``WidgetTimelineScrubber`` diff --git a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift b/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift deleted file mode 100644 index 6cb1cd0..0000000 --- a/Sources/WidgetDevelopmentKit/WidgetDevelopmentKit.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -import SwiftUI - -/// WidgetDevelopmentKit provides tools for building modern, high-performance iOS widgets. -@MainActor -public final class WidgetDevelopmentKit { - public static let shared = WidgetDevelopmentKit() - - private init() {} - - public func configure() { - // Setup logic here - } -} diff --git a/Sources/Widgets/WidgetContentView.swift b/Sources/Widgets/WidgetContentView.swift deleted file mode 100644 index 99eb6ca..0000000 --- a/Sources/Widgets/WidgetContentView.swift +++ /dev/null @@ -1,881 +0,0 @@ -import SwiftUI - -// MARK: - Weather Widget Views - -@available(iOS 16.0, *) -public struct WeatherLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading weather...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct WeatherContentView: View { - let weatherData: WeatherData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(weatherData.temperature)°") - .font(.title) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - - Text(weatherData.condition.capitalized) - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - Image(systemName: weatherIcon) - .font(.title2) - .foregroundColor(customization.accentColor) - } - - HStack { - WeatherDetailView(title: "Humidity", value: "\(weatherData.humidity)%") - Spacer() - WeatherDetailView(title: "Wind", value: "\(weatherData.windSpeed) km/h") - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } - - private var weatherIcon: String { - switch weatherData.condition.lowercased() { - case "sunny": return "sun.max.fill" - case "cloudy": return "cloud.fill" - case "rainy": return "cloud.rain.fill" - case "snowy": return "cloud.snow.fill" - default: return "cloud.fill" - } - } -} - -@available(iOS 16.0, *) -public struct WeatherDetailView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct WeatherErrorView: View { - public var body: some View { - VStack { - Image(systemName: "exclamationmark.triangle") - .font(.title2) - .foregroundColor(.orange) - Text("Weather unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Calendar Widget Views - -@available(iOS 16.0, *) -public struct CalendarLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading calendar...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct CalendarContentView: View { - let events: [CalendarEvent] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "calendar") - .foregroundColor(customization.accentColor) - Text("Today's Events") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(events.prefix(3), id: \.title) { event in - CalendarEventRow(event: event, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct CalendarEventRow: View { - let event: CalendarEvent - let customization: WidgetCustomization - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(event.title) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(customization.textColor) - .lineLimit(1) - - Text(event.time) - .font(.caption2) - .foregroundColor(customization.textColor.opacity(0.7)) - } - - Spacer() - - Text(event.location) - .font(.caption2) - .foregroundColor(customization.accentColor) - .lineLimit(1) - } - } -} - -@available(iOS 16.0, *) -public struct CalendarEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "calendar.badge.plus") - .font(.title2) - .foregroundColor(.green) - Text("No events today") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Fitness Widget Views - -@available(iOS 16.0, *) -public struct FitnessLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading fitness data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct FitnessContentView: View { - let fitnessData: FitnessData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(fitnessData.steps)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Steps") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("\(fitnessData.calories)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Calories") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - FitnessMetricView(title: "Active", value: "\(fitnessData.activeMinutes)m") - Spacer() - FitnessMetricView(title: "Workouts", value: "\(fitnessData.workouts)") - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct FitnessMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct FitnessErrorView: View { - public var body: some View { - VStack { - Image(systemName: "heart.slash") - .font(.title2) - .foregroundColor(.red) - Text("Fitness data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - News Widget Views - -@available(iOS 16.0, *) -public struct NewsLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading news...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct NewsContentView: View { - let newsItems: [NewsItem] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "newspaper") - .foregroundColor(customization.accentColor) - Text("Latest News") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(newsItems.prefix(2), id: \.title) { item in - NewsItemRow(item: item, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct NewsItemRow: View { - let item: NewsItem - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - Text(item.title) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(customization.textColor) - .lineLimit(2) - - HStack { - Text(item.category) - .font(.caption2) - .foregroundColor(customization.accentColor) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(customization.accentColor.opacity(0.1)) - .cornerRadius(4) - - Spacer() - } - } - } -} - -@available(iOS 16.0, *) -public struct NewsEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "newspaper") - .font(.title2) - .foregroundColor(.blue) - Text("No news available") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Social Media Widget Views - -@available(iOS 16.0, *) -public struct SocialLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading social updates...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct SocialContentView: View { - let socialUpdates: [SocialUpdate] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "bubble.left.and.bubble.right") - .foregroundColor(customization.accentColor) - Text("Social Updates") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(socialUpdates.prefix(2), id: \.content) { update in - SocialUpdateRow(update: update, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct SocialUpdateRow: View { - let update: SocialUpdate - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - Text(update.platform) - .font(.caption2) - .foregroundColor(customization.accentColor) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(customization.accentColor.opacity(0.1)) - .cornerRadius(4) - - Spacer() - - Text("\(update.likes) likes") - .font(.caption2) - .foregroundColor(.secondary) - } - - Text(update.content) - .font(.caption) - .foregroundColor(customization.textColor) - .lineLimit(2) - } - } -} - -@available(iOS 16.0, *) -public struct SocialEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "bubble.left.and.bubble.right") - .font(.title2) - .foregroundColor(.pink) - Text("No social updates") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Productivity Widget Views - -@available(iOS 16.0, *) -public struct ProductivityLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading productivity data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct ProductivityContentView: View { - let productivityData: ProductivityData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(productivityData.tasksCompleted)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Completed") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("\(productivityData.productivityScore)%") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Score") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - ProductivityMetricView(title: "Remaining", value: "\(productivityData.tasksRemaining)") - Spacer() - ProductivityMetricView(title: "Focus Time", value: "\(productivityData.focusTime)m") - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct ProductivityMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct ProductivityErrorView: View { - public var body: some View { - VStack { - Image(systemName: "chart.line.downtrend.xyaxis") - .font(.title2) - .foregroundColor(.orange) - Text("Productivity data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Entertainment Widget Views - -@available(iOS 16.0, *) -public struct EntertainmentLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading entertainment...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct EntertainmentContentView: View { - let entertainmentItems: [EntertainmentItem] - let customization: WidgetCustomization - - public var body: some View { - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "play.circle") - .foregroundColor(customization.accentColor) - Text("Entertainment") - .font(.headline) - .foregroundColor(customization.textColor) - Spacer() - } - - ForEach(entertainmentItems.prefix(2), id: \.title) { item in - EntertainmentItemRow(item: item, customization: customization) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct EntertainmentItemRow: View { - let item: EntertainmentItem - let customization: WidgetCustomization - - public var body: some View { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text(item.title) - .font(.caption) - .fontWeight(.medium) - .foregroundColor(customization.textColor) - .lineLimit(1) - - Text(item.type) - .font(.caption2) - .foregroundColor(customization.textColor.opacity(0.7)) - } - - Spacer() - - HStack(spacing: 2) { - Image(systemName: "star.fill") - .font(.caption2) - .foregroundColor(.yellow) - Text(String(format: "%.1f", item.rating)) - .font(.caption2) - .foregroundColor(customization.accentColor) - } - } - } -} - -@available(iOS 16.0, *) -public struct EntertainmentEmptyView: View { - public var body: some View { - VStack { - Image(systemName: "play.circle") - .font(.title2) - .foregroundColor(.red) - Text("No entertainment available") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Finance Widget Views - -@available(iOS 16.0, *) -public struct FinanceLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading finance data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct FinanceContentView: View { - let financeData: FinanceData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("$\(Int(financeData.portfolioValue))") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Portfolio") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("$\(Int(financeData.dailyChange))") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(financeData.dailyChange >= 0 ? .green : .red) - Text("Today") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - } - - HStack { - FinanceMetricView(title: "Top Stock", value: financeData.topStock) - Spacer() - FinanceMetricView(title: "Trend", value: financeData.marketTrend) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct FinanceMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct FinanceErrorView: View { - public var body: some View { - VStack { - Image(systemName: "chart.line.downtrend.xyaxis") - .font(.title2) - .foregroundColor(.red) - Text("Finance data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Health Widget Views - -@available(iOS 16.0, *) -public struct HealthLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading health data...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct HealthContentView: View { - let healthData: HealthData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text("\(healthData.heartRate)") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("BPM") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text("\(healthData.sleepHours, specifier: "%.1f")h") - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Sleep") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - HealthMetricView(title: "Water", value: "\(healthData.waterIntake) cups") - Spacer() - HealthMetricView(title: "Mood", value: healthData.mood) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct HealthMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct HealthErrorView: View { - public var body: some View { - VStack { - Image(systemName: "heart.slash") - .font(.title2) - .foregroundColor(.red) - Text("Health data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -// MARK: - Travel Widget Views - -@available(iOS 16.0, *) -public struct TravelLoadingView: View { - public var body: some View { - VStack { - ProgressView() - .scaleEffect(1.5) - Text("Loading travel info...") - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -@available(iOS 16.0, *) -public struct TravelContentView: View { - let travelData: TravelData - let customization: WidgetCustomization - - public var body: some View { - VStack(spacing: 8) { - HStack { - VStack(alignment: .leading) { - Text(travelData.destination) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.textColor) - Text("Destination") - .font(.caption) - .foregroundColor(customization.textColor.opacity(0.8)) - } - - Spacer() - - VStack(alignment: .trailing) { - Text(travelData.departureTime) - .font(.title2) - .fontWeight(.bold) - .foregroundColor(customization.accentColor) - Text("Departure") - .font(.caption) - .foregroundColor(customization.accentColor.opacity(0.8)) - } - } - - HStack { - TravelMetricView(title: "Flight", value: travelData.flightNumber) - Spacer() - TravelMetricView(title: "Weather", value: travelData.weather) - } - } - .padding(customization.padding) - .background(customization.backgroundColor) - .cornerRadius(customization.cornerRadius) - } -} - -@available(iOS 16.0, *) -public struct TravelMetricView: View { - let title: String - let value: String - - public var body: some View { - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2) - .foregroundColor(.secondary) - Text(value) - .font(.caption) - .fontWeight(.medium) - } - } -} - -@available(iOS 16.0, *) -public struct TravelErrorView: View { - public var body: some View { - VStack { - Image(systemName: "airplane") - .font(.title2) - .foregroundColor(.blue) - Text("Travel data unavailable") - .font(.caption) - .foregroundColor(.secondary) - } - } -} \ No newline at end of file diff --git a/Sources/Widgets/WidgetTemplate.swift b/Sources/Widgets/WidgetTemplate.swift deleted file mode 100644 index a39aa57..0000000 --- a/Sources/Widgets/WidgetTemplate.swift +++ /dev/null @@ -1,417 +0,0 @@ -import SwiftUI -import WidgetKit - -/// Base protocol for all widget templates -@available(iOS 16.0, *) -public protocol WidgetTemplate { - /// Unique identifier for the template - var identifier: String { get } - - /// Display name for the template - var displayName: String { get } - - /// Template description - var description: String { get } - - /// Supported widget sizes - var supportedSizes: [WidgetFamily] { get } - - /// Create widget view with configuration - func createWidget(configuration: WidgetConfiguration) -> AnyView - - /// Validate configuration - func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool - - /// Get default configuration - func getDefaultConfiguration() -> WidgetConfiguration -} - -/// Weather widget template -@available(iOS 16.0, *) -public class WeatherWidgetTemplate: WidgetTemplate { - public let identifier = "weather_widget" - public let displayName = "Weather Widget" - public let description = "Display current weather conditions with beautiful animations" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(WeatherWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .weather } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let weatherWidget = WidgetDefinition( - id: "weather_main", - type: .weather, - dataSourceIdentifier: "weather_api", - customization: WidgetCustomization( - backgroundColor: Color.blue.opacity(0.1), - textColor: .primary, - accentColor: .blue, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "weather_config", - templateIdentifier: identifier, - widgets: [weatherWidget], - settings: WidgetSettings(refreshInterval: 300.0, enableAnimations: true) - ) - } -} - -/// Calendar widget template -@available(iOS 16.0, *) -public class CalendarWidgetTemplate: WidgetTemplate { - public let identifier = "calendar_widget" - public let displayName = "Calendar Widget" - public let description = "Show upcoming events and calendar information" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(CalendarWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .calendar } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let calendarWidget = WidgetDefinition( - id: "calendar_main", - type: .calendar, - dataSourceIdentifier: "calendar_events", - customization: WidgetCustomization( - backgroundColor: Color.green.opacity(0.1), - textColor: .primary, - accentColor: .green, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "calendar_config", - templateIdentifier: identifier, - widgets: [calendarWidget], - settings: WidgetSettings(refreshInterval: 600.0, enableAnimations: true) - ) - } -} - -/// Fitness widget template -@available(iOS 16.0, *) -public class FitnessWidgetTemplate: WidgetTemplate { - public let identifier = "fitness_widget" - public let displayName = "Fitness Widget" - public let description = "Track fitness metrics and workout progress" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(FitnessWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .fitness } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let fitnessWidget = WidgetDefinition( - id: "fitness_main", - type: .fitness, - dataSourceIdentifier: "health_kit", - customization: WidgetCustomization( - backgroundColor: Color.orange.opacity(0.1), - textColor: .primary, - accentColor: .orange, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "fitness_config", - templateIdentifier: identifier, - widgets: [fitnessWidget], - settings: WidgetSettings(refreshInterval: 900.0, enableAnimations: true) - ) - } -} - -/// News widget template -@available(iOS 16.0, *) -public class NewsWidgetTemplate: WidgetTemplate { - public let identifier = "news_widget" - public let displayName = "News Widget" - public let description = "Display latest news and headlines" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(NewsWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .news } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let newsWidget = WidgetDefinition( - id: "news_main", - type: .news, - dataSourceIdentifier: "news_api", - customization: WidgetCustomization( - backgroundColor: Color.purple.opacity(0.1), - textColor: .primary, - accentColor: .purple, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "news_config", - templateIdentifier: identifier, - widgets: [newsWidget], - settings: WidgetSettings(refreshInterval: 1800.0, enableAnimations: true) - ) - } -} - -/// Social media widget template -@available(iOS 16.0, *) -public class SocialMediaWidgetTemplate: WidgetTemplate { - public let identifier = "social_media_widget" - public let displayName = "Social Media Widget" - public let description = "Show social media updates and notifications" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(SocialMediaWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .social } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let socialWidget = WidgetDefinition( - id: "social_main", - type: .social, - dataSourceIdentifier: "social_api", - customization: WidgetCustomization( - backgroundColor: Color.pink.opacity(0.1), - textColor: .primary, - accentColor: .pink, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "social_config", - templateIdentifier: identifier, - widgets: [socialWidget], - settings: WidgetSettings(refreshInterval: 1200.0, enableAnimations: true) - ) - } -} - -/// Productivity widget template -@available(iOS 16.0, *) -public class ProductivityWidgetTemplate: WidgetTemplate { - public let identifier = "productivity_widget" - public let displayName = "Productivity Widget" - public let description = "Track tasks, goals, and productivity metrics" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(ProductivityWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .productivity } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let productivityWidget = WidgetDefinition( - id: "productivity_main", - type: .productivity, - dataSourceIdentifier: "productivity_data", - customization: WidgetCustomization( - backgroundColor: Color.indigo.opacity(0.1), - textColor: .primary, - accentColor: .indigo, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "productivity_config", - templateIdentifier: identifier, - widgets: [productivityWidget], - settings: WidgetSettings(refreshInterval: 600.0, enableAnimations: true) - ) - } -} - -/// Entertainment widget template -@available(iOS 16.0, *) -public class EntertainmentWidgetTemplate: WidgetTemplate { - public let identifier = "entertainment_widget" - public let displayName = "Entertainment Widget" - public let description = "Show entertainment content and recommendations" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(EntertainmentWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .entertainment } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let entertainmentWidget = WidgetDefinition( - id: "entertainment_main", - type: .entertainment, - dataSourceIdentifier: "entertainment_api", - customization: WidgetCustomization( - backgroundColor: Color.red.opacity(0.1), - textColor: .primary, - accentColor: .red, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "entertainment_config", - templateIdentifier: identifier, - widgets: [entertainmentWidget], - settings: WidgetSettings(refreshInterval: 3600.0, enableAnimations: true) - ) - } -} - -/// Finance widget template -@available(iOS 16.0, *) -public class FinanceWidgetTemplate: WidgetTemplate { - public let identifier = "finance_widget" - public let displayName = "Finance Widget" - public let description = "Display financial data and market information" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(FinanceWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .finance } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let financeWidget = WidgetDefinition( - id: "finance_main", - type: .finance, - dataSourceIdentifier: "finance_api", - customization: WidgetCustomization( - backgroundColor: Color.green.opacity(0.1), - textColor: .primary, - accentColor: .green, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "finance_config", - templateIdentifier: identifier, - widgets: [financeWidget], - settings: WidgetSettings(refreshInterval: 300.0, enableAnimations: true) - ) - } -} - -/// Health widget template -@available(iOS 16.0, *) -public class HealthWidgetTemplate: WidgetTemplate { - public let identifier = "health_widget" - public let displayName = "Health Widget" - public let description = "Monitor health metrics and wellness data" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(HealthWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .health } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let healthWidget = WidgetDefinition( - id: "health_main", - type: .health, - dataSourceIdentifier: "health_kit", - customization: WidgetCustomization( - backgroundColor: Color.teal.opacity(0.1), - textColor: .primary, - accentColor: .teal, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "health_config", - templateIdentifier: identifier, - widgets: [healthWidget], - settings: WidgetSettings(refreshInterval: 1800.0, enableAnimations: true) - ) - } -} - -/// Travel widget template -@available(iOS 16.0, *) -public class TravelWidgetTemplate: WidgetTemplate { - public let identifier = "travel_widget" - public let displayName = "Travel Widget" - public let description = "Show travel information and trip details" - public let supportedSizes: [WidgetFamily] = [.systemSmall, .systemMedium, .systemLarge] - - public func createWidget(configuration: WidgetConfiguration) -> AnyView { - return AnyView(TravelWidgetView(configuration: configuration)) - } - - public func validateConfiguration(_ configuration: WidgetConfiguration) -> Bool { - return configuration.widgets.contains { $0.type == .travel } - } - - public func getDefaultConfiguration() -> WidgetConfiguration { - let travelWidget = WidgetDefinition( - id: "travel_main", - type: .travel, - dataSourceIdentifier: "travel_api", - customization: WidgetCustomization( - backgroundColor: Color.cyan.opacity(0.1), - textColor: .primary, - accentColor: .cyan, - cornerRadius: 16, - padding: EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) - ) - ) - - return WidgetConfiguration( - id: "travel_config", - templateIdentifier: identifier, - widgets: [travelWidget], - settings: WidgetSettings(refreshInterval: 900.0, enableAnimations: true) - ) - } -} \ No newline at end of file diff --git a/Sources/Widgets/WidgetViews.swift b/Sources/Widgets/WidgetViews.swift deleted file mode 100644 index e757870..0000000 --- a/Sources/Widgets/WidgetViews.swift +++ /dev/null @@ -1,506 +0,0 @@ -import SwiftUI -import WidgetKit - -/// Weather widget view -@available(iOS 16.0, *) -public struct WeatherWidgetView: View { - let configuration: WidgetConfiguration - @State private var weatherData: WeatherData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - WeatherLoadingView() - } else if let weatherData = weatherData { - WeatherContentView(weatherData: weatherData, customization: getCustomization()) - } else { - WeatherErrorView() - } - } - .onAppear { - loadWeatherData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadWeatherData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.weatherData = WeatherData( - temperature: 22, - condition: "sunny", - humidity: 65, - windSpeed: 12 - ) - self.isLoading = false - } - } -} - -/// Calendar widget view -@available(iOS 16.0, *) -public struct CalendarWidgetView: View { - let configuration: WidgetConfiguration - @State private var events: [CalendarEvent] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - CalendarLoadingView() - } else if !events.isEmpty { - CalendarContentView(events: events, customization: getCustomization()) - } else { - CalendarEmptyView() - } - } - .onAppear { - loadCalendarEvents() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadCalendarEvents() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.events = [ - CalendarEvent(title: "Team Meeting", time: "10:00 AM", location: "Conference Room"), - CalendarEvent(title: "Lunch", time: "12:30 PM", location: "Cafeteria"), - CalendarEvent(title: "Project Review", time: "3:00 PM", location: "Office") - ] - self.isLoading = false - } - } -} - -/// Fitness widget view -@available(iOS 16.0, *) -public struct FitnessWidgetView: View { - let configuration: WidgetConfiguration - @State private var fitnessData: FitnessData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - FitnessLoadingView() - } else if let fitnessData = fitnessData { - FitnessContentView(fitnessData: fitnessData, customization: getCustomization()) - } else { - FitnessErrorView() - } - } - .onAppear { - loadFitnessData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadFitnessData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.fitnessData = FitnessData( - steps: 8420, - calories: 320, - activeMinutes: 45, - workouts: 2 - ) - self.isLoading = false - } - } -} - -/// News widget view -@available(iOS 16.0, *) -public struct NewsWidgetView: View { - let configuration: WidgetConfiguration - @State private var newsItems: [NewsItem] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - NewsLoadingView() - } else if !newsItems.isEmpty { - NewsContentView(newsItems: newsItems, customization: getCustomization()) - } else { - NewsEmptyView() - } - } - .onAppear { - loadNewsData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadNewsData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.newsItems = [ - NewsItem(title: "Tech Innovation", summary: "Latest developments in AI technology", category: "Technology"), - NewsItem(title: "Market Update", summary: "Stock market trends and analysis", category: "Finance"), - NewsItem(title: "Health News", summary: "New medical breakthroughs", category: "Health") - ] - self.isLoading = false - } - } -} - -/// Social media widget view -@available(iOS 16.0, *) -public struct SocialMediaWidgetView: View { - let configuration: WidgetConfiguration - @State private var socialUpdates: [SocialUpdate] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - SocialLoadingView() - } else if !socialUpdates.isEmpty { - SocialContentView(socialUpdates: socialUpdates, customization: getCustomization()) - } else { - SocialEmptyView() - } - } - .onAppear { - loadSocialData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadSocialData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.socialUpdates = [ - SocialUpdate(platform: "Twitter", content: "New product launch!", likes: 150), - SocialUpdate(platform: "Instagram", content: "Beautiful sunset", likes: 89), - SocialUpdate(platform: "LinkedIn", content: "Professional achievement", likes: 45) - ] - self.isLoading = false - } - } -} - -/// Productivity widget view -@available(iOS 16.0, *) -public struct ProductivityWidgetView: View { - let configuration: WidgetConfiguration - @State private var productivityData: ProductivityData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - ProductivityLoadingView() - } else if let productivityData = productivityData { - ProductivityContentView(productivityData: productivityData, customization: getCustomization()) - } else { - ProductivityErrorView() - } - } - .onAppear { - loadProductivityData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadProductivityData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.productivityData = ProductivityData( - tasksCompleted: 8, - tasksRemaining: 3, - focusTime: 240, - productivityScore: 85 - ) - self.isLoading = false - } - } -} - -/// Entertainment widget view -@available(iOS 16.0, *) -public struct EntertainmentWidgetView: View { - let configuration: WidgetConfiguration - @State private var entertainmentItems: [EntertainmentItem] = [] - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - EntertainmentLoadingView() - } else if !entertainmentItems.isEmpty { - EntertainmentContentView(entertainmentItems: entertainmentItems, customization: getCustomization()) - } else { - EntertainmentEmptyView() - } - } - .onAppear { - loadEntertainmentData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadEntertainmentData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.entertainmentItems = [ - EntertainmentItem(title: "New Movie", type: "Movie", rating: 4.5), - EntertainmentItem(title: "Popular Series", type: "TV Show", rating: 4.8), - EntertainmentItem(title: "Trending Game", type: "Game", rating: 4.2) - ] - self.isLoading = false - } - } -} - -/// Finance widget view -@available(iOS 16.0, *) -public struct FinanceWidgetView: View { - let configuration: WidgetConfiguration - @State private var financeData: FinanceData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - FinanceLoadingView() - } else if let financeData = financeData { - FinanceContentView(financeData: financeData, customization: getCustomization()) - } else { - FinanceErrorView() - } - } - .onAppear { - loadFinanceData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadFinanceData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.financeData = FinanceData( - portfolioValue: 125000, - dailyChange: 1250, - topStock: "AAPL", - marketTrend: "Bullish" - ) - self.isLoading = false - } - } -} - -/// Health widget view -@available(iOS 16.0, *) -public struct HealthWidgetView: View { - let configuration: WidgetConfiguration - @State private var healthData: HealthData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - HealthLoadingView() - } else if let healthData = healthData { - HealthContentView(healthData: healthData, customization: getCustomization()) - } else { - HealthErrorView() - } - } - .onAppear { - loadHealthData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadHealthData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.healthData = HealthData( - heartRate: 72, - sleepHours: 7.5, - waterIntake: 8, - mood: "Good" - ) - self.isLoading = false - } - } -} - -/// Travel widget view -@available(iOS 16.0, *) -public struct TravelWidgetView: View { - let configuration: WidgetConfiguration - @State private var travelData: TravelData? - @State private var isLoading = true - - public init(configuration: WidgetConfiguration) { - self.configuration = configuration - } - - public var body: some View { - Group { - if isLoading { - TravelLoadingView() - } else if let travelData = travelData { - TravelContentView(travelData: travelData, customization: getCustomization()) - } else { - TravelErrorView() - } - } - .onAppear { - loadTravelData() - } - } - - private func getCustomization() -> WidgetCustomization { - return configuration.widgets.first?.customization ?? WidgetCustomization() - } - - private func loadTravelData() { - // Simulate API call - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.travelData = TravelData( - destination: "Paris", - departureTime: "10:30 AM", - flightNumber: "AF123", - weather: "Sunny" - ) - self.isLoading = false - } - } -} - -// MARK: - Data Models - -public struct WeatherData { - let temperature: Int - let condition: String - let humidity: Int - let windSpeed: Int -} - -public struct CalendarEvent { - let title: String - let time: String - let location: String -} - -public struct FitnessData { - let steps: Int - let calories: Int - let activeMinutes: Int - let workouts: Int -} - -public struct NewsItem { - let title: String - let summary: String - let category: String -} - -public struct SocialUpdate { - let platform: String - let content: String - let likes: Int -} - -public struct ProductivityData { - let tasksCompleted: Int - let tasksRemaining: Int - let focusTime: Int - let productivityScore: Int -} - -public struct EntertainmentItem { - let title: String - let type: String - let rating: Double -} - -public struct FinanceData { - let portfolioValue: Double - let dailyChange: Double - let topStock: String - let marketTrend: String -} - -public struct HealthData { - let heartRate: Int - let sleepHours: Double - let waterIntake: Int - let mood: String -} - -public struct TravelData { - let destination: String - let departureTime: String - let flightNumber: String - let weather: String -} \ No newline at end of file diff --git a/Templates/ServiceTemplate.swift b/Templates/ServiceTemplate.swift deleted file mode 100644 index 4055145..0000000 --- a/Templates/ServiceTemplate.swift +++ /dev/null @@ -1,126 +0,0 @@ -// MARK: - Widget Service Template -// Use this template for creating Widget data services - -import Foundation -import WidgetKit - -// MARK: - Protocol -protocol __NAME__WidgetServiceProtocol: Sendable { - func fetchData() async throws -> __TYPE__ - func refreshWidget() -} - -// MARK: - Service Implementation -final class __NAME__WidgetService: __NAME__WidgetServiceProtocol, @unchecked Sendable { - // MARK: - Singleton - static let shared = __NAME__WidgetService() - - // MARK: - Properties - private let networkClient: NetworkClientProtocol - private let cache: WidgetCacheProtocol - private let userDefaults: UserDefaults - - // MARK: - Constants - private enum Constants { - static let cacheKey = "__NAME__WidgetCache" - static let suiteName = "group.com.yourcompany.widget" - static let cacheExpiration: TimeInterval = 3600 // 1 hour - } - - // MARK: - Initialization - init( - networkClient: NetworkClientProtocol = NetworkClient.shared, - cache: WidgetCacheProtocol = WidgetCache.shared, - userDefaults: UserDefaults = UserDefaults(suiteName: Constants.suiteName)! - ) { - self.networkClient = networkClient - self.cache = cache - self.userDefaults = userDefaults - } - - // MARK: - Fetch Data - func fetchData() async throws -> __TYPE__ { - // Try cache first - if let cached = loadFromCache(), !isCacheExpired() { - return cached - } - - // Fetch from network - let data = try await fetchFromNetwork() - saveToCache(data) - return data - } - - // MARK: - Refresh Widget - func refreshWidget() { - WidgetCenter.shared.reloadTimelines(ofKind: "__NAME__Widget") - } - - func refreshAllWidgets() { - WidgetCenter.shared.reloadAllTimelines() - } - - // MARK: - Cache Management - private func loadFromCache() -> __TYPE__? { - guard let data = userDefaults.data(forKey: Constants.cacheKey) else { - return nil - } - return try? JSONDecoder().decode(__TYPE__.self, from: data) - } - - private func saveToCache(_ value: __TYPE__) { - guard let data = try? JSONEncoder().encode(value) else { return } - userDefaults.set(data, forKey: Constants.cacheKey) - userDefaults.set(Date(), forKey: "\(Constants.cacheKey)_timestamp") - } - - private func isCacheExpired() -> Bool { - guard let timestamp = userDefaults.object(forKey: "\(Constants.cacheKey)_timestamp") as? Date else { - return true - } - return Date().timeIntervalSince(timestamp) > Constants.cacheExpiration - } - - private func clearCache() { - userDefaults.removeObject(forKey: Constants.cacheKey) - userDefaults.removeObject(forKey: "\(Constants.cacheKey)_timestamp") - } - - // MARK: - Network - private func fetchFromNetwork() async throws -> __TYPE__ { - let url = URL(string: "https://api.example.com/__ENDPOINT__")! - return try await networkClient.get(url) - } -} - -// MARK: - Widget Cache Protocol -protocol WidgetCacheProtocol { - func get(for key: String) -> T? - func set(_ value: T, for key: String) - func remove(for key: String) -} - -// MARK: - Widget Cache Implementation -final class WidgetCache: WidgetCacheProtocol { - static let shared = WidgetCache() - - private let userDefaults: UserDefaults - - init(userDefaults: UserDefaults = UserDefaults(suiteName: "group.com.yourcompany.widget")!) { - self.userDefaults = userDefaults - } - - func get(for key: String) -> T? { - guard let data = userDefaults.data(forKey: key) else { return nil } - return try? JSONDecoder().decode(T.self, from: data) - } - - func set(_ value: T, for key: String) { - guard let data = try? JSONEncoder().encode(value) else { return } - userDefaults.set(data, forKey: key) - } - - func remove(for key: String) { - userDefaults.removeObject(forKey: key) - } -} diff --git a/Templates/TestTemplate.swift b/Templates/TestTemplate.swift deleted file mode 100644 index d42755d..0000000 --- a/Templates/TestTemplate.swift +++ /dev/null @@ -1,152 +0,0 @@ -// MARK: - Widget Test Template -// Use this template for testing Widget components - -import XCTest -import WidgetKit -@testable import __MODULE__ - -final class __NAME__WidgetTests: XCTestCase { - // MARK: - Properties - private var sut: __NAME__Provider! - private var mockService: Mock__NAME__Service! - - // MARK: - Setup & Teardown - override func setUp() { - super.setUp() - mockService = Mock__NAME__Service() - sut = __NAME__Provider(service: mockService) - } - - override func tearDown() { - sut = nil - mockService = nil - super.tearDown() - } - - // MARK: - Placeholder Tests - func test_placeholder_returnsValidEntry() { - // Given - let context = MockTimelineProviderContext() - - // When - let entry = sut.placeholder(in: context) - - // Then - XCTAssertNotNil(entry) - XCTAssertNotNil(entry.date) - } - - // MARK: - Snapshot Tests - func test_getSnapshot_returnsEntryImmediately() { - // Given - let context = MockTimelineProviderContext() - let expectation = expectation(description: "Snapshot returned") - - // When - sut.getSnapshot(in: context) { entry in - // Then - XCTAssertNotNil(entry) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - } - - // MARK: - Timeline Tests - func test_getTimeline_whenSuccessful_returnsEntriesWithPolicy() { - // Given - let context = MockTimelineProviderContext() - let expectation = expectation(description: "Timeline returned") - mockService.fetchResult = .success(.mock) - - // When - sut.getTimeline(in: context) { timeline in - // Then - XCTAssertFalse(timeline.entries.isEmpty) - XCTAssertEqual(timeline.policy, .atEnd) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - func test_getTimeline_whenFails_returnsPlaceholderEntry() { - // Given - let context = MockTimelineProviderContext() - let expectation = expectation(description: "Timeline returned") - mockService.fetchResult = .failure(NSError(domain: "test", code: 1)) - - // When - sut.getTimeline(in: context) { timeline in - // Then - XCTAssertEqual(timeline.entries.count, 1) - expectation.fulfill() - } - - wait(for: [expectation], timeout: 5.0) - } - - // MARK: - Entry Tests - func test_entry_formatsDateCorrectly() { - // Given - let date = Date() - let data = __NAME__Data.mock - - // When - let entry = __NAME__Entry(date: date, data: data) - - // Then - XCTAssertEqual(entry.date, date) - XCTAssertEqual(entry.data.title, data.title) - } - - // MARK: - View Tests - func test_widgetView_rendersWithoutCrash() { - // Given - let entry = __NAME__Entry(date: Date(), data: .mock) - - // When/Then - should not crash - let view = __NAME__WidgetEntryView(entry: entry) - XCTAssertNotNil(view) - } -} - -// MARK: - Mock Objects -final class Mock__NAME__Service: __NAME__ServiceProtocol { - var fetchCalled = false - var fetchResult: Result<__TYPE__, Error> = .success(.mock) - - func fetchData() async throws -> __TYPE__ { - fetchCalled = true - return try fetchResult.get() - } - - func refreshWidget() { - // No-op for testing - } -} - -final class MockTimelineProviderContext: TimelineProviderContext { - var family: WidgetFamily { .systemSmall } - var isPreview: Bool { false } - var displaySize: CGSize { CGSize(width: 169, height: 169) } - var environmentVariants: EnvironmentVariants { [:] } -} - -// MARK: - Test Data -extension __NAME__Data { - static var mock: __NAME__Data { - __NAME__Data( - title: "Test Title", - subtitle: "Test Subtitle", - value: 42.0 - ) - } -} - -extension __TYPE__ { - static var mock: __TYPE__ { - // Return mock instance - fatalError("Implement mock data") - } -} diff --git a/Templates/ViewModelTemplate.swift b/Templates/ViewModelTemplate.swift deleted file mode 100644 index 14a2b6a..0000000 --- a/Templates/ViewModelTemplate.swift +++ /dev/null @@ -1,138 +0,0 @@ -// MARK: - Widget ViewModel Template -// Use this template for creating new Widget ViewModels - -import Foundation -import WidgetKit -import SwiftUI - -// MARK: - Timeline Provider -struct __NAME__Provider: TimelineProvider { - typealias Entry = __NAME__Entry - - // MARK: - Placeholder - func placeholder(in context: Context) -> __NAME__Entry { - __NAME__Entry(date: Date(), data: .placeholder) - } - - // MARK: - Snapshot - func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) { - let entry = __NAME__Entry(date: Date(), data: .placeholder) - completion(entry) - } - - // MARK: - Timeline - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - Task { - do { - let data = try await fetchData() - let entry = __NAME__Entry(date: Date(), data: data) - let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())! - let timeline = Timeline(entries: [entry], policy: .after(nextUpdate)) - completion(timeline) - } catch { - let entry = __NAME__Entry(date: Date(), data: .placeholder) - let timeline = Timeline(entries: [entry], policy: .after(Date().addingTimeInterval(300))) - completion(timeline) - } - } - } - - // MARK: - Data Fetching - private func fetchData() async throws -> __NAME__Data { - // Implement data fetching logic - return .placeholder - } -} - -// MARK: - Entry -struct __NAME__Entry: TimelineEntry { - let date: Date - let data: __NAME__Data -} - -// MARK: - Data Model -struct __NAME__Data { - let title: String - let subtitle: String - let value: Double - - static var placeholder: __NAME__Data { - __NAME__Data(title: "Loading...", subtitle: "", value: 0) - } -} - -// MARK: - Widget View -struct __NAME__WidgetEntryView: View { - var entry: __NAME__Provider.Entry - @Environment(\.widgetFamily) var family - - var body: some View { - switch family { - case .systemSmall: - SmallWidgetView(entry: entry) - case .systemMedium: - MediumWidgetView(entry: entry) - case .systemLarge: - LargeWidgetView(entry: entry) - default: - SmallWidgetView(entry: entry) - } - } -} - -// MARK: - Size-Specific Views -private struct SmallWidgetView: View { - let entry: __NAME__Entry - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text(entry.data.title) - .font(.headline) - Text(entry.data.subtitle) - .font(.caption) - .foregroundColor(.secondary) - } - .padding() - } -} - -private struct MediumWidgetView: View { - let entry: __NAME__Entry - - var body: some View { - HStack { - SmallWidgetView(entry: entry) - Spacer() - Text("\(entry.data.value, specifier: "%.1f")") - .font(.largeTitle) - .bold() - } - .padding() - } -} - -private struct LargeWidgetView: View { - let entry: __NAME__Entry - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - MediumWidgetView(entry: entry) - // Add more content for large widget - } - .padding() - } -} - -// MARK: - Widget Definition -struct __NAME__Widget: Widget { - let kind: String = "__NAME__Widget" - - var body: some WidgetConfiguration { - StaticConfiguration(kind: kind, provider: __NAME__Provider()) { entry in - __NAME__WidgetEntryView(entry: entry) - } - .configurationDisplayName("__NAME__ Widget") - .description("Widget description goes here.") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) - } -} diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml new file mode 100644 index 0000000..0649477 --- /dev/null +++ b/Tests/.swiftlint.yml @@ -0,0 +1,4 @@ +# Tests may force-unwrap values they construct themselves (fixed URLs, dates, test +# fixtures); a crash there is a failing test, not a user-facing bug. +disabled_rules: + - force_unwrapping diff --git a/Tests/Integration/WidgetIntegrationTests.swift b/Tests/Integration/WidgetIntegrationTests.swift deleted file mode 100644 index d55e952..0000000 --- a/Tests/Integration/WidgetIntegrationTests.swift +++ /dev/null @@ -1,231 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit -@testable import WidgetTemplates -@testable import LiveDataIntegration - -final class WidgetIntegrationTests: QuickSpec { - override func spec() { - describe("Widget Integration") { - var widgetEngine: WidgetEngine! - var dataManager: WidgetDataManager! - var templateManager: WidgetTemplateManager! - - beforeEach { - widgetEngine = WidgetEngine() - dataManager = WidgetDataManager() - templateManager = WidgetTemplateManager() - } - - afterEach { - widgetEngine = nil - dataManager = nil - templateManager = nil - } - - context("Widget Template Integration") { - it("should create widget from template") { - let template = WidgetTemplate(name: "WeatherWidget", size: .medium) - template.setContent { context in - VStack { - Text("Weather") - .font(.headline) - Text("72°F") - .font(.title) - } - } - - let widget = templateManager.createWidget(from: template) { result in - expect(result).to(beSuccess()) - } - - expect(widget).toNot(beNil()) - expect(widget.kind).to(equal("WeatherWidget")) - } - - it("should apply template styling") { - let template = WidgetTemplate(name: "StyledWidget", size: .large) - template.applyStyle(.modern) - - let widget = templateManager.createWidget(from: template) { result in - expect(result).to(beSuccess()) - } - - expect(widget.style).to(equal(.modern)) - } - } - - context("Data Integration") { - it("should update widget with real-time data") { - let widget = HomeScreenWidget(kind: "DataWidget") - let dataSource = MockDataSource() - - dataManager.connect(dataSource) { result in - expect(result).to(beSuccess()) - - dataManager.onDataUpdate { data in - widget.update(with: data) { result in - expect(result).to(beSuccess()) - } - } - } - } - - it("should handle data caching") { - let cacheManager = WidgetCacheManager() - let data = WidgetData(id: "test", content: ["temperature": "75°F"]) - - cacheManager.cache(data) { result in - expect(result).to(beSuccess()) - - cacheManager.retrieve(id: "test") { result in - expect(result).to(beSuccess()) - expect(result.value?.content["temperature"]).to(equal("75°F")) - } - } - } - } - - context("Live Activity Integration") { - it("should create live activity from widget") { - let widget = LiveActivityWidget(kind: "ActivityWidget") - let activityManager = LiveActivityManager() - - activityManager.createActivity(from: widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should update live activity in real-time") { - let activity = LiveActivity(id: "test-activity") - let updateManager = LiveActivityUpdateManager() - - updateManager.update(activity, with: ["status": "active"]) { result in - expect(result).to(beSuccess()) - } - } - } - - context("Dynamic Island Integration") { - it("should create Dynamic Island activity") { - let islandManager = DynamicIslandManager() - let activity = DynamicIslandActivity(id: "island-activity") - - islandManager.start(activity) { result in - expect(result).to(beSuccess()) - } - } - - it("should handle Dynamic Island interactions") { - let activity = DynamicIslandActivity(id: "interactive-activity") - let interactionHandler = DynamicIslandInteractionHandler() - - interactionHandler.handleTap(for: activity) { result in - expect(result).to(beSuccess()) - } - } - } - - context("Widget Analytics") { - it("should track widget usage") { - let analytics = WidgetAnalytics() - let widget = HomeScreenWidget(kind: "AnalyticsWidget") - - analytics.trackWidgetView(widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should track widget interactions") { - let analytics = WidgetAnalytics() - let widget = InteractiveWidget(kind: "InteractiveWidget") - - analytics.trackInteraction(widget, action: "tap") { result in - expect(result).to(beSuccess()) - } - } - } - - context("Performance Integration") { - it("should handle multiple widget updates efficiently") { - let widgets = (0..<5).map { HomeScreenWidget(kind: "perf.\($0)") } - let updateManager = WidgetUpdateManager() - - measure { - for widget in widgets { - updateManager.update(widget, with: ["data": "test"]) { _ in } - } - } - } - - it("should manage memory efficiently") { - let memoryManager = WidgetMemoryManager() - let widgets = (0..<10).map { HomeScreenWidget(kind: "memory.\($0)") } - - for widget in widgets { - memoryManager.register(widget) { _ in } - } - - expect(memoryManager.activeWidgetCount).to(equal(10)) - - memoryManager.cleanup() { result in - expect(result).to(beSuccess()) - } - } - } - - context("Error Recovery") { - it("should recover from data source failure") { - let recoveryManager = WidgetRecoveryManager() - let failedDataSource = MockFailingDataSource() - - recoveryManager.handleDataSourceFailure(failedDataSource) { result in - expect(result).to(beSuccess()) - } - } - - it("should retry failed operations") { - let retryManager = WidgetRetryManager() - let operation = MockFailingOperation() - - retryManager.retry(operation, maxAttempts: 3) { result in - expect(result).to(beSuccess()) - } - } - } - } - } -} - -// MARK: - Mock Classes - -class MockDataSource: WidgetDataSource { - override func fetch() async throws -> WidgetData { - return WidgetData(id: "mock", content: ["test": "data"]) - } -} - -class MockFailingDataSource: WidgetDataSource { - override func fetch() async throws -> WidgetData { - throw WidgetError.dataFetchFailed - } -} - -class MockFailingOperation: WidgetOperation { - private var attemptCount = 0 - - override func execute() async throws { - attemptCount += 1 - if attemptCount < 3 { - throw WidgetError.operationFailed - } - } -} - -// MARK: - Widget Error - -enum WidgetError: Error { - case dataFetchFailed - case operationFailed -} \ No newline at end of file diff --git a/Tests/Integration/WidgetTemplatesTests.swift b/Tests/Integration/WidgetTemplatesTests.swift deleted file mode 100644 index 021f95d..0000000 --- a/Tests/Integration/WidgetTemplatesTests.swift +++ /dev/null @@ -1,492 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetTemplates - -@available(iOS 16.0, *) -class WidgetTemplatesTests: QuickSpec { - - override func spec() { - describe("Widget Templates") { - context("Weather Widget Template") { - var template: WeatherWidgetTemplate! - - beforeEach { - template = WeatherWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("weather_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Weather Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("weather conditions")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("weather_config")) - expect(configuration.templateIdentifier).to(equal("weather_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Calendar Widget Template") { - var template: CalendarWidgetTemplate! - - beforeEach { - template = CalendarWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("calendar_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Calendar Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("events")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("calendar_config")) - expect(configuration.templateIdentifier).to(equal("calendar_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Fitness Widget Template") { - var template: FitnessWidgetTemplate! - - beforeEach { - template = FitnessWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("fitness_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Fitness Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("fitness metrics")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("fitness_config")) - expect(configuration.templateIdentifier).to(equal("fitness_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("News Widget Template") { - var template: NewsWidgetTemplate! - - beforeEach { - template = NewsWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("news_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("News Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("news")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("news_config")) - expect(configuration.templateIdentifier).to(equal("news_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Social Media Widget Template") { - var template: SocialMediaWidgetTemplate! - - beforeEach { - template = SocialMediaWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("social_media_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Social Media Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("social media")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("social_config")) - expect(configuration.templateIdentifier).to(equal("social_media_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Productivity Widget Template") { - var template: ProductivityWidgetTemplate! - - beforeEach { - template = ProductivityWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("productivity_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Productivity Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("productivity")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("productivity_config")) - expect(configuration.templateIdentifier).to(equal("productivity_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Entertainment Widget Template") { - var template: EntertainmentWidgetTemplate! - - beforeEach { - template = EntertainmentWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("entertainment_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Entertainment Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("entertainment")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("entertainment_config")) - expect(configuration.templateIdentifier).to(equal("entertainment_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Finance Widget Template") { - var template: FinanceWidgetTemplate! - - beforeEach { - template = FinanceWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("finance_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Finance Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("financial")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("finance_config")) - expect(configuration.templateIdentifier).to(equal("finance_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Health Widget Template") { - var template: HealthWidgetTemplate! - - beforeEach { - template = HealthWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("health_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Health Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("health")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("health_config")) - expect(configuration.templateIdentifier).to(equal("health_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - - context("Travel Widget Template") { - var template: TravelWidgetTemplate! - - beforeEach { - template = TravelWidgetTemplate() - } - - it("should have correct identifier") { - expect(template.identifier).to(equal("travel_widget")) - } - - it("should have correct display name") { - expect(template.displayName).to(equal("Travel Widget")) - } - - it("should have correct description") { - expect(template.description).to(contain("travel")) - } - - it("should support all widget sizes") { - expect(template.supportedSizes).to(contain(.systemSmall)) - expect(template.supportedSizes).to(contain(.systemMedium)) - expect(template.supportedSizes).to(contain(.systemLarge)) - } - - it("should create widget with valid configuration") { - let configuration = template.getDefaultConfiguration() - let widget = template.createWidget(configuration: configuration) - - expect(widget).toNot(beNil()) - } - - it("should validate configuration correctly") { - let validConfiguration = template.getDefaultConfiguration() - let isValid = template.validateConfiguration(validConfiguration) - - expect(isValid).to(beTrue()) - } - - it("should return default configuration") { - let configuration = template.getDefaultConfiguration() - - expect(configuration.id).to(equal("travel_config")) - expect(configuration.templateIdentifier).to(equal("travel_widget")) - expect(configuration.widgets.count).to(equal(1)) - } - } - } - } -} \ No newline at end of file diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index b6d608a..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -import XCTest - -import iOS-Widget-Development-KitTests - -var tests = [XCTestCaseEntry]() -tests += iOS-Widget-Development-KitTests.allTests() -XCTMain(tests) diff --git a/Tests/Performance/LiveDataIntegrationTests.swift b/Tests/Performance/LiveDataIntegrationTests.swift deleted file mode 100644 index 3f3c648..0000000 --- a/Tests/Performance/LiveDataIntegrationTests.swift +++ /dev/null @@ -1,333 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import LiveDataIntegration - -@available(iOS 16.0, *) -class LiveDataIntegrationTests: QuickSpec { - - override func spec() { - describe("Live Data Integration Performance") { - var liveDataIntegration: LiveDataIntegration! - - beforeEach { - liveDataIntegration = LiveDataIntegration.shared - } - - context("Data Source Registration Performance") { - it("should register data sources quickly") { - let startTime = CFAbsoluteTimeGetCurrent() - - let weatherDataSource = WeatherDataSource() - liveDataIntegration.registerDataSource("weather_test", dataSource: weatherDataSource) - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.1)) // Should complete in less than 100ms - } - - it("should register multiple data sources efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - let dataSources = [ - WeatherDataSource(), - CalendarDataSource(), - FitnessDataSource(), - NewsDataSource(), - SocialMediaDataSource() - ] - - for (index, dataSource) in dataSources.enumerated() { - liveDataIntegration.registerDataSource("test_\(index)", dataSource: dataSource) - } - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.5)) // Should complete in less than 500ms - } - } - - context("WebSocket Connection Performance") { - it("should establish connection within reasonable time") { - let expectation = XCTestExpectation(description: "WebSocket connection") - - let startTime = CFAbsoluteTimeGetCurrent() - - // Note: This is a mock test since we can't actually connect to a real WebSocket - // In a real scenario, you would test with a mock WebSocket server - let mockURL = URL(string: "ws://localhost:8080")! - - liveDataIntegration.connectWebSocket(identifier: "test_connection", url: mockURL) - - // Simulate connection time - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.2)) // Should complete in less than 200ms - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - } - - it("should handle multiple connections efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - let urls = [ - URL(string: "ws://localhost:8081")!, - URL(string: "ws://localhost:8082")!, - URL(string: "ws://localhost:8083")! - ] - - for (index, url) in urls.enumerated() { - liveDataIntegration.connectWebSocket(identifier: "test_\(index)", url: url) - } - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.3)) // Should complete in less than 300ms - } - } - - context("Data Publishing Performance") { - it("should publish data quickly") { - let expectation = XCTestExpectation(description: "Data publishing") - - let startTime = CFAbsoluteTimeGetCurrent() - - let publisher = liveDataIntegration.getDataPublisher(for: "test_publisher") - - publisher?.sink { data in - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.05)) // Should complete in less than 50ms - expectation.fulfill() - } - .store(in: &Set()) - - // Simulate data publishing - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - let testData = WidgetData( - id: "test_data", - type: .weather, - content: ["temperature": 22] - ) - - // This would normally be called by the data source - // For testing, we're simulating the data flow - } - - wait(for: [expectation], timeout: 1.0) - } - - it("should handle high-frequency updates efficiently") { - let expectation = XCTestExpectation(description: "High-frequency updates") - expectation.expectedFulfillmentCount = 100 - - let startTime = CFAbsoluteTimeGetCurrent() - var updateCount = 0 - - let publisher = liveDataIntegration.getDataPublisher(for: "test_high_freq") - - publisher?.sink { data in - updateCount += 1 - - if updateCount == 100 { - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(1.0)) // Should complete in less than 1 second - expectation.fulfill() - } - } - .store(in: &Set()) - - // Simulate high-frequency updates - for i in 0..<100 { - DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.01) { - let testData = WidgetData( - id: "test_data_\(i)", - type: .weather, - content: ["temperature": 20 + i] - ) - - // This would normally be called by the data source - } - } - - wait(for: [expectation], timeout: 5.0) - } - } - - context("Memory Usage Performance") { - it("should maintain reasonable memory usage") { - let initialMemory = getCurrentMemoryUsage() - - // Register multiple data sources - for i in 0..<50 { - let dataSource = WeatherDataSource() - liveDataIntegration.registerDataSource("memory_test_\(i)", dataSource: dataSource) - } - - let finalMemory = getCurrentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - // Memory increase should be reasonable (less than 10MB) - expect(memoryIncrease).to(beLessThan(10 * 1024 * 1024)) - } - - it("should clean up resources properly") { - let initialMemory = getCurrentMemoryUsage() - - // Register and then disconnect multiple connections - for i in 0..<20 { - let mockURL = URL(string: "ws://localhost:\(8080 + i)")! - liveDataIntegration.connectWebSocket(identifier: "cleanup_test_\(i)", url: mockURL) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - liveDataIntegration.disconnectWebSocket(identifier: "cleanup_test_\(i)") - } - } - - // Wait for cleanup - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - let finalMemory = getCurrentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - // Memory should not increase significantly after cleanup - expect(memoryIncrease).to(beLessThan(5 * 1024 * 1024)) - } - } - } - - context("Battery Optimization Performance") { - it("should reduce refresh frequency when battery is low") { - // Simulate low battery condition - UIDevice.current.isBatteryMonitoringEnabled = true - - let initialSettings = DataSourceSettings(refreshInterval: 30.0) - let lowBatterySettings = DataSourceSettings(refreshInterval: 60.0) - - let weatherDataSource = WeatherDataSource() - weatherDataSource.configure(initialSettings) - - // Simulate battery level drop - // Note: In a real test, you would need to mock the battery level - // This is a conceptual test - - expect(weatherDataSource).toNot(beNil()) - } - } - - context("Network Performance") { - it("should handle network interruptions gracefully") { - let expectation = XCTestExpectation(description: "Network handling") - - // Simulate network interruption - // In a real test, you would mock network conditions - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - // Simulate network recovery - expectation.fulfill() - } - - wait(for: [expectation], timeout: 1.0) - } - - it("should retry connections efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - // Simulate connection retries - for i in 0..<5 { - let mockURL = URL(string: "ws://localhost:\(9000 + i)")! - liveDataIntegration.connectWebSocket(identifier: "retry_test_\(i)", url: mockURL) - } - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.5)) // Should complete in less than 500ms - } - } - - context("Data Processing Performance") { - it("should process large data sets efficiently") { - let startTime = CFAbsoluteTimeGetCurrent() - - // Simulate processing large data set - let largeDataSet = WidgetData( - id: "large_data", - type: .weather, - content: [ - "temperature": 22, - "humidity": 65, - "pressure": 1013, - "windSpeed": 12, - "windDirection": "NW", - "visibility": 10, - "uvIndex": 5, - "airQuality": "Good", - "forecast": Array(0..<24).map { hour in - ["hour": hour, "temp": 20 + hour % 5, "condition": "sunny"] - } - ] - ) - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(0.01)) // Should complete in less than 10ms - } - - it("should handle concurrent data processing") { - let expectation = XCTestExpectation(description: "Concurrent processing") - expectation.expectedFulfillmentCount = 10 - - let startTime = CFAbsoluteTimeGetCurrent() - - // Simulate concurrent data processing - DispatchQueue.concurrentPerform(iterations: 10) { index in - let testData = WidgetData( - id: "concurrent_\(index)", - type: .weather, - content: ["temperature": 20 + index] - ) - - DispatchQueue.main.async { - expectation.fulfill() - } - } - - wait(for: [expectation], timeout: 2.0) - - let endTime = CFAbsoluteTimeGetCurrent() - let duration = endTime - startTime - - expect(duration).to(beLessThan(1.0)) // Should complete in less than 1 second - } - } - } - } - - // Helper function to get current memory usage - private func getCurrentMemoryUsage() -> UInt64 { - var info = mach_task_basic_info() - var count = mach_msg_type_number_t(MemoryLayout.size)/4 - - let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) { - $0.withMemoryRebound(to: integer_t.self, capacity: 1) { - task_info(mach_task_self_, - task_flavor_t(MACH_TASK_BASIC_INFO), - $0, - &count) - } - } - - return kerr == KERN_SUCCESS ? info.resident_size : 0 - } -} \ No newline at end of file diff --git a/Tests/Performance/WidgetPerformanceTests.swift b/Tests/Performance/WidgetPerformanceTests.swift deleted file mode 100644 index 99b6c7b..0000000 --- a/Tests/Performance/WidgetPerformanceTests.swift +++ /dev/null @@ -1,270 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit - -final class WidgetPerformanceTests: QuickSpec { - override func spec() { - describe("Widget Performance") { - var performanceMonitor: WidgetPerformanceMonitor! - - beforeEach { - performanceMonitor = WidgetPerformanceMonitor() - } - - afterEach { - performanceMonitor = nil - } - - context("Widget Rendering Performance") { - it("should render widget within acceptable time") { - let widget = HomeScreenWidget(kind: "perf.widget") - - measure { - widget.render { _ in } - } - } - - it("should handle complex widget layouts efficiently") { - let complexWidget = ComplexWidget(kind: "complex.widget") - - measure { - complexWidget.render { _ in } - } - } - } - - context("Data Processing Performance") { - it("should process large datasets efficiently") { - let largeDataset = generateLargeDataset(size: 1000) - let dataProcessor = WidgetDataProcessor() - - measure { - dataProcessor.process(largeDataset) { _ in } - } - } - - it("should cache data efficiently") { - let cacheManager = WidgetCacheManager() - let data = generateTestData(count: 100) - - measure { - for item in data { - cacheManager.cache(item) { _ in } - } - } - } - } - - context("Memory Usage") { - it("should maintain stable memory usage") { - let memoryTracker = WidgetMemoryTracker() - let widgets = (0..<50).map { HomeScreenWidget(kind: "memory.\($0)") } - - let initialMemory = memoryTracker.currentMemoryUsage() - - for widget in widgets { - widget.initialize { _ in } - } - - let finalMemory = memoryTracker.currentMemoryUsage() - let memoryIncrease = finalMemory - initialMemory - - expect(memoryIncrease).to(beLessThan(50 * 1024 * 1024)) // 50MB limit - } - - it("should release memory when widgets are destroyed") { - let memoryTracker = WidgetMemoryTracker() - var widgets: [HomeScreenWidget] = [] - - let initialMemory = memoryTracker.currentMemoryUsage() - - for i in 0..<20 { - let widget = HomeScreenWidget(kind: "temp.\(i)") - widgets.append(widget) - widget.initialize { _ in } - } - - widgets.removeAll() - - let finalMemory = memoryTracker.currentMemoryUsage() - let memoryDifference = abs(finalMemory - initialMemory) - - expect(memoryDifference).to(beLessThan(10 * 1024 * 1024)) // 10MB tolerance - } - } - - context("Network Performance") { - it("should handle concurrent network requests efficiently") { - let networkManager = WidgetNetworkManager() - let requests = (0..<10).map { MockNetworkRequest(id: "req.\($0)") } - - measure { - for request in requests { - networkManager.execute(request) { _ in } - } - } - } - - it("should handle network timeouts gracefully") { - let networkManager = WidgetNetworkManager() - let slowRequest = MockSlowNetworkRequest() - - let startTime = Date() - - networkManager.execute(slowRequest) { result in - let endTime = Date() - let duration = endTime.timeIntervalSince(startTime) - - expect(duration).to(beLessThan(5.0)) // 5 second timeout - } - } - } - - context("Background Processing") { - it("should process background updates efficiently") { - let backgroundProcessor = WidgetBackgroundProcessor() - let updates = generateBackgroundUpdates(count: 100) - - measure { - backgroundProcessor.process(updates) { _ in } - } - } - - it("should handle background task scheduling") { - let taskScheduler = WidgetTaskScheduler() - let tasks = (0..<20).map { MockBackgroundTask(id: "task.\($0)") } - - measure { - for task in tasks { - taskScheduler.schedule(task) { _ in } - } - } - } - } - - context("UI Responsiveness") { - it("should maintain UI responsiveness during heavy operations") { - let uiMonitor = WidgetUIMonitor() - let heavyOperation = MockHeavyOperation() - - uiMonitor.startMonitoring() - - heavyOperation.execute { _ in - let responsiveness = uiMonitor.getResponsiveness() - expect(responsiveness).to(beGreaterThan(0.8)) // 80% responsiveness - } - } - - it("should handle rapid widget updates without lag") { - let updateManager = WidgetUpdateManager() - let widget = HomeScreenWidget(kind: "rapid.widget") - - let startTime = Date() - var updateCount = 0 - - for _ in 0..<100 { - updateManager.update(widget, with: ["data": "update"]) { _ in - updateCount += 1 - } - } - - let endTime = Date() - let duration = endTime.timeIntervalSince(startTime) - - expect(duration).to(beLessThan(1.0)) // 1 second for 100 updates - expect(updateCount).to(equal(100)) - } - } - } - } -} - -// MARK: - Helper Methods - -extension WidgetPerformanceTests { - func generateLargeDataset(size: Int) -> [WidgetData] { - return (0.. [WidgetData] { - return (0.. [WidgetUpdate] { - return (0..) -> Void) { - // Simulate complex rendering - DispatchQueue.global().async { - Thread.sleep(forTimeInterval: 0.1) - completion(.success(())) - } - } -} - -class MockNetworkRequest: WidgetNetworkRequest { - let id: String - - init(id: String) { - self.id = id - } - - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - completion(.success(Data())) - } - } -} - -class MockSlowNetworkRequest: WidgetNetworkRequest { - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - Thread.sleep(forTimeInterval: 3.0) - completion(.failure(WidgetError.networkTimeout)) - } - } -} - -class MockBackgroundTask: WidgetBackgroundTask { - let id: String - - init(id: String) { - self.id = id - } - - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - completion(.success(())) - } - } -} - -class MockHeavyOperation: WidgetOperation { - override func execute(completion: @escaping (Result) -> Void) { - DispatchQueue.global().async { - // Simulate heavy processing - for _ in 0..<1000000 { - _ = sqrt(Double.random(in: 0...1000)) - } - completion(.success(())) - } - } -} - -// MARK: - Additional Errors - -extension WidgetError { - static let networkTimeout = WidgetError.operationFailed -} \ No newline at end of file diff --git a/Tests/Unit/WidgetAnalyticsTests.swift b/Tests/Unit/WidgetAnalyticsTests.swift deleted file mode 100644 index 4243fd7..0000000 --- a/Tests/Unit/WidgetAnalyticsTests.swift +++ /dev/null @@ -1,314 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetAnalytics - -@available(iOS 16.0, *) -class WidgetAnalyticsTests: QuickSpec { - - override func spec() { - describe("WidgetAnalyticsService") { - var analyticsService: WidgetAnalyticsService! - - beforeEach { - analyticsService = WidgetAnalyticsService() - } - - context("when logging events") { - it("should log widget updates") { - analyticsService.logWidgetUpdate(widgetId: "test_widget") - let analytics = analyticsService.getAnalytics() - - expect(analytics.totalWidgetUpdates).to(equal(1)) - expect(analytics.widgetUpdates["test_widget"]).to(equal(1)) - } - - it("should log template registrations") { - analyticsService.logTemplateRegistration("weather_template") - let analytics = analyticsService.getAnalytics() - - expect(analytics.templateRegistrations).to(equal(1)) - } - - it("should log data source registrations") { - analyticsService.logDataSourceRegistration("weather_api") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSourceRegistrations).to(equal(1)) - } - - it("should log WebSocket connections") { - let url = URL(string: "ws://test.com")! - analyticsService.logWebSocketConnection(identifier: "test_ws", url: url.absoluteString) - let analytics = analyticsService.getAnalytics() - - expect(analytics.webSocketConnections).to(equal(1)) - } - - it("should log WebSocket disconnections") { - analyticsService.logWebSocketDisconnection(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.webSocketDisconnections).to(equal(1)) - } - - it("should log data sent") { - analyticsService.logDataSent(widgetId: "test_widget", dataType: "weather") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSentCount).to(equal(1)) - } - - it("should log data source updates") { - analyticsService.logDataSourceUpdate(identifier: "weather_api", dataType: "weather") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSourceUpdates).to(equal(1)) - } - - it("should log data source configurations") { - analyticsService.logDataSourceConfiguration(identifier: "weather_api") - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataSourceConfigurations).to(equal(1)) - } - - it("should log connection established") { - analyticsService.logConnectionEstablished(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionsEstablished).to(equal(1)) - } - - it("should log connection failures") { - let error = NSError(domain: "test", code: 1, userInfo: nil) - analyticsService.logConnectionFailed(identifier: "test_ws", error: error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionFailures).to(equal(1)) - } - - it("should log connection cancellations") { - analyticsService.logConnectionCancelled(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionsCancelled).to(equal(1)) - } - - it("should log connection retries") { - analyticsService.logConnectionRetry(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.connectionRetries).to(equal(1)) - } - - it("should log reconnection attempts") { - analyticsService.logReconnectionAttempt(identifier: "test_ws") - let analytics = analyticsService.getAnalytics() - - expect(analytics.reconnectionAttempts).to(equal(1)) - } - - it("should log data receive errors") { - let error = NSError(domain: "test", code: 1, userInfo: nil) - analyticsService.logDataReceiveError(identifier: "test_ws", error: error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataReceiveErrors).to(equal(1)) - } - - it("should log data decode errors") { - let error = NSError(domain: "test", code: 1, userInfo: nil) - analyticsService.logDataDecodeError(identifier: "test_ws", error: error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.dataDecodeErrors).to(equal(1)) - } - - it("should log network unavailable events") { - analyticsService.logNetworkUnavailable() - let analytics = analyticsService.getAnalytics() - - expect(analytics.networkUnavailableEvents).to(equal(1)) - } - - it("should log memory cleanups") { - analyticsService.logMemoryCleanup(removedEntries: 5) - let analytics = analyticsService.getAnalytics() - - expect(analytics.memoryCleanups).to(equal(1)) - } - - it("should log battery optimizations") { - analyticsService.logBatteryOptimization(newInterval: 60.0) - let analytics = analyticsService.getAnalytics() - - expect(analytics.batteryOptimizations).to(equal(1)) - } - - it("should log errors") { - let error = WidgetError.templateNotFound("test_template") - analyticsService.logError(error) - let analytics = analyticsService.getAnalytics() - - expect(analytics.errors).to(equal(1)) - } - - it("should log engine initializations") { - analyticsService.logEngineInitialization() - let analytics = analyticsService.getAnalytics() - - expect(analytics.engineInitializations).to(equal(1)) - } - } - - context("when configuring analytics") { - it("should configure analytics settings") { - let config = AnalyticsConfiguration( - enableExternalAnalytics: true, - maxEventLogSize: 500, - enablePerformanceTracking: true, - enableErrorTracking: true - ) - - analyticsService.configure(config) - - // Verify configuration was applied - expect(analyticsService).toNot(beNil()) - } - } - - context("when exporting analytics") { - it("should export analytics data") { - analyticsService.logWidgetUpdate(widgetId: "test_widget") - - let exportedData = analyticsService.exportAnalytics() - - expect(exportedData).toNot(beNil()) - } - } - - context("when clearing analytics") { - it("should clear analytics data") { - analyticsService.logWidgetUpdate(widgetId: "test_widget") - analyticsService.clearAnalytics() - - let analytics = analyticsService.getAnalytics() - - expect(analytics.totalWidgetUpdates).to(equal(0)) - } - } - } - - describe("WidgetAnalytics") { - context("when creating analytics data") { - it("should have default values") { - let analytics = WidgetAnalytics() - - expect(analytics.totalWidgetUpdates).to(equal(0)) - expect(analytics.templateRegistrations).to(equal(0)) - expect(analytics.dataSourceRegistrations).to(equal(0)) - expect(analytics.webSocketConnections).to(equal(0)) - expect(analytics.webSocketDisconnections).to(equal(0)) - expect(analytics.dataSentCount).to(equal(0)) - expect(analytics.dataSourceUpdates).to(equal(0)) - expect(analytics.dataSourceConfigurations).to(equal(0)) - expect(analytics.connectionsEstablished).to(equal(0)) - expect(analytics.connectionFailures).to(equal(0)) - expect(analytics.connectionsCancelled).to(equal(0)) - expect(analytics.connectionRetries).to(equal(0)) - expect(analytics.reconnectionAttempts).to(equal(0)) - expect(analytics.dataReceiveErrors).to(equal(0)) - expect(analytics.dataDecodeErrors).to(equal(0)) - expect(analytics.networkUnavailableEvents).to(equal(0)) - expect(analytics.memoryCleanups).to(equal(0)) - expect(analytics.batteryOptimizations).to(equal(0)) - expect(analytics.errors).to(equal(0)) - expect(analytics.engineInitializations).to(equal(0)) - } - } - } - - describe("AnalyticsConfiguration") { - context("when creating configuration") { - it("should have default values") { - let config = AnalyticsConfiguration() - - expect(config.enableExternalAnalytics).to(beFalse()) - expect(config.maxEventLogSize).to(equal(1000)) - expect(config.enablePerformanceTracking).to(beTrue()) - expect(config.enableErrorTracking).to(beTrue()) - } - - it("should have custom values") { - let config = AnalyticsConfiguration( - enableExternalAnalytics: true, - maxEventLogSize: 500, - enablePerformanceTracking: false, - enableErrorTracking: false - ) - - expect(config.enableExternalAnalytics).to(beTrue()) - expect(config.maxEventLogSize).to(equal(500)) - expect(config.enablePerformanceTracking).to(beFalse()) - expect(config.enableErrorTracking).to(beFalse()) - } - } - } - - describe("AnalyticsEventType") { - context("when using event types") { - it("should have all required event types") { - let eventTypes = AnalyticsEventType.allCases - - expect(eventTypes).to(contain(.widgetUpdate)) - expect(eventTypes).to(contain(.templateRegistration)) - expect(eventTypes).to(contain(.dataSourceRegistration)) - expect(eventTypes).to(contain(.webSocketConnection)) - expect(eventTypes).to(contain(.webSocketDisconnection)) - expect(eventTypes).to(contain(.dataSent)) - expect(eventTypes).to(contain(.dataSourceUpdate)) - expect(eventTypes).to(contain(.dataSourceConfiguration)) - expect(eventTypes).to(contain(.connectionEstablished)) - expect(eventTypes).to(contain(.connectionFailed)) - expect(eventTypes).to(contain(.connectionCancelled)) - expect(eventTypes).to(contain(.connectionRetry)) - expect(eventTypes).to(contain(.reconnectionAttempt)) - expect(eventTypes).to(contain(.dataReceiveError)) - expect(eventTypes).to(contain(.dataDecodeError)) - expect(eventTypes).to(contain(.networkUnavailable)) - expect(eventTypes).to(contain(.memoryCleanup)) - expect(eventTypes).to(contain(.batteryOptimization)) - expect(eventTypes).to(contain(.error)) - expect(eventTypes).to(contain(.engineInitialization)) - } - - it("should have correct raw values") { - expect(AnalyticsEventType.widgetUpdate.rawValue).to(equal("widget_update")) - expect(AnalyticsEventType.templateRegistration.rawValue).to(equal("template_registration")) - expect(AnalyticsEventType.dataSourceRegistration.rawValue).to(equal("data_source_registration")) - } - } - } - - describe("WidgetError") { - context("when creating errors") { - it("should have correct error descriptions") { - let templateError = WidgetError.templateNotFound("test_template") - let dataSourceError = WidgetError.dataSourceNotFound("test_api") - let connectionError = WidgetError.connectionFailed("Network timeout") - let decodeError = WidgetError.dataDecodeFailed("Invalid JSON") - let networkError = WidgetError.networkUnavailable - let unknownError = WidgetError.unknown - - expect(templateError.localizedDescription).to(contain("Template not found")) - expect(dataSourceError.localizedDescription).to(contain("Data source not found")) - expect(connectionError.localizedDescription).to(contain("Connection failed")) - expect(decodeError.localizedDescription).to(contain("Data decode failed")) - expect(networkError.localizedDescription).to(equal("Network unavailable")) - expect(unknownError.localizedDescription).to(equal("Unknown error")) - } - } - } - } -} \ No newline at end of file diff --git a/Tests/Unit/WidgetDataManagerTests.swift b/Tests/Unit/WidgetDataManagerTests.swift deleted file mode 100644 index e5a828c..0000000 --- a/Tests/Unit/WidgetDataManagerTests.swift +++ /dev/null @@ -1,349 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit -@testable import LiveDataIntegration - -final class WidgetDataManagerTests: QuickSpec { - override func spec() { - describe("WidgetDataManager") { - var dataManager: WidgetDataManager! - var mockDataSource: MockWidgetDataSource! - - beforeEach { - dataManager = WidgetDataManager() - mockDataSource = MockWidgetDataSource() - } - - afterEach { - dataManager = nil - mockDataSource = nil - } - - context("Data Source Connection") { - it("should connect to data source successfully") { - dataManager.connect(mockDataSource) { result in - expect(result).to(beSuccess()) - } - } - - it("should handle connection failure") { - let failingDataSource = MockFailingDataSource() - - dataManager.connect(failingDataSource) { result in - expect(result).to(beFailure()) - } - } - - it("should disconnect from data source") { - dataManager.connect(mockDataSource) { _ in - dataManager.disconnect { result in - expect(result).to(beSuccess()) - } - } - } - } - - context("Data Fetching") { - it("should fetch data successfully") { - dataManager.connect(mockDataSource) { _ in - dataManager.fetchData { result in - expect(result).to(beSuccess()) - expect(result.value?.id).to(equal("test-data")) - } - } - } - - it("should handle fetch failure") { - let failingDataSource = MockFailingDataSource() - - dataManager.connect(failingDataSource) { _ in - dataManager.fetchData { result in - expect(result).to(beFailure()) - } - } - } - - it("should fetch data with caching") { - dataManager.enableCaching = true - dataManager.connect(mockDataSource) { _ in - dataManager.fetchData { result in - expect(result).to(beSuccess()) - - // Second fetch should use cache - dataManager.fetchData { cachedResult in - expect(cachedResult).to(beSuccess()) - expect(cachedResult.value?.id).to(equal("test-data")) - } - } - } - } - } - - context("Real-time Updates") { - it("should receive real-time updates") { - let expectation = XCTestExpectation(description: "Real-time update") - - dataManager.connect(mockDataSource) { _ in - dataManager.onDataUpdate { data in - expect(data.id).to(equal("real-time-data")) - expectation.fulfill() - } - - mockDataSource.simulateUpdate() - } - - wait(for: [expectation], timeout: 2.0) - } - - it("should handle multiple subscribers") { - let expectation1 = XCTestExpectation(description: "Subscriber 1") - let expectation2 = XCTestExpectation(description: "Subscriber 2") - - dataManager.connect(mockDataSource) { _ in - dataManager.onDataUpdate { _ in - expectation1.fulfill() - } - - dataManager.onDataUpdate { _ in - expectation2.fulfill() - } - - mockDataSource.simulateUpdate() - } - - wait(for: [expectation1, expectation2], timeout: 2.0) - } - } - - context("Data Caching") { - it("should cache data successfully") { - let testData = WidgetData(id: "cache-test", content: ["key": "value"]) - - dataManager.cache(testData) { result in - expect(result).to(beSuccess()) - } - } - - it("should retrieve cached data") { - let testData = WidgetData(id: "retrieve-test", content: ["key": "value"]) - - dataManager.cache(testData) { _ in - dataManager.retrieve(id: "retrieve-test") { result in - expect(result).to(beSuccess()) - expect(result.value?.content["key"]).to(equal("value")) - } - } - } - - it("should handle cache miss") { - dataManager.retrieve(id: "non-existent") { result in - expect(result).to(beFailure()) - } - } - - it("should clear cache") { - let testData = WidgetData(id: "clear-test", content: ["key": "value"]) - - dataManager.cache(testData) { _ in - dataManager.clearCache { result in - expect(result).to(beSuccess()) - - dataManager.retrieve(id: "clear-test") { result in - expect(result).to(beFailure()) - } - } - } - } - } - - context("Data Validation") { - it("should validate data format") { - let validData = WidgetData(id: "valid", content: ["key": "value"]) - let invalidData = WidgetData(id: "", content: [:]) - - dataManager.validateData(validData) { result in - expect(result).to(beSuccess()) - } - - dataManager.validateData(invalidData) { result in - expect(result).to(beFailure()) - } - } - - it("should handle data transformation") { - let rawData = ["temperature": "72", "humidity": "65"] - - dataManager.transformData(rawData) { result in - expect(result).to(beSuccess()) - expect(result.value?.content["temperature"]).to(equal("72°F")) - } - } - } - - context("Error Handling") { - it("should handle network errors") { - let networkErrorDataSource = MockNetworkErrorDataSource() - - dataManager.connect(networkErrorDataSource) { result in - expect(result).to(beFailure()) - } - } - - it("should handle timeout errors") { - let timeoutDataSource = MockTimeoutDataSource() - - dataManager.connect(timeoutDataSource) { result in - expect(result).to(beFailure()) - } - } - - it("should retry failed operations") { - let retryDataSource = MockRetryDataSource() - - dataManager.connect(retryDataSource) { result in - expect(result).to(beSuccess()) - } - } - } - - context("Performance") { - it("should handle large datasets efficiently") { - let largeDataSource = MockLargeDataSource() - - measure { - dataManager.connect(largeDataSource) { _ in - dataManager.fetchData { _ in } - } - } - } - - it("should handle concurrent operations") { - let expectation = XCTestExpectation(description: "Concurrent operations") - expectation.expectedFulfillmentCount = 5 - - for i in 0..<5 { - DispatchQueue.global().async { - dataManager.fetchData { _ in - expectation.fulfill() - } - } - } - - wait(for: [expectation], timeout: 5.0) - } - } - } - } -} - -// MARK: - Mock Classes - -class MockWidgetDataSource: WidgetDataSource { - private var updateCallbacks: [() -> Void] = [] - - func fetch() async throws -> WidgetData { - return WidgetData(id: "test-data", content: ["key": "value"]) - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - updateCallbacks.append { - updates(WidgetData(id: "real-time-data", content: ["updated": "true"])) - } - } - - func unsubscribe() { - updateCallbacks.removeAll() - } - - func simulateUpdate() { - updateCallbacks.forEach { $0() } - } -} - -class MockFailingDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - throw WidgetError.dataFetchFailed - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockNetworkErrorDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - throw WidgetError.networkError - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockTimeoutDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - try await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds - throw WidgetError.networkTimeout - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockRetryDataSource: WidgetDataSource { - private var attemptCount = 0 - - func fetch() async throws -> WidgetData { - attemptCount += 1 - if attemptCount < 3 { - throw WidgetError.dataFetchFailed - } - return WidgetData(id: "retry-success", content: ["attempts": "\(attemptCount)"]) - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -class MockLargeDataSource: WidgetDataSource { - func fetch() async throws -> WidgetData { - var largeContent: [String: String] = [:] - for i in 0..<1000 { - largeContent["key\(i)"] = "value\(i)" - } - return WidgetData(id: "large-data", content: largeContent) - } - - func subscribe(to updates: @escaping (WidgetData) -> Void) { - // No updates - } - - func unsubscribe() { - // No cleanup needed - } -} - -// MARK: - Additional Errors - -extension WidgetError { - static let networkTimeout = WidgetError.networkError -} \ No newline at end of file diff --git a/Tests/Unit/WidgetEngineTests.swift b/Tests/Unit/WidgetEngineTests.swift deleted file mode 100644 index 0d0bcef..0000000 --- a/Tests/Unit/WidgetEngineTests.swift +++ /dev/null @@ -1,190 +0,0 @@ -import XCTest -import Quick -import Nimble -@testable import WidgetKit - -final class WidgetEngineTests: QuickSpec { - override func spec() { - describe("WidgetEngine") { - var widgetEngine: WidgetEngine! - - beforeEach { - widgetEngine = WidgetEngine() - } - - afterEach { - widgetEngine = nil - } - - context("Initialization") { - it("should initialize with default configuration") { - expect(widgetEngine).toNot(beNil()) - expect(widgetEngine.isRunning).to(beFalse()) - } - - it("should have correct default settings") { - expect(widgetEngine.configuration.enableHomeScreenWidgets).to(beTrue()) - expect(widgetEngine.configuration.enableLockScreenWidgets).to(beTrue()) - expect(widgetEngine.configuration.enableLiveActivities).to(beTrue()) - } - } - - context("Widget Registration") { - it("should register home screen widget successfully") { - let widget = HomeScreenWidget(kind: "test.widget") - - widgetEngine.register(widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should register lock screen widget successfully") { - let widget = LockScreenWidget(kind: "test.lockwidget") - - widgetEngine.register(widget) { result in - expect(result).to(beSuccess()) - } - } - - it("should fail to register widget with invalid kind") { - let widget = HomeScreenWidget(kind: "") - - widgetEngine.register(widget) { result in - expect(result).to(beFailure()) - } - } - } - - context("Widget Management") { - it("should start widget engine successfully") { - widgetEngine.start { result in - expect(result).to(beSuccess()) - expect(widgetEngine.isRunning).to(beTrue()) - } - } - - it("should stop widget engine successfully") { - widgetEngine.start { _ in - widgetEngine.stop { result in - expect(result).to(beSuccess()) - expect(widgetEngine.isRunning).to(beFalse()) - } - } - } - - it("should update widget configuration") { - let newConfig = WidgetConfiguration() - newConfig.enableHomeScreenWidgets = false - - widgetEngine.updateConfiguration(newConfig) { result in - expect(result).to(beSuccess()) - expect(widgetEngine.configuration.enableHomeScreenWidgets).to(beFalse()) - } - } - } - - context("Data Integration") { - it("should fetch widget data successfully") { - let dataSource = WidgetDataSource(type: .api, endpoint: "https://api.test.com") - - widgetEngine.fetchData(from: dataSource) { result in - expect(result).to(beSuccess()) - } - } - - it("should handle data fetch failure") { - let dataSource = WidgetDataSource(type: .api, endpoint: "https://invalid-url.com") - - widgetEngine.fetchData(from: dataSource) { result in - expect(result).to(beFailure()) - } - } - } - - context("Performance") { - it("should register multiple widgets efficiently") { - let widgets = (0..<10).map { HomeScreenWidget(kind: "widget.\($0)") } - - measure { - for widget in widgets { - widgetEngine.register(widget) { _ in } - } - } - } - - it("should handle concurrent widget updates") { - let expectation = XCTestExpectation(description: "Concurrent updates") - expectation.expectedFulfillmentCount = 5 - - for i in 0..<5 { - DispatchQueue.global().async { - let widget = HomeScreenWidget(kind: "concurrent.\(i)") - widgetEngine.register(widget) { _ in - expectation.fulfill() - } - } - } - - wait(for: [expectation], timeout: 5.0) - } - } - - context("Error Handling") { - it("should handle invalid widget configuration") { - let invalidConfig = WidgetConfiguration() - invalidConfig.refreshInterval = -1 - - widgetEngine.updateConfiguration(invalidConfig) { result in - expect(result).to(beFailure()) - } - } - - it("should handle network errors gracefully") { - let dataSource = WidgetDataSource(type: .api, endpoint: "https://unreachable-url.com") - - widgetEngine.fetchData(from: dataSource) { result in - expect(result).to(beFailure()) - } - } - } - } - } -} - -// MARK: - Test Helpers - -extension Result { - func beSuccess() -> Predicate { - return Predicate { actual in - let message = ExpectationMessage.expectedActualValueTo("be success") - - guard let actual = try actual.evaluate() else { - return PredicateResult(status: .fail, message: message.appendedBeNilHint()) - } - - switch actual { - case .success: - return PredicateResult(status: .matches, message: message) - case .failure: - return PredicateResult(status: .fail, message: message) - } - } - } - - func beFailure() -> Predicate { - return Predicate { actual in - let message = ExpectationMessage.expectedActualValueTo("be failure") - - guard let actual = try actual.evaluate() else { - return PredicateResult(status: .fail, message: message.appendedBeNilHint()) - } - - switch actual { - case .success: - return PredicateResult(status: .fail, message: message) - case .failure: - return PredicateResult(status: .matches, message: message) - } - } - } -} \ No newline at end of file diff --git a/Tests/UnitTests.swift b/Tests/UnitTests.swift deleted file mode 100644 index 7a129a2..0000000 --- a/Tests/UnitTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -import XCTest -@testable import REPO_NAME - -/** - * Unit Tests for REPO_NAME Framework - * - * Comprehensive test coverage for all core functionality. - * Ensures reliability and stability of the framework. - */ - -final class REPO_NAMEUnitTests: XCTestCase { - - // MARK: - Test Properties - var sut: MainFramework! - - // MARK: - Setup and Teardown - override func setUp() { - super.setUp() - sut = MainFramework() - } - - override func tearDown() { - sut = nil - super.tearDown() - } - - // MARK: - Initialization Tests - func testInitialization() { - // Given - let framework = MainFramework() - - // Then - XCTAssertNotNil(framework, "Framework should be initialized successfully") - } - - func testDefaultConfiguration() { - // Given - let framework = MainFramework() - - // Then - XCTAssertFalse(framework.isRunning, "Framework should not be running by default") - XCTAssertNotNil(framework.configuration, "Configuration should be set by default") - } -} diff --git a/Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift b/Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift new file mode 100644 index 0000000..6d7081a --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/CalendarMathTests.swift @@ -0,0 +1,62 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("MonthGrid") +struct MonthGridTests { + @Test func sundayFirstCalendar() { + // 1 October 2026 is a Thursday. + let calendar = TestCalendar.gregorian(firstWeekday: 1) + let grid = MonthGrid(month: TestCalendar.date(2026, 10, 14), calendar: calendar) + + #expect(grid.weekdaySymbols == ["S", "M", "T", "W", "T", "F", "S"]) + #expect(grid.weeks.count == 5) + #expect(grid.weeks[0] == [nil, nil, nil, nil, 1, 2, 3]) + #expect(grid.weeks[4] == [25, 26, 27, 28, 29, 30, 31]) + } + + @Test func mondayFirstCalendarShiftsColumns() { + let calendar = TestCalendar.gregorian(firstWeekday: 2) + let grid = MonthGrid(month: TestCalendar.date(2026, 10, 1), calendar: calendar) + + #expect(grid.weekdaySymbols == ["M", "T", "W", "T", "F", "S", "S"]) + #expect(grid.weeks[0] == [nil, nil, nil, 1, 2, 3, 4]) + #expect(grid.weeks.last == [26, 27, 28, 29, 30, 31, nil]) + } + + @Test func everyRowHasSevenCellsAndEveryDayAppearsOnce() { + let calendar = TestCalendar.gregorian(firstWeekday: 2) + // February 2028 is a leap month. + let grid = MonthGrid(month: TestCalendar.date(2028, 2, 10), calendar: calendar) + + #expect(grid.weeks.allSatisfy { $0.count == 7 }) + #expect(grid.weeks.flatMap { $0 }.compactMap { $0 } == Array(1...29)) + } + + @Test func monthStartingOnFirstWeekdayHasNoLeadingBlanks() { + // 1 February 2026 is a Sunday. + let grid = MonthGrid(month: TestCalendar.date(2026, 2, 1), calendar: TestCalendar.gregorian()) + #expect(grid.weeks.count == 4) + #expect(grid.weeks[0].first == 1) + } +} + +@Suite("CountdownComponents") +struct CountdownComponentsTests { + @Test func splitsRemainingTime() { + let now = TestCalendar.date(2026, 1, 1) + let target = now.addingTimeInterval(2 * 86_400 + 3 * 3_600 + 4 * 60 + 59) + let countdown = CountdownComponents(from: now, to: target) + + #expect(countdown == CountdownComponents(from: now, to: target)) + #expect(countdown.days == 2) + #expect(countdown.hours == 3) + #expect(countdown.minutes == 4) + } + + @Test func pastTargetIsAllZero() { + let now = TestCalendar.date(2026, 1, 1) + let countdown = CountdownComponents(from: now, to: now.addingTimeInterval(-500)) + #expect(countdown.days == 0 && countdown.hours == 0 && countdown.minutes == 0) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift b/Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift new file mode 100644 index 0000000..4649968 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/ConfigurationTests.swift @@ -0,0 +1,98 @@ +import Foundation +import SwiftUI +import Testing +@testable import WidgetDevelopmentKit + +@Suite("WidgetDeepLink") +struct WidgetDeepLinkTests { + @Test func buildsURLWithSortedQuery() { + let link = WidgetDeepLink( + scheme: "MyApp", + host: "content", + path: "/article/", + parameters: ["id": "42", "source": "widget", "a": "1"] + ) + #expect(link.url?.absoluteString == "myapp://content/article?a=1&id=42&source=widget") + } + + @Test func roundTripsThroughURL() throws { + let link = WidgetDeepLink(scheme: "myapp", host: "action", path: "toggle/item", parameters: ["id": "a b&c"]) + let url = try #require(link.url) + #expect(WidgetDeepLink(url: url) == link) + #expect(WidgetDeepLink(url: url)?.path == "toggle/item") + } + + @Test func linkWithoutPathOrParameters() { + let link = WidgetDeepLink(scheme: "myapp", host: "home") + #expect(link.url?.absoluteString == "myapp://home") + #expect(link.pathComponents.isEmpty) + } + + @Test func rejectsWrongSchemeAndMissingHost() throws { + let url = try #require(URL(string: "other://content/article")) + #expect(WidgetDeepLink(url: url, expectedScheme: "myapp") == nil) + #expect(WidgetDeepLink(url: url, expectedScheme: "OTHER") != nil) + + let noHost = try #require(URL(string: "myapp:///path")) + #expect(WidgetDeepLink(url: noHost) == nil) + } + + @Test func firstRepeatedParameterWinsAndValuelessItemsAreSkipped() throws { + let url = try #require(URL(string: "myapp://content?id=1&id=2&flag")) + let link = try #require(WidgetDeepLink(url: url)) + #expect(link.parameters == ["id": "1"]) + } +} + +@Suite("HexColor") +struct HexColorTests { + @Test func parsesSixDigits() throws { + let color = try #require(HexColor("#34C759")) + #expect(color.red == 0x34 / 255.0) + #expect(color.green == 0xC7 / 255.0) + #expect(color.blue == 0x59 / 255.0) + #expect(color.opacity == 1) + } + + @Test func parsesShortAndAlphaForms() throws { + #expect(try #require(HexColor("0af")).hexString == "#00AAFF") + let translucent = try #require(HexColor("FF3B3080")) + #expect(translucent.opacity == 0x80 / 255.0) + #expect(translucent.hexString == "#FF3B3080") + #expect(HexColor(" #ffffff \n")?.hexString == "#FFFFFF") + } + + @Test(arguments: ["", "#", "12345", "#GG0000", "#12345678FF", "red", "#123"]) + func rejectsInvalidInput(_ input: String) { + #expect(HexColor(input) == nil) + } + + @Test func colorInitializer() { + #expect(Color(hex: "#007AFF") != nil) + #expect(Color(hex: "nope") == nil) + } +} + +@Suite("Configuration entities") +struct ConfigurationEntityTests { + @Test func everyAccentColorHexIsValidAndIdsAreUnique() { + #expect(AccentColorOption.all.count == 8) + #expect(Set(AccentColorOption.all.map(\.id)).count == AccentColorOption.all.count) + } + + @Test func queriesReturnRequestedEntities() async throws { + let colors = try await AccentColorQuery().entities(for: ["green", "missing", "red"]) + #expect(colors.map(\.id) == ["green", "red"]) + + let intervals = try await RefreshIntervalQuery().entities(for: [60]) + #expect(intervals.first?.displayName == "1 hour") + #expect(try await RefreshIntervalQuery().suggestedEntities() == RefreshIntervalOption.all) + } + + @Test func refreshIntervalMapsToUpdatePolicy() throws { + let option = try #require(RefreshIntervalOption.all.first { $0.minutes == 30 }) + let start = Date(timeIntervalSince1970: 0) + #expect(option.updatePolicy.nextUpdateDate(from: start, calendar: TestCalendar.gregorian()) + == start.addingTimeInterval(30 * 60)) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift b/Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift new file mode 100644 index 0000000..8b258f5 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/LiveActivityStateMachineTests.swift @@ -0,0 +1,47 @@ +import Testing +@testable import WidgetDevelopmentKit + +@Suite("LiveActivityStateMachine") +struct LiveActivityStateMachineTests { + enum Phase: Hashable, Sendable { + case preparing, onTheWay, delivered, cancelled + } + + private func makeMachine() -> LiveActivityStateMachine { + LiveActivityStateMachine( + initialState: .preparing, + transitions: [ + .preparing: [.onTheWay, .cancelled], + .onTheWay: [.delivered, .cancelled] + ] + ) + } + + @Test func followsAllowedTransitions() async throws { + let machine = makeMachine() + try await machine.transition(to: .onTheWay) + try await machine.transition(to: .delivered) + #expect(await machine.currentState == .delivered) + } + + @Test func rejectsTransitionAndKeepsState() async { + let machine = makeMachine() + await #expect(throws: StateMachineError.invalidTransition(from: Phase.preparing, to: .delivered)) { + try await machine.transition(to: .delivered) + } + #expect(await machine.currentState == .preparing) + } + + @Test func terminalStateHasNoNextStates() async throws { + let machine = makeMachine() + try await machine.transition(to: .cancelled) + #expect(await machine.allowedNextStates().isEmpty) + #expect(await machine.canTransition(to: .preparing) == false) + } + + @Test func reportsAllowedNextStates() async { + let machine = makeMachine() + #expect(await machine.allowedNextStates() == [.onTheWay, .cancelled]) + #expect(await machine.canTransition(to: .onTheWay)) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/NetworkingTests.swift b/Tests/WidgetDevelopmentKitTests/NetworkingTests.swift new file mode 100644 index 0000000..0ce295c --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/NetworkingTests.swift @@ -0,0 +1,160 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +/// Replays scripted responses and records requests and sleeps. +private final class ScriptedTransport: @unchecked Sendable { + enum Step { + case status(Int, String) + case failure(URLError.Code) + case notHTTP + } + + private let lock = NSLock() + private var steps: [Step] + private(set) var requestCount = 0 + private(set) var sleeps: [Duration] = [] + + init(_ steps: [Step]) { self.steps = steps } + + func respond(to request: URLRequest) throws -> (Data, URLResponse) { + let step: Step = lock.withLock { + requestCount += 1 + return steps.removeFirst() + } + let url = request.url! + switch step { + case .status(let code, let body): + let response = HTTPURLResponse(url: url, statusCode: code, httpVersion: nil, headerFields: nil)! + return (Data(body.utf8), response) + case .failure(let code): + throw URLError(code) + case .notHTTP: + return (Data(), URLResponse(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil)) + } + } + + func recordSleep(_ duration: Duration) { + lock.withLock { sleeps.append(duration) } + } + + func loader(maxAttempts: Int = 3) -> WidgetDataLoader { + WidgetDataLoader( + retryPolicy: RetryPolicy(maxAttempts: maxAttempts, initialDelay: .milliseconds(100)), + transport: { try self.respond(to: $0) }, + sleep: { self.recordSleep($0) } + ) + } +} + +private struct Quote: Decodable, Equatable { + let text: String +} + +@Suite("WidgetDataLoader") +struct WidgetDataLoaderTests { + let request = URLRequest(url: URL(string: "https://example.com/quote")!) + + @Test func decodesSuccessfulResponse() async throws { + let transport = ScriptedTransport([.status(200, #"{"text":"Hello"}"#)]) + let quote = try await transport.loader().load(Quote.self, from: request) + #expect(quote == Quote(text: "Hello")) + #expect(transport.requestCount == 1) + } + + @Test func retriesServerErrorsWithBackoff() async throws { + let transport = ScriptedTransport([ + .status(503, ""), .failure(.timedOut), .status(200, #"{"text":"Third time"}"#) + ]) + let quote = try await transport.loader().load(Quote.self, from: request) + #expect(quote.text == "Third time") + #expect(transport.requestCount == 3) + #expect(transport.sleeps == [.milliseconds(100), .milliseconds(200)]) + } + + @Test func doesNotRetryClientErrors() async { + let transport = ScriptedTransport([.status(404, ""), .status(200, "{}")]) + await #expect(throws: WidgetNetworkError.httpStatus(404)) { + try await transport.loader().loadData(from: request) + } + #expect(transport.requestCount == 1) + #expect(transport.sleeps.isEmpty) + } + + @Test func stopsAfterMaxAttempts() async { + let transport = ScriptedTransport([.status(500, ""), .status(502, "")]) + await #expect(throws: WidgetNetworkError.httpStatus(502)) { + try await transport.loader(maxAttempts: 2).loadData(from: request) + } + #expect(transport.requestCount == 2) + } + + @Test func reportsDecodingAndResponseErrors() async { + let badJSON = ScriptedTransport([.status(200, "not json")]) + await #expect { + try await badJSON.loader().load(Quote.self, from: request) + } throws: { error in + guard case WidgetNetworkError.decodingFailed = error else { return false } + return true + } + #expect(badJSON.requestCount == 1) + + let notHTTP = ScriptedTransport([.notHTTP]) + await #expect(throws: WidgetNetworkError.invalidResponse) { + try await notHTTP.loader().loadData(from: request) + } + } + + @Test func offlineIsTransientButCancelledIsNot() { + #expect(WidgetNetworkError.transport(.notConnectedToInternet).isTransient) + #expect(WidgetNetworkError.transport(.cancelled).isTransient == false) + #expect(WidgetNetworkError.httpStatus(429).isTransient == false) + #expect(WidgetNetworkError.httpStatus(408).isTransient) + } +} + +@Suite("RetryPolicy") +struct RetryPolicyTests { + @Test func delaysDoubleAfterFirstRetry() { + let policy = RetryPolicy(maxAttempts: 4, initialDelay: .seconds(1)) + #expect(policy.delay(beforeAttempt: 1) == .zero) + #expect(policy.delay(beforeAttempt: 2) == .seconds(1)) + #expect(policy.delay(beforeAttempt: 3) == .seconds(2)) + #expect(policy.delay(beforeAttempt: 4) == .seconds(4)) + } + + @Test func atLeastOneAttempt() { + #expect(RetryPolicy(maxAttempts: 0).maxAttempts == 1) + #expect(RetryPolicy.none.maxAttempts == 1) + } +} + +@Suite("Endpoint") +struct EndpointTests { + let base = URL(string: "https://api.example.com/v1")! + + @Test func buildsPathQueryAndHeaders() throws { + var endpoint = Endpoint(base: base) + .appendingPath("quotes/random/") + .adding(query: "lang", "en") + .adding(query: "tag", "swift") + .adding(query: "tag", "ios") + .adding(header: "Accept", "application/json") + endpoint.method = "POST" + + let request = try #require(endpoint.request) + #expect(request.url?.absoluteString == "https://api.example.com/v1/quotes/random?lang=en&tag=swift&tag=ios") + #expect(request.value(forHTTPHeaderField: "Accept") == "application/json") + #expect(request.httpMethod == "POST") + } + + @Test func encodesPathSegmentsAndKeepsBaseQuery() throws { + let baseWithQuery = try #require(URL(string: "https://api.example.com?key=abc")) + let url = try #require(Endpoint(base: baseWithQuery).appendingPath("city/New York").adding(query: "q", "a&b").url) + #expect(url.absoluteString == "https://api.example.com/city/New%20York?key=abc&q=a%26b") + } + + @Test func emptyPathLeavesBaseUnchanged() { + #expect(Endpoint(base: base).appendingPath("//").url == base) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift b/Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift new file mode 100644 index 0000000..9628326 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift @@ -0,0 +1,108 @@ +// README.md quotes the interactive button and the networking example below; its other +// examples come from Examples/WidgetKitExample. The steps provider is repeated here so +// its timeline can be tested, and the last example is the one in the Interactive Widgets +// DocC article. ReadmeSyncTests checks that every Swift block in README.md appears in +// this file or in Examples/. + +import AppIntents +import Foundation +import SwiftUI +import Testing +import WidgetKit +import WidgetDevelopmentKit + +// Same provider as Examples/WidgetKitExample/Shared/Steps.swift +struct StepsEntry: TimelineEntry { + let date: Date + let steps: Int +} + +struct StepsProvider: TimelineProvider { + let store = WidgetSnapshotStore(defaults: UserDefaults(suiteName: "group.com.example.widgets") ?? .standard) + + func placeholder(in context: Context) -> StepsEntry { + StepsEntry(date: .now, steps: 6_000) + } + + func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> Void) { + completion(placeholder(in: context)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + completion(makeTimeline(now: .now)) + } + + func makeTimeline(now: Date) -> Timeline { + // Written by the app; stale data is better than an empty widget. + let steps = store.staleValue(Int.self, forKey: "steps") ?? 0 + let entries = TimelineSchedule + .dates(from: now, every: 15 * 60, count: 4) + .map { StepsEntry(date: $0, steps: steps) } + return TimelineBuilder(entries: entries) + .reloading(.every(minutes: 60), from: now) + .build() + } +} + +// Same view and configuration as Examples/WidgetKitExample +struct StepsWidgetView: View { + let entry: StepsEntry + + var body: some View { + WidgetProgressRing(progress: Double(entry.steps), total: 10_000, colors: [.green, .mint]) + .padding() + .containerBackground(.fill.tertiary, for: .widget) + .widgetURL(WidgetDeepLink(scheme: "steps", host: "today")) + } +} + +struct StepsWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration(kind: "steps", provider: StepsProvider()) { entry in + StepsWidgetView(entry: entry) + } + .configurationDisplayName("Steps") + .supportedFamilies([.systemSmall]) + } +} + +// README: Interactive button +struct WaterButton: View { + var body: some View { + Button(intent: IncrementIntent(counterId: "water", amount: 250)) { + Label("+250 ml", systemImage: "drop.fill") + } + } +} + +// README: Networking +struct RandomQuote: Decodable { + let text: String +} + +func loadQuote() async throws -> RandomQuote { + guard let base = URL(string: "https://api.example.com") else { throw URLError(.badURL) } + let endpoint = Endpoint(base: base) + .appendingPath("v1/quotes/random") + .adding(query: "lang", "en") + guard let request = endpoint.request else { throw URLError(.badURL) } + return try await WidgetDataLoader().load(RandomQuote.self, from: request) +} + +@Suite("README examples") +struct ReadmeExamplesTests { + @Test func providerBuildsFourEntriesAndHourlyReload() throws { + let now = Date(timeIntervalSince1970: 1_800_000_000) + let timeline = StepsProvider().makeTimeline(now: now) + #expect(timeline.entries.count == 4) + #expect(timeline.entries.last?.date == now.addingTimeInterval(45 * 60)) + #expect(timeline.policy == .after(now.addingTimeInterval(60 * 60))) + } +} + +// DocC article "Interactive Widgets": register the package's intents +struct AppIntentsRoot: AppIntentsPackage { + static var includedPackages: [any AppIntentsPackage.Type] { + [WidgetDevelopmentKitIntents.self] + } +} diff --git a/Tests/WidgetDevelopmentKitTests/ReadmeSyncTests.swift b/Tests/WidgetDevelopmentKitTests/ReadmeSyncTests.swift new file mode 100644 index 0000000..395b6c3 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/ReadmeSyncTests.swift @@ -0,0 +1,79 @@ +import Foundation +import Testing + +/// README.md says its Swift examples are copied from compiled code. This suite holds it to +/// that: every Swift block in README.md must appear, line by line, in a Swift file under +/// Examples/ (built by the Examples workflow) or in ReadmeExamplesTests.swift (built with +/// the tests). Indentation and blank lines are ignored, so a block may come from inside a +/// function. The `Package.swift` fragment is the only block that is exempt. +@Suite("README code blocks") +struct ReadmeSyncTests { + static let root = URL(filePath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + + static func normalizedLines(_ text: String) -> [String] { + text.split(separator: "\n", omittingEmptySubsequences: false) + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + } + + static func swiftBlocks(in markdown: String) -> [String] { + var blocks: [String] = [] + var current: [String]? + for line in markdown.split(separator: "\n", omittingEmptySubsequences: false) { + if current == nil, line.hasPrefix("```swift") { + current = [] + } else if current != nil, line.hasPrefix("```") { + blocks.append(current?.joined(separator: "\n") ?? "") + current = nil + } else { + current?.append(String(line)) + } + } + return blocks + } + + static func sourceFiles() throws -> [[String]] { + var urls = [root.appending(path: "Tests/WidgetDevelopmentKitTests/ReadmeExamplesTests.swift")] + let examples = root.appending(path: "Examples") + let enumerator = FileManager.default.enumerator(at: examples, includingPropertiesForKeys: nil) + while let url = enumerator?.nextObject() as? URL { + if url.pathExtension == "swift" { urls.append(url) } + } + return try urls.map { normalizedLines(try String(contentsOf: $0, encoding: .utf8)) } + } + + static func contains(_ haystack: [String], _ needle: [String]) -> Bool { + guard !needle.isEmpty, needle.count <= haystack.count else { return needle.isEmpty } + return (0...(haystack.count - needle.count)).contains { start in + Array(haystack[start..<(start + needle.count)]) == needle + } + } + + @Test func everySwiftBlockComesFromCompiledCode() throws { + let readme = try String(contentsOf: Self.root.appending(path: "README.md"), encoding: .utf8) + let blocks = Self.swiftBlocks(in: readme) + .filter { !$0.hasPrefix("dependencies:") } + #expect(blocks.count >= 5, "expected the README's Swift examples, found \(blocks.count)") + let sources = try Self.sourceFiles() + #expect(sources.count > 1, "no Swift files found under Examples/") + for block in blocks { + let lines = Self.normalizedLines(block) + #expect(sources.contains { Self.contains($0, lines) }, "not found in Examples/ or ReadmeExamplesTests.swift:\n\(block)") + } + } + + @Test func matchingIgnoresIndentationButNotContent() { + let file = Self.normalizedLines("func f() {\n let a = 1\n\n let b = 2\n}") + #expect(Self.contains(file, Self.normalizedLines("let a = 1\nlet b = 2"))) + #expect(!Self.contains(file, Self.normalizedLines("let a = 1\nlet b = 3"))) + #expect(!Self.contains(file, Self.normalizedLines("let b = 2\nlet a = 1"))) + } + + @Test func extractsOnlySwiftBlocks() { + let markdown = "text\n```swift\nlet a = 1\n```\n```sh\nswift test\n```\n```swift\nlet b = 2\n```" + #expect(Self.swiftBlocks(in: markdown) == ["let a = 1", "let b = 2"]) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift b/Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift new file mode 100644 index 0000000..5fea387 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/ScreenshotRenderer.swift @@ -0,0 +1,223 @@ +#if os(iOS) +import Foundation +import SwiftUI +import Testing +import UIKit +import WidgetKit +@testable import WidgetDevelopmentKit + +/// Renders the README screenshots with `ImageRenderer`. +/// +/// Disabled unless `WDK_SCREENSHOT_DIR` is set; run `scripts/render-screenshots.sh`. +/// The views are rendered outside WidgetKit, so the system widget chrome +/// (container background, Lock Screen vibrancy, StandBy night mode) is +/// approximated by the frames drawn here. +@MainActor +@Suite("README screenshots", .enabled(if: ProcessInfo.processInfo.environment["WDK_SCREENSHOT_DIR"] != nil)) +struct ScreenshotRenderer { + private static let reference = TestCalendar.date(2026, 10, 14, 9, 41) + private let outputDirectory = URL( + fileURLWithPath: ProcessInfo.processInfo.environment["WDK_SCREENSHOT_DIR"] ?? NSTemporaryDirectory() + ) + + @Test(arguments: [ColorScheme.light, .dark]) + func homeScreenTemplates(scheme: ColorScheme) throws { + try render(name: "home-screen", scheme: scheme) { + Gallery(scheme: scheme) { + HStack(spacing: 16) { + SystemWidget(.small, scheme) { + WeatherWidget(temperature: 21, condition: .partlyCloudy, location: "Istanbul", high: 24, low: 15) + } + SystemWidget(.small, scheme) { + WaterTrackerWidget(currentIntake: 1_250) + } + } + SystemWidget(.medium, scheme) { + FitnessWidget(calories: 420, exercise: 24, standing: 9) + } + HStack(spacing: 16) { + SystemWidget(.small, scheme) { + CountdownWidget( + title: "Launch", + targetDate: Self.reference.addingTimeInterval(3 * 86_400 + 5 * 3_600 + 12 * 60), + referenceDate: Self.reference + ) + } + SystemWidget(.small, scheme) { + QuoteWidget(quote: "Simplicity is prerequisite for reliability.", author: "Edsger W. Dijkstra") + } + } + SystemWidget(.medium, scheme) { + HabitTrackerWidget(habits: [ + .init(name: "Read", icon: "book.fill", color: .orange, + completedDays: [true, true, false, true, true, true, true]), + .init(name: "Run", icon: "figure.run", color: .green, + completedDays: [false, true, true, false, true, false, true]), + .init(name: "Meditate", icon: "brain.head.profile", color: .purple, + completedDays: [true, true, true, true, false, true, false]) + ]) + } + } + } + } + + @Test(arguments: [ColorScheme.light, .dark]) + func charts(scheme: ColorScheme) throws { + let start = Self.reference + let series = [12.0, 18, 15, 22, 19, 27, 24, 31, 29, 35] + try render(name: "charts", scheme: scheme) { + Gallery(scheme: scheme) { + HStack(spacing: 16) { + SystemWidget(.small, scheme) { + WidgetStatsChart(title: "Revenue", value: "$12.4K", change: 8.3, sparklineData: series, accentColor: .green) + .padding() + } + SystemWidget(.small, scheme) { + WidgetProgressRing(progress: 0.68, colors: [.blue, .purple]) + .padding(24) + } + } + SystemWidget(.medium, scheme) { + WidgetLineChart( + data: series.enumerated().map { + TimeSeriesDataPoint(date: start.addingTimeInterval(Double($0.offset) * 3_600), value: $0.element) + }, + accentColor: .orange + ) + .padding() + } + SystemWidget(.medium, scheme) { + WidgetBarChart( + data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].enumerated().map { + ChartDataPoint(label: $0.element, value: [6, 9, 4, 11, 8, 3, 7][$0.offset]) + }, + accentColor: .indigo + ) + .padding() + } + } + } + } + + @Test(arguments: [ColorScheme.light, .dark]) + func lockScreen(scheme: ColorScheme) throws { + try render(name: "lock-screen", scheme: scheme) { + Gallery(scheme: scheme, background: scheme == .dark ? Color(white: 0.08) : Color(white: 0.35)) { + HStack(spacing: 20) { + AccessoryWidget(.circular) { + LockScreenBatteryWidget(level: 0.72, family: .accessoryCircular) + } + AccessoryWidget(.circular) { + LockScreenFitnessWidget(moveProgress: 0.8, exerciseProgress: 0.55, standProgress: 0.9) + } + AccessoryWidget(.circular) { + LockScreenCountdownWidget( + targetDate: Self.reference.addingTimeInterval(12 * 86_400), + eventName: "Trip", + referenceDate: Self.reference + ) + } + } + AccessoryWidget(.rectangular) { + RectangularWeatherWidget(temperature: 21, high: 24, low: 15, condition: "cloud.sun.fill", location: "Istanbul") + } + AccessoryWidget(.rectangular) { + // Linear gauges and progress views render as placeholders in ImageRenderer, + // so the README shows a text-only rectangular widget here. + RectangularStatsWidget(stats: [("Steps", "8.2K"), ("km", "6.1"), ("kcal", "420")], title: "Today") + } + } + } + } + + // MARK: - Rendering + + private func render(name: String, scheme: ColorScheme, @ViewBuilder content: () -> Content) throws { + let view = content() + .environment(\.colorScheme, scheme) + .environment(\.locale, Locale(identifier: "en_US")) + .environment(\.timeZone, TimeZone(identifier: "UTC")!) + let renderer = ImageRenderer(content: view) + renderer.scale = 2 + let image = try #require(renderer.uiImage) + let data = try #require(image.pngData()) + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + let suffix = scheme == .dark ? "dark" : "light" + try data.write(to: outputDirectory.appendingPathComponent("\(name)-\(suffix).png")) + } +} + +/// Lays out a column of widgets on a plain background. +private struct Gallery: View { + let scheme: ColorScheme + var background: Color? + @ViewBuilder let content: Content + + var body: some View { + VStack(spacing: 16) { content } + .padding(24) + .background(background ?? (scheme == .dark ? Color.black : Color(white: 0.93))) + } +} + +/// Approximates a Home Screen widget: fixed size, rounded corners, system background. +private struct SystemWidget: View { + enum Size { + case small, medium + var frame: CGSize { + switch self { + case .small: CGSize(width: 170, height: 170) + case .medium: CGSize(width: 356, height: 170) + } + } + } + + let size: Size + let scheme: ColorScheme + let content: Content + + init(_ size: Size, _ scheme: ColorScheme, @ViewBuilder content: () -> Content) { + self.size = size + self.scheme = scheme + self.content = content() + } + + var body: some View { + content + .frame(width: size.frame.width, height: size.frame.height) + .background(scheme == .dark ? Color(white: 0.11) : .white) + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) + } +} + +/// Approximates a Lock Screen accessory widget: white content on a translucent backing. +private struct AccessoryWidget: View { + enum Shape { + case circular, rectangular + var frame: CGSize { + switch self { + case .circular: CGSize(width: 72, height: 72) + case .rectangular: CGSize(width: 172, height: 76) + } + } + } + + let shape: Shape + let content: Content + + init(_ shape: Shape, @ViewBuilder content: () -> Content) { + self.shape = shape + self.content = content() + } + + var body: some View { + content + .foregroundStyle(.white) + .tint(.white) + .padding(shape == .rectangular ? 8 : 0) + .frame(width: shape.frame.width, height: shape.frame.height) + .background(.white.opacity(0.15), in: RoundedRectangle(cornerRadius: shape == .circular ? 36 : 12)) + .environment(\.colorScheme, .dark) + } +} +#endif diff --git a/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift b/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift new file mode 100644 index 0000000..47f12da --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TemplateLogicTests.swift @@ -0,0 +1,47 @@ +import SwiftUI +import Testing +@testable import WidgetDevelopmentKit + +@MainActor +@Suite("Template logic") +struct TemplateLogicTests { + @Test(arguments: [ + ([true, true, false, true, true, true], 3), + ([true, true, true], 3), + ([true, false], 0), + ([Bool](), 0) + ]) + func habitStreakCountsTrailingCompletedDays(days: [Bool], expected: Int) { + let habit = HabitTrackerWidget.Habit(name: "Read", icon: "book", color: .blue, completedDays: days) + #expect(habit.streak == expected) + } + + @Test func waterTrackerCountsGlassesAndGoal() { + let widget = WaterTrackerWidget(currentIntake: 1_100, dailyGoal: 2_000, glassSize: 250) + #expect(widget.glasses == 4) + #expect(widget.glassGoal == 8) + #expect(widget.progress == 0.55) + } + + @Test func waterTrackerHandlesInvalidSizes() { + let zeroGlass = WaterTrackerWidget(currentIntake: 500, dailyGoal: 2_000, glassSize: 0) + #expect(zeroGlass.glasses == 0) + #expect(zeroGlass.glassGoal == 0) + + let zeroGoal = WaterTrackerWidget(currentIntake: 500, dailyGoal: 0, glassSize: 250) + #expect(zeroGoal.progress == 0) + #expect(zeroGoal.glassGoal == 0) + } + + @Test func waterTrackerCapsGlassIcons() { + let widget = WaterTrackerWidget(currentIntake: 0, dailyGoal: 10_000, glassSize: 100) + #expect(widget.glassGoal == 12) + } + + @Test func scrubberIndexStaysInRange() { + typealias Scrubber = WidgetTimelineScrubber + #expect(Scrubber.clampedIndex(5, count: 3) == 2) + #expect(Scrubber.clampedIndex(-1, count: 3) == 0) + #expect(Scrubber.clampedIndex(4, count: 0) == 0) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/TestSupport.swift b/Tests/WidgetDevelopmentKitTests/TestSupport.swift new file mode 100644 index 0000000..1f38dd3 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TestSupport.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Deterministic calendar and dates so tests do not depend on the machine's locale or time zone. +enum TestCalendar { + static func gregorian(firstWeekday: Int = 1, timeZone: String = "UTC") -> Calendar { + var calendar = Calendar(identifier: .gregorian) + calendar.locale = Locale(identifier: "en_US_POSIX") + calendar.timeZone = TimeZone(identifier: timeZone)! + calendar.firstWeekday = firstWeekday + return calendar + } + + static func date( + _ year: Int, _ month: Int, _ day: Int, + _ hour: Int = 0, _ minute: Int = 0, _ second: Int = 0, + calendar: Calendar = gregorian() + ) -> Date { + let components = DateComponents( + year: year, month: month, day: day, + hour: hour, minute: minute, second: second + ) + return calendar.date(from: components)! + } +} diff --git a/Tests/WidgetDevelopmentKitTests/TimelineTests.swift b/Tests/WidgetDevelopmentKitTests/TimelineTests.swift new file mode 100644 index 0000000..dd13c18 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TimelineTests.swift @@ -0,0 +1,132 @@ +import Foundation +import Testing +import WidgetKit +@testable import WidgetDevelopmentKit + +private struct SampleEntry: TimelineEntry, Equatable { + let date: Date + let value: Int +} + +@Suite("WidgetUpdatePolicy") +struct WidgetUpdatePolicyTests { + let calendar = TestCalendar.gregorian() + let now = TestCalendar.date(2026, 10, 14, 9, 41, 30) + + @Test func nextHourIsStartOfFollowingHour() { + #expect(WidgetUpdatePolicy.nextHour.nextUpdateDate(from: now, calendar: calendar) + == TestCalendar.date(2026, 10, 14, 10)) + } + + @Test func nextHourRollsOverMidnight() { + let lateNight = TestCalendar.date(2026, 12, 31, 23, 15) + #expect(WidgetUpdatePolicy.nextHour.nextUpdateDate(from: lateNight, calendar: calendar) + == TestCalendar.date(2027, 1, 1)) + } + + @Test func startOfNextDayUsesCalendarDays() { + let newYork = TestCalendar.gregorian(timeZone: "America/New_York") + // 1 November 2026 is the US fall-back day and has 25 hours. + let evening = TestCalendar.date(2026, 10, 31, 22, calendar: newYork) + let expected = TestCalendar.date(2026, 11, 1, calendar: newYork) + #expect(WidgetUpdatePolicy.startOfNextDay.nextUpdateDate(from: evening, calendar: newYork) == expected) + + let fallBackDay = TestCalendar.date(2026, 11, 1, 12, calendar: newYork) + let nextMidnight = TestCalendar.date(2026, 11, 2, calendar: newYork) + #expect(WidgetUpdatePolicy.startOfNextDay.nextUpdateDate(from: fallBackDay, calendar: newYork) == nextMidnight) + } + + @Test func intervalAndMinutePolicies() { + #expect(WidgetUpdatePolicy.afterInterval(90).nextUpdateDate(from: now) == now.addingTimeInterval(90)) + #expect(WidgetUpdatePolicy.afterInterval(-5).nextUpdateDate(from: now) == now) + #expect(WidgetUpdatePolicy.every(minutes: 15).nextUpdateDate(from: now, calendar: calendar) + == now.addingTimeInterval(15 * 60)) + // Zero or negative minutes would reload in a tight loop; one minute is the floor. + #expect(WidgetUpdatePolicy.every(minutes: 0).nextUpdateDate(from: now, calendar: calendar) + == now.addingTimeInterval(60)) + } + + @Test func fixedCustomAndNever() { + let target = TestCalendar.date(2027, 1, 1) + #expect(WidgetUpdatePolicy.atDate(target).nextUpdateDate(from: now) == target) + #expect(WidgetUpdatePolicy.custom { $0.addingTimeInterval(5) }.nextUpdateDate(from: now) == now.addingTimeInterval(5)) + #expect(WidgetUpdatePolicy.never.nextUpdateDate(from: now) == nil) + } + + @Test func convertsToReloadPolicy() { + #expect(WidgetUpdatePolicy.never.reloadPolicy(from: now) == .never) + #expect(WidgetUpdatePolicy.afterInterval(60).reloadPolicy(from: now) == .after(now.addingTimeInterval(60))) + } +} + +@Suite("TimelineBuilder") +struct TimelineBuilderTests { + let start = TestCalendar.date(2026, 10, 14, 9) + + @Test func sortsEntriesChronologically() { + let entries = [2, 0, 1].map { SampleEntry(date: start.addingTimeInterval(Double($0) * 60), value: $0) } + let timeline = TimelineBuilder().adding(entries).build() + #expect(timeline.entries.map(\.value) == [0, 1, 2]) + #expect(timeline.policy == .atEnd) + } + + @Test func isAValueType() { + let base = TimelineBuilder().adding(SampleEntry(date: start, value: 1)) + let extended = base.adding(SampleEntry(date: start, value: 2)) + #expect(base.entries.count == 1) + #expect(extended.entries.count == 2) + } + + @Test func appliesUpdatePolicy() { + let calendar = TestCalendar.gregorian() + let timeline = TimelineBuilder() + .adding(SampleEntry(date: start, value: 0)) + .reloading(.nextHour, from: start.addingTimeInterval(120), calendar: calendar) + .build() + #expect(timeline.policy == .after(TestCalendar.date(2026, 10, 14, 10))) + } + + @Test func singleEntryTimeline() { + let timeline = Timeline.single(SampleEntry(date: start, value: 7), policy: .never) + #expect(timeline.entries == [SampleEntry(date: start, value: 7)]) + #expect(timeline.policy == .never) + } +} + +@Suite("TimelineSchedule") +struct TimelineScheduleTests { + let calendar = TestCalendar.gregorian() + + @Test func evenlySpacedDates() { + let start = TestCalendar.date(2026, 10, 14, 9) + let dates = TimelineSchedule.dates(from: start, every: 900, count: 3) + #expect(dates == [start, start.addingTimeInterval(900), start.addingTimeInterval(1_800)]) + #expect(TimelineSchedule.dates(from: start, every: 900, count: 0).isEmpty) + #expect(TimelineSchedule.dates(from: start, every: 0, count: 3).isEmpty) + } + + @Test func alignedDatesStartNowThenSnapToBoundaries() { + let start = TestCalendar.date(2026, 10, 14, 9, 7, 30) + let end = TestCalendar.date(2026, 10, 14, 10) + let dates = TimelineSchedule.alignedDates(from: start, until: end, everyMinutes: 15, calendar: calendar) + #expect(dates == [ + start, + TestCalendar.date(2026, 10, 14, 9, 15), + TestCalendar.date(2026, 10, 14, 9, 30), + TestCalendar.date(2026, 10, 14, 9, 45) + ]) + } + + @Test func alignedDatesWhenStartIsOnBoundary() { + let start = TestCalendar.date(2026, 10, 14, 9, 30) + let end = TestCalendar.date(2026, 10, 14, 10, 1) + let dates = TimelineSchedule.alignedDates(from: start, until: end, everyMinutes: 30, calendar: calendar) + #expect(dates == [start, TestCalendar.date(2026, 10, 14, 10)]) + } + + @Test func alignedDatesRejectInvalidInput() { + let start = TestCalendar.date(2026, 10, 14, 9) + #expect(TimelineSchedule.alignedDates(from: start, until: start, everyMinutes: 15, calendar: calendar).isEmpty) + #expect(TimelineSchedule.alignedDates(from: start, until: start.addingTimeInterval(3_600), everyMinutes: 0, calendar: calendar).isEmpty) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/TimerActivityStateTests.swift b/Tests/WidgetDevelopmentKitTests/TimerActivityStateTests.swift new file mode 100644 index 0000000..796ba1d --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/TimerActivityStateTests.swift @@ -0,0 +1,23 @@ +#if canImport(ActivityKit) && os(iOS) +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("Timer activity state") +struct TimerActivityStateTests { + @Test func runningTimerHasNoPausedLabel() { + let state = TimerAttributes.ContentState(endTime: .now, pausedTimeRemaining: 90) + #expect(state.pausedLabel == nil) + } + + @Test func pausedTimerShowsRemainingTime() { + let state = TimerAttributes.ContentState(endTime: .now, isPaused: true, pausedTimeRemaining: 125) + #expect(state.pausedLabel == "2:05") + } + + @Test func pausedTimerWithoutRemainingTimeReadsZero() { + let state = TimerAttributes.ContentState(endTime: .now, isPaused: true) + #expect(state.pausedLabel == "0:00") + } +} +#endif diff --git a/Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift b/Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift new file mode 100644 index 0000000..0f68a9e --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/WidgetMathTests.swift @@ -0,0 +1,100 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("WidgetMath") +struct WidgetMathTests { + @Test(arguments: [ + (-0.5, 0.0), (0.0, 0.0), (0.42, 0.42), (1.0, 1.0), (7.0, 1.0), + (Double.nan, 0.0), (Double.infinity, 0.0), (-Double.infinity, 0.0) + ]) + func clampedFraction(input: Double, expected: Double) { + #expect(WidgetMath.clampedFraction(input) == expected) + } + + @Test func fractionGuardsAgainstZeroAndNegativeTotals() { + #expect(WidgetMath.fraction(250, of: 500) == 0.5) + #expect(WidgetMath.fraction(900, of: 500) == 1) + #expect(WidgetMath.fraction(10, of: 0) == 0) + #expect(WidgetMath.fraction(10, of: -5) == 0) + #expect(WidgetMath.fraction(10, of: .infinity) == 0) + } + + @Test func percentRoundsDownAndClamps() { + #expect(WidgetMath.percent(0.999) == 99) + #expect(WidgetMath.percent(1.5) == 100) + #expect(WidgetMath.percentString(0.42) == "42%") + #expect(WidgetMath.percentString(-1) == "0%") + } + + @Test(arguments: [ + (0.05, "battery.0"), (0.1, "battery.25"), (0.3, "battery.50"), + (0.6, "battery.75"), (0.75, "battery.100"), (2.0, "battery.100"), (-1.0, "battery.0") + ]) + func batterySymbol(level: Double, expected: String) { + #expect(WidgetMath.batterySymbolName(level: level, isCharging: false) == expected) + } + + @Test func chargingAlwaysUsesBoltSymbol() { + #expect(WidgetMath.batterySymbolName(level: 0.05, isCharging: true) == "battery.100.bolt") + } + + @Test func timerIntervalNeverReversesBounds() { + let start = TestCalendar.date(2026, 3, 1, 12) + let earlier = start.addingTimeInterval(-60) + let later = start.addingTimeInterval(60) + + #expect(WidgetMath.timerInterval(start: start, end: later) == start...later) + #expect(WidgetMath.timerInterval(start: start, end: earlier) == start...start) + } + + @Test func daysRemainingCountsCalendarDays() { + let calendar = TestCalendar.gregorian() + let lateEvening = TestCalendar.date(2026, 3, 1, 23, 30) + let nextMorning = TestCalendar.date(2026, 3, 2, 8) + let nextWeek = TestCalendar.date(2026, 3, 8, 0, 1) + + // Less than 24 hours away, but on the next calendar day. + #expect(WidgetMath.daysRemaining(until: nextMorning, from: lateEvening, calendar: calendar) == 1) + #expect(WidgetMath.daysRemaining(until: nextWeek, from: lateEvening, calendar: calendar) == 7) + #expect(WidgetMath.daysRemaining(until: lateEvening, from: nextMorning, calendar: calendar) == 0) + } + + @Test func daysRemainingIsStableAcrossDaylightSavingChange() { + let calendar = TestCalendar.gregorian(timeZone: "America/New_York") + // US clocks spring forward on 8 March 2026, so that day has 23 hours. + let before = TestCalendar.date(2026, 3, 7, 12, calendar: calendar) + let after = TestCalendar.date(2026, 3, 9, 12, calendar: calendar) + #expect(WidgetMath.daysRemaining(until: after, from: before, calendar: calendar) == 2) + } + + @Test func intensityLevels() { + #expect(WidgetMath.intensityLevel(0, maximum: 10, levels: 5) == 0) + #expect(WidgetMath.intensityLevel(-3, maximum: 10, levels: 5) == 0) + #expect(WidgetMath.intensityLevel(0.1, maximum: 10, levels: 5) == 1) + #expect(WidgetMath.intensityLevel(5, maximum: 10, levels: 5) == 2) + #expect(WidgetMath.intensityLevel(10, maximum: 10, levels: 5) == 4) + #expect(WidgetMath.intensityLevel(25, maximum: 10, levels: 5) == 4) + #expect(WidgetMath.intensityLevel(5, maximum: 0, levels: 5) == 0) + #expect(WidgetMath.intensityLevel(5, maximum: 10, levels: 1) == 0) + } +} + +@Suite("WidgetFormat") +struct WidgetFormatTests { + @Test(arguments: [ + (0, "0"), (999, "999"), (1_000, "1.0K"), (12_345, "12.3K"), + (1_500_000, "1.5M"), (-2_500, "-2.5K"), (-42, "-42") + ]) + func compactNumber(input: Int, expected: String) { + #expect(WidgetFormat.compactNumber(input) == expected) + } + + @Test(arguments: [ + (0.0, "0:00"), (7.9, "0:07"), (187, "3:07"), (3_725, "1:02:05"), + (-10, "0:00"), (Double.nan, "0:00"), (Double.infinity, "0:00") + ]) + func playbackTime(input: Double, expected: String) { + #expect(WidgetFormat.playbackTime(input) == expected) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift b/Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift new file mode 100644 index 0000000..7717553 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/WidgetSnapshotStoreTests.swift @@ -0,0 +1,71 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +private struct Weather: Codable, Equatable { + let city: String + let temperature: Int +} + +/// A clock the test can move forward. +private final class TestClock: @unchecked Sendable { + private let lock = NSLock() + private var current: Date + init(_ date: Date) { current = date } + func now() -> Date { lock.withLock { current } } + func advance(by seconds: TimeInterval) { lock.withLock { current += seconds } } +} + +@Suite("WidgetSnapshotStore") +struct WidgetSnapshotStoreTests { + private let suiteName = "WidgetSnapshotStoreTests.\(UUID().uuidString)" + private let clock = TestClock(Date(timeIntervalSince1970: 1_800_000_000)) + + private func makeStore() -> (WidgetSnapshotStore, UserDefaults) { + let defaults = UserDefaults(suiteName: suiteName)! + let clock = clock + return (WidgetSnapshotStore(defaults: defaults, now: { clock.now() }), defaults) + } + + @Test func roundTripsBeforeExpiry() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save(Weather(city: "Istanbul", temperature: 21), forKey: "weather", lifetime: 600) + clock.advance(by: 599) + #expect(store.value(Weather.self, forKey: "weather") == Weather(city: "Istanbul", temperature: 21)) + } + + @Test func expiredValueIsRemovedButStaleReadWorksBeforeThat() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save(Weather(city: "Ankara", temperature: 12), forKey: "weather", lifetime: 60) + clock.advance(by: 61) + #expect(store.staleValue(Weather.self, forKey: "weather")?.city == "Ankara") + #expect(store.value(Weather.self, forKey: "weather") == nil) + #expect(store.staleValue(Weather.self, forKey: "weather") == nil) + } + + @Test func wrongTypeAndMissingKeyReturnNil() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save(42, forKey: "count", lifetime: 60) + #expect(store.value(Weather.self, forKey: "count") == nil) + #expect(store.value(Int.self, forKey: "count") == 42) + #expect(store.value(Int.self, forKey: "missing") == nil) + } + + @Test func keysAreNamespaced() throws { + let (store, defaults) = makeStore() + defer { defaults.removePersistentDomain(forName: suiteName) } + + try store.save("hello", forKey: "greeting", lifetime: 60) + #expect(defaults.data(forKey: "widget.snapshot.greeting") != nil) + #expect(defaults.data(forKey: "greeting") == nil) + + store.removeValue(forKey: "greeting") + #expect(store.value(String.self, forKey: "greeting") == nil) + } +} diff --git a/Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift b/Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift new file mode 100644 index 0000000..51d7bc3 --- /dev/null +++ b/Tests/WidgetDevelopmentKitTests/WidgetStateStoreTests.swift @@ -0,0 +1,68 @@ +import Foundation +import Testing +@testable import WidgetDevelopmentKit + +@Suite("WidgetStateStore") +struct WidgetStateStoreTests { + private let suiteName = "WidgetStateStoreTests.\(UUID().uuidString)" + + private func withStore(_ body: (WidgetStateStore, UserDefaults) throws -> Void) rethrows { + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + try body(WidgetStateStore(defaults: defaults), defaults) + } + + @Test func flagsDefaultToOffAndPersist() { + withStore { store, _ in + #expect(store.isOn("lamp") == false) + store.setOn(true, for: "lamp") + #expect(store.isOn("lamp")) + #expect(store.isOn("other") == false) + } + } + + @Test func incrementRespectsMinimum() { + withStore { store, _ in + #expect(store.increment("water", by: 250) == 250) + #expect(store.increment("water", by: 250) == 500) + #expect(store.increment("water", by: -1_000, minimum: 0) == 0) + #expect(store.value("water") == 0) + } + } + + @Test func incrementSaturatesInsteadOfOverflowing() { + withStore { store, _ in + store.setValue(.max - 1, for: "big") + #expect(store.increment("big", by: 10) == .max) + store.setValue(.min + 1, for: "small") + #expect(store.increment("small", by: -10) == .min) + } + } + + @Test func pendingActionsAreFIFOAndDrained() { + withStore { store, _ in + store.enqueueAction("timer.start.tea") + store.enqueueAction("timer.stop.tea") + #expect(store.drainPendingActions() == ["timer.start.tea", "timer.stop.tea"]) + #expect(store.pendingActions().isEmpty) + } + } + + @Test func pendingActionsKeepMostRecentFifty() { + withStore { store, _ in + for index in 0..<60 { store.enqueueAction("action.\(index)") } + let actions = store.pendingActions() + #expect(actions.count == 50) + #expect(actions.first == "action.10") + #expect(actions.last == "action.59") + } + } + + @Test func keysAreNamespaced() { + withStore { store, defaults in + store.setValue(3, for: "rating") + #expect(defaults.integer(forKey: "widget.state.value.rating") == 3) + #expect(defaults.object(forKey: "rating") == nil) + } + } +} diff --git a/Tests/iOS-Widget-Development-KitTests.swift b/Tests/iOS-Widget-Development-KitTests.swift deleted file mode 100644 index 06c27a2..0000000 --- a/Tests/iOS-Widget-Development-KitTests.swift +++ /dev/null @@ -1,45 +0,0 @@ -import XCTest -@testable import iOS-Widget-Development-Kit - -final class iOS-Widget-Development-KitTests: XCTestCase { - var framework: iOS-Widget-Development-Kit! - - override func setUpWithError() throws { - framework = iOS-Widget-Development-Kit() - } - - override func tearDownWithError() throws { - framework = nil - } - - func testInitialization() throws { - // Test basic initialization - XCTAssertNotNil(framework) - XCTAssertTrue(framework is iOS-Widget-Development-Kit) - } - - func testConfiguration() throws { - // Test configuration - XCTAssertNoThrow(framework.configure()) - } - - func testPerformance() throws { - // Performance test - measure { - framework.configure() - } - } - - func testErrorHandling() throws { - // Test error scenarios - // Add your error handling tests here - XCTAssertTrue(true) // Placeholder - } - - static var allTests = [ - ("testInitialization", testInitialization), - ("testConfiguration", testConfiguration), - ("testPerformance", testPerformance), - ("testErrorHandling", testErrorHandling) - ] -} diff --git a/WidgetDevelopmentKit.podspec b/WidgetDevelopmentKit.podspec deleted file mode 100644 index 9d9478e..0000000 --- a/WidgetDevelopmentKit.podspec +++ /dev/null @@ -1,55 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'WidgetDevelopmentKit' - s.version = '1.0.0' - s.summary = 'The complete toolkit for building beautiful iOS widgets with WidgetKit, Live Activities, and Dynamic Island.' - s.description = <<-DESC - WidgetDevelopmentKit provides everything you need to build stunning iOS widgets. - Features include Home Screen Widgets, Lock Screen Widgets, Live Activities, - StandBy Mode support, Interactive Widgets with App Intents, and 20+ pre-built - templates. Build beautiful widgets in minutes, not days. - DESC - - s.homepage = 'https://github.com/muhittincamdali/iOS-Widget-Development-Kit' - s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'Muhittin Camdali' => 'contact@muhittincamdali.com' } - s.source = { :git => 'https://github.com/muhittincamdali/iOS-Widget-Development-Kit.git', :tag => s.version.to_s } - s.social_media_url = 'https://twitter.com/muhittincamdali' - - s.ios.deployment_target = '16.0' - s.osx.deployment_target = '13.0' - s.tvos.deployment_target = '16.0' - s.watchos.deployment_target = '9.0' - - s.swift_versions = ['5.9', '5.10', '6.0'] - - s.source_files = 'Sources/**/*.swift' - - s.frameworks = 'Foundation', 'SwiftUI', 'WidgetKit' - - s.default_subspecs = 'Core' - - s.subspec 'Core' do |core| - core.source_files = 'Sources/Core/**/*.swift' - end - - s.subspec 'Security' do |security| - security.source_files = 'Sources/Security/**/*.swift' - security.dependency 'WidgetDevelopmentKit/Core' - end - - s.subspec 'Performance' do |performance| - performance.source_files = 'Sources/Performance/**/*.swift' - performance.dependency 'WidgetDevelopmentKit/Core' - end - - s.subspec 'Enterprise' do |enterprise| - enterprise.source_files = 'Sources/Enterprise/**/*.swift' - enterprise.dependency 'WidgetDevelopmentKit/Core' - enterprise.dependency 'WidgetDevelopmentKit/Security' - end - - s.subspec 'Integration' do |integration| - integration.source_files = 'Sources/Integration/**/*.swift' - integration.dependency 'WidgetDevelopmentKit/Core' - end -end diff --git a/docs/images/charts-dark.png b/docs/images/charts-dark.png new file mode 100644 index 0000000..1784449 Binary files /dev/null and b/docs/images/charts-dark.png differ diff --git a/docs/images/charts-light.png b/docs/images/charts-light.png new file mode 100644 index 0000000..d979397 Binary files /dev/null and b/docs/images/charts-light.png differ diff --git a/docs/images/home-screen-dark.png b/docs/images/home-screen-dark.png new file mode 100644 index 0000000..8697407 Binary files /dev/null and b/docs/images/home-screen-dark.png differ diff --git a/docs/images/home-screen-light.png b/docs/images/home-screen-light.png new file mode 100644 index 0000000..b6ae151 Binary files /dev/null and b/docs/images/home-screen-light.png differ diff --git a/docs/images/lock-screen-dark.png b/docs/images/lock-screen-dark.png new file mode 100644 index 0000000..0900c8f Binary files /dev/null and b/docs/images/lock-screen-dark.png differ diff --git a/docs/images/lock-screen-light.png b/docs/images/lock-screen-light.png new file mode 100644 index 0000000..1f95f9f Binary files /dev/null and b/docs/images/lock-screen-light.png differ diff --git a/docs/social-preview.png b/docs/social-preview.png new file mode 100644 index 0000000..728584e Binary files /dev/null and b/docs/social-preview.png differ diff --git a/scripts/render-screenshots.sh b/scripts/render-screenshots.sh new file mode 100755 index 0000000..ee2a441 --- /dev/null +++ b/scripts/render-screenshots.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# Renders the README screenshots into docs/images/ by running the opt-in +# "README screenshots" test suite on an iOS simulator. +# +# Usage: scripts/render-screenshots.sh ["" | ] +# DESTINATION='platform=iOS Simulator,id=...' scripts/render-screenshots.sh +# +# If more than one simulator has the same name, xcodebuild refuses to pick one; pass the +# UDID instead (see `xcrun simctl list devices available`). +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +simulator="${1:-iPhone 17 Pro}" +udid_pattern='^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$' +if [[ -n "${DESTINATION:-}" ]]; then + destination="$DESTINATION" +elif [[ "$simulator" =~ $udid_pattern ]]; then + destination="platform=iOS Simulator,id=$simulator" +else + destination="platform=iOS Simulator,name=$simulator" +fi +output_dir="$repo_root/docs/images" +derived_data="$(mktemp -d)" +trap 'rm -rf "$derived_data"' EXIT + +mkdir -p "$output_dir" +cd "$repo_root" + +# xcodebuild forwards TEST_RUNNER_-prefixed variables to the test process. +TEST_RUNNER_WDK_SCREENSHOT_DIR="$output_dir" xcodebuild test \ + -scheme WidgetDevelopmentKit \ + -destination "$destination" \ + -derivedDataPath "$derived_data" \ + -only-testing:WidgetDevelopmentKitTests/ScreenshotRenderer \ + -quiet + +ls -1 "$output_dir"