Popover Controller на iPhone – пример создания из кода. ч.2
В первой части данного урока мы создали проект XCode, добавили файлы и классы для наших popover контроллеров, разместили контроллеры на Storyboard, добавили элементы интерфейса, с которыми будем работать и создали outlet’ы к ним. Т.е. сделали всю подготовительную работу — первые два пункта нашего плана.
В этой части урока мы будем только писать код.
Вернее, сначала мы все-таки «нарисуем» вызов popoverController в storyboard, а затем напишем все что нам надо…
Перейдите в Interface Builder — т.е. кликните на файл Main.Storyboard.
В нем сделайте ctrl-перетягивание от кнопки «Зеленый» до PopVC контроллера. В открывшемся меню нужно выбрать пункт «Present As Popover»
Появится Segue со значком popver. Далее нужно выбрать этот Segue и в инспекторе атрибутов задать ему идентификатор. Я установил «ppcs». Нужно запомнить этот идентификатор, потому что мы будем использовать его для определения перехода и презентации popover контроллера.
В класс ViewController сразу после метода didReciveMemoryWarning() добавьте следующий код:
// // proSwift.ru // override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // 1 if segue.identifier == "ppcs" { // 2 if let testPPVC = segue.destinationViewController as? PopVC{ // 3 if let ppc = testPPVC.popoverPresentationController { ppc.delegate = self } } } }
При совершении перехода (Segue) происходит вызов метода prepareForSegue. Наша задача обработать именно наш переход. Для этого мы и будем использовать идентификатор, который вводили ранее.
- Проверяем идентификатор перехода и если он совпадает с нашим — выполняем дальнейший код.
- Получаем контроллер в который происходит переход с помощью свойства destinationViewController и пытаемся привести его к классу PopVC. Если нам это удается, а нам это удается, потому что нет другого перехода с искомым в п.1 идентификатором, то выполняем пункт 3
- Проверяем может ли контроллер, в который мы переходим быть представленный как popover и если может, то устанавливаем в качестве делегата self т.е. ViewController (наш корневой контроллер с серым фоном)
В этот момент компилятор заругается на класс ViewController, т.к. пока мы его не объявили соответствующим протоколу делегата UIPopoverPresentationControllerDelegate. Самое время это сделать.
Swift позволяет это сделать в качестве расширения (extension) для класса, и рекомендую делать это именно так для читабельности кода и сохранения его структуры.
В файл ViewController.swift ПОСЛЕ (НЕ ВНУТРИ) реализации класса нужно добавить следующий код:
// // proSwift.ru // extension ViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle { return .None } }
Тут мы сообщили компилятору, что класс ViewController соответствует протоколу UIPopoverPresentationControllerDelegate и реализовали метод этого протокола adaptivePresentationStyleForPresentationController:
В данный момент можно запустить проект и посмотреть что получилось.
Наш контроллер заработал и показал зеленый popover. Пока не обращайте внимание на размеры контроллера и на точку, на которую указывает стрелка — в данном случае она указывает на точку с координатами (0,0) в системе координат view кнопки «Зеленый».
Давайте сделаем второй вариант вызова контроллера, используя код.
И для начала нужно задать идентификатор для голубого контроллера, чтобы получить ссылку на него из Storyboard.
Я задал идентификатор «FromVTppc»
Далее нам потребуется обработка нажатия на наше blueView. Для этого будем использовать распознавание жестов.
Измените метод viewDidLoad класса ViewController, чтобы он выглядел следующим образом:
// // proSwift.ru // override func viewDidLoad() { super.viewDidLoad() let tap = UITapGestureRecognizer(target: self, action: "tapHandler:") blueView.addGestureRecognizer(tap) }
Мы создали экземпляр класса UITapGestureRecognizer, который у нашего класса ViewController (self) будет вызывать метод tapHandler:, в момент когда жест буде распознан. Как вы понимаете, жест этот — это тап. Затем мы добавляем экземпляр этого класса на наше blueView.
Теперь нужно реализовать метод tapHandler:. Можно просто добавить этот метод в класс ViewController, но я опять рекомендую сделать расширение класса.
После расширения соответствия протоколу добавьте следующее расширение, в котором будет описан метод tapHandler:
// // proSwift.ru // extension ViewController { func tapHandler(tap: UITapGestureRecognizer) { // 1 let storyboard = UIStoryboard(name: "Main", bundle: nil) // 2 let fvtppc = storyboard.instantiateViewControllerWithIdentifier("FromVTppc") as! FromViewTapPPC // 3 fvtppc.modalPresentationStyle = .Popover // 4 if let ppc = fvtppc.popoverPresentationController { // 5 ppc.delegate = self // 6 ppc.permittedArrowDirections = .Any // 7 ppc.sourceView = tap.view } // 8 presentViewController(fvtppc, animated: true, completion: nil) } }
В нашем случае голубой контроллер создан в Storyboard и нам нужно показать его как popover.
- Получаем ссылку на наш Storyboard
- Получаем контроллер из storyboard с идентификатором «FromVTppc» и приводим его к классу FromViewTapPPC
- Полученному контроллеру задаем стиль модальной презентации popover
- Проверяем может ли полученный контроллер быть представленный как popover. и если может, то …
- Устанавливаем в качестве делегата self т.е ViewController
- Задаем разрешение показывать стрелки popover в любом направлении
- И обязательно указываем источник, на который эта стрелка будет показывать.
- Выводим на экран наш контроллер.
Класс ViewController уже соответствует протоколу UIPopoverPresentationControllerDelegate, поэтому вопросов у компилятора не возникнет.
Запустите проект и понажимайте на синее view
Оба контроллера работают, но стрелка голубого указывает немного неверно. Скоро мы и это поправим. Но чуть позже…
А сейчас давайте попробуем передать сообщение из нашего ViewController в FromViewTapPPC. Сделать это очень просто — все подготовительные работы мы проделали в первой части урока. Теперь надо установить нужное нам сообщение в свойство «name» экземпляра класса FromViewTapPPC. Добавьте следующую строку прямо перед presentViewController(fvtppc, animated: true, completion: nil)
fvtppc.name = "Сообщение переданное из серого"
Таким образом можно послать сообщения из серого контроллера в голубой, — не только текст но и любые данные.
Запустите проект и нажмите на синее view.
Все хорошо, вот только размер открывающихся контроллеров немного великоват. Но и это можно легко исправить.
Для начала исправим зеленый: в нем статическое сообщение и значит размер ему можно задать вполне определенный. Для этого в реализацию класса PopVC нужно добавить свойство preferredContentSize. Измените реализацию этого класса на следующую:
// // proSwift.ru // import UIKit class PopVC: UIViewController { override var preferredContentSize: CGSize { get { return CGSize(width: 360, height: 180) } set { super.preferredContentSize = newValue } } }
В данном случае мы задали вполне конкретный размер для зеленого popover. 360х180
Для синего контроллера ситуация немного другая — нужно определить размер содержимого в контроллере и исходя из этого размера вычислить размер самого контроллера. Замените реализацию класса на следующую:
// // proSwift.ru // import UIKit class FromViewTapPPC: UIViewController { @IBOutlet weak var label: UILabel! var name = "Программно сделаный popover" { didSet { label?.text = name } } override func viewDidLoad() { super.viewDidLoad() label.text = name } override var preferredContentSize: CGSize { get { if label != nil && presentingViewController != nil { let size = label.sizeThatFits(presentingViewController!.view.bounds.size) let size20 = CGSize.init(width: size.width + 30, height: size.height + 60) return size20 } else { return super.preferredContentSize } } set { super.preferredContentSize = newValue } } }
Думаю, по коду все понятно. В size мы получили размер, в который вписывается label и для эстетичности добавили 30 по ширине и 60 по высоте. Почему именно эти цифры? Они были получены методом подбора — я добавлял в размер различные значения и смотрел как будет красивее.
Запустите проект теперь.
Последнее, с чем нам осталось разобраться это с местом, в которое указывает стрелочка popover контроллера. Как было сказано выше стрелочка зеленого контроллера указывает на точку с координатами (0.0) того view, которое его вызвало. Мы нарисовали вызов от кнопки «Зеленый» значит стрелка будет указывать в левый верхний угол этой кнопки.
Стрелка голубого указывает приблизительно туда же, и это в корне не правильно. Давайте поменяем стрелку для голубого контроллера. сразу после строки ppc.sourceView = tap.view нужно добавить вот эту строку:
// // proSwift.ru // ppc.sourceRect = CGRect(x: (tap.view?.bounds.size.width)! / 2, y: (tap.view?.bounds.size.height)! / 2, width: 1, height: 1)
В ней мы просим наш popover показывать стрелку на прямоугольник 1х1 в центре синего view. Можем запустить проект и посмотреть изменения:
Можно было бы и так оставить проект, но можно сделать его еще более совершенным. Я имею ввиду, что мы можем указывать стрелку в то место синего view, куда тапнул пользователь. У нас для этого все есть, не нужно получать никаких дополнительных данных. Т.к. для обработки нажатия используется распознаватель жестов UITapGestureRecognizer координаты нажатия содержатся в нем. Нам нужно толко взять их и передать в свойство sourceRect.
Закоментируйте строку с sourceRect и вместо нее напишите вот этот код:
// // proSwift.ru // let location = tap.locationInView(blueView) ppc.sourceRect = CGRect(x: location.x, y: location.y, width: 1, height: 1)
После компиляции проекта голубой контроллер работает очень хорошо.
На этом все, уважаемые читатели.
Ссылка на gitHub: https://github.com/Ironrnd/popoverVC
Также оставлю исходный код файлов проекта ниже по тексту.
ViewController.swift:
// // proSwift.ru // import UIKit class ViewController: UIViewController { @IBOutlet weak var blueView: UIView! override func viewDidLoad() { super.viewDidLoad() let tap = UITapGestureRecognizer(target: self, action: "tapHandler:") blueView.addGestureRecognizer(tap) // Do any additional setup after loading the view, typically from a nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == "ppcs" { if let testPPVC = segue.destinationViewController as? PopVC{ if let ppc = testPPVC.popoverPresentationController { ppc.delegate = self } } } } } extension ViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle { return .None } } extension ViewController { func tapHandler(tap: UITapGestureRecognizer) { let storyboard = UIStoryboard(name: "Main", bundle: nil) let fvtppc = storyboard.instantiateViewControllerWithIdentifier("FromVTppc") as! FromViewTapPPC fvtppc.modalPresentationStyle = .Popover if let ppc = fvtppc.popoverPresentationController { ppc.delegate = self ppc.permittedArrowDirections = .Any ppc.sourceView = tap.view //ppc.sourceRect = CGRect(x: (tap.view?.bounds.size.width)! / 2, y: (tap.view?.bounds.size.height)! / 2, width: 1, height: 1) let location = tap.locationInView(blueView) ppc.sourceRect = CGRect(x: location.x, y: location.y, width: 1, height: 1) } fvtppc.name = "Сообщение переданное из серого" presentViewController(fvtppc, animated: true, completion: nil) } }
FromViewTapPPC.swift
import UIKit class FromViewTapPPC: UIViewController { @IBOutlet weak var label: UILabel! var name = "Программно сделаный popover" { didSet { label?.text = name } } override func viewDidLoad() { super.viewDidLoad() label.text = name } override var preferredContentSize: CGSize { get { if label != nil && presentingViewController != nil { let size = label.sizeThatFits(presentingViewController!.view.bounds.size) let size20 = CGSize.init(width: size.width + 30, height: size.height + 60) return size20 } else { return super.preferredContentSize } } set { super.preferredContentSize = newValue } } }
PopVC.swift
import UIKit class PopVC: UIViewController { override var preferredContentSize: CGSize { get { return CGSize(width: 360, height: 180) } set { super.preferredContentSize = newValue } } }
Спасибо автору. Хороший материал для новичков.
Но вот название переменных и идентификаторов… меня если често начинает трясти, когда я вижу подобные названия в виде аббревиатур (fvtppc, size20 — почему 20), ppc)
Спасибо, Юрий!
А по поводу имен — тут даже немного смешно вышло…
fvtppc — это легко — при написании я имел ввиду From View Tap Popover Controller. А вот про size20 — пришлось лезть в код и вспоминать. И не получилось с ходу вспомнить! Но когда я залез в предыдущие версии проекта, то сразу вспомнил, что у меня там был CGRect 20х20 который в более поздних версиях поменял свой размер.
Сокращенные названия я использовал, чтобы код помещался в том числе и на странице данного сайта.
В любом случае, я понимаю, что это не верный, с точки зрения тру-программирования, подход. Постараюсь избежать данного подхода в дальнейшем.
И вот еще проблема — я так и не смог победить Refractor в XCode со всеми его функциями, в.т.ч. изменение имен переменных. XCode мне говорит, что он может рефракторить только код Objective-C, а Swift ему не подвластен. Может кто сталкивался и решил проблему — подскажите.
Я пониманию что статья не первой свежести. И все меняется. Но У меня не заработал ваш пример. Можете помочь разобраться ?
Ошибка: No method declared with Objective-C selector ‘tapHandler:’
Хотя функцию я описал как extention
И второй момент это Method does not override any method from its superclass
на воит эту функцию prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
помогите решить данные ошибки
prepareForSegue эту ошибку убрал удалением override.
выкачал с гитхаба, по сути там тоже что сделал я сам и ошибке те же при компиляции