Skip to content

Commit 3f0d177

Browse files
authored
Merge pull request #2210 from woocommerce/issue/2053-add-product-more-details-button
Add "+ Add more details" CTA to the bottom of the product form
2 parents 5bddf6b + fdcd599 commit 3f0d177

File tree

8 files changed

+139
-24
lines changed

8 files changed

+139
-24
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import UIKit
2+
3+
extension UIButton {
4+
/// Sets a spacing between button title and image.
5+
/// - Parameters:
6+
/// - spacing: spacing between the title and image.
7+
/// - layoutDirection: layout direction of the button (LTR/RTL).
8+
func distributeTitleAndImage(spacing: CGFloat, layoutDirection: UIUserInterfaceLayoutDirection = UIApplication.shared.userInterfaceLayoutDirection) {
9+
let insetAmount = spacing / 2.0
10+
11+
switch layoutDirection {
12+
case .rightToLeft:
13+
imageEdgeInsets = UIEdgeInsets(top: imageEdgeInsets.top, left: insetAmount, bottom: imageEdgeInsets.bottom, right: imageEdgeInsets.right)
14+
titleEdgeInsets = UIEdgeInsets(top: titleEdgeInsets.top, left: titleEdgeInsets.left, bottom: titleEdgeInsets.bottom, right: insetAmount)
15+
default:
16+
imageEdgeInsets = UIEdgeInsets(top: imageEdgeInsets.top, left: imageEdgeInsets.left, bottom: imageEdgeInsets.bottom, right: insetAmount)
17+
titleEdgeInsets = UIEdgeInsets(top: titleEdgeInsets.top, left: insetAmount, bottom: titleEdgeInsets.bottom, right: titleEdgeInsets.right)
18+
}
19+
}
20+
}

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@ extension UIImage {
300300
.imageFlippedForRightToLeftLayoutDirection()
301301
}
302302

303+
/// Plus Icon
304+
///
305+
static var plusImage: UIImage {
306+
return UIImage.gridicon(.plus)
307+
}
308+
303309
/// Search Icon
304310
///
305311
static var searchImage: UIImage {

WooCommerce/Classes/ViewRelated/BottomButtonContainer/BottomButtonContainerView.swift

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
11
import UIKit
22

3-
/// Contains a primary button with insets to be displayed at the bottom of a view.
3+
/// Contains a button with insets to be displayed at the bottom of a view.
44
///
55
final class BottomButtonContainerView: UIView {
6+
/// The style of the button.
7+
enum ButtonStyle {
8+
case primary
9+
case link
10+
}
11+
612
struct ViewModel {
7-
let buttonTitle: String
8-
let onButtonTapped: () -> Void
13+
let style: ButtonStyle
14+
let title: String
15+
let image: UIImage?
16+
let onButtonTapped: (UIButton) -> Void
17+
18+
init(style: ButtonStyle, title: String, onButtonTapped: @escaping (UIButton) -> Void) {
19+
self.init(style: style, title: title, image: nil, onButtonTapped: onButtonTapped)
20+
}
21+
22+
init(style: ButtonStyle, title: String, image: UIImage?, onButtonTapped: @escaping (UIButton) -> Void) {
23+
self.style = style
24+
self.title = title
25+
self.image = image
26+
self.onButtonTapped = onButtonTapped
27+
}
928
}
1029

1130
private let button: UIButton = UIButton(type: .custom)
@@ -42,14 +61,35 @@ private extension BottomButtonContainerView {
4261
func configureButton() {
4362
addSubview(button)
4463
button.translatesAutoresizingMaskIntoConstraints = false
45-
pinSubviewToAllEdges(button, insets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16))
64+
pinSubviewToAllEdges(button, insets: Constants.buttonMarginInsets)
65+
66+
button.setTitle(viewModel.title, for: .normal)
67+
button.addTarget(self, action: #selector(buttonTapped(sender:)), for: .touchUpInside)
68+
button.titleLabel?.lineBreakMode = .byTruncatingTail
4669

47-
button.setTitle(viewModel.buttonTitle, for: .normal)
48-
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
49-
button.applyPrimaryButtonStyle()
70+
switch viewModel.style {
71+
case .primary:
72+
button.applyPrimaryButtonStyle()
73+
case .link:
74+
button.applyLinkButtonStyle()
75+
button.contentHorizontalAlignment = .leading
76+
button.contentEdgeInsets = .zero
77+
}
78+
79+
if let image = viewModel.image {
80+
button.setImage(image, for: .normal)
81+
button.distributeTitleAndImage(spacing: Constants.buttonTitleAndImageSpacing)
82+
}
5083
}
5184

52-
@objc func buttonTapped() {
53-
viewModel.onButtonTapped()
85+
@objc func buttonTapped(sender: UIButton) {
86+
viewModel.onButtonTapped(sender)
87+
}
88+
}
89+
90+
private extension BottomButtonContainerView {
91+
enum Constants {
92+
static let buttonMarginInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
93+
static let buttonTitleAndImageSpacing: CGFloat = 16
5494
}
5595
}

WooCommerce/Classes/ViewRelated/Filters/FilterListViewController.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,9 @@ private extension FilterListViewController {
224224
}
225225

226226
func configureBottomFilterButtonContainerView() {
227-
let buttonContainerViewModel = BottomButtonContainerView.ViewModel(buttonTitle: viewModel.filterActionTitle) { [weak self] in
228-
self?.filterActionButtonTapped()
227+
let buttonContainerViewModel = BottomButtonContainerView.ViewModel(style: .primary,
228+
title: viewModel.filterActionTitle) { [weak self] _ in
229+
self?.filterActionButtonTapped()
229230
}
230231
let buttonContainerView = BottomButtonContainerView(viewModel: buttonContainerViewModel)
231232
filterActionContainerView.addSubview(buttonContainerView)

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Yosemite
66
final class ProductFormViewController: UIViewController {
77

88
@IBOutlet private weak var tableView: UITableView!
9+
@IBOutlet private weak var moreDetailsContainerView: UIView!
910

1011
private lazy var keyboardFrameObserver: KeyboardFrameObserver = {
1112
let keyboardFrameObserver = KeyboardFrameObserver { [weak self] keyboardFrame in
@@ -54,9 +55,11 @@ final class ProductFormViewController: UIViewController {
5455
private let productUIImageLoader: ProductUIImageLoader
5556

5657
private let currency: String
58+
private let featureFlagService: FeatureFlagService
5759

58-
init(product: Product, currency: String) {
60+
init(product: Product, currency: String, featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
5961
self.currency = currency
62+
self.featureFlagService = featureFlagService
6063
self.originalProduct = product
6164
self.product = product
6265
self.viewModel = DefaultProductFormTableViewModel(product: product, currency: currency)
@@ -85,6 +88,7 @@ final class ProductFormViewController: UIViewController {
8588
configureNavigationBar()
8689
configureMainView()
8790
configureTableView()
91+
configureMoreDetailsContainerView()
8892

8993
startListeningToNotifications()
9094
handleSwipeBackGesture()
@@ -118,7 +122,7 @@ private extension ProductFormViewController {
118122
}
119123

120124
func configureMainView() {
121-
view.backgroundColor = .listBackground
125+
view.backgroundColor = .basicBackground
122126
}
123127

124128
func configureTableView() {
@@ -127,9 +131,12 @@ private extension ProductFormViewController {
127131
tableView.dataSource = tableViewDataSource
128132
tableView.delegate = self
129133

130-
tableView.backgroundColor = .listBackground
134+
tableView.backgroundColor = .listForeground
131135
tableView.removeLastCellSeparator()
132136

137+
// Since the table view is in a container under a stack view, the safe area adjustment should be handled in the container view.
138+
tableView.contentInsetAdjustmentBehavior = .never
139+
133140
tableView.reloadData()
134141
}
135142

@@ -149,6 +156,26 @@ private extension ProductFormViewController {
149156
}
150157
}
151158
}
159+
160+
func configureMoreDetailsContainerView() {
161+
guard featureFlagService.isFeatureFlagEnabled(.editProductsRelease2) else {
162+
moreDetailsContainerView.isHidden = true
163+
return
164+
}
165+
166+
let title = NSLocalizedString("Add more details", comment: "Title of the button at the bottom of the product form to add more product details.")
167+
let viewModel = BottomButtonContainerView.ViewModel(style: .link,
168+
title: title,
169+
image: .plusImage) { _ in
170+
// TODO-2053: show more details bottom sheet
171+
}
172+
let buttonContainerView = BottomButtonContainerView(viewModel: viewModel)
173+
174+
moreDetailsContainerView.addSubview(buttonContainerView)
175+
moreDetailsContainerView.pinSubviewToAllEdges(buttonContainerView)
176+
moreDetailsContainerView.setContentCompressionResistancePriority(.required, for: .vertical)
177+
moreDetailsContainerView.setContentHuggingPriority(.required, for: .vertical)
178+
}
152179
}
153180

154181
// MARK: Navigation actions
@@ -355,7 +382,7 @@ extension ProductFormViewController: UITableViewDelegate {
355382
switch section {
356383
case .settings:
357384
let clearView = UIView(frame: .zero)
358-
clearView.backgroundColor = .clear
385+
clearView.backgroundColor = .listBackground
359386
return clearView
360387
default:
361388
return nil

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.xib

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
33
<device id="retina6_1" orientation="portrait" appearance="light"/>
44
<dependencies>
55
<deployment identifier="iOS"/>
6-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
77
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
88
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
99
</dependencies>
1010
<objects>
1111
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ProductFormViewController" customModule="WooCommerce" customModuleProvider="target">
1212
<connections>
13+
<outlet property="moreDetailsContainerView" destination="wEZ-yI-3BV" id="t5o-n0-TNf"/>
1314
<outlet property="tableView" destination="jKS-9T-UtT" id="B2S-Hy-ugv"/>
1415
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
1516
</connections>
@@ -19,17 +20,29 @@
1920
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
2021
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
2122
<subviews>
22-
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="jKS-9T-UtT">
23+
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="lnJ-QS-EwA">
2324
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
24-
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
25-
</tableView>
25+
<subviews>
26+
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="jKS-9T-UtT">
27+
<rect key="frame" x="0.0" y="0.0" width="414" height="768"/>
28+
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
29+
</tableView>
30+
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wEZ-yI-3BV">
31+
<rect key="frame" x="0.0" y="768" width="414" height="50"/>
32+
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
33+
<constraints>
34+
<constraint firstAttribute="height" constant="50" placeholder="YES" id="4De-Hk-k6R"/>
35+
</constraints>
36+
</view>
37+
</subviews>
38+
</stackView>
2639
</subviews>
2740
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
2841
<constraints>
29-
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="jKS-9T-UtT" secondAttribute="trailing" id="IAY-Ez-MeG"/>
30-
<constraint firstItem="jKS-9T-UtT" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="XNr-rD-CSG"/>
31-
<constraint firstItem="jKS-9T-UtT" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="XUS-aw-ncD"/>
32-
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="jKS-9T-UtT" secondAttribute="bottom" id="ki0-L9-nvX"/>
42+
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="lnJ-QS-EwA" secondAttribute="bottom" id="2pu-vl-eJo"/>
43+
<constraint firstItem="lnJ-QS-EwA" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="iSP-Yg-fhy"/>
44+
<constraint firstItem="lnJ-QS-EwA" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="lnT-av-lRI"/>
45+
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="lnJ-QS-EwA" secondAttribute="trailing" id="pRJ-Xx-P8D"/>
3346
</constraints>
3447
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
3548
<point key="canvasLocation" x="139" y="96"/>

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
02913E9723A774E600707A0C /* DecimalInputFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02913E9623A774E600707A0C /* DecimalInputFormatter.swift */; };
181181
0295355B245ADF8100BDC42B /* FilterType+Products.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0295355A245ADF8100BDC42B /* FilterType+Products.swift */; };
182182
029B0F57234197B80010C1F3 /* ProductSearchUICommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029B0F56234197B80010C1F3 /* ProductSearchUICommand.swift */; };
183+
029BFD4F24597D4B00FDDEEC /* UIButton+TitleAndImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029BFD4E24597D4B00FDDEEC /* UIButton+TitleAndImage.swift */; };
183184
029D444922F13F8A00DEFA8A /* DashboardUIFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D444822F13F8A00DEFA8A /* DashboardUIFactory.swift */; };
184185
029D444E22F141CD00DEFA8A /* DashboardStatsV3ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029D444D22F141CD00DEFA8A /* DashboardStatsV3ViewController.swift */; };
185186
02A275BA23FE50AA005C560F /* ProductUIImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A275B923FE50AA005C560F /* ProductUIImageLoader.swift */; };
@@ -1010,6 +1011,7 @@
10101011
02913E9623A774E600707A0C /* DecimalInputFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalInputFormatter.swift; sourceTree = "<group>"; };
10111012
0295355A245ADF8100BDC42B /* FilterType+Products.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilterType+Products.swift"; sourceTree = "<group>"; };
10121013
029B0F56234197B80010C1F3 /* ProductSearchUICommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductSearchUICommand.swift; sourceTree = "<group>"; };
1014+
029BFD4E24597D4B00FDDEEC /* UIButton+TitleAndImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+TitleAndImage.swift"; sourceTree = "<group>"; };
10131015
029D444822F13F8A00DEFA8A /* DashboardUIFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardUIFactory.swift; sourceTree = "<group>"; };
10141016
029D444D22F141CD00DEFA8A /* DashboardStatsV3ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardStatsV3ViewController.swift; sourceTree = "<group>"; };
10151017
02A275B923FE50AA005C560F /* ProductUIImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductUIImageLoader.swift; sourceTree = "<group>"; };
@@ -3379,6 +3381,7 @@
33793381
02396250239948470096F34C /* UIImage+TintColor.swift */,
33803382
F997174323DC065900592D8E /* XLPagerStrip+AccessibilityIdentifier.swift */,
33813383
0215320A24231D5A003F2BBD /* UIStackView+Subviews.swift */,
3384+
029BFD4E24597D4B00FDDEEC /* UIButton+TitleAndImage.swift */,
33823385
57612988245888E2007BB2D9 /* NumberFormatter+LocalizedOrNinetyNinePlus.swift */,
33833386
);
33843387
path = Extensions;
@@ -4471,6 +4474,7 @@
44714474
D8C251D2230CA90200F49782 /* StoresManager.swift in Sources */,
44724475
02DD81FA242CAA400060E50B /* Media+WPMediaAsset.swift in Sources */,
44734476
024DF31F23743045006658FE /* Header+AztecFormatting.swift in Sources */,
4477+
029BFD4F24597D4B00FDDEEC /* UIButton+TitleAndImage.swift in Sources */,
44744478
B50911312049E27A007D25DC /* OrdersViewController.swift in Sources */,
44754479
B5A8F8AD20B88D9900D211DE /* LoginPrologueViewController.swift in Sources */,
44764480
B5D1AFC620BC7B7300DB0E8C /* StorePickerViewController.swift in Sources */,

WooCommerce/WooCommerceTests/Extensions/IconsTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ final class IconsTests: XCTestCase {
139139
XCTAssertNotNil(UIImage.moreImage)
140140
}
141141

142+
func testPlusImageIconIsNotNil() {
143+
XCTAssertNotNil(UIImage.plusImage)
144+
}
145+
142146
func testPriceImageIconIsNotNil() {
143147
XCTAssertNotNil(UIImage.priceImage)
144148
}

0 commit comments

Comments
 (0)