πŸ¦…βš‘ Π”Π΅Π»Π°Π΅ΠΌ Π½Π°Ρ‚ΠΈΠ²Π½ΠΎΠ΅ мобильноС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ с ИИ ΠΈ бэкСндом

Π’ этой ΡΡ‚Π°Ρ‚ΡŒΠ΅ ΠΌΡ‹ рассмотрим ΠΏΡ€ΠΈΠΌΠ΅Ρ€ создания Proof of Concept (PoC) мобильного прилоТСния, построСнного с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ° SwiftUI ΠΈ бэкСнда с использованиСм FastAPI. Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ я ΠΏΡ€ΠΎΠ΄Π΅ΠΌΠΎΠ½ΡΡ‚Ρ€ΠΈΡ€ΡƒΡŽ эффСктивныС Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π½Ρ‹Π΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ для SwiftUI-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ, Π² частности MVVMP Π² сочСтании с ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ°ΠΌΠΈ SOLID, Dependency Injection (DI), KISS ΠΈ DRY. Для android ΠΊΠΎΠ΄ ΠΌΠΎΠΆΠ½ΠΎ Π»Π΅Π³ΠΊΠΎ пСрСвСсти Π½Π° Kotlin с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Jetpack Compose Framework.

πŸ¦…βš‘ Π”Π΅Π»Π°Π΅ΠΌ Π½Π°Ρ‚ΠΈΠ²Π½ΠΎΠ΅ мобильноС ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ с ИИ ΠΈ бэкСндом

Π’ этой ΡΡ‚Π°Ρ‚ΡŒΠ΅ ΠΌΡ‹ рассмотрим всС тонкости создания Proof of Concept (PoC) мобильного прилоТСния, построСнного с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠ° SwiftUI ΠΈ бэкСнда с использованиСм FastAPI. Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ я ΠΏΡ€ΠΎΠ΄Π΅ΠΌΠΎΠ½ΡΡ‚Ρ€ΠΈΡ€ΡƒΡŽ эффСктивныС Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π½Ρ‹Π΅ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ для SwiftUI-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ, Π² частности MVVMP Π² сочСтании с ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΠ°ΠΌΠΈ SOLID ΠΈ Dependency Injection (DI). Для android ΠΊΠΎΠ΄ ΠΌΠΎΠΆΠ½ΠΎ Π»Π΅Π³ΠΊΠΎ пСрСвСсти Π½Π° Kotlin с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Jetpack Compose Framework.

Π—Π°Ρ‡Π΅ΠΌ Π½Π°ΠΌ Π½ΡƒΠΆΠ΅Π½ бэкСнд

ΠšΡ‚ΠΎ-Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠΊΠ°Π·Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΌΠΎΠΆΠ½ΠΎ просто Π·Π°ΠΏΠΈΡ…Π½ΡƒΡ‚ΡŒ всю Π»ΠΎΠ³ΠΈΠΊΡƒ Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅, Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ запросы Π² chatgpt ΠΈ ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π±Π΅Π· бэкСнда. И я согласСн, это Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, Π½ΠΎ бэкСнд Π΄Π°Π΅Ρ‚ нСсколько Π²Π°ΠΆΠ½Ρ‹Ρ… прСимущСств.

БэкСнд слуТит основой для любого слоТного прилоТСния, особСнно для Ρ‚Π΅Ρ…, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ бСзопасного управлСния Π΄Π°Π½Π½Ρ‹ΠΌΠΈ, ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ ΠΈ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΈ сСрвисов. Π’ΠΎΡ‚ ΠΏΠΎΡ‡Π΅ΠΌΡƒ Π½Π°Π΄Π΅ΠΆΠ½Ρ‹ΠΉ бэкэнд ΠΈΠΌΠ΅Π΅Ρ‚ Ρ€Π΅ΡˆΠ°ΡŽΡ‰Π΅Π΅ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅:

  1. Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ: Бэкэнд ΠΏΠΎΠΌΠΎΠ³Π°Π΅Ρ‚ Π·Π°Ρ‰ΠΈΡ‚ΠΈΡ‚ΡŒ ΠΊΠΎΠ½Ρ„ΠΈΠ΄Π΅Π½Ρ†ΠΈΠ°Π»ΡŒΠ½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅ ΠΈ Ρ‚ΠΎΠΊΠ΅Π½Ρ‹ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΎΡ‚ Π°Ρ‚Π°ΠΊ Ρ‚ΠΈΠΏΠ° MITM (Man-in-the-Middle). Он выступаСт Π² качСствС Π·Π°Ρ‰ΠΈΡ‰Π΅Π½Π½ΠΎΠ³ΠΎ шлюза ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠΌ устройством ΠΈ Π±Π°Π·ΠΎΠΉ Π΄Π°Π½Π½Ρ‹Ρ… ΠΈΠ»ΠΈ внСшними слуТбами, обСспСчивая ΡˆΠΈΡ„Ρ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΡŽ всСх Π΄Π°Π½Π½Ρ‹Ρ….
  2. ΠšΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΡŒ Π½Π°Π΄ использованиСм сСрвисов: Управляя API ΠΈ взаимодСйствиСм с ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡΠΌΠΈ Ρ‡Π΅Ρ€Π΅Π· бэкэнд, Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Ρ‚ΡŒ ΠΈ ΠΊΠΎΠ½Ρ‚Ρ€ΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ использованиС прилоТСния. Π­Ρ‚ΠΎ Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ Π² сСбя дроссСлированиС для управлСния Π½Π°Π³Ρ€ΡƒΠ·ΠΊΠΎΠΉ, ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π΅Π½ΠΈΠ΅ Π·Π»ΠΎΡƒΠΏΠΎΡ‚Ρ€Π΅Π±Π»Π΅Π½ΠΈΠΉ ΠΈ обСспСчСниС эффСктивного использования рСсурсов.
  3. Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с Π±Π°Π·ΠΎΠΉ Π΄Π°Π½Π½Ρ‹Ρ…: Бэкэнд обСспСчиваСт Π±Π΅ΡΡˆΠΎΠ²Π½ΡƒΡŽ ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡŽ с Π±Π°Π·Π°ΠΌΠΈ Π΄Π°Π½Π½Ρ‹Ρ…, позволяя динамичСски Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ, ΠΈΠ·Π²Π»Π΅ΠΊΠ°Ρ‚ΡŒ ΠΈ ΠΎΠ±Π½ΠΎΠ²Π»ΡΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ Π² Ρ€Π΅ΠΆΠΈΠΌΠ΅ Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠ³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ. Π­Ρ‚ΠΎ Π²Π°ΠΆΠ½ΠΎ для ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Ρ‚Ρ€Π΅Π±ΡƒΡŽΡ‚ ΡƒΡ‡Π΅Ρ‚Π½Ρ‹Ρ… записСй ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ, хранят ΠΈΡ… прСдпочтСния ΠΈΠ»ΠΈ Π½ΡƒΠΆΠ΄Π°ΡŽΡ‚ΡΡ Π² быстром ΠΈ бСзопасном ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ Π±ΠΎΠ»ΡŒΡˆΠΈΡ… объСмов Π΄Π°Π½Π½Ρ‹Ρ….
  4. МодСли подписки ΠΈ Freemium: РСализация услуг ΠΏΠΎ подпискС ΠΈΠ»ΠΈ ΠΌΠΎΠ΄Π΅Π»ΠΈ freemium Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ наличия бэкСнда для выставлСния счСтов, отслСТивания использования ΠΈ управлСния уровнями ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ. Бэкэнд ΠΌΠΎΠΆΠ΅Ρ‚ бСзопасно ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Ρ‚ΡŒ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠΈ ΠΈ подписки, обСспСчивая Π±Π΅ΡΠΏΠ΅Ρ€Π΅Π±ΠΎΠΉΠ½ΡƒΡŽ Ρ€Π°Π±ΠΎΡ‚Ρƒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΈ соблюдая трСбования ΠΏΠΎ Π·Π°Ρ‰ΠΈΡ‚Π΅ Π΄Π°Π½Π½Ρ‹Ρ….
  5. ΠœΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΡƒΠ΅ΠΌΠΎΡΡ‚ΡŒ ΠΈ обслуТиваниС: Бэкэнд позволяСт Π±ΠΎΠ»Π΅Π΅ эффСктивно ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅. Π›ΠΎΠ³ΠΈΠΊΡƒ Π½Π° сторонС сСрвСра ΠΌΠΎΠΆΠ½ΠΎ ΠΎΠ±Π½ΠΎΠ²Π»ΡΡ‚ΡŒ Π±Π΅Π· нСобходимости ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ обновлСния ΠΊΠ»ΠΈΠ΅Π½Ρ‚Ρƒ, Ρ‡Ρ‚ΠΎ ΡƒΠΏΡ€ΠΎΡ‰Π°Π΅Ρ‚ обслуТиваниС ΠΈ ускоряСт Π²Π½Π΅Π΄Ρ€Π΅Π½ΠΈΠ΅ Π½ΠΎΠ²Ρ‹Ρ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ.

