SwiftUI onDrag. How to provide multiple NSItemProviders? - swift

In SwiftUI on MacOs, when implementing
onDrop(of supportedTypes: [String], isTargeted: Binding<Bool>?, perform action: #escaping ([NSItemProvider]) -> Bool) -> some View
we receive an array of NSItemProvider and this makes it possible to drop multiple items inside our view.
When implementing onDrag(_ data: #escaping () -> NSItemProvider) -> some View , how can we provide multiple items to drag?
I've not been able to find any examples online of multiple items drag and I'd like to know if there's another way to implement a drag operation that allows me to provide multiple NSItemProvider or the way to do it with the above method
My goal is to be able to select multiple items and drag them exactly how it happens in the Finder. In order to do that I want to provide an [URL] as [NItemProvider], but at the moment I can only provide one URL per drag Operation.

Might be worth checking if View's exportsItemProviders functions added in macOS 12 do what we need. If you use the version of List that supports multi-selection (List(selection: $selection) where #State var selection: Set<UUID> = [] (or whatever)).
Unfortunately my Mac is still on macOS 11.x so I can't test this :-/

Actually, you do not need an [NSItemProvider] to process a drag and drop with multiple items in SwiftUI. Since you must keep track of the multiple selected Items in your own selection manager anyway, use that selection when generating a custom dragging preview and when processing the drop.
Replace the ContentView of a new MacOS App project with all of the code below. This is a complete working sample of how to drag and drop multiple items using SwiftUI.
To use it, you must select one or more items in order to initiate a drag and then it/they may be dragged onto any other unselected item. The results of what would happen during the drop operation is printed on the console.
I threw this together fairly quickly, so there may be some inefficiencies in my sample, but it does seem to work well.
import SwiftUI
import Combine
struct ContentView: View {
private let items = ["Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7"]
#StateObject var selection = StringSelectionManager()
#State private var refreshID = UUID()
#State private var dropTargetIndex: Int? = nil
var body: some View {
VStack(alignment: .leading) {
ForEach(0 ..< items.count, id: \.self) { index in
HStack {
Image(systemName: "folder")
Text(items[index])
}
.opacity((dropTargetIndex != nil) && (dropTargetIndex == index) ? 0.5 : 1.0)
// This id must change whenever the selection changes, otherwise SwiftUI will use a cached preview
.id(refreshID)
.onDrag { itemProvider(index: index) } preview: {
DraggingPreview(selection: selection)
}
.onDrop(of: [.text], delegate: MyDropDelegate(items: items,
selection: selection,
dropTargetIndex: $dropTargetIndex,
index: index) )
.padding(2)
.onTapGesture { selection.toggle(items[index]) }
.background(selection.isSelected(items[index]) ?
Color(NSColor.selectedContentBackgroundColor) : Color(NSColor.windowBackgroundColor))
.cornerRadius(5.0)
}
}
.onReceive(selection.objectWillChange, perform: { refreshID = UUID() } )
.frame(width: 300, height: 300)
}
private func itemProvider(index: Int) -> NSItemProvider {
// Only allow Items that are part of a selection to be dragged
if selection.isSelected(items[index]) {
return NSItemProvider(object: items[index] as NSString)
} else {
return NSItemProvider()
}
}
}
struct DraggingPreview: View {
var selection: StringSelectionManager
var body: some View {
VStack(alignment: .leading, spacing: 1.0) {
ForEach(selection.items, id: \.self) { item in
HStack {
Image(systemName: "folder")
Text(item)
.padding(2.0)
.background(Color(NSColor.selectedContentBackgroundColor))
.cornerRadius(5.0)
Spacer()
}
}
}
.frame(width: 300, height: 300)
}
}
struct MyDropDelegate: DropDelegate {
var items: [String]
var selection: StringSelectionManager
#Binding var dropTargetIndex: Int?
var index: Int
func dropEntered(info: DropInfo) {
dropTargetIndex = index
}
func dropExited(info: DropInfo) {
dropTargetIndex = nil
}
func validateDrop(info: DropInfo) -> Bool {
// Only allow non-selected Items to be drop targets
if !selection.isSelected(items[index]) {
return info.hasItemsConforming(to: [.text])
} else {
return false
}
}
func dropUpdated(info: DropInfo) -> DropProposal? {
// Sets the proper DropOperation
if !selection.isSelected(items[index]) {
let dragOperation = NSEvent.modifierFlags.contains(NSEvent.ModifierFlags.option) ? DropOperation.copy : DropOperation.move
return DropProposal(operation: dragOperation)
} else {
return DropProposal(operation: .forbidden)
}
}
func performDrop(info: DropInfo) -> Bool {
// Only allows non-selected Items to be drop targets & gets the "operation"
let dropProposal = dropUpdated(info: info)
if dropProposal?.operation != .forbidden {
let dropOperation = dropProposal!.operation == .move ? "Move" : "Copy"
if selection.selection.count > 1 {
for item in selection.selection {
print("\(dropOperation): \(item) Onto: \(items[index])")
}
} else {
// https://stackoverflow.com/a/69325742/899918
if let item = info.itemProviders(for: ["public.utf8-plain-text"]).first {
item.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil) { (data, error) in
if let data = data as? Data {
let item = NSString(data: data, encoding: 4)
print("\(dropOperation): \(item ?? "") Onto: \(items[index])")
}
}
}
return true
}
}
return false
}
}
class StringSelectionManager: ObservableObject {
#Published var selection: Set<String> = Set<String>()
let objectWillChange = PassthroughSubject<Void, Never>()
// Helper for ForEach
var items: [String] {
return Array(selection)
}
func isSelected(_ value: String) -> Bool {
return selection.contains(value)
}
func toggle(_ value: String) {
if isSelected(value) {
deselect(value)
} else {
select(value)
}
}
func select(_ value: String?) {
if let value = value {
objectWillChange.send()
selection.insert(value)
}
}
func deselect(_ value: String) {
objectWillChange.send()
selection.remove(value)
}
}

Related

SwiftUI - multiple levels deep NavigationLink does not work

So I thought I found a way to make navigation in SwiftUI flexible and loosely coupled, yet still state-based and somewhat free of imperative-navigation bugs (double push, etc).
Basic idea is to have a linked list of Views (erased to AnyView) and a recursive view with NavigationLink in it, which is active when corresponding view is present in the list
But it does not work and I don't understand why. On iOS device it only pushes one level deep, even though the list is multiple levels deep and the bindings return true
Is it a SwiftUI bug or am I missing something?
struct ContentView: View {
#State
var navigationList: NavigationList?
var body: some View {
NavigationView {
Navigatable(list: $navigationList) {
Button("Push test", action: {
navigationList = .init(next: nil, screen: Screen {
TestView()
})
})
}
}
}
}
struct TestView: View {
#Environment(\.navigationList)
#Binding
var list
var body: some View {
Button("Push me", action: {
list = .init(next: nil, screen: Screen {
TestView()
})
})
}
}
struct Navigatable<Content: View>: View {
#Binding
var list: NavigationList?
let content: () -> Content
init(list: Binding<NavigationList?>, #ViewBuilder content: #escaping () -> Content) {
self._list = list
self.content = content
}
var body: some View {
ZStack {
NavigationLink(
isActive: isActive,
destination: {
Navigatable<Screen?>(list: childBinding) {
list?.screen
}
},
label: EmptyView.init
).hidden()
LazyView {
content()
}.environment(\.navigationList, $list)
}
}
var isActive: Binding<Bool> {
.init(
get: { list != nil },
set: {
if !$0 {
list = nil
}
}
)
}
var childBinding: Binding<NavigationList?> {
.init(
get: { list?.next },
set: { list?.next = $0 }
)
}
}
struct Screen: View {
let content: () -> AnyView
init<C: View>(#ViewBuilder content: #escaping () -> C) {
self.content = {
.init(content())
}
}
var body: some View {
content()
}
}
struct NavigationList {
#Indirect
var next: NavigationList?
let screen: Screen
}
enum NavigationListKey: EnvironmentKey {
static var defaultValue: Binding<NavigationList?> {
.constant(nil)
}
}
extension EnvironmentValues {
var navigationList: Binding<NavigationList?> {
get { self[NavigationListKey.self] }
set { self[NavigationListKey.self] = newValue }
}
}
struct LazyView<Content: View>: View {
#ViewBuilder var content: () -> Content
var body: some View {
content()
}
}
#propertyWrapper
struct Indirect<Wrapped> {
private final class Storage: CustomReflectable {
var wrapped: Wrapped
init(_ wrapped: Wrapped) {
self.wrapped = wrapped
}
var customMirror: Mirror {
.init(self, children: [(label: "wrapped", value: wrapped)])
}
}
private let storage: Storage
var wrappedValue: Wrapped {
get { storage.wrapped }
mutating set { storage.wrapped = newValue }
}
init(wrappedValue: Wrapped) {
self.storage = .init(wrappedValue)
}
}
You’re missing isDetailLink(false) which is what allows multiple screens to be pushed on to one navigation controller.
But there are also structural problems with the code. It's best to use SwiftUI View data structs as designed and let them store the hierachy of data. If you go off on your own architecture then you lose the magic like invalidation and diffing and it'll likely slow down too.

How can I use onDisappear modifier for knowing the killed View in SwiftUI?

I have a study project also you can call it testing/ learning project! Which has a goal to recreate the data source and trying get sinked with latest updates to data source!
In this project I was successful to create the same data source without sending or showing data source to the Custom View! like this example project in down, so I want keep the duplicated created data source with original data source updated!
For example: I am deleting an element in original data source and I am trying read the id of the View that get disappeared! for deleting the same one in duplicated one! but it does not work well!
The Goal: As you can see in my project and my codes! I want create and keep the duplicated data source sinked and equal to original without sending the data source directly to custom View! or even sending any kind of notification! the goal is that original data source should put zero work to make itself known to duplicated data source and the all work is belong to custom View to figure out the deleted item.
PS: please do not ask about use case or something like that! I am experimenting this method to see if it would work or where it could be helpful!
struct ContentView: View {
#State private var array: [Int] = Array(0...3)
var body: some View {
VStack(alignment: .leading, spacing: 10.0) {
ForEach(array.indices, id:\.self) { index in
CustomView(id: index) {
HStack {
Text(String(describing: array[index]))
.frame(width: 50.0)
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture { array.remove(at: index) }
}
}
}
}
.font(Font.body.bold())
}
}
struct CustomView<Content: View>: View {
let id: Int
let content: () -> Content
#StateObject private var customModel: CustomModel = CustomModel.shared
var body: some View {
content()
.preference(key: IntPreferenceKey.self, value: id)
.onPreferenceChange(IntPreferenceKey.self) { newValue in
if !customModel.array.contains(newValue) { customModel.array.append(newValue) }
}
.onDisappear(perform: {
print("id:", id, "is going get removed!")
customModel.array.remove(at: id)
})
}
}
class CustomModel: ObservableObject {
static let shared: CustomModel = CustomModel()
#Published var array: Array<Int> = Array<Int>() {
didSet {
print(array.sorted())
}
}
}
struct IntPreferenceKey: PreferenceKey {
static var defaultValue: Int { get { return Int() } }
static func reduce(value: inout Int, nextValue: () -> Int) { value = nextValue() }
}
Relying on array indexes for ForEach will be unreliable for this sort of work, since the ID you're using is self -- ie the index of the item. This will result in unreliable recalculations of the items in ForEach since they're not actually identifiable by their index. For example, if item 0 gets removed, then what was item 1 now becomes item 0, making using an index as the identifier relatively useless.
Instead, use actual unique IDs to describe your models and everything works as expected:
struct Item: Identifiable {
let id = UUID()
var label : Int
}
struct ContentView: View {
#State private var array: [Item] = [.init(label: 0),.init(label: 1),.init(label: 2),.init(label: 3)]
var body: some View {
VStack(alignment: .leading, spacing: 10.0) {
ForEach(array) { item in
CustomView(id: item.id) {
HStack {
Text("\(item.label) - \(item.id)")
.frame(width: 50.0)
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture { array.removeAll(where: { $0.id == item.id }) }
}
}
}
}
.font(.body.bold())
}
}
struct CustomView<Content: View>: View {
let id: UUID
let content: () -> Content
#StateObject private var customModel: CustomModel = CustomModel.shared
var body: some View {
content()
.preference(key: UUIDPreferenceKey.self, value: id)
.onPreferenceChange(UUIDPreferenceKey.self) { newValue in
if !customModel.array.contains(where: { $0 == id }) {
customModel.array.append(id)
}
}
.onDisappear(perform: {
print("id:", id, "is going to get removed!")
customModel.array.removeAll(where: { $0 == id })
})
}
}
class CustomModel: ObservableObject {
static let shared: CustomModel = CustomModel()
#Published var array: Array<UUID> = Array<UUID>() {
didSet {
print(array)
}
}
}
struct UUIDPreferenceKey: PreferenceKey {
static var defaultValue: UUID { get { return UUID() } }
static func reduce(value: inout UUID, nextValue: () -> UUID) { value = nextValue() }
}

