SwiftUI позволяет описывать интерфейс приложений в декларативном стиле и сильно сокращает количество кода.
Apple уже представила несколько интересных туториалов на английском языке с множеством примеров. Я же постараюсь рассказать о новом фреймворке в форме вопросов и ответов. Итак, поехали.
Для работы со SwiftUI необходимо скачать последнюю версию Xcode. Вы также должны быть зарегистрированным разработчиком Apple. Иметь последнюю macOS Catalina желательно, но не обязательно. Без нее не будет доступен Canvas.
Итак, в Xcode создайте новый проект и убедитесь, что стоит галочка “Use SwiftUI”.
Для SwiftUI теперь не нужен Interface Builder – ему на смену пришел Canvas, интерактивный редактор интерфейса, который тесно связан с кодом. При написании кода автоматически генерируется его визуальная часть в canvas и наоборот. Очень удобно, а главное безопасно. Теперь ваше приложение не будет падать из-за того, что вы забыли обновить связь @IBOutlet
с переменной.В данной статье мы не будем затрагивать canvas, рассмотрим только код.
Да, теперь начальным объектом в интерфейсе приложения является не UIWindow
, а новый класс UIScene
(либо его наследник UIWindowScene
). И уже к сцене добавляется окно. Эти изменения касаются не только SwiftUI, но и iOS 13 в целом.
При создании проекта вы увидите файлы AppDelegate
, SceneDelegate
и ContentView
. SceneDelegate
– делегат класса UIWindowScene
, используется для управления сценами в приложении. Сильно напоминает AppDelegate
.
В методе делегата scene: willConnectTo: options:
создается окно и рутовый UIHostingController
, который содержит в себе ContentView
. ContentView
– и есть наша “домашняя” страница. Вся разработка будет вестись в этом классе.
Открыв ContentView.swift
, вы увидите объявление контейнера ContentView
. Как вы уже поняли, в SwiftUI нет привычных методов viewDidLoad
или viewDidAppear
. Основой экранов здесь является не UIViewController
, а View. Первое, что стоит заметить, ContentView
— это структура (struct
), которая принимает протокол View
. Да, View
теперь стал протоколом, причем очень простым. Единственный метод, который вы должны реализовать в ContentView
– это описать переменную body
. Все ваши subview и кастомные view должны принимать протокол View
, то есть должны иметь переменную body
.
struct ContentView: View { var body: some View { Text("Hello, world!") } }
Body
– это непосредственно наш контейнер, куда добавляются все остальные subview. Это чем-то напоминает body в html странице, где html страница – это ContentView
. Body
всегда должен иметь ровно один потомок, причем любого класса, принимающего протокол View
.
Конструкция some TypeName
– это нововведение Swift 5.1, которое называется opaque return type. Оно используется для случаев, когда нам не важно, какой конкретно объект вернуть, главное, чтобы он поддерживал указанный тип, в данном случае протокол View
.
Если бы мы просто написали var body: View
, то это бы означало, что мы должны вернуть именно View
. Класс Any
тоже не подходит, так как нам пришлось бы выполнить операцию приведения типа (с помощью оператора as!
). Поэтому придумали специальное слово some
перед названием протокола для обозначения opaque return type. Вместо View
мы можем вернуть Text
, Image
, VStack
– что угодно, так как все они поддерживают протокол View
. Но должен быть ровно один элемент: при попытке вернуть больше одной View
компилятор выдаст ошибку.
В Swift 5.1 появилась возможность группировать объекты в нечто единое целое в декларативном стиле. Это похоже на массив внутри closure-блока, однако элементы перечисляются с новой строки без запятых и `return`. Данный механизм назвали Function Builder.
Это нашло широкое применение в SwiftUI. На основе Function Builder сделали ViewBuilder
– декларативный конструктор интерфейса. Используя ViewBuilder
нам больше не нужно писать addSubview
для каждого элемента – достаточно перечислить все View
с новой строки внутри closure-блока. SwiftUI сам добавит и сгруппирует элементы в более сложный родительский контейнер.
// Объявление ViewBuilder в SwiftUI фреймворке @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, \*) @\_functionBuilder public struct ViewBuilder { /// Builds an empty view from an block containing no statements, \`{ }\`. public static func buildBlock() -> EmptyView /// Passes a single view written as a child view (e..g, \`{ Text("Hello") }\`) through /// unmodified. public static func buildBlock<Content>(\_ content: Content) -> Content where Content : View }
Создаются элементы очень просто: каждый View
нужно писать с новой строки и менять внешний вид с помощью функций-модификаторов (view modifiers). Отличие модификаторов от привычных нам функций в том, что они всегда возвращают объект-контейнер вместо void
. Поэтому мы можем создавать целые цепочки модификаторов через точку.
var body: some View { VStack{ Text("World Time").font(.system(size: 30)) Text("Yet another subtitle").font(.system(size: 20)) } }
Однако не все контролы и View имеют свои аналоги в SwiftUI. Вот неполный список классов из UIKit и их аналоги:
UITableView
→ List
UICollectionView
→ не имеет аналогаUILabel
→ Text
UITextField
→ TextField
UIImageView
→ Image
UINavigationController
→ NavigationView
UIButton
→ Button
UIStackView
→ HStack
/ VStack
UISwitch
→ Toggle
UISlider
→ Slider
UITextView
→ не имеет аналогаUIAlertController
→ Alert
/ ActionSheet
UISegmentedControl
→ SegmentedControl
UIStepper
→ Stepper
UIDatePicker
→ DatePicker
Роль navigation controller берет на себя специальный NavigationView
. Достаточно обернуть ваш код в NavigationView{}
. А само действие перехода можно добавить в специальную кнопку NavigationLink
, которая пушит условный экран DetailView
.
var body: some View { NavigationView { Text("World Time").font(.system(size: 30)) NavigationLink(destination: DetailView() { Text("Go Detail") } } }
Как представлять новые view модально? Это делается, например, с помощью конструкции sheet:
Button(action: { print("Button Pushed") self.show\_modal = true }) { Text("Present Modal") }.sheet(isPresented: self.$show\_modal) { ModalView() }
Как говорилось выше, body
может возвращать не только экземпляр View
, но и любой другой класс, принимающий данный протокол. Это дает нам возможность пушить не DetailView
, а даже Text
или Image
!
Элементы располагаются зависимо друг от друга и могут быть расположены вертикально внутри VStack
, горизонтально HStack
и друг над другом ZStack
. Так же нам доступны ScrollView
и ListView
. Можно чередовать и использовать совместно эти контейнеры, чтобы получить любую сетку элементов.
Комбинируя контейнеры между собой можно получить довольно крупное дерево с большим количеством вложений. Однако SwiftUI оптимизирован специально для этого, так что глубокая вложенность контейнеров не влияет на производительность. Об этом говорится в видео с wwdc (начиная с 15:32).
var body: some View { NavigationView { VStack { NavigationLink(destination: LargeView(timeString: subtitle)) { Text("See Fullscreen") } Text("World Time").font(.system(size: 30)) } } }
Объявить NavigationView{}
недостаточно, необходимо указать navigationtitle и стиль navigation bar’а.
NavigationView { VStack{}.navigationBarTitle(Text("World Time"), displayMode: .inline) }
Обратите внимание, что функция navigationBarTitle
вызывается не у NavigationView
, а у ее внутренней View
. DisplayMode – параметр, который указывает стиль navigation bar’а: большой или стандартный.
Если вы хотите выполнить код при инициализации View
, вы можете сделать это, добавив функцию onAppear{}
. OnAppear можно добавить к любой View
, например, к VStack
. В данном примере при появлении контейнера на экране выполняется http-запрос к серверу.
struct ContentView : View { @State var statusString : String = "World Time" var body: some View { NavigationView { VStack { NavigationLink(destination:DetailView()) { Text("Go Detail") } Text(statusString).font(.system(size: 30)) }.onAppear { self.loadTime() } } } func loadTime(){ NetworkService().getTime { (time) in if let aTime = time { self.statusString = "\\(aTime.date())" } } } }
Мы вызываем функцию loadTime
, которая запрашивает текущее время из сервера и возвращает модель WorldTime
. Не будем зацикливаться на классе NetworkService
, вы сможете посмотреть весь код, скачав исходники. Ссылка в конце статьи.
Переменная var statusString
была вынесена для того, чтобы позже к ней присвоить текущее время. Перед переменной стоит особый атрибут @State
. Что он означает?
В Swift 5.1 появились так называемые property wrappers (или property delegates). В SwiftUI property wrappers используются для того, чтобы обновлять или связывать один из параметров view с нашей собственной переменной, например, значение переключателя (Toggle).
Атрибут @State
– специальный атрибут, который ставится перед объявлением переменной. Это позволяет нам автоматически отслеживать изменение свойства без дополнительного кода. В примере выше текст “World Time” изменится на текущую дату, как только мы обновим значение `statusString` .
Для связывания значений (Properties Binding) мы можем указать специальный символ `$` перед названием переменной в самом коде:
// Изменив положения переключателя, изменится и значение переменной struct DetailsView: View { @State var changeToggle: Bool var body: some View { Toggle(isOn: $changeToggle) { Text("Change Toggle") } } }
Property wrappers являются очень важной составляющей SwiftUI, я лишь вскользь упомянул о них. Для более подробного ознакомления с property wrappers посмотрите видео с wwdc здесь (с 37й минуты), здесь (c 12й минуты) и здесь (с 19й минуты).
Сразу стоит отметить, что добавлять view в произвольное время в прямом смысле слова нельзя. SwiftUI – декларативный фреймворк, который рендерит весь view целиком. Однако можно задавать различные условия внутри body и обновлять состояние view при их изменении. В данном примере используем простейшую связку @State — if
с переменной isTimeLoaded
.
struct ContentView : View { @State var statusString : String = "World Time" @State var isTimeLoaded : Bool = false var body: some View { NavigationView { VStack { if isTimeLoaded { addNavigationLink() } Text(statusString).font(.system(size: 30)).lineLimit(nil) }.navigationBarTitle(Text("World Time"), displayMode: .inline) }.onAppear { self.loadTime() } } func addNavigationLink() -> some View { NavigationLink(destination: Text("124!!!")) { Text("Go Detail") } } func loadTime(){ NetworkService().getTime { (time) in if let aTime = time { self.statusString = "\\(aTime.date().description(with: Locale.current))" self.isTimeLoaded = true } } } } struct DetailView : View { var timeString : String var body : some View { Text(timeString).font(.system(size: 40)).lineLimit(nil) } }
Кстати, вы заметили, что в функции addNavigationLink()
нет слова return
? Это еще одно нововведение Swift 5.1 – для функций с одним выражением теперь необязательно писать return
. Но можно и писать.
Это лишь часть вопросов и ответов по SwiftUI. Я рассмотрел общие вопросы, надеюсь, данная статья поможет новичкам понять основные моменты данного фреймворка. SwiftUI еще сырой, но несомненно он будет совершенствоваться.
Логичен вопрос: стоит ли изучать UIKit? Конечно, да. UIKit – основа программирования на iOS и он будет развиваться и дальше. Тем более многие компоненты SwiftUI являются оберткой над UIKit. Ну и пока что нет никаких библиотек, фреймворков, pod-ов для SwiftUI. Все пока придется писать самостоятельно. Так что лучше изучайте оба подхода к разработке – так вы будете более ценным разработчиком.
Исходники проекта вы можете скачать здесь.
Если вы хотите больше знать о том как создавать и развивать своё приложение с подписками, читайте Apphud Blog.