Мне надоело видеть и использовать ту же старую навигацию по вкладкам в приложениях, поэтому я создал виджет меню, который немного смешивает навигацию, не делая ее слишком сложной или запутанной. В этом руководстве рассказывается, как я создал это с нуля и как вы можете легко интегрировать его в свое приложение для iOS. Хотя он встроен в SwiftUI, есть способы интегрировать его с представлениями UIKit (руководство появится позже). Если вам просто нужен готовый продукт, вот репозиторий GitHub.

Начинать с нуля

Давайте начнем с пустого проекта XCode приложения для одного просмотра.

После создания переименуйте ContentView.swift в ViewController.swift; в этом файле и в SceneDelegate.swift будет несколько экземпляров, в которых вам нужно будет изменить ContentView на ViewController. Вам не нужно переименовывать, я просто считаю, что это помогает упростить работу при работе над более крупными проектами.

Поскольку это начинается с нуля, нам нужно добавить несколько файлов SwiftUI, которые будут отображать разные представления. Я создал ViewA, ViewB и ViewC; у каждого есть простая строка текста с названием представления.

Вернувшись в ViewController, я удалил PreviewProvider, поскольку обычно использую симулятор для тестирования, и добавил следующее, в результате чего получился простой белый экран с «View A» в центре.

struct ViewController: View { 
 @State var page:String = "ViewA" 
  var body: some View { 
   VStack { 
    if page == "ViewA" { ViewA() } 
    if page == "ViewB" { ViewB() } 
    if page == "ViewC" { ViewC() } 
   } //end of page vstack 
  } //end of view 
} //end of struct

Переключение экранов

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

  • Установите screenSize и iconSize (установите iconSize относительно screenSize для масштабирования).
  • Ниже страницы VStack мы добавляем ZStack, который содержит VStack кнопок; Вложение VStack в ZStack кажется избыточным, но фрейм с выравниванием .bottomTrailing (необходим для размещения меню в правом нижнем углу, а не по умолчанию в центре экрана) не имеет такого же эффекта, если применяется только к кнопке VStack.
  • Сгруппируйте стопки страниц и кнопок в ZStack, чтобы кнопки располагались на странице.
struct ViewController: View {
 @State var page:String = "ViewA"
 let screenSize = UIScreen.main.bounds
 let iconSize = UIScreen.main.bounds.width*0.07
 
 var body: some View {
  ZStack {
   VStack {
    if page == "ViewA" { ViewA() }
    if page == "ViewB" { ViewB() }
    if page == "ViewC" { ViewC() }
   } //end of page vstack
   ZStack {
    VStack {
     Button(action: { self.page = "ViewA" }) 
     {
      Image(systemName: "a.circle.fill").resizable().frame(width: iconSize, height: iconSize)
     }
     Button(action: { self.page = "ViewB" }) 
     {
      Image(systemName: "b.circle.fill").resizable().frame(width: iconSize, height: iconSize)
     }
     Button(action: { self.page = "ViewC" }) 
     {
      Image(systemName: "c.circle.fill").resizable().frame(width: iconSize, height: iconSize)
     }
    }.padding([.all]) //end of button vstack
   }.frame(width: screenSize.width, height: screenSize.height, alignment: .bottomTrailing) //end of button zstack
  } //end of view zstack
 } //end of view
} //end of struct

Функционально! = Весело использовать

Круто, переключение экранов работает! Но есть еще несколько вещей, которые нужно исправить, прежде чем это действительно можно будет использовать.

  • Правильно расположите меню на экране
  • Обновите стиль кнопки
  • Добавьте «триггер», чтобы развернуть и свернуть меню, чтобы мы не видели постоянно все кнопки (будут мешать фоновому экрану)

Стиль кнопки

Мы можем убить первых двух зайцев одним выстрелом, создав стиль кнопки (в этом примере я придерживался чего-то невероятно простого, но он демонстрирует создание многоразового унифицированного стиля кнопки). Над структурой ViewController добавьте:

struct PageButtonStyle: ButtonStyle { 
 let buttonSize = UIScreen.main.bounds.width*0.12 
 func makeBody(configuration: Self.Configuration) -> some View { 
  return configuration.label 
   .foregroundColor(Color.white) 
   .frame(width: buttonSize, height: buttonSize) 
   .background(Color.green) 
   .scaleEffect(configuration.isPressed ? 0.9 : 1.0) 
 } 
}

Теперь добавьте стиль кнопки к каждой кнопке в VStack следующим образом:

Button(action: { self.page = "ViewA" }) 
{ 
 Image(systemName: "a.circle.fill").resizable().frame(width: iconSize, height: iconSize) }.buttonStyle(PageButtonStyle()).cornerRadius(15)

Развертывание и сворачивание

Как это будет работать? Давай подумаем.

Если мы находимся на экране, виджет меню должен быть одной кнопкой со значком, соответствующим текущему экрану. Когда мы нажимаем на нее, она должна расширяться и показывать все кнопки в меню, а кнопка, на которой был значок текущего экрана, должна измениться на своего рода стрелку, указывающую вверх, показывая, что меню развернуто. При нажатии кнопки в меню экран переключается. Меню должно сворачиваться только при нажатии кнопки со стрелкой, а кнопка со стрелкой должна вернуться к отображению значка текущего экрана.

Давайте реализовывать!

Под существующими переменными добавьте expand (указывает, следует ли раскрывать меню) и icon (указывает значок текущего экрана).

