Pull to refresh
105.28
Productivity Inside
Для старательного нет ничего невозможного

Реализация интерфейса с выдвижной панелью в iOS приложении

Reading time 6 min
Views 14K
В сегодняшней статье мы расскажем о приемах и инструментах, которыми пользовались разработчики из Everyday Tools при работе над интерфейсом с выдвижной панелью, или, говоря проще, «шторкой». Шторки — оптимальное решение для приложений, в которых пользователь взаимодействует преимущественно с главным экраном, но периодически нуждается в быстром доступе к дополнительной информации.



В случае с нашими коллегами интерфейс разрабатывался под утилиту 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()

Готово!

Вот какие навыки получили наши коллеги в ходе первого опыта создания шторки. Надеемся, эти наблюдения будут полезны и другим разработчикам. Спасибо за внимание!
Tags:
Hubs:
+12
Comments 6
Comments Comments 6

Articles

Information

Website
productivityinside.com
Registered
Founded
Employees
101–200 employees
Location
Россия