I'm probably missing something but why does this work fine for a Picker but not for a List? I don't see why it is complaining about a missing parameter type.
struct ContentView: View {
enum FooBar: CaseIterable, Identifiable {
public var id : String { UUID().uuidString }
case foo
case bar
case buzz
case bizz
}
#State var selectedFooBar: FooBar = .bar
var body: some View {
VStack {
Picker("Select", selection: $selectedFooBar) {
ForEach(FooBar.allCases) { item in
Text(self.string(from: item)).tag(item)
}
}
List(FooBar.allCases, selection: $selectedFooBar) { item in
Text(self.string(from: item)).tag(item)
}
Text("You selected: \(self.string(from: selectedFooBar))")
}
}
private func string(from item: FooBar) -> String {
var str = ""
switch item {
case .foo:
str = "Foo"
case .bar:
str = "Bar"
case .buzz:
str = "Buzz"
case .bizz:
str = "Bizz"
}
return str
}
}
I tried to find explanations and examples but couldn't find anything.
If you extract the list out to it's own variable outside of body:
var list: some View {
List(FooBar.allCases, selection: $selectedFooBar) { item in
Text(self.string(from: item)).tag(item)
}
}
...You get this error: "Cannot invoke initializer for type 'List<_, _>' with an argument list of type '([ContentView.FooBar], selection: Binding<ContentView.FooBar>, #escaping (ContentView.FooBar) -> some View)'"
It looks like you're trying to use the same initializer from Picker on List, but they are quite different. Maybe this is what you're looking for:
List {
ForEach(FooBar.allCases) { item in
Text(self.string(from: item)).tag(item)
}
}
The selected item has the right type FooBar for the picker but the wrong type for the list. if you used Set<String> then the compiler will not complain
#State var selectedFooBar: FooBar = .bar
#State var selectedItems: Set<String>
var body: some View {
VStack {
Picker("Select", selection: $selectedFooBar) {
ForEach(FooBar.allCases) { item in
Text(self.string1(from: item)).tag(item)
}
}
List(FooBar.allCases, selection: $selectedItems) { item in
Text(self.string1(from: item))
}
Text("You selected: \(self.string1(from: selectedFooBar))")
}
Note that the list will need to be in "Edit mode" to use selection.
Related
Basically what i want to do is if you press the Button then entries should get a new CEntry. It would be nice if someone could help me out. Thanks!
struct AView: View {
var entries = [CEntries]()
var body: some View {
ZStack {
VStack {
Text("Hello")
ScrollView{
ForEach(entries) { entry in
VStack{
Text(entry.string1)
Text(entry.string2)
}
}
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp")) <-- Error
}) {
someButtonStyle()
}
}
}
}
The Class CEntries
class CEntries: ObservableObject, Identifiable{
#Published var string1 = ""
#Published var string2 = ""
init(string1: String, string2: String) {
self.string1 = string1
self.string2 = string2
}
}
Views are immutable in SwiftUI. You can only mutate their state, which is done by changing the properties that have a #State property wrapper:
#State var entries: [CEntries] = []
However, while you could do that, in your case CEntries is a class - i.e. a reference type - so while you could detect changes in the array of entries - additions and removals of elements, you won't be able to detect changes in the elements themselves, for example when .string1 property is updated.
And it doesn't help that it's an ObservableObject.
Instead, change CEntries to be a struct - a value type, so that if it changes, the value itself will change:
struct CEntries: Identifiable {
var id: UUID = .init()
var string1 = ""
var string2 = ""
}
struct AView: View {
#State var entries = [CEntries]()
var body: some View {
VStack() {
ForEach(entries) { entry in
VStack {
Text(entry.string1)
Text(entry.string2)
}
}
Button(action: {
self.entries.append(CEntries(string1: "he", string2: "lp"))
}) {
someButtonStyle()
}
}
}
}
For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.
Normally I can display a list of items like this in SwiftUI:
enum Fruit {
case apple
case orange
case banana
}
struct FruitView: View {
#State private var fruit = Fruit.apple
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit)
}
}
}
}
This works perfectly, allowing me to select whichever fruit I want. If I want to switch fruit to be nullable (aka an optional), though, it causes problems:
struct FruitView: View {
#State private var fruit: Fruit?
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit)
}
}
}
}
The selected fruit name is no longer displayed on the first screen, and no matter what selection item I choose, it doesn't update the fruit value.
How do I use Picker with an optional type?
The tag must match the exact data type as the binding is wrapping. In this case the data type provided to tag is Fruit but the data type of $fruit.wrappedValue is Fruit?. You can fix this by casting the datatype in the tag method:
struct FruitView: View {
#State private var fruit: Fruit?
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit as Fruit?)
}
}
}
}
Bonus: If you want custom text for nil (instead of just blank), and want the user to be allowed to select nil (Note: it's either all or nothing here), you can include an item for nil:
struct FruitView: View {
#State private var fruit: Fruit?
var body: some View {
Picker(selection: $fruit, label: Text("Fruit")) {
Text("No fruit").tag(nil as Fruit?)
ForEach(Fruit.allCases) { fruit in
Text(fruit.rawValue).tag(fruit as Fruit?)
}
}
}
}
Don't forget to cast the nil value as well.
I made a public repo here with Senseful's solution:
https://github.com/andrewthedina/SwiftUIPickerWithOptionalSelection
EDIT: Thank you for the comments regarding posting links. Here is the code which answers the question. Copy/paste will do the trick, or clone the repo from the link.
import SwiftUI
struct ContentView: View {
#State private var selectionOne: String? = nil
#State private var selectionTwo: String? = nil
let items = ["Item A", "Item B", "Item C"]
var body: some View {
NavigationView {
Form {
// MARK: - Option 1: NIL by SELECTION
Picker(selection: $selectionOne, label: Text("Picker with option to select nil item [none]")) {
Text("[none]").tag(nil as String?)
.foregroundColor(.red)
ForEach(items, id: \.self) { item in
Text(item).tag(item as String?)
// Tags must be cast to same type as Picker selection
}
}
// MARK: - Option 2: NIL by BUTTON ACTION
Picker(selection: $selectionTwo, label: Text("Picker with Button that removes selection")) {
ForEach(items, id: \.self) { item in
Text(item).tag(item as String?)
// Tags must be cast to same type as Picker selection
}
}
if selectionTwo != nil { // "Remove item" button only appears if selection is not nil
Button("Remove item") {
self.selectionTwo = nil
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I actually prefer #Senseful's solution for a point solution, but for posterity: you could also create a wrapper enum, which if you have a ton of entity types in your app scales quite nicely via protocol extensions.
// utility constraint to ensure a default id can be produced
protocol EmptyInitializable {
init()
}
// primary constraint on PickerValue wrapper
protocol Pickable {
associatedtype Element: Identifiable where Element.ID: EmptyInitializable
}
// wrapper to hide optionality
enum PickerValue<Element>: Pickable where Element: Identifiable, Element.ID: EmptyInitializable {
case none
case some(Element)
}
// hashable & equtable on the wrapper
extension PickerValue: Hashable & Equatable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
}
// common identifiable types
extension String: EmptyInitializable {}
extension Int: EmptyInitializable {}
extension UInt: EmptyInitializable {}
extension UInt8: EmptyInitializable {}
extension UInt16: EmptyInitializable {}
extension UInt32: EmptyInitializable {}
extension UInt64: EmptyInitializable {}
extension UUID: EmptyInitializable {}
// id producer on wrapper
extension PickerValue: Identifiable {
var id: Element.ID {
switch self {
case .some(let e):
return e.id
case .none:
return Element.ID()
}
}
}
// utility extensions on Array to wrap into PickerValues
extension Array where Element: Identifiable, Element.ID: EmptyInitializable {
var pickable: Array<PickerValue<Element>> {
map { .some($0) }
}
var optionalPickable: Array<PickerValue<Element>> {
[.none] + pickable
}
}
// benefit of wrapping with protocols is that item views can be common
// across data sets. (Here TitleComponent { var title: String { get }})
extension PickerValue where Element: TitleComponent {
#ViewBuilder
var itemView: some View {
Group {
switch self {
case .some(let e):
Text(e.title)
case .none:
Text("None")
.italic()
.foregroundColor(.accentColor)
}
}
.tag(self)
}
}
Usage is then quite tight:
Picker(selection: $task.job, label: Text("Job")) {
ForEach(Model.shared.jobs.optionalPickable) { p in
p.itemView
}
}
I learned almost all I know about SwiftUI Bindings (with Core Data) by reading this blog by Jim Dovey. The remainder is a combination of some research and quite a few hours of making mistakes.
So when I use Jim's technique to create Extensions on SwiftUI Binding then we end up with something like this...
public extension Binding where Value: Equatable {
init(_ source: Binding<Value>, deselectTo value: Value) {
self.init(get: { source.wrappedValue },
set: { source.wrappedValue = $0 == source.wrappedValue ? value : $0 }
)
}
}
Which can then be used throughout your code like this...
Picker("country", selection: Binding($selection, deselectTo: nil)) { ... }
OR
Picker("country", selection: Binding($selection, deselectTo: someOtherValue)) { ... }
OR when using .pickerStyle(.segmented)
Picker("country", selection: Binding($selection, deselectTo: -1)) { ... }
which sets the index of the segmented style picker to -1 as per the documentation for UISegmentedControl and selectedSegmentIndex.
The default value is noSegment (no segment selected) until the user
touches a segment. Set this property to -1 to turn off the current
selection.
Why not extending the enum with a default value? If this is not what you are trying to achieve, maybe you can also provide some information, why you want to have it optional.
enum Fruit: String, CaseIterable, Hashable {
case apple = "apple"
case orange = "orange"
case banana = "banana"
case noValue = ""
}
struct ContentView: View {
#State private var fruit = Fruit.noValue
var body: some View {
VStack{
Picker(selection: $fruit, label: Text("Fruit")) {
ForEach(Fruit.allCases, id:\.self) { fruit in
Text(fruit.rawValue)
}
}
Text("Selected Fruit: \(fruit.rawValue)")
}
}
}
How do I build a dynamic list with #Binding-driven controls without having to reference the array manually? It seems obvious but using List or ForEach to iterate through the array give all sorts of strange errors.
struct OrderItem : Identifiable {
let id = UUID()
var label : String
var value : Bool = false
}
struct ContentView: View {
#State var items = [OrderItem(label: "Shirts"),
OrderItem(label: "Pants"),
OrderItem(label: "Socks")]
var body: some View {
NavigationView {
Form {
Section {
List {
Toggle(items[0].label, isOn: $items[0].value)
Toggle(items[1].label, isOn: $items[1].value)
Toggle(items[2].label, isOn: $items[2].value)
}
}
}.navigationBarTitle("Clothing")
}
}
}
This doesn't work:
...
Section {
List($items, id: \.id) { item in
Toggle(item.label, isOn: item.value)
}
}
...
Type '_' has no member 'id'
Nor does:
...
Section {
List($items) { item in
Toggle(item.label, isOn: item.value)
}
}
...
Generic parameter 'SelectionValue' could not be inferred
Try something like
...
Section {
List(items.indices) { index in
Toggle(self.items[index].label, isOn: self.$items[index].value)
}
}
...
While Maki's answer works (in some cases). It is not optimal and it's discouraged by Apple. Instead, they proposed the following solution during WWDC 2021:
Simply pass a binding to your collection into the list, using the
normal dollar sign operator, and SwiftUI will pass back a binding to
each individual element within the closure.
Like this:
struct ContentView: View {
#State var items = [OrderItem(label: "Shirts"),
OrderItem(label: "Pants"),
OrderItem(label: "Socks")]
var body: some View {
NavigationView {
Form {
Section {
List($items) { $item in
Toggle(item.label, isOn: $item.value)
}
}
}.navigationBarTitle("Clothing")
}
}
}
I'm trying to follow along with this chat app tutorial and the syntax for the ForEach function has been updated in SwiftUI. Can you please help me make this list successfully compile using SwiftUI?
import SwiftUI
struct ChatMessage : Hashable {
var message: String
var avatar: String
}
struct ContentView : View {
var messages = [
ChatMessage(message: "Hello world", avatar: "A"),
ChatMessage(message: "Hi", avatar: "B")
]
var body: some View {
List {
ForEach(messages.identified(by: \.self)) {
Text($0.avatar)
Text($0.message)
}
}
}
}
I tried changing the ForEach to the updated syntax:
var body: some View {
List {
ForEach(messages, id: \.self) {
Text($0.avatar)
Text($0.message)
}
}
}
However, I'm receiving an error message:
"Unable to infer complex closure return type; add explicit type to disambiguate"
#dfd already answered you in the comment above. The problem here is not related to the ForEach view (please, note this important thing: ForEach is not a function, is a View and it's completely different from the forEach you are used to in Swift).
The ForEach view takes the elements in the array one by one and build a single view for each of them. In your #ViewBuilder closure (the closure right after the ForEach) you are passing more than one view. You must wrap your two Text in a single view depending on your needs. For example, if you want your texts to be stacked vertically you must do:
struct ChatMessage : Hashable {
var message: String
var avatar: String
}
struct ContentView : View {
var messages = [
ChatMessage(message: "Hello world", avatar: "A"),
ChatMessage(message: "Hi", avatar: "B")
]
var body: some View {
List {
ForEach(messages, id: \.self) {val in
VStack {
Text(val.avatar)
Text(val.message)
}
}
}
}
}
struct ContentView: View {
#State var showFavoritesOnly = true
var body: some View {
NavigationView {
List {
ForEach(landmarkData) { landmark in
if !self.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: UserDetail(landmark: landmark)) {
UsersList(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Users"))
}
}
}