Swiftui SortDescriptor - Personalise sorting - swiftui

#State var sortDescriptor: SortDescriptor<Spell> = SortDescriptor(\.name)
I want to define the SortDescriptor so that letters come first than numbers.
Example: ["2st", "Cant", "1st"] -> ["Cant", "1st", "2nd"]
I don't know how to.

let array = ["2nd", "Cant", "1st", "Abc", "12some", "hello"]
extension Array where Element == String {
func sortedNrLast() -> Self {
self.filter { !"0123456789".map {$0}.contains($0.first) }.sorted() +
self.filter { "0123456789".map {$0}.contains($0.first) }.sorted()
}
}
print(array.sortedNrLast())

Related

How to display highlighted words using swift ui

var str:String = "When I was ###young$$$, I thought ###time$$$ was money"
VStsck{
Text(str)
}
enter image description here
You can parse your string using regular expression and build Text based on found ranges:
struct HighlightedText: View{
let text: Text
private static let regularExpression = try! NSRegularExpression(
pattern: "###(?<content>((?!\\$\\$\\$).)*)\\$\\$\\$"
)
private struct SubstringRange {
let content: NSRange
let full: NSRange
}
init(_ string: String) {
let ranges = Self.regularExpression
.matches(
in: string,
options: [],
range: NSRange(location: 0, length: string.count)
)
.map { match in
SubstringRange(
content: match.range(withName: "content"),
full: match.range(at: 0)
)
}
var nextNotProcessedSymbol = 0
var text = Text("")
let nsString = string as NSString
func appendSubstringStartingNextIfNeeded(until endLocation: Int) {
if nextNotProcessedSymbol < endLocation {
text = text + Text(nsString.substring(
with: NSRange(
location: nextNotProcessedSymbol,
length: endLocation - nextNotProcessedSymbol
)
))
}
}
for range in ranges {
appendSubstringStartingNextIfNeeded(until: range.full.location)
text = text + Text(nsString.substring(with: range.content))
.foregroundColor(Color.red)
nextNotProcessedSymbol = range.full.upperBound
}
appendSubstringStartingNextIfNeeded(until: string.count)
self.text = text
}
var body: some View {
text
}
}
Usage:
HighlightedText("When I was ###young$$$, I thought ###time$$$ was money")
func hilightText(str:String) -> Text{
var resultText:Text = Text("")
if(str.contains("###")){
let titleArr:Array<String> = str.components(spearatedBy:"###")
for(title in titleArr){
resultText = resultText
+ Text(title.components(spearatedBy:"$$$")[0]).foregroundColor(Color.red)
+ Text(title.components(spearatedBy:"$$$")[1]).foregroundColor(Color.gray)
}else{
resultText = resultText + Text(title).foregroundColor(Color.gray)
}
}
return resultText
}else{
return Text(str).foregroundColor(Color.gray)
}
struct View1:View{
#State var title = "When I was ###young$$$, I thought ###time$$$ was money"
var body : some View{
hilightText(str:title)
}
}

How to read and filter large Realm dataset in SwiftUI?

