From dadb0cf67d34f58ca1d967bed7d937f78ed489b6 Mon Sep 17 00:00:00 2001 From: Rapinchuk Igor Date: Mon, 28 Oct 2024 13:09:41 +0300 Subject: [PATCH 1/5] Offline map --- .../check.imageset/Contents.json | 16 + .../check.imageset/approved.png | Bin 0 -> 6255 bytes .../uncheck.imageset/Contents.json | 16 + .../uncheck.imageset/unchecked.png | Bin 0 -> 4764 bytes mapkit-samples/Common/Utils/UserStorage.swift | 26 ++ .../MapOffline/DataMoveListener.swift | 33 ++ .../MapOffline/MapCameraListener.swift | 31 ++ mapkit-samples/MapOffline/MapUIState.swift | 35 ++ .../MapOffline/MapViewController.swift | 300 +++++++++++++++++ .../OfflineCacheRegionListener.swift | 29 ++ .../OfflineMapRegionListUpdatesListener.swift | 25 ++ .../MapOffline/OptionsViewController.swift | 310 ++++++++++++++++++ .../MapOffline/OptionsViewModel.swift | 25 ++ mapkit-samples/MapOffline/ProgressView.swift | 34 ++ mapkit-samples/MapOffline/RegionInfo.swift | 20 ++ mapkit-samples/MapOffline/RegionItem.swift | 11 + .../MapOffline/RegionViewController.swift | 261 +++++++++++++++ .../MapOffline/RegionViewModel.swift | 92 ++++++ .../MapOffline/SearchViewModel.swift | 167 ++++++++++ .../MapOffline/TitleCheckboxView.swift | 93 ++++++ .../MapRouting/RoutingViewModel.swift | 2 +- .../MapSearch/MapViewController.swift | 8 +- .../MapkitSamples.xcodeproj/project.pbxproj | 249 +++++++++++++- .../xcschemes/xcschememanagement.plist | 44 +++ mapkit-samples/Podfile | 4 + 25 files changed, 1822 insertions(+), 9 deletions(-) create mode 100644 mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/Contents.json create mode 100644 mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/approved.png create mode 100644 mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/Contents.json create mode 100644 mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/unchecked.png create mode 100644 mapkit-samples/Common/Utils/UserStorage.swift create mode 100644 mapkit-samples/MapOffline/DataMoveListener.swift create mode 100644 mapkit-samples/MapOffline/MapCameraListener.swift create mode 100644 mapkit-samples/MapOffline/MapUIState.swift create mode 100644 mapkit-samples/MapOffline/MapViewController.swift create mode 100644 mapkit-samples/MapOffline/OfflineCacheRegionListener.swift create mode 100644 mapkit-samples/MapOffline/OfflineMapRegionListUpdatesListener.swift create mode 100644 mapkit-samples/MapOffline/OptionsViewController.swift create mode 100644 mapkit-samples/MapOffline/OptionsViewModel.swift create mode 100644 mapkit-samples/MapOffline/ProgressView.swift create mode 100644 mapkit-samples/MapOffline/RegionInfo.swift create mode 100644 mapkit-samples/MapOffline/RegionItem.swift create mode 100644 mapkit-samples/MapOffline/RegionViewController.swift create mode 100644 mapkit-samples/MapOffline/RegionViewModel.swift create mode 100644 mapkit-samples/MapOffline/SearchViewModel.swift create mode 100644 mapkit-samples/MapOffline/TitleCheckboxView.swift create mode 100644 mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/Contents.json b/mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/Contents.json new file mode 100644 index 0000000..07607a6 --- /dev/null +++ b/mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "approved.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/approved.png b/mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/approved.png new file mode 100644 index 0000000000000000000000000000000000000000..34b8582186de0e1a8a9d60953026ae668fc8f3dc GIT binary patch literal 6255 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 literal 0 HcmV?d00001 diff --git a/mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/Contents.json b/mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/Contents.json new file mode 100644 index 0000000..6eada1a --- /dev/null +++ b/mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "unchecked.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/unchecked.png b/mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/unchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..7b17a1791416a604fe88132ad68fd0089705c81e GIT binary patch literal 4764 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 + + // MARK: - Private nesting + + private enum Const { + static let startPoint = YMKPoint(latitude: 55.753284, longitude: 37.622034) + static let startPosition = YMKCameraPosition(target: startPoint, zoom: 13.0, azimuth: .zero, tilt: .zero) + } + + // MARK: - Layout + + private enum Layout { + static let buttonSize: CGFloat = 50.0 + } +} + +extension MapViewController: UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate { + func updateSearchResults(for searchController: UISearchController) { + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + searchViewModel.reset() + searchViewModel.setQueryText(with: searchText) + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + searchViewModel.startSearch() + searchBarController.searchBar.text = searchViewModel.mapUIState.query + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + if case .idle = searchViewModel.mapUIState.searchState { + updatePlaceholder() + } + } + + func setupStateUpdates() { + searchViewModel.$mapUIState.sink { [weak self] state in + let query = state?.query ?? String() + self?.searchBarController.searchBar.text = query + self?.updatePlaceholder(with: query) + + if case let .success(items, zoomToItems, itemsBoundingBox) = state?.searchState { + if zoomToItems { + self?.focusCamera(points: items.map { $0.point }, boundingBox: itemsBoundingBox) + } + } + if let regionState = state?.regionListState, + case .success = state?.regionListState { + self?.updateRegions(with: regionState, query: query) + } + } + .store(in: &bag) + } + + private func showRegionInfo(regionId: Int) { + regionViewController.setRegion(with: regionId) + present(regionViewController, animated: true) + } + + private func updateRegions(with regionListState: RegionListState, query: String) { + switch regionListState { + case .success: + resultsTableController.items = offlineManager.regions() + .map { region in + RegionItem(model: region) { + self.searchViewModel.startSearch(with: region.name) + self.showRegionInfo(regionId: Int(region.id)) + } + } + .filter { $0.model.name.contains(query) } + resultsTableController.tableView.reloadData() + default: + return + } + } + + private func updatePlaceholder(with text: String = String()) { + searchBarController.searchBar.placeholder = text.isEmpty ? "Search places" : text + } +} + +extension MapViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.row < resultsTableController.items.count else { return } + + searchBarController.isActive = false + + let item = resultsTableController.items[indexPath.row] + item.onClick() + } +} + +private class ResultsTableController: UITableViewController { + + private let cellIdentifier = "cellIdentifier" + + var items = [RegionItem]() + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier) + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return items.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + cell.textLabel?.numberOfLines = 0 + + let item = items[indexPath.row] + + cell.textLabel?.attributedText = item.cellText + + return cell + } +} + +fileprivate extension RegionItem { + + var cellText: NSAttributedString { + let result = NSMutableAttributedString(string: model.name) + result.append(NSAttributedString(string: " ")) + + let subtitle = NSMutableAttributedString(string: "") + subtitle.setAttributes( + [.foregroundColor: UIColor.secondaryLabel], + range: NSRange(location: 0, length: subtitle.string.count) + ) + result.append(subtitle) + + return result + } +} diff --git a/mapkit-samples/MapOffline/OfflineCacheRegionListener.swift b/mapkit-samples/MapOffline/OfflineCacheRegionListener.swift new file mode 100644 index 0000000..d9a7cf8 --- /dev/null +++ b/mapkit-samples/MapOffline/OfflineCacheRegionListener.swift @@ -0,0 +1,29 @@ +// +// MapCameraListener.swift +// MapSearch +// + +import YandexMapsMobile + +class OfflineCacheRegionListener: NSObject, YMKOfflineCacheRegionListener { + + // MARK: - Constructor + + init(regionViewModel: RegionViewModel) { + self.regionViewModel = regionViewModel + } + + // MARK: - Public methods + + func onRegionStateChanged(withRegionId regionId: UInt) { + regionViewModel.setRegion(with: Int(regionId)) + } + + func onRegionProgress(withRegionId regionId: UInt) { + regionViewModel.setRegion(with: Int(regionId)) + } + + // MARK: - Private properties + + private let regionViewModel: RegionViewModel +} diff --git a/mapkit-samples/MapOffline/OfflineMapRegionListUpdatesListener.swift b/mapkit-samples/MapOffline/OfflineMapRegionListUpdatesListener.swift new file mode 100644 index 0000000..168d70d --- /dev/null +++ b/mapkit-samples/MapOffline/OfflineMapRegionListUpdatesListener.swift @@ -0,0 +1,25 @@ +// +// MapCameraListener.swift +// MapSearch +// + +import YandexMapsMobile + +class OfflineMapRegionListUpdatesListener: NSObject, YMKOfflineMapRegionListUpdatesListener { + + // MARK: - Constructor + + init(searchViewModel: SearchViewModel) { + self.searchViewModel = searchViewModel + } + + // MARK: - Public methods + + func onListUpdated() { + searchViewModel.regionListState = .success + } + + // MARK: - Private properties + + private let searchViewModel: SearchViewModel +} diff --git a/mapkit-samples/MapOffline/OptionsViewController.swift b/mapkit-samples/MapOffline/OptionsViewController.swift new file mode 100644 index 0000000..33a458b --- /dev/null +++ b/mapkit-samples/MapOffline/OptionsViewController.swift @@ -0,0 +1,310 @@ +// +// MapViewController.swift +// MapSearch +// + +import Combine +import UIKit +import YandexMapsMobile + +class OptionsViewController: UIViewController { + // MARK: - Public methods + + override func viewDidLoad() { + super.viewDidLoad() + + setupSubscriptions() + + setupSubviews() + configureContentInfo() + view.backgroundColor = .white + } + + // MARK: - Private methods + + private func setupSubviews() { + view.addSubview(optionsView) + optionsView.axis = .vertical + optionsView.alignment = .fill + optionsView.spacing = 10 + optionsView.translatesAutoresizingMaskIntoConstraints = false + + progressView.isHidden = true + dividerView.backgroundColor = .black + + cellularNetworkView.checkBox.delegate = self + cellularNetworkView.checkBox.type = .cellularNetwork + autoEnabledView.checkBox.delegate = self + autoEnabledView.checkBox.type = .autoEnabled + + cachesPathTitleLabel.textColor = .black + cachesPathTitleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + cachesPathTextView.textColor = .black + cachesPathTextView.delegate = self + cachesPathTextView.font = UIFont.systemFont(ofSize: 16) + + cashSizeButton.translatesAutoresizingMaskIntoConstraints = false + cashSizeButton.layer.cornerRadius = 4 + cashSizeButton.setTitleColor(.black, for: .normal) + cashSizeButton.contentMode = .scaleAspectFill + cashSizeButton.backgroundColor = Palette.background + + clearSizeButton.translatesAutoresizingMaskIntoConstraints = false + clearSizeButton.layer.cornerRadius = 4 + clearSizeButton.setTitleColor(.black, for: .normal) + clearSizeButton.contentMode = .scaleAspectFill + clearSizeButton.backgroundColor = Palette.background + + pathButtonsView.axis = .horizontal + pathButtonsView.distribution = .fillEqually + pathButtonsView.spacing = 10 + pathButtonsView.translatesAutoresizingMaskIntoConstraints = false + + [ + optionsView.topAnchor.constraint(equalTo: view.topAnchor, constant: 30), + optionsView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Layout.defaultInset), + optionsView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Layout.defaultInset) + ] + .forEach { $0.isActive = true } + + [cellularNetworkView, autoEnabledView, cashSizeButton, clearSizeButton, + cachesPathTitleLabel, cachesPathTextView, dividerView, pathButtonsView, progressView] + .forEach { + optionsView.addArrangedSubview($0) + } + + [ + progressView.heightAnchor.constraint(equalToConstant: Layout.progressViewHeight) + ] + .forEach { $0.isActive = true } + + optionsView.setCustomSpacing(25, after: cellularNetworkView) + optionsView.setCustomSpacing(40, after: autoEnabledView) + + [ + dividerView.heightAnchor.constraint(equalToConstant: 1) + ] + .forEach { $0.isActive = true } + + [ + cachesPathTextView.heightAnchor.constraint(equalToConstant: Layout.cachesPathTextViewHeight) + ] + .forEach { $0.isActive = true } + + [ + cashSizeButton.widthAnchor.constraint(equalToConstant: Layout.buttonWidth), + cashSizeButton.heightAnchor.constraint(equalToConstant: Layout.buttonHeight) + ] + .forEach { $0.isActive = true } + + [ + clearSizeButton.widthAnchor.constraint(equalToConstant: Layout.buttonWidth), + clearSizeButton.heightAnchor.constraint(equalToConstant: Layout.buttonHeight) + ] + .forEach { $0.isActive = true } + + [moveButton, switchButton] + .forEach { + pathButtonsView.addArrangedSubview($0) + + $0.translatesAutoresizingMaskIntoConstraints = false + [ + $0.heightAnchor.constraint(equalToConstant: Layout.buttonHeight) + ] + .forEach { $0.isActive = true } + + $0.layer.cornerRadius = 4 + $0.setTitleColor(.black, for: .normal) + $0.contentMode = .scaleAspectFill + $0.backgroundColor = Palette.background + } + + pathButtonsView.addArrangedSubview(UIView()) + + cashSizeButton.addTarget( + self, + action: #selector(cashSizeButtonTapHandler), + for: .touchUpInside + ) + + clearSizeButton.addTarget( + self, + action: #selector(clearButtonTapHandler), + for: .touchUpInside + ) + + moveButton.addTarget( + self, + action: #selector(moveButtonTapHandler), + for: .touchUpInside + ) + + switchButton.addTarget( + self, + action: #selector(switchButtonTapHandler), + for: .touchUpInside + ) + } + + private func setupSubscriptions() { + setupProgressSubscriptions() + setupSuccessMoveSubscriptions() + setupErrorMoveSubscriptions() + } + + private func setupProgressSubscriptions() { + optionsViewModel.$progress.sink { [weak self] progress in + self?.progressView.isHidden = false + self?.progressView.configureView(value: progress ?? .zero) + } + .store(in: &bag) + } + + private func setupSuccessMoveSubscriptions() { + optionsViewModel.$isSuccessMove.sink { [weak self] _ in + self?.progressView.isHidden = true + let alert = UIAlertController( + title: "", + message: "Caches moved to: \(self?.cachesPathTextView.text ?? "")", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Ok", style: .cancel)) + + self?.present(alert, animated: true) + } + .store(in: &bag) + } + + private func setupErrorMoveSubscriptions() { + optionsViewModel.$errorText.sink { [weak self] text in + self?.progressView.isHidden = true + let alert = UIAlertController( + title: "", + message: "Error on moving: \(text ?? "")", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Ok", style: .cancel)) + + self?.present(alert, animated: true) + } + .store(in: &bag) + } + + private func configureContentInfo() { + cachesPathTitleLabel.text = "Caches path" + offlineManager.requestPath(pathGetterListener: { [weak self] path in + self?.cachesPathTextView.text = path + }) + + cashSizeButton.setTitle("CASHE SIZE", for: .normal) + clearSizeButton.setTitle("CLEAR CACHE", for: .normal) + moveButton.setTitle("MOVE", for: .normal) + switchButton.setTitle("SWITCH", for: .normal) + + cellularNetworkView.checkBox.isChecked = UserStorage.isCellularNetwork + autoEnabledView.checkBox.isChecked = UserStorage.isAutoUpdate + } + + @objc + private func cashSizeButtonTapHandler() { + offlineManager.computeCacheSize { [weak self] number in + let alert = UIAlertController( + title: "", + message: "Total size caches: \(number ?? NSNumber()) bytes", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Ok", style: .cancel)) + + self?.present(alert, animated: true) + } + } + + @objc + private func clearButtonTapHandler() { + offlineManager.clear { + let alert = UIAlertController( + title: "", + message: "All caches were cleared", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Ok", style: .cancel)) + + self.present(alert, animated: true) + } + } + + @objc + private func moveButtonTapHandler() { + offlineManager.moveData(withNewPath: self.cachesPathTextView.text, dataMoveListener: dataMoveListener) + } + + @objc + private func switchButtonTapHandler() { + offlineManager.setCachePathWithPath(self.cachesPathTextView.text) { [weak self] error in + let alert = UIAlertController( + title: "", + message: "Error on setting path: \(error?.localizedDescription ?? "")", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Ok", style: .cancel)) + + self?.present(alert, animated: true) + } + } + + // MARK: - Private properties + + private let optionsView = UIStackView() + private let cellularNetworkView = TitleCheckBoxView(title: "Allow cellular network") + private let autoEnabledView = TitleCheckBoxView(title: "Auto update enabled") + private let cashSizeButton = UIButton() + private let clearSizeButton = UIButton() + private let cachesPathTitleLabel = UILabel() + private let cachesPathTextView = UITextView() + private let dividerView = UIView() + private let pathButtonsView = UIStackView() + private let moveButton = UIButton() + private let switchButton = UIButton() + private let progressView = ProgressView() + + private let optionsViewModel = OptionsViewModel() + + private lazy var dataMoveListener = DataMoveListener(optionViewModel: optionsViewModel) + private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager + + private var bag = Set() + + // MARK: - Layout + + private enum Layout { + static let buttonHeight: CGFloat = 36.0 + static let defaultInset: CGFloat = 20.0 + static let buttonWidth: CGFloat = 60.0 + static let cachesPathTextViewHeight = 80.0 + static let progressViewHeight = 6.0 + } +} + +extension OptionsViewController: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + self.cachesPathTextView.text = textView.text + } +} + +extension OptionsViewController: CheckBoxDelegate { + func tapCheckbox(isChecked: Bool, type: CheckBox.CheckboxType) { + switch type { + case .cellularNetwork: + UserStorage.isCellularNetwork = isChecked + offlineManager.allowUseCellularNetwork(withUseCellular: isChecked) + case .autoEnabled: + UserStorage.isAutoUpdate = isChecked + offlineManager.enableAutoUpdate(withEnable: isChecked) + } + } +} diff --git a/mapkit-samples/MapOffline/OptionsViewModel.swift b/mapkit-samples/MapOffline/OptionsViewModel.swift new file mode 100644 index 0000000..2b61798 --- /dev/null +++ b/mapkit-samples/MapOffline/OptionsViewModel.swift @@ -0,0 +1,25 @@ +// +// SearchViewModel.swift +// MapSearch +// + +import Combine +import YandexMapsMobile + +class OptionsViewModel { + // MARK: - Public properties + + @Published var progress: Float? + @Published var isSuccessMove: Bool? + @Published var errorText: String? + + // MARK: - Private properties + + private var bag = Set() + + // MARK: - Private nesting + + private enum Const { + + } +} diff --git a/mapkit-samples/MapOffline/ProgressView.swift b/mapkit-samples/MapOffline/ProgressView.swift new file mode 100644 index 0000000..1b09334 --- /dev/null +++ b/mapkit-samples/MapOffline/ProgressView.swift @@ -0,0 +1,34 @@ +import UIKit + +final class ProgressView: UIView { + + // MARK: - Private Properties + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Private Methods + + private func commonInit() { + setupView() + } + + private func setupView() { + backgroundColor = .lightGray + } + + // MARK: - Public Methods + + func configureView(value: Float) { + layer.sublayers?.removeAll() + + let myLayer = CALayer() + myLayer.frame = CGRect(x: 0, y: 0, width: CGFloat(value) * frame.size.width, height: 6) + myLayer.backgroundColor = UIColor.darkGray.cgColor + layer.addSublayer(myLayer) + } +} diff --git a/mapkit-samples/MapOffline/RegionInfo.swift b/mapkit-samples/MapOffline/RegionInfo.swift new file mode 100644 index 0000000..cbb8b49 --- /dev/null +++ b/mapkit-samples/MapOffline/RegionInfo.swift @@ -0,0 +1,20 @@ +// +// SuggestItem.swift +// MapSearch +// + +import YandexMapsMobile + +struct RegionInfo { + let id: String + let name: String + let country: String + let center: String + let cities: String + let size: String + let downloadProgress: Float + let parentId: String + let state: String + let realeseTime: String? + let downloadedReleaseTime: String? +} diff --git a/mapkit-samples/MapOffline/RegionItem.swift b/mapkit-samples/MapOffline/RegionItem.swift new file mode 100644 index 0000000..e33ef38 --- /dev/null +++ b/mapkit-samples/MapOffline/RegionItem.swift @@ -0,0 +1,11 @@ +// +// SuggestItem.swift +// MapSearch +// + +import YandexMapsMobile + +struct RegionItem { + let model: YMKOfflineCacheRegion + let onClick: () -> Void +} diff --git a/mapkit-samples/MapOffline/RegionViewController.swift b/mapkit-samples/MapOffline/RegionViewController.swift new file mode 100644 index 0000000..a5aee0a --- /dev/null +++ b/mapkit-samples/MapOffline/RegionViewController.swift @@ -0,0 +1,261 @@ +// +// MapViewController.swift +// MapSearch +// + +import Combine +import UIKit +import YandexMapsMobile + +class RegionViewController: UIViewController { + // MARK: - Public Properties + + // MARK: - Public methods + + override func viewDidLoad() { + super.viewDidLoad() + + offlineManager.addRegionListener(with: offlineCacheRegionListener) + regionViewModel.setupSubscriptions() + + setupRegionSubscriptions() + setupSubviews() + view.backgroundColor = .white + } + + func setRegion(with regionId: Int) { + self.regionId = regionId + regionViewModel.setRegion(with: regionId) + } + + // MARK: - Private methods + + private func setupSubviews() { + view.addSubview(regionView) + regionView.axis = .vertical + regionView.alignment = .fill + regionView.spacing = 10 + regionView.translatesAutoresizingMaskIntoConstraints = false + + idLabel.textColor = .black + idLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + nameLabel.textColor = .black + nameLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + countyLabel.textColor = .black + citiesLabel.textColor = .black + citiesLabel.numberOfLines = .zero + parentIdLabel.textColor = .black + centerLabel.textColor = .black + + showButton.translatesAutoresizingMaskIntoConstraints = false + showButton.layer.cornerRadius = 4 + showButton.setTitleColor(.black, for: .normal) + showButton.contentMode = .scaleAspectFill + showButton.backgroundColor = Palette.background + + downloadView.axis = .vertical + downloadView.alignment = .fill + downloadView.spacing = 10 + downloadView.translatesAutoresizingMaskIntoConstraints = false + + progressView.isHidden = true + stateLabel.textColor = .black + stateLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + sizeLabel.textColor = .black + releaseLabel.textColor = .black + downloadedTimeLabel.textColor = .black + + downloadButtonsView.axis = .horizontal + downloadButtonsView.distribution = .fillEqually + downloadButtonsView.spacing = 10 + downloadButtonsView.translatesAutoresizingMaskIntoConstraints = false + + [ + regionView.topAnchor.constraint(equalTo: view.topAnchor, constant: Layout.defaultInset), + regionView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Layout.defaultInset) + ] + .forEach { $0.isActive = true } + + [idLabel, nameLabel, countyLabel, citiesLabel, parentIdLabel, centerLabel] + .forEach { + regionView.addArrangedSubview($0) + } + + regionView.setCustomSpacing(25, after: parentIdLabel) + + view.addSubview(showButton) + + [ + showButton.leadingAnchor.constraint(equalTo: regionView.trailingAnchor, constant: Layout.defaultInset), + showButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Layout.defaultInset), + showButton.centerYAnchor.constraint(equalTo: centerLabel.centerYAnchor), + showButton.heightAnchor.constraint(equalToConstant: Layout.buttonHeight) + ] + .forEach { $0.isActive = true } + + view.addSubview(downloadView) + + [ + downloadView.topAnchor.constraint(equalTo: regionView.bottomAnchor, constant: Layout.downloadViewSpace), + downloadView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Layout.defaultInset), + downloadView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Layout.defaultInset) + ] + .forEach { $0.isActive = true } + + [stateLabel, downloadButtonsView, progressView, sizeLabel, releaseLabel, downloadedTimeLabel] + .forEach { + downloadView.addArrangedSubview($0) + } + + [ + progressView.heightAnchor.constraint(equalToConstant: Layout.progressViewHeight) + ] + .forEach { $0.isActive = true } + + [startButton, stopButton, pauseButton, dropButton] + .forEach { + downloadButtonsView.addArrangedSubview($0) + + $0.translatesAutoresizingMaskIntoConstraints = false + [ + $0.heightAnchor.constraint(equalToConstant: Layout.buttonHeight) + ] + .forEach { $0.isActive = true } + + $0.layer.cornerRadius = 4 + $0.setTitleColor(.black, for: .normal) + $0.contentMode = .scaleAspectFill + $0.backgroundColor = Palette.background + } + + showButton.addTarget( + self, + action: #selector(showButtonTapHandler), + for: .touchUpInside + ) + + startButton.addTarget( + self, + action: #selector(startButtonTapHandler), + for: .touchUpInside + ) + + stopButton.addTarget( + self, + action: #selector(stopButtonTapHandler), + for: .touchUpInside + ) + + pauseButton.addTarget( + self, + action: #selector(pauseButtonTapHandler), + for: .touchUpInside + ) + + dropButton.addTarget( + self, + action: #selector(dropButtonTapHandler), + for: .touchUpInside + ) + } + + private func setupRegionSubscriptions() { + regionViewModel.$region.sink { [weak self] regionInfo in + guard let regionInfo = regionInfo else { return } + self?.regionInfo = regionInfo + self?.configureContentInfo() + } + .store(in: &bag) + } + + private func configureContentInfo() { + guard let regionInfo = regionInfo else { return } + idLabel.text = "Id: \(regionInfo.id)" + nameLabel.text = "Name: \(regionInfo.name)" + countyLabel.text = "Country: \(regionInfo.country)" + citiesLabel.text = "Cities: [\(regionInfo.cities)]" + parentIdLabel.text = "Parent id: \(regionInfo.parentId)" + centerLabel.text = "Center: (\(regionInfo.center))" + stateLabel.text = "State: \(regionInfo.state)" + sizeLabel.text = "Size: \(regionInfo.size)" + releaseLabel.text = "Release time: \(regionInfo.realeseTime ?? "")" + downloadedTimeLabel.text = "Downloaded time: \(regionInfo.downloadedReleaseTime ?? "")" + + progressView.configureView(value: regionInfo.downloadProgress) + progressView.isHidden = regionInfo.state != "DOWNLOADING" && regionInfo.state != "PAUSED" + + showButton.setTitle("SHOW", for: .normal) + startButton.setTitle("START", for: .normal) + stopButton.setTitle("STOP", for: .normal) + pauseButton.setTitle("PAUSE", for: .normal) + dropButton.setTitle("DROP", for: .normal) + } + + @objc + private func showButtonTapHandler() { + dismiss(animated: true) + } + + @objc + private func startButtonTapHandler() { + guard !offlineManager.mayBeOutOfAvailableSpace(withRegionId: UInt(regionId ?? .zero)) else { return } + offlineManager.startDownload(withRegionId: UInt(regionId ?? .zero)) + } + + @objc + private func stopButtonTapHandler() { + offlineManager.stopDownload(withRegionId: UInt(regionId ?? .zero)) + } + + @objc + private func pauseButtonTapHandler() { + offlineManager.pauseDownload(withRegionId: UInt(regionId ?? .zero)) + } + + @objc + private func dropButtonTapHandler() { + offlineManager.drop(withRegionId: UInt(regionId ?? .zero)) + } + + // MARK: - Private properties + + private let regionView = UIStackView() + private let idLabel = UILabel() + private let nameLabel = UILabel() + private let countyLabel = UILabel() + private let citiesLabel = UILabel() + private let parentIdLabel = UILabel() + private let centerLabel = UILabel() + private let showButton = UIButton() + private let dividerView = UIView() + private let downloadView = UIStackView() + private let stateLabel = UILabel() + private let downloadButtonsView = UIStackView() + private let startButton = UIButton() + private let stopButton = UIButton() + private let pauseButton = UIButton() + private let dropButton = UIButton() + private let sizeLabel = UILabel() + private let releaseLabel = UILabel() + private let downloadedTimeLabel = UILabel() + private let progressView = ProgressView() + + private let regionViewModel = RegionViewModel() + + private var regionInfo: RegionInfo? + private var regionId: Int? + + private lazy var offlineCacheRegionListener = OfflineCacheRegionListener(regionViewModel: regionViewModel) + private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager + + private var bag = Set() + + // MARK: - Layout + + private enum Layout { + static let buttonHeight: CGFloat = 36.0 + static let defaultInset: CGFloat = 20.0 + static let downloadViewSpace: CGFloat = 60.0 + static let progressViewHeight = 6.0 + } +} diff --git a/mapkit-samples/MapOffline/RegionViewModel.swift b/mapkit-samples/MapOffline/RegionViewModel.swift new file mode 100644 index 0000000..d398955 --- /dev/null +++ b/mapkit-samples/MapOffline/RegionViewModel.swift @@ -0,0 +1,92 @@ +// +// SearchViewModel.swift +// MapSearch +// + +import Combine +import YandexMapsMobile + +class RegionViewModel { + // MARK: - Public properties + + @Published var region: RegionInfo? + + // MARK: - Public methods + + func setupSubscriptions() { + setupRegionSubscription() + } + + func setRegion(with regionId: Int) { + self.regionId = regionId + } + + // MARK: - Private methods + + private func setupRegionSubscription() { + $regionId + .map { [weak self] regionId in + let cacheRegion = self?.offlineManager.regions().first { $0.id == regionId ?? .zero } + return RegionInfo( + id: "\(cacheRegion?.id ?? .zero)", + name: cacheRegion?.name ?? "", + country: cacheRegion?.country ?? "", + center: "\(cacheRegion?.center.latitude ?? .zero) \(cacheRegion?.center.longitude ?? .zero)", + cities: self?.offlineManager + .getCitiesWithRegionId(UInt(regionId ?? .zero)).joined(separator: ", ") ?? "", + size: cacheRegion?.size.text ?? "", + downloadProgress: self?.offlineManager.getProgressWithRegionId(UInt(regionId ?? .zero)) ?? .zero, + parentId: "\(cacheRegion?.parentId ?? NSNumber())", + state: self?.getDownloadState() ?? "", + realeseTime: self?.convertTimeString(with: cacheRegion?.releaseTime ?? Date()) ?? "", + downloadedReleaseTime: self?.convertTimeString( + with: self?.offlineManager + .getDownloadedReleaseTime(withRegionId: (UInt(regionId ?? .zero))) ?? Date() + ) ?? "" + ) + } + .assign(to: \.region, on: self) + .store(in: &bag) + } + + private func getDownloadState() -> String { + switch self.offlineManager.getStateWithRegionId(UInt(regionId ?? .zero)) { + case .available: + return "AVAILABLE" + case .downloading: + return "DOWNLOADING" + case .paused: + return "PAUSED" + case .completed: + return "COMPLETED" + case .outdated: + return "OUTDATED" + case .unsupported: + return "UNSUPPORTED" + case .needUpdate: + return "NEED UPDATE" + default: + return "" + } + } + + private func convertTimeString(with date: Date) -> String? { + let formatter1 = DateFormatter() + formatter1.dateStyle = .short + return formatter1.string(from: date) + } + + // MARK: - Private properties + + private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager + + @Published private var regionId: Int? + + private var bag = Set() + + // MARK: - Private nesting + + private enum Const { + + } +} diff --git a/mapkit-samples/MapOffline/SearchViewModel.swift b/mapkit-samples/MapOffline/SearchViewModel.swift new file mode 100644 index 0000000..05bbc3c --- /dev/null +++ b/mapkit-samples/MapOffline/SearchViewModel.swift @@ -0,0 +1,167 @@ +// +// SearchViewModel.swift +// MapSearch +// + +import Combine +import YandexMapsMobile + +class SearchViewModel { + // MARK: - Public properties + + @Published var mapUIState: MapUIState! + @Published var regionListState = RegionListState.idle + + // MARK: - Public methods + + func setQueryText(with text: String?) { + query = text ?? String() + } + + func setVisibleRegion(with region: YMKVisibleRegion) { + visibleRegion = region + } + + func startSearch(with searchText: String? = nil) { + let text = searchText ?? query + + guard !text.isEmpty, + let visibleRegion else { + return + } + + submitSearch(with: text, geometry: YMKVisibleRegionUtils.toPolygon(with: visibleRegion)) + } + + func reset() { + stopSearch() + query = String() + } + + func stopSearch() { + searchSession?.cancel() + searchSession = nil + searchState = .idle + } + + func setupSubscriptions() { + setupVisibleRegionSubscription() + setupSearchSubscription() + setupMapUIStateSubscription() + } + + // MARK: - Private methods + + private func setupSearchSubscription() { + $debouncedVisibleRegion + .filter { [weak self] _ in + if case .success = self?.searchState { + return true + } else { + return false + } + } + .compactMap { $0 } + .sink { [weak self] visibleRegion in + guard let self = self else { + return + } + self.searchSession?.setSearchAreaWithArea(YMKVisibleRegionUtils.toPolygon(with: visibleRegion)) + self.searchSession?.resubmit(responseHandler: self.handleSearchSessionResponse) + self.searchState = .loading + self.zoomToSearchResult = false + } + .store(in: &bag) + } + + private func submitSearch(with query: String, geometry: YMKGeometry) { + searchSession?.cancel() + searchSession = searchManager.submit( + withText: query, + geometry: geometry, + searchOptions: Const.searchOptions, + responseHandler: handleSearchSessionResponse + ) + searchState = .loading + zoomToSearchResult = true + } + + private func setupVisibleRegionSubscription() { + $visibleRegion + .debounce(for: 1, scheduler: DispatchQueue.main) + .assign(to: \.debouncedVisibleRegion, on: self) + .store(in: &bag) + } + + private func setupMapUIStateSubscription() { + Publishers + .CombineLatest3( + $query, + $searchState, + $regionListState + ) + .map { query, searchState, regionListState in + MapUIState( + query: query, + searchState: searchState, + regionListState: regionListState + ) + } + .assign(to: \.mapUIState, on: self) + .store(in: &bag) + } + + private func handleSearchSessionResponse(response: YMKSearchResponse?, error: Error?) { + if let error = error { + onSearchError(error: error) + return + } + + guard let response = response, + let boundingBox = response.metadata.boundingBox else { + return + } + + let items = response.collection.children.compactMap { + if let point = $0.obj?.geometry.first?.point { + return SearchResponseItem(point: point, geoObject: $0.obj) + } else { + return nil + } + } + + searchState = SearchState.success( + items: items, + zoomToItems: zoomToSearchResult, + itemsBoundingBox: boundingBox + ) + } + + private func onSearchError(error: Error) { + searchState = .error + } + + // MARK: - Private properties + + private lazy var searchManager = YMKSearchFactory.instance().createSearchManager(with: .combined) + private var searchSession: YMKSearchSession? + private var zoomToSearchResult = false + + @Published private var visibleRegion: YMKVisibleRegion? + @Published private var debouncedVisibleRegion: YMKVisibleRegion? + + @Published private var query = String() + @Published private var searchState = SearchState.idle + + private var bag = Set() + + // MARK: - Private nesting + + private enum Const { + static let searchOptions: YMKSearchOptions = { + let options = YMKSearchOptions() + options.resultPageSize = 32 + return options + }() + } +} diff --git a/mapkit-samples/MapOffline/TitleCheckboxView.swift b/mapkit-samples/MapOffline/TitleCheckboxView.swift new file mode 100644 index 0000000..3a7ec31 --- /dev/null +++ b/mapkit-samples/MapOffline/TitleCheckboxView.swift @@ -0,0 +1,93 @@ +import UIKit + +protocol CheckBoxDelegate: AnyObject { + func tapCheckbox(isChecked: Bool, type: CheckBox.CheckboxType) +} + +class CheckBox: UIButton { + + enum CheckboxType { + case cellularNetwork + case autoEnabled + } + + weak var delegate: CheckBoxDelegate? + var type: CheckboxType? + + var isChecked: Bool = false { + didSet { + updateAppearance() + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + self.addTarget(self, action: #selector(toggleCheckBox), for: .touchUpInside) + updateAppearance() + self.backgroundColor = .clear + } + + @objc private func toggleCheckBox() { + isChecked.toggle() + } + + private func updateAppearance() { + if isChecked { + self.setImage(UIImage(named: "check"), for: .normal) + } else { + self.setImage(UIImage(named: "uncheck"), for: .normal) + } + delegate?.tapCheckbox(isChecked: isChecked, type: type ?? .autoEnabled) + } +} + +class TitleCheckBoxView: UIView { + + private let titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let checkBox: CheckBox = { + let checkBox = CheckBox() + checkBox.translatesAutoresizingMaskIntoConstraints = false + return checkBox + }() + + init(title: String) { + super.init(frame: .zero) + setupView() + titleLabel.text = title + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + addSubview(titleLabel) + addSubview(checkBox) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: topAnchor), + titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + checkBox.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + checkBox.centerYAnchor.constraint(equalTo: centerYAnchor), + checkBox.trailingAnchor.constraint(equalTo: trailingAnchor), + checkBox.widthAnchor.constraint(equalToConstant: 30), + checkBox.heightAnchor.constraint(equalToConstant: 30) + ]) + } +} diff --git a/mapkit-samples/MapRouting/RoutingViewModel.swift b/mapkit-samples/MapRouting/RoutingViewModel.swift index ef01714..34e6e14 100644 --- a/mapkit-samples/MapRouting/RoutingViewModel.swift +++ b/mapkit-samples/MapRouting/RoutingViewModel.swift @@ -43,7 +43,7 @@ final class RoutingViewModel { private func onRoutingPointsUpdated() { guard let image = UIImage(systemName: "circle.fill") else { return } - + placemarksCollection.clear() if routePoints.isEmpty { diff --git a/mapkit-samples/MapSearch/MapViewController.swift b/mapkit-samples/MapSearch/MapViewController.swift index 4c9ac27..29fdc6e 100644 --- a/mapkit-samples/MapSearch/MapViewController.swift +++ b/mapkit-samples/MapSearch/MapViewController.swift @@ -80,7 +80,7 @@ class MapViewController: UIViewController { items.forEach { item in let image = UIImage(systemName: "circle.circle.fill")! .withTintColor(view.tintColor) - + let placemark = map.mapObjects.addPlacemark() placemark.geometry = item.point placemark.setViewWithView(YRTViewProvider(uiView: UIImageView(image: image))) @@ -187,7 +187,7 @@ extension MapViewController: UITableViewDelegate { } } -fileprivate class ResultsTableController: UITableViewController { +private class ResultsTableController: UITableViewController { private let cellIdentifier = "cellIdentifier" @@ -198,9 +198,9 @@ fileprivate class ResultsTableController: UITableViewController { tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier) } - + // MARK: - UITableViewDataSource - + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } diff --git a/mapkit-samples/MapkitSamples.xcodeproj/project.pbxproj b/mapkit-samples/MapkitSamples.xcodeproj/project.pbxproj index dab7744..632d98d 100644 --- a/mapkit-samples/MapkitSamples.xcodeproj/project.pbxproj +++ b/mapkit-samples/MapkitSamples.xcodeproj/project.pbxproj @@ -7,9 +7,39 @@ objects = { /* Begin PBXBuildFile section */ + 53B0D1DF92B00901DB1664AA /* libPods-MapSearch.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3913542F05B97C229C728585 /* libPods-MapSearch.a */; }; 6F9044962DC7354CAB52D5C3 /* libPods-MapObjects.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBD9B0CAA5062026C5A403 /* libPods-MapObjects.a */; }; 71B0A398FF9ADDE1A9D2D1D6 /* libPods-MapInteraction.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C4817A7A6B8F04206D17C4A2 /* libPods-MapInteraction.a */; }; 755E718517F5F29B8796A442 /* libPods-MapWithPlacemark.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8420C1C67B164494687E0618 /* libPods-MapWithPlacemark.a */; }; + 781986CD2CCF930900A9C606 /* DeviceCheck.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F576CBF2B66F48B00D4106B /* DeviceCheck.framework */; }; + 781986D02CCF930900A9C606 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B2DCDED82A55AF1900FA8948 /* Assets.xcassets */; }; + 781986D12CCF930900A9C606 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B2DCDEDA2A55AF1900FA8948 /* LaunchScreen.storyboard */; }; + 781987192CCF961C00A9C606 /* DataMoveListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987092CCF960A00A9C606 /* DataMoveListener.swift */; }; + 7819871A2CCF961C00A9C606 /* MapCameraListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819870A2CCF960A00A9C606 /* MapCameraListener.swift */; }; + 7819871B2CCF961C00A9C606 /* MapUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819870B2CCF960A00A9C606 /* MapUIState.swift */; }; + 7819871C2CCF961C00A9C606 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819870C2CCF960A00A9C606 /* MapViewController.swift */; }; + 7819871D2CCF961C00A9C606 /* OfflineCacheRegionListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819870D2CCF960A00A9C606 /* OfflineCacheRegionListener.swift */; }; + 7819871E2CCF961C00A9C606 /* OfflineMapRegionListUpdatesListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819870E2CCF960A00A9C606 /* OfflineMapRegionListUpdatesListener.swift */; }; + 7819871F2CCF961C00A9C606 /* OptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819870F2CCF960A00A9C606 /* OptionsViewController.swift */; }; + 781987202CCF961C00A9C606 /* OptionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987102CCF960A00A9C606 /* OptionsViewModel.swift */; }; + 781987212CCF961C00A9C606 /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987112CCF960A00A9C606 /* ProgressView.swift */; }; + 781987222CCF961C00A9C606 /* RegionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987122CCF960A00A9C606 /* RegionInfo.swift */; }; + 781987232CCF961C00A9C606 /* RegionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987132CCF960A00A9C606 /* RegionItem.swift */; }; + 781987242CCF961C00A9C606 /* RegionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987142CCF960A00A9C606 /* RegionViewController.swift */; }; + 781987252CCF961C00A9C606 /* RegionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987152CCF960A00A9C606 /* RegionViewModel.swift */; }; + 781987262CCF961C00A9C606 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987162CCF960A00A9C606 /* SearchViewModel.swift */; }; + 781987272CCF961C00A9C606 /* TitleCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781987172CCF960A00A9C606 /* TitleCheckboxView.swift */; }; + 781987282CCF964300A9C606 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2DCDECF2A55AF1800FA8948 /* AppDelegate.swift */; }; + 781987292CCF964400A9C606 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2DCDED12A55AF1800FA8948 /* SceneDelegate.swift */; }; + 7819872A2CCF964400A9C606 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25B31AE2A57054D00F3455F /* AlertPresenter.swift */; }; + 7819872B2CCF964400A9C606 /* ApiKeyStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25B31AC2A56BFC500F3455F /* ApiKeyStorage.swift */; }; + 7819872C2CCF964400A9C606 /* Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D80E182A7D2F1F00B138EF /* Palette.swift */; }; + 7819872E2CCF97AF00A9C606 /* UserStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819872D2CCF97AF00A9C606 /* UserStorage.swift */; }; + 7819872F2CCF97AF00A9C606 /* UserStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819872D2CCF97AF00A9C606 /* UserStorage.swift */; }; + 781987302CCF97AF00A9C606 /* UserStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819872D2CCF97AF00A9C606 /* UserStorage.swift */; }; + 781987312CCF97AF00A9C606 /* UserStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819872D2CCF97AF00A9C606 /* UserStorage.swift */; }; + 781987322CCF97AF00A9C606 /* UserStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819872D2CCF97AF00A9C606 /* UserStorage.swift */; }; + 781987332CCF97AF00A9C606 /* UserStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7819872D2CCF97AF00A9C606 /* UserStorage.swift */; }; 9F576CC02B66F48B00D4106B /* DeviceCheck.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F576CBF2B66F48B00D4106B /* DeviceCheck.framework */; }; 9F576CC12B66F49300D4106B /* DeviceCheck.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F576CBF2B66F48B00D4106B /* DeviceCheck.framework */; }; 9F576CC22B66F49C00D4106B /* DeviceCheck.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F576CBF2B66F48B00D4106B /* DeviceCheck.framework */; }; @@ -75,17 +105,38 @@ B2D8FE992A77F50C009117A0 /* GeometryProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D8FE982A77F50C009117A0 /* GeometryProvider.swift */; }; B2D8FE9B2A77FC2A009117A0 /* ClusterListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D8FE9A2A77FC2A009117A0 /* ClusterListener.swift */; }; B2D8FE9F2A780296009117A0 /* GeometryVisibilityVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2D8FE9E2A780296009117A0 /* GeometryVisibilityVisitor.swift */; }; + B436FEDAC12AD4ED88AC626C /* libPods-MapOffline.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C54787854ABEEFBAA30D988 /* libPods-MapOffline.a */; }; B6277A557F259E771B4B7A9C /* libPods-MapRouting.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F837C7C4EC92C1C612520C7 /* libPods-MapRouting.a */; }; - D7A4DE2081C5DD7CC437DEC8 /* libPods-MapSearch.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F0CB421A75B037683C8DE535 /* libPods-MapSearch.a */; }; + D7A4DE2081C5DD7CC437DEC8 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 1FEBBECC65E06B4ADA6FEB04 /* Pods-MapInteraction.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapInteraction.release.xcconfig"; path = "Target Support Files/Pods-MapInteraction/Pods-MapInteraction.release.xcconfig"; sourceTree = ""; }; 21AB4751AEEFFEE83B858758 /* Pods-MapRouting.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapRouting.release.xcconfig"; path = "Target Support Files/Pods-MapRouting/Pods-MapRouting.release.xcconfig"; sourceTree = ""; }; + 248F9FE14A71C192B35ABDF8 /* Pods-MapOffline.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapOffline.release.xcconfig"; path = "Target Support Files/Pods-MapOffline/Pods-MapOffline.release.xcconfig"; sourceTree = ""; }; 29B7FDA28317D6E47B8F4425 /* Pods-MapObjects.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapObjects.release.xcconfig"; path = "Target Support Files/Pods-MapObjects/Pods-MapObjects.release.xcconfig"; sourceTree = ""; }; + 3913542F05B97C229C728585 /* libPods-MapSearch.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapSearch.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4C54787854ABEEFBAA30D988 /* libPods-MapOffline.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapOffline.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 5C822D66C99F665A68E1309F /* Pods-MapRouting.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapRouting.debug.xcconfig"; path = "Target Support Files/Pods-MapRouting/Pods-MapRouting.debug.xcconfig"; sourceTree = ""; }; 5F837C7C4EC92C1C612520C7 /* libPods-MapRouting.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapRouting.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 61AA67B1BD26F1C3452843FE /* Pods-MapWithPlacemark.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapWithPlacemark.debug.xcconfig"; path = "Target Support Files/Pods-MapWithPlacemark/Pods-MapWithPlacemark.debug.xcconfig"; sourceTree = ""; }; + 781986D72CCF930900A9C606 /* MapOffline.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MapOffline.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 781987092CCF960A00A9C606 /* DataMoveListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataMoveListener.swift; sourceTree = ""; }; + 7819870A2CCF960A00A9C606 /* MapCameraListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCameraListener.swift; sourceTree = ""; }; + 7819870B2CCF960A00A9C606 /* MapUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUIState.swift; sourceTree = ""; }; + 7819870C2CCF960A00A9C606 /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; }; + 7819870D2CCF960A00A9C606 /* OfflineCacheRegionListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineCacheRegionListener.swift; sourceTree = ""; }; + 7819870E2CCF960A00A9C606 /* OfflineMapRegionListUpdatesListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineMapRegionListUpdatesListener.swift; sourceTree = ""; }; + 7819870F2CCF960A00A9C606 /* OptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsViewController.swift; sourceTree = ""; }; + 781987102CCF960A00A9C606 /* OptionsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsViewModel.swift; sourceTree = ""; }; + 781987112CCF960A00A9C606 /* ProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; + 781987122CCF960A00A9C606 /* RegionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionInfo.swift; sourceTree = ""; }; + 781987132CCF960A00A9C606 /* RegionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionItem.swift; sourceTree = ""; }; + 781987142CCF960A00A9C606 /* RegionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionViewController.swift; sourceTree = ""; }; + 781987152CCF960A00A9C606 /* RegionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionViewModel.swift; sourceTree = ""; }; + 781987162CCF960A00A9C606 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; + 781987172CCF960A00A9C606 /* TitleCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleCheckboxView.swift; sourceTree = ""; }; + 7819872D2CCF97AF00A9C606 /* UserStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserStorage.swift; sourceTree = ""; }; 8420C1C67B164494687E0618 /* libPods-MapWithPlacemark.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapWithPlacemark.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 98538E3305B7A48793C98835 /* Pods-MapWithPlacemark.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapWithPlacemark.release.xcconfig"; path = "Target Support Files/Pods-MapWithPlacemark/Pods-MapWithPlacemark.release.xcconfig"; sourceTree = ""; }; 9F576CBF2B66F48B00D4106B /* DeviceCheck.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceCheck.framework; path = System/Library/Frameworks/DeviceCheck.framework; sourceTree = SDKROOT; }; @@ -130,13 +181,22 @@ C4817A7A6B8F04206D17C4A2 /* libPods-MapInteraction.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapInteraction.a"; sourceTree = BUILT_PRODUCTS_DIR; }; D681837822AB96ADC487D42D /* Pods-MapObjects.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapObjects.debug.xcconfig"; path = "Target Support Files/Pods-MapObjects/Pods-MapObjects.debug.xcconfig"; sourceTree = ""; }; EF6D4EA204092649FB705844 /* Pods-MapInteraction.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapInteraction.debug.xcconfig"; path = "Target Support Files/Pods-MapInteraction/Pods-MapInteraction.debug.xcconfig"; sourceTree = ""; }; - F0CB421A75B037683C8DE535 /* libPods-MapSearch.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapSearch.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F1C9187889D8537D84670CEE /* Pods-MapOffline.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapOffline.debug.xcconfig"; path = "Target Support Files/Pods-MapOffline/Pods-MapOffline.debug.xcconfig"; sourceTree = ""; }; F5EBD9B0CAA5062026C5A403 /* libPods-MapObjects.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-MapObjects.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F60BA6C275F6F0D5514DC20D /* Pods-MapSearch.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapSearch.debug.xcconfig"; path = "Target Support Files/Pods-MapSearch/Pods-MapSearch.debug.xcconfig"; sourceTree = ""; }; F8925CF3BF185FA1C24E8BAC /* Pods-MapSearch.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MapSearch.release.xcconfig"; path = "Target Support Files/Pods-MapSearch/Pods-MapSearch.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 781986CC2CCF930900A9C606 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 781986CD2CCF930900A9C606 /* DeviceCheck.framework in Frameworks */, + B436FEDAC12AD4ED88AC626C /* libPods-MapOffline.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B209ADF32A5C359400399A79 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -160,7 +220,8 @@ buildActionMask = 2147483647; files = ( 9F576CC32B66F4A400D4106B /* DeviceCheck.framework in Frameworks */, - D7A4DE2081C5DD7CC437DEC8 /* libPods-MapSearch.a in Frameworks */, + D7A4DE2081C5DD7CC437DEC8 /* (null) in Frameworks */, + 53B0D1DF92B00901DB1664AA /* libPods-MapSearch.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -198,6 +259,8 @@ F8925CF3BF185FA1C24E8BAC /* Pods-MapSearch.release.xcconfig */, 61AA67B1BD26F1C3452843FE /* Pods-MapWithPlacemark.debug.xcconfig */, 98538E3305B7A48793C98835 /* Pods-MapWithPlacemark.release.xcconfig */, + F1C9187889D8537D84670CEE /* Pods-MapOffline.debug.xcconfig */, + 248F9FE14A71C192B35ABDF8 /* Pods-MapOffline.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -209,12 +272,35 @@ C4817A7A6B8F04206D17C4A2 /* libPods-MapInteraction.a */, F5EBD9B0CAA5062026C5A403 /* libPods-MapObjects.a */, 5F837C7C4EC92C1C612520C7 /* libPods-MapRouting.a */, - F0CB421A75B037683C8DE535 /* libPods-MapSearch.a */, 8420C1C67B164494687E0618 /* libPods-MapWithPlacemark.a */, + 4C54787854ABEEFBAA30D988 /* libPods-MapOffline.a */, + 3913542F05B97C229C728585 /* libPods-MapSearch.a */, ); name = Frameworks; sourceTree = ""; }; + 781987182CCF960A00A9C606 /* MapOffline */ = { + isa = PBXGroup; + children = ( + 781987092CCF960A00A9C606 /* DataMoveListener.swift */, + 7819870A2CCF960A00A9C606 /* MapCameraListener.swift */, + 7819870B2CCF960A00A9C606 /* MapUIState.swift */, + 7819870C2CCF960A00A9C606 /* MapViewController.swift */, + 7819870D2CCF960A00A9C606 /* OfflineCacheRegionListener.swift */, + 7819870E2CCF960A00A9C606 /* OfflineMapRegionListUpdatesListener.swift */, + 7819870F2CCF960A00A9C606 /* OptionsViewController.swift */, + 781987102CCF960A00A9C606 /* OptionsViewModel.swift */, + 781987112CCF960A00A9C606 /* ProgressView.swift */, + 781987122CCF960A00A9C606 /* RegionInfo.swift */, + 781987132CCF960A00A9C606 /* RegionItem.swift */, + 781987142CCF960A00A9C606 /* RegionViewController.swift */, + 781987152CCF960A00A9C606 /* RegionViewModel.swift */, + 781987162CCF960A00A9C606 /* SearchViewModel.swift */, + 781987172CCF960A00A9C606 /* TitleCheckboxView.swift */, + ); + path = MapOffline; + sourceTree = ""; + }; B209ADF72A5C359400399A79 /* MapInteraction */ = { isa = PBXGroup; children = ( @@ -250,6 +336,7 @@ B25B31B02A5706E600F3455F /* Utils */ = { isa = PBXGroup; children = ( + 7819872D2CCF97AF00A9C606 /* UserStorage.swift */, B25B31AE2A57054D00F3455F /* AlertPresenter.swift */, B25B31AC2A56BFC500F3455F /* ApiKeyStorage.swift */, B2D80E182A7D2F1F00B138EF /* Palette.swift */, @@ -317,6 +404,7 @@ B2DCDEC32A55AF1800FA8948 = { isa = PBXGroup; children = ( + 781987182CCF960A00A9C606 /* MapOffline */, B2D011C32A73CEFB0011E7EC /* Common */, B25B31B82A570C0400F3455F /* MapWithPlacemark */, B209ADF72A5C359400399A79 /* MapInteraction */, @@ -337,6 +425,7 @@ B2B99A8C2A77E91700861554 /* MapObjects.app */, B257C50B2A8A3D4D00602FEF /* MapSearch.app */, B22C84FB2A9CD3EF0040B0AF /* MapRouting.app */, + 781986D72CCF930900A9C606 /* MapOffline.app */, ); name = Products; sourceTree = ""; @@ -344,6 +433,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 781986BE2CCF930900A9C606 /* MapOffline */ = { + isa = PBXNativeTarget; + buildConfigurationList = 781986D42CCF930900A9C606 /* Build configuration list for PBXNativeTarget "MapOffline" */; + buildPhases = ( + 781986BF2CCF930900A9C606 /* [CP] Check Pods Manifest.lock */, + 781986CC2CCF930900A9C606 /* Frameworks */, + 781986CF2CCF930900A9C606 /* Resources */, + 781986D22CCF930900A9C606 /* Run Swiftlint */, + 781986D32CCF930900A9C606 /* [CP] Copy Pods Resources */, + 781986F82CCF959200A9C606 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MapOffline; + productName = MapSearch; + productReference = 781986D72CCF930900A9C606 /* MapOffline.app */; + productType = "com.apple.product-type.application"; + }; B209ADF52A5C359400399A79 /* MapInteraction */ = { isa = PBXNativeTarget; buildConfigurationList = B209AE092A5C359500399A79 /* Build configuration list for PBXNativeTarget "MapInteraction" */; @@ -493,11 +602,21 @@ B2B99A8B2A77E91700861554 /* MapObjects */, B257C50A2A8A3D4D00602FEF /* MapSearch */, B22C84FA2A9CD3EF0040B0AF /* MapRouting */, + 781986BE2CCF930900A9C606 /* MapOffline */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 781986CF2CCF930900A9C606 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 781986D02CCF930900A9C606 /* Assets.xcassets in Resources */, + 781986D12CCF930900A9C606 /* LaunchScreen.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B209ADF42A5C359400399A79 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -642,6 +761,64 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MapWithPlacemark/Pods-MapWithPlacemark-resources.sh\"\n"; showEnvVarsInLog = 0; }; + 781986BF2CCF930900A9C606 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-MapOffline-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 781986D22CCF930900A9C606 /* Run Swiftlint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Swiftlint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint lint --autocorrect --format\n swiftlint lint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; + 781986D32CCF930900A9C606 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MapOffline/Pods-MapOffline-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-MapOffline/Pods-MapOffline-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-MapOffline/Pods-MapOffline-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 802639843E07688A5CCD4DA0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -821,6 +998,34 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 781986F82CCF959200A9C606 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 781987282CCF964300A9C606 /* AppDelegate.swift in Sources */, + 781987292CCF964400A9C606 /* SceneDelegate.swift in Sources */, + 7819872A2CCF964400A9C606 /* AlertPresenter.swift in Sources */, + 7819872B2CCF964400A9C606 /* ApiKeyStorage.swift in Sources */, + 7819872C2CCF964400A9C606 /* Palette.swift in Sources */, + 781987192CCF961C00A9C606 /* DataMoveListener.swift in Sources */, + 7819871A2CCF961C00A9C606 /* MapCameraListener.swift in Sources */, + 7819871B2CCF961C00A9C606 /* MapUIState.swift in Sources */, + 7819871C2CCF961C00A9C606 /* MapViewController.swift in Sources */, + 7819871D2CCF961C00A9C606 /* OfflineCacheRegionListener.swift in Sources */, + 7819871E2CCF961C00A9C606 /* OfflineMapRegionListUpdatesListener.swift in Sources */, + 781987332CCF97AF00A9C606 /* UserStorage.swift in Sources */, + 7819871F2CCF961C00A9C606 /* OptionsViewController.swift in Sources */, + 781987202CCF961C00A9C606 /* OptionsViewModel.swift in Sources */, + 781987212CCF961C00A9C606 /* ProgressView.swift in Sources */, + 781987222CCF961C00A9C606 /* RegionInfo.swift in Sources */, + 781987232CCF961C00A9C606 /* RegionItem.swift in Sources */, + 781987242CCF961C00A9C606 /* RegionViewController.swift in Sources */, + 781987252CCF961C00A9C606 /* RegionViewModel.swift in Sources */, + 781987262CCF961C00A9C606 /* SearchViewModel.swift in Sources */, + 781987272CCF961C00A9C606 /* TitleCheckboxView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B209ADF22A5C359400399A79 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -829,6 +1034,7 @@ B209AE0D2A5C35BD00399A79 /* ApiKeyStorage.swift in Sources */, B209AE0B2A5C35B500399A79 /* SceneDelegate.swift in Sources */, B2D80E1B2A7D304A00B138EF /* Palette.swift in Sources */, + 7819872F2CCF97AF00A9C606 /* UserStorage.swift in Sources */, B209AE112A5C387100399A79 /* MapViewController.swift in Sources */, B209AE0C2A5C35B900399A79 /* AlertPresenter.swift in Sources */, ); @@ -842,6 +1048,7 @@ B22C85142A9CD4290040B0AF /* SceneDelegate.swift in Sources */, B22C85122A9CD4290040B0AF /* AppDelegate.swift in Sources */, B22C85172A9CD6E60040B0AF /* MapInputListener.swift in Sources */, + 781987322CCF97AF00A9C606 /* UserStorage.swift in Sources */, B22C85112A9CD4290040B0AF /* Palette.swift in Sources */, B22C85192A9CD71E0040B0AF /* RoutingViewModel.swift in Sources */, B22C85152A9CD4290040B0AF /* ApiKeyStorage.swift in Sources */, @@ -862,6 +1069,7 @@ B2B6F75A2A94EE5300E90924 /* MapCameraListener.swift in Sources */, B257C5222A8A3D6400602FEF /* Palette.swift in Sources */, B2B6F75E2A9746E300E90924 /* MapObjectTapListener.swift in Sources */, + 781987312CCF97AF00A9C606 /* UserStorage.swift in Sources */, B257C51F2A8A3D6400602FEF /* SceneDelegate.swift in Sources */, B257C52E2A8A554100602FEF /* SuggestItem.swift in Sources */, B257C5232A8A3D6400602FEF /* ApiKeyStorage.swift in Sources */, @@ -876,6 +1084,7 @@ B23895652A5846E200AB961F /* MapViewController.swift in Sources */, B25B31CE2A570E0100F3455F /* SceneDelegate.swift in Sources */, B2D80E1A2A7D304A00B138EF /* Palette.swift in Sources */, + 7819872E2CCF97AF00A9C606 /* UserStorage.swift in Sources */, B25B31CB2A570D8E00F3455F /* AlertPresenter.swift in Sources */, B25B31CC2A570D9100F3455F /* ApiKeyStorage.swift in Sources */, ); @@ -899,6 +1108,7 @@ B2B99AA22A77E94D00861554 /* ApiKeyStorage.swift in Sources */, B2B936B32A7BC5F700412F38 /* PlacemarkUserData.swift in Sources */, B2D8FE9F2A780296009117A0 /* GeometryVisibilityVisitor.swift in Sources */, + 781987302CCF97AF00A9C606 /* UserStorage.swift in Sources */, B2B99AA42A77E94D00861554 /* AlertPresenter.swift in Sources */, B2D8FE952A77F305009117A0 /* MapObjectTapListener.swift in Sources */, B2D8FE972A77F38D009117A0 /* PolylineColorPalette.swift in Sources */, @@ -919,6 +1129,28 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 781986D52CCF930900A9C606 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F1C9187889D8537D84670CEE /* Pods-MapOffline.debug.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = Common/Resources/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.yandex.maps.MapkitSamples; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 781986D62CCF930900A9C606 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 248F9FE14A71C192B35ABDF8 /* Pods-MapOffline.release.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = Common/Resources/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.yandex.maps.MapkitSamples; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; B209AE072A5C359500399A79 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EF6D4EA204092649FB705844 /* Pods-MapInteraction.debug.xcconfig */; @@ -1168,6 +1400,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 781986D42CCF930900A9C606 /* Build configuration list for PBXNativeTarget "MapOffline" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 781986D52CCF930900A9C606 /* Debug */, + 781986D62CCF930900A9C606 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; B209AE092A5C359500399A79 /* Build configuration list for PBXNativeTarget "MapInteraction" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist b/mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..e410e80 --- /dev/null +++ b/mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,44 @@ + + + + + SchemeUserState + + MapInteraction.xcscheme_^#shared#^_ + + orderHint + 9 + + MapObjects.xcscheme_^#shared#^_ + + orderHint + 11 + + MapOffline.xcscheme_^#shared#^_ + + orderHint + 10 + + MapRouting.xcscheme_^#shared#^_ + + orderHint + 7 + + MapSearch copy.xcscheme_^#shared#^_ + + orderHint + 11 + + MapSearch.xcscheme_^#shared#^_ + + orderHint + 12 + + MapWithPlacemark.xcscheme_^#shared#^_ + + orderHint + 8 + + + + diff --git a/mapkit-samples/Podfile b/mapkit-samples/Podfile index 3302ed9..ca5f33c 100644 --- a/mapkit-samples/Podfile +++ b/mapkit-samples/Podfile @@ -23,3 +23,7 @@ end target 'MapRouting' do mapkit_pod end + +target 'MapOffline' do + mapkit_pod +end From b9f0588cf1e2a046dbe40c986470e8b037813023 Mon Sep 17 00:00:00 2001 From: Rapinchuk Igor Date: Mon, 4 Nov 2024 22:21:21 +0300 Subject: [PATCH 2/5] bug fix --- mapkit-samples/MapOffline/RegionViewModel.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mapkit-samples/MapOffline/RegionViewModel.swift b/mapkit-samples/MapOffline/RegionViewModel.swift index d398955..8a5515b 100644 --- a/mapkit-samples/MapOffline/RegionViewModel.swift +++ b/mapkit-samples/MapOffline/RegionViewModel.swift @@ -25,9 +25,9 @@ class RegionViewModel { private func setupRegionSubscription() { $regionId - .map { [weak self] regionId in + .sink { [weak self ] regionId in let cacheRegion = self?.offlineManager.regions().first { $0.id == regionId ?? .zero } - return RegionInfo( + self?.region = RegionInfo( id: "\(cacheRegion?.id ?? .zero)", name: cacheRegion?.name ?? "", country: cacheRegion?.country ?? "", @@ -37,7 +37,7 @@ class RegionViewModel { size: cacheRegion?.size.text ?? "", downloadProgress: self?.offlineManager.getProgressWithRegionId(UInt(regionId ?? .zero)) ?? .zero, parentId: "\(cacheRegion?.parentId ?? NSNumber())", - state: self?.getDownloadState() ?? "", + state: self?.getDownloadState(regionId: regionId ?? .zero) ?? "", realeseTime: self?.convertTimeString(with: cacheRegion?.releaseTime ?? Date()) ?? "", downloadedReleaseTime: self?.convertTimeString( with: self?.offlineManager @@ -45,12 +45,11 @@ class RegionViewModel { ) ?? "" ) } - .assign(to: \.region, on: self) .store(in: &bag) } - private func getDownloadState() -> String { - switch self.offlineManager.getStateWithRegionId(UInt(regionId ?? .zero)) { + private func getDownloadState(regionId: Int) -> String { + switch self.offlineManager.getStateWithRegionId(UInt(regionId)) { case .available: return "AVAILABLE" case .downloading: From 599b34dd08f409c135702fc7c7594445b364a089 Mon Sep 17 00:00:00 2001 From: Rapinchuk Igor Date: Mon, 4 Nov 2024 23:16:28 +0300 Subject: [PATCH 3/5] git ignore --- .../xcschemes/xcschememanagement.plist | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist b/mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index e410e80..0000000 --- a/mapkit-samples/MapkitSamples.xcodeproj/xcuserdata/igor.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,44 +0,0 @@ - - - - - SchemeUserState - - MapInteraction.xcscheme_^#shared#^_ - - orderHint - 9 - - MapObjects.xcscheme_^#shared#^_ - - orderHint - 11 - - MapOffline.xcscheme_^#shared#^_ - - orderHint - 10 - - MapRouting.xcscheme_^#shared#^_ - - orderHint - 7 - - MapSearch copy.xcscheme_^#shared#^_ - - orderHint - 11 - - MapSearch.xcscheme_^#shared#^_ - - orderHint - 12 - - MapWithPlacemark.xcscheme_^#shared#^_ - - orderHint - 8 - - - - From c85d305e3c2f5f9e107310f69ac8097a52253413 Mon Sep 17 00:00:00 2001 From: Rapinchuk_IA Date: Thu, 14 Nov 2024 03:20:32 +0300 Subject: [PATCH 4/5] Work with bug download maps --- .../MapOffline/MapViewController.swift | 11 +++---- .../MapOffline/RegionViewController.swift | 33 ++++++++++++++++--- .../MapOffline/RegionViewModel.swift | 6 +++- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/mapkit-samples/MapOffline/MapViewController.swift b/mapkit-samples/MapOffline/MapViewController.swift index 36bfb4e..d03fa84 100644 --- a/mapkit-samples/MapOffline/MapViewController.swift +++ b/mapkit-samples/MapOffline/MapViewController.swift @@ -109,6 +109,7 @@ class MapViewController: UIViewController { @objc private func optionsButtonTapHandler() { + let optionsViewController = OptionsViewController() present(optionsViewController, animated: true) } @@ -155,8 +156,6 @@ class MapViewController: UIViewController { private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager private lazy var resultsTableController = ResultsTableController() private lazy var searchBarController = UISearchController(searchResultsController: resultsTableController) - private lazy var regionViewController = RegionViewController() - private lazy var optionsViewController = OptionsViewController() private let searchViewModel = SearchViewModel() private var bag = Set() @@ -214,7 +213,7 @@ extension MapViewController: UISearchResultsUpdating, UISearchControllerDelegate } private func showRegionInfo(regionId: Int) { - regionViewController.setRegion(with: regionId) + let regionViewController = RegionViewController(regionId: regionId) present(regionViewController, animated: true) } @@ -222,10 +221,10 @@ extension MapViewController: UISearchResultsUpdating, UISearchControllerDelegate switch regionListState { case .success: resultsTableController.items = offlineManager.regions() - .map { region in + .map { [weak self] region in RegionItem(model: region) { - self.searchViewModel.startSearch(with: region.name) - self.showRegionInfo(regionId: Int(region.id)) + self?.searchViewModel.startSearch(with: region.name) + self?.showRegionInfo(regionId: Int(region.id)) } } .filter { $0.model.name.contains(query) } diff --git a/mapkit-samples/MapOffline/RegionViewController.swift b/mapkit-samples/MapOffline/RegionViewController.swift index a5aee0a..7376628 100644 --- a/mapkit-samples/MapOffline/RegionViewController.swift +++ b/mapkit-samples/MapOffline/RegionViewController.swift @@ -12,9 +12,17 @@ class RegionViewController: UIViewController { // MARK: - Public methods + convenience init(regionId: Int) { + self.init() + self.regionId = regionId + regionViewModel.setRegion(with: regionId) + } + override func viewDidLoad() { super.viewDidLoad() + offlineCacheRegionListener = OfflineCacheRegionListener(regionViewModel: regionViewModel) + guard let offlineCacheRegionListener = offlineCacheRegionListener else { return } offlineManager.addRegionListener(with: offlineCacheRegionListener) regionViewModel.setupSubscriptions() @@ -23,9 +31,16 @@ class RegionViewController: UIViewController { view.backgroundColor = .white } - func setRegion(with regionId: Int) { - self.regionId = regionId - regionViewModel.setRegion(with: regionId) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + guard let offlineCacheRegionListener = offlineCacheRegionListener else { return } + self.offlineCacheRegionListener = nil + offlineManager.removeRegionListener(with: offlineCacheRegionListener) + bag.removeAll() } // MARK: - Private methods @@ -41,6 +56,7 @@ class RegionViewController: UIViewController { idLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) nameLabel.textColor = .black nameLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold) + nameLabel.numberOfLines = .zero countyLabel.textColor = .black citiesLabel.textColor = .black citiesLabel.numberOfLines = .zero @@ -89,7 +105,8 @@ class RegionViewController: UIViewController { showButton.leadingAnchor.constraint(equalTo: regionView.trailingAnchor, constant: Layout.defaultInset), showButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Layout.defaultInset), showButton.centerYAnchor.constraint(equalTo: centerLabel.centerYAnchor), - showButton.heightAnchor.constraint(equalToConstant: Layout.buttonHeight) + showButton.heightAnchor.constraint(equalToConstant: Layout.buttonHeight), + showButton.widthAnchor.constraint(equalToConstant: Layout.buttonWidth) ] .forEach { $0.isActive = true } @@ -193,27 +210,32 @@ class RegionViewController: UIViewController { @objc private func showButtonTapHandler() { + guard !isModalInPresentation else { return } dismiss(animated: true) } @objc private func startButtonTapHandler() { guard !offlineManager.mayBeOutOfAvailableSpace(withRegionId: UInt(regionId ?? .zero)) else { return } + isModalInPresentation = true offlineManager.startDownload(withRegionId: UInt(regionId ?? .zero)) } @objc private func stopButtonTapHandler() { + isModalInPresentation = false offlineManager.stopDownload(withRegionId: UInt(regionId ?? .zero)) } @objc private func pauseButtonTapHandler() { + isModalInPresentation = false offlineManager.pauseDownload(withRegionId: UInt(regionId ?? .zero)) } @objc private func dropButtonTapHandler() { + isModalInPresentation = false offlineManager.drop(withRegionId: UInt(regionId ?? .zero)) } @@ -245,7 +267,7 @@ class RegionViewController: UIViewController { private var regionInfo: RegionInfo? private var regionId: Int? - private lazy var offlineCacheRegionListener = OfflineCacheRegionListener(regionViewModel: regionViewModel) + private var offlineCacheRegionListener: OfflineCacheRegionListener? private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager private var bag = Set() @@ -254,6 +276,7 @@ class RegionViewController: UIViewController { private enum Layout { static let buttonHeight: CGFloat = 36.0 + static let buttonWidth: CGFloat = 80.0 static let defaultInset: CGFloat = 20.0 static let downloadViewSpace: CGFloat = 60.0 static let progressViewHeight = 6.0 diff --git a/mapkit-samples/MapOffline/RegionViewModel.swift b/mapkit-samples/MapOffline/RegionViewModel.swift index 8a5515b..1723b5a 100644 --- a/mapkit-samples/MapOffline/RegionViewModel.swift +++ b/mapkit-samples/MapOffline/RegionViewModel.swift @@ -21,11 +21,15 @@ class RegionViewModel { self.regionId = regionId } + deinit { + bag.removeAll() + } + // MARK: - Private methods private func setupRegionSubscription() { $regionId - .sink { [weak self ] regionId in + .sink { [weak self] regionId in let cacheRegion = self?.offlineManager.regions().first { $0.id == regionId ?? .zero } self?.region = RegionInfo( id: "\(cacheRegion?.id ?? .zero)", From bd857ae8044df8be1df155bde4338a78eb3c6397 Mon Sep 17 00:00:00 2001 From: Rapinchuk_IA Date: Thu, 14 Nov 2024 11:37:31 +0300 Subject: [PATCH 5/5] Set opened region id --- mapkit-samples/MapOffline/RegionViewController.swift | 6 +----- mapkit-samples/MapOffline/RegionViewModel.swift | 6 ++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mapkit-samples/MapOffline/RegionViewController.swift b/mapkit-samples/MapOffline/RegionViewController.swift index 7376628..586cfff 100644 --- a/mapkit-samples/MapOffline/RegionViewController.swift +++ b/mapkit-samples/MapOffline/RegionViewController.swift @@ -16,6 +16,7 @@ class RegionViewController: UIViewController { self.init() self.regionId = regionId regionViewModel.setRegion(with: regionId) + regionViewModel.setRegionOpened(with: regionId) } override func viewDidLoad() { @@ -210,32 +211,27 @@ class RegionViewController: UIViewController { @objc private func showButtonTapHandler() { - guard !isModalInPresentation else { return } dismiss(animated: true) } @objc private func startButtonTapHandler() { guard !offlineManager.mayBeOutOfAvailableSpace(withRegionId: UInt(regionId ?? .zero)) else { return } - isModalInPresentation = true offlineManager.startDownload(withRegionId: UInt(regionId ?? .zero)) } @objc private func stopButtonTapHandler() { - isModalInPresentation = false offlineManager.stopDownload(withRegionId: UInt(regionId ?? .zero)) } @objc private func pauseButtonTapHandler() { - isModalInPresentation = false offlineManager.pauseDownload(withRegionId: UInt(regionId ?? .zero)) } @objc private func dropButtonTapHandler() { - isModalInPresentation = false offlineManager.drop(withRegionId: UInt(regionId ?? .zero)) } diff --git a/mapkit-samples/MapOffline/RegionViewModel.swift b/mapkit-samples/MapOffline/RegionViewModel.swift index 1723b5a..6f31641 100644 --- a/mapkit-samples/MapOffline/RegionViewModel.swift +++ b/mapkit-samples/MapOffline/RegionViewModel.swift @@ -21,6 +21,10 @@ class RegionViewModel { self.regionId = regionId } + func setRegionOpened(with regionId: Int) { + self.regionIdOpened = regionId + } + deinit { bag.removeAll() } @@ -30,6 +34,7 @@ class RegionViewModel { private func setupRegionSubscription() { $regionId .sink { [weak self] regionId in + guard self?.regionIdOpened == regionId else { return } let cacheRegion = self?.offlineManager.regions().first { $0.id == regionId ?? .zero } self?.region = RegionInfo( id: "\(cacheRegion?.id ?? .zero)", @@ -84,6 +89,7 @@ class RegionViewModel { private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager @Published private var regionId: Int? + private var regionIdOpened: Int? private var bag = Set()