IOS – Как реализовать настраиваемую панель вкладок с прозрачным наложением в SwiftUI?

Я пытаюсь создать пользовательскую панель вкладок в 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 будут очень признательны!

Я создал свою собственную панель вкладок с помощью UIKit, но на этот раз я хочу создать ее в SwiftUI. Кроме того, я новичок в SwiftUI, поэтому разместил здесь

Vignesh 07.08.2024 09:03

Да, я открыт для создания нового пользовательского поведения SwiftUI. Если у вас есть какие-либо ссылки, поделитесь ими.

Vignesh 07.08.2024 09:04
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вот отправная точка, из которой вы можете добавить свой собственный стиль/макет и т. д. Здесь просто используется 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 07.08.2024 10:44

когда я выбираю любой элемент вкладки, он должен закрыть еще один лист, если он уже открыт. подскажите пожалуйста, где здесь нужно внести изменения?

Vignesh 08.08.2024 12:59

да, это работает. когда лист отображается, его не следует размещать на панели навигации, не могли бы вы сказать мне, как это показать?

Vignesh 08.08.2024 14:51

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

Vignesh 08.08.2024 14:52

@Vignesh Я не могу это воспроизвести. Color.black.opacity(0.7) закрывает панель навигации, как и ожидалось, из-за .ignoresSafeArea(edges: .top). И, честно говоря, ты меня очень раздражаешь. Пожалуйста, приложите усилия и потратьте некоторое время, чтобы прочитать и понять, что делает код. Пожалуйста, задайте новый вопрос с минимально воспроизводимым примером, если вы все еще застряли.

Sweeper 08.08.2024 14:59

Я уверен, что сделаю все остальное. Спасибо.

Vignesh 08.08.2024 15:05

И, пожалуйста, предложите несколько базовых пакетов, необходимых для создания приложения SwiftUI, или всего, что вы обычно используете при разработке SwiftUI. Это не имеет отношения к вопросу; Я просто прошу предложений.

Vignesh 08.08.2024 15:08

Я не понимаю, почему вы удалили комментарии.

Vignesh 08.08.2024 15:18

@Vignesh Я удалил их, потому что они больше не нужны. На самом деле, вам тоже следует удалить свои комментарии. См. раздел «Когда следует удалять комментарии?» раздел в Meta.stackexchange.com/a/19757/302707

Sweeper 08.08.2024 15:22

окей, теперь я это понимаю

Vignesh 08.08.2024 15:25

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

Vignesh 08.08.2024 15:26

Подметальная машина: stackoverflow.com/questions/78848737/…

Vignesh 08.08.2024 15:33

Другие вопросы по теме