Swift | Вопросы собесов
2.13K subscribers
28 photos
953 links
Download Telegram
🤔 Что известно про capture list?

Используется в замыканиях (closures) для управления тем, как замыкание захватывает переменные и константы из окружающего контекста. Могут захватывать и хранить ссылки на любые константы и переменные из контекста, в котором они определены. Это удобно, но может привести к сильным ссылочным циклам и утечкам памяти, если замыкание захватывает self или другие экземпляры класса.

🚩Как он работает

Предоставляет способ определить правила захвата переменных в замыкании. Она задаётся в начале замыкания и позволяет избежать нежелательных сильных ссылок, особенно при работе с self в методах класса, что очень важно для предотвращения утечек памяти в приложениях.

Синтаксис Capture List
{ [capture rule] (parameters) -> return type in
// Код замыкания
}


Пример с простым замыканием
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}

a = 10
b = 10
closure() // Выведет "0 10"


Использование Capture List для предотвращения сильных ссылочных циклов
class MyClass {
var property = "Property"

func doSomething() {
let closure = { [weak self] in
print(self?.property ?? "нет self")
}
closure()
}

deinit {
print("MyClass экземпляр был деинициализирован")
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
2
🤔 Когда value type может храниться в куче?

Value type может быть размещён в куче, если он используется внутри reference type, например, передан как свойство объекта класса.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Какую структуру имеет диспетчирезацию?

Термин "диспетчеризация" часто используется для описания механизма, посредством которого вызывается метод или функция в ответ на вызов. В контексте ООП, особенно в языках с поддержкой полиморфизма, диспетчеризация может быть статической (ранней) или динамической (поздней).

🚩Статическая диспетчеризация (Static Dispatch)

Использует компиляцию времени компиляции для определения того, какой метод будет вызван. Это означает, что компилятор определяет адрес вызываемого метода на этапе компиляции, и этот адрес не изменяется во время выполнения программы.

🟠Плюсы
Быстродействие: Так как адрес метода известен на этапе компиляции, нет необходимости в дополнительных проверках или вычислениях во время выполнения.
Простота: Проще для оптимизации компилятором.

🟠Структура
В языках, таких как C и Swift (при использовании final классов или структур), методы, известные во время компиляции, могут быть вызваны без дополнительной нагрузки, связанной с поиском в таблице виртуальных функций.

🚩Динамическая диспетчеризация (Dynamic Dispatch)

Используется в языках программирования, поддерживающих полиморфизм и наследование, и означает, что метод, который будет вызван, определяется во время выполнения. Это позволяет объектам различных классов отвечать на одни и те же сообщения (вызовы методов), каждый по-своему.

🟠Плюсы
Гибкость: Позволяет объектам разных классов обрабатывать одинаковые вызовы методов различным образом.
Поддержка полиморфизма: Один и тот же вызов метода может вести к выполнению разных функций.

🟠Структура
Таблица виртуальных методов (VMT): В объектно-ориентированных языках, таких как C++ или Java, каждый класс с виртуальными методами имеет таблицу виртуальных методов. Эта таблица содержит адреса всех виртуальных методов, которые могут быть вызваны для объекта данного класса. Поиск по таблице: Во время выполнения, когда вызывается метод, производится поиск соответствующего метода в таблице VMT, и используется адрес, найденный в таблице, что вносит задержку по сравнению со статической диспетчеризацией.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🤔 Чем отличаются слабые и сильные ссылки?

- Сильные ссылки (strong): увеличивают счётчик ссылок объекта, предотвращая его удаление.
- Слабые ссылки (weak): не увеличивают счётчик ссылок, используются для предотвращения циклических зависимостей.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
🤔 Как устроено наследование?

Это ключевой механизм ООП, позволяющий классам наследовать свойства, методы и другие характеристики от других классов. Это позволяет создавать новые классы на основе существующих, расширяя их функциональность или изменяя её.

🚩Основы наследования

🟠Определение базового класса
Базовый класс определяет общие свойства и методы, которые могут быть унаследованы подклассами.

🟠Создание подкласса
Подкласс наследует (или "расширяет") базовый класс. Он может переопределять унаследованные методы и свойства, добавлять новые методы и свойства, а также добавлять инициализаторы или изменять существующие.

🟠Переопределение методов и свойств
Подклассы могут переопределять методы, свойства и индексаторы базового класса для изменения или расширения их поведения.

🟠Предотвращение переопределения
Можно предотвратить переопределение методов, свойств или индексаторов с помощью ключевого слова final. Если метод, свойство или индексатор объявлен как final, то он не может быть переопределён в подклассе.
class Vehicle {
var currentSpeed = 0.0
var description: String {
return "traveling at \(currentSpeed) miles per hour"
}
func makeNoise() {
// Этот метод будет переопределен в подклассах, если необходимо
}
}

class Bicycle: Vehicle {
var hasBasket = false
}

class Car: Vehicle {
var gear = 1
final func drive() {
print("Car is moving")
}
override func makeNoise() {
print("Vroom!")
}
}


🚩Использование super

Подклассы могут вызывать методы своего суперкласса с помощью ключевого слова super. Это позволяет подклассам расширять, а не заменять поведение суперкласса.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое Grand Central Dispatch (GCD)?

Это технология для работы с многопоточностью в iOS и macOS. Она управляет очередями задач (sync/async, serial/concurrent) и позволяет эффективно распределять задачи между потоками.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
🤔 За счёт чего стек быстрее кучи?

Обычно имеют в виду различия между использованием стека вызовов (память стека) и кучи (heap) в контексте управления памятью в программировании. Эти два типа памяти имеют разные способы выделения и освобождения памяти, каждый со своими преимуществами и недостатками.

🟠Способ выделения памяти
Память в стеке выделяется и освобождается по очень простому и быстрому принципу: LIFO (Last In, First Out). Это означает, что для выделения памяти достаточно переместить указатель стека вверх (при добавлении данных) или вниз (при удалении данных). Этот процесс почти мгновенен и не требует дополнительных вычислений.
В куче память выделяется динамически, что требует управления доступными блоками памяти. Аллокатор памяти должен найти достаточно большой свободный блок, что может занять значительное время, особенно если память фрагментирована. Также освобождение памяти в куче требует более сложной обработки, включая возможную дефрагментацию.

🟠Скорость доступа
Доступ к данным в стеке очень быстрый, потому что данные всегда добавляются и удаляются с "вершины" стека, где находится указатель стека. Это делает доступ к текущим локальным переменным очень быстрым и предсказуемым. Доступ к данным в куче может быть менее эффективным, поскольку данные могут быть разбросаны по разным частям памяти. Кроме того, дополнительное время требуется для поиска и управления блоками памяти.

🟠Детерминированное управление памятью
Его память автоматически очищается при выходе из области видимости, что упрощает управление памятью и снижает риск утечек памяти. Память, выделенная в куче, остаётся занятой до тех пор, пока явно не будет освобождена. Это увеличивает риск утечек памяти, если разработчик забудет освободить память.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
🤔 Что под капотом кучи?

Это область памяти, управляемая системой, где объекты выделяются динамически. Управление включает:
- Аллокацию памяти.
- Освобождение через сборщик мусора (в Java, Swift) или вручную (в C++).
- Компактирование для предотвращения фрагментации.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2🤯2👍1
🤔 Что под капотом стэка?

Это абстрактная структура данных, работающая по принципу LIFO (Last In, First Out), что означает "последний пришёл — первый вышел". Это значит, что последний добавленный элемент будет первым при извлечении из стека. Под капотом реализации стека могут быть разные, и они зависят от конкретного языка программирования и задач, которые необходимо решить.

🟠Массивы
Один из самых распространённых способов реализации стека — это использование массива. В такой реализации элементы стека хранятся в массиве, и индекс последнего элемента (вершина стека) отслеживается отдельной переменной.
struct Stack<Element> {
private var storage: [Element] = []

mutating func push(_ element: Element) {
storage.append(element)
}

mutating func pop() -> Element? {
return storage.popLast()
}

func peek() -> Element? {
return storage.last
}

var isEmpty: Bool {
return storage.isEmpty
}
}


🟠Связные списки
Стек можно реализовать с использованием связных списков, где каждый элемент списка содержит данные и ссылку на следующий элемент в стеке. Вершина стека в такой реализации — это начало связного списка.
class Node<Element> {
var value: Element
var next: Node?

init(value: Element) {
self.value = value
}
}

struct Stack<Element> {
private var head: Node<Element>?

mutating func push(_ element: Element) {
let node = Node(value: element)
node.next = head
head = node
}

mutating func pop() -> Element? {
let node = head
head = head?.next
return node?.value
}

func peek() -> Element? {
return head?.value
}

var isEmpty: Bool {
return head == nil
}
}


🟠Стек вызовов
Это системный стек, который используется во время выполнения программы для хранения информации о вызовах функций/методов. Он хранит адреса возврата, параметры функций, локальные переменные и другие данные, необходимые для управления вызовами функций и их возврата.

🚩Зачем он нужен?

Обратную польскую нотацию для вычисления арифметических выражений. Управление вызовами функций в программном стеке. Поддержка операций undo в приложениях.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
4
🤔 Когда value типы могут стать reference?

Value типы становятся reference, если они упакованы (например, через Box в Swift) или если они хранятся в контейнерах, которые управляются ссылочным механизмом (например, массивы при копировании).

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Как создаешь анимации в приложении?

В iOS-приложениях можно создавать анимации несколькими способами.

🟠Использование `UIView.animate`
Самый простой способ анимации представлений (views) в iOS - это использование метода UIView.animate. Вот пример кода, который изменяет положение и прозрачность представления за 1 секунду:
UIView.animate(withDuration: 1.0) {
myView.frame.origin.y += 100
myView.alpha = 0.5
}


🟠Использование `CABasicAnimation`
Для более сложных анимаций можно использовать Core Animation, например, CABasicAnimation. Вот пример анимации изменения позиции слоя (layer):
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(cgPoint: CGPoint(x: 50, y: 50))
animation.toValue = NSValue(cgPoint: CGPoint(x: 150, y: 150))
animation.duration = 1.0
myView.layer.add(animation, forKey: "positionAnimation")


🟠Использование `UIViewPropertyAnimator`
Этот класс предоставляет более детальный контроль над анимациями. Его можно использовать для создания и управления анимациями в реальном времени:
let animator = UIViewPropertyAnimator(duration: 1.0, curve: .easeInOut) {
myView.frame.origin.y += 100
myView.alpha = 0.5
}
animator.startAnimation()


🟠Использование анимаций с пружинным эффектом
Для создания реалистичных анимаций с пружинным эффектом можно использовать метод UIView.animate с параметрами пружинного демпфирования:
UIView.animate(withDuration: 1.0,
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.5,
options: [],
animations: {
myView.frame.origin.y += 100
myView.alpha = 0.5
}, completion: nil)


🟠Анимация переходов между контроллерами
Для анимации переходов между экранами используется UIViewControllerAnimatedTransitioning. Это требует реализации методов протокола UIViewControllerAnimatedTransitioning:
class CustomTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from),
let toView = transitionContext.view(forKey: .to) else { return }

transitionContext.containerView.addSubview(toView)
toView.alpha = 0.0

UIView.animate(withDuration: 0.5, animations: {
toView.alpha = 1.0
}, completion: { finished in
transitionContext.completeTransition(finished)
})
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое UIResponder?

Это базовый класс для обработки событий в UIKit. Объекты типа UIView, UIViewController, UIApplication наследуются от UIResponder и участвуют в цепочке обработки событий, таких как нажатия, свайпы или жесты.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое unowned?

Это ключевое слово, используемое для объявления слабой ссылки (weak reference) на объект, которая не увеличивает счетчик ссылок этого объекта. В отличие от weak, unowned ссылки никогда не становятся nil, поэтому они используются в тех случаях, когда можно гарантировать, что объект, на который ссылаются, будет существовать так же долго, как и сама ссылка.

🚩Когда использовать?

🟠Оба объекта существуют одновременно
Например, если один объект никогда не будет существовать дольше другого объекта, и тем самым вы уверены, что ссылка всегда будет действительной.

🟠Избегание циклов сильных ссылок
Чтобы предотвратить циклы сильных ссылок, которые могут привести к утечкам памяти.

🚩Пример использования `unowned`

``` swift
class Person {
let name: String
var creditCard: CreditCard?

init(name: String) {
self.name = name
}

deinit {
print("\(name) is being deinitialized")
}
}

class CreditCard {
let number: String
unowned let owner: Person

init(number: String, owner: Person) {
self.number = number
self.owner = owner
}

deinit {
print("CreditCard #\(number) is being deinitialized")
}
}

// Пример использования
var john: Person? = Person(name: "John Appleseed")
john?.creditCard = CreditCard(number: "1234 5678 9012 3456", owner: john!)

john = nil
```

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое деинициализатор?

Это метод, который вызывается перед удалением объекта из памяти. В Swift используется метод deinit. Он позволяет выполнять действия, такие как освобождение ресурсов или закрытие соединений, когда объект больше не нужен.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое очередь?

Это структура данных, которая работает по принципу "первым пришел - первым ушел" (FIFO, First In, First Out). Это значит, что элементы добавляются в конец очереди и извлекаются из начала очереди.

🚩Основные операции с очередью

🟠Добавление (enqueue)
Вставка элемента в конец очереди.
🟠Удаление (dequeue)
Удаление элемента из начала очереди.
🟠Проверка первого элемента (peek)
Просмотр элемента в начале очереди без его удаления.
🟠Проверка на пустоту (isEmpty)
Проверка, содержит ли очередь элементы.

🚩Примеры использования

Очереди часто используются в задачах, где порядок обработки элементов важен.
Управление задачами в системах реального времени.
Обработка запросов в сетевых приложениях.
Имплементация алгоритмов обхода графов (например, поиск в ширину).

struct Queue<T> {
private var elements: [T] = []

// Добавление элемента в конец очереди
mutating func enqueue(_ element: T) {
elements.append(element)
}

// Удаление элемента из начала очереди
mutating func dequeue() -> T? {
return isEmpty ? nil : elements.removeFirst()
}

// Просмотр элемента в начале очереди
func peek() -> T? {
return elements.first
}

// Проверка на пустоту
var isEmpty: Bool {
return elements.isEmpty
}
}

// Пример использования
var queue = Queue<Int>()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)

