问题描述
Xcode运行时,toolbar报错:
Static method 'buildExpression' requires that 'ToolbarItem<(), Button<Label<Text, Image>>>' conform to 'View'
报错代码:
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Scan", systemImage: "qrcode.viewfinder") {
isShowingScanner = true
}
}
ToolbarItem(placement: .topBarLeading) {
EditButton()
}
ToolbarItem(placement: .topBarTrailing) {
Menu("Title") {
Button("姓名排序", action: {
_prospects = Query(sort: \Prospect.self)
})
Button("最近排序", action: {
_prospects = Query(sort: \Prospect.self,order: .reverse)
})
}
}
if selectedProspects.isEmpty == false {
ToolbarItem(placement: .bottomBar) {
Button("Delete Selected", action: delete)
}
}
}
问题原因
SwiftUI 的 ToolbarItem 的 placement 和 content 没有正确配对。特定情况下,ToolbarItem 需要接受一个单独的 View 类型,而不能直接嵌套复杂的 Menu 和 Button 等组合内容。
下面的Button格式不被ToolbarItem所接受:
Button("姓名排序", action: {
_prospects = Query(sort: \Prospect.self)
})
Button("最近排序", action: {
_prospects = Query(sort: \Prospect.self,order: .reverse)
})
问题解析
ToolbarItem 的 content 参数要求返回一个符合 View 协议的单一视图。
在 ToolbarItem 中直接使用了复杂的 Menu 组合内容,而 Menu 不直接符合这种上下文要求,SwiftUI 无法推断内容类型。
SwiftUI 的 DSL(领域特定语言)需要明确的类型约束。
解决方案
将 Menu 中的Button改为Button( ){ } 格式:
ToolbarItem(placement: .topBarTrailing) {
Menu("Title") {
Button("姓名排序") {
nameSore = true
}
Button("最近排序") {
nameSore = false
}
}
}
完整代码
import SwiftUI
import SwiftData
import CodeScanner
struct ProspectsView: View {
@Query var prospects: [Prospect]
@Environment(\.modelContext) var modelContext
@State private var isShowingScanner = false
@State private var selectedProspects = Set<Prospect>()
@State private var hideTips = false
@State private var nameSore = false
let filter: FilterType
init(filter: FilterType) {
self.filter = filter
if filter != .none {
let showContactedOnly = filter == .contacted
_prospects = Query(filter: #Predicate {
$0.isContacted == showContactedOnly
}, sort: [SortDescriptor(\Prospect.name)])
}
}
func delete() {
for prospect in selectedProspects {
modelContext.delete(prospect)
}
selectedProspects=[]
}
func handleScan(result: Result<ScanResult, ScanError>) {
isShowingScanner = false
switch result {
case .success(let result):
let details = result.string.components(separatedBy: "\n")
guard details.count == 2 else { return }
let person = Prospect(name: details[0], emailAddress: details[1], isContacted: false)
modelContext.insert(person)
case .failure(let error):
print("Scanning failed: \(error.localizedDescription)")
}
}
var title: String {
switch filter {
case .none:
"Everyone"
case .contacted:
"Contacted people"
case .uncontacted:
"Uncontacted people"
}
}
var body: some View {
NavigationStack {
if filter == .none,hideTips == false {
VStack {
HStack {
Spacer()
Image(systemName: "minus.square.fill")
.onTapGesture {
hideTips = true
}
}
Image(systemName: "lasso.badge.sparkles")
.font(.title)
Spacer().frame(height: 10)
Text("是否联系了潜在客户")
}
.foregroundColor(Color.white)
.frame(width: 200,height: 100)
.background(Color.blue)
.cornerRadius(10)
}
UserView(nameSort:nameSore,selectedProspects:$selectedProspects)
.navigationTitle(title)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Scan", systemImage: "qrcode.viewfinder") {
isShowingScanner = true
}
}
ToolbarItem(placement: .topBarLeading) {
EditButton()
}
ToolbarItem(placement: .topBarTrailing) {
Menu("排序") {
Button("姓名排序") {
nameSore = true
}
Button("最近排序") {
nameSore = false
}
}
}
if selectedProspects.isEmpty == false {
ToolbarItem(placement: .bottomBar) {
Button("Delete Selected", action: delete)
}
}
}
}
.sheet(isPresented: $isShowingScanner) {
CodeScannerView(codeTypes: [.qr], simulatedData: "Paul Hudson\npaul@hackingwithswift.com", completion: handleScan)
}
}
}
#Preview {
ProspectsView(filter: .none)
.modelContainer(for: Prospect.self)
}