I'm using macOS 14 and Xcode 15 and am new at Swift/SwiftUI.
I have been trying to implement a Picker for the last few days that will allow more than one item to picked in the same manner that Finder does, that is, the user can cancel, pick one item or, using the Shift key, pick two or more contiguous items.
Before I attempted to use "onChange" it kind of worked, the data displayed by Text worked but were not updated and Picker would not let me pick more than one item.
I don't know how to implement onChange as it has been recently updated. My web searches only return code for IOS or code that is old.
I get several errors that I don't understand with my attempted 'onChange' modification. zzzcode.ai doesn't produce code that compiles no matter how I phrase my question.
Note that when a URL is displayed I only display the last component.
I'd like to resolve the 'onChange' issue first as it might fix some of the errors.
import SwiftUI
extension URL: Identifiable {
public var id: Self { self }
}
struct PickTransectView: View {
@Binding var datList: [URL] // local file URLs w/.dat
@Binding var selectedURLs: [URL]? // subset of datList
// only display last URL component in this view
var body: some View {
VStack {
Text("Selected Transects:")
if let selectedURLs = selectedURLs { // if nil take from datList
if(selectedURLs.count > 1) {
Text("\(selectedURLs.first!.lastPathComponent) - \(selectedURLs.last!.lastPathComponent)")
} else {
Text("\(selectedURLs.last!.lastPathComponent)")
}
} else if let firstURL = datList.first, let lastURL = datList.last {
Text("\(firstURL.lastPathComponent) - \(lastURL.lastPathComponent)")
}
// expected return 1 or several contiguous URLs
Picker(selection: $selectedURLs, label: Text("Detailed List")) {
ForEach(datList, id: \.self) { url in
Text(url.lastPathComponent)
}
}
.pickerStyle(MenuPickerStyle())
.onChange(of: selectedURLs) { _ in // it fails here !
// Update Text on change of selectedURLs
if selectedURLs.count > 1 { // first & last URLs in selection
Text("\(selectedURLs.first!.lastPathComponent) - \(selectedURLs.last!.lastPathComponent)")
} else if let firstURL = selectedURLs.first {
Text("\(firstURL.lastPathComponent)")
}
}
}
}
}
When the user taps the Button in my TransectPickerView a pop-up sheet is presented that does the work.
struct TransectPickerView: View {
@Binding var selectedURLs: [URL]
@Binding var datList: [URL] // local file URLs w/.dat
@State private var isPresentingSelection: Bool = false
var body: some View {
VStack {
Button(action: {
isPresentingSelection = true
}) {
Text("Select Transects")
}
.buttonStyle(ButtonStyleControls())
.padding()
} // POPUP
.sheet(isPresented: $isPresentingSelection) {
FileURLSelectionSheetView(
selectedURLs: $selectedURLs,
datList: $datList)
}
}
}
The code below displays the last component of an array of local file URLs and allows the user to select (and deselect) a file. If a second file is selected an array of the files between and including the two selected files is built. Buttons also allow the user to: select all the files, Accept the selections which closes the View, or close the View without saving.
struct FileURLSelectionSheetView: View {
@Binding var selectedURLs: [URL] // to be populated
@Binding var datList: [URL] // local file URLs w/.dat
@Environment(\.presentationMode) var presentationMode
// local
@State var urlIndexSet = Set<Int>() // effanescent
@State var firstURL: URL?
@State var showSelectAll = true
var body: some View {
VStack {
Text(" Select Transects ")
.font(.title)
.padding()
ScrollView {
ForEach(datList.indices, id: \.self) { index in // present all URLs
let url = datList[index] // user selection
Text("\(url.lastPathComponent)")
.background(urlIndexSet.contains(index) ? Color.yellow.opacity(0.4) : Color.clear)
.onTapGesture { // tap logic tree starts here ...
if urlIndexSet.isEmpty {
urlIndexSet.insert(index) // first endpoint
} else if urlIndexSet.count == 1 { // endpoint
if urlIndexSet.contains(index) { // edit
urlIndexSet.removeAll()
} else { // 2nd end point, create list
if index > urlIndexSet.first! { // ascending
for item in urlIndexSet.first!...index {
urlIndexSet.insert(item)
}
} else { // descending
for item in index...urlIndexSet.first!{
urlIndexSet.insert(item)
}
}
}
} // onTapGesture
} // 1st ForEach
} // ScrollView
.frame(maxHeight: .infinity)
Button(action: {
if showSelectAll {
for item in 0..<datList.count {
urlIndexSet.insert(item)
}
showSelectAll = false
} else {
urlIndexSet.removeAll()
showSelectAll = true
}
}) {
if showSelectAll {
Text("Select All")
.padding(.top, 10)
} else {
Text("Reset All")
.padding(.top, 10)
}
}
.buttonStyle(ButtonStyleControls())
HStack() {
Button(action: { // This appears to present correctly
selectedURLs.removeAll() //
presentationMode.wrappedValue.dismiss()
}) {
Text("Cancel")
}
.buttonStyle(ButtonStyleControls())
Spacer()
// ============= populaate selectedURLs ===========
Button(action: {
selectedURLs.removeAll()
let indexList = urlIndexSet.sorted()
for item in indexList
{
selectedURLs.append(datList[item])
}
presentationMode.wrappedValue.dismiss()
}) {
Text("Accept")
}
.buttonStyle(ButtonStyleControls())
.disabled(urlIndexSet.isEmpty)
.opacity(urlIndexSet.isEmpty ? 0.5 : 1.0) // Mute if disabled
} // HStack
.padding()
Divider()
Text("\(urlIndexSet.count) selected")
.font(.title2)
} // VStack
.frame(maxWidth: .infinity, maxHeight: .infinity) // Adjust the height of the VStack
}
} // end popup view struct
}
I've successfully tested this for up to 28 URLs. I expect it to scroll if it would otherwise exceed the height of the monitor.