Skip to content

Commit 7b2cc16

Browse files
authored
Merge pull request #2216 from woocommerce/issue/1961-make-empty-search-vc-reusable
Make EmptySearchResultsViewController Reusable
2 parents e6efb0d + 0eb3865 commit 7b2cc16

File tree

7 files changed

+338
-37
lines changed

7 files changed

+338
-37
lines changed

WooCommerce/Classes/ViewRelated/ReusableViews/EmptySearchResultsViewController/EmptySearchResultsViewController.swift renamed to WooCommerce/Classes/ViewRelated/ReusableViews/EmptyStateViewController/EmptyStateViewController.swift

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,66 @@
11

22
import UIKit
33

4-
/// Shows a view with a message and a standard empty search results image.
4+
/// A configurable view to display an "empty state".
55
///
6-
/// This is generally used with `SearchUICommand`.
6+
/// This can show (from top to bottom):
77
///
8-
final class EmptySearchResultsViewController: UIViewController, KeyboardFrameAdjustmentProvider {
8+
/// - A message
9+
/// - An image
10+
/// - A label suitable for a longer message
11+
/// - An action button
12+
///
13+
/// These elements are hidden by default and can be configured and shown using
14+
/// the `configure` method.
15+
///
16+
final class EmptyStateViewController: UIViewController, KeyboardFrameAdjustmentProvider {
17+
18+
/// The submitted argument when configuring the `actionButton`.
19+
///
20+
struct ActionButtonConfig {
21+
let title: String
22+
let onTap: () -> ()
23+
}
924

25+
/// The main message shown at the top.
26+
///
1027
@IBOutlet private var messageLabel: UILabel! {
1128
didSet {
1229
// Remove dummy text in Interface Builder
1330
messageLabel.text = nil
31+
messageLabel.isHidden = true
32+
}
33+
}
34+
35+
/// An image shown below the message.
36+
///
37+
@IBOutlet private var imageView: UIImageView! {
38+
didSet {
39+
imageView.image = nil
40+
imageView.isHidden = true
41+
}
42+
}
43+
44+
/// Additional text shown below the image.
45+
///
46+
@IBOutlet private var detailsLabel: UILabel! {
47+
didSet {
48+
detailsLabel.text = nil
49+
detailsLabel.isHidden = true
1450
}
1551
}
16-
@IBOutlet private var imageView: UIImageView!
52+
53+
/// The button shown below the detail text.
54+
///
55+
@IBOutlet private var actionButton: UIButton! {
56+
didSet {
57+
actionButton.setTitle(nil, for: .normal)
58+
actionButton.isHidden = true
59+
}
60+
}
61+
62+
/// The scrollable view containing all the content (labels, image, etc).
63+
///
1764
@IBOutlet private var scrollView: UIScrollView!
1865

1966
/// The height adjustment constraint for the content view.
@@ -26,6 +73,10 @@ final class EmptySearchResultsViewController: UIViewController, KeyboardFrameAdj
2673
///
2774
@IBOutlet private var contentViewHeightAdjustmentFromSuperviewConstraint: NSLayoutConstraint!
2875

76+
/// The last `ActionButtonConfig` passed during `configure()`
77+
///
78+
private var lastActionButtonConfig: ActionButtonConfig?
79+
2980
private lazy var keyboardFrameObserver = KeyboardFrameObserver(onKeyboardFrameUpdate: { [weak self] frame in
3081
self?.handleKeyboardFrameUpdate(keyboardFrame: frame)
3182
self?.verticallyAlignStackViewUsing(keyboardHeight: frame.height)
@@ -48,9 +99,9 @@ final class EmptySearchResultsViewController: UIViewController, KeyboardFrameAdj
4899

49100
view.backgroundColor = .basicBackground
50101

51-
imageView.image = .emptySearchResultsImage
52-
53102
messageLabel.applyBodyStyle()
103+
detailsLabel.applySecondaryBodyStyle()
104+
actionButton.applyPrimaryButtonStyle()
54105

55106
keyboardFrameObserver.startObservingKeyboardFrame(sendInitialEvent: true)
56107
}
@@ -61,12 +112,26 @@ final class EmptySearchResultsViewController: UIViewController, KeyboardFrameAdj
61112
updateImageVisibilityUsing(traits: traitCollection)
62113
}
63114

64-
/// Change the message being displayed.
115+
/// Change the elements being displayed.
65116
///
66117
/// This is the only "configurable" point for consumers using this class.
67118
///
68-
func configure(message: NSAttributedString?) {
119+
func configure(message: NSAttributedString? = nil,
120+
image: UIImage? = nil,
121+
details: String? = nil,
122+
actionButton actionButtonConfig: ActionButtonConfig? = nil) {
69123
messageLabel.attributedText = message
124+
messageLabel.isHidden = message == nil
125+
126+
imageView.image = image
127+
imageView.isHidden = image == nil
128+
129+
detailsLabel.text = details
130+
detailsLabel.isHidden = details == nil
131+
132+
lastActionButtonConfig = actionButtonConfig
133+
actionButton.setTitle(actionButtonConfig?.title, for: .normal)
134+
actionButton.isHidden = actionButtonConfig == nil
70135
}
71136

72137
/// Watch for device orientation changes and update the `imageView`'s visibility accordingly.
@@ -83,7 +148,8 @@ final class EmptySearchResultsViewController: UIViewController, KeyboardFrameAdj
83148
/// Hide the `imageView` if there is not enough vertical space (e.g. iPhone landscape).
84149
///
85150
private func updateImageVisibilityUsing(traits: UITraitCollection) {
86-
let shouldShowImageView = traits.verticalSizeClass != .compact
151+
let shouldShowImageView = traits.verticalSizeClass != .compact &&
152+
imageView.image != nil
87153
imageView.isHidden = !shouldShowImageView
88154
}
89155

@@ -109,11 +175,16 @@ final class EmptySearchResultsViewController: UIViewController, KeyboardFrameAdj
109175

110176
contentViewHeightAdjustmentFromSuperviewConstraint.constant = constraintConstant
111177
}
178+
179+
/// OnTouchUpInside handler for the `actionButton`.
180+
@IBAction private func actionButtonTapped(_ sender: Any) {
181+
lastActionButtonConfig?.onTap()
182+
}
112183
}
113184

114185
// MARK: - KeyboardScrollable
115186

116-
extension EmptySearchResultsViewController: KeyboardScrollable {
187+
extension EmptyStateViewController: KeyboardScrollable {
117188
var scrollable: UIScrollView {
118189
scrollView
119190
}

WooCommerce/Classes/ViewRelated/ReusableViews/EmptySearchResultsViewController/EmptySearchResultsViewController.xib renamed to WooCommerce/Classes/ViewRelated/ReusableViews/EmptyStateViewController/EmptyStateViewController.xib

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" 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="15510"/>
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>
11-
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="EmptySearchResultsViewController" customModule="WooCommerce" customModuleProvider="target">
11+
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="EmptyStateViewController" customModule="WooCommerce" customModuleProvider="target">
1212
<connections>
13+
<outlet property="actionButton" destination="MHH-Z6-FbG" id="Qnb-WS-A16"/>
1314
<outlet property="contentViewHeightAdjustmentFromSuperviewConstraint" destination="j6E-vd-ZVT" id="vd1-ew-yqy"/>
15+
<outlet property="detailsLabel" destination="iFj-AP-WfL" id="U8q-I7-0hs"/>
1416
<outlet property="imageView" destination="YZ8-4Q-SGO" id="0Oa-4c-NbW"/>
1517
<outlet property="messageLabel" destination="h54-zO-thf" id="DJy-GP-8Ft"/>
1618
<outlet property="scrollView" destination="4Fw-pj-dQ8" id="XTp-KA-UBh"/>
@@ -28,8 +30,8 @@
2830
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="nZ1-Qr-0N4" userLabel="ContentView">
2931
<rect key="frame" x="0.0" y="0.0" width="414" height="818"/>
3032
<subviews>
31-
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Usx-wg-Fgl">
32-
<rect key="frame" x="58" y="320.5" width="298" height="177"/>
33+
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="32" translatesAutoresizingMaskIntoConstraints="NO" id="Usx-wg-Fgl">
34+
<rect key="frame" x="58" y="219" width="298" height="380.5"/>
3335
<subviews>
3436
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="We're sorry, we couldn't find results for &quot;adidas&quot;" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h54-zO-thf">
3537
<rect key="frame" x="0.0" y="0.0" width="298" height="41"/>
@@ -38,11 +40,25 @@
3840
<nil key="highlightedColor"/>
3941
</label>
4042
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="woo-empty-search-results" translatesAutoresizingMaskIntoConstraints="NO" id="YZ8-4Q-SGO">
41-
<rect key="frame" x="0.0" y="73" width="298" height="104"/>
43+
<rect key="frame" x="76" y="73" width="146" height="105"/>
44+
</imageView>
45+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iFj-AP-WfL">
46+
<rect key="frame" x="0.5" y="210" width="297" height="108.5"/>
47+
<string key="text">Qui eum est quo et recusandae ut. Hic cupiditate nobis et pariatur quidem dolorum. Doloribus est tempora ipsam eius. Quos ab et aspernatur velit corporis aut rem.</string>
48+
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
49+
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
50+
<nil key="highlightedColor"/>
51+
</label>
52+
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="MHH-Z6-FbG" userLabel="Action Button">
53+
<rect key="frame" x="35" y="350.5" width="228" height="30"/>
4254
<constraints>
43-
<constraint firstAttribute="height" constant="104" id="du3-ag-ShI"/>
55+
<constraint firstAttribute="width" constant="228" id="ytx-Jd-N6j"/>
4456
</constraints>
45-
</imageView>
57+
<state key="normal" title="Action Button"/>
58+
<connections>
59+
<action selector="actionButtonTapped:" destination="-1" eventType="touchUpInside" id="Tdd-LW-tQx"/>
60+
</connections>
61+
</button>
4662
</subviews>
4763
</stackView>
4864
</subviews>

WooCommerce/Classes/ViewRelated/Search/Order/OrderSearchUICommand.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ final class OrderSearchUICommand: SearchUICommand {
3636
OrderSearchStarterViewController()
3737
}
3838

39-
func configureEmptyStateViewControllerBeforeDisplay(viewController: EmptySearchResultsViewController,
39+
func configureEmptyStateViewControllerBeforeDisplay(viewController: EmptyStateViewController,
4040
searchKeyword: String) {
4141
let boldSearchKeyword = NSAttributedString(string: searchKeyword, attributes: [.font: viewController.messageFont.bold])
4242

@@ -45,7 +45,7 @@ final class OrderSearchUICommand: SearchUICommand {
4545
let message = NSMutableAttributedString(string: format)
4646
message.replaceFirstOccurrence(of: "%@", with: boldSearchKeyword)
4747

48-
viewController.configure(message: message)
48+
viewController.configure(message: message, image: .emptySearchResultsImage)
4949
}
5050

5151
func createCellViewModel(model: Order) -> OrderSearchCellViewModel {

WooCommerce/Classes/ViewRelated/Search/Product/ProductSearchUICommand.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ final class ProductSearchUICommand: SearchUICommand {
3030
nil
3131
}
3232

33-
func configureEmptyStateViewControllerBeforeDisplay(viewController: EmptySearchResultsViewController,
33+
func configureEmptyStateViewControllerBeforeDisplay(viewController: EmptyStateViewController,
3434
searchKeyword: String) {
3535
let boldSearchKeyword = NSAttributedString(string: searchKeyword, attributes: [.font: viewController.messageFont.bold])
3636

@@ -39,7 +39,7 @@ final class ProductSearchUICommand: SearchUICommand {
3939
let message = NSMutableAttributedString(string: format)
4040
message.replaceFirstOccurrence(of: "%@", with: boldSearchKeyword)
4141

42-
viewController.configure(message: message)
42+
viewController.configure(message: message, image: .emptySearchResultsImage)
4343
}
4444

4545
func createCellViewModel(model: Product) -> ProductsTabProductViewModel {

WooCommerce/Classes/ViewRelated/Search/SearchUICommand.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Yosemite
55
protocol SearchUICommand {
66
associatedtype Model
77
associatedtype CellViewModel
8-
associatedtype EmptyStateViewControllerType: UIViewController = EmptySearchResultsViewController
8+
associatedtype EmptyStateViewControllerType: UIViewController = EmptyStateViewController
99

1010
/// The placeholder of the search bar.
1111
var searchBarPlaceholder: String { get }
@@ -84,13 +84,13 @@ extension SearchUICommand {
8484

8585
/// Creates an instance of `EmptySearchResultsViewController`
8686
///
87-
func createEmptyStateViewController() -> EmptySearchResultsViewController {
88-
EmptySearchResultsViewController()
87+
func createEmptyStateViewController() -> EmptyStateViewController {
88+
EmptyStateViewController()
8989
}
9090

9191
/// Default implementation which does not do anything.
9292
///
93-
func configureEmptyStateViewControllerBeforeDisplay(viewController: EmptySearchResultsViewController,
93+
func configureEmptyStateViewControllerBeforeDisplay(viewController: EmptyStateViewController,
9494
searchKeyword: String) {
9595
// noop
9696
}

0 commit comments

Comments
 (0)