Я пытаюсь создать пользовательскую панель вкладок в SwiftUI, аналогичную той, что есть в приложении Microsoft Teams для iOS. В частности, мне нужен следующий функционал:
При нажатии элемента вкладки «Дополнительно» должно открыться прозрачное наложение, отображающее дополнительные параметры. При выборе любого элемента в этом наложении он должен стать корневым представлением для вкладки «Дополнительно». Я приложил скриншот для справки.
struct ContentView: View {
@State private var selectedTab = 0
@State private var showMoreOptions = false
var body: some View {
VStack {
TabView(selection: $selectedTab) {
Text("Teams").tabItem { Label("Teams", systemImage: "person.3") }.tag(0)
Text("Chat").tabItem { Label("Chat", systemImage: "message") }.tag(1)
Text("Calendar").tabItem { Label("Calendar", systemImage: "calendar") }.tag(2)
Text("Calls").tabItem { Label("Calls", systemImage: "phone") }.tag(3)
Text("More").tabItem { Label("More", systemImage: "ellipsis") }
.tag(4)
.onTapGesture {
showMoreOptions.toggle()
}
}
if showMoreOptions {
TransparentOverlayView()
}
}
}
}
Я застрял здесь, чтобы реализовать ту же логику, что и в приложении Teams. пример: когда я нажимаю «Обновления» на нижнем листе, он должен установить rootview на вкладке «Дополнительно».
Любая помощь или предложения по реализации этого в SwiftUI будут очень признательны!
Да, я открыт для создания нового пользовательского поведения SwiftUI. Если у вас есть какие-либо ссылки, поделитесь ими.





