Универсальные функции и типы в Swift в настоящее время требуют фиксированного количества параметров типа. Невозможно написать функцию или тип, который принимает произвольное количество аргументов с различными типами, не прибегая к одному из следующих обходных путей:
Стирание всех задействованных типов, например, с помощью Any...
Использование одного/единого аргумента типа кортежа вместо отдельных аргументов типа
Перегрузка длины каждого аргумента искусственным ограничением
В одной только Стандартной Библиотеке Swift есть целый ряд примеров таких обходных путей, как 6 перегрузок для каждого оператора сравнения кортежей:
func < (lhs: (), rhs: ()) → Bool
func < <A, B>(lhs: (A, B), rhs: (A, B)) → Bool where A : Comparable, B : Comparable
func < <A, B, C>(lhs: (A, B, C), rhs: (A, B, C)) → Bool where A : Comparable, B : Comparable, C : Comparable
func < <A, B, C, D>(lhs: (A, B, C, D), rhs: (A, B, C, D)) → Bool where A : Comparable, B : Comparable, C : Comparable, D : Comparable
func < <A, B, C, D, E>(lhs: (A, B, C, D, E), rhs: (A, B, C, D, E)) → Bool where A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable
func < <A, B, C, D, E, F>(lhs: (A, B, C, D, E, F), rhs: (A, B, C, D, E, F)) → Bool where A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable, F : Comparable
Данные вариативные API, созданные специально для этой задачи, наглядно демонстрируют/показывают зияющую дыру в системе дженериков. Все вышеуказанные обходные пути имеют значительные компромиссы:
Стирание всех задействованных типов приносит в жертву безопасность статического типа.
Использование единого аргумента типа кортежа жертвует выразительностью, делая отдельные типы элементов непрозрачными, предотвращая работу кода с элементами по отдельности.
Перегрузка длины каждого аргумента жертвует производительностью во время компиляции, ремонтопригодностью библиотеки и накладывает произвольные ограничения на клиентов API.
Вариативные дженерики - это особенность языка, позволяющая писать универсальный код, который абстрагирует переменное количество параметров типа. Целями добавления вариативных дженериков в Swift являются:
Выражение вариативных API с помощью одной реализации, которая абстрагируется по длине, устраняя произвольные ограничения и сокращая работу разработчиков и затраты на обслуживание библиотеки при множественных перегрузках.
Включение обобщения типов кортежей с абстрактными элементами, выражая абстрактные элементы с точки зрения вариативных дженериков.
Поддержка отдельной компиляции вариативного общего(*универсального) кода путем введения понятия абстрактной длины в систему типов, включая представление длины в общих сигнатурах.
Используя вариативные дженерики, вышеуказанные перегрузки операторов могут быть записаны как исполнение < , которое принимает картежи абстрактной длины:
func < <Element...>(lhs: (Element...), rhs: (Element...) -> Bool where Element: Comparable {
for (left, right) in (lhs.element, rhs.element)... {
if left < right { return true }
if left > right { break }
}
return false
}
Подход
Концепция вариативных дженериков, описанная в этой документации, включает в себя:
Конструкцию для абстрагирования по списку с 0 или более параметров на уровне типа и значения и использования такого списка в позициях, которые естественным образом принимают список типов или значений.
Список операции над абстрактным списком типов/значений, включая отображение(*преобразование), итерацию, конкатенацию и деструктуризацию.
Проецирование элементов из абстрактного списка c 0 или более типов или значений.
Распаковка кортежей в абстрактный список с 0 или более элементов.
Вышеуказанные функции обеспечивают большую часть той же функциональности, которую программисты ожидают от Sequence. Однако с вариативными дженериками элементы в списке имеют свой тип, что делает список разнородным. Чтобы выразить статический интерфейс, содержащий абстрактный разнородный список, операции списка должны быть выражены на уровне типа. Явная цель удобства использования вариативных дженериков в Swift заключается в том, что эти разнородные операции списка выражаются одинаково на уровне типов и на уровне значений.
Пакеты параметров: основа абстракции длины
Пакеты параметров - это основная концепция, которая облегчает абстрагирование по переменному количеству параметров. Пакет - это новый вид объекта на уровне типа и на уровне значения, который представляет собой список типов или значений и имеет абстрактную длину. Пакет параметров типа хранит список из 0 или более параметров типа, а пакет параметров значения хранит список из 0 или более параметров значения.
Пакет параметров типа объявляется в угловых скобках с использованием многоточия:
// 'S' is a type parameter pack
struct ZipSequence<S...> { }
Пакет параметров значения - это параметр функции, тип которого содержит ссылку на пакет параметров типа, за которым следует многоточие:
// 'value' is a value parameter pack
func variadicPrint<T...>(_ value: T...) { }
Пакеты параметров заменены списком из нуля или более аргументов. В этой документации конкретные пакеты типов и значений будут обозначаться как список типов или значений, разделенных запятыми, в фигурных скобках, например. {Int, String, Bool} и {1, "hello!", true}, соответственно. Обратите внимание, что концепция(*конструкция) не включает синтаксис для написания конкретных пакетов на самом языке.
struct Tuple<Element...> { }
Tuple<Int> // T := {Int}
Tuple<Int, String, Bool> // T := {Int, String, Bool}
Tuple<> // T := { }
Расширение шаблонного пакета
Пакет параметров сам по себе не является значением или типом первого класса, но используя расширения пакета, его элементы могут использоваться в любом месте, где естественным образом принимается список значений или типов, разделенных запятыми. Расширение распаковывает элементы пакета в список, разделенный запятыми, и элементы могут быть добавлены к любой стороне расширения пакета с помощью внесения в список большего количества значений, разделенных запятыми.
Расширение пакета состоит из типа или выражения, за которым следует “…”. Многоточие называется оператором расширения, а тип или выражение, к которому применяется оператор расширения, называется шаблоном(*паттерном) повторения. Шаблон(*паттерн) повторения должен содержать ссылки на пакеты. Учитывая замену конкретного пакета, шаблон(*паттерн) повторяется для каждого элемента в заменяемом пакете.
Рассмотрим пакет параметров типа T и расширение пакета Mapped<T> .... Замена конкретного пакета {Int, String, Bool} расширит шаблон Mapped<T>, повторив его для каждого элемента в конкретном пакете, заменяя конкретный тип в каждой позиции на ссылку пакета T . Это создает новый список типов Mapped<Int>, Mapped<String>, Mapped<Bool>, разделенных запятыми. Следующий код демонстрирует подмену конкретного пакета:
struct Mapped<Value> { }
func map<T...>(_ t: T...) -> (Mapped<T>...) {
return (Mapped(t)...)
}
map(1, "hello", true)
В приведенном выше коде вызов вариативной функции map выводит подмену пакета параметров типа T:= {Int, String, Bool} из значений аргументов. Расширение шаблона(;паттерна) повторения Mapped<T> в тип кортежа приводит к возвращаемому типу кортежа (Mapped<Int>, Mapped<String>, Mapped<Bool>). Подмена пакета параметров значения на t := {1, "hello", true} и развертывание шаблона(*паттерна) повторения Mapped(t) в значение кортежа дает возвращаемое значение кортежа (Mapped(1), Mapped("hello"), Mapped (true))
Примечание о соглашении об именовании
В коде программисты, естественно, используют имена множественного числа для переменных, представляющих списки. Однако в данной конструкции для вариативных дженериков имена пакетов могут появляться только в шаблонах(* паттернах) повторения, где тип или выражение представляют собой отдельный тип или значение, которое повторяется при расширении. Рекомендуемое соглашение об именовании заключается в использовании имен единственного числа для пакетов параметров, а имена во множественном числе только в названии/метке аргументов:
struct List<Element...> {
let element: Element...
init(elements element: Element...)
}
List(elements: 1, "hello", true)
В более широком смысле пакеты принципиально отличаются от типов и значений первого класса в Swift. Сами пакеты не являются типами или значениями; они представляют собой особый вид сущностей, позволяющий вам написать один фрагмент кода, который повторяется для N отдельных типов или значений. Например, рассмотрим пакет свойств, содержащий element, в типе List:
struct List<Element...> {
let element: Element...
}
Как нам представить пакет свойств let element: Element... - это "свойство, называемое element, с типом Element, повторяющееся N раз". Когда List инициализируется с тремя конкретными аргументами, например: List<Int, String, Bool>, хранимый(хранящийся) пакет свойств расширяется до 3 соответствующих сохраненных свойств типа Int, String и Bool.
Вы можете представить специализацию List для {Int, String, Bool} следующим образом:
struct List<Int, String, Bool> {
let element.0: Int
let element.1: String
let element.2: Bool
}
Уникальный характер названий пакетов становится более очевидным с более сложными шаблонами повторения. Рассмотрим следующий метод withOptionalElements, который возвращает новый список, содержащий необязательные элементы:
extension List {
func withOptionalElements() -> List<Optional<Element>...> {
return List(elements: Optional(element)...)
}
}
В возвращаемом типе шаблон повторения Optional<Element>... означает, что для каждого отдельного элемента в пакете параметров существует опциональный тип. Когда этот метод вызывается для List<Int, String, Bool>, шаблон повторяется один раз для каждого отдельного типа в списке, заменяя ссылку на Element этим отдельным типом, в результате чего получается Optional<Int>, Optional<String>, Optional<Bool>.
Соглашение об именовании в единственном числе поощряет данный образ мышления о пакетах параметров.
Статическая форма пакета параметров
Проверка кода вариативных дженериков отдельно от его применения требует введения понятия абстрактной длины списка параметров типа в систему типов. Операции над параллельными списками, такие как статическое архивирование двух отдельных списков параметров типов для создания нового списка двухэлементных типов кортежей, требуют, чтобы несколько списков имели одинаковую длину.
Требования того же типа в дальнейшем будут характеризовать пакеты. Например, рассмотрим следующую общую подпись firstRemoved, которая содержит требование одного типа, включающее два пакетных расширения:
struct List<Element...> {
func firstRemoved<First, Rest...>() -> List<Rest...> where (Element...) == (First, Rest...) { }
}
Требование (Element...) == (First, Rest...) определяет следующие статические свойства::
Первый элемент пакета параметров Element равен First.
Остальные элементы пакета параметров Element равны Rest
Эти свойства называются формой пакета параметров. Формально форма пакета - это или:
Один скалярный элемент; все скалярные (т.е. непакетные/*без упаковки) типы имеют уникальную скалярную форму
Абстрактная форма, специфичная для пакета параметров
Конкретная форма, состоящая из скалярной формы и абстрактных форм
Например, пакет {First, Rest...} имеет конкретную форму, состоящую из одного элемента скалярного типа и одной абстрактной формы, соответствующей Rest.
Операции по деструктуризации с использованием статической формы
Статическая форма пакета может позволить разбивать пакеты с конкретной формой в составные элементы:
struct List<Element...> {
let element: Element...
}
extension List {
func firstRemoved<First, Rest...>() -> List<Rest...> where (Element...) == (First, Rest...) {
let (first, rest) = (element...)
return List(rest...)
}
}
let list = List(1, "Hello", true)
let firstRemoved = list.firstRemoved() // 'List("Hello", true)’
Тело firstRemoved разбивает Element на компоненты его формы — одно значение типа First и пакет значений типа Rest... — эффективно удаляя первый элемент из списка.
Итерация пакетов
Все операции списка могут быть переданы с помощью выражений расширения пакета путем факторизации кода, помещая условные операторы в функции или замыкании. Однако этот подход не допускает short-circuiting (это концепция программирования, при которой компилятор пропускает выполнение или оценку некоторых подвыражений в логическом выражении.
Компилятор прекращает вычисление дальнейших подвыражений, как только значение выражения определено.*) , потому что выражение шаблона(* паттерна) всегда будет оцениваться один раз для каждого элемента в пакете. Кроме того, требовать функцию или замыкание для кода, включающего условные операторы, неестественно. Решить обе эти проблемы можно с помощью перебора пакетов циклом for-in.
Пакеты значений могут быть расширены до источника цикла for-in, что позволяет перебирать каждый элемент в пакете и привязывать каждое значение к локальной переменной:
func allEmpty<T...>(_ array: [T]...) -> Bool {
for array in array... {
guard array.isEmpty else { return false }
}
return true
}
Тип локальной переменной array в приведенном выше примере — это Array непрозрачного типа элемента с требованиями, которые написаны на T. Для (1, 2, 3…)-й итерации, тип элемента — это параметр (1, 2, 3…)-го типа в пакете параметров типа T соответственно.
Проекция элемента пакета
Сценарии использования вариативных дженериков, которые прерывают итерацию пакета на(при) вызовы(ах) функции, требуют произвольного доступа или работают с конкретными пакетами, - поддерживаются в этом дизайне путем проецирования отдельных элементов из пакета параметров.
Так как элементы пакета имеют разные типы, существует два подхода к проецированию элементов пакета: с использованием индекса Int, который вернет динамический тип элемента, и использование индекса статического типа, который получает параметр по запрашиваемому типу элемента пакета.
Динамическая индексация пакета с помощью Int
Динамическая индексация пакета полезна, когда конкретный тип элемента неизвестен или когда все индексы должны иметь один и тот же тип, например, для манипулирования индексом или хранения значения индекса. Пакеты будут поддерживать вызовы сабскрипта с индексом Int, который будет возвращать динамический тип элемента пакета непосредственно как открытый базовый тип, который может быть назначен локальной переменной с непрозрачным типом. Значения этого типа необходимо стереть или привести к другому типу, чтобы вернуть значение элемента из функции:
func element<T...>(at index: Int, in t: T...) where T: P -> any P {
// The subscript returns 'some P', which is erased to 'any P'
// based on the function return type.
let value: some P = t[index]
return value
}
Рассмотрим следующую структуру данных ChainCollection, которая имеет пакет параметров типа коллекций с тем же типом Element:
struct ChainCollection<Element, C...> where C: Collection, C.Element == Element {
var collection: C...
}
ChainCollection реализует протокол Collection. Итерация по ChainCollection будет повторяться по всем значениям Element для данной коллекции в collections, прежде чем переходить к следующей коллекции в пакете. Внутри, индекс в ChainCollection является двухмерным — у него есть компонент для коллекции в пакете и другой компонент для индекса в этой коллекции:
struct ChainCollectionIndex: Comparable {
// The position of the current collection in the pack.
var collectionPosition: Int
// The position of the element in the current collection.
var elementPosition: any Comparable
}
Теперь сабскрипт ChainCollection можно реализовать, сначала проиндексировав пакет, чтобы получить текущую коллекцию, используя позицию коллекции, а затем проиндексировав эту коллекцию, используя позицию элемента:
extension ChainCollection {
subscript(position: ChainCollectionIndex) -> Element {
func element<C: Collection<Element>>(in c: C, at index: any Comparable) -> Element {
guard let index = index as? C.Index else { fatalError() }
return c[index]
}
return element(in: collection[position.collectionPosition],
at: position.elementPosition)
}
}
Проекция элемента типизированного пакета с использованием ключевых путей
Приведенный выше способ применения ChainCollection требует использования одного и того же типа индекса для всех элементов в пакете. Однако в других сценариях использования проецирования элемента пакета заранее известно, какой тип внутри пакета будет проецироваться, и может использоваться статически типизированный индекс пакета.
Статически типизированный индекс пакета может быть представлен с помощью KeyPath, параметр которого получен по базовому типу для доступа (т. е. пакету) и результирующему типу значения (т. е. элементу пакета для проецирования). Проекция элемента пакета через ключевые пути выпадает из 1) позиционных ключевых путей кортежа и 2) расширения пакетов в значения кортежа:
struct Tuple<Elements...> {
var elements: (Elements...)
subscript<Value>(keyPath: KeyPath<(Element...), Value>) -> Value {
return elements[keyPath: keyPath]
}
}
То же позиционное приложение для ключевого пути должно поддерживаться непосредственно на пакетах:
func apply<T..., Value>(keyPath: KeyPath<T..., Value>, to t: T...) -> Value {
return t[keyPath: keyPath]
}
let value: Int = apply(keyPath: \.0, to: 1, "hello", false)
Конкретные пакеты
Пакеты хранимых свойств помогают избежать вариативные универсальные типы, которым требуется кортеж для хранения пакетов. Для включения пакетов хранимых свойств требуется допустить в язык конкретные пакеты.
Конкретные пакеты значений нуждаются в возможности доступа к отдельным элементам в пакете. Это можно сделать с помощью проекции элемента типизированного пакета непосредственно на конкретный пакет значений:
struct Tuple<Element...> {
var element: Element...
init(elements element: Element...) { self.element = element... }
}
let tuple = Tuple(1, "hello", true)
let number: Int = tuple.element.0
let message: String = tuple.element.1
let condition: Bool = tuple.element.2
Проверенный на статический тип код, который абстрагирует по длине аргумента, требует статического представления типа элемента пакета. Конкретные пакеты также можно использовать в коде, который абстрагирует по длине аргумента, написав аннотацию типа для абстрактного типа элемента:
struct Tuple<Element...> {
var element: Element...
}
func iterate(over tuple: Tuple<Int, String, Bool>) {
for value: some Equatable in tuple.element... {
// do something with an 'Equatable' value
}
}
Многомерные пакеты
Включение связанных пакетов типов необходимо для написания протоколов, выражающих интерфейс для вариативных универсальных типов. Например:
protocol HeterogeneousSequence<Element...> {
associatedtype Element...
}
struct List<Element...>: HeterogeneousSequence {}
В полном общем плане связанные пакеты типов вводят многомерные пакеты в язык:
func twoDimensional<T...>(_ t: T...) where T: HeterogeneousSequence {}
В приведенной выше универсальной подписи для twoDimensional, T.Element представляет собой многомерный пакет. Может быть полезно ввести способ выражения пакетных расширений в многомерных пакетах, например. чтобы свести все значения Element в один список, но непонятно, каким образом это может быть выражено.
Чтобы включить пакеты связанных типов в краткосрочной перспективе, можно ограничить требования соответствия, включающие протоколы с пакетами связанных типов, для скалярных параметров типа, не являющихся пакетами.
Доступ к элементам кортежа как к пакету
Для достижения цели использования вариативных дженериков для обобщения типов кортежей эта конструкция включает возможность получения элементов значений кортежа как пакета значений, открывая все ту же выразительность для кортежей без введения дополнительного набора операций для отображения, итерации, конкатенация и деструктуризация.
Абстрактное значение кортежа содержит список из 0 или более отдельных значений. Упаковка элементов кортежа удаляет структуру кортежа и собирает отдельные элементы кортежа в пакет значений. Эта операция является специальным свойством для типов кортежей с называнием .element:
struct Mapped<Value> {}
func map<T...>(tuple: (T...)) -> (Mapped<T>...) {
return (Mapped(tuple.element)...)
}
Свойство element возвращает элементы кортежа в одном пакете. Для абстрактного кортежа (T...) сигнатурой этого свойства является (T...) -> T, что в противном случае не может быть выражено в языке. Для кортежа длины n сложность преобразования значения кортежа в пакет равна O(n).
Подобно пакетам конкретных значений, использование операции пакета для конкретных или частично конкретных кортежей требует аннотации типа для абстрактного типа элемента:
func iterate(over tuple: (Int, String, Bool)) {
for value: some Equatable in tuple.element... {
// do something with an 'Equatable' value
}
}
Пользовательские соответствия кортежей
Вышеупомянутые функции вместе предоставляют необходимые инструменты для написания абстракций по кортежам с переменной длиной. Последним крупным разрывом в выразительности между кортежами и номинальными типами является возможность объявлять соответствия кортежам.
Эта концепция, наконец, закрывает этот пробел, используя параметризованный синтаксис расширения для объявления соответствия:
extension <T...> (T...): P where T: P {
// Implementation of tuples to 'P'
}
При этом SE-0283: Соответствие кортежей Equatable, Comparable и Hashable может быть реализовано в стандартной библиотеке Swift с помощью следующего кода:
extension <Element...> (Element...): Equatable where Element: Equatable {
public static func ==(lhs: Self, rhs: Self) -> Bool {
for (left, right) in (lhs.element, rhs.element)... {
guard left == right else { return false }
}
return true
}
}
extension<Element...> (Element...): Comparable where Element: Comparable {
public static func <(lhs: Self, rhs: Self) -> Bool {
for (left, right) in (lhs.element, rhs.element)... {
if left < right { return true }
if left > right { break }
}
return false
}
}
extension<Element...> (Element...): Hashable where Element: Hashable {
public func hash(into hasher: inout Hasher) {
for element in self.element... {
hasher.combine(element)
}
}
}
Различие между пакетами и кортежами
Пакеты и кортежи различаются по системе типов, потому что пакет типов сам по себе не является типом, в то время, как кортеж таковым является. Пакет типов состоит из отдельных типов, и пакеты типов можно использовать только в позициях, которые естественным образом принимают список из 0 или более типов, таких как общие списки аргументов. С другой стороны, кортеж можно использовать везде, где можно использовать отдельный тип.
Следующий код демонстрирует семантические различия между использованием значения кортежа и доступом к его элементам в виде пакета:
func printPack<U...>(_ u: U...) {
print("u := {\(u...)}")
}
func print4Ways<T...>(tuple: (T...), pack: T...) {
print("Concatenating tuple with pack")
printPack(tuple, pack...)
print("\n")
print("Concatenating tuple element pack with pack")
printPack(tuple.element..., pack...)
print("\n")
print("Expanding tuple with pack")
_ = (printPack(tuple, pack)...)
print("\n")
print("Expanding tuple element pack with pack")
_ = (printPack(tuple.element, pack)...)
print("\n")
}
print4Ways(tuple: (1, "hello", true), pack: 2, "world", false)
Вывод вышеуказанного кода:
Concatenating tuple with pack
u := {(1, "hello", true), 2, "world", false}
Concatenating tuple element pack with pack
u := {1, "hello", true, 2, "world", false}
Expanding tuple with pack
u := {(1, "hello", true), 2}
u := {(1, "hello", true), "world"}
u := {(1, "hello", true), false}
Expanding tuple element pack with pack
u := {1, 2}
u := {"hello", "world"}
u := {true, false}
Понятие пакета необходимо в языке, потому что, хотя кортежи могут иметь абстрактную длину, существует фундаментальная неопределенность между тем, предназначен ли кортеж для использования в качестве единственного типа, или он должен был быть взорван для формирования сжатого списка его элементов, разделенных запятыми:
func variadicPrint<T...>(_ t: T...) { ... }
func forward(tuple: (Int, String Bool)) {
// Does this print three comma-separated values, or a tuple?
variadicPrint(tuple)
}
Пакеты также можно использовать в списке параметров функционального типа; если бы пакеты были кортежами, универсальный код не смог бы абстрагироваться от типов функций со списками параметров переменной длины.
Изучение альтернатив синтаксиса
Эта конструкция для результатов вариативных дженериков вводит 2 новых значения ..., оставляя Swift в сумме с 4 значениями ...:
Неупаковываемые вариативные параметры
Постфиксный оператор частичного диапазона
Объявление пакета с типом параметров
Оператор расширения пакета
Выбор альтернативного синтаксиса может устранить неясность с существующими значениями ... в Swift. Авторы этого концептуальной документации открыты для рассмотрения альтернативного синтаксиса, но еще не нашли более убедительного.
Ключевые слова: упаковать и распаковать
Другой альтернативой является использование ключевых слов для объявлений пакетов и расширений пакетов, например:
func zip<pack T, pack U>(_ first: unpack T, and second: unpack U) -> (unpack (T, U)) {
return (unpack (first, second))
}
Недостатками выбора синтаксиса ключевого слова являются:
Ключевое слово с операндом в скобках в контексте выражения малозаметно, потому что оно выглядит как вызов функции, а не как встроенная операция расширения.
Новое ключевое слово в контексте выражения нарушило бы существующий код, использующий это имя ключевого слова, например. как имя функции.
Контекстное ключевое слово должно быть разрешено в синтаксическом анализаторе(* парсере), что означает, что нет места для переноса разрешения операции распаковки/расширения на более поздний этап проверки типа, например. для поддержки пакетов участников. Вместо этого ключевое слово unpack должно быть специальной встроенной функцией или методом на уровне выражения и ключевым словом на уровне типа.
Альтернативные постфиксные операторы
Одной из альтернатив является использование другого оператора, такого как *, вместо…
func zip<T*, U*>(_ first: T*, and second: U*) -> ((T, U)*) {
return ((first, second)*)
}
Недостатки постфикса * включают в себя:
* тонкий (коварный/хитрый/деликатный)
* упоминает типы указателей / оператор разыменования (* получения значений переменных объекта по указателю) для программистов, знакомых с другими языками, включая C/C++, Go и Rust.
Другой оператор не устраняет неоднозначности в выражениях, потому что значения могут иметь постфиксный оператор * или любой другой символ оператора, что приводит к той же неясности.
Подписывайся на наши соцсети: Telegram / VKontakte
Вступай в открытый чат для iOS-разработчиков: t.me/swiftbook_chat
Смотри бесплатные уроки по iOS-разработке с нуля