From d4adc5abe9a2ceaabd28a278d2724dd7b8539fed Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 11:11:59 +0700 Subject: [PATCH 01/39] Add lightning image --- WooCommerce/Classes/Extensions/UIImage+Woo.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/WooCommerce/Classes/Extensions/UIImage+Woo.swift b/WooCommerce/Classes/Extensions/UIImage+Woo.swift index 63ee3de472f..1ee2112ec25 100644 --- a/WooCommerce/Classes/Extensions/UIImage+Woo.swift +++ b/WooCommerce/Classes/Extensions/UIImage+Woo.swift @@ -384,6 +384,12 @@ extension UIImage { return UIImage.gridicon(.menu) } + /// Lightning icon on offline banner + /// + static var lightningImage: UIImage { + return UIImage.gridicon(.offline) + } + /// Invisible Image /// static var invisibleImage: UIImage { From 56010426721fd75fa135892596a0d1c64ae63c37 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 11:12:11 +0700 Subject: [PATCH 02/39] Update IconsTests for lightning image --- WooCommerce/WooCommerceTests/Extensions/IconsTests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WooCommerce/WooCommerceTests/Extensions/IconsTests.swift b/WooCommerce/WooCommerceTests/Extensions/IconsTests.swift index bada09f9aef..d97de710797 100644 --- a/WooCommerce/WooCommerceTests/Extensions/IconsTests.swift +++ b/WooCommerce/WooCommerceTests/Extensions/IconsTests.swift @@ -496,4 +496,8 @@ final class IconsTests: XCTestCase { func test_welcomeImage_is_not_nil() { XCTAssertNotNil(UIImage.welcomeImage) } + + func test_lightningImage_is_not_nil() { + XCTAssertNotNil(UIImage.lightningImage) + } } From b21d1961b8f948f42dba6600709b9c52636bbf45 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 11:14:20 +0700 Subject: [PATCH 03/39] Add extension for UIViewController to configure offline banner --- .../UIViewController+Connectivity.swift | 44 +++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 12 +++-- 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift new file mode 100644 index 00000000000..8cdd2a14248 --- /dev/null +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -0,0 +1,44 @@ +import UIKit + +extension UIViewController { + /// Content of offline banner + /// + var offlineContentView: UIView { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 3 + stackView.distribution = .fillProportionally + stackView.alignment = .center + + let imageView = UIImageView(image: .lightningImage) + imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 24), + imageView.heightAnchor.constraint(equalToConstant: 24) + ]) + + let messageLabel = UILabel() + messageLabel.text = NSLocalizedString("Offline - using cached data", comment: "Message for offline banner") + messageLabel.applySecondaryBodyStyle() + messageLabel.textColor = .white + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(messageLabel) + return stackView + } + + /// Set up toolbar for the view controller to display the offline message, + /// and listen to connectivity status changes to change the toolbar's visibility. + /// + func configureOfflineBanner() { + let offlineItem = UIBarButtonItem(customView: offlineContentView) + let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbarItems = [spaceItem, offlineItem, spaceItem] + navigationController?.toolbar.barTintColor = .gray + + ServiceLocator.connectivityObserver.startObserving { [weak self] status in + self?.navigationController?.setToolbarHidden(status != .notReachable, animated: true) + } + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 90081569da2..bd0758e35e6 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1294,10 +1294,11 @@ DE525499268C8B32007A5829 /* UIRefreshControl+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */; }; DE67D46726B98FD000EFE8DB /* Publisher+WithLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */; }; DE67D46926BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */; }; - DE792E1826EF35F40071200C /* ConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1726EF35F40071200C /* ConnectivityObserver.swift */; }; - DE792E1B26EF37ED0071200C /* DefaultConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */; }; DE7842ED26F061650030C792 /* NumberFormatter+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */; }; DE7842EF26F079A60030C792 /* NumberFormatter+LocalizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */; }; + DE7842F726F2E9340030C792 /* UIViewController+Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */; }; + DE792E1826EF35F40071200C /* ConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1726EF35F40071200C /* ConnectivityObserver.swift */; }; + DE792E1B26EF37ED0071200C /* DefaultConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */; }; DE8C94662646990000C94823 /* PluginListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C94652646990000C94823 /* PluginListViewController.swift */; }; DE8C946E264699B600C94823 /* PluginListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8C946D264699B600C94823 /* PluginListViewModel.swift */; }; DEC2961F26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC2961E26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift */; }; @@ -2730,10 +2731,11 @@ DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+Woo.swift"; sourceTree = ""; }; DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFrom.swift"; sourceTree = ""; }; DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFromTests.swift"; sourceTree = ""; }; - DE792E1726EF35F40071200C /* ConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityObserver.swift; sourceTree = ""; }; - DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConnectivityObserver.swift; sourceTree = ""; }; DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Localized.swift"; sourceTree = ""; }; DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+LocalizedTests.swift"; sourceTree = ""; }; + DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Connectivity.swift"; sourceTree = ""; }; + DE792E1726EF35F40071200C /* ConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityObserver.swift; sourceTree = ""; }; + DE792E1A26EF37ED0071200C /* DefaultConnectivityObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultConnectivityObserver.swift; sourceTree = ""; }; DE8C94652646990000C94823 /* PluginListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewController.swift; sourceTree = ""; }; DE8C946D264699B600C94823 /* PluginListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListViewModel.swift; sourceTree = ""; }; DEC2961E26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCustomsFormListViewModel.swift; sourceTree = ""; }; @@ -5741,6 +5743,7 @@ DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */, DEC2962626C17AD8005A056B /* ShippingLabelCustomsForm+Localization.swift */, DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */, + DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */, ); path = Extensions; sourceTree = ""; @@ -7185,6 +7188,7 @@ D8736B5322EF4F5900A14A29 /* NotificationsBadgeController.swift in Sources */, B541B220218A007C008FE7C1 /* NSMutableParagraphStyle+Helpers.swift in Sources */, 45AE582C230D9D35001901E3 /* OrderNoteHeaderTableViewCell.swift in Sources */, + DE7842F726F2E9340030C792 /* UIViewController+Connectivity.swift in Sources */, E1C47209267A1ECC00D06DA1 /* CrashLoggingStack.swift in Sources */, D85A3C5226C15DE200C0E026 /* InPersonPaymentsPluginNotSupportedVersionView.swift in Sources */, CE583A0B2107937F00D73C1C /* TextViewTableViewCell.swift in Sources */, From 2ee175f06c1939b654cbca091a09e8be1d77f7e6 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 12:44:00 +0700 Subject: [PATCH 04/39] Update navigation controller toolbar visibility only when view controller is on screen --- .../Classes/Extensions/UIViewController+Connectivity.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 8cdd2a14248..76f3f93495d 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -38,7 +38,9 @@ extension UIViewController { navigationController?.toolbar.barTintColor = .gray ServiceLocator.connectivityObserver.startObserving { [weak self] status in - self?.navigationController?.setToolbarHidden(status != .notReachable, animated: true) + guard let self = self, + self.isViewOnScreen() else { return } + self.navigationController?.setToolbarHidden(status != .notReachable, animated: true) } } } From 686cf54b7b62a6b977a28566116291f648b4631c Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 12:55:49 +0700 Subject: [PATCH 05/39] Update method name updateListener for updating the listener of connectivity observer --- .../Classes/Extensions/UIViewController+Connectivity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 76f3f93495d..b3c9a1bb67f 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -37,7 +37,7 @@ extension UIViewController { toolbarItems = [spaceItem, offlineItem, spaceItem] navigationController?.toolbar.barTintColor = .gray - ServiceLocator.connectivityObserver.startObserving { [weak self] status in + ServiceLocator.connectivityObserver.updateListener { [weak self] status in guard let self = self, self.isViewOnScreen() else { return } self.navigationController?.setToolbarHidden(status != .notReachable, animated: true) From 0e488318b062dca895b32f4e09782aa0223e941a Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 14:16:06 +0700 Subject: [PATCH 06/39] Revert "Update method name updateListener for updating the listener of connectivity observer" This reverts commit 686cf54b7b62a6b977a28566116291f648b4631c. --- .../Classes/Extensions/UIViewController+Connectivity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index b3c9a1bb67f..76f3f93495d 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -37,7 +37,7 @@ extension UIViewController { toolbarItems = [spaceItem, offlineItem, spaceItem] navigationController?.toolbar.barTintColor = .gray - ServiceLocator.connectivityObserver.updateListener { [weak self] status in + ServiceLocator.connectivityObserver.startObserving { [weak self] status in guard let self = self, self.isViewOnScreen() else { return } self.navigationController?.setToolbarHidden(status != .notReachable, animated: true) From 895dc4c58af5a258c41417b16bdb77be6db97516 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 14:31:11 +0700 Subject: [PATCH 07/39] Apply callout style for offline banner text --- .../Classes/Extensions/UIViewController+Connectivity.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 76f3f93495d..1f8bf0618c3 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -20,7 +20,7 @@ extension UIViewController { let messageLabel = UILabel() messageLabel.text = NSLocalizedString("Offline - using cached data", comment: "Message for offline banner") - messageLabel.applySecondaryBodyStyle() + messageLabel.applyCalloutStyle() messageLabel.textColor = .white stackView.addArrangedSubview(imageView) From decf9e497ead4d61b531aa962233eee7f9d8ba7a Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 14:31:30 +0700 Subject: [PATCH 08/39] Configure offline banner for dashboard screen --- .../Dashboard/DashboardViewController.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index a89229c516a..1a0b26dc3c1 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -115,10 +115,21 @@ final class DashboardViewController: UIViewController { reloadDashboardUIStatsVersion(forced: false) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() dashboardUI?.view.frame = containerView.bounds } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } // MARK: - Configuration From 415bd82dec37c14f9ecbceee0ddca6ead4a03af9 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 14:31:53 +0700 Subject: [PATCH 09/39] Configure offline banner for order flow --- .../Order Details/OrderDetailsViewController.swift | 11 +++++++++++ .../Review Order/ReviewOrderViewController.swift | 11 +++++++++++ .../ViewRelated/Orders/OrdersRootViewController.swift | 11 +++++++++++ 3 files changed, 33 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift index 4372b1d2988..1c2212ded42 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift @@ -118,10 +118,21 @@ final class OrderDetailsViewController: UIViewController { } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tableView.updateHeaderHeight() } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift index f51bd359131..a1cb52d2f6b 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift @@ -60,6 +60,17 @@ final class ReviewOrderViewController: UIViewController { } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tableView.updateFooterHeight() diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift index 01f1da7a064..f73bae74b54 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift @@ -64,6 +64,17 @@ final class OrdersRootViewController: UIViewController { func presentDetails(for note: Note) { ordersViewController.presentDetails(for: note) } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } // MARK: - Configuration From 0ead06f258fcfd937eb2f20270a6d0f3df4afc3e Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 14:44:37 +0700 Subject: [PATCH 10/39] Configure offline banner for review flow --- .../Reviews/ReviewDetailsViewController.swift | 11 +++++++++++ .../ViewRelated/Reviews/ReviewsViewController.swift | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift index f1a2afb1655..03edfe91518 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift @@ -77,6 +77,17 @@ final class ReviewDetailsViewController: UIViewController { super.viewWillAppear(animated) markAsReadIfNeeded(notification) } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift index fdb2ca2fbaf..b00f7a225e9 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift @@ -163,6 +163,17 @@ final class ReviewsViewController: UIViewController { self.displayPlaceholderReviews() } } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } From 660767148fb01fb0f148d75967172a3e70d0f7c3 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 14:44:55 +0700 Subject: [PATCH 11/39] Configure offline banner for product flow --- .../Edit Product/ProductFormViewController.swift | 8 ++++++++ .../ViewRelated/Products/ProductsViewController.swift | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index 1f876f7ffce..7c6c5666d5c 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -134,6 +134,14 @@ final class ProductFormViewController: super.viewWillDisappear(animated) view.endEditing(true) + + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() } // MARK: - Navigation actions handling diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index 0e6e8eed264..6855cc3eba9 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -201,6 +201,17 @@ final class ProductsViewController: UIViewController { updateTableHeaderViewHeight() } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } // MARK: - Navigation Bar Actions From 0bd664318c412c59fb14581e6c578f10c1af7712 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 15:14:21 +0700 Subject: [PATCH 12/39] Update configuring offline banner in UIViewController extension --- .../Classes/Extensions/UIViewController+Connectivity.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 1f8bf0618c3..000a6f86057 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -37,7 +37,10 @@ extension UIViewController { toolbarItems = [spaceItem, offlineItem, spaceItem] navigationController?.toolbar.barTintColor = .gray - ServiceLocator.connectivityObserver.startObserving { [weak self] status in + let connected = ServiceLocator.connectivityObserver.isConnectivityAvailable + navigationController?.setToolbarHidden(connected, animated: true) + + ServiceLocator.connectivityObserver.updateListener { [weak self] status in guard let self = self, self.isViewOnScreen() else { return } self.navigationController?.setToolbarHidden(status != .notReachable, animated: true) From 45dc94910db315973de75690c10f77f9715bb6ec Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 15:14:33 +0700 Subject: [PATCH 13/39] Add offline banner for shipping label form --- .../ShippingLabelFormViewController.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift index 4728c804b44..22f7d48dd53 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift @@ -44,6 +44,17 @@ final class ShippingLabelFormViewController: UIViewController { registerTableViewHeaderFooters() observeViewModel() } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + configureOfflineBanner() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. + navigationController?.isToolbarHidden = true + } } // MARK: - View Configuration From fb4de9aa370330d403e21aef2e36d559ccb14dfc Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 16 Sep 2021 16:43:45 +0700 Subject: [PATCH 14/39] Update release notes --- RELEASE-NOTES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 5762f54d70a..54e4b4dde17 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,7 +2,7 @@ 7.6 ----- - +- [*] Show offline mode banner on screens that display cached data. [https://github.com/woocommerce/woocommerce-ios/pull/5000#event-5310977598] 7.5 ----- From 221eae97853744573f769215a6f2a93b25260e8f Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 15:49:21 +0700 Subject: [PATCH 15/39] Add statusPublisher to ConnectivityObserver --- .../Connectivity/ConnectivityObserver.swift | 5 ++++- .../DefaultConnectivityObserver.swift | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift index cecf6b6ef5b..09d95a13d99 100644 --- a/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift @@ -1,4 +1,4 @@ -import Foundation +import Combine /// Interface for the observing connectivity /// @@ -6,6 +6,9 @@ protocol ConnectivityObserver { /// Getter for current state of the connectivity. var isConnectivityAvailable: Bool { get } + /// Publisher for connectivity availability. + var statusPublisher: AnyPublisher { get } + /// Starts the observer. func startObserving() diff --git a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift index ab34860de4e..8c5870b85a8 100644 --- a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift @@ -1,4 +1,4 @@ -import Foundation +import Combine import Network final class DefaultConnectivityObserver: ConnectivityObserver { @@ -8,16 +8,23 @@ final class DefaultConnectivityObserver: ConnectivityObserver { private let networkMonitor: NWPathMonitor private let observingQueue: DispatchQueue = .global(qos: .background) - var isConnectivityAvailable: Bool { - if case .reachable = connectivityStatus(from: networkMonitor.currentPath) { - return true - } - return false + @Published private(set) var isConnectivityAvailable: Bool = false + @Published private var connectivityStatus: ConnectivityStatus = .unknown + + var statusPublisher: AnyPublisher { + $connectivityStatus.eraseToAnyPublisher() } init(networkMonitor: NWPathMonitor = .init()) { self.networkMonitor = networkMonitor startObserving() + networkMonitor.pathUpdateHandler = { [weak self] path in + guard let self = self else { return } + DispatchQueue.main.async { + self.isConnectivityAvailable = path.status == .satisfied + self.connectivityStatus = self.connectivityStatus(from: path) + } + } } func startObserving() { @@ -29,6 +36,8 @@ final class DefaultConnectivityObserver: ConnectivityObserver { guard let self = self else { return } let connectivityStatus = self.connectivityStatus(from: path) DispatchQueue.main.async { + self.isConnectivityAvailable = path.status == .satisfied + self.connectivityStatus = connectivityStatus listener(connectivityStatus) } } From 37ab5b46d89273b8958d2dd9ccce48e848c203f8 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 15:57:02 +0700 Subject: [PATCH 16/39] Update UIViewController+Connectivity extension to configure offline banner --- .../UIViewController+Connectivity.swift | 58 ++++++------------- 1 file changed, 18 insertions(+), 40 deletions(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 000a6f86057..e7be33fdf76 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -1,49 +1,27 @@ import UIKit +import Combine extension UIViewController { - /// Content of offline banner + /// Defines if the view controller has been configured to show a "no connection" banner when offline. + /// One way to configure the banner is to use `connectivitySubscription`. + /// This requires the view controller to be contained inside a `WooNavigationController`. + /// Defaults to `false`. /// - var offlineContentView: UIView { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 3 - stackView.distribution = .fillProportionally - stackView.alignment = .center - - let imageView = UIImageView(image: .lightningImage) - imageView.tintColor = .white - imageView.contentMode = .scaleAspectFit - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 24), - imageView.heightAnchor.constraint(equalToConstant: 24) - ]) - - let messageLabel = UILabel() - messageLabel.text = NSLocalizedString("Offline - using cached data", comment: "Message for offline banner") - messageLabel.applyCalloutStyle() - messageLabel.textColor = .white - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(messageLabel) - return stackView + @objc func hasConfiguredOfflineBanner() -> Bool { + false } - /// Set up toolbar for the view controller to display the offline message, - /// and listen to connectivity status changes to change the toolbar's visibility. + /// Observes changes in status of connectivity and returns a subscription. + /// Keep a strong reference to this subscription to show the offline banner in the navigation controller's built-in toolbar. + /// This requires the view controller to be contained inside a `WooNavigationController`. /// - func configureOfflineBanner() { - let offlineItem = UIBarButtonItem(customView: offlineContentView) - let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - toolbarItems = [spaceItem, offlineItem, spaceItem] - navigationController?.toolbar.barTintColor = .gray - - let connected = ServiceLocator.connectivityObserver.isConnectivityAvailable - navigationController?.setToolbarHidden(connected, animated: true) - - ServiceLocator.connectivityObserver.updateListener { [weak self] status in - guard let self = self, - self.isViewOnScreen() else { return } - self.navigationController?.setToolbarHidden(status != .notReachable, animated: true) - } + var connectivitySubscription: AnyCancellable { + ServiceLocator.connectivityObserver.statusPublisher + .sink { [weak self] status in + guard let self = self else { return } + guard let navigationController = self.navigationController as? WooNavigationController, + self.isViewOnScreen() else { return } + navigationController.setToolbarHidden(status != .notReachable, animated: true) + } } } From b9b5a85357161c8cadb559df709bad5962eebd99 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:07:38 +0700 Subject: [PATCH 17/39] Configure offline banner in WooNavigationController --- .../UIViewController+Connectivity.swift | 2 +- .../System/WooNavigationController.swift | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index e7be33fdf76..4b09c5fa9b1 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -21,7 +21,7 @@ extension UIViewController { guard let self = self else { return } guard let navigationController = self.navigationController as? WooNavigationController, self.isViewOnScreen() else { return } - navigationController.setToolbarHidden(status != .notReachable, animated: true) + navigationController.isToolbarHidden = status != .notReachable } } } diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index dd85f817a94..436cd572974 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -38,6 +38,36 @@ class WooNavigationController: UINavigationController { /// private class WooNavigationControllerDelegate: NSObject, UINavigationControllerDelegate { + /// Content of offline banner + /// + lazy var toolbarItemsForOfflineBanner: [UIBarButtonItem] = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 3 + stackView.distribution = .fillProportionally + stackView.alignment = .center + + let imageView = UIImageView(image: .lightningImage) + imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 24), + imageView.heightAnchor.constraint(equalToConstant: 24) + ]) + + let messageLabel = UILabel() + messageLabel.text = NSLocalizedString("Offline - using cached data", comment: "Message for offline banner") + messageLabel.applyCalloutStyle() + messageLabel.textColor = .white + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(messageLabel) + + let offlineItem = UIBarButtonItem(customView: stackView) + let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + return [spaceItem, offlineItem, spaceItem] + }() + /// Children delegate, all events will be forwarded to this object /// weak var forwardDelegate: UINavigationControllerDelegate? @@ -45,6 +75,12 @@ private class WooNavigationControllerDelegate: NSObject, UINavigationControllerD /// Configures the back button for the managed `ViewController` and forwards the event to the children delegate. /// func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + if let wooNavigationController = navigationController as? WooNavigationController, + viewController.hasConfiguredOfflineBanner() { + configureOfflineBanner(for: viewController, in: wooNavigationController) + } else { + navigationController.isToolbarHidden = true + } configureBackButton(for: viewController) forwardDelegate?.navigationController?(navigationController, willShow: viewController, animated: animated) } @@ -75,4 +111,15 @@ private extension WooNavigationControllerDelegate { func configureBackButton(for viewController: UIViewController) { viewController.removeNavigationBackBarButtonText() } + + /// Set up toolbar for the view controller to display the offline message, + /// and listen to connectivity status changes to change the toolbar's visibility. + /// + func configureOfflineBanner(for viewController: UIViewController, in navigationController: WooNavigationController) { + viewController.toolbarItems = toolbarItemsForOfflineBanner + navigationController.toolbar.barTintColor = .gray + + let connected = ServiceLocator.connectivityObserver.isConnectivityAvailable + navigationController.setToolbarHidden(connected, animated: false) + } } From 315001aa01f7f5ab838d0f43203582ea281f9245 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:08:12 +0700 Subject: [PATCH 18/39] Update offline banner configuration in My Store --- .../Dashboard/DashboardViewController.swift | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 1a0b26dc3c1..44e8b701956 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import Gridicons import WordPressUI @@ -15,6 +16,7 @@ final class DashboardViewController: UIViewController { private let dashboardUIFactory: DashboardUIFactory private var dashboardUI: DashboardUI? + private var cancellable: AnyCancellable? // Used to enable subtitle with store name private var shouldShowStoreNameAsSubtitle: Bool { @@ -115,20 +117,14 @@ final class DashboardViewController: UIViewController { reloadDashboardUIStatsVersion(forced: false) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() dashboardUI?.view.frame = containerView.bounds } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + cancellable = connectivitySubscription + return true } } From edaf32967b6f6d3203e1cdefe4a40e4bcd663368 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:11:14 +0700 Subject: [PATCH 19/39] Update offline banner configuration in Review flow --- .../Reviews/ReviewDetailsViewController.swift | 16 ++++++---------- .../Reviews/ReviewsViewController.swift | 15 ++++++--------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift index 03edfe91518..d397e0eade9 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift @@ -1,4 +1,4 @@ -import Foundation +import Combine import UIKit import Yosemite import Gridicons @@ -45,6 +45,8 @@ final class ReviewDetailsViewController: UIViewController { /// private var rows = [Row]() + private var cancellable: AnyCancellable? + /// Designated Initializer /// init(productReview: ProductReview, product: Product?, notification: Note?) { @@ -78,15 +80,9 @@ final class ReviewDetailsViewController: UIViewController { markAsReadIfNeeded(notification) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + cancellable = connectivitySubscription + return true } } diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift index b00f7a225e9..972747c55f0 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import SafariServices.SFSafariViewController @@ -109,6 +110,8 @@ final class ReviewsViewController: UIViewController { }) }() + var cancellable: AnyCancellable? + // MARK: - View Lifecycle init(siteID: Int64) { @@ -164,15 +167,9 @@ final class ReviewsViewController: UIViewController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + cancellable = connectivitySubscription + return true } } From c54bce859315d4552e702164f60245511f8690ec Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:24:22 +0700 Subject: [PATCH 20/39] Rename connectivitySubscription to observeConnectivity --- .../Classes/Extensions/UIViewController+Connectivity.swift | 2 +- .../ViewRelated/Dashboard/DashboardViewController.swift | 4 ++-- .../ViewRelated/Reviews/ReviewDetailsViewController.swift | 4 ++-- .../Classes/ViewRelated/Reviews/ReviewsViewController.swift | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 4b09c5fa9b1..384648f7deb 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -15,7 +15,7 @@ extension UIViewController { /// Keep a strong reference to this subscription to show the offline banner in the navigation controller's built-in toolbar. /// This requires the view controller to be contained inside a `WooNavigationController`. /// - var connectivitySubscription: AnyCancellable { + func observeConnectivity() -> AnyCancellable { ServiceLocator.connectivityObserver.statusPublisher .sink { [weak self] status in guard let self = self else { return } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 44e8b701956..040facfea0a 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -16,7 +16,7 @@ final class DashboardViewController: UIViewController { private let dashboardUIFactory: DashboardUIFactory private var dashboardUI: DashboardUI? - private var cancellable: AnyCancellable? + private var connectivitySubscription: AnyCancellable? // Used to enable subtitle with store name private var shouldShowStoreNameAsSubtitle: Bool { @@ -123,7 +123,7 @@ final class DashboardViewController: UIViewController { } override func hasConfiguredOfflineBanner() -> Bool { - cancellable = connectivitySubscription + connectivitySubscription = observeConnectivity() return true } } diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift index d397e0eade9..9f671f789bf 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift @@ -45,7 +45,7 @@ final class ReviewDetailsViewController: UIViewController { /// private var rows = [Row]() - private var cancellable: AnyCancellable? + private var connectivitySubscription: AnyCancellable? /// Designated Initializer /// @@ -81,7 +81,7 @@ final class ReviewDetailsViewController: UIViewController { } override func hasConfiguredOfflineBanner() -> Bool { - cancellable = connectivitySubscription + connectivitySubscription = observeConnectivity() return true } } diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift index 972747c55f0..89d570b663d 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift @@ -110,7 +110,7 @@ final class ReviewsViewController: UIViewController { }) }() - var cancellable: AnyCancellable? + var connectivitySubscription: AnyCancellable? // MARK: - View Lifecycle @@ -168,7 +168,7 @@ final class ReviewsViewController: UIViewController { } override func hasConfiguredOfflineBanner() -> Bool { - cancellable = connectivitySubscription + connectivitySubscription = observeConnectivity() return true } } From d32c80ca03d90f533be77fcf888ad8b753f779f8 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:25:38 +0700 Subject: [PATCH 21/39] Update offline banner configuration for order flow --- .../OrderDetailsViewController.swift | 14 +++++--------- .../ReviewOrderViewController.swift | 19 ++++++++----------- .../ShippingLabelFormViewController.swift | 15 ++++++--------- .../Orders/OrdersRootViewController.swift | 15 ++++++--------- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift index 1c2212ded42..66c5d6c0301 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift @@ -91,6 +91,8 @@ final class OrderDetailsViewController: UIViewController { return storyboard.instantiateViewController(withIdentifier: identifier) as? Self } + private var connectivitySubscription: AnyCancellable? + override func viewDidLoad() { super.viewDidLoad() configureNavigation() @@ -118,20 +120,14 @@ final class OrderDetailsViewController: UIViewController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tableView.updateHeaderHeight() } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + connectivitySubscription = observeConnectivity() + return true } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift index a1cb52d2f6b..4cc1de7d8c3 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift @@ -1,3 +1,4 @@ +import Combine import SafariServices import UIKit import Yosemite @@ -35,6 +36,8 @@ final class ReviewOrderViewController: UIViewController { /// private lazy var footerView: UIView = configureTableFooterView() + private var connectivitySubscription: AnyCancellable? + init(viewModel: ReviewOrderViewModel, markOrderCompleteHandler: @escaping () -> Void) { self.viewModel = viewModel self.markOrderCompleteHandler = markOrderCompleteHandler @@ -60,21 +63,15 @@ final class ReviewOrderViewController: UIViewController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true - } - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tableView.updateFooterHeight() } + + override func hasConfiguredOfflineBanner() -> Bool { + connectivitySubscription = observeConnectivity() + return true + } } // MARK: - UI Configuration diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift index 22f7d48dd53..8fe4acca177 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import Yosemite import SwiftUI @@ -21,6 +22,8 @@ final class ShippingLabelFormViewController: UIViewController { /// var onLabelSave: (() -> Void)? + private var connectivitySubscription: AnyCancellable? + /// Init /// init(order: Order) { @@ -45,15 +48,9 @@ final class ShippingLabelFormViewController: UIViewController { observeViewModel() } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + connectivitySubscription = observeConnectivity() + return true } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift index f73bae74b54..3fbccadb4fd 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import Yosemite @@ -30,6 +31,8 @@ final class OrdersRootViewController: UIViewController { private let siteID: Int64 + private var connectivitySubscription: AnyCancellable? + // MARK: View Lifecycle init(siteID: Int64) { @@ -65,15 +68,9 @@ final class OrdersRootViewController: UIViewController { ordersViewController.presentDetails(for: note) } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + connectivitySubscription = observeConnectivity() + return true } } From 3c6270109b1240b529b5efd9722324cdbf5f6a2c Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:25:50 +0700 Subject: [PATCH 22/39] Update offline banner configuration for products flow --- .../Edit Product/ProductFormViewController.swift | 13 +++++++------ .../Products/ProductsViewController.swift | 15 ++++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index 7c6c5666d5c..60ef81913d0 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -1,3 +1,4 @@ +import Combine import Photos import UIKit import WordPressUI @@ -54,6 +55,9 @@ final class ProductFormViewController: private var cancellableUpdateEnabled: ObservationToken? private var cancellableNewVariationsPrice: ObservationToken? + /// Strong reference to Combine subscription + private var connectivitySubscription: AnyCancellable? + init(viewModel: ViewModel, eventLogger: ProductFormEventLoggerProtocol, productImageActionHandler: ProductImageActionHandler, @@ -134,14 +138,11 @@ final class ProductFormViewController: super.viewWillDisappear(animated) view.endEditing(true) - - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() + override func hasConfiguredOfflineBanner() -> Bool { + connectivitySubscription = observeConnectivity() + return true } // MARK: - Navigation actions handling diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index 6855cc3eba9..3c05c3e6088 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import WordPressUI import Yosemite @@ -139,6 +140,8 @@ final class ProductsViewController: UIViewController { /// private var hasErrorLoadingData: Bool = false + private var connectivitySubscription: AnyCancellable? + deinit { NotificationCenter.default.removeObserver(self) } @@ -202,15 +205,9 @@ final class ProductsViewController: UIViewController { updateTableHeaderViewHeight() } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - configureOfflineBanner() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // hide the toolbar in case the next view controller in the stack doesn't provide contents for its `toolbarItems`. - navigationController?.isToolbarHidden = true + override func hasConfiguredOfflineBanner() -> Bool { + connectivitySubscription = observeConnectivity() + return true } } From 912d117f6101d59d2d905dc44aeccd90d02a8411 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Fri, 17 Sep 2021 16:28:51 +0700 Subject: [PATCH 23/39] Flip lightning image for RTL languages --- WooCommerce/Classes/Extensions/UIImage+Woo.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/Extensions/UIImage+Woo.swift b/WooCommerce/Classes/Extensions/UIImage+Woo.swift index 1ee2112ec25..8bb6c6c6882 100644 --- a/WooCommerce/Classes/Extensions/UIImage+Woo.swift +++ b/WooCommerce/Classes/Extensions/UIImage+Woo.swift @@ -387,7 +387,7 @@ extension UIImage { /// Lightning icon on offline banner /// static var lightningImage: UIImage { - return UIImage.gridicon(.offline) + return UIImage.gridicon(.offline).imageFlippedForRightToLeftLayoutDirection() } /// Invisible Image From 2eee06b794fd85eb653acb6ec6818a7d98a92052 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 20 Sep 2021 10:24:39 +0700 Subject: [PATCH 24/39] Update pathUpdateHandler to networkUpdateHandler to fix build failure --- .../Tools/Connectivity/DefaultConnectivityObserver.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift index 79d9199e728..fe75e902fea 100644 --- a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift @@ -18,11 +18,12 @@ final class DefaultConnectivityObserver: ConnectivityObserver { init(networkMonitor: NetworkMonitoring = NWPathMonitor()) { self.networkMonitor = networkMonitor startObserving() - networkMonitor.pathUpdateHandler = { [weak self] path in + networkMonitor.networkUpdateHandler = { [weak self] path in guard let self = self else { return } + let connectivityStatus = self.connectivityStatus(from: path) DispatchQueue.main.async { self.isConnectivityAvailable = path.status == .satisfied - self.connectivityStatus = self.connectivityStatus(from: path) + self.connectivityStatus = connectivityStatus } } } From 263e5320b748e10c707acdd222b72c2b4b67b231 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 20 Sep 2021 11:54:09 +0700 Subject: [PATCH 25/39] Update function hasConfiguredOfflineBanner to shouldShowOfflineBanner and move configuration logic out of the function --- .../Extensions/UIViewController+Connectivity.swift | 5 ++--- WooCommerce/Classes/System/WooNavigationController.swift | 2 +- .../ViewRelated/Dashboard/DashboardViewController.swift | 8 ++++++-- .../Orders/Order Details/OrderDetailsViewController.swift | 8 ++++++-- .../Review Order/ReviewOrderViewController.swift | 8 ++++++-- .../ShippingLabelFormViewController.swift | 8 ++++++-- .../ViewRelated/Orders/OrdersRootViewController.swift | 8 ++++++-- .../Products/Edit Product/ProductFormViewController.swift | 8 ++++++-- .../ViewRelated/Products/ProductsViewController.swift | 8 ++++++-- .../ViewRelated/Reviews/ReviewDetailsViewController.swift | 8 ++++++-- .../ViewRelated/Reviews/ReviewsViewController.swift | 8 ++++++-- 11 files changed, 57 insertions(+), 22 deletions(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 384648f7deb..15b5947283f 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -2,12 +2,11 @@ import UIKit import Combine extension UIViewController { - /// Defines if the view controller has been configured to show a "no connection" banner when offline. - /// One way to configure the banner is to use `connectivitySubscription`. + /// Defines if the view controller should show a "no connection" banner when offline. /// This requires the view controller to be contained inside a `WooNavigationController`. /// Defaults to `false`. /// - @objc func hasConfiguredOfflineBanner() -> Bool { + @objc var shouldShowOfflineBanner: Bool { false } diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index 436cd572974..07f461fb2c3 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -76,7 +76,7 @@ private class WooNavigationControllerDelegate: NSObject, UINavigationControllerD /// func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { if let wooNavigationController = navigationController as? WooNavigationController, - viewController.hasConfiguredOfflineBanner() { + viewController.shouldShowOfflineBanner { configureOfflineBanner(for: viewController, in: wooNavigationController) } else { navigationController.isToolbarHidden = true diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 040facfea0a..16acc959bdf 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -108,6 +108,7 @@ final class DashboardViewController: UIViewController { configureNavigation() configureView() configureDashboardUIContainer() + configureOfflineBanner() } override func viewWillAppear(_ animated: Bool) { @@ -122,8 +123,7 @@ final class DashboardViewController: UIViewController { dashboardUI?.view.frame = containerView.bounds } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -132,6 +132,10 @@ final class DashboardViewController: UIViewController { // private extension DashboardViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + func configureView() { view.backgroundColor = .listBackground } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift index 66c5d6c0301..260f382b448 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift @@ -102,6 +102,7 @@ final class OrderDetailsViewController: UIViewController { registerTableViewHeaderFooters() configureEntityListener() configureViewModel() + configureOfflineBanner() updateTopBannerView() // FIXME: this is a hack. https://github.com/woocommerce/woocommerce-ios/issues/1779 @@ -125,8 +126,7 @@ final class OrderDetailsViewController: UIViewController { tableView.updateHeaderHeight() } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -136,6 +136,10 @@ final class OrderDetailsViewController: UIViewController { // private extension OrderDetailsViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + /// Setup: TopLoaderView func configureTopLoaderView() { stackView.insertArrangedSubview(topLoaderView, at: 0) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift index 4cc1de7d8c3..ce09d1b0ad9 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift @@ -54,6 +54,7 @@ final class ReviewOrderViewController: UIViewController { configureNavigation() configureTableView() configureViewModel() + configureOfflineBanner() } override func viewWillAppear(_ animated: Bool) { @@ -68,8 +69,7 @@ final class ReviewOrderViewController: UIViewController { tableView.updateFooterHeight() } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -77,6 +77,10 @@ final class ReviewOrderViewController: UIViewController { // MARK: - UI Configuration // private extension ReviewOrderViewController { + private func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + func configureViewModel() { viewModel.configureResultsControllers { [weak self] in self?.tableView.reloadData() diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift index 8fe4acca177..17e303f68ea 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift @@ -46,10 +46,10 @@ final class ShippingLabelFormViewController: UIViewController { registerTableViewCells() registerTableViewHeaderFooters() observeViewModel() + configureOfflineBanner() } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -58,6 +58,10 @@ final class ShippingLabelFormViewController: UIViewController { // private extension ShippingLabelFormViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + func configureNavigationBar() { title = Localization.titleView } diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift index 3fbccadb4fd..f97362d9c14 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift @@ -54,6 +54,7 @@ final class OrdersRootViewController: UIViewController { configureView() configureContainerView() configureChildViewController() + configureOfflineBanner() } override func viewDidLayoutSubviews() { @@ -68,8 +69,7 @@ final class OrdersRootViewController: UIViewController { ordersViewController.presentDetails(for: note) } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -78,6 +78,10 @@ final class OrdersRootViewController: UIViewController { // private extension OrdersRootViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + func configureView() { view.backgroundColor = .listBackground } diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index a955f3e05fa..c6be943199e 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -106,6 +106,7 @@ final class ProductFormViewController: configureMainView() configureTableView() configureMoreDetailsContainerView() + configureOfflineBanner() startListeningToNotifications() handleSwipeBackGesture() @@ -138,8 +139,7 @@ final class ProductFormViewController: view.endEditing(true) } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } @@ -412,6 +412,10 @@ final class ProductFormViewController: // MARK: - Configuration // private extension ProductFormViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + func configureNavigationBar() { updateNavigationBar() updateBackButtonTitle() diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index 3c05c3e6088..387a0ca3c32 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -167,6 +167,7 @@ final class ProductsViewController: UIViewController { configureTableView() configureToolBarView() configureSyncingCoordinator() + configureOfflineBanner() registerTableViewCells() showTopBannerViewIfNeeded() @@ -205,8 +206,7 @@ final class ProductsViewController: UIViewController { updateTableHeaderViewHeight() } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -261,6 +261,10 @@ private extension ProductsViewController { // private extension ProductsViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + /// Set the title. /// func configureNavigationBar() { diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift index 9f671f789bf..f7da76eb3d8 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift @@ -70,6 +70,7 @@ final class ReviewDetailsViewController: UIViewController { configureTableView() configureEntityListener() configureAppRatingEvent() + configureOfflineBanner() registerTableViewCells() reloadInterface() @@ -80,8 +81,7 @@ final class ReviewDetailsViewController: UIViewController { markAsReadIfNeeded(notification) } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -91,6 +91,10 @@ final class ReviewDetailsViewController: UIViewController { // private extension ReviewDetailsViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + /// Setup: Main View /// func configureMainView() { diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift index 89d570b663d..e3f0cdd915b 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift @@ -139,6 +139,7 @@ final class ReviewsViewController: UIViewController { configureTableView() configureTableViewCells() configureResultsController() + configureOfflineBanner() startListeningToNotifications() syncingCoordinator.resynchronize() @@ -167,8 +168,7 @@ final class ReviewsViewController: UIViewController { } } - override func hasConfiguredOfflineBanner() -> Bool { - connectivitySubscription = observeConnectivity() + override var shouldShowOfflineBanner: Bool { return true } } @@ -178,6 +178,10 @@ final class ReviewsViewController: UIViewController { // private extension ReviewsViewController { + func configureOfflineBanner() { + connectivitySubscription = observeConnectivity() + } + /// Setup: Sync'ing Coordinator /// func configureSyncingCoordinator() { From db4fcc6c9b7b68ddeb19402673b8e436f36c3e95 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 20 Sep 2021 12:10:03 +0700 Subject: [PATCH 26/39] Inject connectivity observer to WooNavigationControllerDelegate --- WooCommerce/Classes/System/WooNavigationController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index 07f461fb2c3..ce203a00852 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -38,6 +38,12 @@ class WooNavigationController: UINavigationController { /// private class WooNavigationControllerDelegate: NSObject, UINavigationControllerDelegate { + private let connectivityObserver: ConnectivityObserver + + init(connectivityObserver: ConnectivityObserver = ServiceLocator.connectivityObserver) { + self.connectivityObserver = connectivityObserver + } + /// Content of offline banner /// lazy var toolbarItemsForOfflineBanner: [UIBarButtonItem] = { @@ -119,7 +125,7 @@ private extension WooNavigationControllerDelegate { viewController.toolbarItems = toolbarItemsForOfflineBanner navigationController.toolbar.barTintColor = .gray - let connected = ServiceLocator.connectivityObserver.isConnectivityAvailable + let connected = connectivityObserver.isConnectivityAvailable navigationController.setToolbarHidden(connected, animated: false) } } From 1ce862ec99304dfdfd1060a4635b966540f84d1b Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 20 Sep 2021 14:30:34 +0700 Subject: [PATCH 27/39] Add separate view for offline banner --- .../ReusableViews/OfflineBannerView.swift | 50 +++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 6 ++- 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift new file mode 100644 index 00000000000..b2a143c71df --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift @@ -0,0 +1,50 @@ +import UIKit + +/// Gray banner showing message when device is offline. +/// +final class OfflineBannerView: UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 3 + stackView.distribution = .fillProportionally + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + + let imageView = UIImageView(image: .lightningImage) + imageView.tintColor = .white + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 24), + imageView.heightAnchor.constraint(equalToConstant: 24) + ]) + + let messageLabel = UILabel() + messageLabel.text = NSLocalizedString("Offline - using cached data", comment: "Message for offline banner") + messageLabel.applyCalloutStyle() + messageLabel.textColor = .white + messageLabel.translatesAutoresizingMaskIntoConstraints = false + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(messageLabel) + + addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.safeLeadingAnchor.constraint(equalTo: safeLeadingAnchor, constant: 0), + stackView.safeBottomAnchor.constraint(equalTo: safeBottomAnchor, constant: 0), + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor) + ]) + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 3864a3ed47d..5e8ac47126c 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1297,6 +1297,7 @@ DE525499268C8B32007A5829 /* UIRefreshControl+Woo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */; }; DE67D46726B98FD000EFE8DB /* Publisher+WithLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */; }; DE67D46926BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */; }; + DE68B81F26F86B1700C86CFB /* OfflineBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE68B81E26F86B1700C86CFB /* OfflineBannerView.swift */; }; DE7842ED26F061650030C792 /* NumberFormatter+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */; }; DE7842EF26F079A60030C792 /* NumberFormatter+LocalizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */; }; DE7842F726F2E9340030C792 /* UIViewController+Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */; }; @@ -2737,6 +2738,7 @@ DE525498268C8B32007A5829 /* UIRefreshControl+Woo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIRefreshControl+Woo.swift"; sourceTree = ""; }; DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFrom.swift"; sourceTree = ""; }; DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFromTests.swift"; sourceTree = ""; }; + DE68B81E26F86B1700C86CFB /* OfflineBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineBannerView.swift; sourceTree = ""; }; DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Localized.swift"; sourceTree = ""; }; DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+LocalizedTests.swift"; sourceTree = ""; }; DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Connectivity.swift"; sourceTree = ""; }; @@ -4946,7 +4948,6 @@ 0277AEAA256CAA5300F45C4A /* MockShippingLabelAddress.swift */, 0211252D25773FB00075AD2A /* MockAggregateOrderItem.swift */, 4590B651261C8D1E00A6FCE0 /* WeightFormatterTests.swift */, - DE7842F826F435070030C792 /* DefaultConnectivityObserver.swift */, ); path = Tools; sourceTree = ""; @@ -6094,6 +6095,7 @@ 02A410F32583A84C005E2925 /* SpacerTableViewCell.swift */, 2664210026F3E1BB001FC5B4 /* ModalHostingPresentationController.swift */, 02A410F42583A84C005E2925 /* SpacerTableViewCell.xib */, + DE68B81E26F86B1700C86CFB /* OfflineBannerView.swift */, ); path = ReusableViews; sourceTree = ""; @@ -7659,6 +7661,7 @@ 31B0551E264B3C7A00134D87 /* CardPresentModalFoundReader.swift in Sources */, 45A24E5F2451DF1A0050606B /* ProductMenuOrderViewController.swift in Sources */, 2667BFE1252FA117008099D4 /* RefundItemQuantityListSelectorCommand.swift in Sources */, + DE68B81F26F86B1700C86CFB /* OfflineBannerView.swift in Sources */, 02D4564C231D05E2008CF0A9 /* BetaFeaturesViewController.swift in Sources */, D8610BCC256F284700A5DF27 /* ULErrorViewModel.swift in Sources */, 4590CEE4249BA46700949F05 /* AddProductCategoryViewController.swift in Sources */, @@ -7933,7 +7936,6 @@ 450C2CB324D0803000D570DD /* ProductSettingsRowsTests.swift in Sources */, 45AF9DAF265CFAB4001EB794 /* MockShippingLabelCarrierRate.swift in Sources */, CC593A6726EA116300EF0E04 /* ShippingLabelAddNewPackageViewModelTests.swift in Sources */, - DE7842F926F435070030C792 /* DefaultConnectivityObserver.swift in Sources */, 455A2FDB246B1349000CA72C /* ProductVisibilityTests.swift in Sources */, 0215C6FC2518A3CD005240CD /* ProductFormViewModel+SaveTests.swift in Sources */, 265284092624ACE900F91BA1 /* AddOnCrossreferenceTests.swift in Sources */, From 0033684a739d197a37c30989d269cc49f85d4521 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Mon, 20 Sep 2021 14:30:47 +0700 Subject: [PATCH 28/39] Use new view for offline banner --- .../System/WooNavigationController.swift | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index ce203a00852..f7920cd2134 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -44,36 +44,6 @@ private class WooNavigationControllerDelegate: NSObject, UINavigationControllerD self.connectivityObserver = connectivityObserver } - /// Content of offline banner - /// - lazy var toolbarItemsForOfflineBanner: [UIBarButtonItem] = { - let stackView = UIStackView() - stackView.axis = .horizontal - stackView.spacing = 3 - stackView.distribution = .fillProportionally - stackView.alignment = .center - - let imageView = UIImageView(image: .lightningImage) - imageView.tintColor = .white - imageView.contentMode = .scaleAspectFit - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 24), - imageView.heightAnchor.constraint(equalToConstant: 24) - ]) - - let messageLabel = UILabel() - messageLabel.text = NSLocalizedString("Offline - using cached data", comment: "Message for offline banner") - messageLabel.applyCalloutStyle() - messageLabel.textColor = .white - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(messageLabel) - - let offlineItem = UIBarButtonItem(customView: stackView) - let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - return [spaceItem, offlineItem, spaceItem] - }() - /// Children delegate, all events will be forwarded to this object /// weak var forwardDelegate: UINavigationControllerDelegate? @@ -122,7 +92,12 @@ private extension WooNavigationControllerDelegate { /// and listen to connectivity status changes to change the toolbar's visibility. /// func configureOfflineBanner(for viewController: UIViewController, in navigationController: WooNavigationController) { - viewController.toolbarItems = toolbarItemsForOfflineBanner + let offlineBannerView = OfflineBannerView(frame: .zero) + offlineBannerView.sizeToFit() + let offlineItem = UIBarButtonItem(customView: offlineBannerView) + let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + + viewController.toolbarItems = [spaceItem, offlineItem, spaceItem] navigationController.toolbar.barTintColor = .gray let connected = connectivityObserver.isConnectivityAvailable From 7530df467209577d44de85f8e0266974ab42ec9e Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 10:41:47 +0700 Subject: [PATCH 29/39] Replace isConnectivityAvailable with currentStatus for ConnectivityObserver --- .../Classes/System/WooNavigationController.swift | 2 +- .../Tools/Connectivity/ConnectivityObserver.swift | 2 +- .../Connectivity/DefaultConnectivityObserver.swift | 14 ++++---------- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index f7920cd2134..17245d34d37 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -100,7 +100,7 @@ private extension WooNavigationControllerDelegate { viewController.toolbarItems = [spaceItem, offlineItem, spaceItem] navigationController.toolbar.barTintColor = .gray - let connected = connectivityObserver.isConnectivityAvailable + let connected = connectivityObserver.currentStatus != .notReachable navigationController.setToolbarHidden(connected, animated: false) } } diff --git a/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift index 09d95a13d99..21d4c13733b 100644 --- a/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift @@ -4,7 +4,7 @@ import Combine /// protocol ConnectivityObserver { /// Getter for current state of the connectivity. - var isConnectivityAvailable: Bool { get } + var currentStatus: ConnectivityStatus { get } /// Publisher for connectivity availability. var statusPublisher: AnyPublisher { get } diff --git a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift index fe75e902fea..9fc503b919e 100644 --- a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift @@ -8,11 +8,10 @@ final class DefaultConnectivityObserver: ConnectivityObserver { private let networkMonitor: NetworkMonitoring private let observingQueue: DispatchQueue = .global(qos: .background) - @Published private(set) var isConnectivityAvailable: Bool = false - @Published private var connectivityStatus: ConnectivityStatus = .unknown + @Published private(set) var currentStatus: ConnectivityStatus = .unknown var statusPublisher: AnyPublisher { - $connectivityStatus.eraseToAnyPublisher() + $currentStatus.eraseToAnyPublisher() } init(networkMonitor: NetworkMonitoring = NWPathMonitor()) { @@ -20,10 +19,8 @@ final class DefaultConnectivityObserver: ConnectivityObserver { startObserving() networkMonitor.networkUpdateHandler = { [weak self] path in guard let self = self else { return } - let connectivityStatus = self.connectivityStatus(from: path) DispatchQueue.main.async { - self.isConnectivityAvailable = path.status == .satisfied - self.connectivityStatus = connectivityStatus + self.currentStatus = self.connectivityStatus(from: path) } } } @@ -35,11 +32,8 @@ final class DefaultConnectivityObserver: ConnectivityObserver { func updateListener(_ listener: @escaping (ConnectivityStatus) -> Void) { networkMonitor.networkUpdateHandler = { [weak self] path in guard let self = self else { return } - let connectivityStatus = self.connectivityStatus(from: path) DispatchQueue.main.async { - self.isConnectivityAvailable = path.status == .satisfied - self.connectivityStatus = connectivityStatus - listener(connectivityStatus) + self.currentStatus = self.connectivityStatus(from: path) } } } From 06994f45b2c530ba1eaf0ed0eaf8b616a579cc6f Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 10:45:52 +0700 Subject: [PATCH 30/39] Remove offline banner configuration in view controllers --- .../Extensions/UIViewController+Connectivity.swift | 14 -------------- .../Dashboard/DashboardViewController.swift | 7 ------- .../Order Details/OrderDetailsViewController.swift | 7 ------- .../Review Order/ReviewOrderViewController.swift | 7 ------- .../ShippingLabelFormViewController.swift | 8 -------- .../Orders/OrdersRootViewController.swift | 8 -------- .../Edit Product/ProductFormViewController.swift | 8 -------- .../Products/ProductsViewController.swift | 8 -------- .../Reviews/ReviewDetailsViewController.swift | 8 -------- .../Reviews/ReviewsViewController.swift | 8 -------- 10 files changed, 83 deletions(-) diff --git a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift index 15b5947283f..f9eb5c1c26d 100644 --- a/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift +++ b/WooCommerce/Classes/Extensions/UIViewController+Connectivity.swift @@ -9,18 +9,4 @@ extension UIViewController { @objc var shouldShowOfflineBanner: Bool { false } - - /// Observes changes in status of connectivity and returns a subscription. - /// Keep a strong reference to this subscription to show the offline banner in the navigation controller's built-in toolbar. - /// This requires the view controller to be contained inside a `WooNavigationController`. - /// - func observeConnectivity() -> AnyCancellable { - ServiceLocator.connectivityObserver.statusPublisher - .sink { [weak self] status in - guard let self = self else { return } - guard let navigationController = self.navigationController as? WooNavigationController, - self.isViewOnScreen() else { return } - navigationController.isToolbarHidden = status != .notReachable - } - } } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift index 16acc959bdf..5091e040d80 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/DashboardViewController.swift @@ -1,4 +1,3 @@ -import Combine import UIKit import Gridicons import WordPressUI @@ -16,7 +15,6 @@ final class DashboardViewController: UIViewController { private let dashboardUIFactory: DashboardUIFactory private var dashboardUI: DashboardUI? - private var connectivitySubscription: AnyCancellable? // Used to enable subtitle with store name private var shouldShowStoreNameAsSubtitle: Bool { @@ -108,7 +106,6 @@ final class DashboardViewController: UIViewController { configureNavigation() configureView() configureDashboardUIContainer() - configureOfflineBanner() } override func viewWillAppear(_ animated: Bool) { @@ -132,10 +129,6 @@ final class DashboardViewController: UIViewController { // private extension DashboardViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - func configureView() { view.backgroundColor = .listBackground } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift index 260f382b448..61151d5514e 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift @@ -91,8 +91,6 @@ final class OrderDetailsViewController: UIViewController { return storyboard.instantiateViewController(withIdentifier: identifier) as? Self } - private var connectivitySubscription: AnyCancellable? - override func viewDidLoad() { super.viewDidLoad() configureNavigation() @@ -102,7 +100,6 @@ final class OrderDetailsViewController: UIViewController { registerTableViewHeaderFooters() configureEntityListener() configureViewModel() - configureOfflineBanner() updateTopBannerView() // FIXME: this is a hack. https://github.com/woocommerce/woocommerce-ios/issues/1779 @@ -136,10 +133,6 @@ final class OrderDetailsViewController: UIViewController { // private extension OrderDetailsViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - /// Setup: TopLoaderView func configureTopLoaderView() { stackView.insertArrangedSubview(topLoaderView, at: 0) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift index ce09d1b0ad9..3b79cf8d939 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Review Order/ReviewOrderViewController.swift @@ -1,4 +1,3 @@ -import Combine import SafariServices import UIKit import Yosemite @@ -36,8 +35,6 @@ final class ReviewOrderViewController: UIViewController { /// private lazy var footerView: UIView = configureTableFooterView() - private var connectivitySubscription: AnyCancellable? - init(viewModel: ReviewOrderViewModel, markOrderCompleteHandler: @escaping () -> Void) { self.viewModel = viewModel self.markOrderCompleteHandler = markOrderCompleteHandler @@ -54,7 +51,6 @@ final class ReviewOrderViewController: UIViewController { configureNavigation() configureTableView() configureViewModel() - configureOfflineBanner() } override func viewWillAppear(_ animated: Bool) { @@ -77,9 +73,6 @@ final class ReviewOrderViewController: UIViewController { // MARK: - UI Configuration // private extension ReviewOrderViewController { - private func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } func configureViewModel() { viewModel.configureResultsControllers { [weak self] in diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift index 17e303f68ea..e8818df9ffc 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift @@ -1,4 +1,3 @@ -import Combine import UIKit import Yosemite import SwiftUI @@ -22,8 +21,6 @@ final class ShippingLabelFormViewController: UIViewController { /// var onLabelSave: (() -> Void)? - private var connectivitySubscription: AnyCancellable? - /// Init /// init(order: Order) { @@ -46,7 +43,6 @@ final class ShippingLabelFormViewController: UIViewController { registerTableViewCells() registerTableViewHeaderFooters() observeViewModel() - configureOfflineBanner() } override var shouldShowOfflineBanner: Bool { @@ -58,10 +54,6 @@ final class ShippingLabelFormViewController: UIViewController { // private extension ShippingLabelFormViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - func configureNavigationBar() { title = Localization.titleView } diff --git a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift index f97362d9c14..fa65024b8cf 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift @@ -1,4 +1,3 @@ -import Combine import UIKit import Yosemite @@ -31,8 +30,6 @@ final class OrdersRootViewController: UIViewController { private let siteID: Int64 - private var connectivitySubscription: AnyCancellable? - // MARK: View Lifecycle init(siteID: Int64) { @@ -54,7 +51,6 @@ final class OrdersRootViewController: UIViewController { configureView() configureContainerView() configureChildViewController() - configureOfflineBanner() } override func viewDidLayoutSubviews() { @@ -78,10 +74,6 @@ final class OrdersRootViewController: UIViewController { // private extension OrdersRootViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - func configureView() { view.backgroundColor = .listBackground } diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index c6be943199e..76564bd9af4 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -1,4 +1,3 @@ -import Combine import Photos import UIKit import WordPressUI @@ -55,9 +54,6 @@ final class ProductFormViewController: private var cancellableUpdateEnabled: ObservationToken? private var cancellableNewVariationsPrice: ObservationToken? - /// Strong reference to Combine subscription - private var connectivitySubscription: AnyCancellable? - init(viewModel: ViewModel, eventLogger: ProductFormEventLoggerProtocol, productImageActionHandler: ProductImageActionHandler, @@ -106,7 +102,6 @@ final class ProductFormViewController: configureMainView() configureTableView() configureMoreDetailsContainerView() - configureOfflineBanner() startListeningToNotifications() handleSwipeBackGesture() @@ -412,9 +407,6 @@ final class ProductFormViewController: // MARK: - Configuration // private extension ProductFormViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } func configureNavigationBar() { updateNavigationBar() diff --git a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift index 387a0ca3c32..29b06c8e34a 100644 --- a/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/ProductsViewController.swift @@ -1,4 +1,3 @@ -import Combine import UIKit import WordPressUI import Yosemite @@ -140,8 +139,6 @@ final class ProductsViewController: UIViewController { /// private var hasErrorLoadingData: Bool = false - private var connectivitySubscription: AnyCancellable? - deinit { NotificationCenter.default.removeObserver(self) } @@ -167,7 +164,6 @@ final class ProductsViewController: UIViewController { configureTableView() configureToolBarView() configureSyncingCoordinator() - configureOfflineBanner() registerTableViewCells() showTopBannerViewIfNeeded() @@ -261,10 +257,6 @@ private extension ProductsViewController { // private extension ProductsViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - /// Set the title. /// func configureNavigationBar() { diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift index f7da76eb3d8..818754f3d67 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewDetailsViewController.swift @@ -1,4 +1,3 @@ -import Combine import UIKit import Yosemite import Gridicons @@ -45,8 +44,6 @@ final class ReviewDetailsViewController: UIViewController { /// private var rows = [Row]() - private var connectivitySubscription: AnyCancellable? - /// Designated Initializer /// init(productReview: ProductReview, product: Product?, notification: Note?) { @@ -70,7 +67,6 @@ final class ReviewDetailsViewController: UIViewController { configureTableView() configureEntityListener() configureAppRatingEvent() - configureOfflineBanner() registerTableViewCells() reloadInterface() @@ -91,10 +87,6 @@ final class ReviewDetailsViewController: UIViewController { // private extension ReviewDetailsViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - /// Setup: Main View /// func configureMainView() { diff --git a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift index e3f0cdd915b..575f1f4ad34 100644 --- a/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Reviews/ReviewsViewController.swift @@ -1,4 +1,3 @@ -import Combine import UIKit import SafariServices.SFSafariViewController @@ -110,8 +109,6 @@ final class ReviewsViewController: UIViewController { }) }() - var connectivitySubscription: AnyCancellable? - // MARK: - View Lifecycle init(siteID: Int64) { @@ -139,7 +136,6 @@ final class ReviewsViewController: UIViewController { configureTableView() configureTableViewCells() configureResultsController() - configureOfflineBanner() startListeningToNotifications() syncingCoordinator.resynchronize() @@ -178,10 +174,6 @@ final class ReviewsViewController: UIViewController { // private extension ReviewsViewController { - func configureOfflineBanner() { - connectivitySubscription = observeConnectivity() - } - /// Setup: Sync'ing Coordinator /// func configureSyncingCoordinator() { From c8ca97e483bc98c6b668f926deede67235b3eeaa Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 11:08:11 +0700 Subject: [PATCH 31/39] Handle offline banner configuration on WooNavigationControllerDelegate --- .../System/WooNavigationController.swift | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index 17245d34d37..c387db28465 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit /// Subclass to set Woo styling. Removes back button text on managed view controllers. @@ -39,9 +40,13 @@ class WooNavigationController: UINavigationController { private class WooNavigationControllerDelegate: NSObject, UINavigationControllerDelegate { private let connectivityObserver: ConnectivityObserver + private var currentController: UIViewController? + private var subscriptions: Set = [] init(connectivityObserver: ConnectivityObserver = ServiceLocator.connectivityObserver) { self.connectivityObserver = connectivityObserver + super.init() + observeConnectivity() } /// Children delegate, all events will be forwarded to this object @@ -51,12 +56,8 @@ private class WooNavigationControllerDelegate: NSObject, UINavigationControllerD /// Configures the back button for the managed `ViewController` and forwards the event to the children delegate. /// func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { - if let wooNavigationController = navigationController as? WooNavigationController, - viewController.shouldShowOfflineBanner { - configureOfflineBanner(for: viewController, in: wooNavigationController) - } else { - navigationController.isToolbarHidden = true - } + currentController = viewController + configureOfflineBanner(for: viewController) configureBackButton(for: viewController) forwardDelegate?.navigationController?(navigationController, willShow: viewController, animated: animated) } @@ -87,11 +88,47 @@ private extension WooNavigationControllerDelegate { func configureBackButton(for viewController: UIViewController) { viewController.removeNavigationBackBarButtonText() } +} + +// MARK: - Offline banner configuration +private extension WooNavigationControllerDelegate { + + /// Observes changes in status of connectivity and returns a subscription. + /// Keep a strong reference to this subscription to show the offline banner in the navigation controller's built-in toolbar. + /// + func observeConnectivity() { + connectivityObserver.statusPublisher + .sink { [weak self] status in + guard let self = self, let currentController = self.currentController else { return } + self.configureOfflineBanner(for: currentController, status: status) + } + .store(in: &subscriptions) + } + + /// Shows or hides offline banner based on the input connectivity status and + /// whether the view controller supports showing the banner. + /// + func configureOfflineBanner(for viewController: UIViewController, status: ConnectivityStatus? = nil) { + if viewController.shouldShowOfflineBanner { + setOfflineBannerWhenNoConnection(for: viewController, status: status ?? connectivityObserver.currentStatus) + } else { + removeOfflineBanner(for: viewController) + } + } - /// Set up toolbar for the view controller to display the offline message, - /// and listen to connectivity status changes to change the toolbar's visibility. + /// Displays offline banner in the default tool bar of the view controller's navigation controller. /// - func configureOfflineBanner(for viewController: UIViewController, in navigationController: WooNavigationController) { + func setOfflineBannerWhenNoConnection(for viewController: UIViewController, status: ConnectivityStatus) { + + guard let navigationController = viewController.navigationController else { + return + } + + // We can only show it when we are sure we can't reach the internet + guard status == .notReachable else { + return removeOfflineBanner(for: viewController) + } + let offlineBannerView = OfflineBannerView(frame: .zero) offlineBannerView.sizeToFit() let offlineItem = UIBarButtonItem(customView: offlineBannerView) @@ -100,7 +137,15 @@ private extension WooNavigationControllerDelegate { viewController.toolbarItems = [spaceItem, offlineItem, spaceItem] navigationController.toolbar.barTintColor = .gray - let connected = connectivityObserver.currentStatus != .notReachable - navigationController.setToolbarHidden(connected, animated: false) + navigationController.setToolbarHidden(false, animated: false) + } + + /// Hides the default tool bar in the view controller's navigation controller. + /// + func removeOfflineBanner(for viewController: UIViewController) { + guard let navigationController = viewController.navigationController else { + return + } + navigationController.setToolbarHidden(true, animated: false) } } From 407f229da377845923db7a085422c5f4e3846086 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 11:48:52 +0700 Subject: [PATCH 32/39] Update offline banner configuration by adding subview instead of navigation controller's toolbar --- .../System/WooNavigationController.swift | 46 +++++++++++-------- .../ReusableViews/OfflineBannerView.swift | 6 ++- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index c387db28465..2a26799164f 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -90,11 +90,10 @@ private extension WooNavigationControllerDelegate { } } -// MARK: - Offline banner configuration +// MARK: Offline banner configuration private extension WooNavigationControllerDelegate { - /// Observes changes in status of connectivity and returns a subscription. - /// Keep a strong reference to this subscription to show the offline banner in the navigation controller's built-in toolbar. + /// Observes changes in status of connectivity and updates the offline banner in current view controller accordingly. /// func observeConnectivity() { connectivityObserver.statusPublisher @@ -116,36 +115,43 @@ private extension WooNavigationControllerDelegate { } } - /// Displays offline banner in the default tool bar of the view controller's navigation controller. + /// Adds offline banner at the bottom of the view controller. /// func setOfflineBannerWhenNoConnection(for viewController: UIViewController, status: ConnectivityStatus) { - - guard let navigationController = viewController.navigationController else { - return - } - // We can only show it when we are sure we can't reach the internet guard status == .notReachable else { return removeOfflineBanner(for: viewController) } - let offlineBannerView = OfflineBannerView(frame: .zero) - offlineBannerView.sizeToFit() - let offlineItem = UIBarButtonItem(customView: offlineBannerView) - let spaceItem = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - - viewController.toolbarItems = [spaceItem, offlineItem, spaceItem] - navigationController.toolbar.barTintColor = .gray + // Only add banner view if it's not already added. + guard let navigationController = viewController.navigationController, + let view = viewController.view, + view.subviews.first(where: { $0 is OfflineBannerView }) == nil else { + return + } - navigationController.setToolbarHidden(false, animated: false) + let offlineBannerView = OfflineBannerView(frame: .zero) + offlineBannerView.backgroundColor = .gray + offlineBannerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(offlineBannerView) + + let extraBottomSpace = viewController.hidesBottomBarWhenPushed ? navigationController.view.safeAreaInsets.bottom : 0 + NSLayoutConstraint.activate([ + offlineBannerView.heightAnchor.constraint(equalToConstant: OfflineBannerView.height), + offlineBannerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + offlineBannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + offlineBannerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -extraBottomSpace) + ]) + viewController.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: OfflineBannerView.height, right: 0) } - /// Hides the default tool bar in the view controller's navigation controller. + /// Removes the offline banner from the view controller if it exists. /// func removeOfflineBanner(for viewController: UIViewController) { - guard let navigationController = viewController.navigationController else { + guard let offlineBanner = viewController.view.subviews.first(where: { $0 is OfflineBannerView }) else { return } - navigationController.setToolbarHidden(true, animated: false) + offlineBanner.removeFromSuperview() + viewController.additionalSafeAreaInsets = .zero } } diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift index b2a143c71df..0d66af231ec 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/OfflineBannerView.swift @@ -4,6 +4,8 @@ import UIKit /// final class OfflineBannerView: UIView { + static let height: CGFloat = 44 + override init(frame: CGRect) { super.init(frame: frame) setupView() @@ -41,8 +43,8 @@ final class OfflineBannerView: UIView { addSubview(stackView) NSLayoutConstraint.activate([ - stackView.safeLeadingAnchor.constraint(equalTo: safeLeadingAnchor, constant: 0), - stackView.safeBottomAnchor.constraint(equalTo: safeBottomAnchor, constant: 0), + stackView.safeLeadingAnchor.constraint(greaterThanOrEqualTo: safeLeadingAnchor, constant: 0), + stackView.safeBottomAnchor.constraint(greaterThanOrEqualTo: safeBottomAnchor, constant: 0), stackView.centerXAnchor.constraint(equalTo: centerXAnchor), stackView.centerYAnchor.constraint(equalTo: centerYAnchor) ]) From ebc44efdc2b89c522f5c410d2231e5c2b0d0b204 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 13:13:31 +0700 Subject: [PATCH 33/39] Update unit tests for DeafultConnectivityObserver --- .../WooCommerce.xcodeproj/project.pbxproj | 4 ++++ .../Tools/DefaultConnectivityObserver.swift | 21 +++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 5e8ac47126c..2b89102cf79 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1298,6 +1298,7 @@ DE67D46726B98FD000EFE8DB /* Publisher+WithLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */; }; DE67D46926BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */; }; DE68B81F26F86B1700C86CFB /* OfflineBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE68B81E26F86B1700C86CFB /* OfflineBannerView.swift */; }; + DE68B84326FAF17A00C86CFB /* DefaultConnectivityObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE68B84226FAF17A00C86CFB /* DefaultConnectivityObserver.swift */; }; DE7842ED26F061650030C792 /* NumberFormatter+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */; }; DE7842EF26F079A60030C792 /* NumberFormatter+LocalizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */; }; DE7842F726F2E9340030C792 /* UIViewController+Connectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */; }; @@ -2739,6 +2740,7 @@ DE67D46626B98FD000EFE8DB /* Publisher+WithLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFrom.swift"; sourceTree = ""; }; DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+WithLatestFromTests.swift"; sourceTree = ""; }; DE68B81E26F86B1700C86CFB /* OfflineBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineBannerView.swift; sourceTree = ""; }; + DE68B84226FAF17A00C86CFB /* DefaultConnectivityObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultConnectivityObserver.swift; sourceTree = ""; }; DE7842EC26F061650030C792 /* NumberFormatter+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Localized.swift"; sourceTree = ""; }; DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+LocalizedTests.swift"; sourceTree = ""; }; DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Connectivity.swift"; sourceTree = ""; }; @@ -4947,6 +4949,7 @@ CC0324A2263AD9F40056C6B7 /* MockShippingLabelAccountSettings.swift */, 0277AEAA256CAA5300F45C4A /* MockShippingLabelAddress.swift */, 0211252D25773FB00075AD2A /* MockAggregateOrderItem.swift */, + DE68B84226FAF17A00C86CFB /* DefaultConnectivityObserver.swift */, 4590B651261C8D1E00A6FCE0 /* WeightFormatterTests.swift */, ); path = Tools; @@ -7936,6 +7939,7 @@ 450C2CB324D0803000D570DD /* ProductSettingsRowsTests.swift in Sources */, 45AF9DAF265CFAB4001EB794 /* MockShippingLabelCarrierRate.swift in Sources */, CC593A6726EA116300EF0E04 /* ShippingLabelAddNewPackageViewModelTests.swift in Sources */, + DE68B84326FAF17A00C86CFB /* DefaultConnectivityObserver.swift in Sources */, 455A2FDB246B1349000CA72C /* ProductVisibilityTests.swift in Sources */, 0215C6FC2518A3CD005240CD /* ProductFormViewModel+SaveTests.swift in Sources */, 265284092624ACE900F91BA1 /* AddOnCrossreferenceTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift index 1e9108ccee2..c217fa2f380 100644 --- a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift +++ b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift @@ -28,7 +28,7 @@ final class DefaultConnectivityObserverTests: XCTestCase { XCTAssertTrue(networkMonitor.didStopMonitoring) } - func test_isConnectivityAvailable_returns_true_when_network_is_satisfied() { + func test_currentStatus_returns_correctly_when_network_is_satisfied() { // Given let network = MockNetwork(status: .satisfied, currentInterface: .wifi) let networkMonitor = MockNetworkMonitor(currentNetwork: network) @@ -37,10 +37,12 @@ final class DefaultConnectivityObserverTests: XCTestCase { let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) // Then - XCTAssertTrue(observer.isConnectivityAvailable) + DispatchQueue.main.async { + XCTAssertEqual(observer.currentStatus, .reachable(type: .ethernetOrWiFi)) + } } - func test_isConnectivityAvailable_returns_false_when_network_is_unsatisfied() { + func test_currentStatus_returns_correctly_when_network_is_unsatisfied() { // Given let network = MockNetwork(status: .unsatisfied, currentInterface: .wifi) let networkMonitor = MockNetworkMonitor(currentNetwork: network) @@ -49,7 +51,9 @@ final class DefaultConnectivityObserverTests: XCTestCase { let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) // Then - XCTAssertFalse(observer.isConnectivityAvailable) + DispatchQueue.main.async { + XCTAssertEqual(observer.currentStatus, .notReachable) + } } func test_updateListener_returns_correct_status_in_callback_closure() { @@ -57,23 +61,18 @@ final class DefaultConnectivityObserverTests: XCTestCase { let network = MockNetwork(status: .satisfied, currentInterface: .wifi) let networkMonitor = MockNetworkMonitor(currentNetwork: network) let networkUpdate = MockNetwork(status: .satisfied, currentInterface: .cellular) - let statusExpectation = expectation(description: "Status in callback closure") // When let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) var result: ConnectivityStatus = .unknown observer.updateListener { status in result = status - statusExpectation.fulfill() } networkMonitor.fakeNetworkUpdate(network: networkUpdate) // Then - waitForExpectations(timeout: 0.3, handler: nil) - if case .reachable(let type) = result { - XCTAssertEqual(type, .cellular) - } else { - XCTFail("Incorrect result status in callback closure") + DispatchQueue.main.async { + XCTAssertEqual(result, .reachable(type: .cellular)) } } } From e7395972dfac1edb817a9f8e8e766e66e7245b90 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 13:16:36 +0700 Subject: [PATCH 34/39] Update release notes --- RELEASE-NOTES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 191b5d0a7d6..fc1ca60baf2 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,10 @@ *** PLEASE FOLLOW THIS FORMAT: [] [] +7.7 +----- +- [*] Show banner on screens that use cached data when device is offline. [https://github.com/woocommerce/woocommerce-ios/pull/5000] + + 7.6 ----- - [x] Show an improved error modal if there are problems while selecting a store. [https://github.com/woocommerce/woocommerce-ios/pull/5006] From cc626f8f85cbcac099403104722ae7de60cc494f Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 13:25:17 +0700 Subject: [PATCH 35/39] Add missing listener in updateListener method of DefaultConnectivityObserver --- .../Tools/Connectivity/DefaultConnectivityObserver.swift | 1 + .../Tools/DefaultConnectivityObserver.swift | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift index 9fc503b919e..21e31662546 100644 --- a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift @@ -34,6 +34,7 @@ final class DefaultConnectivityObserver: ConnectivityObserver { guard let self = self else { return } DispatchQueue.main.async { self.currentStatus = self.connectivityStatus(from: path) + listener(self.currentStatus) } } } diff --git a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift index c217fa2f380..d1792fa4e8c 100644 --- a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift +++ b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift @@ -61,19 +61,20 @@ final class DefaultConnectivityObserverTests: XCTestCase { let network = MockNetwork(status: .satisfied, currentInterface: .wifi) let networkMonitor = MockNetworkMonitor(currentNetwork: network) let networkUpdate = MockNetwork(status: .satisfied, currentInterface: .cellular) + let statusExpectation = expectation(description: "Status in callback closure") // When let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) var result: ConnectivityStatus = .unknown observer.updateListener { status in result = status + statusExpectation.fulfill() } networkMonitor.fakeNetworkUpdate(network: networkUpdate) // Then - DispatchQueue.main.async { - XCTAssertEqual(result, .reachable(type: .cellular)) - } + waitForExpectations(timeout: 0.3, handler: nil) + XCTAssertEqual(result, .reachable(type: .cellular)) } } From 67aa0de162b392fd5c3e18493266f3c05288c475 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 15:15:54 +0700 Subject: [PATCH 36/39] Update additionalSafeAreaInsets with extraBottomSpace when offline banner is visible --- WooCommerce/Classes/System/WooNavigationController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index 2a26799164f..cc9c7c0b787 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -142,7 +142,7 @@ private extension WooNavigationControllerDelegate { offlineBannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), offlineBannerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -extraBottomSpace) ]) - viewController.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: OfflineBannerView.height, right: 0) + viewController.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: OfflineBannerView.height + extraBottomSpace, right: 0) } /// Removes the offline banner from the view controller if it exists. From 4fabfb7372eb858db18831e6b00924d3e2470043 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 22 Sep 2021 15:42:55 +0700 Subject: [PATCH 37/39] Revert "Update additionalSafeAreaInsets with extraBottomSpace when offline banner is visible" This reverts commit 67aa0de162b392fd5c3e18493266f3c05288c475. --- WooCommerce/Classes/System/WooNavigationController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/System/WooNavigationController.swift b/WooCommerce/Classes/System/WooNavigationController.swift index cc9c7c0b787..2a26799164f 100644 --- a/WooCommerce/Classes/System/WooNavigationController.swift +++ b/WooCommerce/Classes/System/WooNavigationController.swift @@ -142,7 +142,7 @@ private extension WooNavigationControllerDelegate { offlineBannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), offlineBannerView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -extraBottomSpace) ]) - viewController.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: OfflineBannerView.height + extraBottomSpace, right: 0) + viewController.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: OfflineBannerView.height, right: 0) } /// Removes the offline banner from the view controller if it exists. From 2408211b1f7a3c6da4fd2558be46113ed7ffe85b Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 23 Sep 2021 10:55:05 +0700 Subject: [PATCH 38/39] Remove updateListener method from ConnectivityObserver --- .../Connectivity/ConnectivityObserver.swift | 3 --- .../DefaultConnectivityObserver.swift | 16 ---------------- 2 files changed, 19 deletions(-) diff --git a/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift index 21d4c13733b..0f9cc08d555 100644 --- a/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/ConnectivityObserver.swift @@ -12,9 +12,6 @@ protocol ConnectivityObserver { /// Starts the observer. func startObserving() - /// Updates the listener for the connectivity observer. - func updateListener(_ listener: @escaping (ConnectivityStatus) -> Void) - /// Stops the observer. func stopObserving() } diff --git a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift index 21e31662546..757998e2db4 100644 --- a/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift +++ b/WooCommerce/Classes/Tools/Connectivity/DefaultConnectivityObserver.swift @@ -29,16 +29,6 @@ final class DefaultConnectivityObserver: ConnectivityObserver { networkMonitor.start(queue: observingQueue) } - func updateListener(_ listener: @escaping (ConnectivityStatus) -> Void) { - networkMonitor.networkUpdateHandler = { [weak self] path in - guard let self = self else { return } - DispatchQueue.main.async { - self.currentStatus = self.connectivityStatus(from: path) - listener(self.currentStatus) - } - } - } - func stopObserving() { networkMonitor.cancel() } @@ -70,8 +60,6 @@ final class DefaultConnectivityObserver: ConnectivityObserver { /// Proxy protocol for mocking `NWPathMonitor`. protocol NetworkMonitoring: AnyObject { - var currentNetwork: NetworkMonitorable { get } - /// A handler that receives network updates. var networkUpdateHandler: ((NetworkMonitorable) -> Void)? { get set } @@ -93,10 +81,6 @@ protocol NetworkMonitorable { extension NWPath: NetworkMonitorable {} extension NWPathMonitor: NetworkMonitoring { - var currentNetwork: NetworkMonitorable { - currentPath - } - var networkUpdateHandler: ((NetworkMonitorable) -> Void)? { get { let closure: ((NetworkMonitorable) -> Void)? = { From 53456cee1ce5caa5a5cfccae3d02aa8fc950d59e Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 23 Sep 2021 10:55:18 +0700 Subject: [PATCH 39/39] Update unit tests for DefaultConnectivityObserver --- .../Tools/DefaultConnectivityObserver.swift | 78 +++++++++---------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift index d1792fa4e8c..8af3f64fbaa 100644 --- a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift +++ b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift @@ -1,12 +1,14 @@ +import Combine import Network import XCTest @testable import WooCommerce final class DefaultConnectivityObserverTests: XCTestCase { + private var subscriptions: Set = [] + func test_initializing_observer_triggers_network_monitoring() { // Given - let network = MockNetwork(status: .satisfied, currentInterface: .wifi) - let networkMonitor = MockNetworkMonitor(currentNetwork: network) + let networkMonitor = MockNetworkMonitor() // When let _ = DefaultConnectivityObserver(networkMonitor: networkMonitor) @@ -17,8 +19,7 @@ final class DefaultConnectivityObserverTests: XCTestCase { func test_stopping_observer_stops_network_monitoring() { // Given - let network = MockNetwork(status: .satisfied, currentInterface: .wifi) - let networkMonitor = MockNetworkMonitor(currentNetwork: network) + let networkMonitor = MockNetworkMonitor() // When let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) @@ -28,67 +29,60 @@ final class DefaultConnectivityObserverTests: XCTestCase { XCTAssertTrue(networkMonitor.didStopMonitoring) } - func test_currentStatus_returns_correctly_when_network_is_satisfied() { - // Given - let network = MockNetwork(status: .satisfied, currentInterface: .wifi) - let networkMonitor = MockNetworkMonitor(currentNetwork: network) - - // When - let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) - - // Then - DispatchQueue.main.async { - XCTAssertEqual(observer.currentStatus, .reachable(type: .ethernetOrWiFi)) - } - } - - func test_currentStatus_returns_correctly_when_network_is_unsatisfied() { + func test_currentStatus_and_statusPublisher_return_correctly_when_network_is_satisfied() { // Given - let network = MockNetwork(status: .unsatisfied, currentInterface: .wifi) - let networkMonitor = MockNetworkMonitor(currentNetwork: network) + let networkMonitor = MockNetworkMonitor() + let expectation = expectation(description: "Current status and status publisher values") // When + var result: ConnectivityStatus = .unknown let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) + observer.statusPublisher + .dropFirst() + .sink { status in + result = status + expectation.fulfill() + } + .store(in: &subscriptions) + networkMonitor.fakeNetworkUpdate(network: MockNetwork(status: .satisfied, currentInterface: .wifi)) // Then - DispatchQueue.main.async { - XCTAssertEqual(observer.currentStatus, .notReachable) - } + wait(for: [expectation], timeout: 1) + XCTAssertEqual(observer.currentStatus, .reachable(type: .ethernetOrWiFi)) + XCTAssertEqual(result, .reachable(type: .ethernetOrWiFi)) } - func test_updateListener_returns_correct_status_in_callback_closure() { + func test_currentStatus_and_statusPublisher_return_correctly_when_network_is_unsatisfied() { // Given - let network = MockNetwork(status: .satisfied, currentInterface: .wifi) - let networkMonitor = MockNetworkMonitor(currentNetwork: network) - let networkUpdate = MockNetwork(status: .satisfied, currentInterface: .cellular) - let statusExpectation = expectation(description: "Status in callback closure") + let networkMonitor = MockNetworkMonitor() + let expectation = expectation(description: "Current status and status publisher values") // When - let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) var result: ConnectivityStatus = .unknown - observer.updateListener { status in - result = status - statusExpectation.fulfill() - } - networkMonitor.fakeNetworkUpdate(network: networkUpdate) + let observer = DefaultConnectivityObserver(networkMonitor: networkMonitor) + observer.statusPublisher + .dropFirst() + .sink { status in + result = status + expectation.fulfill() + } + .store(in: &subscriptions) + networkMonitor.fakeNetworkUpdate(network: MockNetwork(status: .unsatisfied, currentInterface: .wifi)) // Then - waitForExpectations(timeout: 0.3, handler: nil) - XCTAssertEqual(result, .reachable(type: .cellular)) + wait(for: [expectation], timeout: 1) + XCTAssertEqual(observer.currentStatus, .notReachable) + XCTAssertEqual(result, .notReachable) } } final class MockNetworkMonitor: NetworkMonitoring { - let currentNetwork: NetworkMonitorable - var networkUpdateHandler: ((NetworkMonitorable) -> Void)? private(set) var didStartMonitoring = false private(set) var didStopMonitoring = false - init(currentNetwork: NetworkMonitorable) { - self.currentNetwork = currentNetwork - } + init() {} func fakeNetworkUpdate(network: NetworkMonitorable) { networkUpdateHandler?(network)