По сути, бэкСнд β€” это Π½Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ, Π½ΠΎ ΠΈ созданиС бСзопасной, ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΡƒΠ΅ΠΌΠΎΠΉ ΠΈ устойчивой срСды для процвСтания вашСго прилоТСния.

πŸ“± Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° мобильного Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°
Π‘ΠΎΠ»ΡŒΡˆΠ΅ ΠΏΠΎΠ»Π΅Π·Π½Ρ‹Ρ… ΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»ΠΎΠ² Π²Ρ‹ Π½Π°ΠΉΠ΄Π΅Ρ‚Π΅ Π½Π° нашСм Ρ‚Π΅Π»Π΅Π³Ρ€Π°ΠΌ-ΠΊΠ°Π½Π°Π»Π΅ Β«Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° мобильного Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ°Β»

ОбъяснСниС тСхничСского стСка

  1. SwiftUI: Π›ΡƒΡ‡ΡˆΠΈΠΉ Π²Π°Ρ€ΠΈΠ°Π½Ρ‚ для Π½Π°Ρ‚ΠΈΠ²Π½Ρ‹Ρ… ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ для iOS послС Π²Ρ‹Ρ…ΠΎΠ΄Π° UIKit. Он Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ‚ΠΈΠ²Π΅Π½ ΠΈ упорядочСн, Π° XCode являСтся Π½Π΅Π·Π°ΠΌΠ΅Π½ΠΈΠΌΡ‹ΠΌ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΎΡ€ΠΎΠΌ благодаря эпл. Для android ΠΊΠΎΠ΄ ΠΌΠΎΠΆΠ½ΠΎ Π»Π΅Π³ΠΊΠΎ пСрСвСсти Π½Π° Kotlin с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Jetpack Compose.
  2. FastAPI: Π’Ρ‹Π±Ρ€Π°Π½ для бэкСнда Π·Π° Π΅Π³ΠΎ ΡΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ, минимальноС количСство шаблонов ΠΈ Π΄Π΅ΠΊΠ»Π°Ρ€Π°Ρ‚ΠΈΠ²Π½ΠΎΡΡ‚ΡŒ, рСдактируСтся с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ прСвосходного Zed.dev.
  3. ChatGPT API: Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π² качСствС большой языковой ΠΌΠΎΠ΄Π΅Π»ΠΈ (LLM); Π²Ρ‹Π±ΠΎΡ€ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΌΠ΅Π½ΡΡ‚ΡŒΡΡ Π² зависимости ΠΎΡ‚ нСобходимости кастомизации.
  4. Ngrok: Π Π΅Π°Π»ΠΈΠ·ΡƒΠ΅Ρ‚ Ρ‚ΡƒΠ½Π½Π΅Π»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ простой ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ CLI для Π²Ρ‹Ρ…ΠΎΠ΄Π° локального сСрвСра Π² ΠΈΠ½Ρ‚Π΅Ρ€Π½Π΅Ρ‚.

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ прилоТСния для iOS

ВСория: АрхитСктурныС ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹


1. MVVMP (Model View ViewModel Presenter):

  • Model: ΠŸΡ€Π΅Π΄ΡΡ‚Π°Π²Π»ΡΠ΅Ρ‚ собой структуры Π΄Π°Π½Π½Ρ‹Ρ…, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Π΅ Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ, Ρ‚Π°ΠΊΠΈΠ΅ ΠΊΠ°ΠΊ Question, Answer, Questionary ΠΈ FilledQuestionary. Π­Ρ‚ΠΈ ΠΌΠΎΠ΄Π΅Π»ΠΈ просты ΠΈ содСрТат Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π΄Π°Π½Π½Ρ‹Π΅, слСдуя ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΡƒ KISS.
  • View: ΠžΡ‚Π²Π΅Ρ‡Π°ΡŽΡ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π·Π° прСдставлСниС ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ интСрфСйса ΠΈ Π΄Π΅Π»Π΅Π³ΠΈΡ€ΡƒΡŽΡ‚ всС Π΄Π°Π½Π½Ρ‹Π΅ ΠΈ Π»ΠΎΠ³ΠΈΠΊΡƒ ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€Π°ΠΌ. Они Π½Π΅ содСрТат Π½ΠΈΠΊΠ°ΠΊΠΎΠΉ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ ΠΈ спроСктированы Ρ‚Π°ΠΊ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π±Ρ‹Ρ‚ΡŒ простыми ΠΈ сосрСдоточСнными Π½Π° Ρ€Π΅Π½Π΄Π΅Ρ€Π΅ UI.
  • ViewModel: Π’ SwiftUI ViewModel прСдставлСна ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠΌ ObservableObject, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ слуТит модСлью наблюдСния Π·Π° измСняСмыми Π΄Π°Π½Π½Ρ‹ΠΌΠΈ. Π—Π΄Π΅ΡΡŒ Π½Π΅Ρ‚ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² ΠΈ Π»ΠΎΠ³ΠΈΠΊΠΈ.
  • Presenter: Presenter управляСт всСй Π»ΠΎΠ³ΠΈΠΊΠΎΠΉ, связанной с ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΌ (экраном ΠΈΠ»ΠΈ прСдставлСниСм), Π½ΠΎ Π½Π΅ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΎΠΉ. Он взаимодСйствуСт с Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹ΠΌ слоСм для выполнСния ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ, Ρ‚Π°ΠΊΠΈΡ… ΠΊΠ°ΠΊ взаимодСйствиС с API ΠΈΠ»ΠΈ ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ сохранСниСм Π΄Π°Π½Π½Ρ‹Ρ….
  • Domain Layer: Π­Ρ‚ΠΎΡ‚ слой содСрТит бизнСс-Π»ΠΎΠ³ΠΈΠΊΡƒ прилоТСния ΠΈ взаимодСйствуСт с внСшними рСсурсами, Ρ‚Π°ΠΊΠΈΠΌΠΈ ΠΊΠ°ΠΊ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ…, API ΠΈΠ»ΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ сСрвисы. Он состоит ΠΈΠ· Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ΠΎΠ², Ρ‚Π°ΠΊΠΈΡ… ΠΊΠ°ΠΊ сСрвисы, ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹, ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€Ρ‹, Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΈ, ΠΌΠ°ΠΏΠΏΠ΅Ρ€Ρ‹, Ρ„Π°Π±Ρ€ΠΈΠΊΠΈ ΠΈ Ρ‚. Π΄.
  • На самом Π΄Π΅Π»Π΅, MP Π² MVVMP являСтся ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»Π°ΠΌΠΈ ΠœΠ°Ρ€ΠΊΠ° ΠŸΠ°Ρ€ΠΊΠ΅Ρ€Π°, Π° полная Ρ„ΠΎΡ€ΠΌΠ° β€” Β«Model View ViewModel by Mark ParkerΒ».

2. ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏΡ‹ SOLID:

  • ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏ Π΅Π΄ΠΈΠ½ΠΎΠΉ отвСтствСнности: Π£ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ класса Π΄ΠΎΠ»ΠΆΠ½Π° Π±Ρ‹Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Π° ΠΏΡ€ΠΈΡ‡ΠΈΠ½Π° для ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ.
  • ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΎΡΡ‚ΡŒ-Π·Π°ΠΊΡ€Ρ‹Ρ‚ΠΎΡΡ‚ΡŒ: ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Ρ‹ для Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ, Π½ΠΎ Π·Π°ΠΊΡ€Ρ‹Ρ‚Ρ‹ для ΠΌΠΎΠ΄ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ.
  • ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏ замСщСния Лискова: ΠžΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ супСркласса Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ замСняСмы ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°ΠΌΠΈ подклассов.
  • ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏ раздСлСния интСрфСйсов: Ни ΠΎΠ΄ΠΈΠ½ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π²Ρ‹Π½ΡƒΠΆΠ΄Π΅Π½ Π·Π°Π²ΠΈΡΠ΅Ρ‚ΡŒ ΠΎΡ‚ интСрфСйсов, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΎΠ½ Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚.
  • ΠŸΡ€ΠΈΠ½Ρ†ΠΈΠΏ инвСрсии зависимостСй: Π—Π°Π²ΠΈΡΠΈΠΌΠΎΡΡ‚ΡŒ ΠΎΡ‚ абстракций, Π° Π½Π΅ ΠΎΡ‚ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚ΠΈΠΊΠΈ, Ρ‡Π΅ΠΌΡƒ способствуСт DI.

3. Π˜Π½ΡŠΠ΅ΠΊΡ†ΠΈΡ зависимостСй (DI):

  • ΠŸΡ€ΠΈ использовании ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Π° Dependency Injection зависимости ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²Π»ΡΡŽΡ‚ΡΡ ΠΈΠ·Π²Π½Π΅ класса, Π° Π½Π΅ ΠΈΠ½ΡΡ‚Π°Π½Ρ†ΠΈΡ€ΡƒΡŽΡ‚ΡΡ Π²Π½ΡƒΡ‚Ρ€ΠΈ Π½Π΅Π³ΠΎ. Π­Ρ‚ΠΎ способствуСт развязкС ΠΈ позволяСт ΡƒΠΏΡ€ΠΎΡΡ‚ΠΈΡ‚ΡŒ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΡƒ ΠΈ тСстированиС ΠΊΠΎΠ΄Π°.

Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° бэкСнда

Код бэкСнда довольно прост. Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚Ρ‹ (main.py):

from typing import Callable
import json
from fastapi import FastAPI, Body, Request, Response
from .models import (Question, FilledQuestionary, DoctorResponseAnswer, DoctorResponseQuestionary)
from .user_card import UserCardSimple
from .prompting import get_response


@app.get("/onboarding", response_model = DoctorResponseQuestionary)
def onboarding():
    return DoctorResponseQuestionary(question=[Question(text=text) for text in UserCardSimple.__fields__.keys()])

@app.post("/doctor")
def doctor(user_card: UserCardSimple, filled_questionary: FilledQuestionary, message: str = Body(...)):
    json_string = get_response(user_card, message, filled_questionary) 
    loaded = json.loads(json_string.strip())
    return loaded

"onboarding" прСдоставляСт список вопросов Π°Π½Π°ΠΌΠ½Π΅Π·Π°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Π·Π°ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π²ΠΎΠΌ запускС прилоТСния. ΠžΡ‚Π²Π΅Ρ‚Ρ‹ Π±ΡƒΠ΄ΡƒΡ‚ сохранСны Π½Π° устройствС ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Π½Ρ‹ для пСрсонализированной диагностики Π² Π±ΡƒΠ΄ΡƒΡ‰Π΅ΠΌ. "doctor" β€” основной эндпоинт: ΠΎΠ½ Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅Ρ‚ вопросы Π½Π° основС ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰ΠΈΡ… ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² ΠΈ ΠΊΠ°Ρ€Ρ‚Ρ‹ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ, Π»ΠΈΠ±ΠΎ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ диагностики.

МодСли:

from pydantic import BaseModel


class Question(BaseModel):
    text: str

class FilledQuestionary(BaseModel):
    filled_questions: dict[str, str]

class DoctorResponseAnswer(BaseModel):
    text: str

class DoctorResponseQuestionary(BaseModel):
    questions: list[str]

class UserCardSimple(BaseModel):
    sex: str
    age: int
    weight: int
    height: int
    special_conditions: str

ΠŸΡ€ΠΎΠΌΠΏΡ‚Ρ‹:

import os
from openai import OpenAI
from .models import FilledQuestionary


api_key = os.environ.get("API_KEY")
client = OpenAI(api_key=api_key)

def get_response(user_card: str, message: str, filled_questionary: FilledQuestionary, max_tokens=200):
    format_question = """{"questions":[{"text":"first question"},{"text":"second question"}]}"""
    format_advice = """{"text":"Advice: Drink more water"}"""
    
    system_prompt = f"""
    You are a doctor that gives user an opportunity to swiftly check up health and diagnos an illness using anamnes and a short questionary. 
    Your task is to ask short questions and give your opinion and advices.
    Your questions are accamulated in the filled questionary, which is empty in the first itteration. 
    
    Strive to about 1-2 questions per iteration and up to 6 questions in total (can be less). Questions must be short, clear, shouldn't repeat, 
    and should be relevant to the user's health condition, and should require easy answers.
    Ask questions only in the json format {format_question}.

    Number of answered questions: {len(filled_questionary.filled_questions)}
    If the Number of answered questions is more then 6, you should stop asking questions an`d provide an give your final opinion, 
    an assumption or an advice in the json format {format_advice}.
    """
    
    prompt = f"""request message: {message}; anamnesis: {user_card}; filled questionary: {filled_questionary};"""

    chat_completion = client.chat.completions.create(
        messages=[
            {
                "role": "system",
                "content": f"{system_prompt}",
            },
            {
                "role": "user",
                "content": f"{prompt}",
            },
        ],
        model="gpt-3.5-turbo",
        max_tokens=max_tokens
    )
    return chat_completion.choices[0].message.content

ΠœΠΎΠ΄ΡƒΠ»ΡŒ ΠΏΡ€ΠΎΠΌΠΏΡ‚ΠΎΠ² ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ GPT-3.5 ΠΎΡ‚ OpenAI для Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² Π½Π° основС ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠ³ΠΎ Π²Π²ΠΎΠ΄Π°, Π°Π½Π°ΠΌΠ½Π΅Π·Π° ΠΈ Π·Π°ΠΏΠΎΠ»Π½Π΅Π½Π½Ρ‹Ρ… Π°Π½ΠΊΠ΅Ρ‚. Он Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŽ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ вопросы ΠΈ совСты ΠΏΠΎ диагностикС Π·Π΄ΠΎΡ€ΠΎΠ²ΡŒΡ. Как Π²ΠΈΠ΄ΠΈΡ‚Π΅, Π½ΠΈΡ‡Π΅Π³ΠΎ слоТного здСсь Π½Π΅Ρ‚. Код элСмСнтарСн, Π° ΠΏΡ€ΠΎΠΌΠΏΡ‚Ρ‹ – просто Π½Π°Π±ΠΎΡ€ Ρ‡Π΅Ρ‚ΠΊΠΈΡ… инструкций для LLM.

