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

в 6:41, , рубрики: ios development, iOS разработка, navigation, sketch, swift, UI, xcode, xcode7, Блог компании Everyday Tools, разработка под iOS

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

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


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

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

Выглядит это примерно так:

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

Начали мы с того, что нарезали вьюшки по типам и разбросали их по отдельным 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 оказался нам полезен в двух отношениях. Во-первых, он точно и наглядно показывает расстояния между любыми элементами.

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

Во-вторых, очень удобно реализован экспорт элементов. Иконки транспортных средств, которые должны были встать в центре канваса, у нас все оказались разными по размерам, хотя использовались в одной и той же вьюшке. Возможность задавать размеры непосредственно при экспорте ощутимо сэкономила нам время на выравнивании.

В плане интерфейса выигрышным является то, что можно открыть несколько окон с артбордами и спокойно между ними переключаться, как видно на скрине, — не нужно метаться по всему списку артбордов в поисках того, с которым только что взаимодействовал.

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

Основные этапы работы с выдвижной панелью мы рассмотрели. Напоследок еще пара мелочей, которые могут пригодиться при работе. Параллельно разрабатывая и на 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"
        }
    }
}

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

А тут вообще красота: мы в один 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js