How do I generate custom TextFields from an array of strings, each with its own edit/delete controls?

My IOS application has a form-builder for users. Users can create a form, add questions to it, and change question types, including Text Response, Multiple Choice, and Checkbox. For the Multiple Choice and Checkbox types, users must provide between 2-5 options.
In my QuestionManager (view model), I'm storing these options as an array of strings. In the view, I'm generating OptionCells using a ForEach loop. Each OptionCell has a TextField and control functions to delete, edit and confirm each option. The issue is that I'm unsure about my approach because there are a few bugs that I think are due to the way I've set this up.
For example, which is really the only issue, I have a #State var isEditing: Bool variable in the OptionCell that is not being updated, which I believe has something do with the way I've declared the #Published var options: [String] in the QuestionManager. Or it might have something to do with the #State var option: String in the OptionCell. (I tried setting this up as #Binding, but for some reason that would not work.) But none of this makes sense to me since the isEditing variable is set up to update when the View is tapped, though I've narrowed the problem down to what I'm most uncertain about and I believe it is somehow related to what I described above. Any suggestions?
struct OptionCell: View {
#ObservedObject var manager: QuestionManager
#State var option: String
#State var isEditing: Bool = false
var optIndex: Int
init(_ manager: QuestionManager, option: String) {
self.manager = manager
self.option = option
self.optIndex = manager.options!.firstIndex(of: option)!
}
var body: some View {
HStack {
MyCustomTextField(text: $option)
.keyboardType(.default)
.onTapGesture {
option = ""
isEditing = true // The main issue -> Isnt updating the view
}
// ActionIcon is just a Button with a SFLabels string input
ActionIcon(isEditing ? "checkmark" : "xmark", size: 10) { // No changes are recognized
isEditing ? saveOption() : deleteOption()
}
.padding(.horizontal)
}
}
func saveOption() {
if option == "" {
manager.error = "An option must not be empty"
} else {
manager.options![optIndex] = option
isEditing = false
}
}
func deleteOption() {
if manager.options!.count == 2 {
manager.error = "You must have at least two options"
} else {
manager.deleteOption(option: option)
}
}
}
struct QuestionView: View {
#Environment(\.presentationMode) var mode
#ObservedObject var manager: QuestionManager
init(_ manager: QuestionManager) { self.manager = manager }
var body: some View {
ZStack {
MyCustomViewWrapper {
// ^ Reskins the background, adds padding, etc.
ScrollView {
VStack(spacing: 20) {
ConstantTitledTextField("Type", manager.type.rawValue)
// ^ Reskinned TextField
.overlay {
HStack {
Spacer()
self.changeTypeMenu
}
.padding(.horizontal)
.padding(.top, 30)
}
TitledTextField("Prompt", $manager.prompt)
// ^ Another reskinned TextField
.keyboardType(.default)
.onTapGesture { manager.prompt = "" }
MyCustomDivider()
if manager.options != nil {
self.optionsSection
}
}
}
}
.alert(isPresented: $manager.error.isNotNil()) {
MyCustomAlert(manager.error!) { manager.error = nil }
}
VStack {
Spacer()
MyCustomButton("Save") { checkAndSave() }
}
.hiddenIfKeyboardActive()
}
}
var changeTypeMenu: some View {
Image(systemName: "chevron.down").icon(15)
.menuOnPress {
Button(action: { self.manager.changeType(.text) } ) { Text("Text Response") }
Button(action: { self.manager.changeType(.multiple) } ) { Text("Multiple Choice") }
Button(action: { self.manager.changeType(.checkbox) } ) { Text("Checkbox") }
}
}
var optionsSection: some View {
VStack(spacing: 15) {
HStack {
Text("Options")
.font(.title3)
.padding(.horizontal)
Spacer()
ActionIcon("plus", size: 15) { manager.addOption() }
.padding(.horizontal)
}
ForEach(manager.options!, id: \.self) { opt in
OptionCell(manager, option: opt)
}
}
}
private func checkAndSave() {
if manager.checkQuestion() {
manager.saveQuestion()
self.mode.wrappedValue.dismiss()
}
}
}
class QuestionManager: ObservableObject {
#Published var question: Question
#Published var type: Question.QuestionType
#Published var prompt: String
#Published var options: [String]?
#Published var error: String? = nil
#Published var id = ""
init(_ question: Question) {
self.question = question
self.type = question.type
self.prompt = question.prompt
self.options = question.options
self.id = question.id
}
func saveQuestion() {
self.question.prompt = prompt
self.question.type = type
self.question.options = options
}
func checkQuestion() -> Bool {
if type == .checkbox || type == .multiple {
if options!.count < 2 {
error = "You must have at least two options"
return false
}
else if options!.contains("") {
error = "Options must not be empty"
return false
}
else if !options!.elementsAreUnique() {
error = "Options must be unique"
return false
}
}
if prompt.isEmpty {
error = "You must include a prompt for your question"
return false
}
return true
}
func changeType(_ type: Question.QuestionType) {
if type == .text {
options = nil
}
else {
options = ["Option 1", "Option 2", "Option 3"]
}
self.type = type
}
func addOption() {
guard options != nil else { return }
if options!.count == 5 {
error = "No more than 5 options are permitted"
}
else {
let count = options!.count
let newOption = "Option \(count + 1)"
options!.append(newOption)
}
}
func deleteOption(option: String) {
guard options != nil else { return }
guard let index = self.options!.firstIndex(of: option) else {
self.error = "An error occured during deleting ... sorry you're screwed"
return
}
self.options!.remove(at: index)
}
}

