问题复现
在学习Swift过程中,发现一个难题,那就是当我尝试点击RatingView视图的星级时,存在整个按钮都被点击的状况。
首先是一个AddBookView视图,该视图中嵌套了一个RatingView(rating:)视图。
struct AddBookView: View {
@State private var rating = 3
var body: some View {
...
Section("Write a review") {
TextEditor(text: $review)
RatingView(rating: $rating)
}
}
}
下面是RatingView视图代码:
struct RatingView: View {
@Binding var rating: Int
var label = ""
var maximumRating = 5
var offImage: Image?
var onImage = Image(systemName: "star.fill")
var offColor = Color.gray
var onColor = Color.yellow
func image(for number: Int) -> Image {
if number > rating {
offImage ?? onImage
} else {
onImage
}
}
var body: some View {
HStack {
if !label.isEmpty {
Text(label)
}
ForEach(1..<maximumRating + 1, id: \.self) {
number in
Button {
print("点击了第\(number)个星星")
rating = number
} label: {
image(for: number)
.foregroundStyle(number > rating ? offColor : onColor)
}
}
}
}
}
在AddBookView视图中点击RatingView的星级,会发现无论点击哪一个星星都是显示所有的星星。
时,Xcode输出了所有按钮,这也说明问题原因在于,某个代码导致所有的星星按钮都执行了一遍,所以显示的始终是五颗星。
点击了第1个星星
点击了第2个星星
点击了第3个星星
点击了第4个星星
点击了第5个星星
于是,在代码中添加了两个调试的按钮:
var body: some View {
Button(action: {
print("点击了整个View的按钮")
}, label: {
Text("View Button")
})
HStack {
if !label.isEmpty {
Text(label)
}
Button(action: {
print("点击了整个HStack的按钮")
}, label: {
Text("HStack Button")
})
ForEach(1..<maximumRating + 1, id: \.self) {
...
}
}
}
如果添加的View按钮和HStack按钮都触发,说明点击外部视图时,存在问题。如果都不触发,说明ForEach存在问题。
重新执行命令后,点击按钮,发现触发的是HStack的按钮,外部的View按钮没有触发。这说明点击的按钮区域存在问题。
经过排查定位问题在于嵌套视图可能导致按钮误触。
解决方案
解决方案一
替换 Button 为 onTapGesture:将星星图标的 Button 替换为 onTapGesture,可以避免嵌套按钮的问题。这种方式只会触发单个星星的点击事件,不会影响外部按钮。
Button {
print("点击了第\(number)个星星")
rating = number
} label: {
image(for: number)
.foregroundStyle(number > rating ? offColor : onColor)
}
修改为:
ForEach(1..<maximumRating + 1, id: \.self) {
number in
image(for: number)
.foregroundStyle(number > rating ? offColor : onColor)
.onTapGesture {
print("点击了第\(number)个星星")
rating = number
}
}
修改后,点击只会触发单个星星的效果,不会影响外部按钮。
解决方案二
HStack {
...
}
.buttonStyle(.plain)
给HStack添加.buttonStyle(.plain),这会禁用点击HStack时触发其内部的按钮。
修改后,SwiftUI就可以单独处理整个按钮。
以上就是全部的问题以及解决方案。