I am using uitableview. I am trying to save the placemark when i have selected a cell. From the code below i have added a tableview and when i select a cell the placemark will be shown. But i'm having trouble saving it because when i go to another view controller and go back it doesn't show. I have researched and find that i need to use UserDefaults but i have no clue how to use it. can someone point me how can i achieve this. Thanks
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
let contactsCell = app.helper.contacts[indexPath.row]
cell!.textLabel?.text = contactsCell
return cell!
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let contactToCall = app.helper.contacts[indexPath.row]
app.helper.contactSelected = contactToCall
if let cell = tableView.cellForRow(at: indexPath) {
if cell.isSelected {
cell.accessoryType = .checkmark
}
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.accessoryType = .none
}
}
You don't need UserDefaults unless you want to maintain state after the app has been closed and restarted, and once you start storing user defaults, you're likely to want to store more - so it makes sense to create a defaults data class to store the selected index and any other 'stuff' you may need.
In this example, I'm going to load the defaults on viewDidLoad and store the defaults on each select / deselect. If you have other values stored, you should have additional storage calls.
Create a class for storing your data
class MyDefaultsData : NSObject, NSCoding
{
var selectedIndexPath : IndexPath?
var otherStuff : String?
override init()
{
// you can set up default values here if you need them
selectedIndexPath = nil
otherStuff = nil
}
// archiving code
func encode(with aCoder: NSCoder)
{
// you can't store IndexPath directly, so split it into row and section
if selectedIndexPath == nil
{
// set invalid values which we can identify later
aCoder.encode(-1, forKey: "indexSection")
aCoder.encode(-1, forKey: "indexRow")
}
else
{
aCoder.encode(selectedIndexPath!.section, forKey: "indexSection")
aCoder.encode(selectedIndexPath!.row, forKey: "indexRow")
}
aCoder.encode(otherStuff, forKey: "otherStuff")
}
required init(coder aDecoder: NSCoder)
{
let indexSection = aDecoder.decodeInteger(forKey: "indexSection")
let indexRow = aDecoder.decodeInteger(forKey: "indexRow")
if indexSection != -1 && indexRow != -1 // if no row is selected, these values will both be -1
{
selectedIndexPath = IndexPath(row: indexRow, section: indexSection)
}
else
{
selectedIndexPath = nil
}
self.otherStuff = aDecoder.decodeObject(forKey: "otherStuff") as? String
}
}
Define a variable of your default data within the ViewController
var defaultData = MyDefaultsData()
Load the data in your viewDidLoad
override func viewDidLoad()
{
loadDefaults()
}
func loadDefaults()
{
var dataDefaults : MyDefaultsData?
if let data = UserDefaults.standard.object(forKey: "NSDefaultsTest") as? Data
{
dataDefaults = NSKeyedUnarchiver.unarchiveObject(with: data) as? MyDefaultsData
self.defaultData.otherStuff = dataDefaults?.otherStuff ?? "No value found"
self.defaultData.selectedIndexPath = dataDefaults?.selectedIndexPath
}
}
then you use that default data within cellForRowAt
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")
let contactsCell = app.helper.contacts[indexPath.row]
cell!.textLabel?.text = contactsCell
// set or clear the checkmark
if selectedIndexPath == indexPath
{
cell.accessoryType = .checkmark
}
else
{
cell.accessoryType = .none
}
return cell!
}
When you change the selected row, you need to reload both the previous selected row (if any) and the currently selected row, to get the checkmark drawn
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let previousIndexPath = defaultData.selectedIndexPath
if previousIndexPath != indexPath
{
defaultData.selectedIndexPath = indexPath
}
else
{
defaultData.selectedIndexPath = nil
}
storeDefaults()
if previousIndexPath != nil
{
tableView.reloadRows(at: [previousIndexPath!], with: .automatic)
}
if defaultData.selectedIndexPath != nil
{
tableView.reloadRows(at: [defaultData.selectedIndexPath!], with: .automatic)
}
}
Related
I am trying to load the updated search results but it doesn't populate the table view.
I used this link https://www.thorntech.com/how-to-search-for-location-using-apples-mapkit/ which belongs to the previous versions but it still works very well except showing the local search results. Please help
class LocationSearchTable : UITableViewController, UISearchResultsUpdating {
var matchingItems:[MKMapItem] = []
var mapView: MKMapView? = nil
}
extension LocationSearchTable {
func updateSearchResults(for searchController: UISearchController) {
guard let MapView = mapView,
let searchBarText = searchController.searchBar.text else { return }
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = searchBarText
request.region = MapView.region
let search = MKLocalSearch(request: request)
search.start { response, _ in
guard let response = response else {
print("No response")
return
}
self.matchingItems = response.mapItems
self.tableView.reloadData()
}
}
}
extension LocationSearchTable {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return matchingItems.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")!
let selectedItem = matchingItems[indexPath.row].placemark
cell.textLabel?.text = selectedItem.name
cell.detailTextLabel?.text = ""
return cell
}
}
//use IndexPath rather than NSIndexPath and you need to use
//override
override func tableView(_ tableView: UITableView,
cellForRowAtIndexPath
indexPath: IndexPath) -> UITableViewCell {
let cell =tableView.dequeueReusableCell(withIdentifier:"cell")!
let selectedItem = matchingItems[indexPath.row].placemark
cell.textLabel?.text = selectedItem.name
cell.detailTextLabel?.text = ""
return cell
}
Hope it is not too late to answer you!
I have a weird situation where I swipe a cell to grey it out and it greys every 4th or 6th cell instead of only the single cell that was swiped.
The tableview is initialized as follows:
func setupView() {
view.backgroundColor = .white
tableView.register(EntityCell.self, forCellReuseIdentifier: "entityCell")
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
}
Here is my query to get the data:
func getEntities(taxId : String) {
dispatchGroup.enter()
db.collection("Taxonomy").whereField("entityId", isEqualTo: entityId).whereField("status", isEqualTo: 401).getDocuments { (orderSnapshot, orderError) in
if orderError != nil {
self.showError(show: "Error", display: orderError!.localizedDescription)
} else {
self.entitiesArray.append(contentsOf: (orderSnapshot?.documents.compactMap({ (orderDocuments) -> Order in
Order(dictionary: orderDocuments.data(), invoiceId: orderDocuments.documentID, opened: false)!
}))!)
self.dispatchGroup.leave()
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
Here are the standard override functions to populate the tableview:
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return entitiesArray.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "entityCell", for: indexPath) as? EntityCell else { return UITableViewCell() }
let entityRow = entitiesArray[indexPath.row]
cell.selectionStyle = .none
cell.setTaxonomy(entity: entityRow) // Setting up the cell with the array values
return cell
}
Everything is working fine upto this point. And finally here is the override func for swipe action:
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let complete = UIContextualAction(style: .normal, title: "Verified") { (action, view, completionHandler) in
self.db.collection("Taxonomy").document(self.entitiesArray[indexPath.row].entityId).updateData(["status": 411]) { (error) in
if error == nil {
let cell = tableView.cellForRow(at: indexPath) as? EntityCell
cell?.changeStatus(currentEntity: self.entitiesArray[indexPath.row])
}
}
completionHandler(true)
}
complete.image = UIImage(named: "icon_approved")
complete.backgroundColor = UIColor(hex: Constants.Colors.secondary)
let swipe = UISwipeActionsConfiguration(actions: [complete])
return swipe
}
So I swipe right from the trailing edge of the cell and I see the underlying color and icon as expected. And the cell turns grey via this function via a protocol:
extension EntityCell : EntityStatusDelegate {
func changeStatus(currentEntity: EntityObject) {
entityCellBackground.backgroundColor = .systemGray4
}
}
The cell turns grey. And then I scroll down and I see every 4th or 6th cell is grey as well. Any idea what is going wrong? I am pretty flummoxed at this point.
Cells get recycled. You need either configure them completely or overwrite the prepareForReuse function of the cell or give each cell an unique reuseidentifyer so the tableview can recycle them.
(Last option is the worst as it cost a lot more memory)
Option 1:
Just set the backgroundcolor:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "entityCell", for: indexPath) as? EntityCell else { return UITableViewCell() }
let entityRow = entitiesArray[indexPath.row]
cell.selectionStyle = .none
cell.setTaxonomy(entity: entityRow) // Setting up the cell with the array values
cell.entityCellBackground.backgroundColor = (whatever the default color is)
return cell
}
It's easier to explain by example. I have original array which is searched and filtered array with searched items. If i found one item after searching and tap on it, i mark it as done (I have todo list), but when i cancel my search, I find that the first element in the original array is marked, not the third item.
I googled some threads and found almost similar problems, but solutions doesn't suit to my problem. For example:
didSelectRowAtIndexPath indexpath after filter UISearchController - Swift
And here some code. Especially at didSelectRowAt I mark the items to done. Does anyone have any ideas?
private var searchBarIsEmpty: Bool {
guard let text = searchController.searchBar.text else { return false }
return text.isEmpty
}
private var isFiltering: Bool {
return searchController.isActive && !searchBarIsEmpty
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isFiltering {
return filteredTasks?.count ?? 0
}
return manager.tasks.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
var currentItem: Task
if isFiltering {
currentItem = filteredTasks?[indexPath.row] ?? manager.tasks[indexPath.row]
} else {
currentItem = manager.tasks[indexPath.row]
}
cell.titleLabel.text = currentItem.taskName
cell.descriptionLabel.text = currentItem.description
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let accessoryType: UITableViewCell.AccessoryType = manager.changeState(at: indexPath.row) ? .checkmark : .none
tableView.cellForRow(at: indexPath)?.accessoryType = accessoryType
}
When you use tableView.dequeueReusableCell, you may get the old cell, so you should update it. You should read doc.
#PGDev already said in comments that you should save checked/unchecked status in your model.
I hope my example will help you.
You can contain state of cells in cell models:
class YourCellModel {
var task: Task
var checked: Bool
init(task: Task, checked: Bool) {
self.task = task
self.checked = checked
}
}
And add it in ToDoCell:
//...
var model: YourCellModel {
didSet {
updateViews()
}
}
func updateViews() {
titleLabel.text = task.taskName
descriptionLabel.text = task.description
if model.checked {
//...
} else {
//....
}
}
And update model here:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Keys.cell.rawValue, for: indexPath) as! ToDoCell
// You should contain cell models to remember their states
let model = cellModels[indexPath.row]
cell.model = model
return cell
}
When the user checks a cell, you should save it in your cell model. You can do it in ToDoCell:
func checked() {
model.checked = true
}
Note: If isFiltering is true, it is a different array of cell models.
UPD. I noticed your Task is similar to a cell model. You can save checked status there. But your cell should have access to it.
I am maintaining UISegmentControl and Search with a single tableview. Here, I am loading the tableview data from a JSON (language list).
Now I have two segment buttons like Source language and Target language and both segments tableviews also have same data. Here, whenever user selects source language a particular row is check marked and if then user clicks target language segment, the same check mark shows. I need to maintain separate data selections, also, I am going to use search bar.
Can you please provide me a solution for two different segment controller buttons but maintaining a single tableview and its data and UI look the same. Checkmark selection should be different and persistent.
My Code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "languagecell", for: indexPath) as! LangCustomCell
let item = langData[indexPath.row]
cell.flag_img.sd_setImage(with:url, placeholderImage: UIImage(named: "usa.png"))
cell.language_label.text = item.languageName
cell.language_label.textColor = UIColor.gray
cell.selectionStyle = .none
//configure you cell here.
if(indexPath.row == selectedIndex) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
Create two separate variables to store selected languages for from and to.
In tableView didSelectRowAt method check save in appropriate variable based on the selectedSegmentIndex. In TableView cellForRowAt check the selected languages with current language. If selectedSegmentIndex and selected language matches use .checkmark else use .none
And create two arrays with type [Language]. In searchBar textDidChange method filter the languages array and reload the tableView.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
struct Language: Equatable {
var title: String
var icon: UIImage?
}
var allLanguages = [Language]()
var filteredLanguages = [Language]()
var selectedFromLanguage:Language?
var selectedToLanguage:Language?
let segmentedControl = UISegmentedControl()
let tableView = UITableView()
let searchBar = UISearchBar()
override func viewDidLoad() {
super.viewDidLoad()
allLanguages = [Language(title: "English", icon: UIImage(named:"uk"))]
filteredLanguages = allLanguages
// add constraints segmentedControl, tableView, searchBar in view
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return filteredLanguages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") ?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.textLabel?.text = filteredLanguages[indexPath.row].title
cell.imageView?.image = filteredLanguages[indexPath.row].icon
if segmentedControl.selectedSegmentIndex == 0 && selectedFromLanguage == filteredLanguages[indexPath.row] {
cell.accessoryType = .checkmark
} else if segmentedControl.selectedSegmentIndex == 1 && selectedToLanguage == filteredLanguages[indexPath.row] {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
// MARK: - Table view Delegate
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if segmentedControl.selectedSegmentIndex == 0 {//from
selectedFromLanguage = filteredLanguages[indexPath.row]
} else {//to
selectedToLanguage = filteredLanguages[indexPath.row]
}
tableView.reloadData()
}
// MARK: - Search bar Delegate
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
if searchText.isEmpty {
filteredLanguages = allLanguages
} else {
filteredLanguages = allLanguages.filter({ $0.title.localizedCaseInsensitiveContains(searchText) })
}
tableView.reloadData()
}
}
Use computed properties like this to persist the selected languages
var selectedFromLanguage:Language? {
get {
if let data = UserDefaults.standard.value(forKey: "fromLanguage") as? Data,
let language = try? JSONDecoder().decode(Language.self, from: data) {
return language
}
return nil
}
set {
if let data = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(data, forKey: "fromLanguage")
}
}
}
var selectedToLanguage:Language? {
get {
if let data = UserDefaults.standard.value(forKey: "toLanguage") as? Data,
let language = try? JSONDecoder().decode(Language.self, from: data) {
return language
}
return nil
}
set {
if let data = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(data, forKey: "toLanguage")
}
}
}
setup an action for your UISegmentControl:
#IBAction func segmentChanged(_ sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
// do what you need with your tableView
case 1:
// do what you need with your tableView
default:
return
}
}
when the index change setup your tableView and reload your data
I am creating an application where when a User searches for an item in the TableView they can click on it and a checkmark appears next to it. However, say when I select the first item I have searched for and click it then delete my search the checkmark stays on the first row but for a completely different object, I searched for, to begin with (see images below).
When Searching
When not Searching
var searchingArray = [Symptoms]()
var filteredArray = [Symptoms]()
var selectedSymptoms = [Symptoms]()
var clicked = [String]()
var searchingUnderWay = false
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = searchingSymptomsTableView.dequeueReusableCell(withIdentifier: "ExtraSymptoms", for: indexPath) as? ExtraSymptomCell {
let searchingArrays: Symptoms!
if searchingUnderWay {
searchingArrays = self.filteredArray[indexPath.row]
} else {
searchingArrays = self.searchingArray[indexPath.row]
}
cell.updateUI(symptomNames: searchingArrays)
return cell
} else {
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedRow: Symptoms!
let symptomName: String!
let cell : UITableViewCell = tableView.cellForRow(at: indexPath)!
if searchingUnderWay {
selectedRow = filteredArray[indexPath.row]
symptomName = filteredArray[indexPath.row].name as String
if clicked.contains(symptomName) {
cell.accessoryType = .none
let indexNumber = clicked.index(of: symptomName)
clicked.remove(at: indexNumber!)
if let element = selectedSymptoms.index(where: { $0.name == selectedRow.name }) {
selectedSymptoms.remove(at: element)
}
} else {
clicked.append(symptomName)
cell.accessoryType = .checkmark
searchingSymptomsTableView.reloadData()
selectedSymptoms.append(selectedRow)
}
} else {
selectedRow = searchingArray[indexPath.row]
symptomName = searchingArray[indexPath.row].name as String
if clicked.contains(symptomName) {
cell.accessoryType = .none
let indexNumber = clicked.index(of: symptomName)
clicked.remove(at: indexNumber!)
if let element = selectedSymptoms.index(where: { $0.name == selectedRow.name }) {
selectedSymptoms.remove(at: element)
}
} else {
clicked.append(symptomName)
cell.accessoryType = .checkmark
searchingSymptomsTableView.reloadData()
selectedSymptoms.append(selectedRow)
}
print(clicked)
print(selectedSymptoms)
}
}
I wish for the item I searched using the searchbar to still be checked when you delete the search.
Many thanks
Welcome to TableViewController logic. It seems really strange, but it works correct)
You need to override prepareForReuse() method in your ExtraSymptomCell. And clear all the values your cell contains including accessoryType
override func prepareForReuse() {
super.prepareForReuse()
accessoryType = .none
}
In your tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath):
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = searchingSymptomsTableView.dequeueReusableCell(withIdentifier: "ExtraSymptoms", for: indexPath) as? ExtraSymptomCell {
let symptomName: String!
let searchingArrays: Symptoms!
if searchingUnderWay {
searchingArrays = self.filteredArray[indexPath.row]
symptomName = filteredArray[indexPath.row].name as String
} else {
searchingArrays = self.searchingArray[indexPath.row]
symptomName = filteredArray[indexPath.row] as String
}
cell.updateUI(symptomNames: searchingArrays)
if clicked.contains(symptomName) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
} else {
return UITableViewCell()
}
}
Since UITableViewCell are reused, the checkmark will appear in a cell when you reload Table Data.
In cellForRowAt set the accessoryType to .none, to remove a previously checked cell checkmark:
let cell : UITableViewCell = tableView.cellForRow(at: indexPath)!
cell.accessoryType = .none
This will remove the check from the previous search.