I'm storing ~100.000 dictionary entries in a realm database and would like to display them. Additionally I want to filter them by a search field. Now I'm running in a problem: The search function is really inefficient although I've tried to debounce the search.
View Model:
class DictionaryViewModel : ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var entries: Results<DictionaryEntry>?
#Published var filteredEntries: Results<DictionaryEntry>?
#Published var searchText: String = ""
#Published var isSearching: Bool = false
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(800), scheduler: RunLoop.main) // debounces the string publisher, such that it delays the process of sending request to remote server.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.filteredEntries = nil
return nil
}
return string
})
.compactMap{ $0 }
.sink { (_) in
} receiveValue: { [self] (searchField) in
filter(with: searchField)
}.store(in: &subscription)
self.fetch()
}
public func fetch(){
self.entries = DatabaseManager.sharedInstance.fetchData(type: DictionaryEntry.self).sorted(byKeyPath: "pinyin", ascending: true)
self.filteredEntries = entries
}
public func filter(with condition: String){
self.filteredEntries = self.entries?.filter("pinyin CONTAINS[cd] %#", searchText).sorted(byKeyPath: "pinyin", ascending: true)
}
In my View I'm just displaying the filteredEtries in a ScrollView
The debouncing works well for short text inputs like "hello", but when I filter for "this is a very long string" my UI freezes. I'm not sure whether something with my debounce function is wrong or the way I handle the data filtering in very inefficient.
EDIT: I've noticed that the UI freezes especially when the result is empty.
EDIT 2:
The .fetchData() function is just this here:
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
All realm objects have a primary key. The structure looks like this:
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var character: String = ""
#objc dynamic var pinyin: String = ""
#objc dynamic var translation: String = ""
override class func primaryKey() -> String {
return "id"
}
EDIT 3: The filtered results are displayed this way:
ScrollView{
LazyVGrid(columns: gridItems, spacing: 0){
if (dictionaryViewModel.filteredEntries != nil) {
ForEach(dictionaryViewModel.filteredEntries!){ entry in
Text("\(entry.translation)")
}
} else {
Text("No results found")
}
}

How to publish changes to a single object in a object array

I have the following classes
class ListItem: Identifiable {
var id: UUID
var name: String
var description: String
var isFavorite: Bool
var debugDescription: String {
return "Name: \(self.name) | Favorite?: \(self.isFavorite)"
}
public init(name: String) {
self.name = name
id = UUID()
self.description = "Some text describing why \(self.name.lowercased()) is awesome"
self.isFavorite = false
}
}
class ListItems: ObservableObject {
#Published var items: [ListItem]
let defaultAnimals = ["Ant", "Bear", "Cat", "Dog", "Elephant",
"Fish", "Giraffe", "Hyena", "Iguana", "Jackal", "Kingfisher", "Leopard", "Monkey"]
public init(animals: [String] = []) {
let animalList: [String] = animals.count > 0 ? animals : defaultAnimals
self.items = animalList.sorted {
$0.lowercased() < $1.lowercased()
}.map {
ListItem(name: $0.firstUppercased)
}
}
}
and the following image view in ContentView
struct ContentView: View {
#ObservedObject var list: ListItems = ListItems()
var body: some View {
List(list.items) {
animal in HStack {
// ...
Image(systemName: animal.isFavorite ? "heart.fill" : "heart").foregroundColor(.pink).onTapGesture {
let index = self.list.items.firstIndex { $0.id == animal.id } ?? -1
if (index >= 0) {
self.list.items[index].isFavorite = !animal.isFavorite
self.list.items = Array(self.list.items[0...self.list.items.count-1]) // <--
}
}
// ...
}
}
}
}
Everytime, the image view is tapped, I am basically reassigning the entire array like this so that the changes can be reflected in the UI
self.list.items = Array(self.list.items[0...self.list.items.count-1])
My question: How can I refactor my code to prevent reassigning the entire object array every time some object property changes?
I am fairly new to Swift & iOS development, not sure if I am missing something basic.
Declare ListItem as an struct instead of a class, this way the view will be notified when isFavorite changes. And just a little suggestion; you can use toggle to change the value of a boolean: self.list.items[index].isFavorite.toggle()

UISearchbar Search in all fields in swift 3

I've got a tableview showing some data and I filter the shown data uisng UISearchbar. Each data struct consists of different values and
struct Cake {
var name = String()
var size = String()
var filling = String()
}
When a user starts typing I don't know whether he is filtering for name, size or filling. I don't want to use a scopebar. Is there a way to filter for various fields at the same time in swift 3?
This is the code I use to filter:
func updateSearchResults(for searchController: UISearchController) {
if searchController.searchBar.text! == "" {
filteredCakes = cakes
} else {
// Filter the results
filteredCakes = cakes.filter { $0.name.lowercased().contains(searchController.searchBar.text!.lowercased()) }
}
self.tableView.reloadData()
}
thanks for your help!
func updateSearchResults(for searchController: UISearchController)
{
guard let searchedText = searchController.searchBar.text?.lowercased() else {return}
filteredCakes = cakes.filter
{
$0.name.lowercased().contains(searchedText) ||
$0.size.lowercased().contains(searchedText) ||
$0.filling.lowercased().contains(searchedText)
}
self.tableView.reloadData()
}

Swift 3: How to apply attributes to parts of string

So the method I used in Swift 2 no longer works because of changes in Swift 3, regarding String indices and ranges. Previously I had
func configureLabel(defaultColor: UIColor, highlightColor: UIColor, boldKeyText: Bool) {
if let index = self.text?.characters.indexOf(Character("|")) {
self.text = self.text!.stringByReplacingOccurrencesOfString("|", withString: "")
let labelLength:Int = Int(String(index))! // Now returns nil
var keyAttr: [String:AnyObject] = [NSForegroundColorAttributeName: highlightColor]
var valAttr: [String:AnyObject] = [NSForegroundColorAttributeName: defaultColor]
if boldKeyText {
keyAttr[NSFontAttributeName] = UIFont.systemFontOfSize(self.font.pointSize)
valAttr[NSFontAttributeName] = UIFont.systemFontOfSize(self.font.pointSize, weight: UIFontWeightHeavy)
}
let attributeString = NSMutableAttributedString(string: self.text!)
attributeString.addAttributes(keyAttr, range: NSRange(location: 0, length: (self.text?.characters.count)!))
attributeString.addAttributes(valAttr, range: NSRange(location: 0, length: labelLength))
self.attributedText = attributeString
}
}
Basically I would be able to take a string like "First Name:| Gary Oak" and have all the parts before and after the | character be different colors, or make part of it bold, but the line I commented above no longer returns a value, which breaks everything else afterwards. Any ideas on how to do this?
In Swift 3 you can use something like this:
func configureLabel(defaultColor: UIColor, highlightColor: UIColor, boldKeyText: Bool) {
if let index = self.text?.characters.index(of: Character("|")) {
self.text = self.text!.replacingOccurrences(of: "|", with: "")
let position = text.distance(from: text.startIndex, to: index)
let labelLength:Int = Int(String(describing: position))!
var keyAttr: [String:AnyObject] = [NSForegroundColorAttributeName: defaultColor]
var valAttr: [String:AnyObject] = [NSForegroundColorAttributeName: highlightColor]
if boldKeyText {
keyAttr[NSFontAttributeName] = UIFont.systemFont(ofSize: self.font.pointSize)
valAttr[NSFontAttributeName] = UIFont.systemFont(ofSize: self.font.pointSize, weight: UIFontWeightHeavy)
}
let attributeString = NSMutableAttributedString(string: self.text!)
attributeString.addAttributes(keyAttr, range: NSRange(location: 0, length: (self.text?.characters.count)!))
attributeString.addAttributes(valAttr, range: NSRange(location: 0, length: labelLength))
self.attributedText = attributeString
}
}
the main idea that using let position = text.distance(from: text.startIndex, to: index) you got not the integer representation of string position but the string Index value. Using text.distance(from: text.startIndex, to: index) you can find int position for string Index