SwiftUI — «DSL на максималках»

Alex Ne
someSwift
Published in
5 min readJun 18, 2019

Презентация нового фреймворка от Apple стала настоящей сенсацией. В один момент эксперты в области разработки приложений под яблочные устройства превратились в джунов. SwiftUI бросает вызов существующим стандартам и видимо навсегда оставит в прошлом многие, уже устоявшиеся, подходы в разработке ios приложений. Огромное количество open source фрейворков, собравшие тысячи звёзд на гитхабе, со временем прекратят своё существование. Если кто-то ждал «второго пришествия» в мире мобильной разработки, это судя по всему оно.

Но как это стало возможным? Почему Apple в один момент решила отказаться от инструментов на разработку и развитие которых было потрачено столько лет !? Что-ж, современный мир быстро меняется и существующие решения перестают удовлетворять растущие потребности. Сложность программных продуктов и приложений постоянно растёт и теперь требуются более умные и продвинутые инструменты. “Simple path to make great app” — эта фраза которая очень часто звучала на wwdc. SwiftUI призван взять на себя огромную часть работы, с которой приходилось сталкиваться разработчику снова и снова, от приложения к приложению. Все это стало возможным благодаря двум основным нововведениям: созданию эффективного DSL для описания интерфейса и использованию реактивного подхода для распространения данных. В этой статье хотелось бы остановиться на первом из них.

Декларативный подход и DSL.

Декларативный подход подразумевает, что вы описываете ожидаемый результат вместо того чтобы описывать последовательность команд благодаря которым этот результат можно получить .

Сравните два подхода для отображения кнопки на экране:

SwiftUI

Используя SwiftUI вы описываете только то, что непосредственно касается отображаемой кнопки:

  1. действие кнопки
  2. текст внутри, его цвет и отступ до границ кнопки
  3. цвет кнопки и скругление углов
UIKit

В привычном UIKit вам необходимо знать как работает отображение элементов на экране, чтобы добиться того же результата.

Во первых нужно помнить, что cornerRadius задается на внутреннем объекте (CALayer), а не на самой кнопки (следует также учитывать, что его иногда нужно заново отрисовывать, когда меняются размеры кнопки).

Помимо этого необходимо также знать, что такое констрейнты, селекторы, зачем нужен атрибут @objc и почему нужно устанавливать флаг translateAutoresizingMaskIntoConstraint в false

Декларативный подход с которым пришел к нам SwiftUI, использует то, что называется DSL (Domain Specific Language). Переводя на русский — «Язык предметной области», смысл его заключается в том, что вы используя язык высокого уровня такой как swift, описываете специфичные для вашей предметной области примитивы на которых и будете строить ваше преложение, вместо того чтобы напрямую реализовывать его на фреймворках предоставляемых самим языком. По сути, вы строите свой язык программирования поверх имеющегося. В данном случае доменом (или предметной областью) является задача построения интерфейса. Swift UI предоставляет свой DSL в виде набора простых и понятных компонентов таких как Text, Button, Spacer, модификаторов, а также задает правила их использования. Такой подход интуитивно понятный, и часто зная только DSL иногда можно легко догадаться как получить желаемый результат.

Но в отличие от того DSL который можно было бы реализовать для своего собственно приложения используя swift 5.0, яблочники идут еще дальше и очищают свой DSL по максимуму, делая его более компактным и понятным. И такой шаг конечно не возможен без изменения в самом языке. Поэтому новый фреймворк идет бок о бок с новой версией языка.

Swift 5.1 вобрал в себя ключевые изменения благодаря которым стал возможен простой и понятный синтаксис SwiftUI. Давайте взглянем на основные из них:

Function builders. [SE-XXXX]

Благодаря этой фичи, стал возможен подобный синтаксис

HStack { 
Text("Очень")
Text("страный")
Text("код")
}

Конструктор для HStack выглядит следующим образом:

init(…, content: @ViewBuilder () -> Content)

@ViewBuilder — это структура объявленная с атрибутом @_functionBuilder, которая и добавляет “магии”. Компилятор разворачивает такой блок в нечто подобное:

HStack {
return ViewBuilder.buildBlock(Text(“Очень”), Text(“страный”), Text(“код”))
}

Используя @_functionBuilder можно создавать свои собственные билдеры, например, @HTMLBuilder для создание HTML документа:

html { 
head {}
body{
p { "Hello HTML" }
}
}

Implicit returns from single-expression functions [SE-0255]

Если в функции или вычислимом свойстве используется только одно выражение то «return» будет подставлен неявно, что избавляет от необходимости указывать return например в body:

var body: some View { 
Text(“Hello World”)
}

Synthesize default values for the memberwise initializer [SE-0242]

В предыдущих версиях swift такой код не собирался бы:

struct someSwift {
let name: String
let count: Int = 0
}
let mySwift = someSwift(name: "me", count: 4)

Компилятор создаст только один конструктор принимающий name, Для Swift 5.1 компилятор создаст в данном примере два конструктора, и код корректно скомпилируется.

Opaque return types [SE-0244]

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

“Скрывание” позволяет вернуть протокол с ассоциативным типом из функции и избежать подобной ошибки:

Protocol ‘MyPAT’ can only be used as a generic constraint because it has Self or associated type requirements

Данная фича имеет еще одно полезное свойство - она позволяет избежать явного указания generic параметров для возвращаемого типа и вместо:

struct EightPointedStar: GameObject {
var shape: Union<Rectangle, Transformed<Rectangle>> {
return Union(Rectangle(),
Transformed(Rectangle(), by: .fortyFiveDegrees)
}
}

использовать более простое объявление:

struct EightPointedStar: GameObject {
var shape: some Shape {
return Union(Rectangle(),
Transformed(Rectangle(), by: .fortyFiveDegrees)
}
}

Property wrappers [SE-0258]

Еще одна интересная функция Swift 5.1 это делегаты свойств. Они позволяют зашить в свойство определенную логику, объявив его с нужным вам атрибутом. Разберем на примере.

Довольно часто в приложениях приходится использовать UserDefaults. Например чтобы реализовывать логику отображения приветственных экранов мы будем записывать в UserDefault значение и проверять его наличие при запуске:

var isFirstLaunch: Bool {
get { return UserDefaults.standard.object(forKey: key) == nil }
set { UserDefaults.standard.set(newValue, forKey: key) }
}

Благодаря property wrappers мы можем создать атрибут который будет содержать всю необходимую логику взаимодействия с UserDefaults и переиспользовать его на любом свойстве.

@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T

var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}

и теперь можно легко объявлять такое свойство:

@UserDefault(key: "IsFirstLaunch", defaultValue: false)
static var isFirstLaunch: Bool

Apple использует эту фичу для биндинга состояния объекта на View

struct SettingsView: View {
@State var saveHistory: Bool
@State var enableAutofill: Bool
var body: some View {
return VStack {
Toggle(isOn: $saveHistory) { ...}
Toggle(isOn: $enableAutofill) { ... }
}
}
}

Как мы можем видеть Apple сильно постарался чтобы их SwiftUI выглядел максимально просто, универсально и чисто. Что в свою очередь избавит новые разработанные приложения от множества багов, сделает их более стандартизированными и приятными. А также значительно повысит скорость разработки, позволяя больше сосредоточится на уникальной ценности приложения.

Thanks for reading ✌🏼

--

--