If you're interested in the living heart of what you do, focus on building things rather than talking about them.
В этой главе мы познакомимся с разными милыми штучками и научимся работать с SafariServices
- фреймворком, расщиряющим способности Safari. Также мы изучим несколько полезных особенностей UIKit
.
Живое представление или LiveView
позволяет вынести какое-то представление или целый контроллер в специальное окно, что позволит им работать как приложению с одним окном (за исключением множества аспектов, вроде того, что это всё-таки не приложение - его нельзя запустить извне без дополнительных настроек, у него не всегда есть свой набор свойств).
До начала работы отключите автоматическое выполнение кода, чтобы запускать его вручную, так как этот код будет несколько прожорливым.
Сперва создайте игровую площадку iOS и добавьте бесконечное выполнение для неё:
Ответimport UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
Теперь создадим представление в виде прямоугольника 125*125
и сделаем его живым:
let view = UIView(frame: CGRect(x: 0, y: 0, width: 125, height: 125))
PlaygroundPage.current.liveView = view
Аниматор свойств - специальный объект, ведущий себя как функция UIView.animate
за исключением того, что это объект и можно управлять его выполнением, добавляя анимации в процессе и, например, меняя их направление.
Теперь мы создадим класс-обёртку, так как нам надо добавить на наше живое представление распознаватель нажатий, а он принимает лишь селекторы. В свою очередь селекторы требуют цель, а также нельзя взять селектор на глобальную функцию:
class Functions {
@objc func viewTapped(_ sender: Any) {
let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) {
//Анимации
}
animator.startAnimation()
}
}
По аналогии с animate
нам нужно добавить внутрь блока изменение анимируемых свойств. Попробуйте сделать таковыми задний фон, изменив его на белый, а также сжатие в два раза прямоугольника.
view.backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
Сделаем распознаватель жестов:
let functions = Functions()
let gesture = UITapGestureRecognizer(target: functions, action: #selector(Functions.viewTapped(_:)))
view.addGestureRecognizer(gesture)
Чтобы открыть Live View
, нужно выбрать соответствующий пункт в редакторе-ассистенте.
Теперь при нажатии на представление справа оно будет сжиматься.
Что будет, если сделать растяжение вместо сжатия? Ничего.
Live View
- небесконечная область, а ограничивается заданным ему представлением. А если нажать несколько раз? Будет всего одно сжатие, так как афинное преобразование задаёт изменение объекта относительно начального состояния и они не накапливаются.
Предположим, Вам нужно встроить в приложение веб-браузер или внешнюю форму аутентификации с ресурсами компании. Что же делать? Есть два разных подхода:
SafariServices
- набор готовых компонент, позволяющих добавлять микро-версию Safari в приложение. Данный подход наиболее оптимален, когда просто нужно использовать веб-страницы, не изменяя их контент, и не добавляя дополнительных веб-обработчиков.WebKit
- содержит набор более базовых компонент для создания своих представлений и контроля их элементов. Его функции следует вызывать лишь из главного потока.В очень многих руководствах до сих пор описывается старая версия
API
. Отличить их можно по присутствию префиксаWK
- новоеWKWebView
, старое -WebView
.
Создадим класс, который будет включать в себя контроллер Safari:
import SafariServices
let url = URL(string: "https://swiftworld.ru")!
class ViewController: UIViewController, SFSafariViewControllerDelegate {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
}
Теперь будем добавлять в метод появления главного представления контроллера функционал. Начнем с объекта конфигурации для представления:
let configuration: SFSafariViewController.Configuration = {
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = true
//Если можно перейти в режим чтения, то мы это сделаем
config.barCollapsingEnabled = true
//Сжимается ли верхняя панель при прокрутке контента, как в Safari
return config
}()
И сам контроллер:
let controller = SFSafariViewController(url: url, configuration: configuration)
do {
controller.delegate = self
controller.dismissButtonStyle = .close
//Каков стиль конпки закрытия
controller.preferredBarTintColor = #colorLiteral(red: 0.925490200519562, green: 0.235294118523598, blue: 0.10196078568697, alpha: 1.0)
//Стиль панели навигации
}
present(controller, animated: true)
Необычно для нас здесь использование блока do
без catch
. Как альтернативу замыканиям (но лучше всё же использовать замыкания) возможно создавать участки кода с помощью do
, при этом перехватывать что-либо необязательно. Такой блок ничем не отличается от обычного - он захватывает родительскую область видимости и создаёт свою локальную.
Также можно задать цвет элементов управления через свойство
controller.preferredControlTintColor
И да - наша площадка уже позволяет нам просматривать сайты!
PlaygroundPage.current.liveView = ViewController()
Но нам этого мало: мы объявили, что наш класс реализует делегат для Safari. Добавим метод, который будет срабатывать по нажатию на эту кнопку:
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
dismiss(animated: true, completion: nil)
UIApplication.shared.open(url)
}
Вторая строка метода открывает ссылку в приложении, которое зарегистрировано на её открытие. Можно также передать опции и обработчик завершения. Так как для веб сайтов стандартным обработчиком выступает веб-бразуер, то сработает именно он.
На iOS в приложении
Swift Playgrounds
запрещено явное использование доступа кUIApplication.shared
.
Другие методы делегата:
safariViewController(_:didCompleteInitialLoad:)
- завершена начальная загрузка. Метод срабатывает лишь для загрузки изначальной страницы.safariViewController(_:activityItemsFor:title:)
- пользователь взывал пункт с Активити. Можно предоставить свои собственные элементы для ссылок.safariViewController(_:excludedActivityTypesFor:title:)
- исключённые действия для ссылки.safariViewController(_:initialLoadDidRedirectTo:)
- будет передан делегату, если загрузка изначальной страницы привела к редиректу.До 11-ой версии, Фреймворк позволял разделять контент обычного Safari и всех объектов данного типа. Но затем в целях безопасности это было отключено.
Но был введён специальный механизм - сессия аутентификации. Она получает данные входа в веб-приложение, которые были получены в других приложениях.
let session = SFAuthenticationSession(url: url, callbackURLScheme: nil) { (url, error) in
print(error, url)
}
session.start()
Чтобы эти данные были доступны, приложение должно всегда где-то хранить сильную ссылку на объект типа. Аргументы:
OAuth
.Закомментируйте начало сессии прежде, чем двигаться дальше.
С точки зрения логического устройства глаза - человеческого зрение старается найти какие-то области резкости на изображениях - переходы, символы, выделения. Но если размещать текст близ границы какой-то области, то его станет менее удобно читать. Все представления обладают специальным свойством readableContentGuide
, через которое возможно задать область, в которой можно помещать объекты, которые должно быть можно отделять зрением.
Сперва создадим обычный лейбл:
let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
label.text = "Hello, world"
label.sizeToFit()
//Расширяем представление, чтобы уместить его контент.
return label
}()
view.addSubview(label)
PlaygroundPage.current.liveView = view
Согласитесь, трудно прочесть текст, так как метка находится слишком близко к краям представления.
Вы можете отрендерить Вашу разметку игровой площадки, перейдя в
Editor -> Show Rendered Markup
Чтобы исправить это создадим ограничения, которые прикрепят наше представление к зоне читаемости:
label.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor).isActive = true
label.topAnchor.constraint(equalTo: view.readableContentGuide.topAnchor).isActive = true
Метку стало легче отследить взглядом!
Есть ряд устройств, на которых сенсор касания срабатывает быстрее, чем экран - так на iPad Air 2 частота обновления экрана 60 герц (она же задаёт верхний предел частоты кадров), а сенсор способен получать касания в два раза чаще. В любом случае система не сможет принять больше действий в секунду, чем позволяет частота экрана. Потому часть действий пересекается, если они расположены близко друг к другу.
Мы попробуем создать систему, ускоряющую касания, путём использования всех пересекающихся действий, а не только основного.
class MyView: UIView {
var points = [CGPoint]()
func drawForFirstTouch(in set: Set<UITouch>, event: UIEvent?) {
guard let touch = set.first,
let event = event,
let touches = event.coalescedTouches(for: touch),
//Пересекающиеся действия для события
touches.count > 0 else {
return
}
points += touches.map{$0.location(in: self)}
setNeedsDisplay()
}
}
В конце метода мы просим систему перерисовать наше представление целиком. Передав в него прямоугольную область, можно запросить перерисовку только лишь её.
Запрос на перерисовку отработает только для объектов, с нативными слоями - например, из Core Graphics, UIKit и так далее. К счастью почти все и всё используют именно их.
Можно перезаписать метод, срабатывающий при начале касаний к представлению:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
points.removeAll(keepingCapacity: true)
drawForFirstTouch(in: touches, event: event)
}
Мы очищаем уже имеющиеся точки и заносим точки первого касания.
Существует вероятность, что жест непрерывного касания будет прерван случайным срывом или переходом в запрещённую или запредельную область:
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
points.removeAll(keepingCapacity: true)
setNeedsDisplay(bounds)
}
В таком случае мы сбрасываем точки и перерисовываем представление.
При смещении пальца пользователя происходит move
:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
drawForFirstTouch(in: touches, event: event)
}
Помимо сцепленных касаний, система обладает алгоритмом, позволяющим получить предсказанные касания - система на основе прошлых данных умеет додумывать, куда ещё коснётся пользователь:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first, let event = event, let predictedTouches = event.predictedTouches(for: touch), predictedTouches.count > 0 else {
return
}
points += predictedTouches.map{$0.location(in: self)}
setNeedsDisplay()
}
Мы используем их для более плавного окончания рисования.
Данные касания нужны для сокращения лага ввода и позволяют начать отрисовку, еще не наступивших действий. Однако после получения настоящего результата следует отбросить эти данные.
Напоследок перезапищем метод для отрисовки представления:
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
//Для примитивного рисования нужно получить текущий графический контекст
context?.setFillColor(#colorLiteral(red: 0.1215686275, green: 0.1294117647, blue: 0.1411764706, alpha: 1))
context?.fill(rect)
//Мы заполняем весь фон черным цветом.
context?.setStrokeColor(#colorLiteral(red: 0.9215686275, green: 0.3764705882, blue: 0.2745098039, alpha: 1))
//Задаем цвет для линий
for point in points {
context?.move(to: point)
//Обходим от точки к точке
//И если она не последняя, то соединяем их линиями
if let last = points.last, point != last {
let next = points[points.index(of: point)! + 1]
context?.addLine(to: next)
}
}
context?.strokePath()
}
Есть множество методов рисования в
Core Graphics
по контексту, но их слишком много, чтобы расписывать их здесь все. Вы можете подробнее узнать о них здесь CGLayer - Core Graphics | Apple Developer Documentation
И последний штрих:
let myView = MyView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
myView.backgroundColor = #colorLiteral(red: 0.9215686275, green: 0.3764705882, blue: 0.2745098039, alpha: 1)
PlaygroundPage.current.liveView = myView
Лучше запускать это на реальном устройстве, так как игровая площадка в Xcode для кода на iOS использует неявный симулятор.
К некоторым устройствам на iOS бывает полезным подключить клавиатуру, что позволит выполнять некоторые действия быстрее. Мы можем создать свой собственный контроллер для управления нажатиями клавиш.
class KeyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let command = UIKeyCommand(input: "N", modifierFlags: [.command, .alternate, .control], action: #selector(handleCommand(_:)))
addKeyCommand(command)
}
}
PlaygroundPage.current.liveView = KeyViewController()
Так в строке надо передать саму клавишу, а в флаги модификаторы - специальные клавиши, которые должны быть нажаты с данной. Мы сделали команду по нажатию CMD+CTRL+OPT+N.
Долгое нажатие CMD вызовет меню со всеми доступными в данный момент клавиатурными сочетаниями.
Попробуйте самостоятельно сделать метод класса, вызывающий контроллер алертов с кнопкой закрытия.
Ответ@objc func handleCommand(_ cmd: UIKeyCommand) {
let alert = UIAlertController(title: "Shortcut Pressed", message: "You pressed the shortcut key", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok!", style: .destructive, handler: nil))
present(alert, animated: true, completion: nil)
}