НастройтС ΠΎΠΊΡ€ΡƒΠΆΠ΅Π½ΠΈΠ΅ ΠΈ запуститС сСрвСр с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ fastapi dev main.py.

ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΡΡ‚ΠΈ:

  1. fastapi.tiangolo.com/tutorial/first-steps
  2. pypi.org/project/openai/

ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ доступа ΠΊ Π»ΠΎΠΊΠ°Π»ΡŒΠ½ΠΎΠΌΡƒ хосту Ρ‡Π΅Ρ€Π΅Π· Π˜Π½Ρ‚Π΅Ρ€Π½Π΅Ρ‚

  1. Π—Π°Ρ€Π΅Π³ΠΈΡΡ‚Ρ€ΠΈΡ€ΡƒΠΉΡ‚Π΅ΡΡŒ Π½Π° сайтС ngrok.com ΠΈ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚Π΅ Ρ‚ΠΎΠΊΠ΅Π½ доступа.
  2. УстановитС ngrok с сайта ngrok.com/download.
  3. Π’Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚Π΅ ΠΊΠΎΠΌΠ°Π½Π΄Ρƒ ngrok config add-authtoken <TOKEN>.
  4. ЗапуститС с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ ngrok http http://localhost:8080 (ΠΏΡ€ΠΈ нСобходимости ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚Π΅ ΠΏΠΎΡ€Ρ‚).

ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½Ρ‹Π΅ инструкции ΠΏΠΎ настройкС ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΠΉΡ‚ΠΈ Π² Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ ngrok.

Кодим ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅

Π― Π½Π΅ Π±ΡƒΠ΄Ρƒ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ здСсь вСсь исходный ΠΊΠΎΠ΄, для этого Π΅ΡΡ‚ΡŒ GitHub. Найти Π΅Π³ΠΎ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎ адрСсу: HouseMDAI iOS App. ВмСсто этого я ΠΎΡΡ‚Π°Π½ΠΎΠ²Π»ΡŽΡΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π° Π²Π°ΠΆΠ½Ρ‹Ρ… (IMO) ΠΌΠΎΠΌΠ΅Π½Ρ‚Π°Ρ….

НачнСм с ΠΊΡ€Π°Ρ‚ΠΊΠΎΠ³ΠΎ описания Π·Π°Π΄Π°Ρ‡ΠΈ: Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ с тСкстовым ΠΏΠΎΠ»Π΅ΠΌ Π½Π° Π³Π»Π°Π²Π½ΠΎΠΌ экранС, Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒΡŽ Π·Π°Π΄Π°Π²Π°Ρ‚ΡŒ Π½Π°Π±ΠΎΡ€ динамичСских вопросов ΠΈ ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ ΠΎΡ‚Π²Π΅Ρ‚. Π’Π°ΠΊΠΆΠ΅ Π½Π°ΠΌ Π½ΡƒΠΆΠ΅Π½ ΠΎΠ΄Π½ΠΎΡ€Π°Π·ΠΎΠ²Ρ‹ΠΉ ΠΎΠ½Π±ΠΎΡ€Π΄ΠΈΠ½Π³. Π˜Ρ‚Π°ΠΊ, приступим ΠΊ ΠΊΠΎΠ΄Ρƒ.

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ Π½Π°ΠΌ Π½ΡƒΠΆΠ½Ρ‹ ΠΌΠΎΠ΄Π΅Π»ΠΈ, ΠΈ ΠΎΠ½ΠΈ довольно просты (ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏ KISS).

struct Question {
    var text: String
}

struct Answer {
    var text: String
}

struct Questionary {
    var questions: [Question]
}

struct FilledQuestionary {
    var filledQuestions: [String: String]
}

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π²Π°ΠΉΡ‚Π΅ сдСлаСм ΠΎΠ½Π±ΠΎΡ€Π΄ΠΈΠ½Π³. ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠ°Π΅ΠΌ ΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚ΡŒ KISS ΠΈ SRP (Single Responsibility Principle), Π½ΠΈΠΊΠ°ΠΊΠΎΠΉ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ Π² прСдставлСниях, Ρ‚ΠΎΠ»ΡŒΠΊΠΎ UI. Π’ Π΄Π°Π½Π½ΠΎΠΌ случаС – Ρ‚ΠΎΠ»ΡŒΠΊΠΎ список вопросов с ΠΏΡ€ΠΎΠΊΡ€ΡƒΡ‚ΠΊΠΎΠΉ. ВсС Π΄Π°Π½Π½Ρ‹Π΅ ΠΈ Π»ΠΎΠ³ΠΈΠΊΠ° Π΄Π΅Π»Π΅Π³ΠΈΡ€ΠΎΠ²Π°Π½Ρ‹ ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€Ρƒ. ЕдинствСнноС, Ρ‡Ρ‚ΠΎ здСсь интСрСсно, это нСбольшой Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΌΠ΅Ρ‚ΠΎΠ΄ bindingForQuestion, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ, вСроятно, Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π² ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€Π΅, Π½ΠΎ сСйчас это Π½Π΅ ΠΈΠΌΠ΅Π΅Ρ‚ значСния.

import SwiftUI

struct OnboardingView: View {
    
    @StateObject var presenter: OnboardingPresenter
    
    var body: some View {
        ScrollView {
            Spacer()
            VStack {
                ForEach(presenter.questions.questions) { question in
                    VStack {
                        Text(question.text)
                        TextField("", text: bindingForQuestion(question))
                            .formItem()
                    }
                    .padding()
                }
            }.padding()
            
            Button("Save", action: presenter.save)
            Spacer()
        }
    }
    
    private func bindingForQuestion(_ question: Question) -> Binding<String> {
        Binding(
            get: { presenter.answers.filledQuestions[question.text] ?? "" },
            set: { presenter.answers.filledQuestions[question.text] = $0 }
        )
    }
}

Π’Ρ‹ Π±ΡƒΠ΄Π΅Ρ‚Π΅ ΡƒΠ΄ΠΈΠ²Π»Π΅Π½Ρ‹, Π½ΠΎ Π² ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€Π΅ Ρ‚Π°ΠΊΠΆΠ΅ Π½Π΅Ρ‚ Π½ΠΈΠΊΠ°ΠΊΠΎΠΉ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ!

class OnboardingPresenter: ObservableObject {

    @Published public var answers: FilledQuestionary
    private(set) public var questions: Questionary
    private var completion: (FilledQuestionary) -> Void
    
    init(questions: Questionary, answers: FilledQuestionary, completion: @escaping (FilledQuestionary) -> Void) {
        self.questions = questions
        self.answers = answers
        self.completion = completion
    }
    
    func save() {
        completion(answers)
    }
}

ВсС ΠΏΠΎ-ΠΏΡ€Π΅ΠΆΠ½Π΅ΠΌΡƒ simple, stupid ΠΈ ΠΈΠΌΠ΅Π΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ ΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²Π΅Π½Π½ΠΎΡΡ‚ΡŒ. Presenter Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π»ΠΎΠ³ΠΈΠΊΡƒ своСго прСдставлСния. БизнСс-Π»ΠΎΠ³ΠΈΠΊΠ° уровня прилоТСния находится Π²Π½Π΅ Π΅Π³ΠΎ ΡŽΡ€ΠΈΡΠ΄ΠΈΠΊΡ†ΠΈΠΈ, поэтому ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€ просто Π΄Π΅Π»Π΅Π³ΠΈΡ€ΡƒΠ΅Ρ‚ Π΅Π΅ Π½Π°Π²Π΅Ρ€Ρ… ΠΏΠΎ стэку Π²Ρ‹Π·ΠΎΠ²Π°.

