How to use two UIPickerViews in one view controller? - xcode

I have two UIPickerControllers in one view controller. I can get one to work, but when I add a second, my app crashes. Here is the code I use for one picker view:
import UIKit
class RegisterJobPosition: UIViewController, UIPickerViewDelegate {
#IBOutlet weak var positionLabel: UILabel!
var position = ["Lifeguard", "Instructor", "Supervisor"]
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfComponentsInPickerView(PickerView: UIPickerView!) -> Int
{
return 1
}
func pickerView(pickerView: UIPickerView!, numberOfRowsInComponent component: Int) -> Int
{
return position.count
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String!
{
return position[row]
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
positionLabel.text = position[row]
}
}
Now, how can I get a second picker to work? Say my second picker view is called location (the other one is called position). I tried duplicating the code within the picker view methods for location but it doesn't work.

Here is my solution:
in the storyboard, add two UIPickerView instances to your view
set the first picker's tag as 1 and set 2 for the second picker under the "Attributes Inspector"
control + drag from each picker to the top yellow view controller icon and choose dataSource. Repeat the same choosing delegate
add UIPickerViewDataSource and UIPickerViewDelegate to your view controller:
class ViewController: UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
in your view controller class, create empty arrays for the pickers:
var picker1Options = []
var picker2Options = []
In viewDidLoad(), populate the arrays with your content:
picker1Options = ["Option 1","Option 2","Option 3","Option 4","Option 5"]
picker2Options = ["Item 1","Item 2","Item 3","Item 4","Item 5"]
implement the delegate and data source methods:
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView.tag == 1 {
return picker1Options.count
} else {
return picker2Options.count
}
}
func pickerView(pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String! {
if pickerView.tag == 1 {
return "\(picker1Options[row])"
} else {
return "\(picker2Options[row])"
}
}

Based on the information I have in the question, I'd say that you need to set up the data source & delegate methods to handle the ability to distinguish between which picker instance is calling them.
Using the tag property on the picker view is one strategy.
There should be some if/else or switch statements in the methods that have varying logic depending on whether it's the location or the position picker that's being referenced.

I found this to work.
class SecondViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
#IBOutlet weak var textbox1: UILabel!
#IBOutlet weak var textbox2: UILabel!
#IBOutlet weak var dropdown1: UIPickerView!
#IBOutlet weak var dropdown2: UIPickerView!
var age = ["10-20", "20-30", "30-40"]
var Gender = ["Male", "Female"]
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
var countrows : Int = age.count
if pickerView == dropdown2 {
countrows = self.Gender.count
}
return countrows
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
if pickerView == dropdown1 {
let titleRow = age[row]
return titleRow
} else if pickerView == dropdown2 {
let titleRow = Gender[row]
return titleRow
}
return ""
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if pickerView == dropdown1 {
self.textbox1.text = self.age[row]
} else if pickerView == dropdown2 {
self.textbox2.text = self.Gender[row]
}
}
}

My background is in Android but my answer is very OOP. I would suggest creating different classes to implement the DataSource and Delegate like this:
class PositionDataSourceDelegate : NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
var position = ["Lifeguard", "Instructor", "Supervisor"]
var selectedPosition : String?
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return position.count
}
func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
return position[row]
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedPosition = position[row]
}
}
and then another one for the Location:
class LocationDataSourceDelegate : NSObject, UIPickerViewDelegate, UIPickerViewDataSource {
var location = ["Up", "Down", "Everywhere"]
var selectedLocation : String?
func numberOfComponentsInPickerView(pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return location.count
}
func pickerView(pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? {
return location[row]
}
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectedLocation = location[row]
}
}
then in your RegisterJobPosition you need to create an instance of each:
let positionDSD = PositionDataSourceDelegate()
let locationDSD = LocationDataSourceDelegate()
and assign them to the pickers like this:
positionPicker.dataSource = positionDSD
positionPicker.delegate = positionDSD
locationPicker.dataSource = locationDSD
locationPicker.delegate = locationDSD
and you can access the selected position and location using:
positionDSD.selectedPosition
locationDSD.selectedLocation
Hope this helps you and others and I'm also hoping for some constructive comments of why this is not "swifty"

I think the biggest issue and different to Java is that Java easily allow for attributes to be passed through the constructor. e.g. you could declare class LocationDataSourceDelegate as generic and call it genericDataSourceDelegate and make the constructor accept and Array public genericDataSourceDelegate (String data[]) and be able to make one class where could just simply create objects of. You just instantiate it and pass location the constructor like genericDataSourceDelegate (location)
The problem with your model you will have to create as many delegate classes in one program which is a strain to your compiler.

