0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>О технологии SwiftUI говорили уже много. Предлагаю вам небольшой цикл статей про то, как использовать этот фреймворк в реальной жизни и реальных приложениях, а не в конструкторах сэндвичей для авокадо. А чтобы все было серьезно и по-взрослому, мы рассмотрим, как сделать так, чтобы наше приложение на SwiftUI соответствовало принципам чистой архитектуры и чистого кода.</p>
1
<p>О технологии SwiftUI говорили уже много. Предлагаю вам небольшой цикл статей про то, как использовать этот фреймворк в реальной жизни и реальных приложениях, а не в конструкторах сэндвичей для авокадо. А чтобы все было серьезно и по-взрослому, мы рассмотрим, как сделать так, чтобы наше приложение на SwiftUI соответствовало принципам чистой архитектуры и чистого кода.</p>
2
<p>Основными особенностями декларативной разработки в новом фреймворке является отход от прямого использования UIViewController, UIView и заменой на структуры, реализующие протокол View. Все компоненты визуальной части описываются также с помощью декларативного синтаксиса и располагаются внутри основного свойства body каждого View. Настройки, стилизации и кастомизации компонентов, навигация между экранными View тоже задается с помощью декларативного синтаксиса.</p>
2
<p>Основными особенностями декларативной разработки в новом фреймворке является отход от прямого использования UIViewController, UIView и заменой на структуры, реализующие протокол View. Все компоненты визуальной части описываются также с помощью декларативного синтаксиса и располагаются внутри основного свойства body каждого View. Настройки, стилизации и кастомизации компонентов, навигация между экранными View тоже задается с помощью декларативного синтаксиса.</p>
3
<p>Например, этот код описывает вью для списка новостей, по клику на который открывается экран с вью отдельной новости:</p>
3
<p>Например, этот код описывает вью для списка новостей, по клику на который открывается экран с вью отдельной новости:</p>
4
struct NewsListView: View{ @State var data: [NewsItemMock] var body: some View { NavigationView{ List(data) { item in NavigationLink(destination:NewsItemView(item:item)) { NewsItemRow(data: item) } } } }<p>SwiftUI использует ViewBuilder - декларативный конструктор интерфейса на основе Functional Builder. Этот механизм появился в Swift 5.1 и позволяет группировать элементы в некий массив внутри closure-блока, например, родительского объекта. Пример использования ViewBuilder представлен на слайде. Мы просто располагаем View-контролы в нужном нам порядке, например, внутри вертикального или горизонтального Stack без использования addSubview, а при компиляции SwiftUI сам добавляет и группирует элементы в более сложный родительский контейнер.</p>
4
struct NewsListView: View{ @State var data: [NewsItemMock] var body: some View { NavigationView{ List(data) { item in NavigationLink(destination:NewsItemView(item:item)) { NewsItemRow(data: item) } } } }<p>SwiftUI использует ViewBuilder - декларативный конструктор интерфейса на основе Functional Builder. Этот механизм появился в Swift 5.1 и позволяет группировать элементы в некий массив внутри closure-блока, например, родительского объекта. Пример использования ViewBuilder представлен на слайде. Мы просто располагаем View-контролы в нужном нам порядке, например, внутри вертикального или горизонтального Stack без использования addSubview, а при компиляции SwiftUI сам добавляет и группирует элементы в более сложный родительский контейнер.</p>
5
<p>И вот такой код:</p>
5
<p>И вот такой код:</p>
6
VStack { HStack { VStack(alignment: .leading,spacing: 10) { HeaderText(text: data.title ?? "") SubheaderText(text: data.description ?? "") SmallText(text: data.publishedAt? .formatToString("dd.MM.yyyy") ?? "") } ThumbImage(withURL: data.urlToImage ?? "") }<p>преобразуется в элемент списка из 3-х текстовых полей и одной картинки:</p>
6
VStack { HStack { VStack(alignment: .leading,spacing: 10) { HeaderText(text: data.title ?? "") SubheaderText(text: data.description ?? "") SmallText(text: data.publishedAt? .formatToString("dd.MM.yyyy") ?? "") } ThumbImage(withURL: data.urlToImage ?? "") }<p>преобразуется в элемент списка из 3-х текстовых полей и одной картинки:</p>
7
<p>Хотя SwiftUI и отрицает концепцию UIViewController, точкой входа в приложение является UIHostingController, внутрь которого передается и встраивается отображаемый View. Т. е. по сути новая технология является надстройкой над UIKit:</p>
7
<p>Хотя SwiftUI и отрицает концепцию UIViewController, точкой входа в приложение является UIHostingController, внутрь которого передается и встраивается отображаемый View. Т. е. по сути новая технология является надстройкой над UIKit:</p>
8
@available(iOS 13.0, tvOS 13.0, *) open class UIHostingController<Content> : UIViewController where Content : View { public var rootView: Content public init(rootView: Content)<p>Кстати, все контролы SwiftUI являются декларативными аналогами UIKit-контролов.</p>
8
@available(iOS 13.0, tvOS 13.0, *) open class UIHostingController<Content> : UIViewController where Content : View { public var rootView: Content public init(rootView: Content)<p>Кстати, все контролы SwiftUI являются декларативными аналогами UIKit-контролов.</p>
9
<p>Например, VStack, HStack - аналоги привычных нам вертикальных и горизонтальных UIStackView соответственно. List - это UITableView, Text - UILabel, Button - UIButton, Image - UIImage и т. д.</p>
9
<p>Например, VStack, HStack - аналоги привычных нам вертикальных и горизонтальных UIStackView соответственно. List - это UITableView, Text - UILabel, Button - UIButton, Image - UIImage и т. д.</p>
10
<p>Подключение и настройка контролов производится декларативно с помощью доступных модификаторов. Группируются элементы внутри аналогов UIStackView с некоторыми предопределенными свойствами.</p>
10
<p>Подключение и настройка контролов производится декларативно с помощью доступных модификаторов. Группируются элементы внутри аналогов UIStackView с некоторыми предопределенными свойствами.</p>
11
<p>Помимо изменения способа описания визуальной части, меняется управление потоком данным и механизм реакции UI на него.<strong>Swift UI является не событийно зависимым фреймворком</strong>. Т.е. View в нем - это результат функции неких состояний, а не последовательности событий. Действие, производимое пользователем, не меняет UI напрямую, нельзя напрямую изменить тот или иной View, добавить или удалить контрол. Сначала меняются свойства или переменные состояния, которые подключены к View через те или иные Property wrappers (обертки свойств).</p>
11
<p>Помимо изменения способа описания визуальной части, меняется управление потоком данным и механизм реакции UI на него.<strong>Swift UI является не событийно зависимым фреймворком</strong>. Т.е. View в нем - это результат функции неких состояний, а не последовательности событий. Действие, производимое пользователем, не меняет UI напрямую, нельзя напрямую изменить тот или иной View, добавить или удалить контрол. Сначала меняются свойства или переменные состояния, которые подключены к View через те или иные Property wrappers (обертки свойств).</p>
12
<p>Основными используемыми Property Wrappers являются:</p>
12
<p>Основными используемыми Property Wrappers являются:</p>
13
<p>1.@State - используется для локальных переменных.</p>
13
<p>1.@State - используется для локальных переменных.</p>
14
struct NewsItemRow: View { @State var title: String @State var description: String @State var dateFormatted: String @State var imageUrl: String var body: some View { VStack { HStack { VStack(alignment: .leading,spacing: 10) { HeaderText(text: title) SubheaderText(text: description) SmallText(text: dateFormatted) } ThumbImage(withURL: imageUrl) } } }<p>2.@Binding - аналог weak, используется при передаче ссылки на значение.</p>
14
struct NewsItemRow: View { @State var title: String @State var description: String @State var dateFormatted: String @State var imageUrl: String var body: some View { VStack { HStack { VStack(alignment: .leading,spacing: 10) { HeaderText(text: title) SubheaderText(text: description) SmallText(text: dateFormatted) } ThumbImage(withURL: imageUrl) } } }<p>2.@Binding - аналог weak, используется при передаче ссылки на значение.</p>
15
<p>Используем, когда от какого-то свойства у нас зависит более одного View. Например, если мы хотим передавать значение исходному View от View второго уровня.</p>
15
<p>Используем, когда от какого-то свойства у нас зависит более одного View. Например, если мы хотим передавать значение исходному View от View второго уровня.</p>
16
struct FirstView: View { @State var isPresented: Bool = true var body: some View { NavigationView { NavigationLink(destination: SecondView(isPresented: self.$isPresented)) { Text("Some") } } } } struct SecondView: View { @Binding var isPresented: Bool var body: some View { Button("Dismiss") { self.$isPresented = false } } }<p>3.@EnvironmentObject - передача объектов между View</p>
16
struct FirstView: View { @State var isPresented: Bool = true var body: some View { NavigationView { NavigationLink(destination: SecondView(isPresented: self.$isPresented)) { Text("Some") } } } } struct SecondView: View { @Binding var isPresented: Bool var body: some View { Button("Dismiss") { self.$isPresented = false } } }<p>3.@EnvironmentObject - передача объектов между View</p>
17
<p>4.@ObjectBinding, @ObservableObject - используется для отслеживания изменения свойств модели с помощью средств фреймворка Combine.</p>
17
<p>4.@ObjectBinding, @ObservableObject - используется для отслеживания изменения свойств модели с помощью средств фреймворка Combine.</p>
18
class NewsItemModel: ObservableObject,IModel { @Published var title: String @Published var description: String @Published var dateFormatted: String @Published var imageUrl: String }<p>О нем мы поговорим позже.</p>
18
class NewsItemModel: ObservableObject,IModel { @Published var title: String @Published var description: String @Published var dateFormatted: String @Published var imageUrl: String }<p>О нем мы поговорим позже.</p>
19
<p>Итак. Если мы хотим изменить наш View, мы меняем свойство, объявление с одним из Property Wrappers. Затем уже декларативный View перестраивается со всеми внутренними контролами.</p>
19
<p>Итак. Если мы хотим изменить наш View, мы меняем свойство, объявление с одним из Property Wrappers. Затем уже декларативный View перестраивается со всеми внутренними контролами.</p>
20
<p>При изменении любой из переменных состояния View перестроится весь целиком.</p>
20
<p>При изменении любой из переменных состояния View перестроится весь целиком.</p>
21
<p>Рассмотрим небольшой пример. У нас есть какой-то экран, в навигационном баре которого расположена кнопка добавления контента в избранное. Чтобы у нас поменялось изображение-индикатор на этой кнопке, мы воспользуемся PropertyWrappers. Например, в данном случае создадим локальную переменную и объявим ее как State:</p>
21
<p>Рассмотрим небольшой пример. У нас есть какой-то экран, в навигационном баре которого расположена кнопка добавления контента в избранное. Чтобы у нас поменялось изображение-индикатор на этой кнопке, мы воспользуемся PropertyWrappers. Например, в данном случае создадим локальную переменную и объявим ее как State:</p>
22
struct NewsItemView: View { @State var isFavorite: Bool ....<p>Изменение значения свойства привяжем к триггерному-событию, возникающему при нажатии на кнопку:</p>
22
struct NewsItemView: View { @State var isFavorite: Bool ....<p>Изменение значения свойства привяжем к триггерному-событию, возникающему при нажатии на кнопку:</p>
23
struct NewsItemView: View{ @State var isFavorite: Bool var body: some View { NavigationView { VStack { Text("Some content") } } .navigationBarItems(trailing: Button(action: { self.isFavorite = !self.isFavorite }){ Image(self.isFavorite ? "favorite" : "unfavorite") .frame(width: 20, height: 20, alignment: .topTrailing) }) }<p>Таким образом будет меняться наш View:</p>
23
struct NewsItemView: View{ @State var isFavorite: Bool var body: some View { NavigationView { VStack { Text("Some content") } } .navigationBarItems(trailing: Button(action: { self.isFavorite = !self.isFavorite }){ Image(self.isFavorite ? "favorite" : "unfavorite") .frame(width: 20, height: 20, alignment: .topTrailing) }) }<p>Таким образом будет меняться наш View:</p>
24
<p>И в принципе это все основное, что вам нужно знать для начала про SwiftUI.</p>
24
<p>И в принципе это все основное, что вам нужно знать для начала про SwiftUI.</p>
25
<p><strong>Но достаточно ли это для работы?</strong></p>
25
<p><strong>Но достаточно ли это для работы?</strong></p>
26
<p>Чтобы создавать несложные UI из простых контролов без привязки к Xib и сторибордам, вполне.</p>
26
<p>Чтобы создавать несложные UI из простых контролов без привязки к Xib и сторибордам, вполне.</p>
27
<p>А для чего-то большего -- нет.</p>
27
<p>А для чего-то большего -- нет.</p>
28
<p>Во-первых, не для всех контролов есть аналоги в SwiftUI. Это относится как к стандартным для UIKit UISearchView, UICollectionView, так и для каких элементов из third-part-библиотек.</p>
28
<p>Во-первых, не для всех контролов есть аналоги в SwiftUI. Это относится как к стандартным для UIKit UISearchView, UICollectionView, так и для каких элементов из third-part-библиотек.</p>
29
<p>Во-вторых, нет (или почти нет, может, кто-то это делает прямо сейчас) сторонних решений для работы с Data Flow SwiftUI.</p>
29
<p>Во-вторых, нет (или почти нет, может, кто-то это делает прямо сейчас) сторонних решений для работы с Data Flow SwiftUI.</p>
30
<p>Значит, придется, адаптировать уже существующие решения под стандартные приложения iOS.</p>
30
<p>Значит, придется, адаптировать уже существующие решения под стандартные приложения iOS.</p>
31
31