I have a UITableView
with sections that is used to display my app's settings. The table's data is a dictionary
.
Each cell
in the table contains a title
, description
and a switch
to represent the setting status.
Whenever the user interacts with one of the switches - the description in at least one of the table cells disappears visually - even though the relevant string is still there in the data dictionary.
I've been cracking my head around it for days now. What am I missing or doing wrong?
This is my data dictionary:
var appSettings: [String:SettingModel] = [
"Search results": SettingModel(
sectionTitle: "Search results",
options: [
"Save filters": SingleSetting (
title: "Save filters",
description: "Allow us to save filters when entering back to the app",
status: .off
),
"Save search results": SingleSetting (
title: "Save search results",
description: "Allow us to save your search result preferences for next search",
status: .off
)
]
),
"App preferences": SettingModel(
sectionTitle: "App preferences",
options: [
"Notification": SingleSetting (
title: "Notification",
description: "",
status: .on
)
]
)
]
And here is my UITableView codes:
Setting up the table view -
func setupTableView() {
tableView.register(UINib(nibName: Constants.NibNames.APP_SETTING, bundle: nil), forCellReuseIdentifier: Constants.TableCellsIdentifier.SETTING)
tableView.register(UINib(nibName: Constants.NibNames.APP_SETTING_SECTION, bundle: nil), forHeaderFooterViewReuseIdentifier: Constants.TableCellsIdentifier.SETTING_SECTION)
tableView.dataSource = self
tableView.delegate = self
}
My TableView delegate functions -
// MARK: - UITableViewDataSource
extension SettingsViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.appSettings.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionKey = viewModel.sectionsSortedKeys[section]!
return viewModel.appSettings[sectionKey]!.options.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Constants.TableCellsIdentifier.SETTING, for: indexPath) as! AppSettingCell
cell.delegate = self
let sectionKey = viewModel.sectionsSortedKeys[indexPath.section]!
var rowKey: String {
switch sectionKey {
case Constants.AppSettingsSectionTitles.SEARCH:
if indexPath.row == 1 {
return Constants.AppSettings.SAVE_FILTERS
} else {
return Constants.AppSettings.SEARCH_RESULTS
}
case Constants.AppSettingsSectionTitles.PREFERENCES:
return Constants.AppSettings.NOTIFICATION
default:
return ""
}
}
cell.settingTitle.text = viewModel.appSettings[sectionKey]!.options[rowKey]?.title
cell.settingDescription.text = viewModel.appSettings[sectionKey]!.options[rowKey]?.description
var settingSwitchImage: UIImage
switch viewModel.appSettings[sectionKey]!.options[rowKey]?.status {
case .on:
settingSwitchImage = UIImage(named: "switch-on")!
break
case .off:
settingSwitchImage = UIImage(named: "switch-off")!
break
default:
settingSwitchImage = UIImage(named: "switch-disabled")!
break
}
cell.settingSwitchImageView.image = settingSwitchImage
return cell
}
}
// MARK: - UITableViewDelegate
extension SettingsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: Constants.TableCellsIdentifier.SETTING_SECTION) as! SettingSectionCell
view.sectionLabel.text = viewModel.sectionsSortedKeys[section]
return view
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 32
}
}
And my cell delegate function -
// MARK: - AppSettingCellDelegate
extension SettingsViewController: AppSettingCellDelegate {
func settingCellDidPress(settingText: String) {
print("CURRENT APP SETTINGS ARRAY:")
print(self.viewModel.appSettings)
viewModel.updateSetting(settingTitle: settingText) {
DispatchQueue.main.async {
print("UPDATED APP SETTINGS ARRAY:")
print(self.viewModel.appSettings)
self.tableView.reloadData()
}
}
}
}
My AppSettingsCell code -
protocol AppSettingCellDelegate {
func settingCellDidPress(settingText: String)
}
class AppSettingCell: UITableViewCell {
@IBOutlet weak var settingTitle: UILabel!
@IBOutlet weak var settingDescription: UILabel!
@IBOutlet weak var settingSwitchImageView: UIImageView!
var delegate: AppSettingCellDelegate?
override func awakeFromNib() {
super.awakeFromNib()
setTextLineSpacing()
setCellColorDesign()
setGestureRecognizer()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func setTextLineSpacing() {
let attributedString = NSMutableAttributedString(string: settingDescription.text!)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 10
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value:paragraphStyle, range:NSMakeRange(0, attributedString.length))
settingDescription.attributedText = attributedString
}
func setCellColorDesign() {
let bgColorView = UIView()
bgColorView.backgroundColor = UIColor.clear
self.selectedBackgroundView = bgColorView
}
func setGestureRecognizer() {
settingSwitchImageView.addGestureRecognizer(UITapGestureRecognizer(target: settingSwitchImageView, action: #selector(switchWasPressed)))
settingSwitchImageView.isUserInteractionEnabled = true
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(switchWasPressed(tapGestureRecognizer:)))
settingSwitchImageView.addGestureRecognizer(tapGestureRecognizer)
}
@objc func switchWasPressed(tapGestureRecognizer: UITapGestureRecognizer) {
delegate?.settingCellDidPress(settingText: settingTitle.text!)
}
}
My ViewModel.updateSettings function:
func updateSetting(settingTitle: String, completionHandler: @escaping (Int?, Int?) -> ()) {
var sectionTitle: String? = nil
var sectionIndex: Int? = nil
if appSettings[Constants.AppSettingsSectionTitles.SEARCH]?.options[settingTitle] != nil {
sectionTitle = Constants.AppSettingsSectionTitles.SEARCH
sectionIndex = 0
} else if appSettings[Constants.AppSettingsSectionTitles.PREFERENCES]?.options[settingTitle] != nil {
sectionTitle = Constants.AppSettingsSectionTitles.PREFERENCES
sectionIndex = 1
}
if let sectionTitle = sectionTitle {
let currentStatus = appSettings[sectionTitle]?.options[settingTitle]?.status
var newStatus: SwitchStatus = .off
if currentStatus != .disabled {
if currentStatus == .on {
appSettings[sectionTitle]?.options[settingTitle]?.status = newStatus // update local array
} else {
newStatus = .on
appSettings[sectionTitle]?.options[settingTitle]?.status = newStatus // update local array
}
let settingIndex = appSettings[sectionTitle]?.options[settingTitle]?.index
repository.updateSavedSetting(settingTitle: settingTitle, newStatus: newStatus) {
completionHandler(sectionIndex, settingIndex)
}
}
}
}
and the repository function it leads to:
func updateSavedSetting(settingTitle: String, newStatus: SwitchStatus, completionHandler: @escaping () -> ()) {
switch settingTitle {
case Constants.AppSettings.SAVE_FILTERS:
userDefaultsManager.setItemToUserDefaults(key: Constants.UserDefaults.SAVE_FILTERS, data: newStatus.rawValue)
case Constants.AppSettings.SEARCH_RESULTS:
userDefaultsManager.setItemToUserDefaults(key: Constants.UserDefaults.SAVE_SEARCH_RESULTS, data: newStatus.rawValue)
if newStatus == .off {
userDefaultsManager.clearSingleKeyValuePairFromUserDefaults(keyToRemove: Constants.UserDefaults.RECENT_SEARCHES)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: Constants.NotificationCenter.RECENT_SEARCHES_EMPTIED), object: nil)
}
case Constants.AppSettings.NOTIFICATION:
userDefaultsManager.setItemToUserDefaults(key: Constants.UserDefaults.SEND_NOTIFICATIONS, data: newStatus.rawValue)
default:
print("not valid option")
}
completionHandler()
}
When I print the appSettings data dictionary before and after the update - you can see that all the descriptions are still there:
CURRENT APP SETTINGS ARRAY:
["Search results": Dispatcher_Development.SettingModel(sectionTitle: "Search results", options: ["Save filters": Dispatcher_Development.SingleSetting(title: "Save filters", description: "Allow us to save filters when entering back to the app", status: Dispatcher_Development.SwitchStatus.on), "Save search results": Dispatcher_Development.SingleSetting(title: "Save search results", description: "Allow us to save your search result preferences for next search", status: Dispatcher_Development.SwitchStatus.on)]), "App preferences": Dispatcher_Development.SettingModel(sectionTitle: "App preferences", options: ["Notification": Dispatcher_Development.SingleSetting(title: "Notification", description: "", status: Dispatcher_Development.SwitchStatus.on)])]
UPDATED APP SETTINGS ARRAY:
["Search results": Dispatcher_Development.SettingModel(sectionTitle: "Search results", options: ["Save filters": Dispatcher_Development.SingleSetting(title: "Save filters", description: "Allow us to save filters when entering back to the app", status: Dispatcher_Development.SwitchStatus.off), "Save search results": Dispatcher_Development.SingleSetting(title: "Save search results", description: "Allow us to save your search result preferences for next search", status: Dispatcher_Development.SwitchStatus.on)]), "App preferences": Dispatcher_Development.SettingModel(sectionTitle: "App preferences", options: ["Notification": Dispatcher_Development.SingleSetting(title: "Notification", description: "", status: Dispatcher_Development.SwitchStatus.on)])]
EDIT -
I updated my code to refresh only the relevant line (instead of the whole table) using the reloadRows(at:with:)
function.
It somewhat improved the situation but the weird disappearance still happens.
Here is how I do it (the right section and row numbers are passed back in the closure):
// MARK: - AppSettingCellDelegate
extension SettingsViewController: AppSettingCellDelegate {
func settingCellDidPress(settingText: String) {
viewModel.updateSetting(settingTitle: settingText) { sectionIndex, settingIndex in
DispatchQueue.main.async {
let indexPath = IndexPath(row: settingIndex ?? 0, section: sectionIndex ?? 0 )
self.tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.none)
}
}
}
}
Here is a photo of how the page looks when you first enter it: enter image description here
Here is a photo of how the page looks after you start flipping the switches: enter image description here
CodePudding user response:
Managed to solve this by defining a row height in heightForRowAt
function and re-arranging the constraints in my custom appSettingCell