You should NEVER use tags!!!!!
The best way for me was:
Create an enum, for example MyPickerViewType
enum MyPickerViewType {
case first
case second
}
Create CustomPickerView class that conforms to UIPickerView and add type parameter
class CustomPickerView: UIPickerView {
let type: MyPickerViewType
init(type: MyPickerViewType) {
self.type = type
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
}
Initialise custom pickerViews in UIViewController
private lazy var firstPickerView: TimeLogCardPickerView = {
let pickerView = TimeLogCardPickerView(type: .first)
pickerView.delegate = self
pickerView.dataSource = self
return pickerView
}()
private lazy var secondPickerView: TimeLogCardPickerView = {
let pickerView = TimeLogCardPickerView(type: .second)
pickerView.delegate = self
pickerView.dataSource = self
return pickerView
}()
Usage
extension ViewController: UIPickerViewDelegate, UIPickerViewDataSource {
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
let customPickerView = pickerView as? CustomPickerView
switch customPickerView?.type {
case .first:
return 2
case .second:
return 5
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
let customPickerView = pickerView as? CustomPickerView
switch customPickerView?.type {
case .first:
return firstTitleArray[row]
case .second:
return secondTitleArray[row]
}
}
}

Related

Searchbar only searches in first section - actionGoal or goalTitle

This is the data model I used to import a json file.
struct ActionResult: Codable {
let data: [Datum]
}
struct Datum: Codable {
let goalTitle, goalDescription, goalImage: String
let action: [Action]
}
struct Action: Codable {
let actionTitle: String
let actionGoal: String
Now I am trying to create a searchbar, but it only searches in one section, although I have not defined a section in the data model but it is picking either the goalTitle or actionGoal.
var index = 0
var action: Action? // this is for the segue
var result: ActionResult? {
didSet {
guard let result = result else { return }
allSectionDataActionMap = Dictionary(uniqueKeysWithValues: result.data.enumerated().map { ($0.0, ($0.1, $0.1.action)) })
updateFilteredData()
}
}
var allSectionDataActionMap = [Int: (datum: Datum, actions: [Action])]()
// Maps the section index to the Datum & filtered [Action]
var filteredSectionDataActions = [Int: (datum: Datum, actions: [Action])]()
let searchController = UISearchController()
This is part of the setup code.
override func numberOfSections(in tableView: UITableView) -> Int {
return filteredSectionDataActions.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredSectionDataActions[section]?.actions.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ActionTableView.dequeueReusableCell(withIdentifier: "ActionCell", for: indexPath) as! ActionTableCell
let action = filteredSectionDataActions[indexPath.section]?.actions[indexPath.row]
// setup cell for action
cell.actionItem.text = action?.actionTitle
cell.actionImage.image = UIImage(named: action!.actionImage)
cell.actionImage.layer.cornerRadius = cell.actionImage.frame.size.width / 2
cell.actionGoal.text = action?.actionGoal
// cell.actionBenefit.text = action?.actionBenefit
// cell.actionCalculator.text = action?.actionCalculator
This is the table view code
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
updateFilteredData(for: searchText.lowercased())
tableView.reloadData()
}
func updateFilteredData(for searchText: String = String()) {
if searchText.isEmpty {
filteredSectionDataActions = allSectionDataActionMap
} else {
for (index, (datum, actions)) in allSectionDataActionMap {
let filteredActions = actions.filter { $0.actionTitle.lowercased().contains(searchText) }
if filteredActions.isEmpty {
filteredSectionDataActions[index] = (datum, actions)
} else {
filteredSectionDataActions[index] = (datum, filteredActions)
}
This is for the searchbar.

Finding click location in SwiftUI on MacOS

I've tried to adapt a solution I've used on an iOS app to macOS using NSViewRepresentable instead of UIViewRepresentable.
Below is my 'Tappable View'. My problem is that when I try to use this view I get the error Cannot find "TappableView" in scope.
Thanks.
(using Xcode Version 12.0 beta 4)
import Foundation
import SwiftUI
struct TappableView: NSViewRepresentable {
var tappedCallback: ((CGPoint, Int) -> Void)
func makeNSView(context: NSViewRepresentableContext<TappableView>) -> NSView {
let v = UIView(frame: .zero)
let gesture = NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
gesture.numberOfTapsRequired = 1
let gesture2 = NSClickGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.doubleTapped))
gesture2.numberOfTapsRequired = 2
gesture.require(toFail: gesture2)
v.addGestureRecognizer(gesture)
v.addGestureRecognizer(gesture2)
return v
}
class Coordinator: NSObject {
var tappedCallback: ((CGPoint, Int) -> Void)
init(tappedCallback: #escaping ((CGPoint, Int) -> Void)) {
self.tappedCallback = tappedCallback
}
#objc func tapped(gesture:NSClickGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point, 1)
}
#objc func doubleTapped(gesture:NSClickGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point, 2)
}
}
func makeCoordinator() -> TappableView.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<TappableView>) {
}
}
Here is working variant
struct TappableView: NSViewRepresentable {
var tappedCallback: ((CGPoint, Int) -> Void)
func makeNSView(context: NSViewRepresentableContext<TappableView>) -> NSView {
let v = NSView(frame: .zero)
context.coordinator.configure(view: v)
return v
}
class Coordinator: NSObject, NSGestureRecognizerDelegate {
var tappedCallback: ((CGPoint, Int) -> Void)
private var gesture: NSClickGestureRecognizer!
private var gesture2: NSClickGestureRecognizer!
init(tappedCallback: #escaping ((CGPoint, Int) -> Void)) {
self.tappedCallback = tappedCallback
}
func configure(view: NSView) {
gesture = NSClickGestureRecognizer(target: self, action: #selector(Coordinator.tapped))
gesture.delegate = self
gesture.numberOfClicksRequired = 1
gesture2 = NSClickGestureRecognizer(target: self, action: #selector(Coordinator.doubleTapped))
gesture2.delegate = self
gesture2.numberOfClicksRequired = 2
view.addGestureRecognizer(gesture)
view.addGestureRecognizer(gesture2)
}
#objc func tapped(gesture:NSClickGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point, 1)
}
#objc func doubleTapped(gesture:NSClickGestureRecognizer) {
let point = gesture.location(in: gesture.view)
self.tappedCallback(point, 2)
}
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
return gestureRecognizer === gesture && otherGestureRecognizer === gesture2
}
}
func makeCoordinator() -> TappableView.Coordinator {
return Coordinator(tappedCallback:self.tappedCallback)
}
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<TappableView>) {
}
}
A better solution is to use a combination of SwiftUI gestures instead of a NSViewRepresentable solution as it better integrates with SwiftUI API. The issue with the other solution proposed is that you cannot use .highPriorityGesture() or any other Gesture API if you want to compose complexe gestures.
Here is a working implementation:
struct ClickGesture: Gesture {
let count: Int
let coordinateSpace: CoordinateSpace
typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
precondition(count > 0, "Count must be greater than or equal to 1.")
self.count = count
self.coordinateSpace = coordinateSpace
}
var body: SimultaneousGesture<TapGesture, DragGesture> {
SimultaneousGesture(
TapGesture(count: count),
DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
)
}
func onEnded(perform action: #escaping (CGPoint) -> Void) -> some Gesture {
ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded { (value: Value) -> Void in
guard value.first != nil else { return }
guard let location = value.second?.startLocation else { return }
guard let endLocation = value.second?.location else { return }
guard ((location.x-1)...(location.x+1)).contains(endLocation.x),
((location.y-1)...(location.y+1)).contains(endLocation.y) else {
return
}
action(location)
}
}
}
extension View {
func onClickGesture(
count: Int,
coordinateSpace: CoordinateSpace = .local,
perform action: #escaping (CGPoint) -> Void
) -> some View {
gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
.onEnded(perform: action)
)
}
func onClickGesture(
count: Int,
perform action: #escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: count, coordinateSpace: .local, perform: action)
}
func onClickGesture(
perform action: #escaping (CGPoint) -> Void
) -> some View {
onClickGesture(count: 1, coordinateSpace: .local, perform: action)
}
}
You can use it via the convenient API .onClickGesture(count:perform:) like any other SwiftUI gesture.