Вот отправная точка, из которой вы можете добавить свой собственный стиль/макет и т. д. Здесь просто используется HStack для нижней панели вкладок и отображаются «дополнительные» вкладки в Grid с 4 вкладками в каждой строке.
struct CustomTabView<Content: View, Selection: Hashable>: View {
@Binding var selectedTab: Selection
@ViewBuilder let content: () -> Content
// The maximum number of tabs that can be shown at the bottom (including "More")
let maxTabsShown = 5
@State private var moreShown = false
var body: some View {
ZStack(alignment: .bottom) {
ExtractMulti(content) { views in
// The current tab
ForEach(views) { view in
// Instead of this 'if', use .opacity(view.id(as: Selection.self) == selectedTab ? 1 : 0)
// to control visibility if you want to preserve the state in each tab (e.g. scroll offset)
if view.id(as: Selection.self) == selectedTab {
view
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// The sheet for selecting the extra tabs
if moreShown {
Color.black.opacity(0.7)
.ignoresSafeArea(edges: .top)
.onTapGesture {
moreShown = false
}
.transition(.opacity)
// Here I've laid the extra tabs in a grid
Grid(horizontalSpacing: 30, verticalSpacing: 30) {
let hiddenTabs = views.dropFirst(maxTabsShown - 1)
let viewRows = hiddenTabs.chunks(ofCount: 4)
ForEach(viewRows.indices, id: \.self) { i in
let row = viewRows[i]
GridRow {
ForEach(row) { view in
Button {
if let selection = view.id(as: Selection.self) {
selectedTab = selection
moreShown = false
}
} label: {
if let label = view[CustomTabItemTrait.self] {
label
} else {
Text("Unnamed")
}
}
}
}
}
}
.padding()
.frame(maxWidth: .infinity)
.background(.background, in: UnevenRoundedRectangle(topLeadingRadius: 10, topTrailingRadius: 10))
.transition(.move(edge: .bottom).combined(with: .opacity))
}
Divider()
}
}
// The bottom tab bar
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
ExtractMulti(content) { views in
let shownTabs =
views.count <= maxTabsShown ?
views.prefix(maxTabsShown) :
views.prefix(maxTabsShown - 1)
ForEach(shownTabs) { view in
Group {
if let label = view[CustomTabItemTrait.self] {
label
} else {
Text("Unnamed")
}
}
.onTapGesture {
if let selection = view.id(as: Selection.self) {
selectedTab = selection
moreShown = false
}
}
.foregroundStyle(
view.id(as: Selection.self) == selectedTab ?
AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.opacity(1))
)
Spacer()
}
if views.count > maxTabsShown {
Label("More", systemImage: "ellipsis")
.onTapGesture {
moreShown.toggle()
}
.foregroundStyle(
shownTabs.contains(where: { $0.id(as: Selection.self) == selectedTab }) ?
AnyShapeStyle(.opacity(1)) : AnyShapeStyle(Color.accentColor)
)
Spacer()
}
}
}
}
.animation(.default, value: moreShown)
}
}
extension View {
func customTabItem<Content: View>(@ViewBuilder content: () -> Content) -> some View {
_trait(CustomTabItemTrait.self, AnyView(content()))
}
}
struct CustomTabItemTrait: _ViewTraitKey {
static let defaultValue: AnyView? = nil
}
Это зависит от ExtractMulti из View Extractor. Это несложно реализовать самостоятельно, если вы не хотите добавлять зависимость:
// From View Extractor - https://github.com/GeorgeElsham/ViewExtractor
public struct ExtractMulti<Content: View, ViewsContent: View>: View {
let content: () -> Content
let views: (Views) -> ViewsContent
public init(_ content: Content, @ViewBuilder views: @escaping (Views) -> ViewsContent) {
self.content = { content }
self.views = views
}
public init(@ViewBuilder _ content: @escaping () -> Content, @ViewBuilder views: @escaping (Views) -> ViewsContent) {
self.content = content
self.views = views
}
public var body: some View {
_VariadicView.Tree(
MultiViewRoot(views: views),
content: content
)
}
}
fileprivate struct MultiViewRoot<Content: View>: _VariadicView_MultiViewRoot {
let views: (Views) -> Content
func body(children: Views) -> some View {
views(children)
}
}
public typealias Views = _VariadicView.Children
Я также использовал chunks(ofCount:) из Swift Algorithms . Опять же, это достаточно легко реализовать самостоятельно, если вы не хотите добавлять зависимость.
При использовании этого не забудьте использовать customTabItem вместо встроенного tabItem и id вместо tag для пометки каждой вкладки.
Пример использования:
@State var selectedTab = 0
var body: some View {
CustomTabView(selectedTab: $selectedTab) {
ForEach(0..<10) { i in
Text("Tab \(i)")
.customTabItem {
Label("\(i)", systemImage: "0\(i).circle")
}
.id(i)
}
}
}
Давайте продолжим обсуждение в чате.
когда я выбираю любой элемент вкладки, он должен закрыть еще один лист, если он уже открыт. подскажите пожалуйста, где здесь нужно внести изменения?
да, это работает. когда лист отображается, его не следует размещать на панели навигации, не могли бы вы сказать мне, как это показать?
в моем случае лист должен находиться поверх панели навигации. когда я добавляю панель навигации в этот код, она просто накладывается только внутри представления. подскажите пожалуйста, как это показать?
@Vignesh Я не могу это воспроизвести. Color.black.opacity(0.7) закрывает панель навигации, как и ожидалось, из-за .ignoresSafeArea(edges: .top). И, честно говоря, ты меня очень раздражаешь. Пожалуйста, приложите усилия и потратьте некоторое время, чтобы прочитать и понять, что делает код. Пожалуйста, задайте новый вопрос с минимально воспроизводимым примером, если вы все еще застряли.
Я уверен, что сделаю все остальное. Спасибо.
И, пожалуйста, предложите несколько базовых пакетов, необходимых для создания приложения SwiftUI, или всего, что вы обычно используете при разработке SwiftUI. Это не имеет отношения к вопросу; Я просто прошу предложений.
Я не понимаю, почему вы удалили комментарии.
@Vignesh Я удалил их, потому что они больше не нужны. На самом деле, вам тоже следует удалить свои комментарии. См. раздел «Когда следует удалять комментарии?» раздел в Meta.stackexchange.com/a/19757/302707
окей, теперь я это понимаю
Если у вас есть базовые необходимые библиотеки для изучения аспектов, поделитесь, пожалуйста, лучше я задам вопрос и отмечу вас, чтобы вы могли ответить.
Подметальная машина: stackoverflow.com/questions/78848737/…
Я создал свою собственную панель вкладок с помощью UIKit, но на этот раз я хочу создать ее в SwiftUI. Кроме того, я новичок в SwiftUI, поэтому разместил здесь