Как изменить внешний вид кнопки в зависимости от состояния ее выбора

Я пытаюсь сделать кнопку такой.

То есть кнопка будет менять состояние отображения при нажатии, что чем-то похоже на TabBar. Когда нижняя часть активирована, появится градиентный фон, граница нижней части исчезнет, ​​а ниже отобразится соответствующий вид. Обе кнопки сделаны таким образом для переключения.

Я планировал реализовать это, разделив Кнопку и Вид, но столкнулся с трудностями.

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

Статус реализации следующий:

Эти две кнопки размещаются в одном Hstack. Сначала я устанавливал HStack(spacing: 0) {}, чтобы исключить предустановленное расстояние между кнопками, но кажется, что набор прямоугольников на заднем плане не считается объектом, поэтому я установил для интервала нужную мне ширину границы, то есть 3. Но я подозреваю, что это лучший метод. Кроме того, я ссылаюсь на https://www.devtechie.com/community/public/posts/151937-round-special-corners-in-swiftui, где описан метод настройки закругленных углов.

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

Код программы следующий:

HStack(spacing: 0) {
    Button(action: {
        
    }){
        Text("行程簡介")
            .font(.title3)
            .fontWeight(.bold)
            .foregroundColor(Color("DesignMiddleMutedOrange"))
            .multilineTextAlignment(.center)
            .padding(.horizontal, 34.0)
        
    }
    .DetailButtonisOnActive()
    
    
    Button(action: {
        
    }){
        Text("Q&A")
            .font(.title3)
            .fontWeight(.bold)
            .foregroundColor(Color("DesignMiddleMutedOrange"))
            .multilineTextAlignment(.center)
            .padding(.horizontal, 34.0)
    }
    .QAButtonisOffActive()
    
    Spacer()
}

Два стиля кнопок:

extension Button {
    func DetailButtonisOnActive() -> some View {
        self
            .frame(width: 150, height: 52, alignment: .leading)
            .background(
                LinearGradient(colors: [Color("#FFFAE6"), Color("DesignLightYellow")],
                               startPoint: .top,
                               endPoint: .bottom)
            )
            .roundedCorner(12, corners: .topRight)
            .background(
                Rectangle()
                .roundedCorner(12, corners: .topRight)
                .frame(width: 153, height: 55, alignment: .leading)
                .padding(.bottom, 3)
                .foregroundColor(Color("DesignLightMutedOrange"))
            )
    }
    func DetailButtonisOffActive() -> some View {
        self
            .frame(width: 150, height: 52, alignment: .leading)
            .background(
                Color("DesignLightYellow")
            )
            .roundedCorner(12, corners: .topRight)
            .border(.bottom, width: 3, color: Color("DesignLightMutedOrange"))
            .background(
                Rectangle()
                .roundedCorner(12, corners: .topRight)
                .frame(width: 153, height: 55, alignment: .leading)
                .padding(.bottom, 3)
                .foregroundColor(Color("DesignLightMutedOrange"))
            )
    }
    
    func QAButtonisOnActive() -> some View {
        self
            .frame(width: 150, height: 52, alignment: .leading)
            .background(
                LinearGradient(colors: [Color("#FFFAE6"), Color("DesignLightYellow")],
                               startPoint: .top,
                               endPoint: .bottom)
            )
            .roundedCorner(12, corners: [.topRight, .topLeft])
            //.padding(.leading, -1.5)
            .background(
                Rectangle()
                    .roundedCorner(12, corners: [.topRight, .topLeft])
                .frame(width: 156, height: 55, alignment: .leading)
                .padding(.bottom, 3)
                .foregroundColor(Color("DesignLightMutedOrange"))
            )
    }
    