Π’Π°ΠΊΠΆΠ΅ ΠΌΠΎΠΆΠ½ΠΎ Π·Π°ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΈ View, ΠΈ Presenter Π½Π΅ ΠΈΠ½ΡΡ‚Π°Π½Ρ†ΠΈΡ€ΡƒΡŽΡ‚ Π½ΠΈ ΠΎΠ΄Π½Ρƒ ΠΈΠ· зависимостСй, Π° ΠΏΠΎΠ»ΡƒΡ‡Π°ΡŽΡ‚ ΠΈΡ… Π² качСствС ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ² ΠΏΡ€ΠΈ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ. Π­Ρ‚ΠΎ соотвСтствуСт ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΡƒ инвСрсии зависимостСй, согласно ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ ΠΌΠΎΠ΄ΡƒΠ»ΠΈ высокого уровня Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π·Π°Π²ΠΈΡΠ΅Ρ‚ΡŒ ΠΎΡ‚ ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ Π½ΠΈΠ·ΠΊΠΎΠ³ΠΎ уровня, Π½ΠΎ ΠΎΠ±Π° Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π·Π°Π²ΠΈΡΠ΅Ρ‚ΡŒ ΠΎΡ‚ абстракций. Π­Ρ‚ΠΎ обСспСчиваСт Π³ΠΈΠ±ΠΊΠΎΡΡ‚ΡŒ ΠΈ ΡƒΠΏΡ€ΠΎΡ‰Π°Π΅Ρ‚ тСстированиС, Π° Ρ‚Π°ΠΊΠΆΠ΅ позволяСт Π»Π΅Π³ΠΊΠΎ Π·Π°ΠΌΠ΅Π½ΡΡ‚ΡŒ зависимости ΠΈΠ»ΠΈ Π²Π½Π΅Π΄Ρ€ΡΡ‚ΡŒ ΠΌΠ°ΠΊΠ΅Ρ‚Ρ‹ для Ρ†Π΅Π»Π΅ΠΉ тСстирования.

ΠŸΡ€ΠΈ использовании ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Π° Dependency Injection зависимости ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²Π»ΡΡŽΡ‚ΡΡ ΠΈΠ·Π²Π½Π΅ класса, Π° Π½Π΅ ΠΈΠ½ΡΡ‚Π°Π½Ρ†ΠΈΡ€ΡƒΡŽΡ‚ΡΡ Π²Π½ΡƒΡ‚Ρ€ΠΈ Π½Π΅Π³ΠΎ. Π­Ρ‚ΠΎ способствуСт развязкС ΠΈ позволяСт ΡƒΠΏΡ€ΠΎΡΡ‚ΠΈΡ‚ΡŒ ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΡƒ ΠΈ тСстированиС ΠΊΠΎΠ΄Π°.

Π₯отя Π² Π΄Π°Π½Π½ΠΎΠΌ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Ρ‹ Π½Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ΡΡ явно, стоит ΠΎΡ‚ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ, Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Ρ‹ ΠΌΠΎΠ³ΡƒΡ‚ ΠΈΠ³Ρ€Π°Ρ‚ΡŒ Π²Π°ΠΆΠ½ΡƒΡŽ Ρ€ΠΎΠ»ΡŒ Π² ΠΊΠΎΠ΄Π΅, особСнно для абстрагирования ΠΈ облСгчСния тСстирования. ΠžΠΏΡ€Π΅Π΄Π΅Π»ΠΈΠ² ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Ρ‹ для прСдставлСний, ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π°Ρ‚ΠΎΡ€ΠΎΠ² ΠΈ зависимостСй, становится ΠΏΡ€ΠΎΡ‰Π΅ Π·Π°ΠΌΠ΅Π½ΡΡ‚ΡŒ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΈΠ»ΠΈ ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²Π»ΡΡ‚ΡŒ ΠΌΠΎΠΊΠΈ Π²ΠΎ врСмя тСстирования.

ΠŸΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Ρ‹ Π² SwiftUI View
Если Π²Ρ‹ рассматриваСтС Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ использования ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»ΠΎΠ² Π² прСдставлСниях SwiftUI, Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ ΠΏΠΎΠΌΠ½ΠΈΡ‚ΡŒ ΠΎΠ± ΠΎΠ΄Π½ΠΎΠΌ Π²Π°ΠΆΠ½ΠΎΠΌ ΠΌΠΎΠΌΠ΅Π½Ρ‚Π΅. ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ View Π² SwiftUI – это структура, ΠΎΠ½Π° Ρ‚Ρ€Π΅Π±ΡƒΠ΅Ρ‚ явного указания Ρ‚ΠΈΠΏΠΎΠ² своих свойств. Π­Ρ‚ΠΎ ΠΎΠ·Π½Π°Ρ‡Π°Π΅Ρ‚, Ρ‡Ρ‚ΠΎ Π²Π°ΠΌ придСтся ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ Π΅Ρ‘ Π΄ΠΆΠ΅Π½Π΅Ρ€ΠΈΠΊΠΎΠΌ ΠΈ ΠΏΡ€ΠΎΠ±Ρ€Π°ΡΡ‹Π²Π°Ρ‚ΡŒ Ρ‚ΠΈΠΏ Ρ‡Π΅Ρ€Π΅Π· вСсь стСк Π²Ρ‹Π·ΠΎΠ²ΠΎΠ², Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΈΠ²Π΅Π΄Π΅Ρ‚ ΠΊ появлСнию большого количСства шаблонного ΠΊΠΎΠ΄Π°. Однако сущСствуСт Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π½Ρ‹ΠΉ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄, ΠΏΡ€Π΅Π΄Π»Π°Π³Π°Π΅ΠΌΡ‹ΠΉ github.com/MarkParker5/AnyObservableObject. Π­Ρ‚Π° Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ Π°Π½Π°Π»ΠΎΠ³ΠΈΡ‡Π½ΠΎ Ρ€ΠΎΠ΄Π½Ρ‹ΠΌ ΠΎΠ±Π΅Ρ€Ρ‚ΠΊΠ°ΠΌ свойств SwiftUI, Π½ΠΎ ΡƒΠ±ΠΈΡ€Π°Π΅Ρ‚ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΡƒ Ρ‚ΠΈΠΏΠ° Π²ΠΎ врСмя компиляции Π² ΠΏΠΎΠ»ΡŒΠ·Ρƒ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π²ΠΎ врСмя Ρ€Π°Π½Ρ‚Π°ΠΉΠΌΠ°. Π₯отя Ρ‚Π°ΠΊΠΎΠΉ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ ΠΌΠΎΠΆΠ΅Ρ‚ внСсти Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ риски, ΠΈΡ… Π»Π΅Π³ΠΊΠΎ ΡΠ½ΠΈΠ·ΠΈΡ‚ΡŒ, написав элСмСнтарныС xcode тСсты, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ просто ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΡŽΡ‚ прСдставлСния Ρ‚Π°ΠΊ ΠΆΠ΅, ΠΊΠ°ΠΊ Π²Ρ‹ Π΄Π΅Π»Π°Π΅Ρ‚Π΅ это Π²ΠΎ врСмя Ρ€Π°Π½Ρ‚Π°ΠΉΠΌΠ°. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ эту Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Ρƒ, Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΡƒΠΏΡ€ΠΎΡΡ‚ΠΈΡ‚ΡŒ свой ΠΊΠΎΠ΄ ΠΈ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ процСсс Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠΏΡ€ΠΎΡ‚ΠΎΠΊΠΎΠ»Π°ΠΌΠΈ Π² SwiftUI.

Π˜Ρ‚Π°ΠΊ, Ссли ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€ Π½Π΅ содСрТит бизнСс-Π»ΠΎΠ³ΠΈΠΊΡƒ, Ρ‚ΠΎ Π³Π΄Π΅ ΠΆΠ΅ ΠΎΠ½Π°? Π­Ρ‚ΠΎ Π·Π°Π΄Π°Ρ‡Π° для Π΄ΠΎΠΌΠ΅Π½Π½ΠΎΠ³ΠΎ слоя, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ содСрТит сСрвисы, ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹ ΠΈ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€Ρ‹. Π£ Π½ΠΈΡ… всСх ΠΎΡ‡Π΅Π½ΡŒ схоТСС ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅, ΠΈ Ρ€Π°Π·Π½ΠΈΡ†Π° ΠΌΠ΅ΠΆΠ΄Ρƒ Π½ΠΈΠΌΠΈ Π΄ΠΎ сих ΠΏΠΎΡ€ являСтся ΠΏΡ€Π΅Π΄ΠΌΠ΅Ρ‚ΠΎΠΌ дискуссий. Π”Π°Π²Π°ΠΉΡ‚Π΅ создадим OnboardingProvider, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ всю бизнСс-Π»ΠΎΠ³ΠΈΠΊΡƒ процСсса ΠΎΠ½Π±ΠΎΡ€Π΄ΠΈΠ½Π³Π°.

