diff --git a/Scribe.xcodeproj/project.pbxproj b/Scribe.xcodeproj/project.pbxproj index 161bad0b..5d2b01d9 100644 --- a/Scribe.xcodeproj/project.pbxproj +++ b/Scribe.xcodeproj/project.pbxproj @@ -940,6 +940,7 @@ E9FAC3A02E98972D008E00AC /* IDCommandVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAC3882E9894F9008E00AC /* IDCommandVariables.swift */; }; E9FAC3A12E98972D008E00AC /* IDKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAC38A2E9894F9008E00AC /* IDKeyboardViewController.swift */; }; E9FAC3A72E989A84008E00AC /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = E9FAC3A62E989A84008E00AC /* SwiftyJSON */; }; + E9FEE6CB2EF14351003A9266 /* WrapperCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEE6CA2EF1433E003A9266 /* WrapperCell.swift */; }; ED2486F32B0B4E8C0038AE6A /* AboutTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED2486F12B0B4E8C0038AE6A /* AboutTableViewCell.swift */; }; ED2486F42B0B4E8C0038AE6A /* AboutTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = ED2486F22B0B4E8C0038AE6A /* AboutTableViewCell.xib */; }; EDB460212B03B3E400BEA967 /* BaseTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB460202B03B3E400BEA967 /* BaseTableViewController.swift */; }; @@ -1235,6 +1236,7 @@ E9FAC3932E989712008E00AC /* Indonesian.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Indonesian.appex; sourceTree = BUILT_PRODUCTS_DIR; }; E9FAC3A42E9898B5008E00AC /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GRDB.swift; path = "/Users/purnamadev/Library/Developer/Xcode/DerivedData/Scribe-dbrvuamymhbpcmeozftkkqqvrdof/SourcePackages/checkouts/GRDB.swift"; sourceTree = ""; }; E9FAC3A52E9898B9008E00AC /* GRDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = GRDB.swift; path = "/Users/purnamadev/Library/Developer/Xcode/DerivedData/Scribe-dbrvuamymhbpcmeozftkkqqvrdof/SourcePackages/checkouts/GRDB.swift"; sourceTree = ""; }; + E9FEE6CA2EF1433E003A9266 /* WrapperCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperCell.swift; sourceTree = ""; }; ED2486F12B0B4E8C0038AE6A /* AboutTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutTableViewCell.swift; sourceTree = ""; }; ED2486F22B0B4E8C0038AE6A /* AboutTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutTableViewCell.xib; sourceTree = ""; }; EDB460202B03B3E400BEA967 /* BaseTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTableViewController.swift; sourceTree = ""; }; @@ -1378,6 +1380,7 @@ 1406B7852A2DFCBE001DF45B /* InfoChildTableViewCell */ = { isa = PBXGroup; children = ( + E9FEE6CA2EF1433E003A9266 /* WrapperCell.swift */, 147797AE2A2CD3370044A53E /* InfoChildTableViewCell.swift */, 147797AF2A2CD3370044A53E /* InfoChildTableViewCell.xib */, ); @@ -2629,6 +2632,7 @@ D1CDED832A85A12C00098546 /* NBCommandVariables.swift in Sources */, D171944927AEF7290038660B /* KeyboardKeys.swift in Sources */, 147797B32A2CD5AB0044A53E /* ParentTableCellModel.swift in Sources */, + E9FEE6CB2EF14351003A9266 /* WrapperCell.swift in Sources */, 1401589B2A45A07200D14E52 /* WikimediaAndScribe.swift in Sources */, 3045396B293B9DC9003AE55B /* ToolTipViewDatasourceable.swift in Sources */, D1B071A027C6A1AA00FD7DBD /* KeyAltChars.swift in Sources */, diff --git a/Scribe/AboutTab/AboutViewController.swift b/Scribe/AboutTab/AboutViewController.swift index a45b6c62..32b5de84 100644 --- a/Scribe/AboutTab/AboutViewController.swift +++ b/Scribe/AboutTab/AboutViewController.swift @@ -22,6 +22,7 @@ final class AboutViewController: BaseTableViewController { private var tipHostingController: UIHostingController! private var tableViewOffset: CGFloat? + private let cornerRadius: CGFloat = 12 override func viewDidLoad() { super.viewDidLoad() @@ -30,6 +31,11 @@ final class AboutViewController: BaseTableViewController { value: "About", comment: "") + tableView.register( + WrapperCell.self, + forCellReuseIdentifier: WrapperCell.reuseIdentifier + ) + tableView.register( UINib(nibName: "AboutTableViewCell", bundle: nil), forCellReuseIdentifier: AboutTableViewCell.reuseIdentifier @@ -50,13 +56,20 @@ final class AboutViewController: BaseTableViewController { extension AboutViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( - withIdentifier: AboutTableViewCell.reuseIdentifier, + withIdentifier: WrapperCell.reuseIdentifier, for: indexPath - ) as? AboutTableViewCell else { - fatalError("Failed to dequeue AboutTableViewCell.") + ) as? WrapperCell else { + fatalError("Failed to dequeue WrapperCell") } - cell.configureCell(for: dataSet[indexPath.section].section[indexPath.row]) - cell.backgroundColor = lightWhiteDarkBlackColor + + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] + + cell.configure(withCellNamed: "AboutTableViewCell", section: setting) + + let isFirstRow = indexPath.row == 0 + let isLastRow = indexPath.row == section.section.count - 1 + WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) return cell } @@ -65,6 +78,14 @@ extension AboutViewController { // MARK: UITableViewDelegate extension AboutViewController { + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] + + let hasDescription = setting.shortDescription != nil + return hasDescription ? 80.0 : 48.0 + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let tableSection = dataSet[indexPath.section] let section = tableSection.section[indexPath.row] diff --git a/Scribe/SettingsTab/SettingsViewController.swift b/Scribe/SettingsTab/SettingsViewController.swift index 38fec4d3..330086b4 100644 --- a/Scribe/SettingsTab/SettingsViewController.swift +++ b/Scribe/SettingsTab/SettingsViewController.swift @@ -12,6 +12,7 @@ final class SettingsViewController: UIViewController { private var sectionHeaderHeight: CGFloat = 0 private let separatorInset = UIEdgeInsets(top: 16.0, left: 16.0, bottom: 16.0, right: 16.0) + private let cornerRadius: CGFloat = 12 private let settingsTipCardState: Bool = { let userDefault = UserDefaults.standard @@ -45,6 +46,11 @@ final class SettingsViewController: UIViewController { title = NSLocalizedString("i18n.app.settings.title", value: "Settings", comment: "") navigationItem.backButtonTitle = title + parentTable.register( + WrapperCell.self, + forCellReuseIdentifier: WrapperCell.reuseIdentifier + ) + parentTable.register( UINib(nibName: "InfoChildTableViewCell", bundle: nil), forCellReuseIdentifier: InfoChildTableViewCell.reuseIdentifier @@ -121,17 +127,20 @@ extension SettingsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( - withIdentifier: InfoChildTableViewCell.reuseIdentifier, + withIdentifier: WrapperCell.reuseIdentifier, for: indexPath - ) as? InfoChildTableViewCell else { - fatalError("Failed to dequeue InfoChildTableViewCell") + ) as? WrapperCell else { + fatalError("Failed to dequeue WrapperCell") } let section = tableData[indexPath.section] let setting = section.section[indexPath.row] - cell.configureCell(for: setting) - cell.backgroundColor = lightWhiteDarkBlackColor + cell.configure(withCellNamed: "InfoChildTableViewCell", section: setting) + + let isFirstRow = indexPath.row == 0 + let isLastRow = indexPath.row == section.section.count - 1 + WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) return cell } diff --git a/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift b/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift new file mode 100644 index 00000000..8f0b20ce --- /dev/null +++ b/Scribe/Views/Cells/InfoChildTableViewCell/WrapperCell.swift @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit + +protocol RoundableCell { + func applyCornerRadius(corners: CACornerMask, radius: CGFloat) + func removeCornerRadius() +} +/// Generic wrapper cell that can wrap any UITableViewCell with padding and corner radius support. +class WrapperCell: UITableViewCell { + static let reuseIdentifier = "WrapperCell" + + private let containerView = UIView() + private(set) var wrappedCell: UITableViewCell? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupViews() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupViews() { + backgroundColor = .clear + contentView.backgroundColor = .clear + selectionStyle = .none + + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.backgroundColor = lightWhiteDarkBlackColor + contentView.addSubview(containerView) + + NSLayoutConstraint.activate([ + containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + containerView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + + /// Configure with any cell loaded from XIB. + func configure(withCellNamed nibName: String, section: Section) { + wrappedCell?.removeFromSuperview() + + guard let cell = Bundle.main.loadNibNamed( + nibName, + owner: nil, + options: nil + )?.first as? UITableViewCell else { + fatalError("Failed to load \(nibName) from XIB") + } + + if let infoCell = cell as? InfoChildTableViewCell { + infoCell.configureCell(for: section) + } else if let aboutCell = cell as? AboutTableViewCell { + aboutCell.configureCell(for: section) + } + + cell.backgroundColor = .clear + cell.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(cell) + + NSLayoutConstraint.activate([ + cell.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + cell.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + cell.topAnchor.constraint(equalTo: containerView.topAnchor), + cell.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + wrappedCell = cell + } + + func applyCornerRadius(corners: CACornerMask, radius: CGFloat) { + containerView.layer.maskedCorners = corners + containerView.layer.cornerRadius = radius + containerView.layer.masksToBounds = true + } + + func removeCornerRadius() { + containerView.layer.cornerRadius = 0 + containerView.layer.masksToBounds = false + } + + // MARK: Static Helper + + /// Apply corner radius to a cell based on its position in the section. + static func applyCornerRadius( + to cell: UITableViewCell, + isFirst: Bool, + isLast: Bool, + radius: CGFloat = 12 + ) { + guard let roundableCell = cell as? RoundableCell else { return } + + if isFirst && isLast { + roundableCell.applyCornerRadius( + corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner], + radius: radius + ) + } else if isFirst { + roundableCell.applyCornerRadius( + corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], + radius: radius + ) + } else if isLast { + roundableCell.applyCornerRadius( + corners: [.layerMinXMaxYCorner, .layerMaxXMaxYCorner], + radius: radius + ) + } else { + roundableCell.removeCornerRadius() + } + } +} + +extension WrapperCell: RoundableCell {} diff --git a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift b/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift index 26818b49..cc335389 100644 --- a/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift +++ b/Scribe/Views/Cells/RadioTableViewCell/RadioTableViewCell.swift @@ -8,11 +8,11 @@ import UIKit final class RadioTableViewCell: UITableViewCell { - // MARK: - Constants + // MARK: Constants static let reuseIdentifier = String(describing: InfoChildTableViewCell.self) - // MARK: - Properties + // MARK: Properties @IBOutlet var titleLabelPhone: UILabel! @IBOutlet var titleLabelPad: UILabel! @@ -56,7 +56,7 @@ final class RadioTableViewCell: UITableViewCell { return action } - // MARK: - Functions + // MARK: Functions func configureCell(for section: Section) { self.section = section diff --git a/Scribe/Views/SelectionViewTemplateViewController.swift b/Scribe/Views/SelectionViewTemplateViewController.swift index 72c778c6..e3670b62 100644 --- a/Scribe/Views/SelectionViewTemplateViewController.swift +++ b/Scribe/Views/SelectionViewTemplateViewController.swift @@ -8,7 +8,7 @@ import UIKit import SwiftUI final class SelectionViewTemplateViewController: BaseTableViewController { - // MARK: - Properties + // MARK: Properties override var dataSet: [ParentTableCellModel] { tableData @@ -22,7 +22,7 @@ final class SelectionViewTemplateViewController: BaseTableViewController { private var langCode: String = "de" - // MARK: - Functions + // MARK: Functions override func viewDidLoad() { super.viewDidLoad() @@ -43,7 +43,7 @@ final class SelectionViewTemplateViewController: BaseTableViewController { } } -// MARK: - UITableViewDataSource +// MARK: UITableViewDataSource extension SelectionViewTemplateViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { diff --git a/Scribe/Views/TableViewTemplateViewController.swift b/Scribe/Views/TableViewTemplateViewController.swift index 46879ec9..7c24a9f9 100644 --- a/Scribe/Views/TableViewTemplateViewController.swift +++ b/Scribe/Views/TableViewTemplateViewController.swift @@ -15,6 +15,7 @@ final class TableViewTemplateViewController: BaseTableViewController { private var tableData: [ParentTableCellModel] = [] private var parentSection: Section? + private let cornerRadius: CGFloat = 12 let userDefaults = UserDefaults(suiteName: "group.be.scri.userDefaultsContainer")! @@ -35,6 +36,11 @@ final class TableViewTemplateViewController: BaseTableViewController { override func viewDidLoad() { super.viewDidLoad() + tableView.register( + WrapperCell.self, + forCellReuseIdentifier: WrapperCell.reuseIdentifier + ) + tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 250 tableView.separatorStyle = .none @@ -47,12 +53,25 @@ final class TableViewTemplateViewController: BaseTableViewController { title = parentSection.sectionTitle } - // Refreshes to check for changes when a translation language is selected + // Refreshes to check for changes when a translation language is selected. override func viewWillAppear(_ animated: Bool) { - for cell in tableView.visibleCells as! [InfoChildTableViewCell] where cell.section?.sectionState == .translateLang { + super.viewWillAppear(animated) + + for visibleCell in tableView.visibleCells { + // Cast to wrapper cell first. + guard let wrapperCell = visibleCell as? WrapperCell, + let innerCell = wrapperCell.wrappedCell as? InfoChildTableViewCell else { + continue + } + + // Now check if it's a translate lang section. + guard innerCell.section?.sectionState == .translateLang else { + continue + } + let langTranslateLanguage = getKeyInDict(givenValue: (userDefaults.string(forKey: langCode + "TranslateLanguage") ?? "en"), dict: languagesAbbrDict) let currentLang = "i18n.app._global." + langTranslateLanguage.lowercased() - cell.subLabel.text = NSLocalizedString(currentLang, value: langTranslateLanguage, comment: "") + innerCell.subLabel.text = NSLocalizedString(currentLang, value: langTranslateLanguage, comment: "") } } } @@ -62,18 +81,33 @@ final class TableViewTemplateViewController: BaseTableViewController { extension TableViewTemplateViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell( - withIdentifier: InfoChildTableViewCell.reuseIdentifier, + withIdentifier: WrapperCell.reuseIdentifier, for: indexPath - ) as? InfoChildTableViewCell else { - fatalError("Failed to dequeue InfoChildTableViewCell.") + ) as? WrapperCell else { + fatalError("Failed to dequeue WrapperCell") } - cell.parentSection = parentSection - cell.configureCell(for: tableData[indexPath.section].section[indexPath.row]) - cell.backgroundColor = lightWhiteDarkBlackColor + + let section = dataSet[indexPath.section] + let setting = section.section[indexPath.row] + + cell.configure(withCellNamed: "InfoChildTableViewCell", section: setting) + + let isFirstRow = indexPath.row == 0 + let isLastRow = indexPath.row == section.section.count - 1 + WrapperCell.applyCornerRadius(to: cell, isFirst: isFirstRow, isLast: isLastRow) return cell } + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let section = tableData[indexPath.section] + let setting = section.section[indexPath.row] + + // If has description, give it more height. + let hasDescription = setting.shortDescription != nil + return hasDescription ? 100.0 : 60.0 + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let tableSection = tableData[indexPath.section] let section = tableSection.section[indexPath.row] @@ -84,7 +118,7 @@ extension TableViewTemplateViewController { ) as? SelectionViewTemplateViewController { var data = SettingsTableData.translateLangSettingsData - // Removes keyboard language from possible translation languages + // Removes keyboard language from possible translation languages. let langCodeIndex = SettingsTableData.translateLangSettingsData[0].section.firstIndex(where: { s in s.sectionState == .specificLang(langCode) }) ?? -1