    func QAButtonisOffActive() -> some View {
        self
            .frame(width: 150, height: 52, alignment: .leading)
            .background(
                Color("DesignLightYellow")
            )
            .roundedCorner(12, corners: [.topRight, .topLeft])
            .background(
                Rectangle()
                    .roundedCorner(12, corners: [.topRight, .topLeft])
                .frame(width: 156, height: 55, alignment: .leading)
                .padding(.bottom, 3)
                .foregroundColor(Color("DesignLightMutedOrange"))
            )
    }
}
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
111
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

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

Я бы посоветовал лучше показать границу, используя .stroke, чтобы вместо этого нарисовать линию вокруг фигуры.

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

Вот пример реализации ButtonStyle на основе предоставленного вами кода:

struct TabButton: ButtonStyle {
    let isOn: Bool
    let withLeftBorder: Bool
    let buttonWidth: CGFloat = 153
    let lineWidth: CGFloat = 2

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.title3)
            .fontWeight(.bold)
            .foregroundStyle(.designMiddleMutedOrange)
            .multilineTextAlignment(.center)
            .frame(width: buttonWidth, height: 55)
            .background {
                if isOn {
                    Rectangle()
                        .fill(
                            LinearGradient(
                                colors: [.FFFAE_6, .designLightYellow],
                                startPoint: .top,
                                endPoint: .bottom
                            )
                        )
                        .padding(.bottom, lineWidth)
                } else {
                    Color.designLightYellow
                }
            }
            .clipShape(
                UnevenRoundedRectangle(
                    cornerRadii: .init(
                        topLeading: withLeftBorder ? 12 : 0,
                        topTrailing: 12
                    )
                )
            )
            .overlay(alignment: .trailing) {
                UnevenRoundedRectangle(
                    cornerRadii: .init(
                        topLeading: withLeftBorder ? 12 : 0,
                        topTrailing: 12
                    )
                )
                .stroke(.designMiddleMutedOrange, lineWidth: lineWidth)
                .padding(.bottom, lineWidth / 2)
                .frame(width: buttonWidth + (withLeftBorder ? 0 : lineWidth / 2))
            }
            .overlay(alignment: .bottom) {
                if isOn {
                    Color.designLightYellow
                        .padding(.leading, withLeftBorder ? lineWidth / 2 : 0)
                        .padding(.trailing, lineWidth / 2)
                        .frame(height: lineWidth)
                }
            }
    }
}

Это работает следующим образом:

  • Текстовая метка отображается на переднем плане.
  • Фон заполняется соответствующим FillStyle, в зависимости от того, включена кнопка или нет.
  • Затем фон обрезается с помощью UnevenRoundedRectangle, закругляя только те углы, которые необходимо скруглить.
  • Другой UnevenRoundedRectangle используется для обводки границы в качестве наложения. Обратите внимание, что когда обводка выполняется таким образом, линия обводки наполовину внутрь и наполовину наружу (середина линии находится на краю кадра), поэтому для удержания ее внутри нижнего края используется отступ.
  • Если левая граница не должна быть видна, то форма обводки делается немного шире, чтобы левая граница была вытеснена за пределы экрана. Наложение использует alignment: .trailing, чтобы гарантировать, что правая граница не перемещается.
  • Если кнопка включена, нижний край границы маскируется наложением цвета фона.

Кстати, я думаю, что ваш цвет «#FFFAE6» назван неправильно, потому что я думаю, что на самом деле он больше похож на #fec890.

Итак, вот как можно использовать стиль кнопки для двух кнопок:

struct ContentView: View {
    @State private var selectedTab = 0

    var body: some View {
        VStack(spacing: 0) {
            HStack(spacing: 0) {
                Button("行程簡介") { selectedTab = 0 }
                    .buttonStyle(
                        TabButton(
                            isOn: selectedTab == 0,
                            withLeftBorder: false
                        )
                    )
                Button("Q&A") { selectedTab = 1 }
                    .buttonStyle(
                        TabButton(
                            isOn: selectedTab == 1,
                            withLeftBorder: true
                        )
                    )
                Spacer()
            }
            .background(alignment: .bottom) {
                Color.designMiddleMutedOrange
                    .frame(height: 2) // = lineWidth of button border
            }

            Color.designLightYellow
        }
        .frame(height: 300)
    }
}

