Я пытаюсь сделать кнопку такой.
То есть кнопка будет менять состояние отображения при нажатии, что чем-то похоже на 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"))
)
}
}





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

РЕДАКТИРОВАТЬ В ответ на ваш комментарий 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))
}
Надеюсь, теперь это решение поможет вам.
@EvanLu Если UnevenRoundedRectangle вам недоступен, вместо этого вы можете использовать собственную форму - ответ обновлен
Это очень любезно с. Код работает правильно. Спасибо.
@EvanLu Рад слышать, что у тебя все работает! Возможно, вы можете принять ответ (нажав галочку), если ответ решил ваш вопрос?
Конечно. Теперь мне нужно двигаться дальше. Мне нужно переключить содержимое области TabBotton. Это тоже может пойти по тому же пути, верно? Это означает, что состояние также следует за selectedTab , но мне нужно разработать пользовательскую структуру двух представлений контента и позволить состоянию решать, показываться или нет при действии кнопки?
@EvanLu Да, верно. ZStack с if-else может быть хорошим способом переключения контента. Пс. спасибо, что приняли ответ!
Очень приятно получить ваш ответ. Я хотел бы ответить как можно скорее. Но я хочу дать вам больше отзывов, поэтому попробовал ваше решение. затем я заметил, что UnevenRoundedRectangle — это новая структура в Xcode15. Я пока не обновляюсь. Предложите мне обновить его? Или мне стоит попробовать другой способ?