How to select an item conditionally in a ForEach element? - swiftui

In SwiftUI I want to show the favourite items only and I'm trying to do this with an if statement within the ForEach-element. See the code:
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(self.buildings, id: \.self) { building in
if(!self.showFavoritesOnly || building.favourite) {
Text("Blah")
}
}
}
}
}
However I getting a 'Unable to infer complex closure return type; add explicit type to disambiguate' error.
How can I select an item conditionally in a ForEach element?

Using a filter is a working solution
var body: some View {
NavigationView {
List {
Toggle(isOn: $showFavoritesOnly) {
Text("Favorites only")
}
ForEach(self.buildings.filter({b in self.showFavoritesOnly || b.favourite}), id: \.self) { building in
Text("Blah")
}
}
}
Still, I wonder how could I have fixed it using an if-statement

Related

Multiple List Sections in NavitagionSplitView Sidebar Only Shows a Single Item

I'm having a problem displaying all the list elements inside a list when there are multiple sections in the sidebar.
Here is an example code.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationSplitView {
List {
Section("Test 1") {
List(0...20, id: \.self) { i in
Text("\(i)")
}
}
Section("Test 2") {
List(21...50, id: \.self) { i in
Text("\(i)")
}
}
}
.listStyle(.sidebar)
} detail: {
Text("Details")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When I run this code, I get two sections; however the list under them are only showing a single item instead of all.
What am I doing wrong?
You are facing this issue because you are trying to create a nested List inside the section of List. Inside the section of the list, you need to use ForEach to loop through your data instead of creating a nested List.
List {
Section("Test 1") {
ForEach(0...20, id: \.self) { i in
Text("\(i)")
}
}
Section("Test 2") {
ForEach(21...50, id: \.self) { i in
Text("\(i)")
}
}
}
.listStyle(.sidebar)

Display a View when rest of content is empty

I have a view body with logic such as this:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
This works fine to conditionally show elements vertically stacked. However, if none of the conditions are satisfied, the VStack is empty and my UI looks broken. I would like to show a placeholder instead. My current solution is to add a manual check at the end on !someCondition && !anotherCondition && !thirdCondition:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
if !someCondition && !anotherCondition && !thirdCondition { // 👈
Text("Please select an element.")
}
}
}
However, this is difficult to keep the condition in sync with the content above. I was hoping there was some sort of view modifier I could use such as:
var body: some View {
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}.emptyState { // 👈
Text("Please select an element.")
}
}
The closest thing I could find is this tutorial, but that requires passing in the condition as well.
Is there a way to build a view modifier like this emptyState which doesn't require duplicating the condition logic?
I was thinking I could use a ZStack for this:
var body: some View {
ZStack { // 👈
// empty state text
Text("Please select an element.")
VStack {
if someCondition {
SomeView()
}
if anotherCondition {
AnotherView()
AnotherView()
}
if thirdCondition {
SomeView()
AnotherView()
}
}
}
}
... but then I run into a different issue where if I'm showing real content (e.g. SomeView()) but it's not large enough, I could see both SomeView() and the empty state text.
Here's one implementation using GeometryReader & it's named emptyState:
extension View {
func emptyState<Content: View>(#ViewBuilder content: () -> Content) -> some View {
return self.modifier(EmptyStateModifier(placeHolder: content()))
}
}
struct EmptyStateModifier<PlaceHolder: View>: ViewModifier {
#State var isEmpty = false
let placeHolder: PlaceHolder
func body(content: Content) -> some View {
ZStack {
if isEmpty {//Thanks to #Asperi
placeHolder
}
content
.background(
GeometryReader { reader in
Color.clear
.onChange(of: reader.frame(in: .global).size == .zero) { newValue in
isEmpty = reader.frame(in: .global).size == .zero
}
}
)
}
}
}
If it is a long chaining condition, you can handle it with switch{}, then use the benefit of default to display the placeholder when 0 condition is met(stack is empty or no selection)
#State var selected = ""
var body: some View {
VStack {
switch selected {
case "a":
SomeView()
case "b":
AnotherView()
case "c":
ThirdView()
//this default will show up
//when there is no selection
//and when the stack is empty meaning that all the above
//conditions did not meet
default:
Text("Please select an element")
}
}
}
There is no straightforward, officially supported way of telling what the return value of a view builder contains.
It is more sensible to handle this at the model layer than in your view. Each of your conditions are part of your model. These should be wrapped up into a single value type, and you can use the presence or absence of that (or an internal calculated value of that, depending on your requirements) to inform what to put in the stack. For example:
struct Model {
let one: Bool
let two: Bool
let three: Bool
}
struct MyView: View {
let model: Model?
var body: some View {
VStack {
switch model {
case .some(let model):
if model.one {
Text("One")
}
if model.two {
Text("Two")
}
if model.three {
Text("Three")
}
case .none:
Text("Empty")
}
}
}
}
(I'm assuming here that there is no valid Model which doesn't contain any of the values, that would be when it is set to nil)

ForEach cannot use branch

I am using Xcode 11.3.1 with SwiftUI.
This code works right
struct ContentView: View {
var body: some View {
VStack {
ForEach(1...5, id: \.self) { index in
Text("\(index) of coffee.")
}
}
}
}
But the following code gives an error.
why?
struct ContentView: View {
var body: some View {
VStack {
ForEach(1...5, id: \.self) { index in
if index == 1 {
Text("Cup of coffee.")
} else {
Text("\(index) cups of coffee")
}
}
}
}
}
The error message is:
Unable to infer complex closure return type; add explicit type to
disambiguate.
Because view builder expected one return of one type view, but condition does not generate opaque return. To solve - just embed condition in Group
ForEach(1...5, id: \.self) { index in
Group {
if index == 1 {
Text("Cup of coffee.")
} else {
Text("\(index) cups of coffee")
}
}
}

SwiftUI dynamic List with #Binding controls

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")
}
}
}

Button and List interaction modifications

I am trying to remove the "sectionStyle" for my List rows. In order to achieve a tableview-like view, you would write the following:
List(items, id: \.id) { item in
ItemRow(with: item)
}
If you want to interpret tap events on each row, you would wrap the ItemRow into a Button:
List(items, id: \.id) { item in
Button(action: rowTapped) {
ItemRow(item: item)
}
}
The interesting part is that when the List knows about the Button, it will automatically give a selection animation like tableView's selectionStyle. For my application, I'd like to not have that. Is there a way to set the "selectionStyle" for the List to "none"?
There are a number of List behaviours, that I haven't been able to alter. It's quite easy to build your own list though if you have specific needs different than the system List view. Here's a simple example that I've used.
struct MyListView:View {
var body: some View {
ScrollView {
ForEach(items, id: \.id) { item in
VStack {
Button(action: self.rowTapped) {
ItemRow(item: item)
}
Divider()
}
}
}.padding()
}
func rowTapped() {
print("rowTapped")
}
}
struct ItemRow:View {
var item:Item
var body: some View {
HStack {
Text(item.name)
Spacer() // force text to left justify
}
}
}