On macOS I get an unexpected crash with the following code when entering a search that is not in the data (e.g. "100"). It does NOT crash on iOS with the same code.
Any hints appreciated.
struct ContentView: View {
let data: [Int] = (1...20).map { $0 }
var filteredData: [Int] {
if search.isEmpty {
return data
} else {
return data.filter { String($0).contains(search) }
}
}
#State private var selection: Int? = nil
#State private var search = ""
var body: some View {
NavigationStack {
List(selection: $selection) {
DisclosureGroup("Group") {
ForEach(filteredData, id: \.self) { item in
Text("\(item)")
}
}
}
.searchable(text: $search)
}
}
}
Related
NOTE: this question is not about how to use .searchable or how to filter a List.
I am using the following view to search an external database:
struct SearchDatabaseView: View {
#Environment(\.dismiss) private var dismiss
#Environment(\.isSearching) private var isSearching: Bool
#State private var searchText: String = ""
#State private var searchResults: [Record] = []
var body: some View {
NavigationStack {
List(searchResults, id: \.self) { record in
/// display results here
}
.navigationTitle("Search Database")
.toolbar {
Button(action: {
dismiss()
}) {
Text("Done")
}
}
.overlay {
if isSearching {
ProgressView("Searching Database...")
}
}
}
.searchable(text: $searchText)
.disableAutocorrection(true)
.onSubmit(of: .search) {
searchDatabase()
}
}
}
Everything works, except the progress view is not showing. I tried putting the .overlay modifier after .onSubmit, but still it doesn't show.
What am I missing, is that not the proper use of isSearching ?
Try this approach, where two views are used (like the docs examples) to perform
the search and dismissal using dismissSearch and display the ProgressView.
This is just an example code, see the docs at: https://developer.apple.com/documentation/swiftui/managing-search-interface-activation
for more comprehensive info and examples.
struct ContentView: View {
var body: some View {
SearchDatabaseView()
}
}
struct SearchDatabaseView: View {
#State private var searchText: String = ""
var body: some View {
NavigationStack {
ListView()
.searchable(text: $searchText)
.disableAutocorrection(true)
.navigationTitle("Search Database")
.onSubmit(of: .search) {
// searchDatabase()
print("----> onSubmit: \(searchText)")
}
}
}
}
struct ListView: View {
#Environment(\.dismissSearch) private var dismissSearch
#Environment(\.isSearching) private var isSearching
#State private var searchResults: [String] = ["a-record", "b-record", "c-record", "d-record"]
var body: some View {
List(searchResults, id: \.self) { record in
Text(record)
}
.toolbar {
Button("Done") {
dismissSearch()
}
.overlay {
if isSearching {
ProgressView("Searching Database...")
}
}
}
}
}
EDIT-1:
To cater for your new question, I would do away with the isSearching thing.
Use a "normal" variable and implement a simple but effective code structure, such as in this example code:
struct SearchDatabaseView: View {
#State private var searchText: String = ""
#State private var showSearching = false
#State private var searchResults: [String] = ["a-record", "b-record", "c-record", "d-record"]
var body: some View {
NavigationStack {
List(searchResults, id: \.self) { record in
Text(record)
}
.toolbar {
Button("Done") {
showSearching = false
}
.overlay {
if showSearching {
ProgressView("Searching Database...")
}
}
.searchable(text: $searchText)
.disableAutocorrection(true)
.navigationTitle("Search Database")
.onSubmit(of: .search) {
showSearching = true
// searchDatabase()
// simulation of searchDatabase(), could also pass showSearching to it
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// .....
showSearching = false // when finished searchDatabase()
}
}
}
}
}
}
When I press the Move button in the contextMenu, I change the isCopied and setOriginPath variables in the EnvironmentObject. When this change is made, the List view is cleared and I can't see anything on the screen. I don't have any problems when I don't use EnvironmentObject.
ContextMenu:
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
View:
struct DetailObjectView: View {
#ObservedObject var safeFileVM: SafeFileViewModel = SafeFileViewModel()
#EnvironmentObject var safeFileClipboard: SafeFileClipBoard
var currentFile: MyFile
var currentLocation = ""
var body: some View {
VStack {
.....
}
.contextMenu {
Button {
safeFileVM.hideSelectedFile(fileName: currentFile.fileName)
safeFileVM.takeArrayOfItems()
} label: {
HStack {
Text(!currentFile.isLock ? "Hide" : "Show")
Image(systemName: currentFile.isLock ? "eye" : "eye.slash")
}
}
Button {
safeFileClipboard.setOriginPath = URL(fileURLWithPath: currentFile.localPath)
safeFileClipboard.isCopied = true
} label: {
HStack {
Text("Move")
Image(systemName: "arrow.up.doc")
}
}
}
}
}
In the mini project below, when the EnvironmentObject value changes, navigation goes to the beginning. Why ? How can I fix this ?
Example Project:
Main:
#main
struct EnvironmentTestApp: App {
#StateObject var fooConfig: FooConfig = FooConfig()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
.environmentObject(fooConfig)
}
}
}
}
ContentView:
struct ContentView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
struct Number: Identifiable {
var id = UUID()
var item: String
}
DetailView:
struct DetailView: View {
#EnvironmentObject var fooConfig: FooConfig
var itemNumber: String = ""
var body: some View {
NavigationLink(destination: ContentView().environmentObject(fooConfig)) {
Text("\(itemNumber) - \(fooConfig.fooBool == true ? "On" : "Off")")
.environmentObject(fooConfig)
.contextMenu {
Button {
fooConfig.fooBool.toggle()
} label: {
HStack {
Text(fooConfig.fooBool != true ? "On" : "Off")
}
}
}
}
}
}
ObservableObject:
class FooConfig: ObservableObject {
#Published var fooBool: Bool = false
}
Move that from scene into ContentView, because scene is a bad place to update view hierarchy, it is better to do inside view hierarchy, so here
struct EnvironmentTestApp: App {
var body: some Scene {
WindowGroup {
ContentView() // only root view here !!
}
}
}
everything else is inside views, like
struct ContentView: View {
#StateObject private var foo = FooConfig()
var body: some View {
NavigationView {
MainView()
.environmentObject(foo) // << here !!
}
}
}
struct MainView: View {
#EnvironmentObject var fooConfig: FooConfig
private let numbers: [Number] = [.init(item: "1"), .init(item: "2"), .init(item: "3")]
var body: some View {
List {
ForEach(numbers, id: \.id) { item in
DetailView(itemNumber: item.item)
}
}
}
}
and so on...
Tested with Xcode 14 / iOS 16
I created the list showing the array of digits. When you select multiple digits and hit the button below, it should tell you the sum of the selected digits.
However, when you select the same digits in the different rows, both of them get checked at a time. I understand that this should be fixed by using UUID, but the final result that I want is the sum of the digits, which is Int. Therefore, I have been lost... Can someone tell me how to make it right, please?
Also, the last row doesn't get checked even when selected for some reason, which is so weird.
Here is the link to the gif showing the current situation and the entire code below.
import SwiftUI
struct MultipleSelectionList: View {
#State var items: [Int] = [20, 20, 50, 23, 3442, 332]
#State var selections: [Int] = []
#State var result: Int = 0
var body: some View {
VStack {
List {
ForEach(items, id: \.self) { item in
MultipleSelectionRow(value: item, isSelected: self.selections.contains(item)) {
if self.selections.contains(item) {
self.selections.removeAll(where: { $0 == item })
}
else {
self.selections.append(item)
}
}
}
}
Button(action: {
result = selections.reduce(0, +)
}, label: {
Text("Show result")
})
.padding()
Text("Result is \(result)")
Text("The number of items in the array is \(selections.count)")
.padding()
Spacer()
}
}
}
struct MultipleSelectionRow: View {
var value: Int
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text("\(self.value)")
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MultipleSelectionList()
}
}
You could just store the index of the items in selections, instead of the actual items.
You could do something like:
ForEach(items.indices) { i in
MultipleSelectionRow(value: items[i], isSelected: self.selections.contains(i)) {
if self.selections.contains(i) {
self.selections.removeAll(where: { $0 == i })
}
else {
self.selections.append(i)
}
}
}
I have not run this so there may be errors but you get the idea.
Before you look at the code below try making your items: [Int] into a items: [UUID: Int] this would mimic having an Identifiable object.
Programming is about figuring how to make things happen.
import SwiftUI
struct MultipleSelectionList: View {
#State var items: [UUID:Int] = [UUID():20, UUID():20, UUID():50, UUID():23, UUID():3442, UUID():332]
#State var selections: [UUID:Int] = [:]
#State var result: Int = 0
var body: some View {
VStack {
List {
ForEach(items.sorted(by: { $0.1 < $1.1 }), id: \.key) { key, value in
MultipleSelectionRow(value: value, isSelected: self.selections[key] != nil) {
if self.selections[key] != nil {
self.selections.removeValue(forKey: key)
}
else {
self.selections[key] = value
}
}
}
}
Button(action: {
result = selections.values.reduce(0, +)
}, label: {
Text("Show result")
})
.padding()
Text("Result is \(result)")
Text("The number of items in the array is \(selections.count)")
.padding()
Spacer()
}
}
}
struct MultipleSelectionRow: View {
var value: Int
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text("\(self.value)")
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
struct MultipleSelect_Previews: PreviewProvider {
static var previews: some View {
MultipleSelectionList()
}
}
Im trying to create an environment object that is editable and putting it in a list.
The Variables are only refreshing when I switch the tab for example (so whenever I leave the NavigationView) and then come back.
The same worked with a ModalView before. Is it a bug maybe? Or am I doing something wrong?
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
try passing .environmentObject on your destination:
NavigationLink(destination: detailView(feed: self._feed, i: i).environmentObject(x))
You have to use willSet instead of didSet.
TestItem should be a value type: struct or enum. SwiftUI's observation system properly works only with value types.
Ím trying to refresh this List whenever I click on a NavLink
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: ListFeedItemDetail(idx: i).environmentObject(self.feed)) {
ListFeedItem(item: self.$feed.items[i])
}
}
}
The list is made out of an array inside an environment object.
The problem: It does only refresh when I switch to another tab or close the app
I had used a modal View before and it worked there. (I did it with .onAppear)
Any Ideas?
Example
Problem: When you tap on an item in the list and tap the toggle button the EnvironmentObject is changed but this changed is only reflected when I change the tab and change it back again
import SwiftUI
import Combine
struct TestView: View {
#State var showSheet: Bool = false
#EnvironmentObject var feed: TestObject
func addObjects() {
var strings = ["one","two","three","four","five","six"]
for s in strings {
var testItem = TestItem(text: s)
self.feed.items.append(testItem)
}
}
var body: some View {
TabView {
NavigationView {
List(feed.items.indices, id:\.self) { i in
NavigationLink(destination: detailView(feed: self._feed, i: i)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}
}
}
}
.tabItem({ Text("Test") })
.tag(0)
Text("Blank")
.tabItem({ Text("Test") })
.tag(0)
}.onAppear {
self.addObjects()
}
}
}
struct detailView: View {
#EnvironmentObject var feed: TestObject
var i: Int
var body: some View {
VStack {
Text(feed.items[i].text)
Text(feed.items[i].read.description)
Button(action: { self.feed.items[self.i].isRead.toggle() }) {
Text("Toggle read")
}
}
}
}
final class TestItem: ObservableObject {
init(text: String) {
self.text = text
self.isRead = false
}
static func == (lhs: TestItem, rhs: TestItem) -> Bool {
lhs.text < rhs.text
}
var text: String
var isRead: Bool
let willChange = PassthroughSubject<TestItem, Never>()
var read: Bool {
set {
self.isRead = newValue
}
get {
self.isRead
}
}
}
class TestObject: ObservableObject {
var willChange = PassthroughSubject<TestObject, Never>()
#Published var items: [TestItem] = [] {
didSet {
willChange.send(self)
}
}
}
I had a similar problem, this is the hack I came up with.
In your "TestView" declare:
#State var needRefresh: Bool = false
Pass this to your "detailView" destination, such as:
NavigationLink(destination: detailView(feed: self._feed, i: i, needRefresh: self.$needRefresh)) {
HStack {
Text(self.feed.items[i].text)
Text("(\(self.feed.items[i].read.description))")
}.accentColor(self.needRefresh ? .white : .black)
}
Note ".accentColor(self.needRefresh ? .white : .black)" to force a refresh when "needRefresh"
is changed.
In your "detailView" destination add:
#Binding var needRefresh: Bool
Then in your "detailView" in your Button action, add:
self.needRefresh.toggle()