Реалізація інтерфейсу з висувною панеллю в iOS додатку

У сьогоднішній статті ми розповімо про прийоми та інструменти, якими користувалися при роботі над інтерфейсом з висувною панеллю, або, кажучи простіше, «шторкою». Шторки — оптимальне рішення для додатків, в яких користувач взаємодіє переважно з головним екраном, але періодично має потребу у швидкому доступі до додаткової інформації.



У нашому випадку інтерфейс розроблявся під утиліту Vehicle Location Tracker, у якій в якості основного екрану виступає карта з позначеними локаціями транспортних засобів, а висувне меню надає користувачеві можливість працювати з конкретною локацією. Далі весь процес буде розглядатися на прикладі цього додатка.

Основне призначення шторки у Vehicle Location Tracker — відображати інформацію про обрану парковці. В залежності від нагальних потреб користувача вона може бути схована, може відображатися на дисплеї у вигляді верхньої панелі (звичайного або розширеного виду) або ж висуватися повністю, показуючи весь набір інструментів редагування.

Виглядає це приблизно так:



Почали ми з того, що нарізали в'юшки за типами і розкидали їх по окремих XIB. Далі елементи просто збиралися як конструктор. Завдання полегшувало те, що можливості зміни порядку положення вьюшек не малося на увазі, тільки зміна деяких відстаней.

Взаємодія будувалося між основним контролером і його контейнером, в якому перебувала шторка. Основний контролер мав посилання на контролер контейнера. Протилежне ж взаємодія здійснювалося через делегат.

Логіка роботи шторки така. Є чотири стану основного вікна:

— додавання нової паркування;
— редагування існуючої;
— звичайне робоче стан з виділеної паркуванням;
— стан, коли нічого не обрано.

enum MapState : Int {
case New
case Edit
case Normal
case Empty
}

У самій же шторки набагато більш різноманітний комплект станів — загалом їх сім. Поділ парковок на тільки що додані і редаговані для шторки не проводиться, але зате режимів Normal і Edit, крім базових версій, з'являються ще і розширені. Крім того, додається стан «в русі» з параметром «останнє фіксоване положення»:


indirect enum MenuState {
case Empty
case Hide(previous: MenuState)
case Normal
case Advanced
case EditNormal
case EditAdvanced
case Motion(previous: MenuState)
}

Передача стану від MapState в MenuState виглядає наступним чином:

class MapViewController: UIViewController {
var currentState = StageMap.Zero {
willSet {
slideMenuVC.currentParentState = newValue
}
}
}

class SlideMenuViewController: UIViewController {
var currentParentState = StageMap.Zero {
willSet {
switch newValue {

case .Empty:
updateVisibleOfViews(toState: .Empty)
animateMove(toState: .Empty)

case .Normal:
updateVisibleOfViews(toState: .Normal)
animateMove(toState: .Normal)

case .New:
updateVisibleOfViews(toState: .EditNormal)
updateHeightOfTagsView()
animateMove(toState: .EditNormal)

case .Edit:
updateVisibleOfViews(toState: .EditNormal)
updateHeightOfTagsView()
animateMove(toState: .EditNormal)
}
}
}
}

Сам рух шторки здійснювалося за рахунок UIView.animateWithDuration і CGAffineTransformMakeTranslation.

Відзначимо, що зміна стану MenuState ніяк не впливає на MapState (і на тому спасибі). Є автоматичне перемикання станів щодо зміни MenuState в основному контролері, а є внутрішні зміни шторки (через UITapGestureRecognizer або UIPanGestureRecognizer). При цьому те, що відбувається зовні, за замовчуванням має більший пріоритет.

Тепер трохи про роботу внутрішніх змін шторки. Додаємо рекогнайзеры:

func addGesturesToView() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(SlideMenuViewController.tapGestureHandler(_:)))
actionView.addGestureRecognizer(tapGesture)

let panGesture = UIPanGestureRecognizer(target: self, action: #selector(SlideMenuViewController.panGestureHandler(_:)))
actionView.addGestureRecognizer(panGesture)
}

і їх реалізацію:

func tapGestureHandler(recognizer:UITapGestureRecognizer) {

var state = MenuState.Hide
var canMove = true

switch currentState {
case .Hide(previous: .Normal),
.Hide(previous: .UpNormalAdvanced):
toState = .Normal

case .Hide(previous: .EditNormal),
.Hide(previous: .EditAdvanced):
toState = .EditNormal

case .Normal:
toState = .Hide(previous: .Normal)

case .Advanced:
toState = .Normal

case .EditAdvanced:
toState = .EditNormal

case .EditNormal:
toState = .EditAdvanced

case .Motion:
canMove = false

default: break
}

updateVisibleOfViews(toState: state)
if canMove {
animateMove(toState: state)
}
}