Custom Delegate Filter VC Swift5

I'm adding a custom delegate to my app and, for some reason, it is not working.
My app has a map where I show several markers of different company types. There is also a button that, once pressed, takes me to another viewController where the user can input some filters. The user then presses "Apply" which would pass the filtering data to the map viewController.
The issue here is that no data is being passed.
As reference I followed the guideline https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef which works perfectly fine.
Here is the full project code https://github.com/afernandes0001/Custom-Delegate
I use Firebase but code below just shows pieces related to the delegate.
mapViewController - you will notice that I added a print to the prepareForSegue. When first loading the app and clicking "Search" button it shows nav1 as nil (which is expected) but, if I click Search and Apply (in filterVC), that print is never done.
import UIKit
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate, FilterVCDelegate {
#IBOutlet weak var map: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
map.register(MyAnnotationView.self, forAnnotationViewWithReuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "clinicDetailsSegue" {
let clinicsDetailsViewController = segue.destination as! ClinicsDetailsViewController
clinicsDetailsViewController.id = self.note.mapId
} else if segue.identifier == "searchSegue" {
print("segue call")
let nav1 = segue.destination as? UINavigationController
print("nav1 \(nav1)")
if let nav = segue.destination as? UINavigationController, let filterVC = nav.topViewController as? FilterViewController {
filterVC.delegate = self
}
}
}
func chosenData(clinicNameFilter: String, stateFilter: String, cityFilter: String, esp1Filter: String, esp2Filter: String) {
print("Received data \(clinicNameFilter), \(stateFilter), \(cityFilter), \(esp1Filter), \(esp2Filter)")
}
}
FilterViewController
import UIKit
protocol FilterVCDelegate: class {
func chosenData(clinicNameFilter: String, stateFilter: String, cityFilter: String, esp1Filter: String, esp2Filter: String)
}
class FilterViewController: UIViewController, UIPickerViewDelegate, UIPickerViewDataSource {
weak var delegate: FilterVCDelegate?
var selectedName = ""
var statesJSON = [Estado]()
var cities = [Cidade]()
var state : Estate? // Selected State identifier
var city : City? // Selected City identifier
var selectedState = "" // Used to retrieve info from Firebase
var selectedCity = "" // Used to retrieve info from Firebase
var specialtiesJSON = [Specialty]()
var specialties2 = [Specialty2]()
var specialty1 : Specialty? // Selected Specialty1 identifier
var specialty2 : Specialty2? // Selected Specialty2 identifier
var selectedSpecialty1 = ""
var selectedSpecialty2 = ""
#IBOutlet weak var clinicName: UITextField!
#IBOutlet weak var statePicker: UIPickerView!
#IBOutlet weak var esp1Picker: UIPickerView!
#IBOutlet weak var esp2Picker: UIPickerView!
override func viewDidLoad() {
readJsonStates()
readJsonSpecialties()
super.viewDidLoad()
clinicName.text = ""
}
#IBAction func applyFilter(_ sender: Any) {
if clinicName.text == nil {
clinicName.text = ""
}
if selectedState != "" {
if selectedCity != "" {
if selectedSpecialty1 != ""{
if selectedSpecialty2 != "" {
delegate?.chosenData(clinicNameFilter: clinicName.text!, stateFilter: selectedState, cityFilter: selectedCity, esp1Filter: selectedSpecialty1, esp2Filter: selectedSpecialty2)
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController]
self.navigationController?.popToViewController(viewControllers[viewControllers.count - 2], animated: true)
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
} else {
print("Fill in all filter data")
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
statePicker.reloadAllComponents()
if pickerView == statePicker {
if component == 0 {
self.state = self.statesJSON[row]
self.coties = self.statesJSON[row].cities
statePicker.reloadComponent(1)
statePicker.selectRow(0, inComponent: 1, animated: true)
} else {
self.city = self.cities[row]
statePicker.reloadAllComponents()
}
} else if pickerView == esp1Picker {
self.specialty1 = self.specialtiesJSON[row]
self.specialties2 = self.specialtiesJSON[row].specialty2
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
esp2Picker.selectRow(0, inComponent: 0, animated: true)
} else if pickerView == esp2Picker {
self.specialty2 = self.specialties2[row]
esp1Picker.reloadComponent(0)
esp2Picker.reloadComponent(0)
}
let indexSelectedState = statePicker.selectedRow(inComponent: 0)
let indexSelectedCity = statePicker.selectedRow(inComponent: 1)
let indexSelectedEsp1 = esp1Picker.selectedRow(inComponent: 0)
let indexSelectedEsp2 = esp2Picker.selectedRow(inComponent: 0)
if indexSelectedState >= 0 {
if indexSelectedCity >= 0 {
selectedState = estadosJSON[indexSelectedState].name
selectedCity = cidades[indexSelectedCity].name
}
}
if indexSelectedEsp1 >= 0 {
if indexSelectedEsp2 >= 0 {
selectedSpecialty1 = specialtiesJSON[indexSelectedEsp1].name
selectedSpecialty2 = specialtiesJSON[indexSelectedEsp1].specialty2[indexSelectedEsp2].name
}
}
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
if pickerView == statePicker {
return 2
} else if pickerView == esp1Picker {
return 1
} else if pickerView == esp2Picker {
return 1
}
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
if pickerView == statePicker {
if component == 0 {
return statesJSON.count
} else {
return cities.count
}
} else if pickerView == esp1Picker {
return self.specialtiesJSON.count
} else if pickerView == esp2Picker {
return specialties2.count
}
return 1
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
var rowTitle = ""
let pickerLabel = UILabel()
pickerLabel.textColor = UIColor.black
if pickerView == statePicker {
if component == 0 {
rowTitle = statesJSON[row].name
} else {
rowTitle = cities[row].name
}
} else if pickerView == esp1Picker {
rowTitle = specialtiesJSON[row].name
} else if pickerView == esp2Picker {
rowTitle = specialties2[row].name
}
pickerLabel.text = rowTitle
pickerLabel.font = UIFont(name: fontName, size: 16.0)
pickerLabel.textAlignment = .center
return pickerLabel
}
func pickerView(_ pickerView: UIPickerView, widthForComponent component: Int) -> CGFloat {
if pickerView == statePicker {
if component == 0 {
return 50
} else {
return 300
}
}
return 300
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
func readJsonStates() {
let url = Bundle.main.url(forResource: "StatesAndCities", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootState.self, from: data)
//handles the array of countries on your json file.
self.statesJSON = jsonResult.state
self.cities = self.statesJSON.first!.cities
} catch {
}
}
func readJsonSpecialties() {
let url = Bundle.main.url(forResource: "Specialties", withExtension: "json")!
do {
let data = try Data(contentsOf: url)
let jsonResult = try JSONDecoder().decode(RootEsp.self, from: data)
//handles the array of specialties on your json file.
self.specialtiesJSON = jsonResult.specialty
self.specialties2 = self.specialtiesJSON.first!.specialty2
} catch {
}
}
}
Any idea why, when I click ApplyFilter, delegate is not updated in the MapViewController?
Thanks
I found the error in my project.
The issue was with my Navigation Controller.
When I posted the error above, my Storyboard looked like the below
To make it work, I added the Navigation Controller to the Filter View Controller as below
That did the work and protocol is working as expected.

How to sort NSTableView with custom class data

I am using cell-based tableview to store custom class data, for example
class Person {
var name:String = ""
var age: Int = 0
}
And then use a NSMUtableArray to collect person1, person2 ... datas, call it dataArray.
Present Person name on column1 and age on column2, works fine and editable.
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
if tableColumn?.identifier == "Name"
{
return (self.dataArray[row] as! Person).name
}
else if tableColumn?.identifier == "Age"
{
return (self.dataArray[row] as! Person).age
}
}
How can I sort each column with respected sortDescriptor?
I follow that fefernce but "sortDescriptorWithKey:tableColumn.identifier" always fail in my code, it can be passed if set the tableColumn.identifier to "self", it also confused me.
Below are my other code fragments.
func tableView(_ tableView: NSTableView, sortDescriptorsDidChange oldDescriptors: [NSSortDescriptor]) {
let newDescriptors = tableView.sortDescriptors
dataArray.sort(using: newDescriptors)
tableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
// Sort
for tableColumn in tableView.tableColumns {
let sortDescrptor = NSSortDescriptor.init(key: tableColumn.identifier, ascending: true, selector: #selector(NSString.caseInsensitiveCompare))
tableColumn.sortDescriptorPrototype = sortDescrptor
}
}

Reordering Realm.io data in tableView with Swift

I have implemented a basic example of an ios app using Realm.io
I'd like to be able to reorder table rows in my iOS app and save the order back to Realm.
Realm model contains a property called position for this purpose.
P.S: Sorry for so much code.
import UIKit
import Realm
class Cell: UITableViewCell {
var position: Int!
init(style: UITableViewCellStyle, reuseIdentifier: String!) {
super.init(style: .Subtitle, reuseIdentifier: reuseIdentifier)
}
}
class Language: RLMObject {
var title = ""
var position = Int()
}
class ManagerLanguagesController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
var array = RLMArray()
var notificationToken: RLMNotificationToken?
var editButton = UIBarButtonItem()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
notificationToken = RLMRealm.defaultRealm().addNotificationBlock { note, realm in
self.reloadData()
}
reloadData()
}
override func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
return Int(array.count)
}
func setupUI() {
tableView.registerClass(Cell.self, forCellReuseIdentifier: "cell")
self.title = "Languages"
var addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "add")
editButton = UIBarButtonItem(title: "Edit", style: .Plain, target: self, action: "edit")
var buttons = [addButton, editButton]
self.navigationItem.rightBarButtonItems = buttons
}
func add() {
var addLanguageView:UIViewController = self.storyboard.instantiateViewControllerWithIdentifier("newLanguage") as UIViewController
self.navigationController.presentViewController(addLanguageView, animated: true, completion: nil)
}
func edit () {
if tableView.editing {
/* FROM THIS POINT I'M PROBABLY DOING SOMETHING WRONG.. IT IS NOT WORKING */
var positionArray = NSMutableArray()
let realm = RLMRealm.defaultRealm()
var i = 0
for var row = 0; row < tableView.numberOfRowsInSection(0); row++ {
var cellPath = NSIndexPath(forRow: row, inSection: 0)
var cell:Cell = tableView.cellForRowAtIndexPath(cellPath) as Cell
positionArray.addObject(cell.position)
}
realm.beginWriteTransaction()
for row: RLMObject in array {
row["position"] = positionArray[i]
i++
}
realm.commitWriteTransaction()
/* -- NOT WORKING END -- */
tableView.setEditing(false, animated: true)
editButton.style = UIBarButtonItemStyle.Plain
editButton.title = "Edit"
} else{
tableView.setEditing(true, animated: true)
editButton.title = "Done"
editButton.style = UIBarButtonItemStyle.Done
}
}
override func tableView(tableView: UITableView?, cellForRowAtIndexPath indexPath: NSIndexPath?) -> UITableViewCell? {
let cell = tableView!.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as Cell
let object = array[UInt(indexPath!.row)] as Language
cell.textLabel.text = object.title
cell.position = object.position // I have implemented this to be able to retain initial positions for each row and maybe use this when reordering..
return cell
}
override func tableView(tableView: UITableView!, canMoveRowAtIndexPath indexPath: NSIndexPath!) -> Bool {
return true
}
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
// println("Old index: \(sourceIndexPath.indexAtPosition(sourceIndexPath.length - 1)+1)")
// println("New index: \(destinationIndexPath.indexAtPosition(sourceIndexPath.length - 1)+1)")
// Maybe something needs to be implemented here instead...
}
func reloadData() {
array = Language.allObjects().arraySortedByProperty("position", ascending: true)
tableView.reloadData()
}
}
Thanks in advance
Instead of using a position property, you could instead keep an ordered array as a property on another object. This way you don't have to keep the position up to date and instead arrange your objects as needed:
class Language: RLMObject {
dynamic var title = ""
}
class LanguageList: RLMObject {
dynamic var languages = RLMArray(objectClassName: "Language")
}
class ManagerLanguagesController: UITableViewController, UITableViewDelegate, UITableViewDataSource {
override func viewDidLoad() {
super.viewDidLoad()
// create our list
var realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
realm.addObject(LanguageList())
realm.commitWriteTransaction()
...
}
// helper to get the RLMArray of languages in our list
func array() -> RLMArray {
return (LanguageList.allObjects().firstObject() as LanguageList).languages
}
override func tableView(tableView: UITableView!, moveRowAtIndexPath sourceIndexPath: NSIndexPath!, toIndexPath destinationIndexPath: NSIndexPath!) {
var languages = array()
var object = languages.objectAtIndex(UInt(sourceIndexPath.row)) as Language
var realm = RLMRealm.defaultRealm()
realm.beginWriteTransaction()
languages.removeObjectAtIndex(UInt(sourceIndexPath.row))
languages.insertObject(object, atIndex: UInt(destinationIndexPath.row))
realm.commitWriteTransaction()
}
...
}
this work for me to move rows in tableview using realm with swift 2.2:
func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
let aux = TimesHome.mutableCopy() as! NSMutableArray
let itemToMove = aux[fromIndexPath.row]
let realm = try! Realm()
realm.beginWrite()
aux.removeObjectAtIndex(fromIndexPath.row)
aux.insertObject(itemToMove, atIndex: toIndexPath.row)
try! realm.commitWrite()
TimesHome = aux
let times = realm.objects(ParciaisTimes)
if times.count > 0 {
for tm in times {
for i in 1...aux.count {
if aux[i-1].valueForKey("time_id") as! Int == tm.time_id {
realm.beginWrite()
tm.ordem = i
try! realm.commitWrite()
}
}
}
}
}

Resources