class OnboardingProvider: ObservableObject {
    
    init() {
        loadFilledOnboardingFromDefaults()
    }
    
    // MARK: Interface
	
	@Published private(set) var needsOnboarding: Bool = true
    
    private(set) var filledOnboarding: FilledQuestionary? {
        didSet {
            if let filledOnboarding {
                saveFilledOnboardingToDefaults(filledQuestionary: filledOnboarding)
            }
        }
    }
    
    func getOnboardingQuestionary() -> Questionary {
        // NOTE: it's better to take the questions from the backend
        Questionary(questions: [
            Question(text: "sex"),
            Question(text: "age"),
            Question(text: "weight"),
            Question(text: "height"),
            Question(text: "special_conditions"),
        ])
    }
    
    func saveOnboardingAnswers(filledQuestionary: FilledQuestionary) {
        needsOnboarding = false
        filledOnboarding = filledQuestionary
    }
    
    // MARK: - Private
    
    private func saveFilledOnboardingToDefaults(filledQuestionary: FilledQuestionary) {
        UserDefaults.standard.removeObject(forKey: "filledOnboarding")
        let encoder = JSONEncoder()
        let encoded = try! encoder.encode(filledQuestionary)
        UserDefaults.standard.set(encoded, forKey: "filledOnboarding")
    }
    
    private func loadFilledOnboardingFromDefaults() {
        guard let object = UserDefaults.standard.object(forKey: "filledOnboarding") else {
            needsOnboarding = true
            return
        }
        let savedFilledQuestionary = object as! Data
        let decoder = JSONDecoder()
        let loadedQuestionary = try! decoder.decode(FilledQuestionary.self, from: savedFilledQuestionary)
        self.filledOnboarding = loadedQuestionary
        self.needsOnboarding = false
    }
}

ΠžΠΏΡΡ‚ΡŒ ΠΆΠ΅, ΠΎΠ½ выполняСт Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄Π½Ρƒ Π·Π°Π΄Π°Ρ‡Ρƒ: ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΎΠΉ процСсса onboarding. Вакая инкапсуляция позволяСт Π΄Ρ€ΡƒΠ³ΠΈΠΌ классам Π²Π·Π°ΠΈΠΌΠΎΠ΄Π΅ΠΉΡΡ‚Π²ΠΎΠ²Π°Ρ‚ΡŒ с Π½ΠΈΠΌ Π±Π΅Π· нСобходимости Π±Π΅ΡΠΏΠΎΠΊΠΎΠΈΡ‚ΡŒΡΡ ΠΎ дСталях Π΅Π³ΠΎ Π²Π½ΡƒΡ‚Ρ€Π΅Π½Π½Π΅ΠΉ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ, Ρ‡Ρ‚ΠΎ способствуСт созданию Π±ΠΎΠ»Π΅Π΅ чистой ΠΈ ΡƒΠ΄ΠΎΠ±Π½ΠΎΠΉ ΠΊΠΎΠ΄ΠΎΠ²ΠΎΠΉ Π±Π°Π·Ρ‹.

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π²Π°ΠΉΡ‚Π΅ собСрСм всС вмСстС Π² ΠΊΠΎΡ€Π½Π΅ прилоТСния.

import SwiftUI

@main
struct HouseMDAI: App {
    
    @StateObject private var onboardingProvider: OnboardingProvider
    @StateObject private var onboardingPresenter: OnboardingPresenter
    @StateObject private var homePresenter: HomePresenter
    
    init() {
        let onboardingProvider = OnboardingProvider()
        
        let onboardingPresenter = OnboardingPresenter(
            questions: onboardingProvider.getOnboardingQuestionary(),
            answers: FilledQuestionary(filledQuestions: [:]),
            completion: onboardingProvider.saveOnboardingAnswers
        )
        
        let homePresenter = HomePresenter()
        
        _onboardingProvider = StateObject(wrappedValue: onboardingProvider)
        _onboardingPresenter = StateObject(wrappedValue: onboardingPresenter)
        _homePresenter = StateObject(wrappedValue: homePresenter)
    }
    
    var body: some Scene {
        WindowGroup {
            if onboardingProvider.needsOnboarding {
                OnboardingView(presenter: onboardingPresenter)
            } else {
                TabView {
                    HomeView(presenter: homePresenter)
                    
                    if let profile = onboardingProvider.filledOnboarding {
                        ProfileView(profile: profile)
                    }
                }
            }
        }
    } // body
}

Π­Ρ‚ΠΎ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ SwiftUI устанавливаСт своС Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎΠ΅ состояниС с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΎΠ±Π΅Ρ€Ρ‚ΠΎΠΊ ΠΏΠΎΠ»Π΅ΠΉ StateObject. Оно ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ OnboardingProvider, OnboardingPresenter ΠΈ HomePresenter Π² своСм ΠΌΠ΅Ρ‚ΠΎΠ΄Π΅ init. ΠŸΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ OnboardingProvider ΠΎΡ‚Π²Π΅Ρ‡Π°Π΅Ρ‚ Π·Π° ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹ΠΌΠΈ, связанными с ΠΎΠ½Π±ΠΎΡ€Π΄ΠΈΠ½Π³ΠΎΠΌ, Π° OnboardingPresenter управляСт Π»ΠΎΠ³ΠΈΠΊΠΎΠΉ прСдставлСния ΠΎΠ½Π±ΠΎΡ€Π΄ΠΈΠ½Π³Π°. HomePresenter управляСт Π³Π»Π°Π²Π½Ρ‹ΠΌ домашним прСдставлСниСм.

Π’ Ρ‚Π΅Π»Π΅ сцСны прилоТСния провСряСтся, Π½ΡƒΠΆΠ½Π° Π»ΠΈ рСгистрация Π½Π° сайтС. Если Π΄Π°, Ρ‚ΠΎ ΠΎΠ½Π° прСдставляСт OnboardingView с OnboardingPresenter. Π’ ΠΏΡ€ΠΎΡ‚ΠΈΠ²Π½ΠΎΠΌ случаС ΠΎΠ½Π° прСдставляСт TabView, содСрТащий HomeView с HomePresenter ΠΈ, Ссли доступно, ProfileView.

Π’Π΅ΠΏΠ΅Ρ€ΡŒ настало врСмя для домашнСго прСдставлСния. Π›ΠΎΠ³ΠΈΠΊΠ° проста:

  1. ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ сообщСниС ΠΎΡ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ.
  2. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ сообщСниС, Π·Π°ΠΏΡ€Π°ΡˆΠΈΠ²Π°Π΅ΠΌ список вопросов ΠΈΠ· бэкСнда.
  3. ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ вопросы ΠΏΠΎ ΠΎΠ΄Π½ΠΎΠΌΡƒ, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ Π²ΡΡ‚Ρ€ΠΎΠ΅Π½Π½ΡƒΡŽ push-Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΡŽ.
  4. ДобавляСм ΠΎΡ‚Π²Π΅Ρ‚Ρ‹ ΠΊ запросу ΠΈ повторяСм 2-4, ΠΏΠΎΠΊΠ° бэкСнд-Π΄ΠΎΠΊΡ‚ΠΎΡ€ Π½Π΅ Π²Π΅Ρ€Π½Π΅Ρ‚ ΠΎΠΊΠΎΠ½Ρ‡Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚.
  5. ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Π΅ΠΌ Ρ„ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚.
struct HomeView: View {
    
    @StateObject var presenter: HomePresenter
    
    var body: some View {
        NavigationStack(path: $presenter.navigationPath) {
            VStack {
	            // 1
                Text("How are you?")
                TextField("...", text: $presenter.message)
                    .lineLimit(5...10)
                    .formItem()
					
				// 2
                Button("Send", action: presenter.onSend)
            }
            .padding()
            .navigationDestination(for: NavigationPage.self) { page in
                switch page {
                case .questinary(let questions, let answers):
	                // 3
                    QuestionaryView(
                        presenter: QuestionaryPresenter(
                            questions: questions,
                            answers: answers,
                            completion: presenter.onQuestionaryFilled
                        )
                    )
                case .answer(let string):
	                // 5
                    VStack {
                        Text("The doctor says...")
                        Text(string)
                            .font(.title2)
                            .padding()
                    }
                }
            }
        }
    }
}

