πŸ“±ΠŸΠΈΡˆΠ΅ΠΌ 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

  1. РСгистрируСмся Π½Π° сайтС https://airtable.com/
  2. Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ‚Π°Π±Π»ΠΈΡ†Ρƒ (ΠΈΠΌΠ΅Π½Π° ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Ρ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° ΠΈ столбцов ΡΠΎΠ²ΠΏΠ°Π΄Π°ΡŽΡ‚) ΠΈ заполняСм Π΅Π΅:

ΠžΠ±Ρ€Π°Ρ‚ΠΈΡ‚Π΅ Π²Π½ΠΈΠΌΠ°Π½ΠΈΠ΅, Ρ‡Ρ‚ΠΎ Ρƒ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ столбца свой Ρ‚ΠΈΠΏ Π΄Π°Π½Π½Ρ‹Ρ…. Π’ΠΈΠΏ Π΄Π°Π½Π½Ρ‹Ρ… ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Ρ‡Π΅Ρ€Π΅Π· 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 ΠΈ ΠΎΠ½ Π½Π΅ отобразится Π² ΠΎΠ±Ρ‰Π΅ΠΌ спискС.

Π’ ΠΊΠΎΠ½Ρ†Π΅ Π²Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΡΠ²Π΅Ρ€ΠΈΡ‚ΡŒΡΡ с Ρ„ΠΈΠ½Π°Π»ΡŒΠ½Ρ‹ΠΌ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠΌ.

На этом всС!

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

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

Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° программиста
16 фСвраля 2018

Мобильная Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π½Π° Python: ΠΎΠ±Π·ΠΎΡ€ Π΄Π²ΡƒΡ… Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊΠΎΠ²

Мобильная Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Π½Π° Python – ΠΎΠ΄Π½ΠΎ ΠΈΠ· пСрспСктивных Π½Π°ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠΉ. Π’ ΡΡ‚Π°Ρ‚ΡŒ...
Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° программиста
13 фСвраля 2018

10 ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹Ρ… ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½Π°ΡƒΡ‡Π°Ρ‚ вас ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ

Π˜Ρ‰Π΅Ρ‚Π΅ курсы, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½Π°ΡƒΡ‡Π°Ρ‚ вас ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ? ΠœΡ‹ собрали Π»ΡƒΡ‡ΡˆΠΈΠ΅ ΠΌΠΎΠ±ΠΈΠ»ΡŒΠ½Ρ‹...
Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° программиста
01 сСнтября 2017

15 классных ΠΈΠ΄Π΅ΠΉ для создания своСго мобильного прилоТСния

Если Π΄ΡƒΠΌΠ°Π»ΠΈ, Ρ‡Ρ‚ΠΎ Π½Π΅ΠΏΠ»ΠΎΡ…ΠΎ Π±Ρ‹ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ свои знания Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΉ Π½Π° ...