π±ΠΠΈΡΠ΅ΠΌ iOS-ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π΄Π»Ρ ΠΏΠ»Π°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΡ Π·Π°Π΄Π°Ρ Ρ ΠΏΠΎΠΌΠΎΡΡΡ AirTable, Moya ΠΈ VIPER
Π ΡΡΠΎΠΉ ΡΡΠ°ΡΡΠ΅ ΠΌΡ ΡΠΎΠ·Π΄Π°Π΄ΠΈΠΌ iOS-ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π΄Π»Ρ ΠΏΠ»Π°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΡ Π·Π°Π΄Π°Ρ ΠΈ Π²ΠΎΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌΡΡ AirTable Π² ΠΊΠ°ΡΠ΅ΡΡΠ²Π΅ Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΠΎΠ³ΠΎ ΠΎΠ½Π»Π°ΠΉΠ½-ΡΠ΅ΡΠ²ΠΈΡΠ° Π΄Π»Ρ ΡΠ΄Π°Π»Π΅Π½Π½ΠΎΠ³ΠΎ Ρ ΡΠ°Π½Π΅Π½ΠΈΡ Π΄Π°Π½Π½ΡΡ .
Π― ΠΏΠΈΡΡ ΠΈ ΠΏΠ΅ΡΠ΅Π²ΠΎΠΆΡ ΡΡΠ°ΡΡΠΈ ΡΠΆΠ΅ 2.5 Π³ΠΎΠ΄Π°, ΠΈ Π² ΠΊΠ°ΠΊΠΎΠΉ-ΡΠΎ ΠΌΠΎΠΌΠ΅Π½Ρ Ρ ΠΎΡΠΎΠ·Π½Π°Π»Π°, ΡΡΠΎ ΠΌΠ½Π΅ Π½Π΅ Ρ Π²Π°ΡΠ°Π΅Ρ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ ΠΏΠΎΠ΄ ΠΌΠΎΠΈ Π½ΡΠΆΠ΄Ρ: ΡΠ»Π΅Π΄ΠΈΡΡ Π·Π° Π΄Π΅Π΄Π»Π°ΠΉΠ½Π°ΠΌΠΈ ΠΏΠΎ ΡΠ΄Π°ΡΠ΅ ΡΡΠ°ΡΠ΅ΠΉ, Ρ ΡΠ°Π½ΠΈΡΡ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ ΠΎ Π·Π°ΠΊΠ°Π·ΡΠΈΠΊΠ°Ρ , Π²Π΅ΡΡΠΈ ΡΠΌΠ΅ΡΡ, Π΄Π΅ΡΠΆΠ°ΡΡ ΠΏΠΎΠ΄ ΡΡΠΊΠΎΠΉ Π²ΠΈΠ·ΠΈΡΠΊΡ Ρ ΠΊΠΎΠ½ΡΠ°ΠΊΡΠ°ΠΌΠΈ ΠΈ Ρ. Π΄. ΠΠΎΡΡΠΎΠΌΡ Ρ ΡΠ΅ΡΠΈΠ»Π° ΡΠΎΠ·Π΄Π°ΡΡ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅, ΠΊΠΎΡΠΎΡΠΎΠ΅ ΠΎΠ±Π»Π΅Π³ΡΠΈΡ ΠΏΠ»Π°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΌΠΎΠΈΡ Π·Π°Π΄Π°Ρ.
Π Ρ ΠΎΠ΄Π΅ ΡΠ΅Π°Π»ΠΈΠ·Π°ΡΠΈΠΈ ΠΌΠ½Π΅ ΠΏΠΎΡΡΠ΅Π±ΠΎΠ²Π°Π»ΡΡ Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΡΠΉ (ΠΈΠ»ΠΈ ΡΡΠ»ΠΎΠ²Π½ΠΎ Π±Π΅ΡΠΏΠ»Π°ΡΠ½ΡΠΉ) ΠΎΠ½Π»Π°ΠΉΠ½-ΡΠ΅ΡΠ²ΠΈΡ Π΄Π»Ρ Ρ ΡΠ°Π½Π΅Π½ΠΈΡ Π΄Π°Π½Π½ΡΡ ΡΠ΄Π°Π»Π΅Π½Π½ΠΎ, ΠΈ ΡΠΎΠ³Π΄Π° ΠΊΠΎΠ»Π»Π΅Π³Π° ΡΠ°ΡΡΠΊΠ°Π·Π°Π»Π° ΠΌΠ½Π΅ ΠΏΡΠΎ Airtable. ΠΠ΄Π½Π°ΠΊΠΎ Π² ΠΈΠ½ΡΠ΅ΡΠ½Π΅ΡΠ΅ ΠΌΠ½ΠΎΡ Π½Π΅ Π±ΡΠ»ΠΈ Π½Π°ΠΉΠ΄Π΅Π½Ρ ΠΊΠ°ΠΊΠΈΠ΅-Π»ΠΈΠ±ΠΎ ΡΡΠ°ΡΡΠΈ ΠΏΠΎ ΡΠ°Π±ΠΎΡΠ΅ Ρ Π½ΠΈΠΌ (ΡΠΎΠ»ΡΠΊΠΎ ΡΠΏΠΎΠΌΠΈΠ½Π°Π½ΠΈΡ), Π² ΡΠ²ΡΠ·ΠΈ Ρ ΡΠ΅ΠΌ ΠΏΠΎΡΠ²ΠΈΠ»Π°ΡΡ ΠΈΠ΄Π΅Ρ Π½Π°ΠΏΠΈΡΠ°ΡΡ ΡΡΠ°ΡΡΡ ΠΏΠΎ ΡΠ°Π±ΠΎΡΠ΅ Ρ AirTable Π΄Π»Ρ Π½Π°ΡΠΈΠ½Π°ΡΡΠΈΡ ΡΠ°Π·ΡΠ°Π±ΠΎΡΡΠΈΠΊΠΎΠ².
AirTable ΠΏΠΎΠ·Π²ΠΎΠ»ΡΠ΅Ρ Π΄ΠΎΡΡΠ°ΡΠΎΡΠ½ΠΎ ΠΏΡΠΎΡΡΠΎ ΠΈΠ½ΡΠ΅Π³ΡΠΈΡΠΎΠ²Π°ΡΡ Π΄Π°Π½Π½ΡΠ΅ Π² ΠΏΡΠΎΠ΅ΠΊΡ. API ΡΠΎΡΠ½ΠΎ ΡΠ»Π΅Π΄ΡΠ΅Ρ ΡΠ΅ΠΌΠ°Π½ΡΠΈΠΊΠ΅ REST, ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ JSON Π΄Π»Ρ ΠΊΠΎΠ΄ΠΈΡΠΎΠ²Π°Π½ΠΈΡ ΠΎΠ±ΡΠ΅ΠΊΡΠΎΠ² ΠΈ ΠΏΠΎΠ»Π°Π³Π°Π΅ΡΡΡ Π½Π° ΡΡΠ°Π½Π΄Π°ΡΡΠ½ΡΠ΅ ΠΊΠΎΠ΄Ρ HTTP Π΄Π»Ρ ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΡ ΠΎ ΡΠ΅Π·ΡΠ»ΡΡΠ°ΡΠ°Ρ ΠΎΠΏΠ΅ΡΠ°ΡΠΈΠΈ. ΠΠ»Ρ ΡΠ°Π±ΠΎΡΡ Ρ ΡΠ΅ΡΠ΅Π²ΡΠΌ ΡΠ»ΠΎΠ΅ΠΌ Π±ΡΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ Moya, Π΄ΠΎΡΡΠ°ΡΠΎΡΠ½ΠΎ Π²ΠΎΡΡΡΠ΅Π±ΠΎΠ²Π°Π½Π½ΡΠΉ ΠΈ Π»Π΅Π³ΠΊΠΈΠΉ ΡΡΠ΅ΠΉΠΌΠ²ΠΎΡΠΊ.
Π ΠΈΠ½ΡΠ΅ΡΠ½Π΅ΡΠ΅ ΠΏΠΎΠ»Π½ΠΎ ΠΎΠ±ΡΡΠ°ΡΡΠΈΡ ΡΡΠ°ΡΠ΅ΠΉ Π½Π° ΡΠ°ΠΌΡΠ΅ ΡΠ°Π·Π½ΡΠ΅ ΡΠ΅ΠΌΡ, ΡΠ΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½ΡΡ Π½Π° ΠΏΡΠΎΡΡΠ΅ΠΉΡΠΈΡ Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΠ°Ρ (MVC ΠΈ Ρ.ΠΏ.), Π½ΠΎ Π½Π° VIPER-Π΅ ΠΈΡ Π½Π΅ ΡΠ°ΠΊ ΠΌΠ½ΠΎΠ³ΠΎ. ΠΡΠΈ ΡΡΠΎΠΌ VIPER Π·Π°ΡΠ°ΡΡΡΡ ΡΠΏΡΠ°ΡΠΈΠ²Π°ΡΡ Π½Π° ΡΠΎΠ±Π΅ΡΠ΅Π΄ΠΎΠ²Π°Π½ΠΈΡΡ Π΄Π°ΠΆΠ΅ Ρ Π΄ΠΆΡΠ½ΠΎΠ². ΠΠΎΡΡΠΎΠΌΡ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π°ΠΏΠΈΡΠ΅ΠΌ Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ ΡΡΠΎΠΉ Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΡ.
Π‘Π½Π°ΡΠ°Π»Π° ΠΌΡ ΡΠ°ΡΡΠΌΠΎΡΡΠΈΠΌ ΠΏΡΠΎΡΡΠΎΠ΅ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π° VIPER, Π° Π·Π°ΡΠ΅ΠΌ ΠΏΠΎΡΠ°Π³ΠΎΠ²ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΠΌ AirTable ΠΈ Moya. Π ΡΠ°ΠΌΠΊΠ°Ρ Π·Π½Π°ΠΊΠΎΠΌΡΡΠ²Π° Ρ ΡΠΏΡΠΎΡΡΠΈΠ»Π° ΠΊΠΎΠ΄ ΡΠΆΠ΅ Π³ΠΎΡΠΎΠ²ΠΎΠ³ΠΎ ΠΏΡΠΎΠ΅ΠΊΡΠ°, ΡΠ±ΡΠ°Π² Π»ΠΈΡΠ½ΠΈΠ΅ ΠΌΠΎΠ΄ΡΠ»ΠΈ ΠΈ ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΠ΅, ΡΡΠΎΠ±Ρ ΡΡΠΎ Π½Π΅ ΠΏΠΎΠΌΠ΅ΡΠ°Π»ΠΎ Π·Π½Π°ΠΊΠΎΠΌΡΡΠ²Ρ Ρ ΠΎΠ±ΠΎΠ·Π½Π°ΡΠ΅Π½Π½ΡΠΌΠΈ Π²ΡΡΠ΅ ΡΠ΅ΠΌΠ°ΠΌΠΈ.
VIPER
ΠΠ° ΠΈΠ΄Π΅Ρ ΡΠ΅Π°Π»ΠΈΠ·Π°ΡΠΈΠΈ Π°ΡΡ ΠΈΡΠ΅ΠΊΡΡΡΡ VIPER Π±ΡΠ» Π²Π·ΡΡ Π²Π°ΡΠΈΠ°Π½Ρ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Alfian Losari. ΠΠ°Π½Π½Π°Ρ ΡΠ΅Π°Π»ΠΈΠ·Π°ΡΠΈΡ ΠΎΡΠ»ΠΈΡΠ½ΠΎ ΠΏΠΎΠ΄ΠΎΠΉΠ΄Π΅Ρ Π΄Π»Ρ Π·Π½Π°ΠΊΠΎΠΌΡΡΠ²Π° Ρ VIPER, ΠΊΠΎΠ΄ ΠΏΠΎΠ½ΡΡΠ΅Π½, Π΅Π³ΠΎ Π»Π΅Π³ΠΊΠΎ ΡΠΈΡΠ°ΡΡ ΠΈ ΠΌΠ°ΡΡΡΠ°Π±ΠΈΡΠΎΠ²Π°ΡΡ. Π‘ΠΎΠ²Π΅ΡΡΡ ΠΎΠ·Π½Π°ΠΊΠΎΠΌΠΈΡΡΡΡ Ρ ΠΏΠΎΠ΄ΡΠΎΠ±Π½ΠΎΠΉ ΡΠ΅ΠΎΡΠΈΠ΅ΠΉ Π² Π²ΠΈΠ΄Π΅ΠΎ.
Π‘ΡΠ°Π½Π΄Π°ΡΡΠ½Π°Ρ ΡΡ Π΅ΠΌΠ° VIPER Π²ΡΠ³Π»ΡΠ΄ΠΈΡ ΡΠ°ΠΊ:
ΠΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ Π²Π·ΡΡΠΎ ΠΎΡΡΡΠ΄Π°.
ΠΡΡ ΠΈΡΠ΅ΠΊΡΡΡΠ° VIPER ΡΠΎΡΡΠΎΠΈΡ ΠΈΠ· ΡΠ»Π΅Π΄ΡΡΡΠΈΡ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠΎΠ²:
Entity β ΠΎΡΠ²Π΅ΡΠ°Π΅Ρ Π·Π° Ρ ΡΠ°Π½Π΅Π½ΠΈΠ΅ ΡΡΡΠ½ΠΎΡΡΠ΅ΠΉ (Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ, Ρ Π½Π°Ρ ΡΡΠΎ ΡΡΡΠ½ΠΎΡΡΡ "OrderItem", Π² ΠΊΠΎΡΠΎΡΠΎΠΉ Ρ ΡΠ°Π½ΡΡΡΡ Π·Π°ΠΊΠ°Π·Ρ).
Interactor β ΠΏΠΎΡΡΠ΅Π΄Π½ΠΈΠΊ ΠΌΠ΅ΠΆΠ΄Ρ Entity (ΡΡΡΠ½ΠΎΡΡΡΠΌΠΈ) ΠΈ Presenter. ΠΠΈΠ·Π½Π΅Ρ-Π»ΠΎΠ³ΠΈΠΊΠ° ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ Ρ ΡΠ°Π½ΠΈΡΡΡ Π·Π΄Π΅ΡΡ.
Presenter β ΡΠ²ΠΎΠ΅ΠΎΠ±ΡΠ°Π·Π½ΡΠΉ ΠΌΠΎΡΡ ΠΌΠ΅ΠΆΠ΄Ρ Π²ΡΠ΅ΠΌΠΈ Π²Π°ΠΆΠ½ΡΠΌΠΈ ΡΠ°ΡΡΡΠΌΠΈ VIPER (ΠΊΡΠΎΠΌΠ΅ Entity). Π‘ ΠΎΠ΄Π½ΠΎΠΉ ΡΡΠΎΡΠΎΠ½Ρ, ΠΎΠ½ ΠΏΠΎΠ»ΡΡΠ°Π΅Ρ Π½Π° Π²Ρ ΠΎΠ΄Π΅ ΡΠΎΠ±ΡΡΠΈΡ, ΠΏΠΎΡΡΡΠΏΠ°ΡΡΠΈΠ΅ ΠΈΠ· View, ΠΈ ΡΠ΅Π°Π³ΠΈΡΡΠ΅Ρ Π½Π° Π½ΠΈΡ , Π·Π°ΠΏΡΠ°ΡΠΈΠ²Π°Ρ Π΄Π°Π½Π½ΡΠ΅ Ρ Interactor. Π‘ Π΄ΡΡΠ³ΠΎΠΉ ΡΡΠΎΡΠΎΠ½Ρ, ΠΎΠ½ ΠΏΠΎΠ»ΡΡΠ°Π΅Ρ Π΄Π°Π½Π½ΡΠ΅, ΠΏΠΎΡΡΡΠΏΠ°ΡΡΠΈΠ΅ ΠΎΡ Interactor, ΠΏΡΠΈΠΌΠ΅Π½ΡΠ΅Ρ Π»ΠΎΠ³ΠΈΠΊΡ ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»Π΅Π½ΠΈΡ ΠΊ ΡΡΠΈΠΌ Π΄Π°Π½Π½ΡΠΌ, ΠΈ, Π½Π°ΠΊΠΎΠ½Π΅Ρ, ΡΠΎΠΎΠ±ΡΠ°Π΅Ρ View, ΡΡΠΎ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ°ΡΡ. ΠΡΠΈ ΡΡΠΎΠΌ Presenter Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ Π·Π½Π°Π΅Ρ ΠΏΡΠΎ UIKit.
View β ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»Π΅Π½ΠΈΠ΅, ΠΊΠΎΡΠΎΡΠΎΠ΅ ΠΎΡΠ²Π΅ΡΠ°Π΅Ρ Π·Π° ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ ΠΈ Π½ΠΈΡΠ΅Π³ΠΎ Π½Π΅ Π·Π½Π°Π΅Ρ ΠΏΡΠΎ Π΄Π°Π½Π½ΡΠ΅. Π‘Π²ΡΠ·Ρ ΡΠΎΠ»ΡΠΊΠΎ Ρ Presenter.
Router β ΠΎΡΠ²Π΅ΡΠ°Π΅Ρ Π·Π° Π½Π°Π²ΠΈΠ³Π°ΡΠΈΠΎΠ½Π½ΡΡ Π»ΠΎΠ³ΠΈΠΊΡ, ΠΊΠΎΠ³Π΄Π° ΠΈ ΠΊΠ°ΠΊΠΈΠ΅ ΡΠΊΡΠ°Π½Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ°ΡΡΡΡ.
Π‘ΠΊΠ°ΡΠ°ΠΉΡΠ΅ ΡΡΠ°ΡΡΠΎΠ²ΡΠΉ ΠΏΡΠΎΠ΅ΠΊΡ ΠΏΠΎ ΡΡΡΠ»ΠΊΠ΅. ΠΡΠΎΠ΅ΠΊΡ ΡΠΎΡΡΠΎΠΈΡ ΠΈΠ· Π΄Π²ΡΡ ΠΌΠΎΠ΄ΡΠ»Π΅ΠΉ: OrderListModule ΠΈ OrderDetailModule.
OrderListModule ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»ΡΠ΅Ρ ΡΠΎΠ±ΠΎΠΉ Π½Π°Π±ΠΎΡ ΠΊΠ»Π°ΡΡΠΎΠ² Π΄Π»Ρ ΠΎΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΠΈ Π·Π°Π³ΡΡΠ·ΠΊΠΈ ΡΠΏΠΈΡΠΊΠ° ΡΡΠ°ΡΠ΅ΠΉ:
OrderDetailModule ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»ΡΠ΅Ρ ΡΠΎΠ±ΠΎΠΉ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ ΠΏΠΎ ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΡΡΠ°ΡΡΠ΅:
ΠΠ°ΠΆΠ΄ΡΠΉ ΠΌΠΎΠ΄ΡΠ»Ρ Π²ΠΊΠ»ΡΡΠ°Π΅Ρ Π² ΡΠ΅Π±Ρ View, Interactor, Presenter, Entity, Router, Π° ΡΠ°ΠΊΠΆΠ΅ Π½Π΅ΠΎΠ±Ρ ΠΎΠ΄ΠΈΠΌΡΠ΅ ΠΏΡΠΎΡΠΎΠΊΠΎΠ»Ρ Π΄Π»Ρ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΡ ΠΌΠ΅ΠΆΠ΄Ρ ΡΠ°ΡΡΡΠΌΠΈ ΠΌΠΎΠ΄ΡΠ»Π΅ΠΉ.
Entity ΠΎΡΠ²Π΅ΡΠ°Π΅Ρ Π·Π° ΡΡΡΠ½ΠΎΡΡΠΈ ΠΈ ΡΠ²Π»ΡΠ΅ΡΡΡ ΠΎΠ±ΡΠΈΠΌ Π΄Π»Ρ ΠΎΠ±ΠΎΠΈΡ ΠΌΠΎΠ΄ΡΠ»Π΅ΠΉ:
Π OrderItem.swift ΡΠΎΠ΄Π΅ΡΠΆΠΈΡΡΡ ΠΎΠ΄Π½ΠΎΠΈΠΌΠ΅Π½Π½ΡΠΉ ΠΊΠ»Π°ΡΡ OrderItem. Π£ ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΡΡΠ°ΡΡΠΈ Π΅ΡΡΡ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ (name), Π΄Π΅Π΄Π»Π°ΠΉΠ½ ΡΠ΄Π°ΡΠΈ (deadline), Π·Π°ΠΊΠ°Π·ΡΠΈΠΊ (customer) ΠΈ ΠΏΡΠΈΠΌΠ΅ΡΠ°Π½ΠΈΠ΅/Π·Π°ΠΌΠ΅ΡΠΊΠ° ΠΊ ΡΡΠ°ΡΡΠ΅ (summary).
import Foundation class OrderItem { var summary: String? var deadline: Date? var name: String var customer: String? init(summary: String?, deadline: Date?, name: String, customer:String?) { self.summary = summary self.deadline = deadline self.name = name self.customer = customer } }
OrderAPI ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»ΡΠ΅Ρ ΡΠΎΠ±ΠΎΠΉ ΠΈΠΌΠΈΡΠ°ΡΠΈΡ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ Π΄Π°Π½Π½ΡΡ ΡΠ΅ΡΠ΅Π· ΡΠ΅ΡΡ.
import Foundation class OrderAPI { private init() {} public static let shared = OrderAPI() public private(set) var orders: [OrderItem] = [ OrderItem(summary: "How to use AirTable and how to set Moya", deadline: OrderAPI.createTestDate(value: "2023-01-08"), name: "Moya and AirTable in iOS-app", customer: "proglib"), OrderItem(summary: "Creating Viper app", deadline: OrderAPI.createTestDate(value: "2023-01-08"), name: "VIPER in iOS", customer: "proglib"), OrderItem(summary: "All about MVVM", deadline: OrderAPI.createTestDate(value: "2023-01-09"), name: "MVVM in iOS", customer: "medium"), OrderItem(summary: "Some tips", deadline: OrderAPI.createTestDate(value: "2023-01-12"), name: "How to make good apps", customer: "medium"), ] func addOrder(_ order: OrderItem) { orders.append(order) } static func createTestDate(value: String) -> Date? { let RFC3339DateFormatter = DateFormatter() RFC3339DateFormatter.locale = Locale(identifier: "en_US_POSIX") RFC3339DateFormatter.dateFormat = "yyyy-MM-dd" RFC3339DateFormatter.timeZone = TimeZone(secondsFromGMT: 0) //let string = "1996-12-19T16:39:57-08:00" return RFC3339DateFormatter.date(from: value) } }
ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π΅ΠΌ Moya
Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΡΠ°ΠΉΠ» Podfile ΠΈ ΠΏΡΠΎΠΏΠΈΡΠΈΡΠ΅ ΡΠ°ΠΌ:
# Uncomment the next line to define a global platform for your project platform :ios, '12.0' use_frameworks! inhibit_all_warnings! target 'PlannerTranslator_v4' do pod 'Moya', '~> 15.0' pod 'SwiftyJSON', '5.0.1' end
ΠΡΠ»ΠΈ Ρ Π²Π°Ρ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½Ρ ΡΠ΅ΡΡΡ, Π½Π΅ Π·Π°Π±ΡΠ΄ΡΡΠ΅ ΠΏΡΠΎΠΏΠΈΡΠ°ΡΡ ΠΈ ΠΈΡ :
# Uncomment the next line to define a global platform for your project platform :ios, '12.0' use_frameworks! inhibit_all_warnings! target 'PlannerTranslator_v4' do pod 'Moya', '~> 15.0' pod 'SwiftyJSON', '5.0.1' target 'PlannerTranslator_v4Tests' do pod 'Moya', '~> 15.0' pod 'SwiftyJSON', '5.0.1' end end
ΠΠ°ΡΠ΅ΠΌ ΠΎΡΠΊΡΠΎΠΉΡΠ΅ ΡΠ΅ΡΠΌΠΈΠ½Π°Π» ΠΈ Π²ΡΠΏΠΎΠ»Π½ΠΈΡΠ΅:
pod install
ΠΡΠ»ΠΈ Ρ Π²Π°Ρ Π²ΡΠ΅ Ρ ΠΎΡΠΎΡΠΎ, ΡΠΎ Π² ΡΠ΅ΡΠΌΠΈΠ½Π°Π»Π΅ Ρ Π²Π°Ρ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΠΏΠΎΡΠ²ΠΈΡΡΡΡ:
ΠΠ°Π»Π΅Π΅ ΠΌΡ ΠΎΡΠΊΡΡΠ²Π°Π΅ΠΌ ΡΠ°ΠΉΠ» Ρ ΡΠ°ΡΡΠΈΡΠ΅Π½ΠΈΠ΅ΠΌ .xcworkspace (Π½Π΅ xcodeproj!), ΠΈΠ½Π°ΡΠ΅ ΠΏΡΠΎΠ΅ΠΊΡ Π½Π΅ Π·Π°ΠΏΡΡΡΠΈΡΡΡ:
ΠΡΠ΅ Ρ ΠΎΡΠΎΡΠΎ, ΠΌΡ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠΈΠ»ΠΈ Moya!
Π’Π΅ΠΏΠ΅ΡΡ ΠΏΠ΅ΡΠ΅ΠΉΠ΄Π΅ΠΌ ΠΊ ΡΠ°Π±ΠΎΡΠ΅ Ρ AirTable.
ΠΠΎΠ΄ΠΊΠ»ΡΡΠ°Π΅ΠΌ AirTable
- Π Π΅Π³ΠΈΡΡΡΠΈΡΡΠ΅ΠΌΡΡ Π½Π° ΡΠ°ΠΉΡΠ΅ https://airtable.com/
- Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΡΠ°Π±Π»ΠΈΡΡ (ΠΈΠΌΠ΅Π½Π° ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΡ ΠΏΡΠΎΠ΅ΠΊΡΠ° ΠΈ ΡΡΠΎΠ»Π±ΡΠΎΠ² ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡΡ) ΠΈ Π·Π°ΠΏΠΎΠ»Π½ΡΠ΅ΠΌ Π΅Π΅:
ΠΠ±ΡΠ°ΡΠΈΡΠ΅ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅, ΡΡΠΎ Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΡΡΠΎΠ»Π±ΡΠ° ΡΠ²ΠΎΠΉ ΡΠΈΠΏ Π΄Π°Π½Π½ΡΡ . Π’ΠΈΠΏ Π΄Π°Π½Π½ΡΡ ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΡΠΌΠΎΡΡΠ΅ΡΡ ΡΠ΅ΡΠ΅Π· Edit field:
Π£ name, deadline, summary ΠΈ customer β ΡΡΠΎ Single line text:
Π£ deadline β ΡΡΠΎ Date.
ΠΠΎΡΠ»Π΅ ΡΠΎΠ·Π΄Π°Π½ΠΈΡ ΡΠ°Π±Π»ΠΈΡΡ ΠΏΡΠΈΡΡΡΠΏΠ°Π΅ΠΌ ΠΊ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΡ ΡΠ°ΠΉΠ»ΠΎΠ² Π² ΠΏΡΠΎΠ΅ΠΊΡ.
Π‘Π½Π°ΡΠ°Π»Π° Π΄ΠΎΠ±Π°Π²ΠΈΠΌ Π½ΠΎΠ²ΡΡ ΡΡΡΠ½ΠΎΡΡΡ NetworkEntities:
import Foundation protocol ATProtocol: Codable { var idAT: String? { get set } } struct MoyResponse<T: ATProtocol>: Codable { let records: [SubMoyResponse<T>] enum MoyResponseKeys: CodingKey { case records } } struct SubMoyResponse<T: ATProtocol>: Codable { let id: String let createdTime: String var fields: T enum SubMoyResponseKeys: CodingKey { case id,createdTime,fields } init(from decoder: Decoder) throws { let container: KeyedDecodingContainer<SubMoyResponse<T>.CodingKeys> = try decoder.container(keyedBy: SubMoyResponse<T>.CodingKeys.self) self.id = try container.decode(String.self, forKey: SubMoyResponse<T>.CodingKeys.id) self.createdTime = try container.decode(String.self, forKey: SubMoyResponse<T>.CodingKeys.createdTime) self.fields = try container.decode(T.self, forKey: SubMoyResponse<T>.CodingKeys.fields) self.fields.idAT = self.id } } struct MoyRequest<T: Codable>: Codable { let records: [SubMoyRequest<T>] enum MoyRequestKeys: CodingKey { case records } } struct SubMoyRequest<T: Codable>: Codable { let id: String? let fields: T enum SubMoyRequestKeys: CodingKey { case id,createdTime,fields } func toJSON() -> Dictionary<String, Any> { do { let jsonData = try JSONEncoder().encode(self) let jsonString = String(data: jsonData, encoding: .utf8)! if let data = jsonString.data(using: .utf8) { do { return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? Dictionary<String, Any>() } catch { print(error.localizedDescription) } } return Dictionary<String, Any>() } catch { print(error) } return Dictionary<String, Any>() } }
ΠΠΎΡΠ»Π΅ ΡΠ΅Π³ΠΎ Π΄Π»Ρ ΡΠ΄ΠΎΠ±ΡΡΠ²Π° ΡΠΎΠ·Π΄Π°Π΄ΠΈΠΌ Π½ΠΎΠ²ΡΡ Π³ΡΡΠΏΠΏΡ Moya ΠΈ ΡΠΎΠ·Π΄Π°Π΄ΠΈΠΌ 4 ΡΠ°ΠΉΠ»Π°:
MoyaRequestType.swift
import Foundation import Moya // RequestType Π²ΠΊΠ»ΡΡΠ°Π΅Ρ Π² ΡΠ΅Π±Ρ ΡΠΈΠΏΡ Π·Π°ΠΏΡΠΎΡΠΎΠ². public typealias RequestParametersType = (apiStringURL: String, body: [String: Any]?) //ΡΠΈΠΏΡ Π·Π°ΠΏΡΠΎΡΠΎΠ² enum RequestType { case orders case ordersDetail(String) case create(OrderItem) case edit(OrderItem) } //TargetType - ΠΡΠΎΡΠΎΠΊΠΎΠ», ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌΡΠΉ Π΄Π»Ρ ΠΎΠΏΡΠ΅Π΄Π΅Π»Π΅Π½ΠΈΡ ΡΠΏΠ΅ΡΠΈΡΠΈΠΊΠ°ΡΠΈΠΉ, Π½Π΅ΠΎΠ±Ρ ΠΎΠ΄ΠΈΠΌΡΡ Π΄Π»Ρ ΡΠ°ΠΉΠ»Π° MoyaProvider. protocol WDTargetType: TargetType, Hashable { } extension RequestType: WDTargetType { static func == (lhs: RequestType, rhs: RequestType) -> Bool { lhs.path == rhs.path } func hash(into hasher: inout Hasher) { hasher.combine(path) hasher.combine(method) } //Π°Π΄ΡΠ΅Ρ ΡΠ΅ΡΠ²Π΅ΡΠ°, Π½Π° ΠΊΠΎΡΠΎΡΠΎΠΌ Π»Π΅ΠΆΠΈΡ RESTful API var baseURL: URL { URL(string: "https://api.airtable.com/v0/appuggJ5PZ3FDUE2G/")! } //ΡΠΎΡΡΡ Π·Π°ΠΏΡΠΎΡΠΎΠ² var path: String { switch self { case .orders: return "PlannerTranslator" case .ordersDetail: return "PlannerTranslator" case .create: return "PlannerTranslator" case .edit: return "PlannerTranslator" } } // ΠΌΠ΅ΡΠΎΠ΄, ΠΊΠΎΡΠΎΡΡΠΉ ΠΌΡ ΠΏΠΎΡΡΠ»Π°Π΅ΠΌ. Moya Π±Π΅ΡΡΡ Π²ΡΠ΅ ΠΌΠ΅ΡΠΎΠ΄Ρ ΠΈΠ· Alamofire. var method: Moya.Method { switch self { case .orders, .ordersDetail: return Moya.Method.get case .create: return Moya.Method.post case .edit: return Moya.Method.patch } } //1) ΠΊΠΎΠ΄ΠΈΡΠΎΠ²ΠΊΠ° ΠΏΠ°ΡΠ°ΠΌΠ΅ΡΡΠΎΠ², ΡΠ°ΠΊΠΆΠ΅ Π±Π΅ΡΡΡΡΡ ΠΈΠ· Alamofire. //2) ΠΎΠΏΠΈΡΠ°Π½ΠΈΠ΅ Π·Π°Π΄Π°Ρ, ΠΊΠΎΡΠΎΡΡΠ΅ Π±ΡΠ΄Ρ Π²ΡΠΏΠΎΠ»Π½ΡΡΡΡΡ var task: Task { switch self { case .orders: return .requestParameters( parameters: ["maxRecords":20, "view":"Order"], encoding: URLEncoding.default) case .ordersDetail(let id): return .requestCompositeParameters( bodyParameters: ["id" : id], bodyEncoding: JSONEncoding.default, urlParameters: [:]) case .create(let order): do { let dict = try MoyRequest(records: [ SubMoyRequest<OrderItem>.init( id: nil, fields: order) ]).jsonData() return .requestCompositeData(bodyData: dict, urlParameters: [:]) } catch { return Task.requestPlain } case .edit(let order): do { let dict = try MoyRequest(records: [ SubMoyRequest<OrderItem>.init( id: order.idAT, fields: order) ]).jsonData() return .requestCompositeData(bodyData: dict, urlParameters: [:]) } catch { return Task.requestPlain } } } var headers: [String : String]? { let headersDictionary = MoyaNetworkManager.shared.headers return headersDictionary } }
ΠΠ»Ρ ΡΠΎΠ³ΠΎ ΡΡΠΎΠ±Ρ Π½Π°ΠΉΡΠΈ Π°Π΄ΡΠ΅Ρ ΡΠ΅ΡΠ²Π΅ΡΠ°, Π½ΡΠΆΠ½ΠΎ ΠΎΡΠΊΡΡΡΡ Π² Π΄ΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΠΈ authentication ΠΈ ΡΠΊΠΎΠΏΠΈΡΠΎΠ²Π°ΡΡ ΡΡΡΠ»ΠΊΡ.
MoyaNetworkManager.swift
ΠΡΠΊΡΡΠ²Π°Π΅ΠΌ https://airtable.com/account ΠΈ Π² overview ΡΠΌΠΎΡΡΠΈΠΌ ΡΠ²ΠΎΠΉ API-ΠΊΠ»ΡΡ:
ΠΠΎΠΏΠΈΡΡΠ΅ΠΌ Π΅Π³ΠΎ ΠΈ Π²ΡΡΠ°Π²Π»ΡΠ΅ΠΌ Π² headersDictionary["Authorization"], Π½Π΅ Π·Π°Π±ΡΠ² Π½Π°ΠΏΠΈΡΠ°ΡΡ Bearer ΠΏΠ΅ΡΠ΅Π΄ ΠΊΠ»ΡΡΠΎΠΌ.
import Foundation import Moya final class MoyaNetworkManager { // moyaProvider β ΡΡΠΎ Π°Π±ΡΡΡΠ°ΠΊΡΠΈΡ Π±ΠΈΠ±Π»ΠΈΠΎΡΠ΅ΠΊΠΈ, ΠΊΠΎΡΠΎΡΠ°Ρ Π΄Π°ΡΡ Π΄ΠΎΡΡΡΠΏ ΠΊ Π·Π°ΠΏΡΠΎΡΠ°ΠΌ: private var moyaProvider: AnyObject? = nil var headers: [String : String] { var headersDictionary = [String : String]() headersDictionary["accept"] = "text/plain" headersDictionary["content-type"] = "application/json; charset=utf-8" // AirTable Π½Π°ΡΡΠΎΡΡΠ΅Π»ΡΠ½ΠΎ ΡΠΎΠ²Π΅ΡΡΠ΅Ρ Ρ ΡΠ°Π½ΠΈΡΡ ΡΠ²ΠΎΠΈ API-ΠΊΠ»ΡΡΠΈ ΠΏΡΠΈ ΡΠ΅Π±Π΅, ΠΏΠΎΡΡΠΎΠΌΡ Π½Π΅ Π·Π°Π±ΡΠ΄ΡΡΠ΅ Π·Π°ΠΌΠ΅Π½ΠΈΡΡ Π·Π²Π΅Π·Π΄ΠΎΡΠΊΠΈ Π½Π° ΡΠ²ΠΎΠΉ ΠΊΠ»ΡΡ headersDictionary["Authorization"] = "Bearer *****************" return headersDictionary } static let shared = MoyaNetworkManager() func mainRequest<T: WDTargetType>(_ request: T, withComplition completionHandler: @escaping (ResponseAPI) -> ()) { let endpointClosure = { (target: T) -> Endpoint in let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target) let url = (target.baseURL.absoluteString+target.path).removingPercentEncoding ?? "" return Endpoint(url: url, sampleResponseClosure: defaultEndpoint.sampleResponseClosure, method: target.method, task: target.task, httpHeaderFields: target.headers) } let provider = MoyaProvider<T>(endpointClosure: endpointClosure, stubClosure: MoyaProvider.neverStub)//, stubClosure: MoyaProvider.immediatelyStub) self.moyaProvider = provider //ΠΡΠΏΠΎΠ»Π½ΡΠ΅ΠΌ Π·Π°ΠΏΡΠΎΡΡ Ρ ΠΏΠΎΠΌΠΎΡΡΡ moyaProvider provider.request(request) { result in switch result { case .success(let response): completionHandler(ResponseAPI(statusCode: 0, data: response.data)) case .failure(let error): completionHandler(ResponseAPI(withError: error)) } } } }
OrdersModel.swift
Π’ΡΡ ΠΌΡ ΠΏΡΠΎΠΏΠΈΡΡΠ²Π°Π΅ΠΌ ΡΠ°Π±ΠΎΡΡ ΡΡΠ½ΠΊΡΠΈΠΉ:
import Foundation extension Encodable { /// Encode into JSON and return `Data` func jsonData() throws -> Data { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted encoder.dateEncodingStrategy = .iso8601 return try encoder.encode(self) } } class OrdersModel { static func getDetailOfTask( id: String, completionHandler: @escaping (OrderItem) -> Void, errorHandler: @escaping (WDNetworkError) -> Void) { MoyaNetworkManager.shared.mainRequest(RequestType.ordersDetail(id)) { responseAPI in parseData(responseAPI: responseAPI, type: SubMoyResponse<OrderItem>.self, completion: { response in switch response { case .success(let result): completionHandler(result.fields) case .failure(let error): errorHandler(error) } }) } } static func create(_ order: OrderItem, completionHandler: @escaping (OrderItem?) -> Void, errorHandler: @escaping ( WDNetworkError) -> Void) { MoyaNetworkManager.shared.mainRequest(RequestType.create(order)) { responseAPI in parseData(responseAPI: responseAPI, type: MoyResponse<OrderItem>.self, completion: { response in switch response { case .success(let result): completionHandler(result.records.compactMap({ $0.fields }).first) case .failure(let error): errorHandler(error) } }) } } static func edit(_ order: OrderItem, completionHandler: @escaping (OrderItem?) -> Void, errorHandler: @escaping ( WDNetworkError) -> Void) { MoyaNetworkManager.shared.mainRequest(RequestType.edit(order)) { responseAPI in parseData(responseAPI: responseAPI, type: MoyResponse<OrderItem>.self, completion: { response in switch response { case .success(let result): completionHandler(result.records.compactMap({ $0.fields }).first) case .failure(let error): errorHandler(error) } }) } } static func loadTasks( completionHandler: @escaping ([OrderItem]) -> Void, errorHandler: @escaping ( WDNetworkError) -> Void) { MoyaNetworkManager.shared.mainRequest(RequestType.orders) { responseAPI in parseData(responseAPI: responseAPI, type: MoyResponse<OrderItem>.self, completion: { response in switch response { case .success(let result): completionHandler(result.records.compactMap({ $0.fields })) case .failure(let error): errorHandler(error) } }) } } }
ResponseAPI.swift
ΠΠ΄Π΅ΡΡ ΠΌΡ ΡΠ°ΡΡΠΈΡΡΠΎΠ²ΡΠ²Π°Π΅ΠΌ Π΄Π°Π½Π½ΡΠ΅ (Π΄Π΅ΠΊΠΎΠ΄ΠΈΠΌ).
ΠΠΎΡΠ»Π΅ ΡΡΠΎΠ³ΠΎ ΠΌΡ ΠΏΠ΅ΡΠ΅Ρ ΠΎΠ΄ΠΈΠΌ ΠΊ ΡΠ΅Π΄Π°ΠΊΡΠΈΡΠΎΠ²Π°Π½ΠΈΡ OrderItem:
import Foundation struct SectionOrdersItem { var orders: [OrderItem] = [] var date: Date } //Π Π°ΡΡΠΈΡΡΠ΅ΠΌ ΡΡΡΡΠΊΡΡΡΡ ΠΏΡΠΎΡΠΎΠΊΠΎΠ»ΠΎΠΌ ATProtocol, ΠΎΠΏΡΠ΅Π΄Π΅Π»Π΅Π½Π½ΠΎΠ³ΠΎ Π² NetworkEntities struct OrderItem: ATProtocol { var idAT: String? var summary: String? var deadline: String? var name: String = "" var customer: String? init( idAT: String? = nil, summary: String?, deadline: String?, name: String = "", customer:String?) { self.idAT = idAT self.deadline = deadline self.name = name self.customer = customer } //ΠΡΠΎΠΏΠΈΡΡΠ²Π°Π΅ΠΌ Π΄Π΅ΠΊΠΎΠ΄Π΅Ρ init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: OrderKeys.self) self.summary = try container.decodeIfPresent(String.self, forKey: .summary) self.deadline = try container.decodeIfPresent(String.self, forKey: .deadline) self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" self.customer = try container.decodeIfPresent(String.self, forKey: .customer) ?? "" } //ΠΡΠΎΠΏΠΈΡΡΠ²Π°Π΅ΠΌ ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΠ΅ Π΄Π»Ρ ΠΊΠΎΠ΄ΠΈΡΠΎΠ²Π°Π½ΠΈΡ enum OrderKeys: CodingKey { case idAT case summary case deadline case name case customer } //ΠΠ΅ΡΠΎΠ΄ Π·Π°ΡΠΈΡΡΠΎΠ²ΠΊΠΈ Π΄Π°Π½Π½ΡΡ func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: OrderKeys.self) try container.encodeIfPresent(self.summary, forKey: .summary) try container.encodeIfPresent(self.deadline, forKey: .deadline) try container.encode(self.name, forKey: .name) try container.encodeIfPresent(self.customer, forKey: .customer) } }
OrderAPI Π½Π°ΠΌ ΡΠΆΠ΅ Π±ΠΎΠ»ΡΡΠ΅ Π½Π΅ Π½ΡΠΆΠ΅Π½ ΠΈ Π²ΡΠ΅ ΡΠ²ΡΠ·Π°Π½Π½ΠΎΠ΅ Ρ Π½ΠΈΠΌ ΠΌΠΎΠΆΠ½ΠΎ Π·Π°ΠΊΠΎΠΌΠΌΠ΅Π½ΡΠΈΡΠΎΠ²Π°ΡΡ. ΠΠ°ΠΏΡΡΠΊΠ°Π΅ΠΌ ΠΈ ΡΠΌΠΎΡΡΠΈΠΌ.
ΠΠ°Π½Π½ΡΠ΅ Π±Π΅ΡΡΡΡΡ ΠΈΠ·-Π·Π° AirTable, Π° Π½Π΅ ΠΈΠ· OrderAPI. ΠΡΠ΅ ΠΏΠΎΠ»ΡΡΠΈΠ»ΠΎΡΡ!
ΠΠΎΠΏΡΠΎΠ±ΡΠ΅ΠΌ Π΄ΠΎΠ±Π°Π²ΠΈΡΡ Π·Π°Π΄Π°ΡΡ ΡΠ΅ΡΠ΅Π· ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΈ ΠΏΠΎΡΠΌΠΎΡΡΠΈΠΌ, ΠΎΡΠΎΠ±ΡΠ°Π·ΠΈΡΡΡ Π»ΠΈ ΠΎΠ½Π° Π² ΠΎΠ±ΡΠ΅ΠΉ Π±Π°Π·Π΅ AirTable:
ΠΠ°ΠΊ ΠΌΡ Π²ΠΈΠ΄ΠΈΠΌ, Π΄ΠΎΠ±Π°Π²Π»ΡΡΡ Π΄Π°Π½Π½ΡΠ΅ ΠΌΠΎΠΆΠ½ΠΎ, Π΄Π°ΠΆΠ΅ ΠΏΡΠΎΠΏΡΡΠΊΠ°Ρ Π½Π΅ΠΊΠΎΡΠΎΡΡΠ΅ ΠΏΠ°ΡΠ°ΠΌΠ΅ΡΡΡ:
Π Π°Π½Π°Π»ΠΎΠ³ΠΈΡΠ½ΠΎ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΡ, ΠΌΠΎΠΆΠ½ΠΎ ΡΠ΄Π°Π»ΠΈΡΡ Π·Π°ΠΊΠ°Π· ΠΈΠ· AirTable ΠΈ ΠΎΠ½ Π½Π΅ ΠΎΡΠΎΠ±ΡΠ°Π·ΠΈΡΡΡ Π² ΠΎΠ±ΡΠ΅ΠΌ ΡΠΏΠΈΡΠΊΠ΅.
Π ΠΊΠΎΠ½ΡΠ΅ Π²Ρ ΠΌΠΎΠΆΠ΅ΡΠ΅ ΡΠ²Π΅ΡΠΈΡΡΡΡ Ρ ΡΠΈΠ½Π°Π»ΡΠ½ΡΠΌ ΠΏΡΠΎΠ΅ΠΊΡΠΎΠΌ.
ΠΠ° ΡΡΠΎΠΌ Π²ΡΠ΅!