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 0000000..34b8582 Binary files /dev/null and b/mapkit-samples/Common/Resources/Assets.xcassets/check.imageset/approved.png differ 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 0000000..7b17a17 Binary files /dev/null and b/mapkit-samples/Common/Resources/Assets.xcassets/uncheck.imageset/unchecked.png differ diff --git a/mapkit-samples/Common/Utils/UserStorage.swift b/mapkit-samples/Common/Utils/UserStorage.swift new file mode 100644 index 0000000..68deaf1 --- /dev/null +++ b/mapkit-samples/Common/Utils/UserStorage.swift @@ -0,0 +1,26 @@ +import Foundation + +final class UserStorage { + private enum Keys { + static let isCellularNetwork = "Is cellular network" + static let isAutoUpdate = "Is auto update" + } + + static var isCellularNetwork: Bool { + get { + return UserDefaults.standard.bool(forKey: Keys.isCellularNetwork) + } + set { + UserDefaults.standard.set(newValue, forKey: Keys.isCellularNetwork) + } + } + + static var isAutoUpdate: Bool { + get { + return UserDefaults.standard.bool(forKey: Keys.isAutoUpdate) + } + set { + UserDefaults.standard.set(newValue, forKey: Keys.isAutoUpdate) + } + } +} diff --git a/mapkit-samples/MapOffline/DataMoveListener.swift b/mapkit-samples/MapOffline/DataMoveListener.swift new file mode 100644 index 0000000..4a675ef --- /dev/null +++ b/mapkit-samples/MapOffline/DataMoveListener.swift @@ -0,0 +1,33 @@ +// +// MapCameraListener.swift +// MapSearch +// + +import YandexMapsMobile + +class DataMoveListener: NSObject, YMKOfflineCacheDataMoveListener { + + // MARK: - Constructor + + init(optionViewModel: OptionsViewModel) { + self.optionViewModel = optionViewModel + } + + // MARK: - Public methods + + func onDataMoveProgress(withPercent percent: Int) { + optionViewModel.progress = Float(percent) + } + + func onDataMoveCompleted() { + optionViewModel.isSuccessMove = true + } + + func onDataMoveErrorWithError(_ error: any Error) { + optionViewModel.errorText = error.localizedDescription + } + + // MARK: - Private properties + + private let optionViewModel: OptionsViewModel +} diff --git a/mapkit-samples/MapOffline/MapCameraListener.swift b/mapkit-samples/MapOffline/MapCameraListener.swift new file mode 100644 index 0000000..28935de --- /dev/null +++ b/mapkit-samples/MapOffline/MapCameraListener.swift @@ -0,0 +1,31 @@ +// +// MapCameraListener.swift +// MapSearch +// + +import YandexMapsMobile + +class MapCameraListener: NSObject, YMKMapCameraListener { + // MARK: - Constructor + + init(searchViewModel: SearchViewModel) { + self.searchViewModel = searchViewModel + } + + // MARK: - Public methods + + func onCameraPositionChanged( + with map: YMKMap, + cameraPosition: YMKCameraPosition, + cameraUpdateReason: YMKCameraUpdateReason, + finished: Bool + ) { + if cameraUpdateReason == .gestures { + searchViewModel.setVisibleRegion(with: map.visibleRegion) + } + } + + // MARK: - Private properties + + private let searchViewModel: SearchViewModel +} diff --git a/mapkit-samples/MapOffline/MapUIState.swift b/mapkit-samples/MapOffline/MapUIState.swift new file mode 100644 index 0000000..2d8b5b3 --- /dev/null +++ b/mapkit-samples/MapOffline/MapUIState.swift @@ -0,0 +1,35 @@ +// +// MapUIState.swift +// MapSearch +// + +import YandexMapsMobile + +struct MapUIState { + let query: String + let searchState: SearchState + let regionListState: RegionListState + + init(query: String = String(), searchState: SearchState, regionListState: RegionListState) { + self.query = query + self.searchState = searchState + self.regionListState = regionListState + } +} + +struct SearchResponseItem { + let point: YMKPoint + let geoObject: YMKGeoObject? +} + +enum SearchState { + case idle + case loading + case error + case success(items: [SearchResponseItem], zoomToItems: Bool, itemsBoundingBox: YMKBoundingBox) +} + +enum RegionListState { + case idle + case success +} diff --git a/mapkit-samples/MapOffline/MapViewController.swift b/mapkit-samples/MapOffline/MapViewController.swift new file mode 100644 index 0000000..d03fa84 --- /dev/null +++ b/mapkit-samples/MapOffline/MapViewController.swift @@ -0,0 +1,299 @@ +// +// MapViewController.swift +// MapSearch +// + +import Combine +import UIKit +import YandexMapsMobile + +class MapViewController: UIViewController { + // MARK: - Public methods + + override func viewDidLoad() { + super.viewDidLoad() + + mapView = YMKMapView(frame: view.frame) + view.addSubview(mapView) + + map = mapView.mapWindow.map + + map.addCameraListener(with: mapCameraListener) + offlineManager.addRegionListUpdatesListener(with: regionListUpdatesListener) + + searchViewModel.setupSubscriptions() + + setupSearchController() + + setupSubviews() + + moveToStartPoint() + } + + // MARK: - Private methods + + private func moveToStartPoint() { + map.move(with: Const.startPosition, animation: YMKAnimation(type: .smooth, duration: 0.5)) + searchViewModel.setVisibleRegion(with: map.visibleRegion) + } + + private func setupSearchController() { + navigationController?.setNavigationBarHidden(true, animated: false) + self.searchBarController.searchBar.placeholder = "Search places" + + searchBarController.delegate = self + searchBarController.searchBar.delegate = self + searchBarController.searchBar.showsBookmarkButton = false + + searchBarController.view.backgroundColor = .white + resultsTableController.view.backgroundColor = .white + searchBarController.searchResultsController?.view.isHidden = false + + resultsTableController.tableView.contentInset = .init(top: 50, left: .zero, bottom: .zero, right: .zero) + resultsTableController.edgesForExtendedLayout = .bottom + resultsTableController.tableView.delegate = self + + setupStateUpdates() + } + + private func setupSubviews() { + view.addSubview(menuView) + menuView.axis = .horizontal + menuView.alignment = .fill + menuView.distribution = .equalSpacing + menuView.spacing = 15 + menuView.translatesAutoresizingMaskIntoConstraints = false + + [ + menuView.topAnchor.constraint(equalTo: view.topAnchor, constant: 80), + menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20) + ] + .forEach { $0.isActive = true } + + [regionListButton, optionsButton] + .forEach { + menuView.addArrangedSubview($0) + + $0.translatesAutoresizingMaskIntoConstraints = false + [ + $0.heightAnchor.constraint(equalToConstant: 36) + ] + .forEach { $0.isActive = true } + + $0.layer.cornerRadius = 4 + $0.setTitleColor(.black, for: .normal) + $0.contentMode = .scaleAspectFill + $0.backgroundColor = Palette.background + } + + regionListButton.addTarget( + self, + action: #selector(regionListButtonTapHandler), + for: .touchUpInside + ) + regionListButton.setTitle("REGION LIST", for: .normal) + + optionsButton.addTarget( + self, + action: #selector(optionsButtonTapHandler), + for: .touchUpInside + ) + optionsButton.setTitle("OPTIONS", for: .normal) + } + + @objc + private func regionListButtonTapHandler() { + present(searchBarController, animated: true) + searchViewModel.setQueryText(with: "") + } + + @objc + private func optionsButtonTapHandler() { + let optionsViewController = OptionsViewController() + present(optionsViewController, animated: true) + } + + private func focusCamera(points: [YMKPoint], boundingBox: YMKBoundingBox) { + if points.isEmpty { + return + } + + let position = map.cameraPosition(with: YMKGeometry(boundingBox: boundingBox)) + + map.move(with: position, animation: YMKAnimation(type: .smooth, duration: 0.5)) + } + + private func displaySearchResults( + items: [SearchResponseItem], + zoomToItems: Bool, + itemsBoundingBox: YMKBoundingBox + ) { + map.mapObjects.clear() + + 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))) + + placemark.userData = item.geoObject + } + } + + // MARK: - Private properties + + private var mapView: YMKMapView! + private lazy var map: YMKMap = mapView.mapWindow.map + private lazy var mapCameraListener = MapCameraListener(searchViewModel: searchViewModel) + + private let menuView = UIStackView() + private let regionListButton = UIButton() + private let optionsButton = UIButton() + + private lazy var regionListUpdatesListener = OfflineMapRegionListUpdatesListener(searchViewModel: searchViewModel) + private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager + private lazy var resultsTableController = ResultsTableController() + private lazy var searchBarController = UISearchController(searchResultsController: resultsTableController) + private let searchViewModel = SearchViewModel() + private var bag = Set() + + // 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) { + let regionViewController = RegionViewController(regionId: regionId) + present(regionViewController, animated: true) + } + + private func updateRegions(with regionListState: RegionListState, query: String) { + switch regionListState { + case .success: + resultsTableController.items = offlineManager.regions() + .map { [weak self] 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..586cfff --- /dev/null +++ b/mapkit-samples/MapOffline/RegionViewController.swift @@ -0,0 +1,280 @@ +// +// MapViewController.swift +// MapSearch +// + +import Combine +import UIKit +import YandexMapsMobile + +class RegionViewController: UIViewController { + // MARK: - Public Properties + + // MARK: - Public methods + + convenience init(regionId: Int) { + self.init() + self.regionId = regionId + regionViewModel.setRegion(with: regionId) + regionViewModel.setRegionOpened(with: regionId) + } + + override func viewDidLoad() { + super.viewDidLoad() + + offlineCacheRegionListener = OfflineCacheRegionListener(regionViewModel: regionViewModel) + guard let offlineCacheRegionListener = offlineCacheRegionListener else { return } + offlineManager.addRegionListener(with: offlineCacheRegionListener) + regionViewModel.setupSubscriptions() + + setupRegionSubscriptions() + setupSubviews() + view.backgroundColor = .white + } + + 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 + + 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) + nameLabel.numberOfLines = .zero + 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), + showButton.widthAnchor.constraint(equalToConstant: Layout.buttonWidth) + ] + .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 var offlineCacheRegionListener: OfflineCacheRegionListener? + private lazy var offlineManager = YMKMapKit.sharedInstance().offlineCacheManager + + private var bag = Set() + + // MARK: - Layout + + 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 new file mode 100644 index 0000000..6f31641 --- /dev/null +++ b/mapkit-samples/MapOffline/RegionViewModel.swift @@ -0,0 +1,101 @@ +// +// 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 + } + + func setRegionOpened(with regionId: Int) { + self.regionIdOpened = regionId + } + + deinit { + bag.removeAll() + } + + // MARK: - Private methods + + 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)", + 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(regionId: regionId ?? .zero) ?? "", + realeseTime: self?.convertTimeString(with: cacheRegion?.releaseTime ?? Date()) ?? "", + downloadedReleaseTime: self?.convertTimeString( + with: self?.offlineManager + .getDownloadedReleaseTime(withRegionId: (UInt(regionId ?? .zero))) ?? Date() + ) ?? "" + ) + } + .store(in: &bag) + } + + private func getDownloadState(regionId: Int) -> String { + switch self.offlineManager.getStateWithRegionId(UInt(regionId)) { + 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 regionIdOpened: 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/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