Swift 3 - NSOutlineView with expandable items - swift3

i would like to realize an outlineView, which shows string values as root values. the following code works for me:
import Cocoa
class TestController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate {
#IBOutlet weak var outlineView: NSOutlineView!
var items: [String] = ["Item 1", "Item 2", "Item 3", "Item 4","Item 5"]
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
return items[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
return true
}
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if item == nil {
return items.count
}
return 0
}
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let v = outlineView.make(withIdentifier: "HeaderCell", owner: self) as! NSTableCellView
if let tf = v.textField {
tf.stringValue = item as! String
}
return v
}
}
This is the result:
but i don't know, how i can assign different string values for Item 1 (for example). i wish to realize something like that:
+Item 1
++Sub Item 1.1
++Sub Item 1.2
+Item 2
++Sub Item 2.1
+Item 3
++Sub Item 3.1
++Sub Item 3.2
++Sub Item 3.3
...
can somebody help me?

A simple String array will not be too much of use here, you need a dictionary at least to display sub items. I would recommend to introduce a little helper model, lets call it Item. It holds a name, and a number of children Items, those will be the sub-items.
struct Item {
let name: String
var childrens: [Item] = []
/// Create a helper function to recursivly create items
///
/// - Parameters:
/// - parents: number of parent items
/// - childrens: number of children for each parent item
/// - Returns: array of Item
static func itemsForNumberOf(parents: Int, childrens: Int) -> [Item] {
var items: [Item] = []
for parentID in 1...parents {
var parent = Item(name: "Index \(parentID)",childrens: [])
for childrenID in 1...childrens {
let children = Item(name: "Index \(parentID).\(childrenID)",childrens: [])
parent.childrens.append(children)
}
items.append(parent)
}
return items
}
}
Declare a property on your viewController called items, and return some Item's using the itemsForNumberOf helper function.
class TestController: NSViewController, NSOutlineViewDataSource, NSOutlineViewDelegate {
let items: [Item] = {
return Item.itemsForNumberOf(parents: 5, childrens: 3)
}()
#IBOutlet weak var outlineView: NSOutlineView!
}
In TestController override the viewDidLoad() function and assign the delegate and datasource to your viewController.
override func viewDidLoad() {
super.viewDidLoad()
self.outlineView.delegate = self
self.outlineView.dataSource = self
}
Check the documentation of NSOutlineViewDataSource and in specific this API
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if item == nil {
return items[index]
}
if let item = item as? Item, item.childrens.count > index {
return item.childrens[index]
}
return ""
}
Return expandalbe property based on children of the received Item.
If it is empty, no children -> not expandable
If it is not empty, has children -> expandable
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
guard let item = item as? Item else {
return false
}
return !item.childrens.isEmpty
}
Same for returning the sub items count by using childrens property again
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if item == nil {
return items.count
}
if let item = item as? Item {
return item.childrens.count
}
return 0
}
In your viewFor function, you need to make sure to return nil to everything, what is not a Item type asking for a view.
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
guard let item = item as? Item else {
return nil
}
let v = outlineView.make(withIdentifier: "HeaderCell", owner: self) as! NSTableCellView
if let tf = v.textField {
tf.stringValue = item.name
}
return v
}
}
And you should end up with something like this:

If you want to display hierarchical data, the data can't be a simple array of strings. Instead of item strings, use dictionaries or custom objects which have title/name and children properties. The children property is an array of child items.
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any returns the children of item. If item is "Item 1" and index is 1, the return value is "Sub Item 1.2".
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int returns the number of children of ``. If item is "Item 1" , the return value is 2.

Related

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 update any specific MKAnnotationView image on click button from NavigationBar?

