How to make List with single selection with SwiftUI - swift

I am creating single selection list to use in different places in my app.
Questions:
Is there an easy solution I don't know?
If there isn't, how can I finish my current solution?
My goal:
List with always only one item selected or one or none item selected (depending on configuration)
Transparent background
On item select - perform action which is set as parameter via init() method. (That action requires selected item info.)
Change list data programmatically and reset selection to first item
My current solution looks like:
List view with second item selected
I can't use Picker, because outer action (goal Nr. 3) is time consuming. So I think it wouldn't work smoothly.
Most probably there is solution to my problem in SwiftUI, but either I missed it, because I am new to swift or as I understand not everything works perfectly in SwiftUI yet, for example: transparent background for List (which is why i needed to clear background in init()).
So I started implementing selection myself, and stoped here:
My current solution does not update view when item(Button) is clicked. (Only going out and back to the page updates view). And still multiple items can be selected.
import SwiftUI
struct ModuleList: View {
var modules: [Module] = []
#Binding var selectionKeeper: Int
var Action: () -> Void
init(list: [Module], selection: Binding<Int>, action: #escaping () -> Void) {
UITableView.appearance().backgroundColor = .clear
self.modules = list
self._selectionKeeper = selection
self.Action = action
}
var body: some View {
List(){
ForEach(0..<modules.count) { i in
ModuleCell(module: self.modules[i], action: { self.changeSelection(index: i) })
}
}.background(Constants.colorTransparent)
}
func changeSelection(index: Int){
modules[selectionKeeper].isSelected = false
modules[index].isSelected = true
selectionKeeper = index
self.Action()
}
}
struct ModuleCell: View {
var module: Module
var Action: () -> Void
init(module: Module, action: #escaping () -> Void) {
UITableViewCell.appearance().backgroundColor = .clear
self.module = module
self.Action = action
}
var body: some View {
Button(module.name, action: {
self.Action()
})
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.modifier(Constants.CellSelection(isSelected: module.isSelected))
}
}
class Module: Identifiable {
var id = UUID()
var name: String = ""
var isSelected: Bool = false
var address: Int
init(name: String, address: Int){
self.name = name
self.address = address
}
}
let testLines = [
Module(name: "Line1", address: 1),
Module(name: "Line2", address: 3),
Module(name: "Line3", address: 5),
Module(name: "Line4", address: 6),
Module(name: "Line5", address: 7),
Module(name: "Line6", address: 8),
Module(name: "Line7", address: 12),
Module(name: "Line8", address: 14),
Module(name: "Line9", address: 11),
Module(name: "Line10", address: 9),
Module(name: "Line11", address: 22)
]
Testing some ideas:
Tried adding #State array of (isSelected: Bool) in ModuleList and binding it to Module isSelected parameter that MIGHT update view... But failed then populating this array in init(), because #State array parameter would stay empty after .append()... Maybe adding function setList would have solved this, and my goal Nr. 4. But I was not sure if this would really update my view in the first place.
struct ModuleList: View {
var modules: [Module] = []
#State var selections: [Bool] = []
init(list: [String]) {
UITableView.appearance().backgroundColor = .clear
selections = [Bool] (repeating: false, count: list.count) // stays empty
let test = [Bool] (repeating: false, count: list.count) // testing: works as it should
selections = test
for i in 0..<test.count { // for i in 0..<selections.count {
selections.append(false)
modules.append(Module(name: list[i], isSelected: $selections[i])) // Error selections is empty
}
}
var body: some View {
List{
ForEach(0..<modules.count) { i in
ModuleCell(module: self.modules[i], action: { self.changeSelection(index: i) })
}
}.background(Constants.colorTransparent)
}
func changeSelection(index: Int){
modules[index].isSelected = true
}
}
struct ModuleCell: View {
var module: Module
var Method: () -> Void
init(module: Module, action: #escaping () -> Void) {
UITableViewCell.appearance().backgroundColor = .clear
self.module = module
self.Method = action
}
var body: some View {
Button(module.name, action: {
self.Method()
})
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.modifier(Constants.CellSelection(isSelected: module.isSelected))
}
}
struct Module: Identifiable {
var id = UUID()
var name: String = ""
#Binding var isSelected: Bool
init(name: String, isSelected: Binding<Bool>){
self.name = name
self._isSelected = isSelected
}
}
let testLines = ["Line1","Line2","Line3","Line4"
]

