Uiwindow swift что это
Русские Блоги
UIView и UIWindow на основе Swift
Во-первых, связь между UIView и UIWindow
Как видите, UIView представляет собой прямоугольную область на экране и занимает очень важное место в приложении, поскольку почти все визуальные элементы управления в iOS являются подклассами UIView.
UIView наследуется от UIResponder, это холст, который отвечает за отображение, если окно сравнивается с рамкой изображения. Мы постоянно снимаем, заменяем или накладываем холст на раму, или накладываем другие холсты на холст, размер, конечно, определяет художник. С холстом мы можем делать с ним все, что захотим. Я выложу много простых вещей в библиотеке. Если вы разместите слишком много вещей, это не пойдет на пользу. Друзья, перейдите в файл библиотеки. Этот класс находится в UIView.h.
Функции UIView:
Основные функции окна:
Во-вторых, создание и свойства UIView
1. Создание UIView
Определите глобальный объект UIView
2. Метод свойств UIView
UIView может содержать много Subviews (других UIViews), и эти Subviews имеют так называемые иерархические отношения друг с другом, что немного похоже на концепцию слоев в программном обеспечении для рисования. Следующий код демонстрирует несколько уровней управления (Subview) на часто используемые методы.
Переместите Subview вперед или назад на один слой в UIView.Продвижение вперед закроет Subview нижнего слоя, а перемещение назад будет перекрыто Subview верхнего уровня.
Используйте индекс Index в UIView для обмена уровнями слоев двух подпредставлений.
Используйте имя переменной Subview, чтобы получить значение индекса (Index) в UIView.
// Получить индекс
let index = self.view.subviews.indexOf (имя вложенного представления)
Получите все Subviews в UIView, вызов этого метода вернет NSArray и перечислит эти Subviews в порядке от начала к началу.
UIView().subviews
3. На что следует обратить внимание в UIView
В-третьих, создание и свойства UIWindow
1. Создание UIWindow
Определите глобальную переменную UIWindow
2. Введение в свойства UIWindow
Сделать главное окно используемого объекта отображаемым на переднем плане экрана. Примечание:! Означает, что окно == nil запускается, но программа window == nil дает сбой!
Установите цвет фона окна
Вот лишь краткое введение в UIView и UIWindow, в основном для тех, у кого есть определенная основа OC. Я продолжу знакомить с ними в следующих статьях. Надеюсь, вы обратите внимание.
Swift UI — галопом по Европам
22:35. Восторг
Просмотрел WWDC 2019 Key Notes. Ожидаемый декларативный UI действительно стал явью, и это воистину событие вселенского масштаба для мира iOS-разработки. «Надо написать об этом статью», — подумал я и еще тысячи iOS-разработчиков по всему миру, пребывающих в состоянии экзальтации.
05:30. Туториалы
Swift UI — новый framework, разработанный Apple, написан на Swift, предназначен для декларативного описания UI в коде.
Заметил, что с каждым годом в плане документации у «Яблока» становится все круче и круче. В этот раз под Swift UI они запилили несколько полноценных туториалов с пошаговым добавлением и интерактивным отображением результата на view, а в конце еще заботливо добавили контрольные вопросы для закрепления пройденного. Ну прям сказка! Там же — ссылки на example-проекты.
Красиво!
Не буду пересказывать туториал на русском языке, в таком прекрасном виде его лучше потыкать в первоисточнике. Опишу свои впечатления и наблюдения по поводу всей этой истории cо Swift UI и немного побалуюсь с ним.
07:00. Установка
В новом Xcode появился новый режим редактирования кода — Editor And Canvas.
Канвас я увидел не сразу — для этого мало скачать Xcode 11.0, нужно еще обновить и Макось до 10.15. Без нее Xcode работать будет, но без прелестей в виде канваса и, возможно, чего-то еще.
Порадовало, что, когда выделяешь код, выделятся и соответствующий ему элемент в канвасе.
Новая ось, exampl’ы запущены. Крашится? Ну да, бывает. Подсветка отваливается? Нет конечно — в Xcode такого же никогда не было 😉 Но канвас работает, и изменения вьюшки отражаются моментально, если это не таблица со сложными ячейками.
09:22. Новый проект
При создании нового проекта теперь доступна опция Use Swift UI и проект создается с соответствующей конфигурацией.
Ну а сама Default Configuration лежит в Info.plist и SceneDelegate указывается там же. Все встало на свои места.
Но вернемся к SceneDelegate — запуск происходит именно в нем.
Протокол View прост до неприличия:
Тут просто: мы создаем вью из уже реализованных Views and Controls | Apple Developer Documentation. Могли бы сделать ее посложнее, используя различные контейнеры View Layout and Presentation | Apple Developer Documentation и вью, которые уже создали мы сами.
Попробуем же создать экран с примитивной таблицей:
Всё. Готово. Просто, лаконично, элегантно. Вот она вся прелесть от декларативного UI!
Видно, что List — это таблица под капотом. Но не UITableView, а некая UpdateCoalesingTableView.
А еще видно, что нет автолэйаута. Нет contstaint’s, всё на фреймах, а значит, нет этих сложных систем с линейными уравнениями и кучей расчетов. Но, с другой cтороны, верстка выходит адаптивной и фрейм как-то да рассчитывается — посмотрим в дальнейших сессиях WWDC, как именно.
Swift UI — обертка ли это над UIKit’ом? Похоже, что и да, и в то же время нет.
Joe Groff пишет следующее у себя в твиттере:
«Некоторые сущности — это обертки над UI/NS views, но тот факт, что они это делают, и то, какого типа вьюшку они оборачивают, — это вещь, которая может меняться».
Оригинал:
Some things wrap UI/NS views, but whether they do, and what view type they wrap, is subject to change. Best to think about it as a distinct thing.
11:00. Итого
Увы, все эти прелести
То есть переходить на них — это отказываться от старых осей, что слишком радикально и лишено заботы о пользователе.
Уверен, что механизм пока сырой, будет еще немало изменений, да и багов всплывет уйма, и это естественно. Но через год-два можно будет внедрять в прод.
Swift UI — прям революция в мире человеков, называющих себя iOS-разработчиками, и круто, что эта новая дорога, пусть и не идеальная, открылась.
Ну а пока ничего не мешает использовать это в своих пет-проектах, набивать руку и получать эстетическое наслаждение 🙂
Обработка жестов в iOS
В разработке приложений для iOS важно знать как работает система изнутри. По обработке событий много разной интересной инфы, но хотелось бы структурировать и собрать все в одну. Что я и попытаюсь сделать.
Структура:
Разбор основных кейсов
UIKit
Начнем с главного. Что такое ваш UIKit? Если посмотреть в официальную доку, то мы увидим такую инфу:
The UIKit framework provides the required infrastructure for your iOS or tvOS apps. It provides the window and view architecture for implementing your interface, the event handling infrastructure for delivering Multi-Touch and other types of input to your app, and the main run loop needed to manage interactions among the user, the system, and your app.
Так. Здесь мы узнали, что этот фреймворк помогает нам создавать архитектуру окон и вьюшек, инфраструктуру обработки событий, а также основной цикл выполнения. Давайте пройдемся по порядку.
Наше приложение стартует с имплементации экземпляра класса для UIApplication. Каждое iOS приложение имеет ровно один экземпляр UIApplication. Он маршрутизирует события пользователя, а также с помощью UIApplicationDelegate информирует о важных событиях (запуске приложения, не хватки памяти, завершении работы приложения).
Давайте посмотрим как это выглядит в коде. Если бы мы юзали сториборды, то это происходило автоматически. UIApplicationMain проверяет, использует ли ваше приложение сториборды. Он знает, используете ли вы main storyboard и каково ее имя, просмотрев ключ Info.plist “Main storyboard file base name” (UIMainStoryboard- File)
Но мы попробуем все сделать кодом:
UIApplicationMain создает экземпляр UIApplication и сохраняет этот экземпляр. К которому позже можно обращаться через UIApplication.shared
Затем он создает экземпляр класса делегата приложения. Система знает что это за класс, потому что мы пометили его @main (в ранних версиях @UIApplicationMain)
UIApplicationMain вызывает у делегата приложения метод application(_:didFinish- LaunchingWithOptions:).
Но интерфейс приложения не отображается, пока содержащее его window не станет ключевым окном приложения. Функция makeKeyAndVisible поможет нам
Но начиная с iOS 13, функции AppDelegate были разделены между AppDelegate и SceneDelegate. Это результат новой функции поддержки многооконного режима, представленной в ОС iPad, которая разделяет работу AppDelegate на две части.
UIApplicationMain вызывает у делегата приложения метод application(_:didFinish- LaunchingWithOptions:).
UIApplicationMain создает UISceneSession, UIWindowScene и экземпляр, который будет служить делегатом сцены окна
В Info.plist нужно указать (в виде строки) какой класс будет делегатом
UIApplicationMain проверяет, использует ли ваша начальная сцена сториборды. (В Info.plist нужно указать имя сториборда)
Если сцена имеет сториборд, то UIApplicationMain создает экземпляр UIWindow и назначает его делегатом сцены.
UIApplicationMain вызывает отображение интерфейса путем вызова метода экземпляра UIWindow makeKeyAndVisible.
В делегате сцены вызывается метод scene(_:willConnectTo:options:)
Не ожидайте, что window может быть только одно. Есть такие окна UITextEffectsWindow и UIRemoteKeyboardWindow.
Окей. Мы вроде разобрались как создается главный маршрутизатор всех событий. Но что такое эти события и как они выглядят?
Знакомьтесь. Это UIEvent. Главный объект, который содержит много нужной инфы для обработки событий. Когда обнаруживается системное событие, такое как прикосновение к экрану, UIKit внутри создает экземпляры UIEvent и отправляет их в очередь системных событий (main event loop), вызывая UIApplication.shared.sendEvent().
UITouch
Каждый экземпляр UIEvent содержит одно или несколько объектов UITouch. Для данного объекта UITouch могут произойти только четыре вещи. Они называются фазами касания и описываются свойством var phase: UITouch.Phase:
.began — Палец впервые коснулся экрана; этот экземпляр UITouch только что был создан. Это всегда первая фаза, которая наступает только один раз.
.moved — Палец двигается по экрану.
.stationary — Палец оставался на экране неподвижно. Для чего это нужно? Как только экземпляр UITouch был создан, он должен присутствовать каждый раз, когда прибывает UIEvent для этой последовательности мультитач. Таким образом, если UIEvent прибывает из-за того, что произошло что-то еще (например, новый палец коснулся экрана), UIEvent должен сообщить, что этот палец делал, даже если он ничего не делал
По сути этих 4х фаз достаточно, чтобы описать все действия пальца. Но возможна еще одна фаза:
.cancelled — Система прервала последовательность мультитача, потому что что-то прервало ее. Возможно, пользователь нажал кнопку «Домой» или кнопку блокировки экрана в середине последовательности. Возможно, появилось локальное уведомление.
UITouch также имеет такие свойства:
location(in:), previousLocation(in:) — Текущее и предыдущее местоположение этого касания относительно системы координат view.
timestamp — Когда тач последний раз менялся. Прикосновение получает отметку времени, когда оно создается (.began) и каждый раз, когда оно перемещается (.moved)
tapCount — Если два касания происходят примерно в одном и том же месте в быстрой последовательности, а первое короткое, второе можно охарактеризовать как повторение первого. Это разные сенсорные объекты, но второму будет назначено значение tapCount на единицу больше, чем у предыдущего
view — вьюшка, c которой связано это прикосновение
Когда UITouch впервые появляется (.began), ваше приложение определяет, с каким UIView оно связано. (Позже мы узнаем как это происходит). Затем это же UIView устанавливается как свойство var view сенсорного экрана и остается им. C этого момента этот UITouch всегда связан с этим view (до тех пор, пока этот палец не покинет экран).
Main Event Loop
Когда объект приложения получает событие из очереди событий, он отправляет его в window, в котором произошло пользовательское событие. Window отправляет событие в view, которое является для него наиболее подходящим обработчиком
Сразу после запуска приложение настраивает инфраструктуру для основного цикла событий
Когда приложение запускается, оно также устанавливает основную группу объектов, которые отвечают за отрисовку UI и обработку событий. Эти основные объекты включают window и различные виды вьюшек.
Когда объект приложения получает событие из очереди событий, он отправляет его в window, в котором произошло пользовательское событие. Window отправляет событие в view, которое является для него наиболее подходящим обработчиком
Окей. Вроде все понятно. Мы узнали про главный маршрутизатор событий, узнали о самих событиях. Но как события доходят до точки исполнения?
Responder Chain
Экземпляры UIResponder — основные обработчики событий в приложении. Почти все ключевые объекты являются респондерами (UIApplication, UIWindow, UIViewController, UIView).
Чтобы получать события, респондер должен реализовать соответствующие методы обработки событий и, в некоторых случаях, сообщить приложению, что оно может стать первым респондером
Респондеры получают необработанные данные о событии и должны либо обработать событие, либо переслать его другому объекту-респонденту. По связанному списку от репондера к респондеру.
Если первый респондер не может обработать сообщение о событии или действии, он пересылает его «следующему респондеру». Если объект в цепочке респондента не может обработать событие или действие, он передает сообщение следующему респондеру в цепочке. Сообщение движется вверх по цепочке к объектам более высокого уровня, пока не будет обработано. Если он не обрабатывается, то приложение отбрасывает его.
У респондера есть несколько методов обработки событий:
touchesBegan(_:with:) — во view или window произошло одно или несколько новых касаний.
touchesMoved(_:with:) — Сообщает респонденту, когда одно или несколько касаний, связанных с событием, изменились.
touchesEnded(_:with:) — Сообщает респонденту, когда один или несколько пальцев поднимаются из вида или окна.
touchesCancelled(_:with:) — Сообщает респонденту, когда системное событие (например, системное предупреждение) отменяет последовательность касаний.
Аргументы этих методов:
touches: Set — множество прикосновений. Если во множестве только одно касание, то мы получаем его. Если же во множестве много то выполнится first метод (набор неупорядочен, поэтому какой элемент будет первым система выберет произвольно).
event: UIEvent? — сущность объекта UIEvent
Gesture Recognizer
Процесс распознавания жестов довольно сложный механизм. Еще сложней, когда мы хотим обрабатывать разные типы жестов. Решением являются Gesture Recognizers (субкласс UIGestureRecognizer), которые стандартизируют общие жесты и позволяет разделять и инкапсулировать код для разных жестов в разные объекты. Благодаря распознавателям жестов нет необходимости создавать подкласс UIView только для того, чтобы реализовать интерпретацию касания.
Gesture Recognizer — это объект, задача которого обнаруживать, что последовательность мультитач приравнивается к одному конкретному типу жеста. Он прикреплен к UIView. Мы можем добавлять и удалять распознаватели:
UIGestureRecognizer реализует четыре метода касания, но он не является респондером. Поэтому не участвует в responder chain.
По сути это обычный словарь, который хранит все жесты.
В кейсе ниже мы реализуем, используя распознаватель жестов, вьюху, которая позволяет перетаскивать себя в любом направлении одним пальцем.
Window доставляет события касания в словарь распознавания жестов, прежде чем оно доставляет их в hit-testing view
Закрепление доставки событий
Таким образом давайте закрепим доставку тачей от точки прикосновения до точки управления событием:
Когда появляется новое касание, приложение выполняет проверку нажатия, чтобы определить view, к которому прикоснулись. Это view будет навсегда связано с этим касанием и соответственно будет называться hit-test view
Когда происходит другое касание приложение вызывает собственный метод sendEvent(:), который в свою очередь вызывает sendEvent(🙂 окна (window). Window прокладывает путь к прикосновению
Но как этот путь прокладывается?
Hit-Testing
Hit Testing — это рекурсивный поиск среди всей иерархии вьюх к какой прикоснулся пользователь. iOS пытается определить, какой UIView является самой передней вьюшкой под пальцем пользователя, которая должна получить событие касания.
На диаграмме выше hit-testing выполняется каждый раз, когда палец касается экрана. И до того, как какое-либо средство распознавания представления или жеста получит объект UIEvent, представляющий событие, которому принадлежит касание. Полученная UIView становится firstResponder.
Метод hitTest(_: with 🙂 реализует логику проверки касания исключительно для этой вьюхи. Если isUserInteractionEnabled представления имеет значение false, или его isHidden имеет значение true, или его alpha близка к 0,0, то hitTest возвращает nil, что означает, что ни эта вьюха, ни другая из её сабвьюх не могут быть вьюхой для следующего вызова hitTest.
Алгоритм начинается с отправки сообщения экземпляру UIApplication путем вызова sendEvent (_ :). а UIApplication, в свою очередь, передает его UIWindow, вызывая его sendEvent (_ :). Затем UIWindow выполняет сложную логику проверки для каждого тача его иерархии вьюшек
Также нашел схему, которая описывает логику hit testing’а:
Разбор основных кейсов
1. Кейс с выпирающей вьюхой
Кейс очень распространенный. О нем писал яндекс и много ребят на западных ресурсах.
Задача простая. Что будет, если мы нажмем на выпирающую область вьюхи C?
Если вспомнить доку, то ответ будет таким
If a touch location is outside of a view’s bounds, the hitTest(_:with:) method ignores that view and all of its subviews. As a result, when a view’s clipsToBounds property is false, subviews outside of that view’s bounds are not returned even if they happen to contain the touch.
Touch будет проигнорирован вью B. Его координаты не попадают в её область отрисовки. А значит, что самой глубокой вью, которая примет нажатие, будет view A.
В этом случае мы можем переписать вью и написать свой код
Мы определяем, находится ли поинты внутри любых из сабвьюх основной супервьюхи.
Если мы не получаем ни одной, которое включает данную точку, мы проверяем, действительно ли это родительское представление получило обращение
Ниже приведен вспомогательный метод, который проходит через сабвьюхи, и определяет, принадлежат ли поинты какой-либо из супервьюх.
2. Кейс с увеличением нажатия области кнопки
Допустим мы решили увеличить область нажатия кнопки «Х». Пришел дизайнер и сказал, что сложно закрыть эту вьюшку. И нужно, не меняя размера, изменить область нажатия. Тач должен произойти в красной области, а UIButton должен это подхватить
Решение изменить область при тапе с помощью метода pont(inside:):
3. Решение задачи с вырезанной внутри дыркой
Как сделать так, чтобы при нажатии на контент внутри синей вьюшки обрабатывались события вьюшек, под ней лежащих?
Условие задачи — вызвать событие зеленой или желтой вьюшки, которые находятся в центре синей
4. Проход hitTest по слоям
Как мы знаем hitTest работает только с вьюшками. Но это не совсем так.
Допустим у нас есть такой код
Есть 1 вьюшка и в ней 3 слоя.
Но допустим я захочу увеличить один из слоев при таче. Для этого мне стоит переопределить hitTest UIWindow, чтобы ни один из слоев не потерялся
теперь при нажатии hitTest UIWindow проверяет условие наших слоев и выполняет трансформацию
Итого, в этой статье я попытался собрать общую информацию из статей и док, а ткже разобраться в некоторых аспектах сам. Соглашусь, что многие из кейсов специфичные, но все же знать, мне кажется, полезно.
SwiftUI по полочкам
Каждый раз, когда в языке программирования появляется новый Фреймворк, рано или поздно, появляются люди, которые изучают язык именно с него. Вероятно так было и в IOS разработке во времена появления Swift: поначалу он рассматривался как дополнение к Objective-C — но я этого уже не застал. Сейчас, если начинаешь с нуля, выбор языка уже не стоит. Swift вне конкуренции.
То же самое, но в меньшем масштабе, происходит и с фреймворками. Появление SwiftUI — не исключение. Вероятно, я — представитель первого поколения разработчиков, кто стартовал с изучения SwiftUI, проигнорировав UIKit. У этого есть своя цена — обучающих материалов и примеров работающего кода пока очень мало. Да, в сети уже есть некоторое количество статей, рассказывающих о той или иной особенности, том или ином инструменте. На том же www.hackingwithswift.com уже довольно много примеров кода с объяснениями. Однако, они слабо помогают тем, кто решил изучать SwiftUI с нуля, как я. Большинство материалов в сети — это ответы на конкретные, сформулированные вопросы. Опытный разработчик легко разберется, как все устроено, почему именно так, и зачем это нужно применять. Новичку же, сначала нужно понять, какой вопрос задать, и только тогда он сможет добраться до этих статей.
Под катом я попробую систематизировать и разложить по полочкам то, что сам успел усвоить на текущий момент. Формат статьи — почти гайд, хотя скорее, шпаргалка, составленная мной в том виде, в котором я сам бы хотел ее прочитать в начале своего пути. Для опытных разработчиков, еще не вникавшим глубоко в SwiftUI, тоже найдется пара интересных примеров кода, а текстовые пояснения можно читать по-диагонали.
Надеюсь статья поможет вам сэкономить некоторое время, когда вы тоже захотите ощутить немного магии.
Для начала, немного о себе
У меня практически нет бекграунда мобильной разработки, а существенный опыт
в 1с мало чем мог бы здесь помочь. О том, как и почему я решил осваивать SwiftUI я расскажу как-нибудь в другой раз, если это кому-то будет интересно, конечно.
Так уж сложилось, что начало моего погружения в мобильную разработку совпало с выходом IOS 13 и SwiftUI. Это знак, подумал я, и решил стартовать сразу с него, проигнорировав UIKit. Я посчитал забавным совпадением то, что работать с 1с я начал в подобные времена: тогда только-только появились управляемые формы. В случае с 1с, популяризация новой технологии заняла, без малого, лет пять. Каждый раз, когда разработчику поручали реализовать какой-то новый функционал, он вставал перед выбором: сделать это быстро и надежно, знакомыми инструментами, или потратить кучу времени на возню с новыми, и без гарантий результата. Выбор, обычно, делался в пользу скорости и качества прямо сейчас, а инвестирование времени в новые инструменты очень долго откладывалось.
Сейчас, судя по всему, со SwiftUI примерно такая же ситуация. Всем интересно, все понимают, что за этим будущее, но выделять существенное время на его изучение пока мало кто берется. Разве что для пет-проектов.
Мне, по большому счету, было без разницы, какой фреймворк изучать, и я решил рискнуть несмотря на общее мнение, что в production его можно будет запускать через год-два. И раз уж так получилось, что я оказался среди первопроходцев, я решил поделиться практическим опытом. Я хочу сказать, что я не гуру, и вообще в мобильной разработке — чайник. Тем не менее, я уже прошел определенный путь, в процессе которого перерыл все интернеты в поисках информации, и могу уверенно заявить — ее мало, и она практически не систематизирована. А на русском, само собой, ее вообще практически нет. Раз так, я решил собраться с силами, задвинуть подальше комплекс самозванца, и поделиться с сообществом тем, в чем успел разобраться сам. Я буду исходить из предположения, что читатель уже хотя бы минимально знаком со SwiftUI, и не буду расшифровывать такие вещи как VStack <…>, Text(…) и т.п.
Еще раз подчеркну, что далее я буду описывать свои собственные впечатления от попыток добиться от SwiftUI требуемого результата. Я вполне мог чего-то не понять, а из некоторых экспериментов сделать ошибочные или неточные выводы, так что любые поправки и уточнения категорически приветствуются.
Опытным разработчикам данная статья может показаться полна описания очевидных вещей, но не судите строго. Учебников для чайников по SwiftUI еще не написано.
Что вообще такое, этот ваш SwiftUI
Итак, я бы пожалуй начал с того, что вообще такое, этот ваш SwiftUI. Тут опять всплывает мое 1с-ное прошлое. Аналогия с управляемыми формами стала только сильнее, когда я посмотрел несколько обучающих видео, как верстаются интерфейсы в Storyboard (т.е. при работе с UIKit). Аж ностальгия взяла по «не управляемым» формам в 1с: ручное размещение элементов на форме, а особенно — привязки… О, когда автор обучающего видео минут 20 рассказывал о тонкостях привязок различных элементов друг к другу и краям экрана, я с улыбкой вспоминал 1C — там до управляемых форм было все тоже самое. Ну почти… чуть победнее, разумеется, ну и соответственно — попроще. А SwiftUI — это, грубо говоря, управляемые формы от Apple. Никаких привязок. Никаких сторибордов и сегвеев. Вы просто описываете в коде структуру вашего View. И все. Все параметры, размеры и прочее задается непосредственно в коде — но довольно просто. Точнее, редактировать параметры существующих объектов можно в Canvas, но для этого, их сначала нужно в коде добавить. Честно говоря, не знаю, как это будет работать в больших командах разработчиков, где принято разделять верстку дизайна и наполнение кодом самой View, но мне как инди-разработчику такой подход очень нравится.
Декларативный стиль
«Это View. Она
(мне почему-то хочется говорить «вьюшка», и соответственно, применять склонения как к слову женского рода)состоит из двух текстовых полей и одного рисунка. Текстовые поля расположены друг за другом горизонтально. Картинка находится под ними и ее края обрезаны в форме круга».
«Вставить блок, в этот блок вставить текстовое поле, за ним еще одно текстовое поле, а после этого, взять картинку, обрезать ее края скруглив их, и вставить ниже».
Звучит как инструкция к мебели из Икеи. А в swiftUI мы сразу видим то, каким должен быть результат. Даже без Сanvas-а или отладки, структура кода наглядно отражает структуру View. Понятно что и в какой последовательности будет отображаться и с какими эффектами.
Отличная статья о FunctionBuilder, и как он позволяет писать код в декларативном стиле уже есть на Хабре.
В принципе, о декларативном стиле и его преимуществах написано достаточно много, так что закругляюсь. От себя добавлю, что немного пообвыкшись, я действительно прочувствовал, насколько удобно писать код в этом стиле, когда речь идет об интерфейсах. С этим Apple попали, что называется, в самое яблочко!
Из чего состоит View
Но давайте подробнее. Apple предполагает, что декларативный стиль это так:
И так, полочки
Всего, я насчитал три типа элементов, из которых строиться тело View:
В общем, контейнерами я считаю любые View, в которые в качестве параметра можно передавать Content.
И это все. Все элементы чистокровного SwiftUI можно отнести к одному из этих типов. Да, этого недостаточно чтобы наполнить ваши View функционалом, но это все что вам нужно, чтобы показать ваш функционал на экране.
.модификаторы() — как они устроены?
Content + ViewBuilder = View
`content` is a proxy for the view that will have the modifier represented by `Self` applied to it.
Вернемся к нашему модификатору. По идее, объявления структуры FrameFromSize уже достаточно, чтобы начать применять его. Внутри body мы можем написать так:
Но можно сделать еще лаконичнее, объявив собственный модификатор как функцию, расширив тем самым возможности протокола View.
Точно так же можно создавать любые кастомные модификаторы, и использовать их так же, как и встроенные в SwiftUI:
Удобно, лаконично, наглядно.
Я представляю себе цепочку модификаций как бусины, нанизанные на нитку — нашу View. Эта аналогия верна и в том смысле, что порядок вызова модификаций имеет значение.
И все же View
some View — удобство
Ключевое слово some — это «generic» вариант описания типа, возвращаемого замыканием, который не зависит ни от чего, кроме самого написанного кода. Т.е. Результатом обращения к вычисляемому свойству body нашего View должна быть какая-то структура, удовлетворяющая протоколу View. Их может быть много — Text, Image, а может быть, какая-то объявленная вами структура. Вся фишка ключевого слова some — это объявить «generic», удовлетворяющий протоколу View. Он статически определяется кодом, реализованным внутри тела вашей View, и XCode вполне в состоянии разобрать этот код, и вычислить конкретную сигнатуру возвращаемого значения (ну, почти всегда). А some — это всего лишь попытка не обременять разработчика излишними церемониями. Разработчику достаточно сказать: «на выходе будет какая-то View», а какая именно — разбирайтесь сами. Ключевое здесь — конкретный тип определяется не входящими параметрами, как с обычным generic-типом, а непосредственно кодом. Поэтому выше, generic я указывал в кавычках.
Из этого типа можно восстановить примерный код:
some View — и последствия
Подход с вычислением статического типа выражения внутри body рождает, на мой взгляд, два важных замечания:
Кроме body
Однако, в структуре могут быть и другие параметры, с которыми можно работать. В качестве параметров мы можем объявлять следующие вещи.
Внешние параметры
Это простые параметры структуры, которые мы должны передавать извне при инициализации, для того чтобы View каким-то образом их визуализировала:
В данном примере textValue для структуры TextView — это параметр, который должен быть заполнен извне, поскольку он не имеет значения по-умолчанию. Учитывая, что структуры поддерживают автоматическую генерацию инициализаторов — мы можем использовать данную View просто:
Извне также можно передавать замыкания, которые нужно выполнить, при наступлении какого-то события. Например, Button(lable:action:) так и делает: выполняет переданное замыкание action при нажатии на кнопку.
state — параметры
SwiftUI очень активно использует новую фишку Swift 5.1 — Property Wrapper.
Если вы не ищете легких путей, или вам нужна дополнительная функциональность, вместо данной обертки можно использовать ObservableObjectPublisher и отправлять уведомления вручную, используя события willSet() данных параметров, как описано, например, тут.
Помните, я говорил, что body — это просто вычислимое свойство? По-началу я не сразу понял всю фишку State-переменных, и пытался объявлять какие-то State-переменные внутри body безо всяких оберток. Проблема оказалась в том, что body — это, как я уже говорил, stateless инструкция. View сгенерировалась по этой инструкции, и весь контекст, объявленный внутри body отправился на свалку. Дальше живут только хранимые параметры структуры. При изменении State-параметров вся наша View обновляется. Снова достается инструкция, в нее подставляются текущие значения всех параметров структуры, собирается изображение на экране, инструкция снова выкидывается до следующего раза. Переменные, объявленные внутри body — вместе с ней. Для опытных разработчиков это может быть очевидно, но я поначалу, намучался с этим, не понимая сути процесса.
Вы не сможете использовать didSet willSet события параметров структуры, обернутых в какие-то обертки. Компилятор позволяет вам написать этот код, но он просто не выполняется. Вероятно потому, что обертка — это и есть какой-то шаблонный код, выполняемый при наступлении этих событий.
update:
Оказывается, это был баг, наблюдатели свойства не срабатывали если значение модифицировалось не прямым присвоением, а как-то иначе. Например до версии XCode 11.5 вот этот код в одной ветке вызывал срабатывание willSet и didSet, а в другой ветке — нет.
В XCode 11.5 это пофиксили, и обзёрверы срабатывают корректно в обеих ветках кода.
Binding- параметры
Обратите внимание, внутри init для обращения к параметрам, обернутым в какие-то @PropertyWrapper следует использовать знак подчеркивания self._ — это работает в инициализаторах, когда self еще в процессе создания. Точнее, с помощью self._ мы обращаемся к параметру вместе с его оберткой. Обращение непосредственно к значению внутри обертки осуществляется без подчеркивания.
EnvironmentObject
Обычно, передается текущее состояние приложения, или какой-то его части, которая нужна сразу многим View. Например, данные о пользователе, сессии или чем-то подобном, есть смысл положить в EnvironmentObject один раз, в корневой View. В каждой View, где они нужны, их можно достать из окружения, объявляя переменную с оберткой @EnvironmentObject например так
@Environment — это почти тоже самое. По смыслу — это состояние среды, т.е. ОС. Через эту обертку удобно доставать, например, положение экрана (вертикальное или горизонтальное), светлая или темная тема используется, и т.п. Так же, через эту обертку можно получать доступ к БД при использовании CoreData:
Кстати, для работы с CoreData в SwiftUI тоже сделано довольно много интересного. Но об этом, пожалуй, уже в следующий раз. Итак статья разрослась сверх всяких ожиданий.
Custom @PropertyWrapper
Результат можно использовать очень лаконично, сосредоточившись на логике, и визуальном отображении, а вся работа делается под капотом.
Пример изначально описан здесь.
Контейнеры
Просто, первые два параметра не являются обязательными, и в большинстве примеров пропускаются. Ошибка новичка — не посмотреть полный синтаксис.
ForEach
ForEach принимает три параметра: коллекцию ( data: RandomAccesCollection ), адрес идентификатора элемента коллекции ( id: Hashable ) и контент ( content: ()->Content ). Третий мы уже обсуждали: как любой другой контейнер, ForEach принимает Content — т.е. замыкание. Но в отличие от обычных контейнеров, где content не содержит параметров, ForEach передает в замыкание элемент коллекции, который можно использовать при описании контента.
Кстати, организовать обход элементов коллекции, которые не удовлетворяют Identifiable, можно и с помощью индексов. Например так:
В этом случае, обход будет строиться не по самим элементам, а по их индексам. Для небольших коллекций, это вполне приемлемо. На коллекциях с большим количеством элементов это, вероятно, может сказаться на производительности, особенно когда элементы коллекции не ссылочных типов, а, например, объемные строки или JSON данные. В общем, применять с осторожностью.
Custom container View
Ну и как повелось, приведу пример создания кастомного контейнера. Довольно часто, взаимосвязь нескольких объектов типа “1:N” может быть удобно представлять в виде словаря. Выполнить запрос и конвертировать его результат в словарь типа dict: Uiwindow swift что это] не сложно.
Если в вашем приложении планируется какая-то аналитика с группировкой, то есть смысл создать отдельный контейнер для отображения такого рода словарей, чтобы не дублировать весь код в каждой вью. А с учетом того, что группировки могут меняться пользователем, нам придется использовать generic. Я не стал усложнять, добавляя визуальное оформление, оставил только структуру нашего контейнера:
Пример использования нашего контейнера:
и результат на экране в Canvas:
to be continued
На этом пока все. Я хотел так же осветить все грабли, на которые я наступил, пытаясь использовать CoreData в связке с SwiftUI, но, откровенно говоря, не ожидал, что только основы SwiftUI займут столько времени, а статья выйдет такой объемной. Так что, как говориться, продолжение следует.
Если есть что добавить, или исправить — добро пожаловать в комментарии. Существенные замечания постараюсь отразить в статье.