I have added some annotationViews at Map with init method (initialised by there id). Now I want to update specific id annotation view on click button from navigation bar.
Suppose I have added 5 annotation with ids (1, 2, 3, 4, and 5)
Added from VC:
let annotation = MapPinAnnotation(title: storeItem.name!, location: CLLocationCoordinate2DMake(Double(lat), Double(long)), id: storeItem.storeId!)
self.mapview.addAnnotation(annotation)
Initialised AnnotationView:
class MapPinAnnotation: NSObject, MKAnnotation {
var title:String?
var id:String?
private(set) var coordinate = CLLocationCoordinate2D()
init(title newTitle: String, location: CLLocationCoordinate2D, id: String) {
super.init()
self.title = newTitle
self.coordinate = location
self.id = id
}
}
ViewFor annotation method:
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if (annotation is MKUserLocation) {
return nil
}
if (annotation is MapPinAnnotation) {
let pinLocation = annotation as? MapPinAnnotation
// Try to dequeue an existing pin view first.
var annotationView: MKAnnotationView? = mapView.dequeueReusableAnnotationView(withIdentifier: "MapPinAnnotationView")
if annotationView == nil {
annotationView?.image = UIImage(named: Constants.Assets.PinGreen)
}
else {
annotationView?.annotation = annotation
}
return annotationView
}
return nil
}
Now I want to change image of annotation view(id 4) on click button from navigation bar.
How can I update? Please help.
Thanks in advance.
You can get specific MKAnnotationView with view(for: ) method. Try the following code:
func clickButton() {
for annotation in self.mapView.annotations {
if annotation.id == 4 {
let annotationView = self.mapView.view(for: annotation)
annotationView?.image = UIImage(named: "Image name here")
}
}
}

Index out of range (Table with expandable rows) in Swift 3