The easiest way to achieve this would be to have #State in the View containing the list with the selection and pass it as #Binding to the cells:
struct SelectionView: View {
let fruit = ["apples", "pears", "bananas", "pineapples"]
#State var selectedFruit: String? = nil
var body: some View {
List {
ForEach(fruit, id: \.self) { item in
SelectionCell(fruit: item, selectedFruit: self.$selectedFruit)
}
}
}
}
struct SelectionCell: View {
let fruit: String
#Binding var selectedFruit: String?
var body: some View {
HStack {
Text(fruit)
Spacer()
if fruit == selectedFruit {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
} .onTapGesture {
self.selectedFruit = self.fruit
}
}
}

Here is a more generic approach, you can still extend answer according to your needs;
TLDR
https://gist.github.com/EnesKaraosman/d778cdabc98ca269b3d162896bea8aac
Detail
struct SingleSelectionList<Item: Identifiable, Content: View>: View {
var items: [Item]
#Binding var selectedItem: Item?
var rowContent: (Item) -> Content
var body: some View {
List(items) { item in
rowContent(item)
.modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
.contentShape(Rectangle())
.onTapGesture {
self.selectedItem = item
}
}
}
}
struct CheckmarkModifier: ViewModifier {
var checked: Bool = false
func body(content: Content) -> some View {
Group {
if checked {
ZStack(alignment: .trailing) {
content
Image(systemName: "checkmark")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.green)
.shadow(radius: 1)
}
} else {
content
}
}
}
}
And to demonstrate;
struct PlaygroundView: View {
struct Koko: Identifiable {
let id = UUID().uuidString
var name: String
}
var mock = Array(0...10).map { Koko(name: "Item - \($0)") }
#State var selectedItem: Koko?
var body: some View {
VStack {
Text("Selected Item: \(selectedItem?.name ?? "Select one")")
Divider()
SingleSelectionList(items: mock, selectedItem: $selectedItem) { (item) in
HStack {
Text(item.name)
Spacer()
}
}
}
}
}
Final Result

