diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index b93f649..ef285c8 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ D55C468F29E761C000EA6DE9 /* AnyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C468E29E761C000EA6DE9 /* AnyFeature.swift */; }; D55C469129E7631000EA6DE9 /* AnyOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C469029E7631000EA6DE9 /* AnyOption.swift */; }; D560BD8629EDC45600289847 /* ExternalDisplaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */; }; + D56F7ABC2B05988700490ACB /* AttributedHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */; }; D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; }; D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; }; D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */; }; @@ -464,6 +465,7 @@ D55C468E29E761C000EA6DE9 /* AnyFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyFeature.swift; sourceTree = ""; }; D55C469029E7631000EA6DE9 /* AnyOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOption.swift; sourceTree = ""; }; D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplaySceneDelegate.swift; sourceTree = ""; }; + D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedHeaderFooterView.swift; sourceTree = ""; }; D586496F297734280081477E /* CheatMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatMetadata.swift; sourceTree = ""; }; D586497129774ABD0081477E /* CheatBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBase.swift; sourceTree = ""; }; D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = ""; }; @@ -787,6 +789,7 @@ BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */, BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */, BF8A333321A484A000A42FD4 /* BadgedTableViewCell.swift */, + D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */, ); path = "Table View"; sourceTree = ""; @@ -1663,6 +1666,7 @@ BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */, BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */, BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */, + D56F7ABC2B05988700490ACB /* AttributedHeaderFooterView.swift in Sources */, BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */, BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */, BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */, diff --git a/Delta/Components/Table View/AttributedHeaderFooterView.swift b/Delta/Components/Table View/AttributedHeaderFooterView.swift new file mode 100644 index 0000000..a8bf502 --- /dev/null +++ b/Delta/Components/Table View/AttributedHeaderFooterView.swift @@ -0,0 +1,75 @@ +// +// AttributedHeaderFooterView.swift +// Delta +// +// Created by Riley Testut on 11/15/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import UIKit + +@available(iOS 15, *) +final class AttributedHeaderFooterView: UITableViewHeaderFooterView +{ + static let reuseIdentifier: String = "TextViewHeaderFooterView" + + var attributedText: AttributedString? { + get { + guard let attributedText = self.textView.attributedText else { return nil } + return AttributedString(attributedText) + } + set { + guard var attributedText = newValue else { + self.textView.attributedText = nil + return + } + + var attributes = AttributeContainer() + attributes.foregroundColor = UIColor.secondaryLabel + attributes.font = self.textLabel?.font ?? UIFont.preferredFont(forTextStyle: .footnote) + + attributedText.mergeAttributes(attributes, mergePolicy: .keepCurrent) + self.textView.attributedText = NSAttributedString(attributedText) + } + } + + private let textView: UITextView + + override init(reuseIdentifier: String?) + { + self.textView = UITextView(frame: .zero) + self.textView.translatesAutoresizingMaskIntoConstraints = false + self.textView.textContainer.lineFragmentPadding = 0 + self.textView.textContainerInset = .zero + self.textView.isSelectable = true // Must be true to open links + self.textView.isEditable = false + self.textView.isScrollEnabled = false + self.textView.backgroundColor = nil + self.textView.textContainer.lineBreakMode = .byWordWrapping + + super.init(reuseIdentifier: reuseIdentifier) + + self.textView.delegate = self + self.contentView.addSubview(self.textView) + + NSLayoutConstraint.activate([ + self.textView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor), + self.textView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor), + self.textView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor), + self.textView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor), + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +@available(iOS 15, *) +extension AttributedHeaderFooterView: UITextViewDelegate +{ + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool + { + return true + } +} diff --git a/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift b/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift index 0c7e936..8eb3f89 100644 --- a/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift +++ b/Delta/Settings/Cores/MelonDSCoreSettingsViewController.swift @@ -120,6 +120,11 @@ class MelonDSCoreSettingsViewController: UITableViewController self.navigationItem.rightBarButtonItem = nil } + if #available(iOS 15, *) + { + self.tableView.register(AttributedHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: AttributedHeaderFooterView.reuseIdentifier) + } + NotificationCenter.default.addObserver(self, selector: #selector(MelonDSCoreSettingsViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) } @@ -460,8 +465,43 @@ extension MelonDSCoreSettingsViewController case (false, .horizontal): return NSLocalizedString("When AirPlaying DS games, both screens will be placed side-by-side on the external display.", comment: "") } - default: return super.tableView(tableView, titleForFooterInSection: section.rawValue) + case .dsBIOS, .dsiBIOS: + guard #available(iOS 15, *) else { break } + return nil + + default: break } + + return super.tableView(tableView, titleForFooterInSection: section.rawValue) + } + + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? + { + let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return nil } + + switch section + { + case .dsBIOS, .dsiBIOS: + guard #available(iOS 15, *), let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: AttributedHeaderFooterView.reuseIdentifier) as? AttributedHeaderFooterView else { break } + + let systemName = (section == .dsiBIOS) ? String(localized: "DSi") : String(localized: "DS") + + var attributedText = AttributedString(localized: "Delta requires these BIOS files in order to play Nintendo \(systemName) games.") + attributedText += " " + + var learnMore = AttributedString(localized: "Learn more…") + learnMore.link = URL(string: "https://faq.deltaemulator.com/getting-started/nintendo-ds-bios-files") + attributedText += learnMore + + footerView.attributedText = attributedText + + return footerView + + default: break + } + + return super.tableView(tableView, viewForFooterInSection: section.rawValue) } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat @@ -481,14 +521,24 @@ extension MelonDSCoreSettingsViewController override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return 1 } - if isSectionHidden(section) + switch section { - return 1 + case .dsBIOS, .dsiBIOS: return UITableView.automaticDimension + default: return super.tableView(tableView, heightForFooterInSection: section.rawValue) } - else + } + + override func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat + { + let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return 1 } + + switch section { - return super.tableView(tableView, heightForFooterInSection: section.rawValue) + case .dsBIOS, .dsiBIOS: return 30 + default: return UITableView.automaticDimension } } } diff --git a/Delta/Settings/SettingsViewController.swift b/Delta/Settings/SettingsViewController.swift index d3eb423..bb12483 100644 --- a/Delta/Settings/SettingsViewController.swift +++ b/Delta/Settings/SettingsViewController.swift @@ -115,6 +115,11 @@ class SettingsViewController: UITableViewController self.versionLabel.text = NSLocalizedString("Delta", comment: "") #endif } + + if #available(iOS 15, *) + { + self.tableView.register(AttributedHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: AttributedHeaderFooterView.reuseIdentifier) + } } override func viewWillAppear(_ animated: Bool) @@ -584,6 +589,33 @@ extension SettingsViewController } } + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? + { + let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return nil } + + switch section + { + case .controllerSkins: + guard #available(iOS 15, *), let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: AttributedHeaderFooterView.reuseIdentifier) as? AttributedHeaderFooterView else { break } + + var attributedText = AttributedString(localized: "Customize the appearance of each system.") + attributedText += " " + + var learnMore = AttributedString(localized: "Learn more…") + learnMore.link = URL(string: "https://faq.deltaemulator.com/using-delta/controller-skins") + attributedText += learnMore + + footerView.attributedText = attributedText + + return footerView + + default: break + } + + return super.tableView(tableView, viewForFooterInSection: section.rawValue) + } + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { let section = Section(rawValue: section)! @@ -594,7 +626,7 @@ extension SettingsViewController #if !BETA case .advanced: return nil #endif - + case .controllerSkins: return nil default: return super.tableView(tableView, titleForFooterInSection: section.rawValue) } } @@ -616,14 +648,24 @@ extension SettingsViewController override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return 1 } - if isSectionHidden(section) + switch section { - return 1 + case .controllerSkins: return UITableView.automaticDimension + default: return super.tableView(tableView, heightForFooterInSection: section.rawValue) } - else + } + + override func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat + { + let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return 1 } + + switch section { - return super.tableView(tableView, heightForFooterInSection: section.rawValue) + case .controllerSkins: return 30 + default: return UITableView.automaticDimension } } }