ΠŸΠΎΡ…ΠΎΠΆΠ΅, я пропустил 4-ΠΉ ΠΏΡƒΠ½ΠΊΡ‚... ΠΈΠ»ΠΈ Π½Π΅Ρ‚? ΠŸΠΎΡΠΊΠΎΠ»ΡŒΠΊΡƒ прСдставлСниС Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Π½ΠΈΠΊΠ°ΠΊΠΎΠΉ Π»ΠΎΠ³ΠΈΠΊΠΈ, эту Ρ‡Π°ΡΡ‚ΡŒ выполняСт Π΅Π³ΠΎ ΠΏΡ€Π΅Π·Π΅Π½Ρ‚Π΅Ρ€.

enum NavigationPage: Hashable {
    case questinary(Questionary, FilledQuestionary)
    case answer(String)
}

class HomePresenter: ObservableObject {
    
    @Published var message: String = ""
    @Published var navigationPath: [NavigationPage] = []
    
    init(message: String = "") {
        self.message = message
    }
    
    func onSend() {
        Task {
            let doctor = DoctorProvider()
            let answer = try! await doctor.sendMessage(message: message)
            
            switch answer {
            case .questions(let questions):
                navigationPath.append(.questinary(questions, FilledQuestionary(filledQuestions: [:])))
            case .answer(let string):
                navigationPath.append(.answer(string))
            }
        }
    }
    
    func onQuestionaryFilled(filled: FilledQuestionary) {
        Task {
            let doctor = DoctorProvider()
            let answer = try! await doctor.sendAnswers(message: message, answers: filled)
            
            switch answer {
            case .questions(let newQuestions):
                navigationPath.append(.questinary(newQuestions, filled))
            case .answer(let string):
                navigationPath.append(.answer(string))
            }
        }
    }
}

Он управляСт Π²Π²ΠΎΠ΄ΠΎΠΌ сообщСния ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΌ ΠΈ обновляСт ΠΏΡƒΡ‚ΡŒ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ Π½Π° основС ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² ΠΎΡ‚ бэкСнда.

ΠŸΡ€ΠΈ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠ΅ сообщСния ΠΌΠ΅Ρ‚ΠΎΠ΄ onSend() отправляСт Π΅Π³ΠΎ Π½Π° бэкСнд с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ DoctorProvider ΠΈ ΠΎΠΆΠΈΠ΄Π°Π΅Ρ‚ ΠΎΡ‚Π²Π΅Ρ‚Π°. Π’ зависимости ΠΎΡ‚ Ρ‚ΠΈΠΏΠ° ΠΎΡ‚Π²Π΅Ρ‚Π° ΠΎΠ½ обновляСт Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΎΠ½Π½Ρ‹ΠΉ ΠΏΡƒΡ‚ΡŒ, отобраТая Π»ΠΈΠ±ΠΎ Π½Π°Π±ΠΎΡ€ вопросов, Π»ΠΈΠ±ΠΎ ΠΎΠΊΠΎΠ½Ρ‡Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚.

Аналогично, ΠΊΠΎΠ³Π΄Π° заполняСтся вопросник, ΠΌΠ΅Ρ‚ΠΎΠ΄ onQuestionaryFilled() отправляСт Π·Π°ΠΏΠΎΠ»Π½Π΅Π½Π½Ρ‹ΠΉ вопросник Π½Π° бэкСнд ΠΈ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ обновляСт ΠΏΡƒΡ‚ΡŒ Π½Π°Π²ΠΈΠ³Π°Ρ†ΠΈΠΈ.

Π—Π΄Π΅ΡΡŒ Π΅ΡΡ‚ΡŒ нСбольшоС Π΄ΡƒΠ±Π»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΌΠ΅Ρ‚ΠΎΠ΄Π°ΠΌΠΈ onSend() ΠΈ onQuestionaryFilled(), ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ Π±Ρ‹ ΠΎΡ‚Ρ€Π΅Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π² ΠΎΠ΄ΠΈΠ½ ΠΌΠ΅Ρ‚ΠΎΠ΄ для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΎΠ±ΠΎΠΈΡ… случаСв. Однако оставим это ΠΊΠ°ΠΊ ΡƒΠΏΡ€Π°ΠΆΠ½Π΅Π½ΠΈΠ΅ для дальнСйшСй Π΄ΠΎΡ€Π°Π±ΠΎΡ‚ΠΊΠΈ.

ΠœΠΎΠ΄ΡƒΠ»ΡŒ Questionary (View+Presenter) ΠΏΠΎΡ‡Ρ‚ΠΈ являСтся ΠΊΠΎΠΏΠΈΠ΅ΠΉ Onboarding ΠΈ просто Π΄Π΅Π»Π΅Π³ΠΈΡ€ΡƒΠ΅Ρ‚ Π»ΠΎΠ³ΠΈΠΊΡƒ Π΄ΠΎ HomePresenter, поэтому я Π½Π΅ Π²ΠΈΠΆΡƒ нСобходимости ΠΏΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ ΠΊΠΎΠ΄. ΠžΠΏΡΡ‚ΡŒ ΠΆΠ΅, для этого Π΅ΡΡ‚ΡŒ github.

ПослСднСС, Ρ‡Ρ‚ΠΎ я Ρ…ΠΎΡ‡Ρƒ ΠΏΠΎΠΊΠ°Π·Π°Ρ‚ΡŒ, это Π΄Π²Π΅ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ DoctorProvider, СдинствСнной ΠΎΠ±ΡΠ·Π°Π½Π½ΠΎΡΡ‚ΡŒΡŽ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… являСтся Π²Ρ‹Π·ΠΎΠ² API ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‚ DoctorResponse. ΠŸΠ΅Ρ€Π²Π°Ρ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ наш бэкСнд.

import Alamofire

enum DoctorResponse {
    case questions(Questionary)
    case answer(String)
    
    init(from string: String) throws {
        if let data = string.data(using: .utf8) {
            if string.contains("\"questions\""){
                let decoded = try! JSONDecoder().decode(Questionary.self, from: data)
                self = .questions(decoded)
            } else if string.contains("\"text\"") {
                let decoded = try! JSONDecoder().decode(Answer.self, from: data)
                self = .answer(decoded.text)
            } else {
                throw NSError(domain: "DoctorResponseError", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown response format"])
            }
        } else {
            throw NSError(domain: "DoctorResponseError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid string encoding"])
        }
    }
}

class DoctorProvider {
    
    private let baseUrl = ""
    
    func sendMessage(message: String) async throws -> DoctorResponse {
        try! await sendAnswers(message: message, answers: FilledQuestionary(filledQuestions: [:]))
    }
    
    func sendAnswers(message: String, answers: FilledQuestionary) async throws -> DoctorResponse {
        
        struct DoctorParams: Codable {
            var message: String
            var userCard: [String: String]
            var filledQuestionary: FilledQuestionary
        }
        
        let onboard = OnboardingProvider()
            
        let paramsObject = DoctorParams(
            message: message,
            userCard: onboard.filledOnboarding!.filledQuestions,
            filledQuestionary: answers
        )
        
        let encoder = JSONParameterEncoder.default
        encoder.encoder.keyEncodingStrategy = .convertToSnakeCase
        
        let responseString = try await AF.request(
            baseUrl + "/doctor", 
            method: .post,
            parameters: paramsObject,
            encoder: encoder
        ).serializingString().value
        
        return try! DoctorResponse(from: responseString)
    }
}

Вторая Π²Ρ‹Π·Ρ‹Π²Π°Π΅Ρ‚ openai api Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ (ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ backendless) ΠΈ являСтся практичСски ΠΊΠΎΠΏΠΈΠ΅ΠΉ модуля подсказок ΠΈΠ· бэкСнда.

