Optimize application performance when searching in the list - list

How can I optimize the list filtering? There are 2000 records in the list and when entering text into the application's search engine it clips a bit. Do you have any suggestions that could optimize the search?
Code
import CoreData
import SwiftUI
struct SongbookView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
#State private var searchText = ""
var body: some View {
NavigationView{
VStack{
SearchBar(text: $searchText)
Spacer()
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
}
.listStyle(InsetListStyle())
.navigationTitle("Śpiewnik")
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
extension UINavigationController {
// Remove back button text
open override func viewWillLayoutSubviews() {
navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}
}

Related

SwiftUI : How I can use custom font for NavigationBarTitle Strings?

import SwiftUI
struct AddChildrenView: View {
#State var word : String = ""
#State var meaning : String = ""
#State var isShowAlert : Bool = false
#State var isClicked : Bool = false
#State var searchText : String = ""
#EnvironmentObject var itemModel : ItemModel
var item : Item
var searchItem : [Children] {
if searchText.isEmpty {
return item.children
} else {
return item.children.filter({$0.word!.contains(searchText)})
}
}
init() {
UINavigationBar.appearance().titleTextAttributes = [.font : UIFont(name: "NotoSans-Bold", size: 20)!]
}
var body: some View {
NavigationView {
ZStack {
if item.children.count == 0 {
NochildrenView()
} else {
List {
ForEach(searchItem) {child in
WordListRowView(children: child)
.swipeActions(edge: .trailing, allowsFullSwipe: true, content: {
Button(role: .destructive, action: {
itemModel.deleteChildren(children: child, item: item)
}, label: {
Image(systemName: "trash.fill")
})
Button(action: {
itemModel.favoriteChildren(item: item, children: child)
}, label: {
Image(systemName: "star.circle.fill")
.font(.title3)
})
.tint(.green)
})
}
}
.listStyle(.insetGrouped)
.padding(.top, -10)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.autocapitalization(.none)
}
}
.navigationBarTitle("\(item.group!)'s Words")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
HStack {
Button(action: {
addChildren()
}, label: {
Image(systemName: "plus")
})
Button(action: {
itemModel.shuffleChildren(item: item)
}, label: {
Image(systemName: "shuffle")
})
NavigationLink(destination: FlashCardView(item: item)) {
Image(systemName: "play.rectangle")
}
.disabled(item.children.isEmpty)
})
}
}
}
extension AddChildrenView {
func addChildren() {
let alert = UIAlertController(title: "Saving word", message: "Type word and meaning 😀", preferredStyle: .alert)
alert.addTextField { word in
word.placeholder = "Type a word"
word.keyboardType = .alphabet
}
alert.addTextField { meaning in
meaning.placeholder = "a meaning of word"
}
let addfolderAction = UIAlertAction(title: "Add", style: .default, handler: {
(_) in
self.word = alert.textFields![0].text!
self.meaning = alert.textFields![1].text!
itemModel.addNewChildren(item: item, word: word, meaning: meaning)
})
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: {
(_) in
})
alert.addAction(cancelAction)
alert.addAction(addfolderAction)
UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true, completion: nil)
}
}
Hello, first, this view is the second view after click the navigationLink in firstView.
init() {
UINavigationBar.appearance().titleTextAttributes = [.font : UIFont(name: "NotoSans-Bold", size: 20)!]
}
I tried this code to use custom font on my navigationBartitle, but there is error like this.
"Return from initializer without initializing all stored properties"
Could you let me know the way I can solve this problem?
Thank you!

It does not read that the setting has changed

I have a problem I change the "isDynamic" setting in the "SettingView" and I exit the setting window and the "SongbookView" does not register that the setting has changed. I want to change the search engine depending on what option is selected in the settings. What is the cause of this situation?
SongbookView:
import CoreData
import SwiftUI
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
if userSettings.isDynamic == false {
SearchBar(text: $searchText)
} else {
DynamicSearchBar(text: $searchText)
}
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
.listStyle(InsetListStyle())
}
.padding(.top, 10)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack{
Text(String(self.userSettings.isDynamic))
Spacer()
Text("Śpiewnik")
.font(.system(size: 20))
.bold()
Spacer()
Button(action: {
isSettings.toggle()
print(userSettings.isDynamic)
}) {
Image(systemName: "gearshape")
.resizable()
.frame(width: 16.0, height: 16.0)
}
.sheet(isPresented: $isSettings) {
SettingView(isPresented: $isSettings)
}
}
}
}
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
SettingView:
import SwiftUI
struct SettingView: View {
#ObservedObject var userSettings = UserSettings()
#Binding var isPresented: Bool
var body: some View {
NavigationView {
Form {
Toggle("Dynamiczna wyszukiwarka", isOn: $userSettings.isDynamic)
.onChange(of: userSettings.isDynamic) { value in
print(value)
}
Button(action: {
print(userSettings.isDynamic)
isPresented = false
}) {
Text("Test")
}
}
.navigationBarTitle("Ustawienia")
}
}
}
UserSettings:
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var isDynamic: Bool {
didSet {
UserDefaults.standard.set(isDynamic, forKey: "isSearchDynamic")
}
}
init() {
self.isDynamic = UserDefaults.standard.object(forKey: "isSearchDynamic") as? Bool ?? false
}
}
You're using two different instances of UserSettings. When you update isDynamic on one of those instances, the other, even though it has a reference to UserDefaults has no reason to know that it needs to update.
The easiest solution here is to share a single instance of UserSettings:
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
//...
.sheet(isPresented: $isSettings) {
SettingView(userSettings: userSettings,isPresented: $isSettings)
}
//...
}
}
struct SettingView: View {
#ObservedObject var userSettings : UserSettings
#Binding var isPresented: Bool
var body: some View {
//...
}
}
You could also look into property wrappers like #AppStorage (https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper) that actually update dynamically when the UserDefaults values change.

