Skip to content

Commit 09078de

Browse files
committed
feat: update localization handling and improve UI responsiveness
1 parent 8d78687 commit 09078de

File tree

8 files changed

+147
-32
lines changed

8 files changed

+147
-32
lines changed

Example/AppleReminders/Controllers/MainVC.swift

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SwiftUI
1111
import UIKit
1212

1313
final class MainVC: UIViewController {
14+
private let footerHeight: CGFloat = 75
1415

1516
let realm = MyRealm.getConfig()
1617

@@ -23,7 +24,6 @@ final class MainVC: UIViewController {
2324
let tv = UITableView(frame: .zero, style: .insetGrouped)
2425
tv.separatorStyle = .none
2526
tv.backgroundColor = .systemGroupedBackground
26-
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
2727
return tv
2828
}()
2929

@@ -102,6 +102,14 @@ final class MainVC: UIViewController {
102102
setupNavBar()
103103
setupSearch()
104104
}
105+
106+
override func viewDidLayoutSubviews() {
107+
super.viewDidLayoutSubviews()
108+
layoutTableHeaderIfNeeded()
109+
// Keep the footer controls above any table/search content after layout updates.
110+
view.bringSubviewToFront(footerView)
111+
updateTableInsetsForFooter()
112+
}
105113

106114
deinit {
107115
realmListToken?.invalidate()
@@ -157,7 +165,17 @@ final class MainVC: UIViewController {
157165
footerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
158166
footerView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
159167
footerView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor,constant: 0).isActive = true
160-
footerView.heightAnchor.constraint(equalToConstant: 75).isActive = true
168+
footerView.heightAnchor.constraint(equalToConstant: footerHeight).isActive = true
169+
}
170+
171+
private func updateTableInsetsForFooter() {
172+
// Compute real overlap between the table and footer to avoid rows appearing under controls.
173+
let overlap = max(0, tableView.frame.maxY - footerView.frame.minY)
174+
let bottomInset = overlap + 12
175+
if tableView.contentInset.bottom != bottomInset {
176+
tableView.contentInset.bottom = bottomInset
177+
tableView.scrollIndicatorInsets.bottom = bottomInset
178+
}
161179
}
162180

163181
private func setupNavBar() {
@@ -186,8 +204,21 @@ final class MainVC: UIViewController {
186204
searchController = UISearchController(searchResultsController: searchControllerVC)
187205
searchController?.searchResultsUpdater = self
188206
searchController?.obscuresBackgroundDuringPresentation = false
207+
searchController?.hidesNavigationBarDuringPresentation = false
189208
searchController?.searchBar.placeholder = "Search".localized
190-
navigationItem.searchController = searchController
209+
searchController?.searchBar.sizeToFit()
210+
tableView.tableHeaderView = searchController?.searchBar
211+
definesPresentationContext = true
212+
}
213+
214+
private func layoutTableHeaderIfNeeded() {
215+
guard let headerView = tableView.tableHeaderView else { return }
216+
let targetSize = CGSize(width: tableView.bounds.width, height: UIView.layoutFittingCompressedSize.height)
217+
let height = headerView.sizeThatFits(targetSize).height
218+
if headerView.frame.width != tableView.bounds.width || headerView.frame.height != height {
219+
headerView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: height)
220+
tableView.tableHeaderView = headerView
221+
}
191222
}
192223
}
193224

Example/AppleReminders/Controllers/SettingsVC.swift

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import CrowdinSDK
1111

