forked from openfoodfacts/openfoodfacts-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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: openfoodfacts#707
- Loading branch information
1 parent
62da3e3
commit 93c9c0b
Showing
6 changed files
with
402 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
131 changes: 131 additions & 0 deletions
131
Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
Sources/Views/Products/Detail/ProductAttributes/AttributeTableViewCell.xib
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
144
Sources/Views/Products/Detail/ProductAttributes/AttributeView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.