SwiftUI - Update data on Firebase's Realtime database

I have successfully displayed the data to the UI, but I want the user to be able to update my data again when tapping the "Save" button . Hope you can help me!
Profile
I have successfully displayed the data to the UI, but I want the user to be able to update my data again when tapping the "Save" button . Hope you can help me!
There are many ways to achieve what you want. This is just one approach, by
passing the profileViewModel to EditProfile:
class ProfileViewModel: ObservableObject {
#Published var user = Profile(id: "", image: "", birthDay: "", role: [], gender: "", name: "")
private var ref: DatabaseReference = Database.database().reference()
func fetchData(userId: String? = nil) {
// 8hOqqnFlfGZTj1u5tCkTdxAED2I3
ref.child("users").child(userId ?? "default").observe(.value) { [weak self] (snapshot) in
guard let self = self,
let value = snapshot.value else { return }
do {
print("user: \(value)")
self.user = try FirebaseDecoder().decode(Profile.self, from: value)
} catch let error {
print(error)
}
}
}
func saveUser() {
// save the user using your ref DatabaseReference
// using setValue, or updateChildValues
// see https://firebase.google.com/docs/database/ios/read-and-write
}
}
struct EditProfile: View {
#ObservedObject var profileViewModel: ProfileViewModel // <--- here
var body: some View {
VStack {
Text(profileViewModel.user.name) // <--- you probably meant TextField
.font(.custom("Poppins-Regular", size: 15))
.foregroundColor(Color.black)
Text("\(profileViewModel.user.birthDay)!")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Text("\(profileViewModel.user.gender)")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Text(profileViewModel.user.role.first ?? "")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Button(action: {
// save the profileViewModel.user to database
profileViewModel.saveUser() // <--- here
}) {
Text("Save")
}
}
.padding()
}
}
struct CategoriesView: View {
#ObservedObject var viewModel = SectionViewModel()
#EnvironmentObject var loginViewModel : LoginViewModel
#StateObject var profileViewModel = ProfileViewModel()
var body: some View {
ZStack {
VStack (alignment: .leading, spacing:0) {
EditProfile(profileViewModel: profileViewModel) // <--- here
.padding()
.padding(.bottom,-10)
}
}
.onAppear() {
self.viewModel.fetchData()
profileViewModel.fetchData(userId: loginViewModel.session?.uid)
}
}
}
EDIT1: regarding the updated code.
In your new code, in ProfileHost you are not passing ProfileViewModel.
Use:
NavigationLink(destination: ProfileEditor(profileViewModel: viewModel)) {
ProfileRow(profileSetting: profile)
}
And in ProfileEditor replace profile with profileViewModel.user
You will probably need to adjust profileItem and put it in a .onAppear {...} . Something like this:
struct ProfileEditor: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var profileViewModel: ProfileViewModel
#EnvironmentObject var loginViewModel: LoginViewModel
let profileLabel: [String] = ["Name", "Account", "Gender", "Role", "Email"]
#State var profileItem: [String] = []
#State var profileEditorRow: [ProfileEditorItem] = []
var body: some View {
VStack {
ForEach(profileEditorRow) { editor in
if editor.id == 5 {
ProfileEditorRow(editor: editor, showLastLine: true)
} else {
ProfileEditorRow(editor: editor, showLastLine: false)
}
}
Button("Save") {
profileViewModel.updateData(userId: loginViewModel.session?.uid)
}
}
.onAppear {
profileItem = [profileViewModel.user.name,
profileViewModel.user.birthDay,
profileViewModel.user.gender,
profileViewModel.user.role.first ?? "",
profileViewModel.user.birthDay]
for n in 1...5 {
profileEditorRow.append(ProfileEditorItem(id: n, label: profileLabel[n-1], item: profileItem[n-1]))
}
}
}
}
EDIT2: update func
func updateData() {
ref.("users").child(user.id).updateChildValues([
"name": user.name,
"birthDay": user.birthDay,
"gender": user.gender,
"role": user.role.first ?? ""])
}
and use this in ProfileEditor :
Button("Save") {
profileViewModel.updateData()
}

Optimize list searches