print(queue.dequeue() ?? "Queue is empty") // Вывод: 1
print(queue.peek() ?? "Queue is empty") // Вывод: 2


🚩Варианты использования в iOS

🟠Очереди задач (DispatchQueue)
GCD (Grand Central Dispatch) использует очереди для управления многозадачностью. Например, DispatchQueue.main.async добавляет задачи в очередь для выполнения на главном потоке:
DispatchQueue.main.async {
// Код выполняется на главном потоке
}


🟠Операционные очереди (OperationQueue)
Используются для управления и упорядочивания выполнения множества Operation объектов:
let queue = OperationQueue()
queue.addOperation {
print("Operation 1")
}
queue.addOperation {
print("Operation 2")
}


Ставь
👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔 Что такое retain/release?

Это механизм управления памятью в системах с подсчетом ссылок (Reference Counting):
1. Retain: увеличивает счетчик ссылок объекта, указывая, что объект используется.
2. Release: уменьшает счетчик ссылок. Когда счетчик достигает нуля, объект освобождается из памяти.
Эта модель широко использовалась в Objective-C до появления ARC (Automatic Reference Counting).


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
🤔 Знаешь, что относится к value типам помимо структур в свифте?

В Swift, помимо структур (structs), к типам-значениям (value types) также относятся перечисления (enums) и кортежи (tuples).

