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 Screenshot 2024-03-19 at 17 42 58 ##### 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 Screenshot 2024-03-20 at 13 18 34 Screenshot 2024-03-20 at 13 18 31 Screenshot 2024-03-20 at 13 18 29 ###### 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 @@ - + + + +