How can I optimize the searches in the list. I have two thousand records. I don't want to do a search through NSPredicate, because I want to pass what is in the field through a function that cleans up the numbers and reduces the letters, before comparing. Can you somehow give a delay so that it does not search immediately but after some time or if the user finishes typing. I also heard about something like Combine, but I have no idea how to use it.
Songbook List
import CoreData
import SwiftUI
struct SongbookView: View {
#State var searchText: String = ""
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
SearchBar(text: $searchText)
Spacer()
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
.listStyle(InsetListStyle())
.animation(.default)
}
.padding(.top, 10)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
Text("Śpiewnik")
.font(.system(size: 20))
.bold()
}
}
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
Search Bar
import SwiftUI
struct SearchBar: View {
#Binding var text: String
#State var isEditing = false
var body: some View {
HStack {
TextField("Szukaj ...", text: $text)
.padding(7)
.padding(.horizontal, 25)
.background(Color(.systemGray6))
.cornerRadius(8)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
.padding(.leading, 8)
if isEditing {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.foregroundColor(.gray)
.padding(.trailing, 8)
}
}
}
)
.padding(.horizontal, 10)
.onTapGesture {
self.isEditing = true
}
if isEditing {
Button(action: {
self.isEditing = false
self.text = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}) {
Text("Anuluj")
}
.padding(.trailing, 10)
.transition(.move(edge: .trailing))
.animation(.default)
}
}
}
}
whenever you change the text in your SearchBar (that is every character you type),
the SongbookView is updated
because you are using a binding for text. What you want is to do the update
only once when you press return. There are many ways to do this. A quick way to do this and keep your binding setup, is:
struct SearchBar: View {
#Binding var text: String
#State var txt: String = "" // <--- here a temp var
#State var isEditing = false
var body: some View {
HStack {
TextField("Szukaj ...", text: $txt) // <--- here
.onSubmit {
text = txt // <--- here only update on return press
}
.padding(7)
....
.onAppear {
txt = text // <--- here if needed
}
If you are using ios-14, use
TextField("Szukaj ...", text: $txt, onCommit: { // <--- here
text = txt // <--- here
})

SwiftUI - i'd like to remove space between "back button" and .navigationbartitle

i would like to remove the space between my back button ("Rezept hinzufügen") and my navigationbarTitle ("Suche")... I cant figure out why this space is there so i need your swarm intelligence. :)
What did i try?
Working with a ZStack, maybe there is NavigationView from before which is doin this bug
Add Spacer()
Recreate the whole view
Now I stuck...
I think the easiest way is to show you my problem with an video...
Here you can see my problem
Here is my code...
import SwiftUI
extension UIApplication
{
func endEditing(_force : Bool)
{
self.windows
.filter{$0.isKeyWindow}
.first?
.endEditing(_force)
}
}
struct ResignKeyboardOnDragGesture: ViewModifier
{
var gesture = DragGesture().onChanged{_ in UIApplication.shared.endEditing(_force: true)}
func body(content: Content) -> some View
{
content.gesture(gesture)
}
}
extension View
{
func resignKeyboardOnDragGesture() -> some View
{
return modifier(ResignKeyboardOnDragGesture())
}
}
/*
Zutaten pflegen Button, zum hinzufügen von Zutaten zu einem Rezept.
**/
struct RecipeIngredientsView: View {
let myArray = ["Dennis", "Tessa", "Peter", "Anna", "Tessa", "Klaus", "Xyan", "Zuhau", "Clown", "Brot", "Bauer"]
#State private var searchText = ""
#State private var showCancelButton: Bool = false
var body: some View {
NavigationView
{
VStack
{
HStack
{
HStack
{
Image(systemName: "magnifyingglass")
TextField("Suche", text: $searchText, onEditingChanged: { isEditing in self.showCancelButton = true}, onCommit: {
print("onCommit")
}).foregroundColor(.primary)
Button(action: {
self.searchText = searchText
}){
Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1)
}
}.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.secondary)
.background(Color(.secondarySystemBackground))
.cornerRadius(10.0)
if showCancelButton {
Button("Abbrechen")
{
UIApplication.shared.endEditing(_force: true)
self.searchText = ""
self.showCancelButton = false
}
.foregroundColor(Color(.systemBlue))
}
}
.padding(.horizontal)
.navigationBarHidden(showCancelButton)
//Gefilterte Liste der Namen aus meinem Array
List {
ForEach(myArray.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self)
{
searchText in Text(searchText)
}
}
.navigationBarTitle(Text("Suche"))
.resignKeyboardOnDragGesture()
}
}
}
}
Thanks for your help!
:-)
Just remove redundant NavigationView - it is needed only one in same view hierarchy, and obviously there is already some in parent view
struct RecipeIngredientsView: View {
let myArray = ["Dennis", "Tessa", "Peter", "Anna", "Tessa", "Klaus", "Xyan", "Zuhau", "Clown", "Brot", "Bauer"]
#State private var searchText = ""
#State private var showCancelButton: Bool = false
var body: some View {
NavigationView // << remove this one !!
{