Вместо предисловия
Я уже писал о SwiftUi, когда разрабатывал первое приложение с его помощью. Не сказать, что SwiftUI мне был не знаком на тот момент, но в целом, я бы назвал это первым полноценным опытом, который конечно, добавил мне пару седых волос, но при этом, отрицательным я его конечно не назову. Тогда же, я по своему и влюбился в SwiftUi т.к. он дает очень интересные ощущения и возможности.
Но вот прошло время, я выполнил несколько десятков заказов с его использованием и конечно, приступил к разработке следующего приложения, пощупав так сказать SwiftUI поглубже. Появилась смелость, желание эксперементировать. В общем, я думаю каждый разработчик так или иначе знаком с ощущением, когда ретивый конь неизвестности начинает подчиняться.
Сейчас, когда прошло немного времени, а приложение уже готово и Скилл еще повысился, могу сказать о нем больше.
Изменилось ли мое мнение? Безусловно! Теперь я однозначно выделяю SwiftUI в своем списке фаворитов. Стоит отметить, что при его использовании то и дело возникают косяки, но в целом это не проблема SwiftUI, а скорее отдельных View, либо отсутствие необходимых данных т.к. Apple все же бывает скудна до документации там, где это необходимо.
В предыдущей публикации связанной со SwiftUI, я сформировал своеобразную шпаргалку по нему (посмотреть Вы её можете здесь - Тыц). Шпаргалку по азам т.к. там вы наврятли увидите, что-либо новое/интересное, а вот темой этой публикации, будет уже более широкий взгляд на этот чудесный инструмент.
Обратите внимание! Подача материала, специально гиперболизированна и более подробна т.к. материал читают не только гуру разработки, но и новички, не забывайте об этом.
Приложение
Приложение, разработка которого, позволила мне получить новый взгляд на SwiftUI доступно в AppStore, оно бесплатное - Тыц.
В нем используется классический набор: CoreData, UserDefault и SwiftUI.
Состав публикации
Вычисляемые свойства
Инициализация оболочек(оберток) свойств
Собственные оболочки свойств
Динамический предикат (NSPredicate)
@ViewBuilder
Optional(nil) != nil
Пару слов о NavigationView
Вычисляемые свойства
struct MyView: View {
@State var isDisplacedText: Bool = false
@State var isDisplacedRectangle: Bool = false
var body: some View {
VStack{
Text("Текст")
.offset(y: isDisplacedText ? 100 : 0)
Rectangle()
.offset(y: isDisplacedRectangle ? 100 : 0)
}
.padding(.top, isDisplacedText ? 100 : 0)
.padding(.top, isDisplacedRectangle ? 100 : 0)
}
}
Незатейливый пример, демонстрирует проблему смешивания логики и представления. Подход конечно является удобным, многие его используют, значительная часть туториалов его преподносят как единственно верное решение, но не стоит пренебрегать возможностями языка. Вот очевидное, но более (на мой взгляд) логичное и удобное решение.
struct MyView: View {
@State var isDisplacedText: Bool = false
@State var isDisplacedRectangle: Bool = false
private var offsetText: CGFloat {
isDisplacedText ? 100 : 0
}
private var offsetRectangle: CGFloat {
isDisplacedRectangle ? 100 : 0
}
private var paddingVstask: CGFloat {
(isDisplacedRectangle ? 100 : 0) + (isDisplacedRectangle ? 100 : 0)
}
var body: some View {
VStack{
Text("Текст")
.offset(y: offsetText)
Rectangle()
.offset(y: isDisplacedRectangle ? 100 : 0)
}
.padding(.top, paddingVstask)
}
}
Пример конечно очень примитивный и не раскрывает всей сути, но представьте сложное View, где много логики и это не вынесено за рамки body. Получается каша, которой очень сложно управлять. Вычисляемый свойства же, позволяют изменять отображение не хуже т.к. body тоже является вычисляемым свойством. Соответственно сложность вычислений никто не ограничивает и это особенно полезно в сложных ситуациях, где многое зависит от многого. Можно так же выделять отдельные методы и т.д.
struct MyView: View {
@State var isDisplacedText: Bool = false
@State var isDisplacedRectangle: Bool = false
private var offsetText: CGFloat {
isDisplacedText ? 100 : 0
}
private var offsetRectangle: CGFloat {
isDisplacedRectangle ? 100 : 0
}
private var paddingVstask: CGFloat { self.returnPaddingVstask() }
var body: some View {
VStack{
Text("Текст")
.offset(y: offsetText)
Rectangle()
.offset(y: isDisplacedRectangle ? 100 : 0)
}
.padding(.top, paddingVstask)
}
private func returnPaddingVstask() -> CGFloat {
(isDisplacedRectangle ? 100 : 0) + (isDisplacedRectangle ? 100 : 0)
}
}
Код приобретает более читаемый вид и делит View на логические блоки.
Инициализация оболочек свойств
Рассмотрим View
struct MyView: View {
@State var isShowed: Bool = false
var body: some View {
//...код
}
}
Согласно трактовке Apple (по возможности отказаться от инициализаторов и использовать .onApear) переодически он (инициализатор) бывает необходим и отказаться от его реализации не представляется возможным (ниже будет более необходимая ситуация, требующая инициализатор). Собственно посмотрим код
struct MyView: View {
@State var isShowed: Bool
init(isShowed: Bool){
self.isShowed = isShowed
}
var body: some View {
//...код
}
}
сейчас проблем не возникает, но такая же вариация инициализатора с @Binding не получится, чтобы понять почему вернемся к @State и запишем инициалиацию иначе
struct MyView: View {
@State var isShowed: Bool
init(isShowed: Bool){
self._isShowed = State(initialValue: isShowed)
}
//Более подробно
init(isShowed: Bool){
let stateValue: State<Bool> = State<Bool>(initialValue: isShowed)
self._isShowed = stateValue
}
var body: some View {
//...код
}
}
@State - это оболочка свойства, представленная State<>.
@Binding - представлен Binding<>, соответственно его инициализация доступна таким же образом как и выше
struct MyFirstView: View {
@State var isShowed: Bool
init(isShowed: Bool){
self._isShowed = State(initialValue: isShowed)
}
var body: some View {
MySecondView($isShowed)
}
}
struct MySecondView: View {
@Binding var isShowed: Bool
init(_ isShowed: Binding<Bool>){
self._isShowed = isShowed
}
var body: some View {
//...Код
}
}
Собственные оболочки свойств
Давайте теперь для примера создадим свою оболочку, которая будет обновлять представление и выполнять какие либо задачи.
@propertyWrapper struct File: DynamicProperty {
private let string: State<String>
private let path: URL
var wrappedValue: String {
get {
string.wrappedValue
}
nonmutating set { setWrappedValue(newValue) }
}
public var projectedValue: Binding<String> {
Binding(
get: { string.wrappedValue },
set: { wrappedValue = $0 }
)
}
private func setWrappedValue(_ value: String){
print("принял \(value)")
do {
try value.write(to: path, atomically: true, encoding: .utf8)
self.string.wrappedValue = value
} catch {
print("Ошибка записи")
}
}
init(wrappedValue fileName: String) {
self.path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName)
self.string = State(initialValue: (try? String(contentsOf: self.path)) ?? "пусто")
}
}
Обратите внимание! Код выше, просто пример, его нельзя использовать в коде без значительных доработок. Существует множество проблем. Например файл может изменить другой участок кода, постоянная запись в файл при каждом изменении и т.д.
Мы получаем контент из файла и записываем новое значение в файл при изменении свойства. Конечно же оно динамическое и позволяет перерисовывать интерфейс.
struct MyView: View {
@File var file = "file.txt"
var body: some View {
VStack{
Text(file)
TextField("", text: $file)
}
Button("Кнопка") {
file = "новое значение"
}
}
}
projectedValue, если выражать мысли простым языком, необходим для Binding связок
wrappedValue, то к чему мы собственно обращаемся и во что записываем при использовании
wrappedValue в инициализаторе, дает нам удобную инициализацию, но можно конечно сипользовать оболочки иначе
//...
init(name fileName: String) {
self.path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(fileName)
self.string = State(initialValue: (try? String(contentsOf: self.path)) ?? "пусто")
}
///...
@File(name: "file.txt") var doc
DynamicProperty протокол, который позволяет SwiftUI перерисовывать представление при изменении динамических свойств.
Можно записать еще короче и вместо State<> использовать @State в нашей оболочке. При таком использовании мы лишимся возможности использовать константу, но и сможем избавится от метода в немутируещем сеттере.
Динамический предикат (NSPredicate)
Посмотрите на представленный ниже код
struct MyApp: App {
let coreData = CoreDataController.shared
var body: some Scene {
WindowGroup {
MyView()
.environment(\.managedObjectContext, coreData.container.viewContext)
}
}
}
struct MyView: View {
@Environment(\.managedObjectContext)
private var viewContext
@FetchRequest( sortDescriptors:[], NSPredicate() )
private var myEntities: FetchedResults<MyEntity>
var body: some View {
//...код
}
}
Как видно, SwiftUI позволяет взаимодействовать с CoreData буквально в две строчки кода, используя все те же оболочки свойств.
Можно сортировать результат выборки сущностей, использовать предикаты, в общем классика. Идеальное решение, почти... Для реализации динамического предиката, нам потребуется инициализатор (это именно тот случай, где без него не обойтись).
struct MyView: View {
@Environment(\.managedObjectContext)
private var viewContext
private var myEntitiesRequest : FetchRequest<MyEntity>
var myEntities : FetchedResults<MyEntity> {
myEntitiesRequest.wrappedValue
}
init(predicate: NSPredicate) {
self.transactionRequest = FetchRequest(
sortDescriptors: [],
predicate: predicate
)
}
var body: some View {
//...код
}
}
Необходимо просто использовать то, что мы разобрали ранее.
Собственно теперь мы можем в одном View генерировать предикат, а в другом его использовать.
@ViewBuilder
Посмотрите на код ниже
struct MyView: View {
var body: some View {
VStack{
Rectangle()
Text("")
}
}
}
Внутри Vstack мы размещаем View. Как сделать так же?
Зайдем с другой стороны.
func example(value: String, closure: () -> Void){
print(value)
closure()
}
Мы видим функцию, которая принимает замыкание, вызвать можно вот так, все очевидно.
example(value: "значение", closure: { print("замыкание") } )
Теперь сократим.
example(value: "значение"){
print("замыкание")
}
Если убрать value или присвоить значение по умолчанию, то можно написать вот так.
example(){
print("замыкание")
}
Ну и конечно сократим скобки.
example{
print("замыкание")
}
Знакомая картина? Замыкание которое передается последним параметром, можно вынести за скобки. Замыкания представляют собой тип данных и их можно хранить в свойствах типов (с указанием @escaping), передавать в инициализаторе и т.д.
@ViewBuilder же возвращает View и тоже является замыканием. Соответственно реализация достаточно простая, нам нужно только определить получаемый тип данных, как тип соответствующий протоколу View
struct MyStack<Content>: View where Content: View {
let content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
var body: some View {
VStack{
Rectangle()
.frame(height: 100)
.foregroundColor(.gray)
Spacer()
content() //Все, что получает ViewBuilder
Spacer()
Rectangle()
.frame(height: 100)
.foregroundColor(.gray)
}
.background(Color.blue)
.ignoresSafeArea()
}
}
И использование
struct MyView: View {
var body: some View {
MyStack{
Text("Текст")
.foregroundColor(.white)
}
}
}
На выходе получим
Optional(nil) != nil
Вообще - это не относится к моему приложению, но относится к публикации. А так как приложение является причиной появляния публикации, одно другому не противоречит.
Так сложилось, что на днях я читал публикацию и там описывается интересная особенность SwiftUI. Она заключается в том, что при инициализации View с получаемыми View, в инициализацию может пройти Optional т.к. согласно документации Apple, Optional соответствут View. Следовательно Optional(nil) может так или иначе пройти в инициализацию и не дать провести проверку на nil т.к. Optional(nil) не равно nil, но при этом соответствует View. В той же публикации приведен участок кода как пример
Код из публикации с демонстрацией проблемы
struct ContentView: View {
var body: some View {
let v: Optional<Optional<Text>> = .some(nil)
VStack{
Helper.ultraView(title: "чук рулит")
Helper.ultraView(title: "гек норм", description: "потому что брат")
UltraView(
title: Text("текст"),
description: v)
}
.padding(.horizontal, 10)
}
}
struct UltraView<T1: View, T2: View>: View {
let title: T1
let description: T2? // вьюхи может и не быть же, верно?
// базовый конструктор
init(title: T1, description: T2?) {
self.title = title
self.description = description
}
// сокращенный вариант конструктора, когда второго элемента нет
init(title: T1) where T2 == EmptyView {
self.title = title
self.description = nil
}
var body: some View {
let color = description == nil ? Color.blue : Color.green
VStack {
title
if let description = description {
description
}
}
.frame(maxWidth: .infinity)
.padding()
.border(color) // маленький хелпер для рисования рамки
}
}
struct Helper {
static func ultraView(title: String, description: String? = nil) -> some View {
UltraView(
title: Text(title),
description: description.map { Text($0).font(.footnote) })
// ^ просто трансформируем опционал в Text
}
}
Мне стало интересно как работать с этой ситуацией и мне пришло в голову вот такое решение
extension Optional where Wrapped: View {
var isNil: Bool {
if ((self as AnyObject) as? NSNull) == nil {
return false
} else {
return true
}
}
}
Объясню что происходит. Несмотря на то, что SwiftUI работает с типами значений. View все же является протоколом. Как то ограничить class на соответствие протоколу нельзя, следовательно View можно привести к AnyObject, которому соответствуют все классы и к которому нельзя привести Optional. При преобразовании Optional(nil) к AnyObject мы получим null, который можно привести к NSNull, следовательно если наш View (потенциальный Optional(nil)) можно привести к NSNull через прокладку в виде AnyObject, то это nil. Довольно костыльное решение, но оно работает.
init(title: T1, description: T2?) {
self.title = title
self.description = description.isNil ? nil : description
}
Пару слов о NavigationView
NavigationView в SwiftUI работает с ошибками, часто проблемы возникают на ровном месте, представления выбрасывает, часть проблем мне понятны, а другая часть осталась загадкой.
Все привело к тому, что пришлось разрабатывать собственную навигацию, о ней я сделал отдельную публикацию - Тыц
See you later...
s21462
Спасибо за статью.
Я когда пытался изучить все это дело, мне было не понятно зачем мешать логику и UI и все это кажется очень не привычным, по сравнению с тем же XAML например.
Gummilion
Видимо, в Apple решили сделать свой аналог React, но на Свифте. А использовать XAML - ну это же как в Андроиде будет, как можно!