From 7a2852109d4ec82dcaae4b064b2bcf22779f2a4c Mon Sep 17 00:00:00 2001
From: Jack Alto <384288+aokj4ck@users.noreply.github.com>
Date: Wed, 20 Mar 2024 14:28:15 -0400
Subject: [PATCH] Fix #184: Incorrect tap area for Right-to-Left languages
(#194)
### Description
Fixes GitHub issue #184
- CategoriesFavoritesSegmentControl was missing Right-to-Left language support
- Tested with Right-to-left pseudo language
- Add PreviewCategoriesFavoritesSegmentControl.swift to fix previews in Xcode 14
- Fix xib errors for CategoriesFavoritesSegmentControl
### Checklist
- [x] Update `CHANGELOG`
### Screenshots
##### Xcode Right-to-left pseudo language
Edit Scheme > App language > change to Right-to-left pseudolanguage
##### Updated behavior
| Left-to-right | Right-to-left |
| -- | -- |
| ![Simulator Screen Recording - iPhone 15 Pro - 2024-03-19 at 17 44 13](https://github.com/mapbox/mapbox-search-ios/assets/384288/b3254b52-8127-4e2e-92a5-a3b207d1f3f0) | ![Simulator Screen Recording - iPhone 15 Pro - 2024-03-19 at 17 44 50](https://github.com/mapbox/mapbox-search-ios/assets/384288/d0225339-5888-467a-9dcf-5401445ff240) |
###### SwiftUI Previews
###### Right to Left behavior
| With Results | Dark mode categories | Dark mode favorites |
| -- | -- | -- |
| ![Simulator Screenshot - iPhone 15 Pro - 2024-03-20 at 13 20 33](https://github.com/mapbox/mapbox-search-ios/assets/384288/9ecf021c-cab3-4847-93b9-b61adb7a9d8a) | ![Simulator Screenshot - iPhone 15 Pro - 2024-03-20 at 13 22 37](https://github.com/mapbox/mapbox-search-ios/assets/384288/0f976a7e-a9d3-46e5-b00d-95f8fac06aaa) | ![Simulator Screenshot - iPhone 15 Pro - 2024-03-20 at 13 22 39](https://github.com/mapbox/mapbox-search-ios/assets/384288/736355eb-2892-42a8-b3bc-1eaecf4f5ae4) |
---
CHANGELOG.md | 3 +
MapboxSearch.xcodeproj/project.pbxproj | 4 +
.../CategoriesFavoritesSegmentControl.swift | 33 +-------
...iewCategoriesFavoritesSegmentControl.swift | 45 +++++++++++
.../SearchCategoriesRootView.swift | 75 ++++++++++++++++---
.../SearchCategoriesRootView.xib | 17 +++--
6 files changed, 129 insertions(+), 48 deletions(-)
create mode 100644 Sources/MapboxSearchUI/PreviewCategoriesFavoritesSegmentControl.swift
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d4e6fc8e..62b065d6d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,9 @@ Guide: https://keepachangelog.com/en/1.0.0/
+- [UI] Add Right-to-Left language support for Categories/Favorites segment control and fix xib errors.
+- [UI] Add Preview file for CategoriesFavoritesSegmentControl to fix compiler problems.
+
- [Core] Add SearchError.owningObjectDeallocated when network responses fail to unwrap guard-let-self. If you encounter this error you must own the reference to the search engine.
- [Tests] Add UnownedObjectError tests to validate behavior of SearchError.owningObjectDeallocated.
diff --git a/MapboxSearch.xcodeproj/project.pbxproj b/MapboxSearch.xcodeproj/project.pbxproj
index 9a07dc491..e526c36f4 100644
--- a/MapboxSearch.xcodeproj/project.pbxproj
+++ b/MapboxSearch.xcodeproj/project.pbxproj
@@ -13,6 +13,7 @@
042477C62B72CCB000D870D5 /* geocoding-reverse-geocoding.json in Resources */ = {isa = PBXBuildFile; fileRef = 042477C42B72CCB000D870D5 /* geocoding-reverse-geocoding.json */; };
042477C72B72CCB000D870D5 /* geocoding-reverse-geocoding.json in Resources */ = {isa = PBXBuildFile; fileRef = 042477C42B72CCB000D870D5 /* geocoding-reverse-geocoding.json */; };
043A3D4D2B30F38300DB681B /* CoreAddress+AddressComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 043A3D4C2B30F38300DB681B /* CoreAddress+AddressComponents.swift */; };
+ 044A6B732BA8933200A9F2A2 /* PreviewCategoriesFavoritesSegmentControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044A6B722BA8933200A9F2A2 /* PreviewCategoriesFavoritesSegmentControl.swift */; };
045514C22B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045514C12B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift */; };
045514C32B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045514C12B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift */; };
045514C42B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045514C12B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift */; };
@@ -533,6 +534,7 @@
042477C12B7290E700D870D5 /* SearchEngineGeocodingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchEngineGeocodingIntegrationTests.swift; sourceTree = ""; };
042477C42B72CCB000D870D5 /* geocoding-reverse-geocoding.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "geocoding-reverse-geocoding.json"; sourceTree = ""; };
043A3D4C2B30F38300DB681B /* CoreAddress+AddressComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreAddress+AddressComponents.swift"; sourceTree = ""; };
+ 044A6B722BA8933200A9F2A2 /* PreviewCategoriesFavoritesSegmentControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewCategoriesFavoritesSegmentControl.swift; sourceTree = ""; };
045514C12B7D4B58000D88B9 /* CoreApiType+ToSDKType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreApiType+ToSDKType.swift"; sourceTree = ""; };
046818D32B87F2A70082B188 /* SearchBox_CategorySearchEngineIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBox_CategorySearchEngineIntegrationTests.swift; sourceTree = ""; };
046818DA2B87FAB20082B188 /* search-box-category.json */ = {isa = PBXFileReference; explicitFileType = text.json; path = "search-box-category.json"; sourceTree = ""; };
@@ -1835,6 +1837,7 @@
FEEDD3472508E02700DC0A98 /* AddToFavoritesCell.xib */,
FEEDD3352508E02700DC0A98 /* Assets.xcassets */,
FEEDD3412508E02700DC0A98 /* CategoriesFavoritesSegmentControl.swift */,
+ 044A6B722BA8933200A9F2A2 /* PreviewCategoriesFavoritesSegmentControl.swift */,
FEEDD34C2508E02700DC0A98 /* CategoriesFavoritesSegmentControl.xib */,
FEEDD34B2508E02700DC0A98 /* CategoriesProvider.swift */,
FEEDD32E2508E02700DC0A98 /* CategoriesTableViewCell.swift */,
@@ -2893,6 +2896,7 @@
FE49CF6E2510EFD00059C189 /* DefaultCategoryDataProvider.swift in Sources */,
F9C98874270B42FA00D030B0 /* LocationCoordinateHelpers.swift in Sources */,
FEEDD3782508E02700DC0A98 /* FavoriteEntry.swift in Sources */,
+ 044A6B732BA8933200A9F2A2 /* PreviewCategoriesFavoritesSegmentControl.swift in Sources */,
FEEDD3562508E02700DC0A98 /* MapboxSearchController.swift in Sources */,
FEEDD3682508E02700DC0A98 /* HistoryHeader.swift in Sources */,
FEEDD37F2508E02700DC0A98 /* ActivityProgressView.swift in Sources */,
diff --git a/Sources/MapboxSearchUI/CategoriesFavoritesSegmentControl.swift b/Sources/MapboxSearchUI/CategoriesFavoritesSegmentControl.swift
index 23af5fff5..7b63f03fd 100644
--- a/Sources/MapboxSearchUI/CategoriesFavoritesSegmentControl.swift
+++ b/Sources/MapboxSearchUI/CategoriesFavoritesSegmentControl.swift
@@ -1,5 +1,6 @@
import UIKit
+// Preview available in PreviewCategoriesFavoritesSegmentControl.swift
class CategoriesFavoritesSegmentControl: UIControl {
enum Tab {
case categories
@@ -40,8 +41,6 @@ class CategoriesFavoritesSegmentControl: UIControl {
@IBOutlet private var favoritesInactiveTitle: UIButton!
@IBOutlet private var selectionSegment: UIView!
- @IBOutlet private var selectionCategoriesHorizontalConstraint: NSLayoutConstraint!
- @IBOutlet private var selectionFavoritesHorizontalConstraint: NSLayoutConstraint!
var configuration: Configuration! {
didSet {
@@ -163,33 +162,3 @@ class CategoriesFavoritesSegmentControl: UIControl {
favoritesInactiveTitleMask.path = selectionSegmentPath
}
}
-
-#if canImport(SwiftUI) && DEBUG
-import SwiftUI
-
-@available(iOS 13.0, *)
-struct TabsSegmentControlRepresentable: UIViewRepresentable {
- func makeUIView(context: Context) -> UIView {
- UINib(nibName: "CategoriesFavoritesSegmentControl", bundle: .mapboxSearchUI)
- .instantiate(withOwner: nil, options: nil)[0] as! UIView
- // swiftlint:disable:previous force_cast
- }
-
- func updateUIView(_ view: UIView, context: Context) {}
-}
-
-@available(iOS 13.0, *)
-struct CategoriesFavoritesSegmentControlPreview: PreviewProvider {
- static var previews: some View {
- Group {
- TabsSegmentControlRepresentable()
- .previewDisplayName("Light Mode")
- .previewLayout(PreviewLayout.fixed(width: 202, height: 28))
- TabsSegmentControlRepresentable()
- .previewDisplayName("Dark Mode")
- .preferredColorScheme(.dark)
- .previewLayout(PreviewLayout.fixed(width: 300, height: 40))
- }
- }
-}
-#endif
diff --git a/Sources/MapboxSearchUI/PreviewCategoriesFavoritesSegmentControl.swift b/Sources/MapboxSearchUI/PreviewCategoriesFavoritesSegmentControl.swift
new file mode 100644
index 000000000..a340ee2e2
--- /dev/null
+++ b/Sources/MapboxSearchUI/PreviewCategoriesFavoritesSegmentControl.swift
@@ -0,0 +1,45 @@
+// Copyright © 2024 Mapbox. All rights reserved.
+
+import Foundation
+
+#if canImport(SwiftUI) && DEBUG
+import SwiftUI
+
+@available(iOS 13.0, *)
+struct TabsSegmentControlRepresentable: UIViewRepresentable {
+ func makeUIView(context: Context) -> UIView {
+ let segmentControl: CategoriesFavoritesSegmentControl = UINib(
+ nibName: "CategoriesFavoritesSegmentControl",
+ bundle: .mapboxSearchUI
+ )
+ .instantiate(withOwner: nil, options: nil)[0] as! CategoriesFavoritesSegmentControl
+ // swiftlint:disable:previous force_cast
+
+ segmentControl.configuration = Configuration()
+
+ return segmentControl
+ }
+
+ func updateUIView(_ view: UIView, context: Context) {}
+}
+
+@available(iOS 13.0, *)
+struct CategoriesFavoritesSegmentControlPreview: PreviewProvider {
+ static var previews: some View {
+ Group {
+ TabsSegmentControlRepresentable()
+ .previewDisplayName("Light Mode")
+ .previewLayout(PreviewLayout.fixed(width: 202, height: 28))
+ TabsSegmentControlRepresentable()
+ .previewDisplayName("Dark Mode")
+ .preferredColorScheme(.dark)
+ .previewLayout(PreviewLayout.fixed(width: 202, height: 28))
+ .previewLayout(.sizeThatFits)
+ TabsSegmentControlRepresentable()
+ .previewDisplayName("Right to Left")
+ .previewLayout(PreviewLayout.fixed(width: 202, height: 28))
+ .environment(\.layoutDirection, .rightToLeft)
+ }
+ }
+}
+#endif
diff --git a/Sources/MapboxSearchUI/SearchCategoriesRootView.swift b/Sources/MapboxSearchUI/SearchCategoriesRootView.swift
index 5cac06f21..1de4b2ec4 100644
--- a/Sources/MapboxSearchUI/SearchCategoriesRootView.swift
+++ b/Sources/MapboxSearchUI/SearchCategoriesRootView.swift
@@ -102,6 +102,42 @@ class SearchCategoriesRootView: UIView {
categoriesTableView.separatorColor = configuration.style.separatorColor
}
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ /// Make sure that RTL users display the default tab
+ /// The scroll view will start at content offset (0, 0)
+ /// but the start tab will register as favorites.
+ /// When starting out with RTL, the current tab is favorites and != to `.categories` tab,
+ /// then manually set the content offset to the appropriate `.categories` default tab.
+ if effectiveUserInterfaceLayoutDirection == .rightToLeft {
+ let page = contentScrollView.contentOffset.x / contentScrollView.bounds.width
+ let currentTab = CategoriesFavoritesSegmentControl.Tab(
+ scrollViewPageProgress: page,
+ direction: effectiveUserInterfaceLayoutDirection
+ )
+
+ let defaultTab = CategoriesFavoritesSegmentControl.Tab.categories
+ guard currentTab != defaultTab else {
+ return
+ }
+
+ contentScrollView.contentOffset.x = defaultTab.horizontalOffsetFor(scrollView: contentScrollView)
+
+ // Without forcing a refresh the titles and masks will not display correctly (invisible or grayed-out)
+ // Force another layout pass to ensure these display correctly.
+ segmentedControl.setNeedsLayout()
+ segmentedControl.setNeedsDisplay()
+ segmentedControl.layoutIfNeeded()
+
+ // On first-draw we have just assigned the tab to the default and we know
+ // that this will render incorrectly for RTL users.
+ // Re-assigning the progress to the (backwards) location of the second tab
+ // (really "first tab" (which is zero-indexed)) will force it to redraw correctly.
+ segmentedControl.selectionSegmentProgress = 1
+ }
+ }
+
func resetUI(animated: Bool) {
contentScrollView.setContentOffset(.zero, animated: animated)
}
@@ -139,7 +175,10 @@ extension SearchCategoriesRootView: FavoritesTableViewSourceDelegate {
extension SearchCategoriesRootView: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let page = scrollView.contentOffset.x / scrollView.bounds.width
- let newTab = CategoriesFavoritesSegmentControl.Tab(scrollViewPageProgress: page)
+ let newTab = CategoriesFavoritesSegmentControl.Tab(
+ scrollViewPageProgress: page,
+ direction: scrollView.effectiveUserInterfaceLayoutDirection
+ )
if segmentedControl.selectedTab != newTab {
segmentedControl.selectedTab = newTab
@@ -159,7 +198,8 @@ extension SearchCategoriesRootView {
.beginFromCurrentState,
.allowUserInteraction,
.curveEaseInOut,
- ], animations: {
+ ], animations: { [weak self] in
+ guard let self else { return }
self.contentScrollView.contentOffset.x = self.segmentedControl.selectedTab.horizontalOffsetFor(
scrollView:
self.contentScrollView
@@ -169,20 +209,37 @@ extension SearchCategoriesRootView {
}
extension CategoriesFavoritesSegmentControl.Tab {
- fileprivate init(scrollViewPageProgress: CGFloat) {
- if scrollViewPageProgress <= 0.5 {
- self = .categories
+ /// When the scroll view is instantiated and at the default location, show the categories Tab
+ ///
+ fileprivate init(scrollViewPageProgress: CGFloat, direction: UIUserInterfaceLayoutDirection) {
+ if direction == .leftToRight {
+ if scrollViewPageProgress <= 0.5 {
+ self = .categories
+ } else {
+ self = .favorites
+ }
} else {
- self = .favorites
+ /// Right to Left behavior
+ if scrollViewPageProgress <= 0.5 {
+ self = .favorites
+ } else {
+ self = .categories
+ }
}
}
fileprivate func horizontalOffsetFor(scrollView: UIScrollView) -> CGFloat {
- switch self {
- case .categories:
+ switch (self, scrollView.effectiveUserInterfaceLayoutDirection) {
+ case (.categories, .leftToRight):
return 0
- case .favorites:
+ case (.favorites, .leftToRight):
return scrollView.bounds.width
+ case (.categories, .rightToLeft):
+ return scrollView.bounds.width
+ case (.favorites, .rightToLeft):
+ return 0
+ case (_, _):
+ fatalError("Unsupported text direction")
}
}
}
diff --git a/Sources/MapboxSearchUI/SearchCategoriesRootView.xib b/Sources/MapboxSearchUI/SearchCategoriesRootView.xib
index 1de24908d..f00e3e3d4 100644
--- a/Sources/MapboxSearchUI/SearchCategoriesRootView.xib
+++ b/Sources/MapboxSearchUI/SearchCategoriesRootView.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -43,19 +43,19 @@
-
+
-
+
-
+
-
+
@@ -90,9 +90,12 @@
-
+
+
+
+