Hello i am trying to make a shop page on my application. The shop page contains a table with a title that is clickable so it expands into subcategories of that title. Etc click coffee and get some different kind of coffee to buy from.
Title = Drinks, Sandwiches & Drinks in the picture below.
subcategory from title = Ham & cheese, Chicken, bacon & curry .... and so on.
Now the expanding and closing the titles work fine. But when i press + (add iteem to cart) i get a out out of index if it's clicked in a specific order.
if i expand sandwiches (like it is in the picture) and then expand drinks (the one below sandwiches) and then click on the plus on any subcategory in sandwiches and then close sandwiches again and press on plus on any subcategory in Drinks (the one below the now closed sandwiches) the application will crash with a out of index error. Only this combination gives this error.
How the code works:
/* Create Cells */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -
> UITableViewCell {
// Row is DefaultCell
if let rowData = destinationData?[indexPath.row] {
let defaultCell = Bundle.main.loadNibNamed("TableViewCellMenuItems", owner: self, options: nil)?.first as! TableViewCellMenuItems
// tableView.dequeueReusableCell(withIdentifier: "TableViewCellMenuItems", for: indexPath).first as! TableViewCellMenuItems
defaultCell.menuName.text = rowData.name
defaultCell.menuPicture.image = rowData.image
defaultCell.menuFoldPic.image = #imageLiteral(resourceName: "ic_keyboard_arrow_down")
defaultCell.selectionStyle = .none
return defaultCell
}
// Row is ExpansionCell
else {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: indexPath.row)] {
// print("her åbner vi")
// // Create an ExpansionCell
let expansionCell =
Bundle.main.loadNibNamed("TableViewCellMenuItemExpanded", owner: self, options: nil)?.first as! TableViewCellMenuItemExpanded
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: indexPath.row)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = indexPath.row - parentCellIndex - 1
// Set the cell's data
expansionCell.itemPrice.text = String (describing: "\(rowData.menuItems![flightIndex].price) kr")
expansionCell.itemName.text = rowData.menuItems?[flightIndex].name
expansionCell.itemCount.text = String (describing: rowData.menuItems![flightIndex].count)
// expansionCell.itemCount.text = String (describing: indexPath.row)
expansionCell.itemAdd.tag = Int(indexPath.row)
expansionCell.itemMinus.tag = Int(indexPath.row)
expansionCell.itemAdd.addTarget(self, action: #selector(HomeSelectedShopViewController.plus(_:)), for: .touchUpInside)
expansionCell.itemMinus.addTarget(self, action: #selector(HomeSelectedShopViewController.minus(_:)), for: .touchUpInside)
expansionCell.selectionStyle = .none
return expansionCell
}
}
return UITableViewCell()
}
as you can see the expansionCells get a tag for my function plus and minus (so i can know which indexPath i should add up or down)
Plus function:
func plus(_ sender: AnyObject?) {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: sender!.tag)] {
self.table.reloadData()
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: sender!.tag)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = sender!.tag - parentCellIndex - 1
print(sender!.tag)
print(flightIndex)
print(rowData.menuItems!.count)
var con = 0;
for a in rowData.menuItems! {
print("her er navn : \(a.name) og her er id \(con)")
con += 1
}
let data = rowData.menuItems![flightIndex]
let item = Items(name: data.name, price: data.price, count: data.count)
var counter = 0;
for x in self.basket {
if(x.name == item.name || x.price == item.price)
{
counter = counter+1;
}
}
if(counter >= 99)
{
}
else
{
DispatchQueue.main.async {
self.basket.append(item)
self.updateBasket()
rowData.menuItems![flightIndex].count = rowData.menuItems![flightIndex].count+1;
self.table.reloadData()
}
}
}
}
Rest of the functions:
/* Expand cell at given index */
private func expandCell(tableView: UITableView, index: Int) {
// print("when is this run 1")
// Expand Cell (add ExpansionCells
if let flights = destinationData?[index]?.menuItems {
for i in 1...flights.count {
destinationData?.insert(nil, at: index + i)
tableView.insertRows(at: [NSIndexPath(row: index + i, section: 0) as IndexPath] , with: .top)
}
}
}
/* Contract cell at given index */
private func contractCell(tableView: UITableView, index: Int) {
// print("when is this run 4")
if let flights = destinationData?[index]?.menuItems {
for i in 1...flights.count {
destinationData?.remove(at: index+1)
tableView.deleteRows(at: [NSIndexPath(row: index+1, section: 0) as IndexPath], with: .top)
}
}
}
/* Get parent cell index for selected ExpansionCell */
private func getParentCellIndex(expansionIndex: Int) -> Int {
// print("when is this run 5")
var selectedCell: Menu?
var selectedCellIndex = expansionIndex
while(selectedCell == nil && selectedCellIndex >= 0) {
selectedCellIndex -= 1
selectedCell = destinationData?[selectedCellIndex]
}
return selectedCellIndex
}
func minus(_ sender: AnyObject?) {
if let rowData = destinationData?[getParentCellIndex(expansionIndex: sender!.tag)] {
// Get the index of the parent Cell (containing the data)
let parentCellIndex = getParentCellIndex(expansionIndex: sender!.tag)
// Get the index of the flight data (e.g. if there are multiple ExpansionCells
let flightIndex = sender!.tag - parentCellIndex - 1
let data = rowData.menuItems![flightIndex]
let item = Items(name: data.name, price: data.price, count: data.count)
DispatchQueue.main.async {
var counter = 0;
for x in self.basket {
if(x.name == item.name || x.price == item.price)
{
if(item.count == 0)
{
break
}
else
{
self.basket.remove(at: counter)
self.updateBasket()
rowData.menuItems![flightIndex].count = rowData.menuItems![flightIndex].count-1;
self.table.reloadData()
break
}
}
counter = counter+1;
}
}
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let data = destinationData?[indexPath.row] {
// If user clicked last cell, do not try to access cell+1 (out of range)
if(indexPath.row + 1 >= (destinationData?.count)!) {
expandCell(tableView: tableView, index: indexPath.row)
}
else {
// If next cell is not nil, then cell is not expanded
if(destinationData?[indexPath.row+1] != nil) {
expandCell(tableView: tableView, index: indexPath.row)
} else {
contractCell(tableView: tableView, index: indexPath.row)
}
}
}
}
I dont really know how to solve this, all i know is that the plus function gets out of range on this bit "let data = rowData.menuItems![flightIndex]".

Creating A Numbered List In UITextView Swift 3

I am trying to make a numbered list out of the information the user inputs into a UITextView. For example,
List item one
List item two
List item three
Here is the code that I have tried but does not give me the desired effect.
var currentLine: Int = 1
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Add "1" when the user starts typing into the text field
if (textView.text.isEmpty && !text.isEmpty) {
textView.text = "\(currentLine). "
currentLine += 1
}
else {
if text.isEmpty {
if textView.text.characters.count >= 4 {
let str = textView.text.substring(from:textView.text.index(textView.text.endIndex, offsetBy: -4))
if str.hasPrefix("\n") {
textView.text = String(textView.text.characters.dropLast(3))
currentLine -= 1
}
}
else if text.isEmpty && textView.text.characters.count == 3 {
textView.text = String(textView.text.characters.dropLast(3))
currentLine = 1
}
}
else {
let str = textView.text.substring(from:textView.text.index(textView.text.endIndex, offsetBy: -1))
if str == "\n" {
textView.text = "\(textView.text!)\(currentLine). "
currentLine += 1
}
}
}
return true
}
as suggested here: How to make auto numbering on UITextview when press return key in swift but had no success.
Any help is much appreciated.
You can subclass UITextView, override method willMove(toSuperview and add an observer for UITextViewTextDidChange with a selector that break up your text into lines enumerating and numbering it accordingly. Try like this:
class NumberedTextView: UITextView {
override func willMove(toSuperview newSuperview: UIView?) {
frame = newSuperview?.frame.insetBy(dx: 50, dy: 80) ?? frame
backgroundColor = .lightGray
NotificationCenter.default.addObserver(self, selector: #selector(textViewDidChange), name: .UITextViewTextDidChange, object: nil)
}
func textViewDidChange(notification: Notification) {
var lines: [String] = []
for (index, line) in text.components(separatedBy: .newlines).enumerated() {
if !line.hasPrefix("\(index.advanced(by: 1))") &&
!line.trimmingCharacters(in: .whitespaces).isEmpty {
lines.append("\(index.advanced(by: 1)). " + line)
} else {
lines.append(line)
}
}
text = lines.joined(separator: "\n")
// this prevents two empty lines at the bottom
if text.hasSuffix("\n\n") {
text = String(text.characters.dropLast())
}
}
}
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textView = NumberedTextView()
view.addSubview(textView)
}
}

