What’s new in Apphud: Free Trials Performance Report, Paddle Integration, SDK Updates, and moreLet’s see
Apphud
Why Apphud?PricingContact
Ren
Ren
June 24, 2019
8 min read

Все, что вы хотели знать о SwiftUI, но боялись спросить

На WWDC 2019 Apple представила свой новый декларативный фреймворк SwiftUI, который призван в будущем заменить (или нет?) привычный нам UIKit.

Все, что вы хотели знать о SwiftUI, но боялись спросить

SwiftUI позволяет описывать интерфейс приложений в декларативном стиле и сильно сокращает количество кода.

Apple уже представила несколько интересных туториалов на английском языке с множеством примеров. Я же постараюсь рассказать о новом фреймворке в форме вопросов и ответов. Итак, поехали.

Перед началом работы

Для работы со SwiftUI необходимо скачать последнюю версию Xcode. Вы также должны быть зарегистрированным разработчиком Apple. Иметь последнюю macOS Catalina желательно, но не обязательно. Без нее не будет доступен Canvas.

Итак, в Xcode создайте новый проект и убедитесь, что стоит галочка “Use SwiftUI”.

Вопросы и ответы

Куда подевался Interface Builder?

Для SwiftUI теперь не нужен Interface Builder – ему на смену пришел Canvas, интерактивный редактор интерфейса, который тесно связан с кодом. При написании кода автоматически генерируется его визуальная часть в canvas и наоборот. Очень удобно, а главное безопасно. Теперь ваше приложение не будет падать из-за того, что вы забыли обновить связь @IBOutlet с переменной.В данной статье мы не будем затрагивать canvas, рассмотрим только код.

Изменился ли запуск приложения?

Да, теперь начальным объектом в интерфейсе приложения является не UIWindow, а новый класс UIScene (либо его наследник UIWindowScene). И уже к сцене добавляется окно. Эти изменения касаются не только SwiftUI, но и iOS 13 в целом.

При создании проекта вы увидите файлы AppDelegateSceneDelegate и ContentViewSceneDelegate – делегат класса UIWindowScene, используется для управления сценами в приложении. Сильно напоминает AppDelegate.

Класс SceneDelegate указывается в Info.plistКласс SceneDelegate указывается в Info.plist

В методе делегата scene: willConnectTo: options: создается окно и рутовый UIHostingController , который содержит в себе ContentView . ContentView – и есть наша “домашняя” страница. Вся разработка будет вестись в этом классе.

Чем "View" отличается от "UIView"?

Открыв 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"?

Body – это непосредственно наш контейнер, куда добавляются все остальные subview. Это чем-то напоминает body в html странице, где html страница – это ContentViewBody всегда должен иметь ровно один потомок, причем любого класса, принимающего протокол View.

Opaque return types или что такое "some"?

Конструкция some TypeName – это нововведение Swift 5.1, которое называется opaque return type. Оно используется для случаев, когда нам не важно, какой конкретно объект вернуть, главное, чтобы он поддерживал указанный тип, в данном случае протокол View.

Если бы мы просто написали var body: View , то это бы означало, что мы должны вернуть именно View. Класс Any тоже не подходит, так как нам пришлось бы выполнить операцию приведения типа (с помощью оператора as!). Поэтому придумали специальное слово some перед названием протокола для обозначения opaque return type. Вместо View мы можем вернуть TextImageVStack – что угодно, так как все они поддерживают протокол View. Но должен быть ровно один элемент: при попытке вернуть больше одной View компилятор выдаст ошибку.

Ошибка компилирования при попытке вернуть в body более одного элементаОшибка компилирования при попытке вернуть в body более одного элемента

Что за синтаксис внутри скобок и где "addSubview"?

В 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
}

Как добавлять "UILabel", "UIImageView" и другие элементы?

Создаются элементы очень просто: каждый 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 → не имеет аналога
  • UILabelText
  • UITextField → TextField
  • UIImageView → Image
  • UINavigationController → NavigationView
  • UIButton → Button
  • UIStackView → HStackVStack
  • UISwitch → Toggle
  • UISlider → Slider
  • UITextView → не имеет аналога
  • UIAlertController → AlertActionSheet
  • 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))
          }
     }
}

Как показать Navigation Bar?

Объявить  NavigationView{} недостаточно, необходимо указать navigationtitle и стиль navigation bar’а.

NavigationView {
     VStack{}.navigationBarTitle(Text("World Time"),  displayMode: .inline)
}

Обратите внимание, что функция navigationBarTitle вызывается не у NavigationView, а у ее внутренней ViewDisplayMode – параметр, который указывает стиль navigation bar’а: большой или стандартный.

Есть ли аналог метода "viewDidLoad"?

Если вы хотите выполнить код при инициализации 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. Что он означает?

Property Wrappers или что такое "@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 в runtime?

Сразу стоит отметить, что добавлять 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.  

Ren
Ren
Co-founder at Apphud
Ex iOS app and game developer. 11 years in the industry since iOS 3. More than 50 apps are in the background with 4 exits. Entrepreneur and traveler.

Related Posts