Selection
SwiftUI does not currently have a built in way to select one row of a list and change its appearance accordingly. But you're actually very close to your answer. In fact, your selection is in fact already working, but just isn't being used in any way.
To illustrate, add the following line right after ModuleCell(...) in your ForEach:
.background(i == self.selectionKeeper ? Color.red : nil)
In other words, "If my current row (i) matches the value stored in selectionKeeper, color the cell red, otherwise, use the default color." You'll see that as you tap on different rows, the red coloring follows your taps, showing the underlying selection is in fact changing.
Deselection
If you wanted to enable deselection, you could pass in a Binding<Int?> as your selection, and set it to nil when the currently selected row is tapped:
struct ModuleList: View {
var modules: [Module] = []
// this is new ------------------v
#Binding var selectionKeeper: Int?
var Action: () -> Void
// this is new ----------------------------v
init(list: [Module], selection: Binding<Int?>, action: #escaping () -> Void) {
...
func changeSelection(index: Int){
if selectionKeeper != index {
selectionKeeper = index
} else {
selectionKeeper = nil
}
self.Action()
}
}
Deduplicating State and Separation of Concerns
On a more structural level, you really want a single source of truth when using a declarative UI framework like SwiftUI, and to cleanly separate your view from your model. At present, you have duplicated state — selectionKeeper in ModuleList and isSelected in Module both keep track of whether a given module is selected.
In addition, isSelected should really be a property of your view (ModuleCell), not of your model (Module), because it has to do with how your view appears, not the intrinsic data of each module.
Thus, your ModuleCell should look something like this:
struct ModuleCell: View {
var module: Module
var isSelected: Bool // Added this
var Action: () -> Void
// Added this -------v
init(module: Module, isSelected: Bool, action: #escaping () -> Void) {
UITableViewCell.appearance().backgroundColor = .clear
self.module = module
self.isSelected = isSelected // Added this
self.Action = action
}
var body: some View {
Button(module.name, action: {
self.Action()
})
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.modifier(Constants.CellSelection(isSelected: isSelected))
// Changed this ------------------------------^
}
}
And your ForEach would look like
ForEach(0..<modules.count) { i in
ModuleCell(module: self.modules[i],
isSelected: i == self.selectionKeeper,
action: { self.changeSelection(index: i) })
}

Use binding with Optional type selection variable.
List will allow only one element selected.
struct ContentView: View {
// Use Optional for selection.
// Instead od Set or Array like this...
// #State var selection = Set<Int>()
#State var selection = Int?.none
var body: some View {
List(selection: $selection) {
ForEach(0..<128) { _ in
Text("Sample")
}
}
}
}
Tested with Xcode Version 11.3 (11C29) on macOS 10.15.2 (19C57) for a Mac app.

Related

SwiftUI: Can only Edit the first item in a ForEach List

I have a list of bookmarks saved in Core Data. When i try to click on the edit button for a particular bookmark, only the values of the first bookmark on the list will fill up the textfields in the editing sheet no matter which bookmark i pick.
This is a macOS 10.15 app.
ContentView
#State private var showEditView = false
ForEach(vm.myBookmarks) { myBookmark in
Text(myBookmark.name)
Text(myBookmark.url)
Button {
showEditView = true
} label: {
Image("pencil")
}
.sheet(isPresented: $showEditView) {
EditBookmarkView(name: myBookmark.name, url: myBookmark.url, isVisible: $showEditView, bm: myBookmark)
}
}
EditBookmarkView
struct EditBookmarkView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var isVisible: Bool
#ObservedObject var vm: EditBookmarkViewModel
let name: String
let url: String
var bm: MyBookmarkViewModel
init(name: String, url: String, isVisible: Binding<Bool>, bm: MyBookmarkViewModel) {
self.bm = bm
self.vm = EditBookmarkViewModel(bookmarkVM: bm)
_isVisible = isVisible
self.name = name
self.url = url
}
var body: some View {
TextField("Edit Name", text: $vm.name)
Spacer()
TextField("Edit url", text: $vm.url)
Button("Update") {
vm.save()
}
}
}
You create a sheet for each item in ForEach, and only one sheet can be represented at a time. So showEditView shows the sheet related to the first view in the hierarchy and it in turn captures the first item in your list.
Instead you can you use sheet which accepts item binding: sheet is presented when the item is not nil. You only need one such sheet, so add it to your list, not to each item.
When you finish editing, set selectedBookmark to nil to hide the sheet.
Full working example:
struct Bookmark: Identifiable {
let id: Int
}
struct ContentView: View {
#State private var selectedBookmark: Bookmark?
let items = (0..<10).map { Bookmark(id: $0) }
var body: some View {
ForEach(items) { myBookmark in
HStack {
Text(String(describing: myBookmark))
Button {
selectedBookmark = myBookmark
} label: {
Image(systemName: "pencil").foregroundColor(.red)
}
}
}
.sheet(item: $selectedBookmark) { selectedBookmark in
let _ = print(String(describing: selectedBookmark))
Text(String(describing: selectedBookmark))
}
}
}

How to set up Binding of computed value inside class (SwiftUI)

In my Model I have an array of Items, and a computed property proxy which using set{} and get{} to set and return currently selected item inside array and works as shortcut. Setting item's value manually as model.proxy?.value = 10 works, but can't figure out how to Bind this value to a component using $.
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 1), Item(value: 2), Item(value: 3)]
var proxy: Item? {
get {
return items[1]
}
set {
items[1] = newValue!
}
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
VStack {
Text("Value: \(model.proxy!.value)")
Button(action: {model.proxy?.value = 123}, label: {Text("123")}) // method 1: this works fine
SubView(value: $model.proxy.value) // method 2: binding won't work
}.padding()
}
}
struct SubView <B:BinaryFloatingPoint> : View {
#Binding var value: B
var body: some View {
Button( action: {value = 100}, label: {Text("1")})
}
}
Is there a way to modify proxy so it would be modifiable and bindable so both methods would be available?
Thanks!
Day 2: Binding
Thanks to George, I have managed to set up Binding, but the desired binding with SubView still won't work. Here is the code:
import SwiftUI
struct Item {
var value: Double
}
class Model: ObservableObject {
#Published var items: [Item] = [Item(value: 0), Item(value: 0), Item(value: 0)]
var proxy: Binding <Item?> {
Binding <Item?> (
get: { self.items[1] },
set: { self.items[1] = $0! }
)
}
}
struct ContentView: View {
#StateObject var model = Model()
#State var myval: Double = 10
var body: some View {
VStack {
Text("Value: \(model.proxy.wrappedValue!.value)")
Button(action: {model.proxy.wrappedValue?.value = 555}, label: {Text("555")})
SubView(value: model.proxy.value) // this still wont work
}.padding()
}
}
struct SubView <T:BinaryFloatingPoint> : View {
#Binding var value: T
var body: some View {
Button( action: {value = 100}, label: {Text("B 100")})
}
}
Create a Binding instead.
Example:
var proxy: Binding<Item?> {
Binding<Item?>(
get: { items[1] },
set: { items[1] = $0! }
)
}

SwiftUI ForEach over #State dynamic array [duplicate]

SwiftUI seems to have a rather annoying limitation that makes it hard to create a List or a ForEach while getting a binding to each element to pass to child views.
The most often suggested approach I've seen is to iterate over indices, and get the binding with $arr[index] (in fact, something similar was suggested by Apple when they removed Binding's conformance to Collection):
#State var arr: [Bool] = [true, true, false]
var body: some View {
List(arr.indices, id: \.self) { index in
Toggle(isOn: self.$arr[index], label: { Text("\(idx)") } )
}
}
That works until the array changes in size, and then it crashes with index out of range run-time error.
Here's an example that will crash:
class ViewModel: ObservableObject {
#Published var arr: [Bool] = [true, true, false]
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.arr = []
}
}
}
struct ContentView: View {
#ObservedObject var vm: ViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self) { idx in
Toggle(isOn: self.$vm.arr[idx], label: { Text("\(idx)") } )
}
}
}
What's the right way to handle deletion from a List, while still maintaining the ability to modify elements of it with a Binding?
Using insights from #pawello2222 and #Asperi, I came up with an approach that I think works well, without being overly nasty (still kinda hacky).
I wanted to make the approach more general than just for the simplified example in the question, and also not one that breaks separation of concerns.
So, I created a new wrapper view that creates a binding to an array element inside itself (which seems to fix the state invalidation/update ordering as per #pawello2222's observation), and passes the binding as a parameter to the content closure.
I initially expected to be needing to do safety checks on the index, but turns out it wasn't required for this problem.
struct Safe<T: RandomAccessCollection & MutableCollection, C: View>: View {
typealias BoundElement = Binding<T.Element>
private let binding: BoundElement
private let content: (BoundElement) -> C
init(_ binding: Binding<T>, index: T.Index, #ViewBuilder content: #escaping (BoundElement) -> C) {
self.content = content
self.binding = .init(get: { binding.wrappedValue[index] },
set: { binding.wrappedValue[index] = $0 })
}
var body: some View {
content(binding)
}
}
Usage is:
#ObservedObject var vm: ViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self) { index in
Safe(self.$vm.arr, index: index) { binding in
Toggle("", isOn: binding)
Divider()
Text(binding.wrappedValue ? "on" : "off")
}
}
}
It looks like your Toggle is refreshed before the List (possibly a bug, fixed in SwiftUI 2.0).
You can extract your row to another view and check if the index still exists.
struct ContentView: View {
#ObservedObject var vm: ViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self) { index in
ToggleView(vm: self.vm, index: index)
}
}
}
struct ToggleView: View {
#ObservedObject var vm: ViewModel
let index: Int
#ViewBuilder
var body: some View {
if index < vm.arr.count {
Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
}
}
}
This way the ToggleView will be refreshed after the List.
If you do the same but inside the ContentView it will still crash:
ContentView {
...
#ViewBuilder
func toggleView(forIndex index: Int) -> some View {
if index < vm.arr.count {
Toggle(isOn: $vm.arr[index], label: { Text("\(vm.arr[index].description)") })
}
}
}
SwiftUI 2.0
As tested with Xcode 12 / iOS 14 - crash not reproducible
SwiftUI 1.0+
Crash happens due to dangling bindings to removed elements (presumably `cause of bad invalidation/update order).
Here is a safe workaround. Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
#ObservedObject var vm: ToggleViewModel = .init()
var body: some View {
List(vm.arr.indices, id: \.self, rowContent: row(for:))
}
// helper function to have possibility to generate & inject proxy binding
private func row(for idx: Int) -> some View {
let isOn = Binding(
get: {
// safe getter with bounds validation
idx < self.vm.arr.count ? self.vm.arr[idx] : false
},
set: { self.vm.arr[idx] = $0 }
)
return Toggle(isOn: isOn, label: { Text("\(idx)") } )
}
}
If anyone interested I combined the Safe solution by New dev with a ForEach:
struct ForEachSafe<T: RandomAccessCollection & MutableCollection, C: View>: View where T.Index: Hashable {
private let bindingArray: Binding<T>
private let array: T
private let content: (Binding<T.Element>) -> C
init(_ bindingArray: Binding<T>, _ array: T, #ViewBuilder content: #escaping (Binding<T.Element>) -> C) {
self.bindingArray = bindingArray
self.array = array
self.content = content
}
var body: some View {
ForEach(array.indices, id: \.self) { index in
Safe(bindingArray, index: index) {
content($0)
}
}
}
}

SwiftUI ObservedObject in View has two references (instances) BUG

I do not why but I have very frustrating bug in my SwiftUI view.
This view has reference to ViewModel object. But this View is created multiple times on screen appear, and at the end the single View have multiple references to ViewModel object.
I reference this view model object in custom Binding setter/getter or in closure. But object references in Binding and in closure are totally different. This causes many problems with proper View refreshing or saving changes.
struct DealDetailsStagePicker : View {
// MARK: - Observed
#ObservedObject var viewModel: DealDetailsStageViewModel
// MARK: - State
/// TODO: It is workaround as viewModel.dealStageId doesn't work correctly
/// viewModel object is instantiated several times and pickerBinding and onDone
/// closure has different references to viewModel object
/// so updating dealStageId via pickerBinding refreshes it in different viewModel
/// instance than onDone closure executed changeDealStage() method (where dealStageId
/// property stays with initial or nil value.
#State var dealStageId: String? = nil
// MARK: - Binding
#Binding private var showPicker: Bool
// MARK: - Properties
let deal : Deal
// MARK: - Init
init(deal: Deal, showPicker: Binding<Bool>) {
self.deal = deal
self._showPicker = showPicker
self.viewModel = DealDetailsStageViewModel(dealId: deal.id!)
}
var body: some View {
let pickerBinding = Binding<String>(get: {
if self.viewModel.dealStageId == nil {
self.viewModel.dealStageId = self.dealStage?.id ?? ""
}
return self.viewModel.dealStageId!
}, set: { id in
self.viewModel.dealStageId = id //THIS viewModel is reference to object 0x8784783
self.dealStageId = id
})
return VStack(alignment: .leading, spacing: 4) {
Text("Stage".uppercased())
Button(action: {
self.showPicker = true
}) {
HStack {
Text("\(deal.status ?? "")")
Image(systemName: "chevron.down")
}
.contentShape(Rectangle())
}
}
.buttonStyle(BorderlessButtonStyle())
.adaptivePicker(isPresented: $showPicker, selection: pickerBinding, popoverSize: CGSize(width: 400, height: 200), popoverArrowDirection: .up, onDone: {
// save change
self.viewModel.changeDealStage(self.dealStages, self.dealStageId) // THIS viewModel references 0x92392983
}) {
ForEach(self.dealStages, id: \.id) { stage in
Text(stage.name)
.foregroundColor(Color("Black"))
}
}
}
}
I am experiencing this problem in multiple places writing SwiftUI code.
I have several workarounds:
1) as you can see here I use additional #State var to store dealStageId and pass it to viewModale.changeDealStage() instead of updating it on viewModal
2) in other places I am using Wrapper View around such view, then add #State var viewModel: SomeViewModel, then pass this viewModel and assign to #ObservedObject.
But this errors happens randomly depending on placement this View as Subview of other Views. Sometimes it works, sometime it does not work.
It is very od that SINGLE view can have references to multiple view models even if it is instantiated multiple times.
Maybe the problem is with closure as it keeps reference to first ViewModel instance and then this closure is not refreshed in adaptivePicker view modifier?
Workarounds around this issue needs many debugging and boilerplate code to write!
Anyone can help what I am doing wrong or what is wrong with SwiftUI/ObservableObject?
UPDATE
Here is the usage of this View:
private func makeDealHeader() -> some View {
VStack(spacing: 10) {
Spacer()
VStack(spacing: 4) {
Text(self.deal?.name ?? "")
Text(NumberFormatter.price.string(from: NSNumber(value: Double(self.deal?.amount ?? 0)/100.0))!)
}.frame(width: UIScreen.main.bounds.width*0.667)
HStack {
if deal != nil {
DealDetailsStagePicker(deal: self.deal!, showPicker: self.$showStagePicker)
}
Spacer(minLength: 24)
if deal != nil {
DealDetailsClientPicker(deal: self.deal!, showPicker: self.$showClientPicker)
}
}
.padding(.horizontal, 24)
self.makeDealIcons()
Spacer()
}
.frame(maxWidth: .infinity)
.listRowInsets(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0))
}
var body: some View {
ZStack {
Color("White").edgesIgnoringSafeArea(.all)
VStack {
self.makeNavigationLink()
List {
self.makeDealHeader()
Section(header: self.makeSegmentedControl()) {
self.makeSection()
}
}
....
UPDATE 2
Here is adaptivePicker
extension View {
func adaptivePicker<Data, ID, Content>(isPresented: Binding<Bool>, selection: Binding<ID>, popoverSize: CGSize? = nil, popoverArrowDirection: UIPopoverArrowDirection = .any, onDone: (() -> Void)? = nil, #ViewBuilder content: #escaping () -> ForEach<Data, ID, Content>) -> some View where Data : RandomAccessCollection, ID: Hashable, Content: View {
self.modifier(AdaptivePicker2(isPresented: isPresented, selection: selection, popoverSize: popoverSize, popoverArrowDirection: popoverArrowDirection, onDone: onDone, content: content))
}
and here is AdaptivePicker2 view modifier implementation
struct AdaptivePicker2<Data, ID, RowContent> : ViewModifier, OrientationAdjustable where Data : RandomAccessCollection, ID: Hashable , RowContent: View {
// MARK: - Environment
#Environment(\.verticalSizeClass) var _verticalSizeClass
var verticalSizeClass: UserInterfaceSizeClass? {
_verticalSizeClass
}
// MARK: - Binding
private var isPresented: Binding<Bool>
private var selection: Binding<ID>
// MARK: - State
#State private var showPicker : Bool = false
// MARK: - Actions
private let onDone: (() -> Void)?
// MARK: - Properties
private let popoverSize: CGSize?
private let popoverArrowDirection: UIPopoverArrowDirection
private let pickerContent: () -> ForEach<Data, ID, RowContent>
// MARK: - Init
init(isPresented: Binding<Bool>, selection: Binding<ID>, popoverSize: CGSize? = nil, popoverArrowDirection: UIPopoverArrowDirection = .any, onDone: (() -> Void)? = nil, #ViewBuilder content: #escaping () -> ForEach<Data, ID, RowContent>) {
self.isPresented = isPresented
self.selection = selection
self.popoverSize = popoverSize
self.popoverArrowDirection = popoverArrowDirection
self.onDone = onDone
self.pickerContent = content
}
var pickerView: some View {
Picker("Select State", selection: self.selection) {
self.pickerContent()
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
func body(content: Content) -> some View {
let isShowingBinding = Binding<Bool>(get: {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
withAnimation {
self.showPicker = self.isPresented.wrappedValue
}
}
return self.isPresented.wrappedValue
}, set: {
self.isPresented.wrappedValue = $0
})
let popoverBinding = Binding<Bool>(get: {
self.isPresented.wrappedValue
}, set: {
self.onDone?()
self.isPresented.wrappedValue = $0
})
return Group {
if DeviceType.IS_ANY_IPAD {
if self.popoverSize != nil {
content.presentPopover(isShowing: popoverBinding, popoverSize: popoverSize, arrowDirection: popoverArrowDirection) { self.pickerView }
} else {
content.popover(isPresented: popoverBinding) { self.pickerView }
}
} else {
content.present(isShowing: isShowingBinding) {
ZStack {
Color("Dim")
.opacity(0.25)
.transition(.opacity)
.onTapGesture {
self.isPresented.wrappedValue = false
self.onDone?()
}
VStack {
Spacer()
// TEST: Text("Show Picker: \(self.showPicker ? "True" : "False")")
if self.showPicker {
VStack {
Divider().background(Color.white)
.shadow(color: Color("Dim"), radius: 4)
HStack {
Spacer()
Button("Done") {
print("Tapped picker done button!")
self.isPresented.wrappedValue = false
self.onDone?()
}
.foregroundColor(Color("Accent"))
.padding(.trailing, 16)
}
self.pickerView
.frame(height: self.isLandscape ? 120 : nil)
}
.background(Color.white)
.transition(.move(edge: .bottom))
.animation(.easeInOut(duration: 0.35))
}
}
}
.edgesIgnoringSafeArea(.all)
}
}
}
}
}
It seems new #StateObject from iOS 14 will solve this issue in SwiftUI.

List selection as Set<String> - how to use?

Am playing around with SwiftUI and am obviously not getting it.
Basic example which works and is just displaying the selected name.
struct ContentView: View {
let names = ["Joe", "Jim", "Paul"]
#State var selectedName = Set<String>()
var body: some View {
VStack {
List(names, id: \.self, selection: $selectedName) { name in
Text(name)
}
if !selectedName.isEmpty {
Text(selectedName.first!) // <-- this line
}
}
}
}
What I want is a textfield where that name can be changed. Tried many ways but getting another error every time.
TextField("Name", text: $selectedName)
Gives this error: Cannot convert value of type 'Binding<Set<String>>' to expected argument type 'Binding<String>'
TextField("Name", text: $selectedName.first!)
Cannot force unwrap value of non-optional type 'Binding<((String) throws -> Bool) throws -> String?>'
How would I do this?
You may make a binding by yourself:
TextField("Name", text: Binding<String>(get: {self.selectedName.first!}, set: { _ in}) )
Obviously you can't pass Binding<Set<String>> to Binding<String>. Here gives you an idea or solution to change selectedName variable using TextField:
I added a new variable which is Binding<String>. Then I change the selectedName inside the TextField's onCommit closure.
struct ContentView: View {
let names = ["Joe", "Jim", "Paul"]
#State var selectedName = Set<String>()
#State var textFieldName = ""
var body: some View {
VStack {
List(names, id: \.self, selection: $selectedName) { name in
Text(name)
}
if !selectedName.isEmpty {
Text(selectedName.first!)
}
Text(textFieldName)
TextField("Name", text: $textFieldName, onEditingChanged: { (Bool) in
//onEditing
}) {
//onCommit
self.selectedName.insert(self.textFieldName)
}
}
}
}
Ok, here is my alternate if I'd needed to edit some value of names having in one screen and list and edit field and make them all synchronised and not confuse each other.
Here is full testable module (tested on Xcode 11.2/iOS 13.2). As I tested it for iOS there are API requirement for put List into EditMode to process selection, so this included.
struct TestChangeSelectedItem: View {
#State var names = ["Joe", "Jim", "Paul"] // made modifiable
#State var selectedName: String? = nil // only one can be edited, so single selection
#State var editMode: EditMode = .active // Tested for iOS, so it is needed
var body: some View {
VStack {
List(selection: $selectedName) {
ForEach(names, id: \.self) { name in
Text(name)
}
}
.environment(\.editMode, $editMode) // Tested for iOS, so it is needed
if selectedName != nil {
Divider()
Text(selectedName!) // Left to see updates for selection
editor(for: selectedName!) // Separated to make more clear
}
}
}
private func editor(for selection: String) -> some View {
let index = names.firstIndex(of: selection)!
var editedValue = selection // local to avoid cycling in refresh
return HStack {
Text("New name:")
TextField("Name", text: Binding<String>(get: { editedValue }, set: { editedValue = $0}), onCommit: {
self.names[index] = editedValue
self.selectedName = editedValue
})
}
}
}
struct TestChangeSelectedItem_Previews: PreviewProvider {
static var previews: some View {
TestChangeSelectedItem()
}
}