func panGestureHandler(recognizer:UIPanGestureRecognizer) {

switch recognizer.state {
case .Began:
currentState = .Moved(previous: currentState)

case .Changed:
//рухаємо шторку за пальцем

case .Ended, .Cancelled:
//перевіряємо який стан був до цього .Moved(previous: lastState)
//і порівнюємо початкові і кінцеві координати, щоб знати куди дотягнути шторку після відриву пальця

}
}

Наступний пункт програми — розрахунок розмірів і станів. Панель тегів і панель стандартного стану мають плаваючу висоту, тому для коректного розрахунку розмірів елемента в'юшки за запитом віддавали потрібне значення, обчислене з урахуванням кількості та обсягу контенту.

Приклад розрахунку висоти для контенту collectionView:

func getContentHeight() -> CGFloat {
let amountOfItems = tagCollectionView.numberOfItemsInSection(0)

guard amountOfItems > 0 else {
return kDefaultCollectionHeight
}

let indexPath = NSIndexPath(forItem: amountOfItems - 1, inSection: 0)

guard let attributes = tagCollectionView.collectionViewLayout.layoutAttributesForItemAtIndexPath(indexPath) else {
return kDefaultCollectionHeight
}

let collectionViewContentHeight = attributes.frame.origin.y + attributes.frame.size.height

return collectionViewContentHeight
}

Не всі розміри необхідно прописувати вручну, подекуди ми автоматизували процес за допомогою софта.

У роботі над Vehicle Location Tracker нам дуже знадобився Sketchode — інструмент, про який ми дізналися тут же, на Хабре. Для тих хто не читав: мова йде про програму, яка дозволяє розробнику вивчати і «розбирати» макет з Sketch для власних потреб, при цьому не вносячи в нього жодних змін. І вовки ситі, і дизайнер спокійний.

Sketchode виявився нам корисний у двох відносинах. По-перше, він точно і наочно показує відстані між будь-якими елементами.



По-друге, дуже зручно реалізований експорт елементів. Іконки транспортних засобів, які повинні були встати в центрі кинувся, у нас всі виявилися різними за розмірами, хоча використовувалися в одній і тій же в'юшки. Можливість задавати розміри безпосередньо при експорті відчутно заощадила нам час на вирівнюванні.

У плані інтерфейсу виграшним є те, що можна відкрити декілька вікон з артбордами і спокійно між ними перемикатися, як видно на скріні, — не потрібно кидатися по всьому списку артбордов в пошуках того, з яким тільки що взаємодіяв.



Основні етапи роботи з висувною панеллю ми розглянули. Наостанок ще кілька дрібниць, які можуть знадобитися при роботі. Паралельно розробляючи і на obj-c і на swift, ми іноді щиро розчулюємося, які зручні штуки можна робити на swift. Ось, наприклад:

indirect enum MapButtonStage {
case Disable(previous: MapButtonStage)
case Off
case On
}

— і всі можливі стани кнопки описані, причому enum сам запам'ятає, в якому моді була кнопка, якщо ми її примусово заблокуємо:

enum PinColor : Int {
case Red
case Violet
case Green
case Blue
case Black
case Yellow

func getColor() -> UIColor {
switch self {
case .Violet:
return UIColor.colorFromHexString("#8E44AD")
case .Red:
return UIColor.colorFromHexString("#FF3824")
case .Green:
return UIColor.colorFromHexString("#16A085")
case .Blue:
return UIColor.colorFromHexString("#0076FF")
case .Black:
return UIColor.colorFromHexString("#44464E")
case .Yellow:
return UIColor.colorFromHexString("#F5A623")
}
}

var descriptionImage: String {
switch self {
case .Violet:
return "_purple"
case .Red:
return "_red"
case .Green:
return "_green"
case .Blue:
return "_blue"
case .Black:
return "_grey"
case .Yellow:
return "_yellow"
}
}
}



А тут взагалі краса: ми в один enum помістили та асоційований UIColor, і шматок імені для завантаження потрібних картинок з ассетов. Можна, звичайно, зберігати всі ці імена в одному місці, але тоді додавати нові буде незручно і негарно.

Щоб не виникало проблем з компонуванням імен, робимо структуру:

struct ImageName {
var color: PinColor
var category: PinCategory

func imageName() -> String {
return category.descriptionImage + color.descriptionImage;
}
}

і викликаємо її:

let name = ImageName(pinColor: color, pinCategory: category).imageName()

Готово!

Ось які навички ми отримали для себе під час першого досвіду створення шторки. Сподіваємося, наші спостереження будуть корисні іншим розробникам. Спасибі за увагу!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.