struct ViewController: View {
 @State var page:String = "ViewA"
 let screenSize = UIScreen.main.bounds
 let iconSize = UIScreen.main.bounds.width*0.07
 @State var expand:Bool = false
 @State var icon:String = "a.circle.fill"

Внутри кнопки VStack добавьте оператор if вокруг всех существующих кнопок; это отобразит все кнопки, если expand истинно, и только кнопку текущего экрана, если ложь. Под оператором if добавьте еще одну кнопку; это будет текущий экран / кнопка со стрелкой, которая должна отображаться всегда. Внутри каждого действия кнопки добавьте присвоение переменной icon, которое соответствует странице; это используется для кнопки текущего экрана.

VStack { 
 if expand { 
   Button(action: { 
   self.page = "ViewA" 
   self.icon = "a.circle.fill" 
  }) { 
   Image(systemName: "a.circle.fill").resizable().frame(width: iconSize, height: iconSize) 
  }.buttonStyle(PageButtonStyle()).cornerRadius(15) 
  Button(action: { 
   self.page = "ViewB" 
   self.icon = "b.circle.fill" 
  }) { 
   Image(systemName: "b.circle.fill").resizable().frame(width: iconSize, height: iconSize) 
  }.buttonStyle(PageButtonStyle()).cornerRadius(15) 
  Button(action: { 
   self.page = "ViewC" 
   self.icon = "c.circle.fill" 
  }) { 
   Image(systemName: "c.circle.fill").resizable().frame(width: iconSize, height: iconSize) 
  }.buttonStyle(PageButtonStyle()).cornerRadius(15) 
 } //end of if statement
  
  //Arrow/Current Screen 
  Button(action: { 
   self.expand.toggle() 
  }) { 
   Image(systemName: expand ? "chevron.up" : icon).resizable().frame(width: iconSize, height: expand ? iconSize/3 : iconSize) 
  }.buttonStyle(PageButtonStyle()).cornerRadius(15) 
}.padding([.all]) //end of button vstack

На кнопку «Стрелка / Текущий экран» мы добавили действие, которое переключает expand, указывая, следует ли развернуть / свернуть меню. Для значка мы показываем шевронную стрелку, если меню развернуто, и icon (независимо от того, что было установлено), если нет (с помощью удобного тернарного оператора). Кроме того, мы хотим, чтобы шевронная стрелка была пропорциональной, поэтому, если это значок кнопки (который будет, когда expand истинно), уменьшите высоту (выполняется с помощью тернарного оператора ... они лучше всего!).

Лучше, чем раньше

И здесь мы идем! Улучшенный дизайн кнопок, правильное расположение на экране и раскрывающееся / сворачивающееся меню. Почти готово.

Обычная Джейн получает улучшение

Выглядит неплохо, но все же отчасти блекнет. Было бы оооочень намного лучше с анимацией и жестами, не так ли?

Анимация

Это невероятно просто, вы даже не поверите.

Я хочу, чтобы шевронная стрелка сжималась и растягивалась при нажатии, а меню появлялось и исчезало при его расширении и сворачивании. Просто добавьте .animation(.spring()) к кнопке шеврона и кнопке VStack.

...
 //Chevron/Current Screen
 Button(action: {
  self.expand.toggle() 
 }) {
  Image(systemName: expand ? "chevron.up" : icon).resizable().frame(width: iconSize, height: expand ? iconSize/3 : iconSize)
 }.buttonStyle(PageButtonStyle()).cornerRadius(15).animation(.spring())
}.padding([.all]).animation(.spring()) //end of button vstack

Жесты - Перетаскивание меню

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

Итак, давайте реализуем жест перетаскивания. Под существующими переменными добавьте rightSide (указывает, находится ли меню справа или слева).

struct ViewController: View {
 @State var page:String = "ViewA"
 let screenSize = UIScreen.main.bounds
 let iconSize = UIScreen.main.bounds.width*0.07
 @State var expand:Bool = false
 @State var icon:String = "a.circle.fill"
 @State private var rightSide:Bool = true 

Под этими переменными и над телом представления добавьте жест menuDrag. Это переключит rightSide, если пользователь перетаскивает правую левую половину экрана и наоборот.

var menuDrag: some Gesture {
 DragGesture().onChanged { value in }.onEnded { value in
  if (self.rightSide && value.translation.width < -(self.screenSize.width*0.5)) || (!self.rightSide && value.translation.width > self.screenSize.width*0.5) {
   self.rightSide.toggle()
  }
 }
}

Мы просто добавили .animation(.spring()) к кнопке шеврона и кнопке VStack; давайте добавим к ним и .gesture(menuDrag). Мы также хотим добавить тернарный оператор к выравниванию кнопки ZStack.

...
  //Chevron/Current Screen
  Button(action: {
   self.expand.toggle() 
  }) {
   Image(systemName: expand ? "chevron.up" : icon).resizable().frame(width: iconSize, height: expand ? iconSize/3 : iconSize)
}.buttonStyle(PageButtonStyle()).cornerRadius(15).animation(.spring()).gesture(menuDrag)
 }.padding([.all]).animation(.spring()).gesture(menuDrag) //end of button vstack
}.frame(width: screenSize.width, height: screenSize.height, alignment: rightSide ? .bottomTrailing : .bottomLeading) //end of button zstack

Вуаля!

И это еще не все!

Теперь у нас есть настраиваемый виджет меню, который можно легко интегрировать в любой проект SwiftUI. Также невероятно легко поменять местами свои собственные цвета (даже градиенты!), Значки, экраны и т. Д. Вы даже можете изменить его так, чтобы он расширялся по горизонтали или закрывался, когда вы нажимаете любую из кнопок, а не только стрелку шеврона.

Ознакомьтесь с проектом на GitHub, чтобы узнать больше.