UICollectionView reloadData() crashes after emtying datasource dictionary

I have a UICollectionView which takes the results of an API search. The search is triggered by the following code. The results are appended to a dictionary [[String: Any]] and I call self.collectionView.reloadData() after my query completes.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var newValue = textField.text!
let location = min(range.location, newValue.characters.count)
let startIndex = newValue.characters.index(newValue.startIndex, offsetBy: location)
let endIndex = newValue.characters.index(newValue.startIndex, offsetBy: location + range.length)
let newRangeValue = Range<String.Index>(startIndex ..< endIndex)
newValue.replaceSubrange(newRangeValue, with: string)
searchView.searchFieldValueChanged(newValue)
return true
}
Then, if I want to change the search string and search again I want to empty the dictionary and call reloadData() again I get an app crash.
The error is
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist:
Here is my datasource implementation
var searchResults = [[String: Any]]()
let layout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10)
layout.estimatedItemSize.height = 200
layout.estimatedItemSize.width = 200
layout.minimumInteritemSpacing = 10
layout.minimumLineSpacing = 10
return layout
}()
collectionView = UICollectionView(frame: self.frame, collectionViewLayout: layout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(LanesCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
collectionView.backgroundColor = .yellow // Constants.APP_BACKGROUND_COLOR
collectionView.alwaysBounceVertical = true
collectionView.clipsToBounds = true
collectionView.translatesAutoresizingMaskIntoConstraints = false
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if searchResults.count == 0 {
collectionView.alpha = 0.0
} else {
collectionView.alpha = 1.0
}
return searchResults.count
}
after query
func parseMoreData(jsonData: [String: Any]) {
let items = jsonData["items"] as! [[String: Any]]
self.collectionView.layoutIfNeeded()
self.collectionView.reloadData()
}
}
func searchFieldValueChanged(_ textValue: String) {
searchResults = []
This looks fixed by using this instead of layoutIfNeeded()
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()