moai-adk 0.4.5__py3-none-any.whl → 0.20.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of moai-adk might be problematic. Click here for more details.
- moai_adk/__init__.py +1 -1
- moai_adk/__main__.py +74 -1
- moai_adk/cli/commands/__init__.py +1 -1
- moai_adk/cli/commands/analyze.py +119 -0
- moai_adk/cli/commands/backup.py +25 -1
- moai_adk/cli/commands/doctor.py +31 -5
- moai_adk/cli/commands/improve_user_experience.py +307 -0
- moai_adk/cli/commands/init.py +111 -10
- moai_adk/cli/commands/status.py +33 -3
- moai_adk/cli/commands/update.py +921 -130
- moai_adk/cli/commands/validate_links.py +120 -0
- moai_adk/cli/prompts/init_prompts.py +22 -87
- moai_adk/core/analysis/__init__.py +9 -0
- moai_adk/core/analysis/session_analyzer.py +388 -0
- moai_adk/core/analysis/tag_chain_analyzer.py +344 -0
- moai_adk/core/analysis/tag_chain_repair.py +879 -0
- moai_adk/core/config/__init__.py +19 -0
- moai_adk/core/config/migration.py +235 -0
- moai_adk/core/git/__init__.py +1 -1
- moai_adk/core/git/branch.py +1 -1
- moai_adk/core/git/commit.py +1 -1
- moai_adk/core/git/manager.py +1 -1
- moai_adk/core/issue_creator.py +313 -0
- moai_adk/core/mcp/setup.py +56 -0
- moai_adk/core/mcp/setup_old.py +296 -0
- moai_adk/core/project/backup_utils.py +1 -1
- moai_adk/core/project/checker.py +2 -2
- moai_adk/core/project/detector.py +211 -12
- moai_adk/core/project/initializer.py +85 -15
- moai_adk/core/project/phase_executor.py +76 -13
- moai_adk/core/project/validator.py +13 -13
- moai_adk/core/quality/__init__.py +1 -1
- moai_adk/core/quality/trust_checker.py +1 -1
- moai_adk/core/quality/validators/__init__.py +1 -1
- moai_adk/core/quality/validators/base_validator.py +1 -1
- moai_adk/core/tags/__init__.py +86 -0
- moai_adk/core/tags/auto_corrector.py +693 -0
- moai_adk/core/tags/ci_validator.py +463 -0
- moai_adk/core/tags/cli.py +283 -0
- moai_adk/core/tags/generator.py +109 -0
- moai_adk/core/tags/inserter.py +99 -0
- moai_adk/core/tags/mapper.py +126 -0
- moai_adk/core/tags/parser.py +76 -0
- moai_adk/core/tags/policy_validator.py +580 -0
- moai_adk/core/tags/pre_commit_validator.py +421 -0
- moai_adk/core/tags/reporter.py +956 -0
- moai_adk/core/tags/rollback_manager.py +525 -0
- moai_adk/core/tags/tags.py +149 -0
- moai_adk/core/tags/validator.py +897 -0
- moai_adk/core/template/__init__.py +1 -1
- moai_adk/core/template/backup.py +1 -1
- moai_adk/core/template/merger.py +50 -1
- moai_adk/core/template/processor.py +119 -13
- moai_adk/core/template_engine.py +268 -0
- moai_adk/templates/.claude/agents/alfred/backend-expert.md +348 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +209 -944
- moai_adk/templates/.claude/agents/alfred/database-expert.md +352 -0
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +34 -5
- moai_adk/templates/.claude/agents/alfred/devops-expert.md +464 -0
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +38 -8
- moai_adk/templates/.claude/agents/alfred/format-expert.md +469 -0
- moai_adk/templates/.claude/agents/alfred/frontend-expert.md +357 -0
- moai_adk/templates/.claude/agents/alfred/git-manager.md +128 -9
- moai_adk/templates/.claude/agents/alfred/implementation-planner.md +104 -6
- moai_adk/templates/.claude/agents/alfred/project-manager.md +88 -16
- moai_adk/templates/.claude/agents/alfred/quality-gate.md +36 -9
- moai_adk/templates/.claude/agents/alfred/security-expert.md +270 -0
- moai_adk/templates/.claude/agents/alfred/skill-factory.md +865 -0
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +214 -43
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +111 -9
- moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +309 -160
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +36 -7
- moai_adk/templates/.claude/agents/alfred/ui-ux-expert.md +605 -0
- moai_adk/templates/.claude/commands/alfred/0-project.md +393 -966
- moai_adk/templates/.claude/commands/alfred/1-plan.md +651 -367
- moai_adk/templates/.claude/commands/alfred/2-run.md +388 -241
- moai_adk/templates/.claude/commands/alfred/3-sync.md +1921 -410
- moai_adk/templates/.claude/commands/alfred/9-feedback.md +153 -0
- moai_adk/templates/.claude/commands/alfred/release-new.md +3604 -0
- moai_adk/templates/.claude/hooks/alfred/core/project.py +484 -20
- moai_adk/templates/.claude/hooks/alfred/core/timeout.py +136 -0
- moai_adk/templates/.claude/hooks/alfred/core/ttl_cache.py +108 -0
- moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/handlers/__init__.py +14 -6
- moai_adk/templates/.claude/hooks/alfred/post_tool__enable_streaming_ui.py +50 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__tag_auto_corrector.py +407 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +99 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__realtime_tag_monitor.py +335 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__tag_policy_validator.py +325 -0
- moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +93 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__auto_cleanup.py +580 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +298 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/__init__.py +170 -0
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/checkpoint.py +3 -3
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/context.py +5 -5
- moai_adk/templates/.claude/hooks/alfred/shared/core/project.py +749 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/tags.py +230 -0
- moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/__init__.py +21 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/daily_analysis.py +351 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/notification.py +154 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/session.py +174 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/tool.py +87 -0
- moai_adk/templates/.claude/hooks/alfred/shared/handlers/user.py +61 -0
- moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +111 -0
- moai_adk/templates/.claude/hooks/alfred/utils/__init__.py +1 -0
- moai_adk/templates/.claude/hooks/alfred/utils/hook_config.py +94 -0
- moai_adk/templates/.claude/hooks/alfred/utils/timeout.py +161 -0
- moai_adk/templates/.claude/output-styles/alfred/alfred-moai-adk-beginner.md +267 -0
- moai_adk/templates/.claude/output-styles/alfred/keating-personal-tutor.md +440 -0
- moai_adk/templates/.claude/output-styles/alfred/r2d2-agentic-coding.md +583 -0
- moai_adk/templates/.claude/settings.json +96 -14
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/SKILL.md +70 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/examples.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-agent-guide/reference.md +242 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/SKILL.md +237 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/examples.md +871 -0
- moai_adk/templates/.claude/skills/moai-alfred-ask-user-questions/reference.md +653 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/README.md +162 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/SKILL.md +227 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/examples.md +354 -0
- moai_adk/templates/.claude/skills/moai-alfred-clone-pattern/reference.md +158 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/SKILL.md +179 -79
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/examples.md +117 -0
- moai_adk/templates/.claude/skills/moai-alfred-code-reviewer/scripts/pre-review-check.sh +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/SKILL.md +132 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-config-schema/reference.md +444 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/SKILL.md +62 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/examples.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-context-budget/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/SKILL.md +51 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/examples.md +355 -0
- moai_adk/templates/.claude/skills/moai-alfred-dev-guide/reference.md +239 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/SKILL.md +323 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/examples.md +286 -0
- moai_adk/templates/.claude/skills/moai-alfred-expertise-detection/reference.md +126 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/SKILL.md +229 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-issue-labels/reference.md +150 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/SKILL.md +87 -73
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-alfred-language-detection/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/README.md +42 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/SKILL.md +429 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/examples.md +520 -0
- moai_adk/templates/.claude/skills/moai-alfred-personas/reference.md +405 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/SKILL.md +89 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/examples.md +122 -0
- moai_adk/templates/.claude/skills/moai-alfred-practices/reference.md +369 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/SKILL.md +508 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/examples.md +481 -0
- moai_adk/templates/.claude/skills/moai-alfred-proactive-suggestions/reference.md +100 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/SKILL.md +77 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/examples.md +265 -0
- moai_adk/templates/.claude/skills/moai-alfred-rules/reference.md +539 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/SKILL.md +320 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-session-state/reference.md +84 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/README.md +137 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/SKILL.md +219 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples/validate-spec.sh +161 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/examples.md +541 -0
- moai_adk/templates/.claude/skills/moai-alfred-spec-authoring/reference.md +622 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/SKILL.md +19 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/examples.md +4 -0
- moai_adk/templates/.claude/skills/moai-alfred-todowrite-pattern/reference.md +211 -0
- moai_adk/templates/.claude/skills/moai-alfred-workflow/SKILL.md +288 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/SKILL.md +269 -0
- moai_adk/templates/.claude/skills/moai-cc-agents/templates/agent-template.md +32 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/SKILL.md +298 -0
- moai_adk/templates/.claude/skills/moai-cc-claude-md/templates/CLAUDE-template.md +26 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/SKILL.md +307 -0
- moai_adk/templates/.claude/skills/moai-cc-commands/templates/command-template.md +21 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/SKILL.md +252 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/pre-bash-check.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/preserve-permissions.sh +19 -0
- moai_adk/templates/.claude/skills/moai-cc-hooks/scripts/validate-bash-command.py +24 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/SKILL.md +199 -0
- moai_adk/templates/.claude/skills/moai-cc-mcp-plugins/templates/settings-mcp-template.json +39 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/SKILL.md +316 -0
- moai_adk/templates/.claude/skills/moai-cc-memory/templates/session-summary-template.md +18 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/SKILL.md +263 -0
- moai_adk/templates/.claude/skills/moai-cc-settings/templates/settings-complete-template.json +30 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/CHECKLIST.md +482 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/EXAMPLES.md +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/INTERACTIVE-DISCOVERY.md +524 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/METADATA.md +477 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PARALLEL-ANALYSIS-REPORT.md +429 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/PYTHON-VERSION-MATRIX.md +391 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-FACTORY-WORKFLOW.md +431 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL-UPDATE-ADVISOR.md +577 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/SKILL.md +273 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STEP-BY-STEP-GUIDE.md +466 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/STRUCTURE.md +583 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/WEB-RESEARCH.md +526 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/reference.md +608 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/generate-structure.sh +328 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/scripts/validate-skill.sh +312 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/SKILL_TEMPLATE.md +245 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/examples-template.md +285 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/reference-template.md +278 -0
- moai_adk/templates/.claude/skills/moai-cc-skill-factory/templates/scripts-template.sh +303 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/SKILL.md +291 -0
- moai_adk/templates/.claude/skills/moai-cc-skills/templates/SKILL-template.md +15 -0
- moai_adk/templates/.claude/skills/moai-change-logger/SKILL.md +563 -0
- moai_adk/templates/.claude/skills/moai-design-systems/SKILL.md +802 -0
- moai_adk/templates/.claude/skills/moai-design-systems/examples.md +1238 -0
- moai_adk/templates/.claude/skills/moai-design-systems/reference.md +673 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/SKILL.md +234 -43
- moai_adk/templates/.claude/skills/moai-domain-backend/examples.md +1633 -0
- moai_adk/templates/.claude/skills/moai-domain-backend/reference.md +660 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/SKILL.md +97 -69
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-cli-tool/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/SKILL.md +97 -72
- moai_adk/templates/.claude/skills/moai-domain-data-science/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-data-science/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-database/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-domain-database/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-database/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-domain-devops/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-devops/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/SKILL.md +102 -73
- moai_adk/templates/.claude/skills/moai-domain-frontend/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-frontend/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-domain-ml/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-ml/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/SKILL.md +97 -67
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-mobile-app/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-security/SKILL.md +97 -79
- moai_adk/templates/.claude/skills/moai-domain-security/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-security/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/SKILL.md +97 -71
- moai_adk/templates/.claude/skills/moai-domain-web-api/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-domain-web-api/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/SKILL.md +265 -64
- moai_adk/templates/.claude/skills/moai-essentials-debug/examples.md +1064 -0
- moai_adk/templates/.claude/skills/moai-essentials-debug/reference.md +1047 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/SKILL.md +87 -78
- moai_adk/templates/.claude/skills/moai-essentials-perf/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-perf/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/SKILL.md +87 -70
- moai_adk/templates/.claude/skills/moai-essentials-refactor/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-refactor/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/SKILL.md +87 -86
- moai_adk/templates/.claude/skills/moai-essentials-review/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-essentials-review/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +80 -62
- moai_adk/templates/.claude/skills/moai-foundation-ears/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-ears/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/SKILL.md +207 -50
- moai_adk/templates/.claude/skills/moai-foundation-git/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-git/reference.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/SKILL.md +90 -71
- moai_adk/templates/.claude/skills/moai-foundation-langs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-langs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/SKILL.md +78 -58
- moai_adk/templates/.claude/skills/moai-foundation-specs/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-specs/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/SKILL.md +78 -51
- moai_adk/templates/.claude/skills/moai-foundation-tags/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-foundation-tags/reference.md +28 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/.!11330!examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/SKILL.md +253 -32
- moai_adk/templates/.claude/skills/moai-foundation-trust/examples.md +0 -0
- moai_adk/templates/.claude/skills/moai-foundation-trust/reference.md +1099 -0
- moai_adk/templates/.claude/skills/moai-jit-docs-enhanced/SKILL.md +460 -0
- moai_adk/templates/.claude/skills/moai-lang-c/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-c/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-c/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/SKILL.md +98 -76
- moai_adk/templates/.claude/skills/moai-lang-cpp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-cpp/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/SKILL.md +2358 -70
- moai_adk/templates/.claude/skills/moai-lang-csharp/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-csharp/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/SKILL.md +2962 -68
- moai_adk/templates/.claude/skills/moai-lang-dart/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-dart/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-go/SKILL.md +1898 -70
- moai_adk/templates/.claude/skills/moai-lang-go/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-go/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-java/SKILL.md +1465 -68
- moai_adk/templates/.claude/skills/moai-lang-java/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-java/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/SKILL.md +2364 -66
- moai_adk/templates/.claude/skills/moai-lang-javascript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-javascript/reference.md +32 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/SKILL.md +1630 -69
- moai_adk/templates/.claude/skills/moai-lang-kotlin/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-kotlin/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-php/SKILL.md +89 -61
- moai_adk/templates/.claude/skills/moai-lang-php/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-php/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-python/SKILL.md +735 -66
- moai_adk/templates/.claude/skills/moai-lang-python/examples.md +624 -0
- moai_adk/templates/.claude/skills/moai-lang-python/reference.md +316 -0
- moai_adk/templates/.claude/skills/moai-lang-r/SKILL.md +97 -73
- moai_adk/templates/.claude/skills/moai-lang-r/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-r/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/SKILL.md +98 -73
- moai_adk/templates/.claude/skills/moai-lang-ruby/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-ruby/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/SKILL.md +1834 -70
- moai_adk/templates/.claude/skills/moai-lang-rust/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-rust/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/SKILL.md +99 -74
- moai_adk/templates/.claude/skills/moai-lang-scala/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-scala/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/SKILL.md +97 -74
- moai_adk/templates/.claude/skills/moai-lang-shell/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-shell/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/SKILL.md +98 -74
- moai_adk/templates/.claude/skills/moai-lang-sql/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-sql/reference.md +31 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/SKILL.md +1959 -69
- moai_adk/templates/.claude/skills/moai-lang-swift/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-swift/reference.md +30 -0
- moai_adk/templates/.claude/skills/moai-lang-template/SKILL.md +348 -0
- moai_adk/templates/.claude/skills/moai-lang-template/VARIABLES.md +98 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/SKILL.md +1230 -66
- moai_adk/templates/.claude/skills/moai-lang-typescript/examples.md +29 -0
- moai_adk/templates/.claude/skills/moai-lang-typescript/reference.md +34 -0
- moai_adk/templates/.claude/skills/moai-learning-optimizer/SKILL.md +575 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/README.md +50 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/SKILL.md +304 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/examples.md +417 -0
- moai_adk/templates/.claude/skills/moai-project-batch-questions/reference.md +704 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/README.md +87 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/examples.md +1109 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/reference.md +514 -0
- moai_adk/templates/.claude/skills/moai-project-config-manager/validate.py +106 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/README.md +11 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/SKILL.md +622 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/examples.md +20 -0
- moai_adk/templates/.claude/skills/moai-project-documentation/reference.md +12 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/README.md +152 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/SKILL.md +285 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/examples.md +333 -0
- moai_adk/templates/.claude/skills/moai-project-language-initializer/reference.md +386 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/README.md +49 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/SKILL.md +319 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/examples.md +58 -0
- moai_adk/templates/.claude/skills/moai-project-template-optimizer/reference.md +123 -0
- moai_adk/templates/.claude/skills/moai-session-info/SKILL.md +314 -0
- moai_adk/templates/.claude/skills/moai-streaming-ui/SKILL.md +552 -0
- moai_adk/templates/.claude/skills/moai-tag-policy-validator/SKILL.md +570 -0
- moai_adk/templates/.git-hooks/pre-commit +66 -0
- moai_adk/templates/.git-hooks/pre-push +255 -0
- moai_adk/templates/.github/workflows/c-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/cpp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/csharp-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/dart-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/go-tag-validation.yml +130 -0
- moai_adk/templates/.github/workflows/java-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/javascript-tag-validation.yml +135 -0
- moai_adk/templates/.github/workflows/kotlin-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/moai-gitflow.yml +166 -3
- moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
- moai_adk/templates/.github/workflows/moai-release-pipeline.yml +188 -0
- moai_adk/templates/.github/workflows/php-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/python-tag-validation.yml +118 -0
- moai_adk/templates/.github/workflows/release.yml +118 -0
- moai_adk/templates/.github/workflows/ruby-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/rust-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/shell-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/spec-issue-sync.yml +338 -0
- moai_adk/templates/.github/workflows/swift-tag-validation.yml +11 -0
- moai_adk/templates/.github/workflows/tag-report.yml +269 -0
- moai_adk/templates/.github/workflows/tag-validation.yml +186 -0
- moai_adk/templates/.github/workflows/typescript-tag-validation.yml +154 -0
- moai_adk/templates/.mcp.json +31 -0
- moai_adk/templates/.moai/config.json +80 -7
- moai_adk/templates/CLAUDE.md +562 -546
- moai_adk/utils/banner.py +5 -5
- moai_adk/utils/common.py +294 -0
- moai_adk/utils/link_validator.py +235 -0
- moai_adk/utils/logger.py +8 -8
- moai_adk/utils/user_experience.py +451 -0
- moai_adk-0.20.1.dist-info/METADATA +233 -0
- moai_adk-0.20.1.dist-info/RECORD +404 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +0 -230
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +0 -156
- moai_adk/templates/.claude/hooks/alfred/core/__init__.py +0 -85
- moai_adk/templates/.claude/hooks/alfred/handlers/notification.py +0 -25
- moai_adk/templates/.claude/hooks/alfred/handlers/session.py +0 -92
- moai_adk/templates/.claude/hooks/alfred/handlers/tool.py +0 -70
- moai_adk/templates/.claude/hooks/alfred/handlers/user.py +0 -41
- moai_adk/templates/.claude/output-styles/alfred/agentic-coding.md +0 -636
- moai_adk/templates/.claude/output-styles/alfred/moai-adk-learning.md +0 -692
- moai_adk/templates/.claude/output-styles/alfred/study-with-alfred.md +0 -470
- moai_adk/templates/.claude/skills/moai-alfred-debugger-pro/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-ears-authoring/SKILL.md +0 -103
- moai_adk/templates/.claude/skills/moai-alfred-git-workflow/SKILL.md +0 -95
- moai_adk/templates/.claude/skills/moai-alfred-performance-optimizer/SKILL.md +0 -105
- moai_adk/templates/.claude/skills/moai-alfred-refactoring-coach/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-spec-metadata-validation/SKILL.md +0 -97
- moai_adk/templates/.claude/skills/moai-alfred-tag-scanning/SKILL.md +0 -90
- moai_adk/templates/.claude/skills/moai-alfred-trust-validation/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/SKILL.md +0 -87
- moai_adk/templates/.claude/skills/moai-alfred-tui-survey/examples.md +0 -62
- moai_adk/templates/.claude/skills/moai-claude-code/SKILL.md +0 -94
- moai_adk/templates/.claude/skills/moai-claude-code/examples.md +0 -513
- moai_adk/templates/.claude/skills/moai-claude-code/reference.md +0 -433
- moai_adk/templates/.claude/skills/moai-claude-code/templates/agent-full.md +0 -332
- moai_adk/templates/.claude/skills/moai-claude-code/templates/command-full.md +0 -384
- moai_adk/templates/.claude/skills/moai-claude-code/templates/plugin-full.json +0 -363
- moai_adk/templates/.claude/skills/moai-claude-code/templates/settings-full.json +0 -595
- moai_adk/templates/.claude/skills/moai-claude-code/templates/skill-full.md +0 -496
- moai_adk/templates/.claude/skills/moai-lang-clojure/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-elixir/SKILL.md +0 -99
- moai_adk/templates/.claude/skills/moai-lang-haskell/SKILL.md +0 -100
- moai_adk/templates/.claude/skills/moai-lang-julia/SKILL.md +0 -98
- moai_adk/templates/.claude/skills/moai-lang-lua/SKILL.md +0 -98
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +0 -69
- moai_adk/templates/.moai/memory/development-guide.md +0 -344
- moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -220
- moai_adk/templates/.moai/memory/spec-metadata.md +0 -356
- moai_adk/templates/.moai/project/product.md +0 -161
- moai_adk/templates/.moai/project/structure.md +0 -156
- moai_adk/templates/.moai/project/tech.md +0 -227
- moai_adk/templates/__init__.py +0 -2
- moai_adk-0.4.5.dist-info/METADATA +0 -369
- moai_adk-0.4.5.dist-info/RECORD +0 -152
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/WHEEL +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.4.5.dist-info → moai_adk-0.20.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,99 +1,1989 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
2
|
name: moai-lang-swift
|
|
4
|
-
|
|
3
|
+
version: 2.0.0
|
|
4
|
+
created: 2025-11-06
|
|
5
|
+
updated: 2025-11-06
|
|
6
|
+
status: active
|
|
7
|
+
description: "Swift best practices with SwiftUI, iOS development, Swift Concurrency, and server-side Swift for 2025"
|
|
8
|
+
keywords: [swift, programming, ios, swiftui, server-side, concurrency, vapor, mobile]
|
|
5
9
|
allowed-tools:
|
|
6
10
|
- Read
|
|
11
|
+
- Write
|
|
12
|
+
- Edit
|
|
7
13
|
- Bash
|
|
14
|
+
- WebFetch
|
|
15
|
+
- WebSearch
|
|
8
16
|
---
|
|
9
17
|
|
|
10
|
-
# Swift
|
|
18
|
+
# Swift Development Mastery
|
|
19
|
+
|
|
20
|
+
**Modern Swift Development with 2025 Best Practices**
|
|
21
|
+
|
|
22
|
+
> Comprehensive Swift development guidance covering iOS/macOS applications with SwiftUI, Swift Concurrency, server-side development with Vapor, and cross-platform Swift applications using the latest tools and frameworks.
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
|
|
26
|
+
### iOS/macOS Development
|
|
27
|
+
- **Mobile App Development**: SwiftUI with modern declarative UI patterns, MVVM architecture
|
|
28
|
+
- **Platform Integration**: Core Data, Core Location, Camera, Push Notifications, Background Tasks
|
|
29
|
+
- **Performance Optimization**: Memory management, battery optimization, SwiftUI performance
|
|
30
|
+
- **Testing**: Unit tests, UI tests, performance tests with XCTest framework
|
|
11
31
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
| Trigger cues | Swift code discussions, framework guidance, or file extensions such as .swift. |
|
|
18
|
-
| Tier | 3 |
|
|
32
|
+
### Server-Side Development
|
|
33
|
+
- **API Development**: Vapor 4, Hummingbird, or Perfect for backend services
|
|
34
|
+
- **Database Integration**: Fluent ORM, PostgreSQL, MongoDB with async/await
|
|
35
|
+
- **Real-time Communication**: WebSockets, Server-Sent Events with Swift Concurrency
|
|
36
|
+
- **Testing**: XCTVapor, Mocking libraries, integration testing
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
### Cross-Platform Development
|
|
39
|
+
- **SwiftUI for Multiple Platforms**: iOS, iPadOS, macOS, watchOS, visionOS
|
|
40
|
+
- **Shared Codebases**: Swift Package Manager, custom frameworks
|
|
41
|
+
- **Platform-Specific Optimizations**: Conditional compilation, platform APIs
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
## When to Use
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
### Perfect Scenarios
|
|
46
|
+
- **Building iOS and iPadOS applications with SwiftUI**
|
|
47
|
+
- **Developing macOS applications with modern Swift patterns**
|
|
48
|
+
- **Creating server-side APIs with Swift**
|
|
49
|
+
- **Implementing cross-platform Swift applications**
|
|
50
|
+
- **Building real-time applications with Swift Concurrency**
|
|
51
|
+
- **Developing watchOS and visionOS applications**
|
|
52
|
+
- **Creating Swift frameworks and libraries**
|
|
25
53
|
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- Swift
|
|
54
|
+
### Common Triggers
|
|
55
|
+
- "Create iOS app with Swift"
|
|
56
|
+
- "Build SwiftUI application"
|
|
57
|
+
- "Develop Swift backend API"
|
|
58
|
+
- "Implement Swift Concurrency"
|
|
59
|
+
- "Optimize Swift performance"
|
|
60
|
+
- "Test Swift application"
|
|
61
|
+
- "Swift best practices"
|
|
30
62
|
|
|
31
|
-
##
|
|
63
|
+
## Tool Version Matrix (2025-11-06)
|
|
32
64
|
|
|
33
|
-
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
-
|
|
65
|
+
### Core Swift
|
|
66
|
+
- **Swift**: 6.0 (current) / 5.10 (LTS)
|
|
67
|
+
- **Xcode**: 16.1 (current) / 15.4 (LTS)
|
|
68
|
+
- **Swift Package Manager**: Built-in with Swift 6.0
|
|
69
|
+
- **Platforms**: iOS 18+, iPadOS 18+, macOS 15+, watchOS 11+, visionOS 2+
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
- **
|
|
41
|
-
- **
|
|
42
|
-
- **
|
|
71
|
+
### UI Frameworks
|
|
72
|
+
- **SwiftUI**: iOS 18.0, macOS 15.0
|
|
73
|
+
- **UIKit**: iOS 17.0+ (legacy support)
|
|
74
|
+
- **AppKit**: macOS 14.0+ (legacy support)
|
|
75
|
+
- **Combine**: iOS 13.0+, macOS 10.15+
|
|
43
76
|
|
|
44
|
-
|
|
45
|
-
- **
|
|
46
|
-
- **
|
|
47
|
-
- **
|
|
77
|
+
### Server-Side Frameworks
|
|
78
|
+
- **Vapor**: 4.93.x - Web framework
|
|
79
|
+
- **Hummingbird**: 2.2.x - Lightweight web framework
|
|
80
|
+
- **Fluent**: 4.8.x - ORM for Vapor
|
|
81
|
+
- **PostgresNIO**: 2.12.x - PostgreSQL driver
|
|
82
|
+
- **MongoKitten**: 7.0.x - MongoDB driver
|
|
48
83
|
|
|
49
|
-
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
53
|
-
- **
|
|
54
|
-
- **Closures**: First-class functions
|
|
84
|
+
### Testing Tools
|
|
85
|
+
- **XCTest**: Built-in testing framework
|
|
86
|
+
- **XCUITest**: UI testing framework
|
|
87
|
+
- **Quick/Nimble**: BDD-style testing
|
|
88
|
+
- **XCTVapor**: Vapor testing utilities
|
|
55
89
|
|
|
56
|
-
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
|
|
90
|
+
### Development Tools
|
|
91
|
+
- **SwiftLint**: 0.54.x - Code style enforcement
|
|
92
|
+
- **SwiftFormat**: 0.53.x - Code formatting
|
|
93
|
+
- **Periphery**: 2.10.x - Unused code detection
|
|
94
|
+
|
|
95
|
+
## Ecosystem Overview
|
|
96
|
+
|
|
97
|
+
### Package Management
|
|
61
98
|
|
|
62
|
-
## Examples
|
|
63
99
|
```bash
|
|
64
|
-
|
|
100
|
+
# Swift Package Manager initialization
|
|
101
|
+
swift package init --type executable
|
|
102
|
+
swift package init --type library
|
|
103
|
+
|
|
104
|
+
# Adding dependencies
|
|
105
|
+
swift package add dependency https://github.com/vapor/vapor.git --from 4.93.0
|
|
106
|
+
swift package add dependency https://github.com/Alamofire/Alamofire.git --from 5.9.0
|
|
107
|
+
|
|
108
|
+
# Building and testing
|
|
109
|
+
swift build
|
|
110
|
+
swift test
|
|
111
|
+
swift run
|
|
112
|
+
|
|
113
|
+
# Xcode project generation
|
|
114
|
+
swift package generate-xcodeproj
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Project Structure (2025 Best Practice)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
SwiftProject/
|
|
121
|
+
├── Package.swift # SPM configuration
|
|
122
|
+
├── Sources/
|
|
123
|
+
│ ├── App/ # Main application
|
|
124
|
+
│ │ ├── App.swift # Entry point
|
|
125
|
+
│ │ ├── ContentView.swift # Main view
|
|
126
|
+
│ │ └── AppState.swift # App state management
|
|
127
|
+
│ ├── Features/ # Feature modules
|
|
128
|
+
│ │ ├── Authentication/
|
|
129
|
+
│ │ │ ├── Models/
|
|
130
|
+
│ │ │ ├── Views/
|
|
131
|
+
│ │ │ ├── ViewModels/
|
|
132
|
+
│ │ │ └── Services/
|
|
133
|
+
│ │ └── UserProfile/
|
|
134
|
+
│ ├── Core/ # Core utilities
|
|
135
|
+
│ │ ├── Extensions/
|
|
136
|
+
│ │ ├── Protocols/
|
|
137
|
+
│ │ └── Utilities/
|
|
138
|
+
│ └── Shared/ # Shared code
|
|
139
|
+
│ ├── Models/
|
|
140
|
+
│ ├── Networking/
|
|
141
|
+
│ └── Database/
|
|
142
|
+
├── Tests/
|
|
143
|
+
│ ├── AppTests/
|
|
144
|
+
│ ├── FeaturesTests/
|
|
145
|
+
│ └── IntegrationTests/
|
|
146
|
+
├── Resources/ # Resources
|
|
147
|
+
│ ├── Assets.xcassets
|
|
148
|
+
│ ├── Localizable.strings
|
|
149
|
+
│ └── Configuration.plist
|
|
150
|
+
├── .swiftlint.yml # SwiftLint configuration
|
|
151
|
+
├── .swiftformat # SwiftFormat configuration
|
|
152
|
+
└── Package.resolved # Resolved dependencies
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Modern Development Patterns
|
|
156
|
+
|
|
157
|
+
### Swift 6.0 Concurrency Patterns
|
|
158
|
+
|
|
159
|
+
```swift
|
|
160
|
+
import Foundation
|
|
161
|
+
import SwiftConcurrency
|
|
162
|
+
|
|
163
|
+
// Async/await with structured concurrency
|
|
164
|
+
actor UserManager {
|
|
165
|
+
private var users: [String: User] = [:]
|
|
166
|
+
|
|
167
|
+
func addUser(_ user: User) async throws {
|
|
168
|
+
// Actor ensures thread-safe access
|
|
169
|
+
users[user.id] = user
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func getUser(id: String) async -> User? {
|
|
173
|
+
return users[id]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func updateProfile(id: String, profile: UserProfile) async throws -> User {
|
|
177
|
+
guard var user = users[id] else {
|
|
178
|
+
throw UserError.notFound
|
|
179
|
+
}
|
|
180
|
+
user.profile = profile
|
|
181
|
+
users[id] = user
|
|
182
|
+
return user
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Async sequence for real-time updates
|
|
186
|
+
func userUpdates() -> AsyncStream<UserUpdate> {
|
|
187
|
+
AsyncStream { continuation in
|
|
188
|
+
let task = Task {
|
|
189
|
+
for await update in userUpdateChannel {
|
|
190
|
+
continuation.yield(update)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
continuation.onTermination = { _ in
|
|
195
|
+
task.cancel()
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// TaskGroup for concurrent operations
|
|
202
|
+
class DataProcessor {
|
|
203
|
+
func processItems(_ items: [DataItem]) async throws -> [ProcessedItem] {
|
|
204
|
+
return try await withThrowingTaskGroup(of: ProcessedItem.self) { group in
|
|
205
|
+
for item in items {
|
|
206
|
+
group.addTask {
|
|
207
|
+
return await self.processSingleItem(item)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
var results: [ProcessedItem] = []
|
|
212
|
+
for try await result in group {
|
|
213
|
+
results.append(result)
|
|
214
|
+
}
|
|
215
|
+
return results
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private func processSingleItem(_ item: DataItem) async -> ProcessedItem {
|
|
220
|
+
// Processing logic
|
|
221
|
+
return ProcessedItem(from: item)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// AsyncImage with SwiftUI
|
|
226
|
+
struct UserAvatarView: View {
|
|
227
|
+
let user: User
|
|
228
|
+
|
|
229
|
+
var body: some View {
|
|
230
|
+
AsyncImage(url: user.avatarURL) { image in
|
|
231
|
+
image
|
|
232
|
+
.resizable()
|
|
233
|
+
.aspectRatio(contentMode: .fill)
|
|
234
|
+
} placeholder: {
|
|
235
|
+
ProgressView()
|
|
236
|
+
.frame(width: 100, height: 100)
|
|
237
|
+
}
|
|
238
|
+
.frame(width: 100, height: 100)
|
|
239
|
+
.clipShape(Circle())
|
|
240
|
+
.overlay(
|
|
241
|
+
Circle()
|
|
242
|
+
.stroke(Color.blue, lineWidth: 2)
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// AsyncStream for real-time data
|
|
248
|
+
class RealtimeDataService {
|
|
249
|
+
private let continuation: AsyncStream<DataUpdate>.Continuation
|
|
250
|
+
|
|
251
|
+
init() {
|
|
252
|
+
let (stream, continuation) = AsyncStream<DataUpdate>.makeStream()
|
|
253
|
+
self.continuation = continuation
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
var updates: AsyncStream<DataUpdate> {
|
|
257
|
+
AsyncStream { continuation in
|
|
258
|
+
let task = Task {
|
|
259
|
+
for await update in dataChannel {
|
|
260
|
+
continuation.yield(update)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
continuation.onTermination = { _ in
|
|
265
|
+
task.cancel()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func sendUpdate(_ update: DataUpdate) {
|
|
271
|
+
continuation.yield(update)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Modern SwiftUI Patterns
|
|
277
|
+
|
|
278
|
+
```swift
|
|
279
|
+
import SwiftUI
|
|
280
|
+
import Combine
|
|
281
|
+
|
|
282
|
+
// MVVM with Observable macro
|
|
283
|
+
@Observable
|
|
284
|
+
class UserProfileViewModel {
|
|
285
|
+
var user: User?
|
|
286
|
+
var isLoading = false
|
|
287
|
+
var errorMessage: String?
|
|
288
|
+
|
|
289
|
+
private let userService: UserService
|
|
290
|
+
private var cancellables = Set<AnyCancellable>()
|
|
291
|
+
|
|
292
|
+
init(userService: UserService) {
|
|
293
|
+
self.userService = userService
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func loadUser(id: String) async {
|
|
297
|
+
isLoading = true
|
|
298
|
+
errorMessage = nil
|
|
299
|
+
|
|
300
|
+
defer { isLoading = false }
|
|
301
|
+
|
|
302
|
+
do {
|
|
303
|
+
user = try await userService.getUser(id: id)
|
|
304
|
+
} catch {
|
|
305
|
+
errorMessage = error.localizedDescription
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func updateUserProfile(_ profile: UserProfile) async {
|
|
310
|
+
guard let user = user else { return }
|
|
311
|
+
|
|
312
|
+
isLoading = true
|
|
313
|
+
errorMessage = nil
|
|
314
|
+
|
|
315
|
+
defer { isLoading = false }
|
|
316
|
+
|
|
317
|
+
do {
|
|
318
|
+
let updatedUser = try await userService.updateUser(id: user.id, profile: profile)
|
|
319
|
+
self.user = updatedUser
|
|
320
|
+
} catch {
|
|
321
|
+
errorMessage = error.localizedDescription
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Modern SwiftUI view with navigation
|
|
327
|
+
struct UserProfileView: View {
|
|
328
|
+
@State private var viewModel: UserProfileViewModel
|
|
329
|
+
@State private var showingEditSheet = false
|
|
330
|
+
|
|
331
|
+
init(userId: String, userService: UserService = .shared) {
|
|
332
|
+
_viewModel = State(initialValue: UserProfileViewModel(userService: userService))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
var body: some View {
|
|
336
|
+
NavigationStack {
|
|
337
|
+
Group {
|
|
338
|
+
if let user = viewModel.user {
|
|
339
|
+
UserProfileContent(user: user, viewModel: viewModel)
|
|
340
|
+
} else if viewModel.isLoading {
|
|
341
|
+
LoadingView()
|
|
342
|
+
} else if let errorMessage = viewModel.errorMessage {
|
|
343
|
+
ErrorView(message: errorMessage) {
|
|
344
|
+
Task {
|
|
345
|
+
await viewModel.loadUser(id: userId)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
EmptyStateView()
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
.navigationTitle("Profile")
|
|
353
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
354
|
+
.toolbar {
|
|
355
|
+
ToolbarItem(placement: .navigationBarTrailing) {
|
|
356
|
+
Button("Edit") {
|
|
357
|
+
showingEditSheet = true
|
|
358
|
+
}
|
|
359
|
+
.disabled(viewModel.user == nil || viewModel.isLoading)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
.sheet(isPresented: $showingEditSheet) {
|
|
363
|
+
if let user = viewModel.user {
|
|
364
|
+
EditProfileView(user: user) { profile in
|
|
365
|
+
Task {
|
|
366
|
+
await viewModel.updateUserProfile(profile)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
.task {
|
|
372
|
+
if viewModel.user == nil {
|
|
373
|
+
await viewModel.loadUser(id: userId)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Reusable view components
|
|
381
|
+
struct UserProfileContent: View {
|
|
382
|
+
let user: User
|
|
383
|
+
let viewModel: UserProfileViewModel
|
|
384
|
+
|
|
385
|
+
var body: some View {
|
|
386
|
+
ScrollView {
|
|
387
|
+
VStack(spacing: 24) {
|
|
388
|
+
// Avatar section
|
|
389
|
+
UserAvatarSection(user: user)
|
|
390
|
+
|
|
391
|
+
// Profile information
|
|
392
|
+
ProfileInfoSection(user: user)
|
|
393
|
+
|
|
394
|
+
// Statistics
|
|
395
|
+
UserStatsSection(user: user)
|
|
396
|
+
|
|
397
|
+
// Actions
|
|
398
|
+
UserActionsSection(user: user, viewModel: viewModel)
|
|
399
|
+
}
|
|
400
|
+
.padding()
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
struct UserAvatarSection: View {
|
|
406
|
+
let user: User
|
|
407
|
+
|
|
408
|
+
var body: some View {
|
|
409
|
+
VStack(spacing: 16) {
|
|
410
|
+
UserAvatarView(user: user)
|
|
411
|
+
.frame(width: 120, height: 120)
|
|
412
|
+
|
|
413
|
+
Text(user.name)
|
|
414
|
+
.font(.title2)
|
|
415
|
+
.fontWeight(.semibold)
|
|
416
|
+
|
|
417
|
+
Text("@\(user.username)")
|
|
418
|
+
.font(.body)
|
|
419
|
+
.foregroundColor(.secondary)
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Custom modifiers
|
|
425
|
+
struct ProfileCardStyle: ViewModifier {
|
|
426
|
+
func body(content: Content) -> some View {
|
|
427
|
+
content
|
|
428
|
+
.padding()
|
|
429
|
+
.background(Color(.systemBackground))
|
|
430
|
+
.cornerRadius(12)
|
|
431
|
+
.shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
extension View {
|
|
436
|
+
func profileCardStyle() -> some View {
|
|
437
|
+
modifier(ProfileCardStyle())
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// State management with Environment
|
|
442
|
+
@main
|
|
443
|
+
struct MyApp: App {
|
|
444
|
+
@State private var appState = AppState()
|
|
445
|
+
|
|
446
|
+
var body: some Scene {
|
|
447
|
+
WindowGroup {
|
|
448
|
+
ContentView()
|
|
449
|
+
.environment(appState)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Observable app state
|
|
455
|
+
@Observable
|
|
456
|
+
class AppState {
|
|
457
|
+
var currentUser: User?
|
|
458
|
+
var isAuthenticated = false
|
|
459
|
+
var theme: Theme = .system
|
|
460
|
+
|
|
461
|
+
func login(user: User) {
|
|
462
|
+
currentUser = user
|
|
463
|
+
isAuthenticated = true
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
func logout() {
|
|
467
|
+
currentUser = nil
|
|
468
|
+
isAuthenticated = false
|
|
469
|
+
}
|
|
470
|
+
}
|
|
65
471
|
```
|
|
66
472
|
|
|
67
|
-
|
|
68
|
-
- Language-specific source directories (e.g. `src/`, `app/`).
|
|
69
|
-
- Language-specific build/test configuration files (e.g. `package.json`, `pyproject.toml`, `go.mod`).
|
|
70
|
-
- Relevant test suites and sample data.
|
|
473
|
+
### Server-Side Swift with Vapor
|
|
71
474
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
475
|
+
```swift
|
|
476
|
+
import Vapor
|
|
477
|
+
import Fluent
|
|
478
|
+
import PostgresNIO
|
|
75
479
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
480
|
+
// User model with Fluent
|
|
481
|
+
final class User: Model, Content {
|
|
482
|
+
static let schema = "users"
|
|
483
|
+
|
|
484
|
+
@ID(key: .id)
|
|
485
|
+
var id: UUID?
|
|
486
|
+
|
|
487
|
+
@Field(key: "username")
|
|
488
|
+
var username: String
|
|
489
|
+
|
|
490
|
+
@Field(key: "email")
|
|
491
|
+
var email: String
|
|
492
|
+
|
|
493
|
+
@Field(key: "password_hash")
|
|
494
|
+
var passwordHash: String
|
|
495
|
+
|
|
496
|
+
@Timestamp(key: "created_at", on: .create)
|
|
497
|
+
var createdAt: Date?
|
|
498
|
+
|
|
499
|
+
@Timestamp(key: "updated_at", on: .update)
|
|
500
|
+
var updatedAt: Date?
|
|
501
|
+
|
|
502
|
+
@Children(for: \.$user)
|
|
503
|
+
var posts: [Post]
|
|
504
|
+
|
|
505
|
+
init() { }
|
|
506
|
+
|
|
507
|
+
init(id: UUID? = nil, username: String, email: String, passwordHash: String) {
|
|
508
|
+
self.id = id
|
|
509
|
+
self.username = username
|
|
510
|
+
self.email = email
|
|
511
|
+
self.passwordHash = passwordHash
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Public representation
|
|
515
|
+
struct Public: Content {
|
|
516
|
+
var id: UUID
|
|
517
|
+
var username: String
|
|
518
|
+
var email: String
|
|
519
|
+
var createdAt: Date?
|
|
520
|
+
var updatedAt: Date?
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
func convertToPublic() -> Public {
|
|
524
|
+
return Public(
|
|
525
|
+
id: id!,
|
|
526
|
+
username: username,
|
|
527
|
+
email: email,
|
|
528
|
+
createdAt: createdAt,
|
|
529
|
+
updatedAt: updatedAt
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Repository pattern with async/await
|
|
535
|
+
struct UserRepository {
|
|
536
|
+
let database: Database
|
|
537
|
+
|
|
538
|
+
func create(_ user: User) async throws -> User {
|
|
539
|
+
try await user.save(on: database)
|
|
540
|
+
return user
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
func findByID(_ id: UUID) async throws -> User? {
|
|
544
|
+
try await User.query(on: database)
|
|
545
|
+
.filter(\.$id == id)
|
|
546
|
+
.first()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
func findByUsername(_ username: String) async throws -> User? {
|
|
550
|
+
try await User.query(on: database)
|
|
551
|
+
.filter(\.$username == username)
|
|
552
|
+
.first()
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
func findAll(page: Int, per: Int = 10) async throws -> [User] {
|
|
556
|
+
try await User.query(on: database)
|
|
557
|
+
.range(page * per ..< (page + 1) * per)
|
|
558
|
+
.all()
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Controllers with async routes
|
|
563
|
+
struct UserController: RouteCollection {
|
|
564
|
+
func boot(routes: RoutesBuilder) throws {
|
|
565
|
+
let users = routes.grouped("api/users")
|
|
566
|
+
|
|
567
|
+
users.get(use: index)
|
|
568
|
+
users.post(use: create)
|
|
569
|
+
users.get(":userID", use: show)
|
|
570
|
+
users.put(":userID", use: update)
|
|
571
|
+
users.delete(":userID", use: delete)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
@Sendable
|
|
575
|
+
func index(req: Request) async throws -> [User.Public] {
|
|
576
|
+
let users = try await User.query(on: req.db).all()
|
|
577
|
+
return users.map { $0.convertToPublic() }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@Sendable
|
|
581
|
+
func create(req: Request) async throws -> User.Public {
|
|
582
|
+
let createUserData = try req.content.decode(CreateUser.self)
|
|
583
|
+
|
|
584
|
+
guard let passwordHash = try? await req.password.hash(createUserData.password) else {
|
|
585
|
+
throw Abort(.badRequest, reason: "Failed to hash password")
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
let user = User(
|
|
589
|
+
username: createUserData.username,
|
|
590
|
+
email: createUserData.email,
|
|
591
|
+
passwordHash: passwordHash
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
try await user.save(on: req.db)
|
|
595
|
+
return user.convertToPublic()
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
@Sendable
|
|
599
|
+
func show(req: Request) async throws -> User.Public {
|
|
600
|
+
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
|
|
601
|
+
throw Abort(.notFound)
|
|
602
|
+
}
|
|
603
|
+
return user.convertToPublic()
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
@Sendable
|
|
607
|
+
func update(req: Request) async throws -> User.Public {
|
|
608
|
+
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
|
|
609
|
+
throw Abort(.notFound)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
let updateData = try req.content.decode(UpdateUser.self)
|
|
613
|
+
user.username = updateData.username ?? user.username
|
|
614
|
+
user.email = updateData.email ?? user.email
|
|
615
|
+
|
|
616
|
+
try await user.save(on: req.db)
|
|
617
|
+
return user.convertToPublic()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
@Sendable
|
|
621
|
+
func delete(req: Request) async throws -> HTTPStatus {
|
|
622
|
+
guard let user = try await User.find(req.parameters.get("userID"), on: req.db) else {
|
|
623
|
+
throw Abort(.notFound)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
try await user.delete(on: req.db)
|
|
627
|
+
return .noContent
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// DTOs for request/response
|
|
632
|
+
struct CreateUser: Content {
|
|
633
|
+
var username: String
|
|
634
|
+
var email: String
|
|
635
|
+
var password: String
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
struct UpdateUser: Content {
|
|
639
|
+
var username: String?
|
|
640
|
+
var email: String?
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// WebSocket support
|
|
644
|
+
struct WebSocketController: RouteCollection {
|
|
645
|
+
func boot(routes: RoutesBuilder) throws {
|
|
646
|
+
let websockets = routes.grouped("ws")
|
|
647
|
+
websockets.webSocket("chat", onUpgrade: handleChat)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
func handleChat(req: Request, ws: WebSocket) async {
|
|
651
|
+
ws.onText { ws, text in
|
|
652
|
+
// Handle incoming message
|
|
653
|
+
do {
|
|
654
|
+
let message = try JSONDecoder().decode(ChatMessage.self, from: text.data(using: .utf8)!)
|
|
655
|
+
|
|
656
|
+
// Broadcast to all connected clients
|
|
657
|
+
await broadcastMessage(message)
|
|
658
|
+
} catch {
|
|
659
|
+
ws.close(code: .invalidData)
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
ws.onBinary { ws, data in
|
|
664
|
+
// Handle binary data
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
ws.onClose.whenComplete { _ in
|
|
668
|
+
// Handle connection close
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private func broadcastMessage(_ message: ChatMessage) async {
|
|
673
|
+
// Implementation for broadcasting to all clients
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Swift Package Manager Configuration
|
|
679
|
+
|
|
680
|
+
```swift
|
|
681
|
+
// Package.swift
|
|
682
|
+
// swift-tools-version: 6.0
|
|
683
|
+
|
|
684
|
+
import PackageDescription
|
|
685
|
+
|
|
686
|
+
let package = Package(
|
|
687
|
+
name: "MySwiftApp",
|
|
688
|
+
platforms: [
|
|
689
|
+
.iOS(.v18),
|
|
690
|
+
.macOS(.v15),
|
|
691
|
+
.watchOS(.v11),
|
|
692
|
+
.visionOS(.v2)
|
|
693
|
+
],
|
|
694
|
+
products: [
|
|
695
|
+
.library(name: "MySwiftApp", targets: ["MySwiftApp"]),
|
|
696
|
+
.executable(name: "MyServer", targets: ["MyServer"])
|
|
697
|
+
],
|
|
698
|
+
dependencies: [
|
|
699
|
+
// Networking
|
|
700
|
+
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0"),
|
|
701
|
+
.package(url: "https://github.com/vapor/vapor.git", from: "4.93.0"),
|
|
702
|
+
|
|
703
|
+
// Utilities
|
|
704
|
+
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
|
|
705
|
+
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.0"),
|
|
706
|
+
|
|
707
|
+
// Testing
|
|
708
|
+
.package(url: "https://github.com/Quick/Quick.git", from: "7.3.0"),
|
|
709
|
+
.package(url: "https://github.com/Quick/Nimble.git", from: "13.3.0")
|
|
710
|
+
],
|
|
711
|
+
targets: [
|
|
712
|
+
.target(
|
|
713
|
+
name: "MySwiftApp",
|
|
714
|
+
dependencies: [
|
|
715
|
+
.product(name: "Alamofire", package: "Alamofire"),
|
|
716
|
+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
|
|
717
|
+
.product(name: "Logging", package: "swift-log")
|
|
718
|
+
],
|
|
719
|
+
path: "Sources/MySwiftApp"
|
|
720
|
+
),
|
|
721
|
+
.target(
|
|
722
|
+
name: "MyServer",
|
|
723
|
+
dependencies: [
|
|
724
|
+
.product(name: "Vapor", package: "vapor"),
|
|
725
|
+
.product(name: "Fluent", package: "vapor"),
|
|
726
|
+
.product(name: "FluentPostgresDriver", package: "vapor")
|
|
727
|
+
],
|
|
728
|
+
path: "Sources/MyServer"
|
|
729
|
+
),
|
|
730
|
+
.testTarget(
|
|
731
|
+
name: "MySwiftAppTests",
|
|
732
|
+
dependencies: [
|
|
733
|
+
"MySwiftApp",
|
|
734
|
+
"Quick",
|
|
735
|
+
"Nimble"
|
|
736
|
+
],
|
|
737
|
+
path: "Tests/MySwiftAppTests"
|
|
738
|
+
),
|
|
739
|
+
.testTarget(
|
|
740
|
+
name: "MyServerTests",
|
|
741
|
+
dependencies: [
|
|
742
|
+
"MyServer",
|
|
743
|
+
.product(name: "XCTVapor", package: "vapor")
|
|
744
|
+
],
|
|
745
|
+
path: "Tests/MyServerTests"
|
|
746
|
+
)
|
|
747
|
+
]
|
|
748
|
+
)
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
## Performance Considerations
|
|
752
|
+
|
|
753
|
+
### Memory Management
|
|
754
|
+
|
|
755
|
+
```swift
|
|
756
|
+
// Efficient memory usage with weak references
|
|
757
|
+
class ImageCache {
|
|
758
|
+
private var cache: [String: UIImage] = [:]
|
|
759
|
+
private let queue = DispatchQueue(label: "com.app.imagecache", attributes: .concurrent)
|
|
760
|
+
|
|
761
|
+
func setImage(_ image: UIImage, for key: String) {
|
|
762
|
+
queue.async(flags: .barrier) {
|
|
763
|
+
self.cache[key] = image
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
func getImage(for key: String) -> UIImage? {
|
|
768
|
+
return queue.sync {
|
|
769
|
+
return cache[key]
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
func clearCache() {
|
|
774
|
+
queue.async(flags: .barrier) {
|
|
775
|
+
self.cache.removeAll()
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Lazy loading and memory optimization
|
|
781
|
+
class DataViewController: UIViewController {
|
|
782
|
+
private lazy var dataLoader: DataLoader = {
|
|
783
|
+
return DataLoader(configuration: .default)
|
|
784
|
+
}()
|
|
785
|
+
|
|
786
|
+
private lazy var imageCache: NSCache<NSString, UIImage> = {
|
|
787
|
+
let cache = NSCache<NSString, UIImage>()
|
|
788
|
+
cache.countLimit = 100
|
|
789
|
+
cache.totalCostLimit = 1024 * 1024 * 50 // 50MB
|
|
790
|
+
return cache
|
|
791
|
+
}()
|
|
792
|
+
|
|
793
|
+
func loadImage(from url: URL) async -> UIImage? {
|
|
794
|
+
let key = url.absoluteString as NSString
|
|
795
|
+
|
|
796
|
+
if let cachedImage = imageCache.object(forKey: key) {
|
|
797
|
+
return cachedImage
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
do {
|
|
801
|
+
let (data, _) = try await dataLoader.data(from: url)
|
|
802
|
+
if let image = UIImage(data: data) {
|
|
803
|
+
imageCache.setObject(image, forKey: key)
|
|
804
|
+
return image
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
print("Failed to load image: \(error)")
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return nil
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Efficient data structures
|
|
815
|
+
class EfficientDataProcessor {
|
|
816
|
+
private var observations: [Observation] = []
|
|
817
|
+
private var processedData: ProcessedData?
|
|
818
|
+
|
|
819
|
+
// Use lazy evaluation for expensive computations
|
|
820
|
+
private lazy var statistics: Statistics = {
|
|
821
|
+
calculateStatistics(from: observations)
|
|
822
|
+
}()
|
|
823
|
+
|
|
824
|
+
func addObservation(_ observation: Observation) {
|
|
825
|
+
observations.append(observation)
|
|
826
|
+
// Invalidate cached data when underlying data changes
|
|
827
|
+
processedData = nil
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
var data: ProcessedData {
|
|
831
|
+
if let cached = processedData {
|
|
832
|
+
return cached
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
let result = processData(observations)
|
|
836
|
+
processedData = result
|
|
837
|
+
return result
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
var stats: Statistics {
|
|
841
|
+
return statistics
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private func calculateStatistics(from observations: [Observation]) -> Statistics {
|
|
845
|
+
// Expensive calculation
|
|
846
|
+
return Statistics(observations: observations)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private func processData(_ observations: [Observation]) -> ProcessedData {
|
|
850
|
+
// Data processing logic
|
|
851
|
+
return ProcessedData(observations: observations)
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### SwiftUI Performance Optimization
|
|
857
|
+
|
|
858
|
+
```swift
|
|
859
|
+
// Performance optimizations for SwiftUI
|
|
860
|
+
struct OptimizedListView: View {
|
|
861
|
+
@State private var items: [Item] = []
|
|
862
|
+
|
|
863
|
+
var body: some View {
|
|
864
|
+
// Use OnAppear for data loading instead of init
|
|
865
|
+
List(items) { item in
|
|
866
|
+
ItemRow(item: item)
|
|
867
|
+
.id(item.id) // Stable identity for view recycling
|
|
868
|
+
}
|
|
869
|
+
.listStyle(.plain)
|
|
870
|
+
.onAppear {
|
|
871
|
+
loadItems()
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
private func loadItems() {
|
|
876
|
+
// Load data asynchronously
|
|
877
|
+
Task {
|
|
878
|
+
let loadedItems = await loadItemsFromAPI()
|
|
879
|
+
|
|
880
|
+
// Update UI on main thread
|
|
881
|
+
await MainActor.run {
|
|
882
|
+
items = loadedItems
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
struct ItemRow: View {
|
|
889
|
+
let item: Item
|
|
890
|
+
|
|
891
|
+
var body: some View {
|
|
892
|
+
HStack {
|
|
893
|
+
AsyncImage(url: item.imageURL) { image in
|
|
894
|
+
image
|
|
895
|
+
.resizable()
|
|
896
|
+
.aspectRatio(contentMode: .fill)
|
|
897
|
+
} placeholder: {
|
|
898
|
+
Color.gray.opacity(0.3)
|
|
899
|
+
}
|
|
900
|
+
.frame(width: 50, height: 50)
|
|
901
|
+
.clipped()
|
|
902
|
+
|
|
903
|
+
VStack(alignment: .leading, spacing: 4) {
|
|
904
|
+
Text(item.title)
|
|
905
|
+
.font(.headline)
|
|
906
|
+
.lineLimit(1)
|
|
907
|
+
|
|
908
|
+
Text(item.description)
|
|
909
|
+
.font(.body)
|
|
910
|
+
.foregroundColor(.secondary)
|
|
911
|
+
.lineLimit(2)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
Spacer()
|
|
915
|
+
}
|
|
916
|
+
.padding(.vertical, 8)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Efficient state management
|
|
921
|
+
@Observable
|
|
922
|
+
class PerformanceOptimizedViewModel {
|
|
923
|
+
// Use @Published sparingly and only when UI needs to react
|
|
924
|
+
@Published private(set) var items: [Item] = []
|
|
925
|
+
@Published var isLoading = false
|
|
926
|
+
@Published var error: Error?
|
|
927
|
+
|
|
928
|
+
private let service: APIService
|
|
929
|
+
private var currentPage = 0
|
|
930
|
+
private var hasMorePages = true
|
|
931
|
+
|
|
932
|
+
init(service: APIService) {
|
|
933
|
+
self.service = service
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
func loadMoreItems() async {
|
|
937
|
+
guard !isLoading, hasMorePages else { return }
|
|
938
|
+
|
|
939
|
+
isLoading = true
|
|
940
|
+
|
|
941
|
+
defer { isLoading = false }
|
|
942
|
+
|
|
943
|
+
do {
|
|
944
|
+
let newItems = try await service.fetchItems(page: currentPage + 1)
|
|
945
|
+
|
|
946
|
+
await MainActor.run {
|
|
947
|
+
items.append(contentsOf: newItems)
|
|
948
|
+
currentPage += 1
|
|
949
|
+
hasMorePages = !newItems.isEmpty
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
952
|
+
await MainActor.run {
|
|
953
|
+
self.error = error
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
func refresh() async {
|
|
959
|
+
currentPage = 0
|
|
960
|
+
hasMorePages = true
|
|
961
|
+
items = []
|
|
962
|
+
error = nil
|
|
963
|
+
|
|
964
|
+
await loadMoreItems()
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Server-Side Performance
|
|
970
|
+
|
|
971
|
+
```swift
|
|
972
|
+
// Connection pooling and optimization
|
|
973
|
+
final class DatabaseService {
|
|
974
|
+
private let pool: EventLoopGroupConnectionPool<PostgresConnectionSource>
|
|
975
|
+
|
|
976
|
+
init(configuration: PostgresConfiguration) {
|
|
977
|
+
let source = PostgresConnectionSource(configuration: configuration)
|
|
978
|
+
pool = EventLoopGroupConnectionPool(
|
|
979
|
+
source: source,
|
|
980
|
+
maxConnectionsPerEventLoop: 10,
|
|
981
|
+
on: MultiThreadedEventLoopGroup(numberOfThreads: 4)
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
func execute<T>(_ query: SQLQuery, on eventLoop: EventLoop) async throws -> [T] {
|
|
986
|
+
return try await withThrowingCheckedContinuation { continuation in
|
|
987
|
+
pool.withConnection(on: eventLoop) { connection in
|
|
988
|
+
connection.query(query)
|
|
989
|
+
.eachRow { row in
|
|
990
|
+
// Process each row
|
|
991
|
+
}
|
|
992
|
+
.flatMapThrowing { rows in
|
|
993
|
+
// Convert rows to result type
|
|
994
|
+
return rows.map { self.convertRow($0) }
|
|
995
|
+
}
|
|
996
|
+
.map { continuation.resume(returning: $0) }
|
|
997
|
+
.recover { continuation.resume(throwing: $0) }
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Caching with async/await
|
|
1004
|
+
actor CacheService {
|
|
1005
|
+
private var cache: [String: CacheEntry] = [:]
|
|
1006
|
+
private let cleanupTimer: Timer
|
|
1007
|
+
|
|
1008
|
+
init() {
|
|
1009
|
+
cleanupTimer = Timer.scheduledTimer(withTimeInterval: 300, repeats: true) { [weak self] _ in
|
|
1010
|
+
Task {
|
|
1011
|
+
await self?.cleanup()
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
func get(_ key: String) async -> Data? {
|
|
1017
|
+
guard let entry = cache[key], !entry.isExpired else {
|
|
1018
|
+
return nil
|
|
1019
|
+
}
|
|
1020
|
+
return entry.data
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
func set(_ data: Data, forKey key: String, ttl: TimeInterval = 3600) async {
|
|
1024
|
+
cache[key] = CacheEntry(data: data, expiresAt: Date().addingTimeInterval(ttl))
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
private func cleanup() async {
|
|
1028
|
+
let expiredKeys = cache.compactMapValues { entry in
|
|
1029
|
+
entry.isExpired ? nil : entry
|
|
1030
|
+
}.keys
|
|
1031
|
+
|
|
1032
|
+
for key in expiredKeys {
|
|
1033
|
+
cache.removeValue(forKey: key)
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
struct CacheEntry {
|
|
1038
|
+
let data: Data
|
|
1039
|
+
let expiresAt: Date
|
|
1040
|
+
|
|
1041
|
+
var isExpired: Bool {
|
|
1042
|
+
Date() > expiresAt
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
## Testing Strategy
|
|
1049
|
+
|
|
1050
|
+
### XCTest Configuration
|
|
1051
|
+
|
|
1052
|
+
```swift
|
|
1053
|
+
// XCTest-based testing
|
|
1054
|
+
class UserServiceTests: XCTestCase {
|
|
1055
|
+
var userService: UserService!
|
|
1056
|
+
var mockAPIClient: MockAPIClient!
|
|
1057
|
+
|
|
1058
|
+
override func setUpWithError() throws {
|
|
1059
|
+
mockAPIClient = MockAPIClient()
|
|
1060
|
+
userService = UserService(apiClient: mockAPIClient)
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
override func tearDownWithError() throws {
|
|
1064
|
+
userService = nil
|
|
1065
|
+
mockAPIClient = nil
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
func testCreateUser_Success() async throws {
|
|
1069
|
+
// Given
|
|
1070
|
+
let userData = CreateUserData(username: "testuser", email: "test@example.com")
|
|
1071
|
+
let expectedUser = User(id: UUID(), username: "testuser", email: "test@example.com")
|
|
1072
|
+
|
|
1073
|
+
mockAPIClient.createUserResult = .success(expectedUser)
|
|
1074
|
+
|
|
1075
|
+
// When
|
|
1076
|
+
let result = try await userService.createUser(userData)
|
|
1077
|
+
|
|
1078
|
+
// Then
|
|
1079
|
+
XCTAssertEqual(result.id, expectedUser.id)
|
|
1080
|
+
XCTAssertEqual(result.username, expectedUser.username)
|
|
1081
|
+
XCTAssertEqual(result.email, expectedUser.email)
|
|
1082
|
+
XCTAssertEqual(mockAPIClient.createUserCallCount, 1)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
func testCreateUser_Failure() async {
|
|
1086
|
+
// Given
|
|
1087
|
+
let userData = CreateUserData(username: "testuser", email: "test@example.com")
|
|
1088
|
+
mockAPIClient.createUserResult = .failure(APIError.networkError)
|
|
1089
|
+
|
|
1090
|
+
// When & Then
|
|
1091
|
+
do {
|
|
1092
|
+
_ = try await userService.createUser(userData)
|
|
1093
|
+
XCTFail("Expected error to be thrown")
|
|
1094
|
+
} catch {
|
|
1095
|
+
XCTAssertTrue(error is APIError)
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
func testPerformanceLoadUsers() async throws {
|
|
1100
|
+
// Given
|
|
1101
|
+
let users = (0..<1000).map { User(id: UUID(), username: "user\($0)", email: "user\($0)@example.com") }
|
|
1102
|
+
mockAPIClient.loadUsersResult = .success(users)
|
|
1103
|
+
|
|
1104
|
+
// Measure performance
|
|
1105
|
+
measure {
|
|
1106
|
+
Task {
|
|
1107
|
+
_ = try! await userService.loadUsers()
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Mock objects for testing
|
|
1114
|
+
class MockAPIClient: APIClientProtocol {
|
|
1115
|
+
var createUserResult: Result<User, APIError>!
|
|
1116
|
+
var loadUsersResult: Result<[User], APIError>!
|
|
1117
|
+
var createUserCallCount = 0
|
|
1118
|
+
|
|
1119
|
+
func createUser(_ data: CreateUserData) async throws -> User {
|
|
1120
|
+
createUserCallCount += 1
|
|
1121
|
+
switch createUserResult {
|
|
1122
|
+
case .success(let user):
|
|
1123
|
+
return user
|
|
1124
|
+
case .failure(let error):
|
|
1125
|
+
throw error
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
func loadUsers() async throws -> [User] {
|
|
1130
|
+
switch loadUsersResult {
|
|
1131
|
+
case .success(let users):
|
|
1132
|
+
return users
|
|
1133
|
+
case .failure(let error):
|
|
1134
|
+
throw error
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Quick/Nimble BDD-style testing
|
|
1140
|
+
import Quick
|
|
1141
|
+
import Nimble
|
|
1142
|
+
|
|
1143
|
+
class UserServiceSpec: QuickSpec {
|
|
1144
|
+
override func spec() {
|
|
1145
|
+
var userService: UserService!
|
|
1146
|
+
var mockAPIClient: MockAPIClient!
|
|
1147
|
+
|
|
1148
|
+
beforeEach {
|
|
1149
|
+
mockAPIClient = MockAPIClient()
|
|
1150
|
+
userService = UserService(apiClient: mockAPIClient)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
describe("createUser") {
|
|
1154
|
+
context("when API call succeeds") {
|
|
1155
|
+
it("should return the created user") {
|
|
1156
|
+
let userData = CreateUserData(username: "testuser", email: "test@example.com")
|
|
1157
|
+
let expectedUser = User(id: UUID(), username: "testuser", email: "test@example.com")
|
|
1158
|
+
mockAPIClient.createUserResult = .success(expectedUser)
|
|
1159
|
+
|
|
1160
|
+
waitUntil { done in
|
|
1161
|
+
Task {
|
|
1162
|
+
do {
|
|
1163
|
+
let result = try await userService.createUser(userData)
|
|
1164
|
+
expect(result.username).to(equal(expectedUser.username))
|
|
1165
|
+
expect(result.email).to(equal(expectedUser.email))
|
|
1166
|
+
done()
|
|
1167
|
+
} catch {
|
|
1168
|
+
fail("Unexpected error: \(error)")
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
context("when API call fails") {
|
|
1176
|
+
it("should throw an error") {
|
|
1177
|
+
let userData = CreateUserData(username: "testuser", email: "test@example.com")
|
|
1178
|
+
mockAPIClient.createUserResult = .failure(APIError.networkError)
|
|
1179
|
+
|
|
1180
|
+
waitUntil { done in
|
|
1181
|
+
Task {
|
|
1182
|
+
do {
|
|
1183
|
+
_ = try await userService.createUser(userData)
|
|
1184
|
+
fail("Expected error to be thrown")
|
|
1185
|
+
} catch {
|
|
1186
|
+
expect(error).to(beAKindOf(APIError.self))
|
|
1187
|
+
done()
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
```
|
|
79
1197
|
|
|
80
|
-
|
|
81
|
-
- Access to the project file is required using the Read/Grep tool.
|
|
82
|
-
- When used with `Skill("moai-foundation-langs")`, it is easy to share cross-language conventions.
|
|
1198
|
+
### SwiftUI Testing
|
|
83
1199
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1200
|
+
```swift
|
|
1201
|
+
import XCTest
|
|
1202
|
+
import SwiftUI
|
|
1203
|
+
@testable import MySwiftApp
|
|
87
1204
|
|
|
88
|
-
|
|
89
|
-
|
|
1205
|
+
class UserProfileViewTests: XCTestCase {
|
|
1206
|
+
|
|
1207
|
+
func testUserProfileView_DisplaysUserInfo() {
|
|
1208
|
+
// Given
|
|
1209
|
+
let user = User(id: UUID(), username: "testuser", name: "Test User", email: "test@example.com")
|
|
1210
|
+
let userService = MockUserService()
|
|
1211
|
+
userService.mockUser = user
|
|
1212
|
+
|
|
1213
|
+
// When
|
|
1214
|
+
let view = UserProfileView(userId: user.id.uuidString, userService: userService)
|
|
1215
|
+
|
|
1216
|
+
// Then
|
|
1217
|
+
let hostingController = UIHostingController(rootView: view)
|
|
1218
|
+
hostingController.loadViewIfNeeded()
|
|
1219
|
+
|
|
1220
|
+
// Test view content
|
|
1221
|
+
// Note: This is a simplified example - in practice, you'd use view inspection or snapshot testing
|
|
1222
|
+
XCTAssertNotNil(hostingController.view)
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
90
1225
|
|
|
91
|
-
|
|
1226
|
+
// ViewInspector for SwiftUI testing (third-party library)
|
|
1227
|
+
import ViewInspector
|
|
1228
|
+
|
|
1229
|
+
extension UserProfileView: Inspectable { }
|
|
1230
|
+
|
|
1231
|
+
class UserProfileViewInspectorTests: XCTestCase {
|
|
1232
|
+
|
|
1233
|
+
func testUserProfileView_WhenLoaded_ShowsUserData() throws {
|
|
1234
|
+
// Given
|
|
1235
|
+
let user = User(id: UUID(), username: "testuser", name: "Test User", email: "test@example.com")
|
|
1236
|
+
let userService = MockUserService()
|
|
1237
|
+
userService.mockUser = user
|
|
1238
|
+
|
|
1239
|
+
// When
|
|
1240
|
+
let view = UserProfileView(userId: user.id.uuidString, userService: userService)
|
|
1241
|
+
let inspectedView = try view.inspect()
|
|
1242
|
+
|
|
1243
|
+
// Then
|
|
1244
|
+
let navigationStack = try inspectedView.navigationStack()
|
|
1245
|
+
let navigationTitle = try navigationStack.navigationBarTitleLabel().string()
|
|
1246
|
+
XCTAssertEqual(navigationTitle, "Profile")
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
### Server-Side Testing
|
|
1252
|
+
|
|
1253
|
+
```swift
|
|
1254
|
+
import XCTVapor
|
|
1255
|
+
import XCTest
|
|
1256
|
+
@testable import MyServer
|
|
1257
|
+
|
|
1258
|
+
final class UserControllerTests: XCTestCase {
|
|
1259
|
+
|
|
1260
|
+
func testCreateUser() async throws {
|
|
1261
|
+
let app = Application(.testing)
|
|
1262
|
+
try configure(app)
|
|
1263
|
+
|
|
1264
|
+
// Test data
|
|
1265
|
+
let userData = CreateUserData(username: "testuser", email: "test@example.com", password: "password123")
|
|
1266
|
+
|
|
1267
|
+
try app.test(.POST, "/api/users", beforeRequest: { req in
|
|
1268
|
+
try req.content.encode(userData)
|
|
1269
|
+
}, afterResponse: { res in
|
|
1270
|
+
XCTAssertEqual(res.status, .ok)
|
|
1271
|
+
let user = try res.content.decode(User.Public.self)
|
|
1272
|
+
XCTAssertEqual(user.username, "testuser")
|
|
1273
|
+
XCTAssertEqual(user.email, "test@example.com")
|
|
1274
|
+
})
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
func testGetUser() async throws {
|
|
1278
|
+
let app = Application(.testing)
|
|
1279
|
+
try configure(app)
|
|
1280
|
+
|
|
1281
|
+
// Create a user first
|
|
1282
|
+
let userData = CreateUserData(username: "testuser", email: "test@example.com", password: "password123")
|
|
1283
|
+
|
|
1284
|
+
try app.test(.POST, "/api/users", beforeRequest: { req in
|
|
1285
|
+
try req.content.encode(userData)
|
|
1286
|
+
}, afterResponse: { createRes in
|
|
1287
|
+
XCTAssertEqual(createRes.status, .ok)
|
|
1288
|
+
let createdUser = try createRes.content.decode(User.Public.self)
|
|
1289
|
+
|
|
1290
|
+
// Test getting the user
|
|
1291
|
+
try app.test(.GET, "/api/users/\(createdUser.id)", afterResponse: { getRes in
|
|
1292
|
+
XCTAssertEqual(getRes.status, .ok)
|
|
1293
|
+
let user = try getRes.content.decode(User.Public.self)
|
|
1294
|
+
XCTAssertEqual(user.id, createdUser.id)
|
|
1295
|
+
XCTAssertEqual(user.username, "testuser")
|
|
1296
|
+
})
|
|
1297
|
+
})
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
func testWebSocketConnection() async throws {
|
|
1301
|
+
let app = Application(.testing)
|
|
1302
|
+
try configure(app)
|
|
1303
|
+
|
|
1304
|
+
try app.test(.websocket, "/ws/chat") { conn in
|
|
1305
|
+
try conn.send("Hello, server!")
|
|
1306
|
+
try conn.expectString(text: "Hello, client!")
|
|
1307
|
+
try conn.close()
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
## Security Best Practices
|
|
1314
|
+
|
|
1315
|
+
### Input Validation and Sanitization
|
|
1316
|
+
|
|
1317
|
+
```swift
|
|
1318
|
+
// Input validation with property wrappers
|
|
1319
|
+
@propertyWrapper
|
|
1320
|
+
struct ValidatedEmail: Decodable {
|
|
1321
|
+
private let _value: String
|
|
1322
|
+
|
|
1323
|
+
var wrappedValue: String {
|
|
1324
|
+
return _value
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
init(wrappedValue: String) throws {
|
|
1328
|
+
guard Self.isValidEmail(wrappedValue) else {
|
|
1329
|
+
throw ValidationError.invalidEmail
|
|
1330
|
+
}
|
|
1331
|
+
_value = wrappedValue.lowercased()
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
init(from decoder: Decoder) throws {
|
|
1335
|
+
let container = try decoder.singleValueContainer()
|
|
1336
|
+
let value = try container.decode(String.self)
|
|
1337
|
+
try self.init(wrappedValue: value)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
private static func isValidEmail(_ email: String) -> Bool {
|
|
1341
|
+
let emailRegex = #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"#
|
|
1342
|
+
return email.range(of: emailRegex, options: .regularExpression) != nil
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
struct CreateUserRequest: Decodable {
|
|
1347
|
+
@ValidatedEmail var email: String
|
|
1348
|
+
@ValidatedUsername var username: String
|
|
1349
|
+
@SecurePassword var password: String
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
@propertyWrapper
|
|
1353
|
+
struct SecurePassword: Decodable {
|
|
1354
|
+
private let _value: String
|
|
1355
|
+
|
|
1356
|
+
var wrappedValue: String {
|
|
1357
|
+
return _value
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
init(wrappedValue: String) throws {
|
|
1361
|
+
guard Self.isValidPassword(wrappedValue) else {
|
|
1362
|
+
throw ValidationError.weakPassword
|
|
1363
|
+
}
|
|
1364
|
+
_value = wrappedValue
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
init(from decoder: Decoder) throws {
|
|
1368
|
+
let container = try decoder.singleValueContainer()
|
|
1369
|
+
let value = try container.decode(String.self)
|
|
1370
|
+
try self.init(wrappedValue: value)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
private static func isValidPassword(_ password: String) -> Bool {
|
|
1374
|
+
// At least 8 characters, one uppercase, one lowercase, one digit, one special character
|
|
1375
|
+
let passwordRegex = #"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$"#
|
|
1376
|
+
return password.range(of: passwordRegex, options: .regularExpression) != nil
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
enum ValidationError: Error, LocalizedError {
|
|
1381
|
+
case invalidEmail
|
|
1382
|
+
case weakPassword
|
|
1383
|
+
|
|
1384
|
+
var errorDescription: String? {
|
|
1385
|
+
switch self {
|
|
1386
|
+
case .invalidEmail:
|
|
1387
|
+
return "Invalid email format"
|
|
1388
|
+
case .weakPassword:
|
|
1389
|
+
return "Password must be at least 8 characters and contain uppercase, lowercase, digit, and special character"
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
### Authentication and Authorization
|
|
1396
|
+
|
|
1397
|
+
```swift
|
|
1398
|
+
// JWT authentication middleware
|
|
1399
|
+
struct JWTMiddleware: AsyncMiddleware {
|
|
1400
|
+
let jwtSecret: String
|
|
1401
|
+
|
|
1402
|
+
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
|
1403
|
+
guard let token = request.headers.bearerAuthorization?.token else {
|
|
1404
|
+
throw Abort(.unauthorized, reason: "Missing authorization token")
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
do {
|
|
1408
|
+
let payload = try JWT<AuthPayload>(from: token, verifiedUsing: .hs256(key: jwtSecret))
|
|
1409
|
+
request.auth.login(payload.user)
|
|
1410
|
+
return try await next.respond(to: request)
|
|
1411
|
+
} catch {
|
|
1412
|
+
throw Abort(.unauthorized, reason: "Invalid token")
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
struct AuthPayload: JWTPayload {
|
|
1418
|
+
var user: User
|
|
1419
|
+
var expirationTime: ExpirationClaim
|
|
1420
|
+
|
|
1421
|
+
func verify(using algorithm: some JWTAlgorithm) throws {
|
|
1422
|
+
try expirationTime.verifyNotExpired()
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// Role-based access control
|
|
1427
|
+
enum UserRole: String, Codable {
|
|
1428
|
+
case admin
|
|
1429
|
+
case moderator
|
|
1430
|
+
case user
|
|
1431
|
+
case guest
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
extension User {
|
|
1435
|
+
func hasRole(_ role: UserRole) -> Bool {
|
|
1436
|
+
return self.role == role
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
func canAccessResource(_ resource: Resource) -> Bool {
|
|
1440
|
+
switch resource.type {
|
|
1441
|
+
case .adminOnly:
|
|
1442
|
+
return hasRole(.admin)
|
|
1443
|
+
case .moderatorAndAbove:
|
|
1444
|
+
return hasRole(.admin) || hasRole(.moderator)
|
|
1445
|
+
case .userAndAbove:
|
|
1446
|
+
return hasRole(.admin) || hasRole(.moderator) || hasRole(.user)
|
|
1447
|
+
case .public:
|
|
1448
|
+
return true
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Request extension for authentication
|
|
1454
|
+
extension Request {
|
|
1455
|
+
var authenticatedUser: User? {
|
|
1456
|
+
return auth.get(User.self)
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
func requireAuthentication() throws -> User {
|
|
1460
|
+
guard let user = authenticatedUser else {
|
|
1461
|
+
throw Abort(.unauthorized, reason: "Authentication required")
|
|
1462
|
+
}
|
|
1463
|
+
return user
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
func requireRole(_ role: UserRole) throws -> User {
|
|
1467
|
+
let user = try requireAuthentication()
|
|
1468
|
+
guard user.hasRole(role) else {
|
|
1469
|
+
throw Abort(.forbidden, reason: "Insufficient permissions")
|
|
1470
|
+
}
|
|
1471
|
+
return user
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
### Security Headers and CORS
|
|
1477
|
+
|
|
1478
|
+
```swift
|
|
1479
|
+
// Security headers middleware
|
|
1480
|
+
struct SecurityHeadersMiddleware: AsyncMiddleware {
|
|
1481
|
+
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
|
1482
|
+
let response = try await next.respond(to: request)
|
|
1483
|
+
|
|
1484
|
+
// Add security headers
|
|
1485
|
+
response.headers.add(name: .xContentTypeOptions, value: "nosniff")
|
|
1486
|
+
response.headers.add(name: .xFrameOptions, value: "DENY")
|
|
1487
|
+
response.headers.add(name: .xXSSProtection, value: "1; mode=block")
|
|
1488
|
+
response.headers.add(name: .strictTransportSecurity, value: "max-age=31536000; includeSubDomains")
|
|
1489
|
+
response.headers.add(name: .contentSecurityPolicy, value: "default-src 'self'")
|
|
1490
|
+
|
|
1491
|
+
return response
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// CORS configuration
|
|
1496
|
+
struct CORSMiddleware: AsyncMiddleware {
|
|
1497
|
+
private let allowedOrigins: [String]
|
|
1498
|
+
private let allowedMethods: [HTTPMethod]
|
|
1499
|
+
private let allowedHeaders: [String]
|
|
1500
|
+
|
|
1501
|
+
init(allowedOrigins: [String] = ["*"], allowedMethods: [HTTPMethod] = [.GET, .POST, .PUT, .DELETE], allowedHeaders: [String] = ["Content-Type", "Authorization"]) {
|
|
1502
|
+
self.allowedOrigins = allowedOrigins
|
|
1503
|
+
self.allowedMethods = allowedMethods
|
|
1504
|
+
self.allowedHeaders = allowedHeaders
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
|
|
1508
|
+
let response = try await next.respond(to: request)
|
|
1509
|
+
|
|
1510
|
+
// Add CORS headers
|
|
1511
|
+
if let origin = request.headers.first(name: .origin) {
|
|
1512
|
+
if allowedOrigins.contains("*") || allowedOrigins.contains(origin) {
|
|
1513
|
+
response.headers.add(name: .accessControlAllowOrigin, value: origin)
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
response.headers.add(name: .accessControlAllowMethods, value: allowedMethods.map { $0.string }.joined(separator: ", "))
|
|
1518
|
+
response.headers.add(name: .accessControlAllowHeaders, value: allowedHeaders.joined(separator: ", "))
|
|
1519
|
+
|
|
1520
|
+
return response
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
## Integration Patterns
|
|
1526
|
+
|
|
1527
|
+
### Core Data Integration
|
|
1528
|
+
|
|
1529
|
+
```swift
|
|
1530
|
+
import CoreData
|
|
1531
|
+
import SwiftUI
|
|
1532
|
+
|
|
1533
|
+
// Core Data manager with async/await
|
|
1534
|
+
actor CoreDataManager {
|
|
1535
|
+
static let shared = CoreDataManager()
|
|
1536
|
+
|
|
1537
|
+
lazy var persistentContainer: NSPersistentContainer = {
|
|
1538
|
+
let container = NSPersistentContainer(name: "DataModel")
|
|
1539
|
+
container.loadPersistentStores { _, error in
|
|
1540
|
+
if let error = error {
|
|
1541
|
+
fatalError("Core Data error: \(error.localizedDescription)")
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
container.viewContext.automaticallyMergesChangesFromParent = true
|
|
1545
|
+
return container
|
|
1546
|
+
}()
|
|
1547
|
+
|
|
1548
|
+
var viewContext: NSManagedObjectContext {
|
|
1549
|
+
persistentContainer.viewContext
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
private var backgroundContext: NSManagedObjectContext {
|
|
1553
|
+
persistentContainer.newBackgroundContext()
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
func fetchUsers() async -> [User] {
|
|
1557
|
+
return await withCheckedContinuation { continuation in
|
|
1558
|
+
let request: NSFetchRequest<User> = User.fetchRequest()
|
|
1559
|
+
|
|
1560
|
+
backgroundContext.perform {
|
|
1561
|
+
do {
|
|
1562
|
+
let users = try self.backgroundContext.fetch(request)
|
|
1563
|
+
continuation.resume(returning: users)
|
|
1564
|
+
} catch {
|
|
1565
|
+
continuation.resume(returning: [])
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
func saveUser(_ user: User) async {
|
|
1572
|
+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
1573
|
+
backgroundContext.perform {
|
|
1574
|
+
do {
|
|
1575
|
+
try self.backgroundContext.save()
|
|
1576
|
+
continuation.resume()
|
|
1577
|
+
} catch {
|
|
1578
|
+
print("Failed to save user: \(error)")
|
|
1579
|
+
continuation.resume()
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
func deleteUser(_ user: User) async {
|
|
1586
|
+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
1587
|
+
backgroundContext.perform {
|
|
1588
|
+
self.backgroundContext.delete(user)
|
|
1589
|
+
do {
|
|
1590
|
+
try self.backgroundContext.save()
|
|
1591
|
+
continuation.resume()
|
|
1592
|
+
} catch {
|
|
1593
|
+
print("Failed to delete user: \(error)")
|
|
1594
|
+
continuation.resume()
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// SwiftUI Core Data integration
|
|
1602
|
+
struct CoreDataUserListView: View {
|
|
1603
|
+
@Environment(\.managedObjectContext) private var viewContext
|
|
1604
|
+
@FetchRequest(
|
|
1605
|
+
sortDescriptors: [NSSortDescriptor(keyPath: \User.createdAt, ascending: true)],
|
|
1606
|
+
animation: .default)
|
|
1607
|
+
private var users: FetchedResults<User>
|
|
1608
|
+
|
|
1609
|
+
var body: some View {
|
|
1610
|
+
List {
|
|
1611
|
+
ForEach(users, id: \.objectID) { user in
|
|
1612
|
+
UserRow(user: user)
|
|
1613
|
+
}
|
|
1614
|
+
.onDelete(perform: deleteUsers)
|
|
1615
|
+
}
|
|
1616
|
+
.toolbar {
|
|
1617
|
+
ToolbarItem(placement: .navigationBarTrailing) {
|
|
1618
|
+
Button(action: addUser) {
|
|
1619
|
+
Label("Add User", systemImage: "plus")
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
private func addUser() {
|
|
1626
|
+
withAnimation {
|
|
1627
|
+
let newUser = User(context: viewContext)
|
|
1628
|
+
newUser.name = "New User"
|
|
1629
|
+
newUser.createdAt = Date()
|
|
1630
|
+
|
|
1631
|
+
saveContext()
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
private func deleteUsers(offsets: IndexSet) {
|
|
1636
|
+
withAnimation {
|
|
1637
|
+
offsets.map { users[$0] }.forEach(viewContext.delete)
|
|
1638
|
+
saveContext()
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
private func saveContext() {
|
|
1643
|
+
do {
|
|
1644
|
+
try viewContext.save()
|
|
1645
|
+
} catch {
|
|
1646
|
+
let nsError = error as NSError
|
|
1647
|
+
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
```
|
|
1652
|
+
|
|
1653
|
+
### Networking with Async/Await
|
|
1654
|
+
|
|
1655
|
+
```swift
|
|
1656
|
+
import Foundation
|
|
1657
|
+
|
|
1658
|
+
// Modern networking client
|
|
1659
|
+
class APIClient: APIClientProtocol {
|
|
1660
|
+
private let session: URLSession
|
|
1661
|
+
private let baseURL: URL
|
|
1662
|
+
private let decoder: JSONDecoder
|
|
1663
|
+
|
|
1664
|
+
init(baseURL: URL = URL(string: "https://api.example.com")!) {
|
|
1665
|
+
self.baseURL = baseURL
|
|
1666
|
+
self.decoder = JSONDecoder()
|
|
1667
|
+
self.decoder.dateDecodingStrategy = .iso8601
|
|
1668
|
+
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
1669
|
+
|
|
1670
|
+
let config = URLSessionConfiguration.default
|
|
1671
|
+
config.timeoutIntervalForRequest = 30
|
|
1672
|
+
config.timeoutIntervalForResource = 60
|
|
1673
|
+
config.waitsForConnectivity = true
|
|
1674
|
+
|
|
1675
|
+
self.session = URLSession(configuration: config)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
func request<T: Decodable>(_ endpoint: APIEndpoint, responseType: T.Type) async throws -> T {
|
|
1679
|
+
let request = try buildRequest(for: endpoint)
|
|
1680
|
+
|
|
1681
|
+
let (data, response) = try await session.data(for: request)
|
|
1682
|
+
|
|
1683
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
1684
|
+
throw APIError.invalidResponse
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
guard 200...299 ~= httpResponse.statusCode else {
|
|
1688
|
+
throw APIError.serverError(statusCode: httpResponse.statusCode)
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
do {
|
|
1692
|
+
return try decoder.decode(T.self, from: data)
|
|
1693
|
+
} catch {
|
|
1694
|
+
throw APIError.decodingError(error)
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
func uploadData<T: Decodable>(_ data: Data, to endpoint: APIEndpoint, responseType: T.Type) async throws -> T {
|
|
1699
|
+
var request = try buildRequest(for: endpoint)
|
|
1700
|
+
request.httpMethod = "POST"
|
|
1701
|
+
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
|
1702
|
+
request.httpBody = data
|
|
1703
|
+
|
|
1704
|
+
let (responseData, response) = try await session.upload(for: request, from: data)
|
|
1705
|
+
|
|
1706
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
1707
|
+
throw APIError.invalidResponse
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
guard 200...299 ~= httpResponse.statusCode else {
|
|
1711
|
+
throw APIError.serverError(statusCode: httpResponse.statusCode)
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
do {
|
|
1715
|
+
return try decoder.decode(T.self, from: responseData)
|
|
1716
|
+
} catch {
|
|
1717
|
+
throw APIError.decodingError(error)
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private func buildRequest(for endpoint: APIEndpoint) throws -> URLRequest {
|
|
1722
|
+
var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path), resolvingAgainstBaseURL: false)!
|
|
1723
|
+
|
|
1724
|
+
if !endpoint.parameters.isEmpty {
|
|
1725
|
+
components.queryItems = endpoint.parameters.map { key, value in
|
|
1726
|
+
URLQueryItem(name: key, value: "\(value)")
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
var request = URLRequest(url: components.url!)
|
|
1731
|
+
request.httpMethod = endpoint.method.rawValue
|
|
1732
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
1733
|
+
|
|
1734
|
+
if let body = endpoint.body {
|
|
1735
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
return request
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// API endpoint configuration
|
|
1743
|
+
enum APIEndpoint {
|
|
1744
|
+
case getUser(id: String)
|
|
1745
|
+
case getUsers(page: Int, limit: Int)
|
|
1746
|
+
case createUser(CreateUserData)
|
|
1747
|
+
case updateUser(id: String, UpdateUserData)
|
|
1748
|
+
case deleteUser(id: String)
|
|
1749
|
+
|
|
1750
|
+
var path: String {
|
|
1751
|
+
switch self {
|
|
1752
|
+
case .getUser(let id):
|
|
1753
|
+
return "users/\(id)"
|
|
1754
|
+
case .getUsers:
|
|
1755
|
+
return "users"
|
|
1756
|
+
case .createUser:
|
|
1757
|
+
return "users"
|
|
1758
|
+
case .updateUser(let id, _):
|
|
1759
|
+
return "users/\(id)"
|
|
1760
|
+
case .deleteUser(let id):
|
|
1761
|
+
return "users/\(id)"
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
var method: HTTPMethod {
|
|
1766
|
+
switch self {
|
|
1767
|
+
case .getUser, .getUsers:
|
|
1768
|
+
return .GET
|
|
1769
|
+
case .createUser:
|
|
1770
|
+
return .POST
|
|
1771
|
+
case .updateUser:
|
|
1772
|
+
return .PUT
|
|
1773
|
+
case .deleteUser:
|
|
1774
|
+
return .DELETE
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
var parameters: [String: Any] {
|
|
1779
|
+
switch self {
|
|
1780
|
+
case .getUsers(let page, let limit):
|
|
1781
|
+
return ["page": page, "limit": limit]
|
|
1782
|
+
default:
|
|
1783
|
+
return [:]
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
var body: [String: Any]? {
|
|
1788
|
+
switch self {
|
|
1789
|
+
case .createUser(let userData):
|
|
1790
|
+
return try? userData.asDictionary()
|
|
1791
|
+
case .updateUser(_, let userData):
|
|
1792
|
+
return try? userData.asDictionary()
|
|
1793
|
+
default:
|
|
1794
|
+
return nil
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
enum HTTPMethod: String {
|
|
1800
|
+
case GET = "GET"
|
|
1801
|
+
case POST = "POST"
|
|
1802
|
+
case PUT = "PUT"
|
|
1803
|
+
case DELETE = "DELETE"
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
enum APIError: Error, LocalizedError {
|
|
1807
|
+
case invalidURL
|
|
1808
|
+
case invalidResponse
|
|
1809
|
+
case serverError(statusCode: Int)
|
|
1810
|
+
case decodingError(Error)
|
|
1811
|
+
case networkError(Error)
|
|
1812
|
+
|
|
1813
|
+
var errorDescription: String? {
|
|
1814
|
+
switch self {
|
|
1815
|
+
case .invalidURL:
|
|
1816
|
+
return "Invalid URL"
|
|
1817
|
+
case .invalidResponse:
|
|
1818
|
+
return "Invalid response"
|
|
1819
|
+
case .serverError(let statusCode):
|
|
1820
|
+
return "Server error with status code: \(statusCode)"
|
|
1821
|
+
case .decodingError(let error):
|
|
1822
|
+
return "Decoding error: \(error.localizedDescription)"
|
|
1823
|
+
case .networkError(let error):
|
|
1824
|
+
return "Network error: \(error.localizedDescription)"
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
```
|
|
1829
|
+
|
|
1830
|
+
## Modern Development Workflow
|
|
1831
|
+
|
|
1832
|
+
### Xcode Configuration
|
|
1833
|
+
|
|
1834
|
+
```swift
|
|
1835
|
+
// Project-level configuration for modern Swift development
|
|
1836
|
+
|
|
1837
|
+
// Info.plist additions for security and privacy
|
|
1838
|
+
<key>NSAppTransportSecurity</key>
|
|
1839
|
+
<dict>
|
|
1840
|
+
<key>NSAllowsArbitraryLoads</key>
|
|
1841
|
+
<false/>
|
|
1842
|
+
<key>NSExceptionDomains</key>
|
|
1843
|
+
<dict>
|
|
1844
|
+
<key>api.example.com</key>
|
|
1845
|
+
<dict>
|
|
1846
|
+
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
|
1847
|
+
<true/>
|
|
1848
|
+
<key>NSExceptionMinimumTLSVersion</key>
|
|
1849
|
+
<string>TLSv1.2</string>
|
|
1850
|
+
</dict>
|
|
1851
|
+
</dict>
|
|
1852
|
+
</dict>
|
|
1853
|
+
|
|
1854
|
+
<key>NSCameraUsageDescription</key>
|
|
1855
|
+
<string>This app needs camera access for profile photos</string>
|
|
1856
|
+
|
|
1857
|
+
<key>NSLocationWhenInUseUsageDescription</key>
|
|
1858
|
+
<string>This app needs location access for location-based features</string>
|
|
1859
|
+
|
|
1860
|
+
<key>NSUserTrackingUsageDescription</key>
|
|
1861
|
+
<string>This app uses tracking for personalized advertising</string>
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
### SwiftLint Configuration
|
|
1865
|
+
|
|
1866
|
+
```yaml
|
|
1867
|
+
# .swiftlint.yml
|
|
1868
|
+
excluded:
|
|
1869
|
+
- Carthage
|
|
1870
|
+
- Pods
|
|
1871
|
+
- build
|
|
1872
|
+
- .build
|
|
1873
|
+
|
|
1874
|
+
opt_in_rules:
|
|
1875
|
+
- empty_count
|
|
1876
|
+
- force_unwrapping
|
|
1877
|
+
- implicitly_unwrapped_optional
|
|
1878
|
+
|
|
1879
|
+
disabled_rules:
|
|
1880
|
+
- trailing_whitespace
|
|
1881
|
+
- line_length
|
|
1882
|
+
|
|
1883
|
+
line_length:
|
|
1884
|
+
warning: 120
|
|
1885
|
+
error: 150
|
|
1886
|
+
|
|
1887
|
+
function_body_length:
|
|
1888
|
+
warning: 50
|
|
1889
|
+
error: 100
|
|
1890
|
+
|
|
1891
|
+
type_body_length:
|
|
1892
|
+
warning: 300
|
|
1893
|
+
error: 500
|
|
1894
|
+
|
|
1895
|
+
file_length:
|
|
1896
|
+
warning: 400
|
|
1897
|
+
error: 800
|
|
1898
|
+
|
|
1899
|
+
cyclomatic_complexity:
|
|
1900
|
+
warning: 10
|
|
1901
|
+
error: 20
|
|
1902
|
+
|
|
1903
|
+
custom_rules:
|
|
1904
|
+
no_console_log:
|
|
1905
|
+
name: "No Console Logging"
|
|
1906
|
+
regex: '(print|NSLog|debugPrint)\('
|
|
1907
|
+
message: "Console logging should be removed in production."
|
|
1908
|
+
severity: warning
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
### CI/CD Configuration
|
|
1912
|
+
|
|
1913
|
+
```yaml
|
|
1914
|
+
# .github/workflows/swift.yml
|
|
1915
|
+
name: Swift CI
|
|
1916
|
+
|
|
1917
|
+
on:
|
|
1918
|
+
push:
|
|
1919
|
+
branches: [ main, develop ]
|
|
1920
|
+
pull_request:
|
|
1921
|
+
branches: [ main, develop ]
|
|
1922
|
+
|
|
1923
|
+
jobs:
|
|
1924
|
+
test:
|
|
1925
|
+
runs-on: macos-latest
|
|
1926
|
+
|
|
1927
|
+
steps:
|
|
1928
|
+
- uses: actions/checkout@v4
|
|
1929
|
+
|
|
1930
|
+
- name: Select Xcode
|
|
1931
|
+
uses: maxim-lobanov/setup-xcode@v1
|
|
1932
|
+
with:
|
|
1933
|
+
xcode-version: latest-stable
|
|
1934
|
+
|
|
1935
|
+
- name: Cache Swift Package Manager
|
|
1936
|
+
uses: actions/cache@v3
|
|
1937
|
+
with:
|
|
1938
|
+
path: .build
|
|
1939
|
+
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
|
|
1940
|
+
|
|
1941
|
+
- name: Build
|
|
1942
|
+
run: swift build
|
|
1943
|
+
|
|
1944
|
+
- name: Run tests
|
|
1945
|
+
run: swift test --enable-code-coverage
|
|
1946
|
+
|
|
1947
|
+
- name: Generate code coverage report
|
|
1948
|
+
run: xcrun llvm-cov report -build-path .build -use-llvm --json > coverage.json
|
|
1949
|
+
|
|
1950
|
+
- name: Upload coverage to Codecov
|
|
1951
|
+
uses: codecov/codecov-action@v3
|
|
1952
|
+
with:
|
|
1953
|
+
file: ./coverage.json
|
|
1954
|
+
|
|
1955
|
+
- name: Run SwiftLint
|
|
1956
|
+
run: |
|
|
1957
|
+
mint run swiftlint/swiftlint swiftlint
|
|
1958
|
+
mint run swiftlint/swiftlint swiftlint --strict
|
|
1959
|
+
|
|
1960
|
+
- name: Run SwiftFormat
|
|
1961
|
+
run: |
|
|
1962
|
+
mint run swiftformat/swiftformat swiftformat --lint --strict .
|
|
1963
|
+
|
|
1964
|
+
ios_test:
|
|
1965
|
+
runs-on: macos-latest
|
|
1966
|
+
|
|
1967
|
+
steps:
|
|
1968
|
+
- uses: actions/checkout@v4
|
|
1969
|
+
|
|
1970
|
+
- name: Select Xcode
|
|
1971
|
+
uses: maxim-lobanov/setup-xcode@v1
|
|
1972
|
+
with:
|
|
1973
|
+
xcode-version: latest-stable
|
|
1974
|
+
|
|
1975
|
+
- name: Build iOS App
|
|
1976
|
+
run: xcodebuild -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' clean build
|
|
1977
|
+
|
|
1978
|
+
- name: Run iOS Tests
|
|
1979
|
+
run: xcodebuild -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' test
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
---
|
|
92
1983
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
1984
|
+
**Created by**: MoAI Language Skill Factory
|
|
1985
|
+
**Last Updated**: 2025-11-06
|
|
1986
|
+
**Version**: 2.0.0
|
|
1987
|
+
**Swift Target**: 6.0 with modern SwiftUI, Swift Concurrency, and server-side Swift
|
|
96
1988
|
|
|
97
|
-
|
|
98
|
-
- Enable automatic validation by matching your linter with the language's official style guide.
|
|
99
|
-
- Fix test/build pipelines with reproducible commands in CI.
|
|
1989
|
+
This skill provides comprehensive Swift development guidance with 2025 best practices, covering everything from iOS/macOS applications with SwiftUI to server-side development with Vapor and modern concurrency patterns.
|