Skip to content
64 changes: 50 additions & 14 deletions Example/AppleReminders/Controllers/MainVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import SwiftUI
import UIKit

final class MainVC: UIViewController {
private let footerHeight: CGFloat = 75
private let footerInsetPadding: CGFloat = 12

let realm = MyRealm.getConfig()

Expand All @@ -23,7 +25,6 @@ final class MainVC: UIViewController {
let tv = UITableView(frame: .zero, style: .insetGrouped)
tv.separatorStyle = .none
tv.backgroundColor = .systemGroupedBackground
tv.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
return tv
}()

Expand Down Expand Up @@ -59,6 +60,8 @@ final class MainVC: UIViewController {

setupTableView()
addListView()
setupNavBar()

footerView.addListBtn.addTarget(self, action: #selector(addListBtnTapped), for: .touchUpInside)
footerView.settingsBtn.addTarget(self, action: #selector(settingsBtnTapped), for: .touchUpInside)
footerView.addGroupBtn.addTarget(self, action: #selector(addGroupBtnTapped), for: .touchUpInside)
Expand All @@ -81,9 +84,23 @@ final class MainVC: UIViewController {
}
}

deinit {
realmListToken?.invalidate()
print("MainVC deinit")
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
layoutTableHeaderIfNeeded()
updateTableInsetsForFooter()
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// Keep the footer controls above any table/search content.
view.bringSubviewToFront(footerView)

realmListToken = realm?.objects(ReminderList.self).observe { [weak self] changes in
guard let self = self else { return }
switch changes {
Expand All @@ -98,14 +115,6 @@ final class MainVC: UIViewController {
case .error: break
}
}

setupNavBar()
setupSearch()
}

deinit {
realmListToken?.invalidate()
print("MainVC deinit")
}

private func setupTableView() {
Expand Down Expand Up @@ -154,10 +163,20 @@ final class MainVC: UIViewController {
addViews(views: footerView)

footerView.translatesAutoresizingMaskIntoConstraints = false
footerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0).isActive = true
footerView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
footerView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
footerView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor,constant: 0).isActive = true
footerView.heightAnchor.constraint(equalToConstant: 75).isActive = true
footerView.heightAnchor.constraint(equalToConstant: footerHeight).isActive = true
}

private func updateTableInsetsForFooter() {
// Compute real overlap between the table and footer to avoid rows appearing under controls.
let overlap = max(0, tableView.frame.maxY - footerView.frame.minY)
let bottomInset = overlap + footerInsetPadding
if tableView.contentInset.bottom != bottomInset {
tableView.contentInset.bottom = bottomInset
tableView.scrollIndicatorInsets.bottom = bottomInset
}
}

private func setupNavBar() {
Expand Down Expand Up @@ -186,11 +205,23 @@ final class MainVC: UIViewController {
searchController = UISearchController(searchResultsController: searchControllerVC)
searchController?.searchResultsUpdater = self
searchController?.obscuresBackgroundDuringPresentation = false
searchController?.hidesNavigationBarDuringPresentation = false
searchController?.searchBar.placeholder = "Search".localized

// Use navigation item search controller for better reliability
navigationItem.searchController = searchController
if #available(iOS 16.0, *) {
// Keep search UI in the navigation bar
navigationItem.preferredSearchBarPlacement = .stacked
navigationItem.hidesSearchBarWhenScrolling = false

definesPresentationContext = true
}

private func layoutTableHeaderIfNeeded() {
guard let headerView = tableView.tableHeaderView else { return }
let targetSize = CGSize(width: tableView.bounds.width, height: UIView.layoutFittingCompressedSize.height)
let height = headerView.sizeThatFits(targetSize).height
if headerView.frame.width != tableView.bounds.width || headerView.frame.height != height {
headerView.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: height)
tableView.tableHeaderView = headerView
}
}
}
Expand Down Expand Up @@ -222,6 +253,11 @@ extension MainVC {
})

updateDatasource()

// Setup search after datasource is configured
if searchController == nil {
setupSearch()
}
}

func updateDatasource() {
Expand Down
70 changes: 65 additions & 5 deletions Example/AppleReminders/Controllers/SettingsVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import CrowdinSDK

class SettingsVC: UITableViewController {
var localizations = CrowdinSDK.allAvailableLocalizations
private var isLocalizationLoading = false

private let loadingIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .large)
indicator.hidesWhenStopped = true
indicator.translatesAutoresizingMaskIntoConstraints = false
return indicator
}()

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

Expand Down Expand Up @@ -56,12 +65,63 @@ class SettingsVC: UITableViewController {
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let localization = localizations[indexPath.row]
if localization == Strings.auto.rawValue.capitalized.localized {
CrowdinSDK.currentLocalization = nil
tableView.deselectRow(at: indexPath, animated: true)

guard !isLocalizationLoading else { return }

let localization = localizationCode(for: indexPath)
guard localization != CrowdinSDK.currentLocalization else {
self.tableView.reloadData()
return
}
Comment thread
serhii-londar marked this conversation as resolved.

setLocalizationLoading(true)
CrowdinSDK.setCurrentLocalization(localization) { [weak self] error in
DispatchQueue.main.async {
guard let self = self else { return }
self.setLocalizationLoading(false)

if let error = error {
self.alert(message: error.localizedDescription, title: "Error")
self.tableView.reloadData()
return
}

self.reloadLocalizedUI()
}
}
}

private func setupLoadingIndicator() {
view.addSubview(loadingIndicator)

NSLayoutConstraint.activate([
loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}

private func setLocalizationLoading(_ loading: Bool) {
isLocalizationLoading = loading
tableView.allowsSelection = !loading
navigationItem.rightBarButtonItem?.isEnabled = !loading

if loading {
loadingIndicator.startAnimating()
} else {
CrowdinSDK.currentLocalization = localization
loadingIndicator.stopAnimating()
}
}

private func localizationCode(for indexPath: IndexPath) -> String? {
return indexPath.row == 0 ? nil : localizations[indexPath.row]
}

private func reloadLocalizedUI() {
if let sceneDelegate = view.window?.windowScene?.delegate as? SceneDelegate {
sceneDelegate.reloadLocalizedUI()
} else {
tableView.reloadData()
}
self.tableView.reloadData()
}
}
48 changes: 30 additions & 18 deletions Example/AppleReminders/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
.with(accessToken: Self.accessToken)
.with(screenshotsEnabled: true)

CrowdinSDK.currentLocalization = locale
CrowdinSDK.setCurrentLocalization(locale) { _ in }

CrowdinSDK.startWithConfig(crowdinSDKConfig) {
DispatchQueue.main.async {
guard let windowScene = (scene as? UIWindowScene) else { return }

let navController = UINavigationController(rootViewController: MainVC())

self.window = UIWindow(frame: windowScene.coordinateSpace.bounds)
self.window?.windowScene = windowScene
self.window?.rootViewController = navController
self.window?.makeKeyAndVisible()
self.setupMainInterface(for: windowScene, animated: false)
}
}

Expand Down Expand Up @@ -80,15 +74,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = (scene as? UIWindowScene) else { return }

//Create Nav Controller
let navController = UINavigationController(rootViewController: MainVC())

//source: https://www.youtube.com/watch?v=Htn4h51BQsk
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = navController
window?.makeKeyAndVisible()
setupMainInterface(for: windowScene, animated: false)
}

func sceneDidDisconnect(_ scene: UIScene) {
Expand Down Expand Up @@ -123,5 +109,31 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
guard let url = URLContexts.first?.url else { return }
CrowdinSDK.handle(url: url)
}
}

func reloadLocalizedUI() {
guard let windowScene = window?.windowScene else { return }
setupMainInterface(for: windowScene, animated: true)
}

private func setupMainInterface(for windowScene: UIWindowScene, animated: Bool) {
let navController = UINavigationController(rootViewController: MainVC())

if window == nil {
// source: https://www.youtube.com/watch?v=Htn4h51BQsk
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
}

guard let window = window else { return }

if animated {
UIView.transition(with: window, duration: 0.2, options: [.transitionCrossDissolve, .allowAnimatedContent], animations: {
window.rootViewController = navController
})
} else {
window.rootViewController = navController
}

window.makeKeyAndVisible()
}
Comment thread
serhii-londar marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class AppleRemindersUITestsCrowdinScreenhsotTests: XCTestCase {
.with(accessToken: Self.accessToken)
.with(screenshotsEnabled: true)

CrowdinSDK.currentLocalization = localization
CrowdinSDK.setCurrentLocalization(localization) { _ in }

CrowdinSDK.startWithConfigSync(crowdinSDKConfig)
}
Expand Down
13 changes: 11 additions & 2 deletions Sources/CrowdinSDK/CrowdinSDK/CrowdinSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
public var onLogCallback: ((String) -> Void)?

/// Current localization language code. If SDK is started than after setting new localization it triggers localization download.
///
/// - Note: Deprecated. Use ``setCurrentLocalization(_:completion:)`` instead for async operation with completion handler.
@available(*, deprecated, message: "Please use setCurrentLocalization(_:completion:) to update localization.")
public class var currentLocalization: String? {
get {
return Localization.currentLocalization ?? Localization.current?.provider.localization
Expand Down Expand Up @@ -69,7 +72,7 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
/// - Parameter completion: Remote storage preparation completion handler. Called when all required data is downloaded.
class func startWithRemoteStorage(_ remoteStorage: RemoteLocalizationStorageProtocol, completion: @escaping () -> Void) {
let localizations = remoteStorage.localizations + self.inBundleLocalizations
let localization = self.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations)
let localization = Localization.currentLocalization ?? Localization.current?.provider.localization ?? Bundle.main.preferredLanguage(with: localizations)
let localStorage = LocalLocalizationStorage(localization: localization)
let localizationProvider = LocalizationProvider(localization: localization, localStorage: localStorage, remoteStorage: remoteStorage)

Expand All @@ -90,7 +93,7 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
/// - Parameters:
/// - sdkLocalization: Bool value which indicate whether to use SDK localization or native in bundle localization.
/// - localization: Localization code to use.
@available(*, deprecated, message: "Please use currentLocalization instead.")
@available(*, deprecated, message: "Please use setCurrentLocalization(_:completion:) and getCurrentLocalization() methods instead.")
public class func enableSDKLocalization(_ sdkLocalization: Bool, localization: String?) {
self.currentLocalization = localization
}
Expand All @@ -103,6 +106,12 @@ public typealias CrowdinSDKLogMessage = (String) -> Void
public class func setCurrentLocalization(_ localization: String?, completion: @escaping CrowdinSDKLocalizationChangeCompletion) {
Localization.setCurrentLocalization(localization, completion: completion)
}

/// Method to get current SDK localization.
/// - Returns: Current SDK localization
public class func getCurrentLocalization() -> String? {
return Localization.currentLocalization
}

/// Utils method for extracting all localization strings and plurals to Documents folder.
/// This method will extract all localization for all languages and store it in Extracted subfolder in Crowdin folder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class RealtimeUpdateFeature: RealtimeUpdateFeatureProtocol {
var disconnect: (() -> Void)?
var localization: String {
let localizations = Localization.current.provider.remoteStorage.localizations
Comment thread
serhii-londar marked this conversation as resolved.
return CrowdinSDK.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations)
return Localization.currentLocalization ?? Bundle.main.preferredLanguage(with: localizations)
Comment thread
cursor[bot] marked this conversation as resolved.
}
let hashString: String
let sourceLanguage: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ extension CrowdinSDK {
public class func startWithConfig(_ config: CrowdinSDKConfig, completion: @escaping () -> Void) {
self.config = config
let crowdinProviderConfig = config.crowdinProviderConfig ?? CrowdinProviderConfig()
let localization = currentLocalization ?? Bundle.main.preferredLanguage
let localization = Localization.currentLocalization ?? Localization.current?.provider.localization ?? Bundle.main.preferredLanguage
let remoteStorage = CrowdinRemoteLocalizationStorage(localization: localization, config: crowdinProviderConfig)
self.startWithRemoteStorage(remoteStorage, completion: completion)
}
Expand Down
Loading