SwiftUI list empty state view/modifier

I was wondering how to provide an empty state view in a list when the data source of the list is empty. Below is an example, where I have to wrap it in an if/else statement. Is there a better alternative for this, or is there a way to create a modifier on a List that'll make this possible i.e. List.emptyView(Text("No data available...")).
import SwiftUI
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
} else {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
}
}
struct EmptyListExample_Previews: PreviewProvider {
static var previews: some View {
EmptyListExample(objects: [])
}
}
I quite like to use an overlay attached to the List for this because it's quite a simple, flexible modifier:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
VStack {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.overlay(Group {
if objects.isEmpty {
Text("Oops, loos like there's no data...")
}
})
}
}
}
It has the advantage of being nicely centred & if you use larger placeholders with an image, etc. they will fill the same area as the list.
One of the solutions is to use a #ViewBuilder:
struct EmptyListExample: View {
var objects: [Int]
var body: some View {
listView
}
#ViewBuilder
var listView: some View {
if objects.isEmpty {
emptyListView
} else {
objectsListView
}
}
var emptyListView: some View {
Text("Oops, loos like there's no data...")
}
var objectsListView: some View {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
}
}
You can create a custom modifier that substitutes a placeholder view when your list is empty. Use it like this:
List(items) { item in
Text(item.name)
}
.emptyPlaceholder(items) {
Image(systemName: "nosign")
}
This is the modifier:
struct EmptyPlaceholderModifier<Items: Collection>: ViewModifier {
let items: Items
let placeholder: AnyView
#ViewBuilder func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
extension View {
func emptyPlaceholder<Items: Collection, PlaceholderView: View>(_ items: Items, _ placeholder: #escaping () -> PlaceholderView) -> some View {
modifier(EmptyPlaceholderModifier(items: items, placeholder: AnyView(placeholder())))
}
}
I tried #pawello2222's approach, but the view didn't get rerendered if the passed objects' content change from empty(0) to not empty(>0), or vice versa, but it worked if the objects' content was always not empty.
Below is my approach to work all the time:
struct SampleList: View {
var objects: [IdentifiableObject]
var body: some View {
ZStack {
Empty() // Show when empty
List {
ForEach(objects) { object in
// Do something about object
}
}
.opacity(objects.isEmpty ? 0.0 : 1.0)
}
}
}
You can make ViewModifier like this for showing the empty view. Also, use View extension for easy use.
Here is the demo code,
//MARK: View Modifier
struct EmptyDataView: ViewModifier {
let condition: Bool
let message: String
func body(content: Content) -> some View {
valideView(content: content)
}
#ViewBuilder
private func valideView(content: Content) -> some View {
if condition {
VStack{
Spacer()
Text(message)
.font(.title)
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
Spacer()
}
} else {
content
}
}
}
//MARK: View Extension
extension View {
func onEmpty(for condition: Bool, with message: String) -> some View {
self.modifier(EmptyDataView(condition: condition, message: message))
}
}
Example (How to use)
struct EmptyListExample: View {
#State var objects: [Int] = []
var body: some View {
NavigationView {
List(objects, id: \.self) { obj in
Text("\(obj)")
}
.onEmpty(for: objects.isEmpty, with: "Oops, loos like there's no data...") //<--- Here
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button("Add") {
objects = [1,2,3,4,5,6,7,8,9,10]
}
Button("Empty") {
objects = []
}
}
}
}
}
}
In 2021 Apple did not provide a List placeholder out of the box.
In my opinion, one of the best way to make a placeholder, it's creating a custom ViewModifier.
struct EmptyDataModifier<Placeholder: View>: ViewModifier {
let items: [Any]
let placeholder: Placeholder
#ViewBuilder
func body(content: Content) -> some View {
if !items.isEmpty {
content
} else {
placeholder
}
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.modifier(EmptyDataModifier(
items: countries,
placeholder: Text("No Countries").font(.title)) // Placeholder. Can set Any SwiftUI View
)
}
}
Also via extension can little bit improve the solution:
extension List {
func emptyListPlaceholder(_ items: [Any], _ placeholder: AnyView) -> some View {
modifier(EmptyDataModifier(items: items, placeholder: placeholder))
}
}
struct ContentView: View {
#State var countries: [String] = [] // Data source
var body: some View {
List(countries) { country in
Text(country)
.font(.title)
}
.emptyListPlaceholder(
countries,
AnyView(ListPlaceholderView()) // Placeholder
)
}
}
If you are interested in other ways you can read the article

