Реализация интерфейса с выдвижной панелью в 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()

    Готово!

    Вот какие навыки мы получили для себя в ходе первого опыта создания шторки. Надеемся, наши наблюдения будут полезны и другим разработчикам. Спасибо за внимание!
    Everyday Tools
    274,00
    Утилиты на все случаи жизни
    Поделиться публикацией

    Похожие публикации

    Комментарии 6

      0
      спасибо за статью.

      Если не трудно, расскажите пожалуйста как вы сделали такую анимацию
        0
        Спасибо за комментарий! Вы про какую анимацию хотели уточнить? Если про само движение шторки, то оно осуществлялось за счет UIView.animateWithDuration и CGAffineTransformMakeTranslation.
        0
        Всего бы ничего, но вы не подумали о том, что пользователи не смогут дотянуться до вашего экрана на 6-ке, в том числе plus
          0
          Тем не менее у 6ки есть такая же шторка: как-то ей люди пользуются, просто обычно двумя руками, ну либо исхитряются одной (если палец подвижный), те же нотификации еще менее удобно одной рукой мэнэджить, чем пользовать шторку.
            0
            Опираетесь на плохие интерфейсы в своих Юзер Кейсах? Ну как знаете…
              0

              Более того, в iOS нотификации появляются по свайпу вниз, а у вас как я понимаю именно оттягивание.Предвещаю батхерт всех пользователей

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое