I have an interesting looping and conditional problem that I’m having trouble solving.
struct ForEachLoopTesting: View {
let start = ["a", "b", "c", "a"]
let this = ["a", "b"]
var body: some View {
VStack {
ForEach(start, id:\.self) { test1 in
ForEach(start, id:\.self) { test2 in
if [test1, test2] == this {
Text("true") // this prints twice…how can I get to print only once?
}
}
}
}
}
}
Any ideas on how I can show the first result of the multiple results that meet the condition? Or maybe I'm approaching this wrong and there's a better way?
you could try something like this approach, without using the ForEach loop (because they require the entries to be unique):
struct ForEachLoopTesting: View {
let start = ["a", "b", "c", "a"]
let this = ["a", "b"]
var body: some View {
if startContains(this) {
Text("yes start contains this")
} else {
Text("no start does not contain this")
}
}
func startContains(_ this: [String]) -> Bool {
var result = false
outerLoop: for test1 in start {
for test2 in start {
if ([test1, test2] == this) {
result = true
break outerLoop
}
}
}
return result
}
// alternative
func startContains2(_ this: [String]) -> Bool {
for test in this {
if !start.contains(test) {
return false
}
}
return true
}
}
You should not manage this inside the body of the view. Instead, it could be easier to leave the logic to a supporting function and ask SwiftUI to render the final result on the screen.
The example below separates the logic from the view's body:
struct ForEachLoopTesting: View {
let start = ["a", "b", "c", "a"]
let this = ["a", "b"]
// Supporting function
private var merged: [[String]] {
var merged = [[String]]()
start.forEach { test1 in
start.forEach { test2 in
let item = [test1, test2]
// Check for both: the condition and that
// the pair is not yet included in the array
if item == this && !merged.contains(item) {
merged.append([test1, test2])
}
}
}
return merged
}
var body: some View {
VStack {
// Read and show the final result
ForEach(merged, id:\.self) { item in
HStack {
Text(item[0])
Text(item[1])
Text("true")
}
}
}
}
}
Related
So here is a little piece of code that sums up a problem I cannot figure out atm.
In the code below I add and remove entries to a dictionary keyed by an Enum.
What I would expect is that every time I add an item a new random number is being generated in the Element view and displayed.
What happens is that for every same Ident the same random number shows up - event though the ForEach loop has had a state where that Ident key was not in the dictionary any more. It appears as if ForEach does not purge the #State vars of the Element views that are not present any more, but reuses them when a new entry to the dictionary is added with the same Ident.
Is this expected behavior? What am I doing wrong?
Here is the code:
import Foundation
import SwiftUI
enum Ident:Int, Comparable, CaseIterable {
case one=1, two, three, four
static func < (lhs: Ident, rhs: Ident) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
extension Dictionary where Key == Ident,Value== String {
var asSortedArray:Array<(Ident,Value)> {
Array(self).sorted(by: { $0.key < $1.key })
}
var nextKey:Ident? {
if self.isEmpty {
return .one
}
else {
return Array(Set(Ident.allCases).subtracting(Set(self.keys))).sorted().first
}
}
}
struct ContentView: View {
#State var dictionary:[Ident:String] = [:]
var body: some View {
Form {
Section {
ForEach(dictionary.asSortedArray, id: \.0) { (ident, text) in
Element(dictionary: $dictionary, ident: ident, text: text)
}
}
Section {
Button(action: {
if let nextIdent = dictionary.nextKey {
dictionary[nextIdent] = "Something"
}
}, label: {
Text("Add one")
})
}
}
}
}
struct Element:View {
#Binding var dictionary:[Ident:String]
var ident:Ident
var text:String
#State var random:Int = Int.random(in: 0...1000)
var body: some View {
HStack {
Text(String(ident.rawValue))
Text(String(random))
Button(action: {
dictionary.removeValue(forKey: ident)
}, label: {
Text("Delete me.")
})
Spacer()
}
}
}
I am trying to display a dynamic list of text fields using a ForEach. The following code is working as expected: I can add/remove text fields, and the binding is correct. However, when I move the items in a ObservableObject view model, it does not work anymore and it crashes with an index out of bounds error. Why is that? How can I make it work?
struct ContentView: View {
#State var items = ["A", "B", "C"]
var body: some View {
VStack {
ForEach(items.indices, id: \.self) { index in
FieldView(value: Binding<String>(get: {
items[index]
}, set: { newValue in
items[index] = newValue
})) {
items.remove(at: index)
}
}
Button("Add") {
items.append("")
}
}
}
}
struct FieldView: View {
#Binding var value: String
let onDelete: () -> Void
var body: some View {
HStack {
TextField("item", text: $value)
Button(action: {
onDelete()
}, label: {
Image(systemName: "multiply")
})
}
}
}
The view model I am trying to use:
class ViewModel: Observable {
#Published var items: [String]
}
#ObservedObject var viewModel: ViewModel
I found many questions dealing with the same problem but I could not make one work with my case. Some of them do not mention the TextField, some other are not working (anymore?).
Thanks a lot
By checking the bounds inside the Binding, you can solve the issue:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel = ViewModel(items: ["A", "B", "C"])
var body: some View {
VStack {
ForEach(viewModel.items.indices, id: \.self) { index in
FieldView(value: Binding<String>(get: {
guard index < viewModel.items.count else { return "" } // <- HERE
return viewModel.items[index]
}, set: { newValue in
viewModel.items[index] = newValue
})) {
viewModel.items.remove(at: index)
}
}
Button("Add") {
viewModel.items.append("")
}
}
}
}
It is a SwiftUI bug, similar question to this for example.
I can not perfectly explain what is causing that crash, but I've been able to reproduce the error and it looks like after deleting a field,SwiftUI is still looking for all indices and when it is trying to access the element at a deleted index, it's unable to find it which causes the index out of bounds error.
To fix that, we can write a conditional statement to make sure an element is searched only if its index is included in the collection of indices.
FieldView(value: Binding<String>(get: {
if viewModel.items.indices.contains(index) {
return viewModel.items[index]
} else {
return ""
}
}, set: { newValue in
viewModel.items[index] = newValue
})) {
viewModel.items.remove(at: index)
}
The above solution solves the problem since it makes sure that the element will not be searched when the number of elements (items.count) is not greater than the index.
This is just what I've been able to understand, but something else might be happening under the hood.
*** EDIT 23.20.20 ***
Due to the strange behavior discovered after my original post, I need to completely rephrase my question. I meanwhile re-wrote large parts of my code as well.
The issue:
I run an asynchronous HTTP GET search query, which returns me an Array searchResults, which I store in an ObservedObject FoodDatabaseResults.
struct FoodItemEditor: View {
//...
#ObservedObject var foodDatabaseResults = FoodDatabaseResults()
#State private var activeSheet: FoodItemEditorSheets.State?
//...
var body: some View {
NavigationView {
VStack {
Form {
Section {
HStack {
// Name
TextField(titleKey: "Name", text: $draftFoodItem.name)
// Search and Scan buttons
Button(action: {
if draftFoodItem.name.isEmpty {
self.errorMessage = NSLocalizedString("Search term must not be empty", comment: "")
self.showingAlert = true
} else {
performSearch()
}
}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}.buttonStyle(BorderlessButtonStyle())
//...
}
//...
}
//...
}
}
//...
}
.sheet(item: $activeSheet) {
sheetContent($0)
}
}
private func performSearch() {
UserSettings.shared.foodDatabase.search(for: draftFoodItem.name) { result in
switch result {
case .success(let networkSearchResults):
guard let searchResults = networkSearchResults else {
return
}
DispatchQueue.main.async {
self.foodDatabaseResults.searchResults = searchResults
self.activeSheet = .search
}
case .failure(let error):
debugPrint(error)
}
}
}
#ViewBuilder
private func sheetContent(_ state: FoodItemEditorSheets.State) -> some View {
switch state {
case .search:
FoodSearch(foodDatabaseResults: foodDatabaseResults, draftFoodItem: self.draftFoodItem) // <-- I set a breakpoint here
//...
}
}
}
class FoodDatabaseResults: ObservableObject {
#Published var selectedEntry: FoodDatabaseEntry?
#Published var searchResults: [FoodDatabaseEntry]?
}
I get valid search results in my performSearch function. The DispatchQueue.main.async closure makes sure to perform the update of my #Published var searchResults in the main thread.
I then open a sheet, displaying these search results:
struct FoodSearch: View {
#ObservedObject var foodDatabaseResults: FoodDatabaseResults
#Environment(\.presentationMode) var presentation
//...
var body: some View {
NavigationView {
List {
if foodDatabaseResults.searchResults == nil {
Text("No search results (yet)")
} else {
ForEach(foodDatabaseResults.searchResults!) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
}
.navigationBarTitle("Food Database Search")
.navigationBarItems(leading: Button(action: {
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}) {
Text("Cancel")
}, trailing: Button(action: {
if selectedResult == nil {
//...
} else {
//... Do something with the result
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}
}) {
Text("Select")
})
}
}
}
When I run this on the Simulator, everything works as it should, see https://wolke.rueth.info/index.php/s/KbqETcDtSe4278d
When I run it on a real device with the same iOS version (14.0.1), the FoodSearch view first correctly displays the search result, but is then immediately called a second time with empty (nil) search results. You need to look very closely at the screen cast here and you'll see it displaying the search results for a very short moment before they disappear: https://wolke.rueth.info/index.php/s/9n2DZ88qSB9RWo4
When setting a breakpoint in the marked line in my sheetContent function, the FoodSearch sheet is indeed called twice on the real device, while it's only called once in the Simulator.
I have no idea what is going on here. Hope someone can help. Thanks!
*** ORIGINAL POST ***
I run an HTTP request, which updates a #Published variable searchResults in a DispatchQueue.main.async closure:
class OpenFoodFacts: ObservableObject {
#Published var searchResults = [OpenFoodFactsProduct]()
// ...
func search(for term: String) {
let urlString = "https://\(countrycode)-\(languagecode).openfoodfacts.org/cgi/search.pl?action=process&search_terms=\(term)&sort_by=unique_scans_n&json=true"
let request = prepareRequest(urlString)
let session = URLSession.shared
session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
guard error == nil else {
debugPrint(error!.localizedDescription)
return
}
if let data = data {
do {
let openFoodFactsSearchResult = try JSONDecoder().decode(OpenFoodFactsSearchResult.self, from: data)
guard let products = openFoodFactsSearchResult.products else {
throw FoodDatabaseError.noSearchResults
}
DispatchQueue.main.async {
self.searchResults = products
self.objectWillChange.send()
}
} catch {
debugPrint(error.localizedDescription)
}
}
}).resume()
}
struct OpenFoodFactsSearchResult: Decodable {
var products: [OpenFoodFactsProduct]?
enum CodingKeys: String, CodingKey {
case products
}
}
struct OpenFoodFactsProduct: Decodable, Hashable, Identifiable {
var id = UUID()
// ...
enum CodingKeys: String, CodingKey, CaseIterable {
// ...
}
// ...
}
I call the search function from my view:
struct FoodSearch: View {
#ObservedObject var foodDatabase: OpenFoodFacts
// ...
var body: some View {
NavigationView {
List {
ForEach(foodDatabase.searchResults) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
// ...
}
.onAppear(perform: search)
}
private func search() {
foodDatabase.search(for: draftFoodItem.name)
}
}
My ForEach list will never update, although I have a valid searchResult set in my OpenFoodFacts observable object and also sent an objectWillChange signal. Any idea what I'm missing?
Funny enough: On the simulator it works as expected:
https://wolke.rueth.info/index.php/s/oy4Xf6C5cgrEZdK
On a real device not:
https://wolke.rueth.info/index.php/s/TQz8HnFyjLKtN74
I'm using Xcode 12 beta and trying to create a view where items from a left list can be dragged onto a right list and dropped there.
This crashes in the following situations:
The list is empty.
The list is not empty, but the item is dragged behind the last list element, after dragging it onto other list elements first. The crash already appears while the item is dragged, not when it is dropped (i.e., the .onInsert is not called yet).
The crash message tells:
SwiftUI`generic specialization <SwiftUI._ViewList_ID.Views> of (extension in Swift):Swift.RandomAccessCollection< where A.Index: Swift.Strideable, A.Indices == Swift.Range<A.Index>, A.Index.Stride == Swift.Int>.index(after: A.Index) -> A.Index:
Are there any ideas why this happens and how it can be avoided?
The left list code:
struct AvailableBuildingBricksView: View {
#StateObject var buildingBricksProvider: BuildingBricksProvider = BuildingBricksProvider()
var body: some View {
List {
ForEach(buildingBricksProvider.availableBuildingBricks) { buildingBrickItem in
Text(buildingBrickItem.title)
.onDrag {
self.provider(buildingBrickItem: buildingBrickItem)
}
}
}
}
private func provider(buildingBrickItem: BuildingBrickItem) -> NSItemProvider {
let image = UIImage(systemName: buildingBrickItem.systemImageName) ?? UIImage()
let provider = NSItemProvider(object: image)
provider.suggestedName = buildingBrickItem.title
return provider
}
}
final class BuildingBricksProvider: ObservableObject {
#Published var availableBuildingBricks: [BuildingBrickItem] = []
init() {
self.availableBuildingBricks = [
TopBrick.personalData,
TopBrick.education,
TopBrick.work,
TopBrick.overviews
].map({ return BuildingBrickItem(title: $0.title,
systemImageName: "stop") })
}
}
struct BuildingBrickItem: Identifiable {
var id: UUID = UUID()
var title: String
var systemImageName: String
}
The right list code:
struct DocumentStructureView: View {
#StateObject var documentStructureProvider: DocumentStructureProvider = DocumentStructureProvider()
var body: some View {
List {
ForEach(documentStructureProvider.documentSections) { section in
Text(section.title)
}
.onInsert(of: ["public.image"]) {
self.insertSection(position: $0,
itemProviders: $1,
top: true)
}
}
}
func insertSection(position: Int, itemProviders: [NSItemProvider], top: Bool) {
for item in itemProviders.reversed() {
item.loadObject(ofClass: UIImage.self) { image, _ in
if let _ = image as? UIImage {
DispatchQueue.main.async {
let section = DocumentSectionItem(title: item.suggestedName ?? "Unknown")
self.documentStructureProvider.insert(section: section, at: position)
}
}
}
}
}
}
final class DocumentStructureProvider: ObservableObject {
#Published var documentSections: [DocumentSectionItem] = []
init() {
documentSections = [
DocumentSectionItem(title: "Dummy")
]
}
func insert(section: DocumentSectionItem, at position: Int) {
if documentSections.count == 0 {
documentSections.append(section)
return
}
documentSections.insert(section, at: position)
}
}
struct DocumentSectionItem: Identifiable {
var id: UUID = UUID()
var title: String
}
Well, I succeeded to make the problem reproducable, code below.
Steps to reproduce:
Drag "A" on "1" as first item on the right.
Drag another "A" on "1", hold it dragged, draw it slowly down after "5" -> crash.
The drop function is not called before the crash.
struct ContentView: View {
var body: some View {
HStack {
LeftList()
Divider()
RightList()
}
}
}
import SwiftUI
import UniformTypeIdentifiers
struct LeftList: View {
var list: [String] = ["A", "B", "C", "D", "E"]
var body: some View {
List(list) { item in
Text(item)
.onDrag {
let stringItemProvider = NSItemProvider(object: item as NSString)
return stringItemProvider
}
}
}
}
import SwiftUI
import UniformTypeIdentifiers
struct RightList: View {
#State var list: [String] = ["1", "2", "3", "4", "5"]
var body: some View {
List {
ForEach(list) { item in
Text(item)
}
.onInsert(
of: [UTType.text],
perform: drop)
}
}
private func drop(at index: Int, _ items: [NSItemProvider]) {
debugPrint(index)
for item in items {
_ = item.loadObject(ofClass: NSString.self) { text, _ in
debugPrint(text)
DispatchQueue.main.async {
debugPrint("dispatch")
text.map { self.list.insert($0 as! String, at: index) }
}
}
}
}
}
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.