Animation


РЕДАКТИРОВАТЬ В ответ на ваш комментарий UnevenRoundedRectangle был добавлен в iOS 16.0. Если он вам недоступен, альтернативным подходом будет использование вместо него пользовательского Shape.

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

struct TabButtonShape: Shape {
    let withRoundedTopLeftCorner: Bool
    let cornerRadius: CGFloat = 12
    func path(in rect: CGRect) -> Path {
        var path = Path()
        var x = rect.minX
        var y = rect.maxY
        path.move(to: CGPoint(x: x, y: y))
        y = rect.minY + (withRoundedTopLeftCorner ? cornerRadius : 0)
        path.addLine(to: CGPoint(x: x, y: y))
        if withRoundedTopLeftCorner {
            path.addArc(
                center: CGPoint(x: x + cornerRadius, y: y),
                radius: cornerRadius,
                startAngle: .degrees(180),
                endAngle: .degrees(270),
                clockwise: false
            )
            y = rect.minY
        }
        x = rect.maxX - cornerRadius
        path.addLine(to: CGPoint(x: x, y: y))
        path.addArc(
            center: CGPoint(x: x, y: y + cornerRadius),
            radius: cornerRadius,
            startAngle: .degrees(270),
            endAngle: .degrees(0),
            clockwise: false
        )
        x = rect.maxX
        y = rect.maxY
        path.addLine(to: CGPoint(x: x, y: y))
        path.closeSubpath()
        return path
    }
}

Это можно использовать для замены UnevenRoundedRectangle, который использовался ранее:

.clipShape(
    TabButtonShape(withRoundedTopLeftCorner: withLeftBorder)
//    UnevenRoundedRectangle(
//        cornerRadii: .init(
//            topLeading: withLeftBorder ? 12 : 0,
//            topTrailing: 12
//        )
//    )
)
.overlay(alignment: .trailing) {
    TabButtonShape(withRoundedTopLeftCorner: withLeftBorder)
//    UnevenRoundedRectangle(
//        cornerRadii: .init(
//            topLeading: withLeftBorder ? 12 : 0,
//            topTrailing: 12
//        )
//    )
    .stroke(.designMiddleMutedOrange, lineWidth: lineWidth)
    .padding(.bottom, lineWidth / 2)
    .frame(width: buttonWidth + (withLeftBorder ? 0 : lineWidth / 2))
}

Надеюсь, теперь это решение поможет вам.

Очень приятно получить ваш ответ. Я хотел бы ответить как можно скорее. Но я хочу дать вам больше отзывов, поэтому попробовал ваше решение. затем я заметил, что UnevenRoundedRectangle — это новая структура в Xcode15. Я пока не обновляюсь. Предложите мне обновить его? Или мне стоит попробовать другой способ?

Evan Lu 17.04.2024 07:34

@EvanLu Если UnevenRoundedRectangle вам недоступен, вместо этого вы можете использовать собственную форму - ответ обновлен

Benzy Neez 17.04.2024 10:04

Это очень любезно с. Код работает правильно. Спасибо.

Evan Lu 17.04.2024 13:22

@EvanLu Рад слышать, что у тебя все работает! Возможно, вы можете принять ответ (нажав галочку), если ответ решил ваш вопрос?

Benzy Neez 17.04.2024 13:26

Конечно. Теперь мне нужно двигаться дальше. Мне нужно переключить содержимое области TabBotton. Это тоже может пойти по тому же пути, верно? Это означает, что состояние также следует за selectedTab , но мне нужно разработать пользовательскую структуру двух представлений контента и позволить состоянию решать, показываться или нет при действии кнопки?

Evan Lu 17.04.2024 13:38

@EvanLu Да, верно. ZStack с if-else может быть хорошим способом переключения контента. Пс. спасибо, что приняли ответ!

Benzy Neez 17.04.2024 13:41

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