class PromptsProvider {
    
    private(set) public var homeRole = "" // TODO: take from the backend
    
    func message(message: String) -> String {
        return message
    }
    
    func profile(profile: FilledQuestionary) -> String {
        return try! jsonify(object: profile)
    }
    
    func answers(filled: FilledQuestionary) -> String {
        return try! jsonify(object: filled)
    }
    
    // MARK: - Private
    
    private func jsonify(object: Encodable) throws -> String {
        let coder = JSONEncoder()
        return String(data: try coder.encode(object), encoding: .utf8) ?? ""
    }
}

class HouseMDAIProvider {
    
    private var openAI: OpenAI
    
    init() {
        openAI = OpenAI(apiToken: "")
    }
    
    func sendMessage(message: String) async throws -> DoctorResponse {
        try! await sendAnswers(message: message, answers: FilledQuestionary(filledQuestions: [:]))
    }
    
    func sendAnswers(message: String, answers: FilledQuestionary) async throws -> DoctorResponse {
		// NOTE: Draft version, DI should be used instead!
        let promptProvider = PromptsProvider()
        let profile = OnboardingProvider().filledOnboarding!
        
        let query = ChatQuery(model: .gpt3_5Turbo, messages: [
            Chat(role: .system, content: promptProvider.homeRole),
            Chat(role: .user, content: promptProvider.profile(profile: profile)),
            Chat(role: .user, content: promptProvider.message(message: message)),
            Chat(role: .user, content: promptProvider.answers(filled: answers)),
        ])
        
        let result = try await openAI.chats(query: query)
        return try! DoctorResponse(from: result.choices[0].message.content ?? "")
    }
}

Π”Ρ€ΡƒΠ³ΠΎΠΉ ΠΏΡ€ΠΈΠΌΠ΅Ρ€

ΠŸΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ этой Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Ρ‹ Π² Ρ€Π΅Π°Π»ΡŒΠ½ΠΎΠΌ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ ΠΌΠΎΠΆΠ½ΠΎ Π² ΠΌΠΎΠ΅ΠΌ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π΅ TwiTreads Π½Π° github.com/MarkParker5/TwiTreads

Π§Ρ‚ΠΎ Π΄Π΅Π»Π°Ρ‚ΡŒ дальшС

  1. Π˜Π½Ρ‚Π΅Π³Ρ€ΠΈΡ€ΡƒΠΉΡ‚Π΅ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΡŽ ΠΈ Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ Π² бэкСнд. ΠœΠΎΠΆΠ΅Ρ‚Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΎΡ„ΠΈΡ†ΠΈΠ°Π»ΡŒΠ½Ρ‹ΠΉ шаблон FastAPI ΠΈΠ· FastAPI Project Generation.
  2. Π Π΅Π°Π»ΠΈΠ·ΡƒΠΉΡ‚Π΅ Π»ΠΎΠ³ΠΈΠΊΡƒ Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π² ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ.
  3. Π‘ΠΎΡΡ€Π΅Π΄ΠΎΡ‚ΠΎΡ‡ΡŒΡ‚Π΅ΡΡŒ Π½Π° ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΠΈ Π΄ΠΈΠ·Π°ΠΉΠ½Π° прилоТСния, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΠΎΠ²Ρ‹ΡΠΈΡ‚ΡŒ удобство Ρ€Π°Π±ΠΎΡ‚Ρ‹ с Π½ΠΈΠΌ. Π”Π°Π²Π°ΠΉΡ‚Π΅ ΡΠΎΠ·Π΄Π°Π²Π°Ρ‚ΡŒ красивыС прилоТСния!

Π—Π°ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅

ΠŸΡ€ΠΈΠ²Π΅Π΄Π΅Π½Π½Ρ‹Π΅ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹ ΠΈ ссылки Π½Π° ΠΊΠΎΠ΄ слуТат Ρ€Π΅Π°Π»ΡŒΠ½Ρ‹ΠΌΠΈ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π°ΠΌΠΈ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»Ρ‡ΠΎΠΊ вашСй собствСнной Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅. ΠŸΠΎΠΌΠ½ΠΈΡ‚Π΅, Ρ‡Ρ‚ΠΎ красота Ρ‚Π΅Ρ…Π½ΠΎΠ»ΠΎΠ³ΠΈΠΈ Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² итСрациях. НачнитС с простого: создайтС ΠΏΡ€ΠΎΡ‚ΠΎΡ‚ΠΈΠΏ ΠΈ постоянно ΡΠΎΠ²Π΅Ρ€ΡˆΠ΅Π½ΡΡ‚Π²ΡƒΠΉΡ‚Π΅ Π΅Π³ΠΎ. ΠšΠ°ΠΆΠ΄Ρ‹ΠΉ шаг Π²ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΡ€ΠΈΠ±Π»ΠΈΠΆΠ°Π΅Ρ‚ вас ΠΊ овладСнию искусством Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ½ΠΎΠ³ΠΎ обСспСчСния ΠΈ, Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, ΠΊ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌΡƒ Π±ΠΎΠ»ΡŒΡˆΠΎΠΌΡƒ ΠΏΡ€ΠΎΡ€Ρ‹Π²Ρƒ Π² тСхнологиях. Бчастливого ΠΊΠΎΠ΄ΠΈΠ½Π³Π°!

***

Автор: ΠœΠ°Ρ€ΠΊ ΠŸΠ°Ρ€ΠΊΠ΅Ρ€

www.markparker.me

Π’Π΅Π»Π΅Π³Ρ€Π°ΠΌ-ΠΊΠ°Π½Π°Π»: t.me/parker_is_typing

Π›Π£Π§Π¨Π˜Π• БВАВЬИ ПО Π’Π•ΠœΠ•

Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° программиста
04 октября 2018

ΠŸΠΎΠΌΠ½ΠΈΡ‚ΡŒ всС: дСлимся Π»ΡƒΡ‡ΡˆΠ΅ΠΉ ΡˆΠΏΠ°Ρ€Π³Π°Π»ΠΊΠΎΠΉ ΠΏΠΎ Python

ΠœΡ‹ ΠΏΠΎΠ΄Π³ΠΎΡ‚ΠΎΠ²ΠΈΠ»ΠΈ ΠΎΡ‡Π΅Π½ΡŒ Π·Π°Π½ΠΈΠΌΠ°Ρ‚Π΅Π»ΡŒΠ½ΡƒΡŽ ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΡŽ, которая ΠΏΠΎ ΠΏΡ€Π°Π²Ρƒ ΠΌΠΎΠΆΠ΅Ρ‚ Π½Π°Π·Ρ‹Π²Π°...
Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° программиста
22 апрСля 2017

Английский язык для IT-спСциалистов

ВсСм людям, Ρ‚Π°ΠΊ ΠΈΠ»ΠΈ ΠΈΠ½Π°Ρ‡Π΅ связанным с IT сфСрой, прСкрасно извСстно, Ρ‡Ρ‚ΠΎ Ρ€Π°...
admin
29 января 2017

Π˜Π·ΡƒΡ‡Π°Π΅ΠΌ Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌΡ‹: ΠΏΠΎΠ»Π΅Π·Π½Ρ‹Π΅ ΠΊΠ½ΠΈΠ³ΠΈ, Π²Π΅Π±-сайты, ΠΎΠ½Π»Π°ΠΉΠ½-курсы ΠΈ Π²ΠΈΠ΄Π΅ΠΎΠΌΠ°Ρ‚Π΅Ρ€ΠΈΠ°Π»Ρ‹

Π’ этой ΠΏΠΎΠ΄Π±ΠΎΡ€ΠΊΠ΅ прСдставлСн список ΠΊΠ½ΠΈΠ³, Π²Π΅Π±-сайтов ΠΈ ΠΎΠ½Π»Π°ΠΉΠ½-курсов, Π΄Π°ΡŽΡ‰ΠΈΡ…...