Disable specific picker items

Is there a way to disable specific picker items? In AppKit, you can disable NSPopUpButton items via the NSMenuValidation protocol, but disabling a label in the picker predictably does nothing. Just another API gap in SwiftUI?
I tried:
Picker(selection: $viewModel.providerSelection, label: Text("Try using:")) {
ForEach(0..<Provider.allCases.count) {
Text(Provider.allCases[$0].rawValue.capitalized)
.disabled(true)
}
}
and there was no visual or interaction difference between disabling and not here.
This seems currently not supported in pure SwiftUI. However, you can try wrapping NSPopUpButton in NSViewRepresentable, like this:
/// SwiftUI wrapper for NSPopUpButton which allows items to be disabled, which is currently not supported in SwiftUI's Picker
struct PopUpButtonPicker<Item: Equatable>: NSViewRepresentable {
final class Coordinator: NSObject {
private let parent: PopUpButtonPicker
init(parent: PopUpButtonPicker) {
self.parent = parent
}
#IBAction
func selectItem(_ sender: NSPopUpButton) {
let selectedItem = self.parent.items[sender.indexOfSelectedItem]
print("selected item \(selectedItem) at index=\(sender.indexOfSelectedItem)")
self.parent.selection = selectedItem
}
}
let items: [Item]
var isItemEnabled: (Item) -> Bool = { _ in true }
#Binding var selection: Item
let titleProvider: (Item) -> String
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
func makeNSView(context: Self.Context) -> NSPopUpButton {
let popUpButton = NSPopUpButton(frame: .zero, pullsDown: false)
popUpButton.autoenablesItems = false
popUpButton.target = context.coordinator
popUpButton.action = #selector(Coordinator.selectItem(_:))
for item in items.enumerated() {
popUpButton.addItem(withTitle: self.titleProvider(item.element))
// in order for this to work, autoenablesItems must be set to false
popUpButton.menu?.item(at: item.offset)?.isEnabled = self.isItemEnabled(item.element)
}
if let selectedIndex = self.items.firstIndex(where: { $0 == self.selection }) {
popUpButton.selectItem(at: selectedIndex)
}
return popUpButton
}
func updateNSView(_ view: NSPopUpButton, context: Self.Context) { }
}
// MARK: - Usage
struct ContentView: View {
private let items: [Int] = [1, 2, 3, 4, 5]
#State private var selectedValue: Int = 0
var body: some View {
PopUpButtonPicker(items: self.items, isItemEnabled: { $0 != 4 }, selection: self.$selectedValue, titleProvider: String.init)
}
}