🟠Структуры (structs)
Структуры в Swift являются типами-значениями. Когда вы создаете копию структуры, вы получаете новый экземпляр со своим собственным набором данных. Изменения в одном экземпляре не влияют на другие экземпляры.
struct Point {
var x: Int
var y: Int
}

var point1 = Point(x: 0, y: 0)
var point2 = point1
point2.x = 10

print(point1.x) // Вывод: 0
print(point2.x) // Вывод: 10


🟠Перечисления (enums)
Перечисления также являются типами-значениями. При копировании экземпляра перечисления создается новый экземпляр с тем же значением.
enum CompassDirection {
case north, south, east, west
}

var direction1 = CompassDirection.north
var direction2 = direction1
direction2 = .south

print(direction1) // Вывод: north
print(direction2) // Вывод: south


🟠Кортежи (tuples)
Кортежи в Swift тоже являются типами-значениями. Кортежи позволяют объединять несколько значений в одну составную единицу. При копировании кортежа создается новый кортеж с теми же значениями.
var tuple1 = (a: 1, b: 2)
var tuple2 = tuple1
tuple2.a = 3

print(tuple1.a) // Вывод: 1
print(tuple2.a) // Вывод: 3


🚩Отличия типов-значений от ссылочных типов

Типы-значения копируются при передаче и присваивании, а ссылочные типы (классы и замыкания) передаются по ссылке. Это важное различие влияет на то, как изменяются данные при передаче между переменными и функциями.

