Initial commit
This commit is contained in:
556
Emoney Info/FAQViewController.swift
Normal file
556
Emoney Info/FAQViewController.swift
Normal file
@ -0,0 +1,556 @@
|
||||
// FAQViewController.swift
|
||||
// Emoney Info
|
||||
//
|
||||
// Halaman Pusat Bantuan / FAQ
|
||||
// - Search bar berfungsi (filter pertanyaan real-time)
|
||||
// - Filter chip per kategori berfungsi
|
||||
// - Accordion: tap pertanyaan untuk expand/collapse jawaban
|
||||
|
||||
import UIKit
|
||||
|
||||
final class FAQViewController: UIViewController {
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private var allCategories: [FAQCategory] = FAQData.all
|
||||
private var activeFilter: String? = nil // nil = semua kategori
|
||||
private var searchText: String = ""
|
||||
private var expandedItems: Set<String> = [] // key = "catID|qIndex"
|
||||
|
||||
// Computed: filtered categories based on search + filter chip
|
||||
private var displayedCategories: [FAQCategory] {
|
||||
var source = allCategories
|
||||
|
||||
// 1. Filter by category chip
|
||||
if let filter = activeFilter {
|
||||
source = source.filter { $0.id == filter }
|
||||
}
|
||||
|
||||
// 2. Filter by search text
|
||||
if !searchText.isEmpty {
|
||||
let q = searchText.lowercased()
|
||||
source = source.compactMap { cat in
|
||||
let filtered = cat.items.filter {
|
||||
$0.question.lowercased().contains(q) || $0.answer.lowercased().contains(q)
|
||||
}
|
||||
if filtered.isEmpty { return nil }
|
||||
return FAQCategory(id: cat.id, icon: cat.icon, title: cat.title, items: filtered)
|
||||
}
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentView = UIView()
|
||||
|
||||
private let headerView = UIView()
|
||||
private let backButton = UIButton(type: .system)
|
||||
private let titleLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
private let searchBar = UISearchBar()
|
||||
|
||||
// Category filter chips (horizontal scroll)
|
||||
private let chipScrollView = UIScrollView()
|
||||
private let chipStack = UIStackView()
|
||||
|
||||
// FAQ accordion container
|
||||
private let faqContainerStack = UIStackView()
|
||||
|
||||
// "Still need help?" footer card
|
||||
private let helpCard = UIView()
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = Theme.Color.background
|
||||
setupScrollView()
|
||||
setupHeader()
|
||||
setupChips()
|
||||
setupFAQSection()
|
||||
setupHelpCard()
|
||||
reloadFAQ()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
(tabBarController as? MainTabBarController)?.setTabBarHidden(true, animated: animated)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
(tabBarController as? MainTabBarController)?.setTabBarHidden(false, animated: animated)
|
||||
}
|
||||
|
||||
// MARK: - Setup: ScrollView
|
||||
|
||||
private func setupScrollView() {
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.keyboardDismissMode = .onDrag
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scrollView)
|
||||
scrollView.addSubview(contentView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
|
||||
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Setup: Header
|
||||
|
||||
private func setupHeader() {
|
||||
// Teal rounded-bottom header card
|
||||
headerView.backgroundColor = Theme.Color.primary
|
||||
headerView.layer.cornerRadius = 24
|
||||
headerView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(headerView)
|
||||
|
||||
// Back button
|
||||
let chevron = UIImage(systemName: "chevron.left",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold))
|
||||
backButton.setImage(chevron, for: .normal)
|
||||
backButton.tintColor = .white
|
||||
backButton.addTarget(self, action: #selector(backTapped), for: .touchUpInside)
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Header nav label "Help Center"
|
||||
let navLabel = UILabel()
|
||||
navLabel.text = L10n.helpCenterTitle
|
||||
navLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
navLabel.textColor = UIColor.white.withAlphaComponent(0.85)
|
||||
navLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Title
|
||||
titleLabel.text = L10n.faqHeaderTitle
|
||||
titleLabel.font = Theme.Font.title(weight: .bold)
|
||||
titleLabel.textColor = .white
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Search bar
|
||||
searchBar.placeholder = L10n.faqSearchPlaceholder
|
||||
searchBar.searchBarStyle = .minimal
|
||||
searchBar.backgroundColor = .white
|
||||
searchBar.layer.cornerRadius = 12
|
||||
searchBar.clipsToBounds = true
|
||||
searchBar.delegate = self
|
||||
searchBar.translatesAutoresizingMaskIntoConstraints = false
|
||||
if let tf = searchBar.value(forKey: "searchField") as? UITextField {
|
||||
tf.backgroundColor = .white
|
||||
tf.font = Theme.Font.body(weight: .regular)
|
||||
}
|
||||
|
||||
[backButton, navLabel, titleLabel, searchBar].forEach { headerView.addSubview($0) }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
|
||||
backButton.topAnchor.constraint(equalTo: headerView.safeAreaLayoutGuide.topAnchor, constant: 16),
|
||||
backButton.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 20),
|
||||
backButton.widthAnchor.constraint(equalToConstant: 32),
|
||||
backButton.heightAnchor.constraint(equalToConstant: 32),
|
||||
|
||||
navLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
|
||||
navLabel.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 8),
|
||||
|
||||
titleLabel.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 20),
|
||||
titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 24),
|
||||
titleLabel.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -24),
|
||||
|
||||
searchBar.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16),
|
||||
searchBar.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 16),
|
||||
searchBar.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -16),
|
||||
searchBar.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -20),
|
||||
searchBar.heightAnchor.constraint(equalToConstant: 44),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Setup: Category Chips
|
||||
|
||||
private func setupChips() {
|
||||
chipScrollView.showsHorizontalScrollIndicator = false
|
||||
chipScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(chipScrollView)
|
||||
|
||||
chipStack.axis = .horizontal
|
||||
chipStack.spacing = 10
|
||||
chipStack.alignment = .center
|
||||
chipStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
chipScrollView.addSubview(chipStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
chipScrollView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 20),
|
||||
chipScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
chipScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
chipScrollView.heightAnchor.constraint(equalToConstant: 44),
|
||||
|
||||
chipStack.centerYAnchor.constraint(equalTo: chipScrollView.centerYAnchor),
|
||||
chipStack.leadingAnchor.constraint(equalTo: chipScrollView.leadingAnchor, constant: 20),
|
||||
chipStack.trailingAnchor.constraint(equalTo: chipScrollView.trailingAnchor, constant: -20),
|
||||
])
|
||||
|
||||
// "All" chip + one per category
|
||||
buildChips()
|
||||
}
|
||||
|
||||
private func buildChips() {
|
||||
chipStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
let allChip = makeChip(id: nil, icon: "square.grid.2x2", title: L10n.faqFilterAll)
|
||||
chipStack.addArrangedSubview(allChip)
|
||||
|
||||
for cat in allCategories {
|
||||
let chip = makeChip(id: cat.id, icon: cat.icon, title: cat.title)
|
||||
chipStack.addArrangedSubview(chip)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeChip(id: String?, icon: String, title: String) -> UIView {
|
||||
let isActive = (id == activeFilter)
|
||||
|
||||
let container = UIControl()
|
||||
container.backgroundColor = isActive ? Theme.Color.primary : .white
|
||||
container.layer.cornerRadius = 18
|
||||
container.layer.borderWidth = isActive ? 0 : 1
|
||||
container.layer.borderColor = UIColor.systemGray5.cgColor
|
||||
container.layer.shadowColor = UIColor.black.cgColor
|
||||
container.layer.shadowOpacity = isActive ? 0.08 : 0.04
|
||||
container.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
container.layer.shadowRadius = 4
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: icon,
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 12, weight: .medium)))
|
||||
iconView.tintColor = isActive ? .white : Theme.Color.secondary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let label = UILabel()
|
||||
label.text = title
|
||||
label.font = Theme.Font.caption(weight: .semibold)
|
||||
label.textColor = isActive ? .white : Theme.Color.textPrimary
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [iconView, label])
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 5
|
||||
stack.alignment = .center
|
||||
stack.isUserInteractionEnabled = false
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(stack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
iconView.widthAnchor.constraint(equalToConstant: 14),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 14),
|
||||
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 14),
|
||||
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -14),
|
||||
stack.centerYAnchor.constraint(equalTo: container.centerYAnchor),
|
||||
container.heightAnchor.constraint(equalToConstant: 36),
|
||||
])
|
||||
|
||||
// Store filter ID in accessibilityIdentifier for lookup
|
||||
container.accessibilityIdentifier = id ?? "__all__"
|
||||
container.addTarget(self, action: #selector(chipTapped(_:)), for: .touchUpInside)
|
||||
|
||||
return container
|
||||
}
|
||||
|
||||
// MARK: - Setup: FAQ Accordion Section
|
||||
|
||||
private func setupFAQSection() {
|
||||
faqContainerStack.axis = .vertical
|
||||
faqContainerStack.spacing = 12
|
||||
faqContainerStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(faqContainerStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
faqContainerStack.topAnchor.constraint(equalTo: chipScrollView.bottomAnchor, constant: 20),
|
||||
faqContainerStack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
||||
faqContainerStack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Setup: Help Card Footer
|
||||
|
||||
private func setupHelpCard() {
|
||||
helpCard.backgroundColor = Theme.Color.primary
|
||||
helpCard.layer.cornerRadius = 20
|
||||
helpCard.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(helpCard)
|
||||
|
||||
let iconView = UIImageView(image: UIImage(systemName: "questionmark.bubble.fill",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 28, weight: .medium)))
|
||||
iconView.tintColor = .white
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let helpTitle = UILabel()
|
||||
helpTitle.text = L10n.faqHelpCardTitle
|
||||
helpTitle.font = Theme.Font.subtitle(weight: .bold)
|
||||
helpTitle.textColor = .white
|
||||
helpTitle.numberOfLines = 0
|
||||
helpTitle.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let helpDesc = UILabel()
|
||||
helpDesc.text = L10n.faqHelpCardDesc
|
||||
helpDesc.font = Theme.Font.caption(weight: .regular)
|
||||
helpDesc.textColor = UIColor.white.withAlphaComponent(0.85)
|
||||
helpDesc.numberOfLines = 0
|
||||
helpDesc.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let emailButton = UIButton(type: .system)
|
||||
emailButton.setTitle(L10n.faqEmailSupport, for: .normal)
|
||||
emailButton.setImage(UIImage(systemName: "envelope.fill",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: 14, weight: .medium)), for: .normal)
|
||||
emailButton.tintColor = Theme.Color.primary
|
||||
emailButton.backgroundColor = .white
|
||||
emailButton.layer.cornerRadius = 14
|
||||
emailButton.titleLabel?.font = Theme.Font.body(weight: .semibold)
|
||||
emailButton.contentEdgeInsets = UIEdgeInsets(top: 12, left: 20, bottom: 12, right: 20)
|
||||
emailButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: -6, bottom: 0, right: 0)
|
||||
emailButton.addTarget(self, action: #selector(emailTapped), for: .touchUpInside)
|
||||
emailButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
[iconView, helpTitle, helpDesc, emailButton].forEach { helpCard.addSubview($0) }
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
helpCard.topAnchor.constraint(equalTo: faqContainerStack.bottomAnchor, constant: 28),
|
||||
helpCard.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20),
|
||||
helpCard.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
|
||||
helpCard.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -140),
|
||||
|
||||
iconView.topAnchor.constraint(equalTo: helpCard.topAnchor, constant: 24),
|
||||
iconView.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24),
|
||||
iconView.widthAnchor.constraint(equalToConstant: 36),
|
||||
iconView.heightAnchor.constraint(equalToConstant: 36),
|
||||
|
||||
helpTitle.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 12),
|
||||
helpTitle.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24),
|
||||
helpTitle.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -24),
|
||||
|
||||
helpDesc.topAnchor.constraint(equalTo: helpTitle.bottomAnchor, constant: 8),
|
||||
helpDesc.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24),
|
||||
helpDesc.trailingAnchor.constraint(equalTo: helpCard.trailingAnchor, constant: -24),
|
||||
|
||||
emailButton.topAnchor.constraint(equalTo: helpDesc.bottomAnchor, constant: 20),
|
||||
emailButton.leadingAnchor.constraint(equalTo: helpCard.leadingAnchor, constant: 24),
|
||||
emailButton.bottomAnchor.constraint(equalTo: helpCard.bottomAnchor, constant: -24),
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: - Reload FAQ Accordion
|
||||
|
||||
private func reloadFAQ() {
|
||||
faqContainerStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
let categories = displayedCategories
|
||||
|
||||
if categories.isEmpty {
|
||||
let emptyLabel = UILabel()
|
||||
emptyLabel.text = L10n.faqNoResults
|
||||
emptyLabel.font = Theme.Font.body(weight: .regular)
|
||||
emptyLabel.textColor = Theme.Color.textSecondary
|
||||
emptyLabel.textAlignment = .center
|
||||
emptyLabel.numberOfLines = 0
|
||||
faqContainerStack.addArrangedSubview(emptyLabel)
|
||||
return
|
||||
}
|
||||
|
||||
for cat in categories {
|
||||
// Category header label
|
||||
let catLabel = UILabel()
|
||||
catLabel.text = cat.title.uppercased()
|
||||
catLabel.font = Theme.Font.caption(weight: .semibold)
|
||||
catLabel.textColor = Theme.Color.textSecondary
|
||||
faqContainerStack.addArrangedSubview(catLabel)
|
||||
faqContainerStack.setCustomSpacing(8, after: catLabel)
|
||||
|
||||
// White card holding all items for this category
|
||||
let card = UIView()
|
||||
card.backgroundColor = Theme.Color.card
|
||||
card.layer.cornerRadius = 16
|
||||
card.layer.shadowColor = UIColor.black.cgColor
|
||||
card.layer.shadowOpacity = 0.06
|
||||
card.layer.shadowOffset = CGSize(width: 0, height: 2)
|
||||
card.layer.shadowRadius = 8
|
||||
card.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let itemStack = UIStackView()
|
||||
itemStack.axis = .vertical
|
||||
itemStack.spacing = 0
|
||||
itemStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
card.addSubview(itemStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
itemStack.topAnchor.constraint(equalTo: card.topAnchor, constant: 4),
|
||||
itemStack.leadingAnchor.constraint(equalTo: card.leadingAnchor),
|
||||
itemStack.trailingAnchor.constraint(equalTo: card.trailingAnchor),
|
||||
itemStack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -4),
|
||||
])
|
||||
|
||||
for (qIndex, item) in cat.items.enumerated() {
|
||||
let key = "\(cat.id)|\(qIndex)"
|
||||
let isExpanded = expandedItems.contains(key)
|
||||
|
||||
let row = FAQItemRow(
|
||||
item: item,
|
||||
isExpanded: isExpanded,
|
||||
onToggle: { [weak self] in
|
||||
self?.toggleItem(key: key)
|
||||
}
|
||||
)
|
||||
itemStack.addArrangedSubview(row)
|
||||
|
||||
if qIndex < cat.items.count - 1 {
|
||||
let sep = UIView()
|
||||
sep.backgroundColor = Theme.Color.background
|
||||
sep.translatesAutoresizingMaskIntoConstraints = false
|
||||
sep.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
itemStack.addArrangedSubview(sep)
|
||||
}
|
||||
}
|
||||
|
||||
faqContainerStack.addArrangedSubview(card)
|
||||
faqContainerStack.setCustomSpacing(20, after: card)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Toggle accordion item
|
||||
|
||||
private func toggleItem(key: String) {
|
||||
if expandedItems.contains(key) {
|
||||
expandedItems.remove(key)
|
||||
} else {
|
||||
expandedItems.insert(key)
|
||||
}
|
||||
UIView.animate(withDuration: 0.25, delay: 0,
|
||||
usingSpringWithDamping: 0.85, initialSpringVelocity: 0) {
|
||||
self.reloadFAQ()
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func backTapped() {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@objc private func chipTapped(_ sender: UIControl) {
|
||||
let rawID = sender.accessibilityIdentifier
|
||||
activeFilter = (rawID == "__all__") ? nil : rawID
|
||||
expandedItems.removeAll()
|
||||
buildChips()
|
||||
reloadFAQ()
|
||||
}
|
||||
|
||||
@objc private func emailTapped() {
|
||||
let address = "apps@indonesiainyourhand.com"
|
||||
let subject = "Ask Support"
|
||||
let encoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? subject
|
||||
if let url = URL(string: "mailto:\(address)?subject=\(encoded)") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
|
||||
extension FAQViewController: UISearchBarDelegate {
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
self.searchText = searchText
|
||||
expandedItems.removeAll()
|
||||
reloadFAQ()
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - FAQItemRow (Accordion Row)
|
||||
|
||||
private final class FAQItemRow: UIView {
|
||||
|
||||
private let questionLabel = UILabel()
|
||||
private let answerLabel = UILabel()
|
||||
private let chevron = UIImageView()
|
||||
|
||||
var onToggle: (() -> Void)?
|
||||
|
||||
init(item: FAQItem, isExpanded: Bool, onToggle: @escaping () -> Void) {
|
||||
self.onToggle = onToggle
|
||||
super.init(frame: .zero)
|
||||
setup(item: item, isExpanded: isExpanded)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
private func setup(item: FAQItem, isExpanded: Bool) {
|
||||
// Chevron icon
|
||||
let chevronConfig = UIImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
|
||||
chevron.image = UIImage(systemName: "chevron.down", withConfiguration: chevronConfig)
|
||||
chevron.tintColor = Theme.Color.textSecondary
|
||||
chevron.contentMode = .scaleAspectFit
|
||||
chevron.transform = isExpanded
|
||||
? CGAffineTransform(rotationAngle: .pi)
|
||||
: .identity
|
||||
chevron.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Question
|
||||
questionLabel.text = item.question
|
||||
questionLabel.font = Theme.Font.body(weight: .medium)
|
||||
questionLabel.textColor = Theme.Color.textPrimary
|
||||
questionLabel.numberOfLines = 0
|
||||
|
||||
// Answer
|
||||
answerLabel.text = item.answer
|
||||
answerLabel.font = Theme.Font.caption(weight: .regular)
|
||||
answerLabel.textColor = Theme.Color.textSecondary
|
||||
answerLabel.numberOfLines = 0
|
||||
answerLabel.isHidden = !isExpanded
|
||||
|
||||
let questionStack = UIStackView(arrangedSubviews: [questionLabel, chevron])
|
||||
questionStack.axis = .horizontal
|
||||
questionStack.spacing = 12
|
||||
questionStack.alignment = .top
|
||||
questionStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let mainStack = UIStackView(arrangedSubviews: [questionStack, answerLabel])
|
||||
mainStack.axis = .vertical
|
||||
mainStack.spacing = 10
|
||||
mainStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(mainStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
chevron.widthAnchor.constraint(equalToConstant: 16),
|
||||
chevron.heightAnchor.constraint(equalToConstant: 16),
|
||||
|
||||
mainStack.topAnchor.constraint(equalTo: topAnchor, constant: 16),
|
||||
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
||||
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
|
||||
])
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(rowTapped))
|
||||
addGestureRecognizer(tap)
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
@objc private func rowTapped() { onToggle?() }
|
||||
}
|
||||
Reference in New Issue
Block a user