1212
class SettingsVC: UITableViewController {
1313
var localizations = CrowdinSDK.allAvailableLocalizations
14+
private var isLocalizationLoading = false
15+
16+
private let loadingIndicator: UIActivityIndicatorView = {
17+
let indicator = UIActivityIndicatorView(style: .large)
18+
indicator.hidesWhenStopped = true
19+
indicator.translatesAutoresizingMaskIntoConstraints = false
20+
return indicator
21+
}()
1422

1523
enum Strings: String {
1624
case settings
@@ -27,6 +35,7 @@ class SettingsVC: UITableViewController {
2735
self.title = Strings.settings.rawValue.capitalized.localized
2836
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: Strings.done.rawValue.capitalized.localized, style: .done, target: self, action: #selector(cancelBtnTapped))
2937
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SettingsCell")
38+
setupLoadingIndicator()
3039
self.tableView.reloadData()
3140
}
3241

@@ -56,12 +65,64 @@ class SettingsVC: UITableViewController {
5665
}
5766

5867
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
59-
let localization = localizations[indexPath.row]
60-
if localization == Strings.auto.rawValue.capitalized.localized {
61-
CrowdinSDK.currentLocalization = nil
68+
tableView.deselectRow(at: indexPath, animated: true)
69+
70+
guard !isLocalizationLoading else { return }
71+
72+
let localization = localizationCode(for: indexPath)
73+
guard localization != CrowdinSDK.currentLocalization else {
74+
self.tableView.reloadData()
75+
return
76+
}
77+
78+
setLocalizationLoading(true)
79+
CrowdinSDK.setCurrentLocalization(localization) { [weak self] error in
80+
DispatchQueue.main.async {
81+
guard let self = self else { return }
82+
self.setLocalizationLoading(false)
83+
84+
if let error = error {
85+
self.alert(message: error.localizedDescription, title: "Error")
86+
self.tableView.reloadData()
87+
return
88+
}
89+
90+
self.reloadLocalizedUI()
91+
}
92+
}
93+
}
94+
95+
private func setupLoadingIndicator() {
96+
view.addSubview(loadingIndicator)
97+
98+
NSLayoutConstraint.activate([
99+
loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
100+
loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
101+
])
102+
}
103+
104+
private func setLocalizationLoading(_ loading: Bool) {
105+
isLocalizationLoading = loading
106+
tableView.allowsSelection = !loading
107+
tableView.isUserInteractionEnabled = !loading
108+
navigationItem.rightBarButtonItem?.isEnabled = !loading
109+
110+
if loading {
111+
loadingIndicator.startAnimating()
62112
} else {
63-
CrowdinSDK.currentLocalization = localization
113+
loadingIndicator.stopAnimating()
114+
}
115+
}
116+
117+
private func localizationCode(for indexPath: IndexPath) -> String? {
118+
return indexPath.row == 0 ? nil : localizations[indexPath.row]
119+
}
120+
121+
private func reloadLocalizedUI() {
122+
if let sceneDelegate = view.window?.windowScene?.delegate as? SceneDelegate {
123+
sceneDelegate.reloadLocalizedUI()
124+
} else {
125+
tableView.reloadData()
64126
}
65-
self.tableView.reloadData()
66127
}
67128
}

Example/AppleReminders/SceneDelegate.swift

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
3939
.with(accessToken: Self.accessToken)
4040
.with(screenshotsEnabled: true)
4141

42-
CrowdinSDK.currentLocalization = locale
42+
CrowdinSDK.setCurrentLocalization(locale) { _ in }
4343

4444
CrowdinSDK.startWithConfig(crowdinSDKConfig) {
4545
DispatchQueue.main.async {
4646
guard let windowScene = (scene as? UIWindowScene) else { return }
47-
48-
let navController = UINavigationController(rootViewController: MainVC())
49-
50-
self.window = UIWindow(frame: windowScene.coordinateSpace.bounds)
51-
self.window?.windowScene = windowScene
52-
self.window?.rootViewController = navController
53-
self.window?.makeKeyAndVisible()
47+
self.setupMainInterface(for: windowScene, animated: false)
5448
}
5549
}
5650

@@ -80,15 +74,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
8074
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
8175
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
8276
guard let windowScene = (scene as? UIWindowScene) else { return }
83-
84-
//Create Nav Controller
85-
let navController = UINavigationController(rootViewController: MainVC())
86-
87-
//source: https://www.youtube.com/watch?v=Htn4h51BQsk
88-
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
89-
window?.windowScene = windowScene
90-
window?.rootViewController = navController
91-
window?.makeKeyAndVisible()
77+
setupMainInterface(for: windowScene, animated: false)
9278
}
9379

9480
func sceneDidDisconnect(_ scene: UIScene) {
@@ -123,5 +109,31 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
123109
guard let url = URLContexts.first?.url else { return }
124110
CrowdinSDK.handle(url: url)
125111
}
126-
}
127112

113+
func reloadLocalizedUI() {
114+
guard let windowScene = window?.windowScene else { return }
115+
setupMainInterface(for: windowScene, animated: true)
116+
}
117+
118+
private func setupMainInterface(for windowScene: UIWindowScene, animated: Bool) {
119+
let navController = UINavigationController(rootViewController: MainVC())
120+
121+
if window == nil {
122+
// source: https://www.youtube.com/watch?v=Htn4h51BQsk
123+
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
124+
window?.windowScene = windowScene
125+
}
126+
127+
guard let window = window else { return }
128+
129+
if animated {
130+
UIView.transition(with: window, duration: 0.2, options: [.transitionCrossDissolve, .allowAnimatedContent], animations: {
131+
window.rootViewController = navController
132+
})
133+
} else {
134+
window.rootViewController = navController
135+
}
136+
137+
window.makeKeyAndVisible()
138+
}
139+
}

Example/AppleRemindersUITests/AppleRemindersUITestsCrowdinScreenhsotTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ final class AppleRemindersUITestsCrowdinScreenhsotTests: XCTestCase {
3131
.with(accessToken: Self.accessToken)
3232
.with(screenshotsEnabled: true)
3333

34-
CrowdinSDK.currentLocalization = localization
34+
CrowdinSDK.setCurrentLocalization(localization) { _ in }
3535

3636
CrowdinSDK.startWithConfigSync(crowdinSDKConfig)
3737
}

