From e7587001d69502e1f8e381cb4a2a0685ad7e7fd5 Mon Sep 17 00:00:00 2001 From: Alex Beaty Date: Sat, 17 Oct 2020 15:13:10 -0500 Subject: [PATCH] Create UI for ProductAttributes in Detail View The product summary Overview needs to display rows for a product's Attributes, whose data are fetched from the newly integrated ProductAttribute API endpoint. This required a subclass of UIView and a new class of TableView Cells to hold them. When the user taps an Attribute row, a pop-up info card will be displayed showing a longer description. Swiping left will show more related information on the pop-up info card. Resolves: #707 --- Sources/Models/Common/Form.swift | 3 +- .../Detail/ProductDetailViewController.swift | 19 ++- .../AttributeTableViewCell.swift | 131 ++++++++++++++++ .../AttributeTableViewCell.xib | 50 ++++++ .../ProductAttributes/AttributeView.swift | 144 ++++++++++++++++++ .../ProductAttributes/AttributeView.xib | 57 +++++++ 6 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.swift create mode 100644 Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.xib create mode 100644 Sources/Views/Products/Detail/ProductAttributes/AttributeView.swift create mode 100644 Sources/Views/Products/Detail/ProductAttributes/AttributeView.xib diff --git a/Sources/Models/Common/Form.swift b/Sources/Models/Common/Form.swift index 6233c7bab53..5b255ea6b7c 100644 --- a/Sources/Models/Common/Form.swift +++ b/Sources/Models/Common/Form.swift @@ -23,7 +23,8 @@ struct Form { ProductDetailWebViewTableViewCell.self, SummaryHeaderCell.self, SummaryFooterCell.self, - HostedViewCell.self + HostedViewCell.self, + AttributeTableViewCell.self ] } } diff --git a/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift b/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift index 6f9584e7a59..fb6fece81f9 100644 --- a/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift +++ b/Sources/ViewControllers/Products/Detail/ProductDetailViewController.swift @@ -61,8 +61,6 @@ class ProductDetailViewController: ButtonBarPagerTabStripViewController, DataMan override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - //TODO: Answers.logContentView(withName: "Product's detail", contentType: "product_detail", contentId: product.barcode, customAttributes: ["product_name": product.name ?? ""]) - if let parentVc = parent as? UINavigationController { parentVc.navigationBar.isTranslucent = false @@ -511,6 +509,23 @@ class ProductDetailViewController: ButtonBarPagerTabStripViewController, DataMan } } + private func createProductAttributeRows(rows: inout [FormRow]) { + if let attributeGroups = product.productAttributes?.attributeGroups { + for attrGroup in attributeGroups where attrGroup.id == "labels" { + if let attributes = attrGroup.attributes { + for attribute in attributes { + if let desc = attribute.descriptionShort ?? attribute.title, + desc != "", let name = attribute.name, name != "" { + createFormRow(with: &rows, item: attribute, label: attribute.name, cellType: AttributeTableViewCell.self) + } else { + continue + } + } + } + } + } + } + private func createFormRow(with array: inout [FormRow], item: Any?, label: String? = nil, cellType: ProductDetailBaseCell.Type = InfoRowTableViewCell.self, isCopiable: Bool = false, separator: String = ", ") { // Check item has a value, if so add to the array of rows. switch item { diff --git a/Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.swift b/Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.swift new file mode 100644 index 00000000000..b934e087e11 --- /dev/null +++ b/Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.swift @@ -0,0 +1,131 @@ +// +// AttributeTableViewCell.swift +// OpenFoodFacts +// +// Created by Alexander Scott Beaty on on 17/10/2020. +// Copyright © 2020 Alexander Scott Beaty. All rights reserved. +// + +import UIKit +import BLTNBoard +import Kingfisher +import Cartography + +class AttributeTableViewCell: ProductDetailBaseCell { + + @IBOutlet weak var stackView: UIStackView! + + var viewController: FormTableViewController? + var attribute: Attribute? + + fileprivate var gestureRecognizer: UITapGestureRecognizer? + var bulletinManager: BLTNItemManager! + + override func configure(with formRow: FormRow, in viewController: FormTableViewController) { + guard let attribute = formRow.value as? Attribute else { return } + self.attribute = attribute + self.viewController = viewController + + removeGestureRecognizer() + // 'circle "i"' infoImage is in xib, need to retrieve it and add it back later + let infoImageView = stackView.arrangedSubviews.last + stackView.removeAllViews() + + let attributeView = AttributeView.loadFromNib() + attributeView.configure(attribute) + + configureGestureRecognizer() + stackView.addArrangedSubview(attributeView) + if let iiv = infoImageView { + stackView.addArrangedSubview(iiv) + } + } + + override func dismiss() { + super.dismiss() + stackView.arrangedSubviews.forEach { + if let view = $0 as? IngredientsAnalysisView { + view.removeGestureRecognizer() + } + } + stackView.removeAllViews() + } + + func configureGestureRecognizer() { + self.gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTap)) + if let gestureRecognizer = self.gestureRecognizer { + self.addGestureRecognizer(gestureRecognizer) + self.isUserInteractionEnabled = true + } + } + + func removeGestureRecognizer() { + if let validGesture = self.gestureRecognizer { + self.removeGestureRecognizer(validGesture) + self.gestureRecognizer = nil + } + } + + @objc func didTap(_ sender: UITapGestureRecognizer) { + print("Attribute tapped") + guard let attribute = attribute else { + return + } + let page = AttributeBLTNPageItem() + page.isDismissable = true + page.requiresCloseButton = false + + let attributeView = AttributeView.loadFromNib() + attributeView.configure(attribute) + page.attributeView = attributeView + page.attribute = attribute + + page.iconImageBackgroundColor = self.backgroundColor + page.alternativeButtonTitle = "generic.ok".localized + page.alternativeHandler = { item in + item.manager?.dismissBulletin() + } + + bulletinManager = BLTNItemManager(rootItem: page) + bulletinManager.showBulletin(in: UIApplication.shared) + + page.alternativeButton?.titleLabel?.numberOfLines = 2 + page.alternativeButton?.titleLabel?.textAlignment = .center + } +} + +class AttributeBLTNPageItem: BLTNPageItem { + var attribute: Attribute? + var iconImageBackgroundColor: UIColor? + var attributeView: AttributeView? + + override func makeHeaderViews(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView]? { + let containerA = UIView() + containerA.backgroundColor = iconImageBackgroundColor + + if let attributeV = attributeView { + containerA.addSubview(attributeV) + + constrain(attributeV, containerA) { (attributeV, containerA) in + containerA.width == attributeV.width + containerA.height == attributeV.height + attributeV.edges == containerA.edges + + } + + var views: [UIView] = [containerA] + + if let attribute = attribute { + let descriptionLabel = UILabel() + descriptionLabel.numberOfLines = 0 + descriptionLabel.textAlignment = .center + descriptionLabel.font = UIFont.boldSystemFont(ofSize: 17) + + descriptionLabel.text = attribute.descriptionLong ?? attribute.descriptionShort ?? "empty description" + views.append(descriptionLabel) + } + return views + } + return nil + } +} diff --git a/Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.xib b/Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.xib new file mode 100644 index 00000000000..5bf48e48048 --- /dev/null +++ b/Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.xib @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Views/Products/Detail/ProductAttributes/AttributeView.swift b/Sources/Views/Products/Detail/ProductAttributes/AttributeView.swift new file mode 100644 index 00000000000..9be934a6c83 --- /dev/null +++ b/Sources/Views/Products/Detail/ProductAttributes/AttributeView.swift @@ -0,0 +1,144 @@ +// +// AttributeView.swift +// OpenFoodFacts +// +// Created by Alexander Scott Beaty on on 17/10/2020. +// Copyright © 2020 Alexander Scott Beaty. All rights reserved. +// + +import UIKit +import BLTNBoard +import Kingfisher +import Cartography + +@IBDesignable class AttributeView: UIView { + + @IBOutlet weak var iconImageView: UIImageView! + @IBOutlet weak var descriptionShort: UITextView! + + var attribute: Attribute? + + var openProductEditHandler: (() -> Void)? + + func configure(_ attribute: Attribute) { + self.layer.cornerRadius = 5 + self.attribute = attribute + setIconImageView(imageURL: attribute.iconUrl) + if let label = attribute.name, let description = attribute.descriptionShort ?? attribute.title { + let text = AttributedStringFormatter.formatAttributedText(label: label, description: description) + descriptionShort.attributedText = text + } + } + + static func loadFromNib() -> AttributeView { + let nib = UINib(nibName: "AttributeView", bundle: Bundle.main) + // swiftlint:disable:next force_cast + let view = nib.instantiate(withOwner: self, options: nil).first as! AttributeView + return view + } + + func setIconImageView(imageURL: String?) { + guard let icon = imageURL, + let url = URL(string: icon) + else { + iconImageView.isHidden = false + return + } + iconImageView.kf.indicatorType = .activity + iconImageView.kf.setImage(with: url) + iconImageView.isHidden = false + } + + var bulletinManager: BLTNItemManager! + + deinit { + if bulletinManager != nil { + if bulletinManager.isShowingBulletin { + bulletinManager.dismissBulletin(animated: true) + } + bulletinManager = nil + } + } +} + +protocol formatAttributedString { + static var boldWordsPattern: String {get} + + static func formatAttributedText(label: String, description: String) -> NSMutableAttributedString? + static func makeWordsBold(for originalText: NSAttributedString) -> NSAttributedString +} + +class AttributedStringFormatter: formatAttributedString { + static var boldWordsPattern: String { return "(_\\w+_)" } + + static func formatAttributedText(label: String, description: String) -> NSMutableAttributedString? { + let headline = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.headline), size: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.caption1).pointSize) + var bold: [NSAttributedString.Key: Any] = [:] + if #available(iOS 13.0, *) { + bold = [NSAttributedString.Key.foregroundColor: UIColor.label, NSAttributedString.Key.font: headline] + } else { + bold = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: headline] + } + + let body = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body), size: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.caption2).pointSize) + var regular: [NSAttributedString.Key: Any] = [:] + if #available(iOS 13.0, *) { + regular = [NSAttributedString.Key.foregroundColor: UIColor.label, NSAttributedString.Key.font: body] + } else { + regular = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: body] + } + let combination = NSMutableAttributedString() + combination.append(NSAttributedString(string: label + "\n", attributes: bold)) + + let descrip = NSAttributedString(string: description, attributes: regular) + combination.append(descrip) + + return combination + } + + /// Create an attributed string with word surrounded by '_' (e.g. _Milk_) bold. + /// + /// - Parameter originalText: Original text with words to be made bold surrounded by '_' + /// - Returns: NSAttributedString with highlighted words + static func makeWordsBold(for originalText: NSAttributedString) -> NSAttributedString { + let body = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body), size: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.body).pointSize) + let headline = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.headline), size: UIFontDescriptor.preferredFontDescriptor(withTextStyle: UIFont.TextStyle.headline).pointSize) + let highlightedText = NSMutableAttributedString(attributedString: originalText) + var bold: [NSAttributedString.Key: Any] = [:] + if #available(iOS 13.0, *) { + bold = [NSAttributedString.Key.foregroundColor: UIColor.label, NSAttributedString.Key.font: headline] + } else { + bold = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: headline] + } + var regular: [NSAttributedString.Key: Any] = [:] + if #available(iOS 13.0, *) { + regular = [NSAttributedString.Key.foregroundColor: UIColor.label, NSAttributedString.Key.font: body] + } else { + regular = [NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.font: body] + } + highlightedText.addAttributes(regular, range: originalText.string.nsrange) + + do { + let regex = try NSRegularExpression(pattern: boldWordsPattern) + let matches = regex.matches(in: originalText.string, range: originalText.string.nsrange) + + for match in matches.reversed() { + highlightedText.setAttributes(bold, range: match.range) + + // Delete underscore characters + var trailingRange = match.range(at: 1) + trailingRange.location += trailingRange.length - 1 + trailingRange.length = 1 + var initialRange = match.range(at: 1) + initialRange.length = 1 + highlightedText.deleteCharacters(in: trailingRange) + highlightedText.deleteCharacters(in: initialRange) + } + } catch let error { + let userInfo = ["bold_words_pattern": boldWordsPattern, "original_text": originalText.string] + AnalyticsManager.record(error: error, withAdditionalUserInfo: userInfo) + } + + return highlightedText + } +} diff --git a/Sources/Views/Products/Detail/ProductAttributes/AttributeView.xib b/Sources/Views/Products/Detail/ProductAttributes/AttributeView.xib new file mode 100644 index 00000000000..69ee1556323 --- /dev/null +++ b/Sources/Views/Products/Detail/ProductAttributes/AttributeView.xib @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +