Skip to content

Commit

Permalink
Create UI for ProductAttributes in Detail View
Browse files Browse the repository at this point in the history
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: openfoodfacts#707
  • Loading branch information
jncosideout committed Nov 22, 2020
1 parent 62da3e3 commit 93c9c0b
Show file tree
Hide file tree
Showing 6 changed files with 402 additions and 4 deletions.
3 changes: 2 additions & 1 deletion Sources/Models/Common/Form.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ struct Form {
ProductDetailWebViewTableViewCell.self,
SummaryHeaderCell.self,
SummaryFooterCell.self,
HostedViewCell.self
HostedViewCell.self,
AttributeTableViewCell.self
]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -221,7 +219,7 @@ class ProductDetailViewController: ButtonBarPagerTabStripViewController, DataMan
createFormRow(with: &rows, item: product.labelsTags?.map({ (labelTag: String) -> NSAttributedString in
if let label = dataManager.label(forTag: labelTag) {
if let name = Tag.choose(inTags: Array(label.names)) {
return NSAttributedString(string: name.value, attributes: [NSAttributedString.Key.link : OFFUrlsHelper.url(forLabel: label)])
return NSAttributedString(string: name.value, attributes: [NSAttributedString.Key.link: OFFUrlsHelper.url(forLabel: label)])
}
}
return NSAttributedString(string: labelTag)
Expand Down Expand Up @@ -482,6 +480,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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AttributeTableViewCell" customModule="OpenFoodFacts" customModuleProvider="target"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="292" id="KGk-i7-Jjw" customClass="AttributeTableViewCell" customModule="OpenFoodFacts" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="352" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="352" height="80"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillProportionally" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="cII-ek-SeD">
<rect key="frame" x="2" y="2" width="24" height="76"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="circle-info" translatesAutoresizingMaskIntoConstraints="NO" id="EF2-Xg-lUH">
<rect key="frame" x="0.0" y="0.0" width="24" height="76"/>
<constraints>
<constraint firstAttribute="width" constant="24" id="0RV-os-Uv7"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" constant="24" id="L80-4c-hfc"/>
</constraints>
</imageView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="cII-ek-SeD" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="2" id="7Qm-vp-BcC"/>
<constraint firstAttribute="bottom" secondItem="cII-ek-SeD" secondAttribute="bottom" constant="2" id="R6z-AJ-fFb"/>
<constraint firstItem="cII-ek-SeD" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="2" id="UPc-OB-hQY"/>
<constraint firstAttribute="trailing" secondItem="cII-ek-SeD" secondAttribute="trailing" constant="16" id="klx-m6-hzo"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<connections>
<outlet property="stackView" destination="cII-ek-SeD" id="Jn9-BQ-U33"/>
</connections>
<point key="canvasLocation" x="33.600000000000001" y="178.11094452773614"/>
</tableViewCell>
</objects>
<resources>
<image name="circle-info" width="24" height="24"/>
</resources>
</document>
144 changes: 144 additions & 0 deletions Sources/Views/Products/Detail/ProductAttributes/AttributeView.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 93c9c0b

Please sign in to comment.