Sources/CrowdinSDK/CrowdinSDK/CrowdinSDK.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
2929
public var onLogCallback: ((String) -> Void)?
3030

3131
/// Current localization language code. If SDK is started than after setting new localization it triggers localization download.
32+
@available(*, deprecated, message: "Please use setCurrentLocalization(_:completion:) to update localization.")
3233
public class var currentLocalization: String? {
3334
get {
3435
return Localization.currentLocalization ?? Localization.current?.provider.localization
@@ -69,7 +70,7 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
6970
/// - Parameter completion: Remote storage preparation completion handler. Called when all required data is downloaded.
7071
class func startWithRemoteStorage(_ remoteStorage: RemoteLocalizationStorageProtocol, completion: @escaping () -> Void) {
7172
let localizations = remoteStorage.localizations + self.inBundleLocalizations
72-
let localization = self.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations)
73+
let localization = Localization.currentLocalization ?? Localization.current?.provider.localization ?? Bundle.main.preferredLanguage(with: localizations)
7374
let localStorage = LocalLocalizationStorage(localization: localization)
7475
let localizationProvider = LocalizationProvider(localization: localization, localStorage: localStorage, remoteStorage: remoteStorage)
7576

@@ -90,7 +91,7 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
9091
/// - Parameters:
9192
/// - sdkLocalization: Bool value which indicate whether to use SDK localization or native in bundle localization.
9293
/// - localization: Localization code to use.
93-
@available(*, deprecated, message: "Please use currentLocalization instead.")
94+
@available(*, deprecated, message: "Please use setCurrentLocalization(_:completion:) instead.")
9495
public class func enableSDKLocalization(_ sdkLocalization: Bool, localization: String?) {
9596
self.currentLocalization = localization
9697
}
@@ -104,6 +105,16 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
104105
Localization.setCurrentLocalization(localization, completion: completion)
105106
}
106107

108+
/// Method for changing SDK localization and getting notified when localization refresh completes.
109+
///
110+
/// - Parameters:
111+
/// - localization: Localization code to use. If `nil`, localization will be auto-detected.
112+
/// - completion: Completion handler called when localization refresh finishes.
113+
@available(*, deprecated, message: "Please use setCurrentLocalization(_:completion:) instead.")
114+
public class func setLocalization(_ localization: String?, completion: @escaping CrowdinSDKLocalizationChangeCompletion) {
115+
self.setCurrentLocalization(localization, completion: completion)
116+
}
117+
107118
/// Utils method for extracting all localization strings and plurals to Documents folder.
108119
/// This method will extract all localization for all languages and store it in Extracted subfolder in Crowdin folder.
109120
public class func extractAllLocalization() {

Sources/CrowdinSDK/Features/RealtimeUpdateFeature/RealtimeUpdateFeature.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class RealtimeUpdateFeature: RealtimeUpdateFeatureProtocol {
3434
var disconnect: (() -> Void)?
3535
var localization: String {
3636
let localizations = Localization.current.provider.remoteStorage.localizations
37-
return CrowdinSDK.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations)
37+
return Localization.currentLocalization ?? Localization.current?.provider.localization ?? Bundle.main.preferredLanguage(with: localizations)
3838
}
3939
let hashString: String
4040
let sourceLanguage: String

Sources/CrowdinSDK/Providers/Crowdin/CrowdinRemoteLocalizationStorage.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class CrowdinRemoteLocalizationStorage: RemoteLocalizationStorageProtocol {
6060
self.localizations = self.manifestManager.iOSLanguages
6161
// Only update localization if it wasn't explicitly set and if CrowdinSDK has a current localization
6262
// or if the current localization is not in the available localizations
63-
if let currentLocalization = CrowdinSDK.currentLocalization,
63+
if let currentLocalization = Localization.currentLocalization ?? Localization.current?.provider.localization,
6464
self.localizations.contains(currentLocalization) {
6565
self.localization = currentLocalization
6666
} else if !self.localizations.contains(self.localization) {

Sources/CrowdinSDK/Providers/Crowdin/Extensions/CrowdinSDK+CrowdinProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension CrowdinSDK {
2727
public class func startWithConfig(_ config: CrowdinSDKConfig, completion: @escaping () -> Void) {
2828
self.config = config
2929
let crowdinProviderConfig = config.crowdinProviderConfig ?? CrowdinProviderConfig()
30-
let localization = currentLocalization ?? Bundle.main.preferredLanguage
30+
let localization = Localization.currentLocalization ?? Bundle.main.preferredLanguage
3131
let remoteStorage = CrowdinRemoteLocalizationStorage(localization: localization, config: crowdinProviderConfig)
3232
self.startWithRemoteStorage(remoteStorage, completion: completion)
3333
}

0 commit comments

Comments
 (0)