🚩Пример с классами для сравнения

Для сравнения, классы являются ссылочными типами (reference types). При копировании экземпляра класса копируется не сам объект, а ссылка на него. Поэтому изменения в одном экземпляре отражаются на всех его копиях.
class Person {
var name: String
init(name: String) {
self.name = name
}
}

var person1 = Person(name: "John")
var person2 = person1
person2.name = "Doe"

print(person1.name) // Вывод: Doe
print(person2.name) // Вывод: Doe


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 Есть ли у unowned ссылок счетчик?

Нет, у unowned ссылок нет собственного счетчика ссылок. Они не увеличивают счетчик объекта, к которому ссылаются. Это означает, что если объект удален из памяти, попытка обращения к unowned ссылке вызовет runtime-ошибку.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔1
🤔 Какие есть структуры данных?

В Swift и других языках программирования существует множество структур данных, каждая из которых предназначена для эффективного хранения, организации и управления данными.

🟠Массивы (Arrays)
Это упорядоченные коллекции элементов, которые хранятся в непрерывной области памяти. Массивы позволяют быстро получать доступ к элементам по индексу.
var numbers: [Int] = [1, 2, 3, 4, 5]
print(numbers[2]) // Вывод: 3


🟠Словари (Dictionaries)
Это коллекции пар "ключ-значение", которые позволяют быстро находить значения по ключу. Ключи в словаре должны быть уникальными.
var capitalCities: [String: String] = ["France": "Paris", "Japan": "Tokyo"]
print(capitalCities["France"]!) // Вывод: Paris


🟠Множества (Sets)
Это неупорядоченные коллекции уникальных элементов. Они полезны, когда необходимо проверить наличие элемента или выполнить операции над множествами, такие как объединение или пересечение.
var uniqueNumbers: Set<Int> = [1, 2, 3, 4, 5, 1]
print(uniqueNumbers) // Вывод: [5, 2, 3, 1, 4]


🟠Связные списки (Linked Lists)
Это последовательности элементов, где каждый элемент содержит ссылку на следующий элемент. Связные списки могут быть односвязными (только вперед) или двусвязными (вперед и назад).
class ListNode {
var value: Int
var next: ListNode?

init(value: Int) {
self.value = value
}
}

let head = ListNode(value: 1)
head.next = ListNode(value: 2)
head.next?.next = ListNode(value: 3)


🟠Стеки (Stacks)
Это структура данных, работающая по принципу "последним пришел - первым ушел" (LIFO, Last In, First Out). Стек поддерживает две основные операции: добавление (push) и удаление (pop) элемента.
var stack: [Int] = []
stack.append(1) // push
stack.append(2)
print(stack.pop()!) // pop, вывод: 2


🟠Очереди (Queues)
Это структура данных, работающая по принципу "первым пришел - первым ушел" (FIFO, First In, First Out). Очередь поддерживает операции добавления (enqueue) и удаления (dequeue) элемента.
var queue: [Int] = []
queue.append(1) // enqueue
queue.append(2)
print(queue.removeFirst()) // dequeue, вывод: 1


🟠Деревья (Trees)
Это иерархическая структура данных, состоящая из узлов, где каждый узел имеет одно родительское и ноль или более дочерних узлов. Наиболее распространенный тип дерева - бинарное дерево, где каждый узел имеет не более двух потомков.
class TreeNode {
var value: Int
var left: TreeNode?
var right: TreeNode?

init(value: Int) {
self.value = value
}
}

let root = TreeNode(value: 1)
root.left = TreeNode(value: 2)
root.right = TreeNode(value: 3)


🟠Графы (Graphs)
Это набор узлов (вершин), соединенных ребрами. Графы могут быть направленными или ненаправленными, взвешенными или невзвешенными.
class GraphNode {
var value: Int
var neighbors: [GraphNode] = []

init(value: Int) {
self.value = value
}
}

let node1 = GraphNode(value: 1)
let node2 = GraphNode(value: 2)
let node3 = GraphNode(value: 3)
node1.neighbors = [node2, node3]
node2.neighbors = [node1]
node3.neighbors = [node1]


🟠Хеш-таблицы (Hash Tables)
Это структура данных, которая реализует словарь с использованием хеш-функции для быстрого доступа к данным по ключу.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
3
🤔 Зачем сделан сайд-таблица (side table)?

Сайд-таблица используется для хранения дополнительной информации об объектах, управляемых системой подсчета ссылок:
1. Счетчик ссылок: хранится для каждого объекта.
2. Другие данные: например, слабые ссылки (weak references) или ассоциированные объекты.
Она позволяет эффективно управлять объектами без увеличения их базового размера.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1