diff --git a/app.xcodeproj/project.pbxproj b/app.xcodeproj/project.pbxproj index a2939e3..7409c32 100644 --- a/app.xcodeproj/project.pbxproj +++ b/app.xcodeproj/project.pbxproj @@ -48,6 +48,12 @@ 4BBE80292526E22F00D7EBDB /* SearchResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE80282526E22F00D7EBDB /* SearchResultViewModel.swift */; }; 4BBE802D2526F8A600D7EBDB /* RootViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE802C2526F8A600D7EBDB /* RootViewFactory.swift */; }; 4BBE80302526FB0D00D7EBDB /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBE802F2526FB0D00D7EBDB /* Container.swift */; }; + B709913925F9CBCF00B2F1A5 /* MapObjectCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B709913825F9CBCF00B2F1A5 /* MapObjectCardView.swift */; }; + B709914C25FA11CD00B2F1A5 /* MapObjectCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B709914B25FA11CD00B2F1A5 /* MapObjectCardViewModel.swift */; }; + B709914F25FA6B6F00B2F1A5 /* RenderedObjectInfo+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B709914E25FA6B6F00B2F1A5 /* RenderedObjectInfo+Helpers.swift */; }; + B709915125FA6B9D00B2F1A5 /* GeoPoint+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B709915025FA6B9D00B2F1A5 /* GeoPoint+Helpers.swift */; }; + B709915325FA6BD300B2F1A5 /* TrafficRoute+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B709915225FA6BD300B2F1A5 /* TrafficRoute+Helpers.swift */; }; + B709915525FA72A600B2F1A5 /* Future+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B709915425FA72A600B2F1A5 /* Future+Helpers.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -107,6 +113,12 @@ 4BBE80282526E22F00D7EBDB /* SearchResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultViewModel.swift; sourceTree = ""; }; 4BBE802C2526F8A600D7EBDB /* RootViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewFactory.swift; sourceTree = ""; }; 4BBE802F2526FB0D00D7EBDB /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + B709913825F9CBCF00B2F1A5 /* MapObjectCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapObjectCardView.swift; sourceTree = ""; }; + B709914B25FA11CD00B2F1A5 /* MapObjectCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapObjectCardViewModel.swift; sourceTree = ""; }; + B709914E25FA6B6F00B2F1A5 /* RenderedObjectInfo+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RenderedObjectInfo+Helpers.swift"; sourceTree = ""; }; + B709915025FA6B9D00B2F1A5 /* GeoPoint+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GeoPoint+Helpers.swift"; sourceTree = ""; }; + B709915225FA6BD300B2F1A5 /* TrafficRoute+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrafficRoute+Helpers.swift"; sourceTree = ""; }; + B709915425FA72A600B2F1A5 /* Future+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Future+Helpers.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -172,12 +184,12 @@ 4B27063424A3F24C00F12B48 /* AppDelegate.swift */, 4B1354DB24BCCDC7004E8158 /* NativeApp.entitlements */, 4B27063624A3F24C00F12B48 /* SceneDelegate.swift */, - 095BE97725B80EC800D09D75 /* View+Helpers.swift */, - 0991909725B977CD00F4235B /* Channel+Helpers.swift */, + B709914D25FA6B1000B2F1A5 /* Extensions */, 4BBE822A252741EE00D7EBDB /* Root */, 4BBE822B2527420900D7EBDB /* Search */, 4BBE822C2527422000D7EBDB /* Map */, 4BBE802F2526FB0D00D7EBDB /* Container.swift */, + B709914A25FA0FD700B2F1A5 /* MapObjectCardView */, 09C834F925BFCD4800D347F4 /* Route */, 095BE97225B6FF3700D09D75 /* Marker */, 0938CA9F25B5BB8A00100316 /* LocationService */, @@ -242,6 +254,28 @@ name = Frameworks; sourceTree = ""; }; + B709914A25FA0FD700B2F1A5 /* MapObjectCardView */ = { + isa = PBXGroup; + children = ( + B709913825F9CBCF00B2F1A5 /* MapObjectCardView.swift */, + B709914B25FA11CD00B2F1A5 /* MapObjectCardViewModel.swift */, + ); + path = MapObjectCardView; + sourceTree = ""; + }; + B709914D25FA6B1000B2F1A5 /* Extensions */ = { + isa = PBXGroup; + children = ( + 095BE97725B80EC800D09D75 /* View+Helpers.swift */, + 0991909725B977CD00F4235B /* Channel+Helpers.swift */, + B709914E25FA6B6F00B2F1A5 /* RenderedObjectInfo+Helpers.swift */, + B709915025FA6B9D00B2F1A5 /* GeoPoint+Helpers.swift */, + B709915225FA6BD300B2F1A5 /* TrafficRoute+Helpers.swift */, + B709915425FA72A600B2F1A5 /* Future+Helpers.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -325,11 +359,15 @@ 4BADDFCC2528016600FBF589 /* SuggestResultView.swift in Sources */, 0991909825B977CD00F4235B /* Channel+Helpers.swift in Sources */, 4BADDFEB2529AD9C00FBF589 /* SearchNavigation.swift in Sources */, + B709913925F9CBCF00B2F1A5 /* MapObjectCardView.swift in Sources */, 4B4291682527A4DB006E74BE /* SuggestViewModel.swift in Sources */, 4BBE80292526E22F00D7EBDB /* SearchResultViewModel.swift in Sources */, 0938CAA125B5BBA300100316 /* LocationService.swift in Sources */, 4BADDFD525282A2400FBF589 /* MarkedUpTextView.swift in Sources */, 095BE97625B7042500D09D75 /* MarkerViewModel.swift in Sources */, + B709915525FA72A600B2F1A5 /* Future+Helpers.swift in Sources */, + B709914C25FA11CD00B2F1A5 /* MapObjectCardViewModel.swift in Sources */, + B709915125FA6B9D00B2F1A5 /* GeoPoint+Helpers.swift in Sources */, 4BADDFCF252801BD00FBF589 /* SuggestResultViewModel.swift in Sources */, 4B27063524A3F24C00F12B48 /* AppDelegate.swift in Sources */, 4BADDFE12528941E00FBF589 /* DirectoryObjectViewModel.swift in Sources */, @@ -342,6 +380,7 @@ 09C834FD25BFCD8500D347F4 /* RouteViewModel.swift in Sources */, 4BBE802D2526F8A600D7EBDB /* RootViewFactory.swift in Sources */, 4BBE80222526DCA200D7EBDB /* SearchResultItemView.swift in Sources */, + B709915325FA6BD300B2F1A5 /* TrafficRoute+Helpers.swift in Sources */, 4BADDFFC252AD63800FBF589 /* SearchAction.swift in Sources */, 4BADDFE42528947F00FBF589 /* FormattedAddressView.swift in Sources */, 4BADDFDE25288F9D00FBF589 /* DirectoryObjectView.swift in Sources */, @@ -352,6 +391,7 @@ 0991909625B96E1C00F4235B /* MapControl.swift in Sources */, 4B4291652527A49C006E74BE /* SuggestView.swift in Sources */, 095BE97825B80EC800D09D75 /* View+Helpers.swift in Sources */, + B709914F25FA6B6F00B2F1A5 /* RenderedObjectInfo+Helpers.swift in Sources */, 4BBE80302526FB0D00D7EBDB /* Container.swift in Sources */, 4BADDFDB252866F700FBF589 /* SearchService.swift in Sources */, 4B27063724A3F24C00F12B48 /* SceneDelegate.swift in Sources */, @@ -562,7 +602,7 @@ repositoryURL = "https://github.com/2gis/native-sdk-ios-swift-package.git"; requirement = { kind = exactVersion; - version = 0.7.0; + version = 0.8.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fe212c..49a75a3 100644 --- a/app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/app.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/2gis/native-sdk-ios-swift-package.git", "state": { "branch": null, - "revision": "cc36422ceba49f519f23cde743124d35213a6b79", - "version": "0.7.0" + "revision": "6f770e55a6d65abfadb6fdfbbc364a6c26af3066", + "version": "0.8.0" } } ] diff --git a/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme b/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme index 335a7be..0cdfd4b 100644 --- a/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme +++ b/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme @@ -32,8 +32,8 @@ - + + + - + + + @@ -11,10 +14,19 @@ - + - + + + + + + + + + + @@ -22,4 +34,10 @@ + + + + + + diff --git a/app/Container.swift b/app/Container.swift index 99c0ec3..cdaaa44 100644 --- a/app/Container.swift +++ b/app/Container.swift @@ -52,14 +52,21 @@ final class Container { } private func makeRootViewModel() -> RootViewModel { - let rootViewModel = RootViewModel(searchManagerFactory: { [sdk = self.sdk] in - return sdk.searchManagerFactory.makeOnlineManager()! - }, sourceFactory: { [sdk = self.sdk] in - return sdk.sourceFactory - }, locationManagerFactory: { [weak self] in - guard let self = self else { return nil } - return self.locationManager - }, map: self.sdk.map) + let rootViewModel = RootViewModel( + searchManagerFactory: { [sdk = self.sdk] in + sdk.searchManagerFactory.makeOnlineManager()! + }, + sourceFactory: { [sdk = self.sdk] in + sdk.sourceFactory + }, + imageFactory: { [sdk = self.sdk] in + sdk.imageFactory + }, + locationManagerFactory: { [weak self] in + self?.locationManager + }, + map: self.sdk.map + ) return rootViewModel } } diff --git a/app/Channel+Helpers.swift b/app/Extensions/Channel+Helpers.swift similarity index 100% rename from app/Channel+Helpers.swift rename to app/Extensions/Channel+Helpers.swift diff --git a/app/Extensions/Future+Helpers.swift b/app/Extensions/Future+Helpers.swift new file mode 100644 index 0000000..efeec41 --- /dev/null +++ b/app/Extensions/Future+Helpers.swift @@ -0,0 +1,20 @@ +import SwiftUI +import PlatformSDK + +extension Future { + + @inlinable public func sinkOnMainThread( + receiveValue: @escaping (Value) -> Void, + failure: @escaping (Error) -> Void + ) -> PlatformSDK.Cancellable { + self.sink { value in + DispatchQueue.main.async { + receiveValue(value) + } + } failure: { error in + DispatchQueue.main.async { + failure(error) + } + } + } +} diff --git a/app/Extensions/GeoPoint+Helpers.swift b/app/Extensions/GeoPoint+Helpers.swift new file mode 100644 index 0000000..a25c25d --- /dev/null +++ b/app/Extensions/GeoPoint+Helpers.swift @@ -0,0 +1,15 @@ +import PlatformSDK + +extension GeoPoint: CustomStringConvertible { + + public var description: String { + "Latitude: \(self.latitude.value)\nLongitude: \(self.longitude.value)" + } +} + +extension GeoPointWithElevation: CustomStringConvertible { + + public var description: String { + "Latitude: \(self.latitude.value)\nLongitude: \(self.longitude.value)\nElevation: \(self.elevation.value)" + } +} diff --git a/app/Extensions/RenderedObjectInfo+Helpers.swift b/app/Extensions/RenderedObjectInfo+Helpers.swift new file mode 100644 index 0000000..d097823 --- /dev/null +++ b/app/Extensions/RenderedObjectInfo+Helpers.swift @@ -0,0 +1,28 @@ +import PlatformSDK + +extension RenderedObjectInfo: CustomStringConvertible { + + public var description: String { + let pointDescription = self.closestMapPoint.description + switch self.item.item { + case let dgisMapObject as DgisMapObject: + return "Id: \(dgisMapObject.id.value)" + case let searchResult as SearchResultMarkerObject: + if let id = searchResult.id { + return "Id: \(id.value)" + } else { + return searchResult.markerPosition.description + } + case let cluster as ClusterObject: + return "Objects count: \(cluster.objectCount)" + case let route as RouteMapObject: + return route.route?.description ?? pointDescription + case let routePoint as RoutePointMapObject: + return routePoint.route?.description ?? pointDescription + case is MyLocationMapObject, is GeometryMapObject: + return pointDescription + default: + return pointDescription + } + } +} diff --git a/app/Extensions/TrafficRoute+Helpers.swift b/app/Extensions/TrafficRoute+Helpers.swift new file mode 100644 index 0000000..27ced76 --- /dev/null +++ b/app/Extensions/TrafficRoute+Helpers.swift @@ -0,0 +1,8 @@ +import PlatformSDK + +extension TrafficRoute: CustomStringConvertible { + + public var description: String { + "Distance: \(self.length.millimeters * 1000)m" + } +} diff --git a/app/View+Helpers.swift b/app/Extensions/View+Helpers.swift similarity index 100% rename from app/View+Helpers.swift rename to app/Extensions/View+Helpers.swift diff --git a/app/MapObjectCardView/MapObjectCardView.swift b/app/MapObjectCardView/MapObjectCardView.swift new file mode 100644 index 0000000..3afbe62 --- /dev/null +++ b/app/MapObjectCardView/MapObjectCardView.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct MapObjectCardView: View { + + @ObservedObject private var viewModel: MapObjectCardViewModel + + init(viewModel: MapObjectCardViewModel) { + self.viewModel = viewModel + } + + var body: some View { + ZStack { + HStack(alignment: .top){ + VStack(alignment: .leading) { + Text(self.viewModel.title) + .font(Font.system(size: 24, weight: .regular)) + .foregroundColor(.black) + .padding([.top, .leading], 16) + Text(self.viewModel.description) + .font(Font.system(size: 12, weight: .regular)) + .foregroundColor(.black) + .padding(.top, 2) + .padding([.bottom, .leading], 16) + } + Spacer() + VStack(alignment: .trailing) { + Button(action: { + self.viewModel.close() + }) { + Image(systemName: "xmark.circle.fill") + .resizable() + .frame(width: 20, height: 20) + } + .padding([.top, .trailing], 16) + } + .padding(.leading, 16) + } + } + .background( + RoundedRectangle(cornerRadius: 20, style: .circular) + .fill(Color.white) + .shadow(color: Color.black.opacity(0.2), radius: 3) + ) + .padding([.leading, .bottom, .trailing], 16) + } +} diff --git a/app/MapObjectCardView/MapObjectCardViewModel.swift b/app/MapObjectCardView/MapObjectCardViewModel.swift new file mode 100644 index 0000000..5e39666 --- /dev/null +++ b/app/MapObjectCardView/MapObjectCardViewModel.swift @@ -0,0 +1,71 @@ +import SwiftUI +import PlatformSDK + +final class MapObjectCardViewModel: ObservableObject { + + typealias CloseCallback = () -> Void + + @Published var title: String = "Some place" + @Published var description: String + + private let objectInfo: RenderedObjectInfo + private let onClose: CloseCallback + private var getDirectoryObjectCancellable: Cancellable? + + init( + objectInfo: RenderedObjectInfo, + onClose: @escaping CloseCallback + ) { + self.objectInfo = objectInfo + self.description = objectInfo.description + self.onClose = onClose + self.fetchObjectInfo() + } + + func close() { + self.onClose() + } + + private func fetchObjectInfo() { + let mapObject = self.objectInfo.item.item + switch mapObject { + case let object as DgisMapObject: + self.fetchInfo(dgisMapObject: object) + case let marker as Marker: + self.fetchInfo(marker: marker) + default: + self.fetchInfo(objectInfo: self.objectInfo) + } + } + + private func fetchInfo(dgisMapObject object: DgisMapObject) { + self.getDirectoryObjectCancellable = object.directoryObject.sinkOnMainThread( + receiveValue: { + [weak self] directoryObject in + guard let directoryObject = directoryObject else { return } + + self?.title = directoryObject.title + self?.description = """ + \(directoryObject.subtitle) + \(directoryObject.formattedAddress(type: .short)?.streetAddress ?? "(no address)") + \(directoryObject.markerPosition?.description ?? "(no location)") + ID: \(object.id.value) + """ + }, + failure: { error in + print("Unable to fetch a directory object. Error: \(error).") + } + ) + } + + private func fetchInfo(marker: Marker) { + let text = marker.text + self.title = text.isEmpty ? "Marker" : text + self.description = "\(marker.position)" + } + + private func fetchInfo(objectInfo: RenderedObjectInfo) { + self.title = String(describing: type(of: objectInfo)) + self.description = String(describing: objectInfo) + } +} diff --git a/app/Marker/MarkerViewModel.swift b/app/Marker/MarkerViewModel.swift index 5ccc96d..c753095 100644 --- a/app/Marker/MarkerViewModel.swift +++ b/app/Marker/MarkerViewModel.swift @@ -87,7 +87,7 @@ final class MarkerViewModel: ObservableObject { } func addMarkers(text: String) { - let flatPoint = self.map.camera.position().value.point + let flatPoint = self.map.camera.position.value.point let point = GeoPointWithElevation( latitude: flatPoint.latitude, longitude: flatPoint.longitude diff --git a/app/Root/RootView.swift b/app/Root/RootView.swift index 9b01515..dbfe263 100644 --- a/app/Root/RootView.swift +++ b/app/Root/RootView.swift @@ -1,11 +1,10 @@ import SwiftUI struct RootView: View { - private let viewModel: RootViewModel - private let viewFactory: RootViewFactory + private static let mapCoordinateSpace = "map" - @State private var showMarkers: Bool = false - @State private var showRoutes: Bool = false + @ObservedObject private var viewModel: RootViewModel + private let viewFactory: RootViewFactory init( viewModel: RootViewModel, @@ -19,29 +18,37 @@ struct RootView: View { var body: some View { NavigationView { - ZStack() { - ZStack(alignment: .bottomTrailing) { - self.viewFactory.makeMapView() - if !self.showMarkers { - self.settingsButton().frame(width: 100, height: 100, alignment: .bottomTrailing) - } - if self.showMarkers { - self.viewFactory.makeMarkerView(show: $showMarkers).followKeyboard($keyboardOffset) + GeometryReader { geometry in + ZStack { + ZStack(alignment: .bottomTrailing) { + self.viewFactory.makeMapView() + .coordinateSpace(name: Self.mapCoordinateSpace) + .simultaneousGesture(self.drag) + if !self.viewModel.showMarkers { + self.settingsButton().frame(width: 100, height: 100, alignment: .bottomTrailing) + } + if self.viewModel.showMarkers { + self.viewFactory.makeMarkerView(show: self.$viewModel.showMarkers).followKeyboard($keyboardOffset) + } + if self.viewModel.showRoutes { + self.viewFactory.makeRouteView(show: self.$viewModel.showRoutes).followKeyboard($keyboardOffset) + } + if let cardViewModel = self.viewModel.selectedObjectCardViewModel { + self.viewFactory.makeMapObjectCardView(cardViewModel) + .transition(.move(edge: .bottom)) + } } - if self.showRoutes { - self.viewFactory.makeRouteView(show: $showRoutes).followKeyboard($keyboardOffset) + if self.viewModel.showMarkers || self.viewModel.showRoutes { + Image(systemName: "multiply").frame(width: 40, height: 40, alignment: .center).foregroundColor(.red).opacity(0.4) } + self.zoomControls() } - if self.showMarkers || self.showRoutes { - Image(systemName: "multiply").frame(width: 40, height: 40, alignment: .center).foregroundColor(.red).opacity(0.4) - } - self.zoomControls() + .navigationBarItems( + leading: self.navigationBarLeadingItem() + ) + .navigationBarTitle("2GIS", displayMode: .inline) + .edgesIgnoringSafeArea(.all) } - .navigationBarItems( - leading: self.navigationBarLeadingItem() - ) - .navigationBarTitle("2GIS", displayMode: .inline) - .edgesIgnoringSafeArea(.all) }.navigationViewStyle(StackNavigationViewStyle()) } @@ -91,14 +98,26 @@ struct RootView: View { self.viewModel.showCurrentPosition() }, .default(Text("Тест добавления маркеров")) { - self.showMarkers = true + self.viewModel.showMarkers = true }, .default(Text("Тест поиска маршрута")) { - self.showRoutes = true + self.viewModel.showRoutes = true }, .cancel(Text("Отмена")) ]) } } + + private var drag: some Gesture { + DragGesture( + minimumDistance: 0, + coordinateSpace: .named(Self.mapCoordinateSpace) + ) + .onEnded { info in + if abs(info.translation.width) < 10, abs(info.translation.height) < 10 { + self.viewModel.tap(info.startLocation) + } + } + } } diff --git a/app/Root/RootViewFactory.swift b/app/Root/RootViewFactory.swift index d544e8e..670df35 100644 --- a/app/Root/RootViewFactory.swift +++ b/app/Root/RootViewFactory.swift @@ -42,4 +42,8 @@ struct RootViewFactory { func makeRouteView(show: Binding) -> some View { return RouteView(viewModel: self.routeViewModel, show: show) } + + func makeMapObjectCardView(_ viewModel: MapObjectCardViewModel) -> some View { + return MapObjectCardView(viewModel: viewModel) + } } diff --git a/app/Root/RootViewModel.swift b/app/Root/RootViewModel.swift index e8126e8..8cb96e1 100644 --- a/app/Root/RootViewModel.swift +++ b/app/Root/RootViewModel.swift @@ -1,17 +1,39 @@ import SwiftUI import PlatformSDK -final class RootViewModel { +final class RootViewModel: ObservableObject { + + private enum Constants { + static let tapRadius = ScreenDistance(value: 1) + } let searchStore: SearchStore + @Published var showMarkers: Bool = false + @Published var showRoutes: Bool = false + @Published var selectedObjectCardViewModel: MapObjectCardViewModel? + private let searchManagerFactory: () -> ISearchManager private let sourceFactory: () -> ISourceFactory + private let imageFactory: () -> IImageFactory private let locationManagerFactory: () -> LocationService? private let map: Map + private let toMap: CGAffineTransform private var locationService: LocationService? private var moveCameraCancellable: Cancellable? + private var getRenderedObjectsCancellable: Cancellable? + private var getDirectoryObjectCancellable: Cancellable? + private var selectedMarker: Marker? + private lazy var mapObjectManager: MapObjectManager = createMapObjectManager(map: self.map) + private lazy var selectedMarkerIcon: PlatformSDK.Image = { + let factory = self.imageFactory() + let icon = UIImage(systemName: "mappin.and.ellipse")! + .withTintColor(#colorLiteral(red: 0.2470588235, green: 0.6, blue: 0.1607843137, alpha: 1)) + .withConfiguration(UIImage.SymbolConfiguration(scale: .large)) + return factory.make(image: icon) + }() + private let testPoints: [(position: CameraPosition, time: TimeInterval, type: CameraAnimationType)] = { return [ (.init( @@ -50,14 +72,19 @@ final class RootViewModel { init( searchManagerFactory: @escaping () -> ISearchManager, sourceFactory: @escaping () -> ISourceFactory, + imageFactory: @escaping () -> IImageFactory, locationManagerFactory: @escaping () -> LocationService?, map: Map ) { self.searchManagerFactory = searchManagerFactory self.sourceFactory = sourceFactory + self.imageFactory = imageFactory self.locationManagerFactory = locationManagerFactory self.map = map + let scale = UIScreen.main.nativeScale + self.toMap = CGAffineTransform(scaleX: scale, y: scale) + let service = SearchService( searchManagerFactory: self.searchManagerFactory, scheduler: DispatchQueue.main @@ -109,6 +136,36 @@ final class RootViewModel { } } + func tap(_ location: CGPoint) { + let mapLocation = location.applying(self.toMap) + let tapPoint = ScreenPoint(x: Float(mapLocation.x), y: Float(mapLocation.y)) + self.tap(point: tapPoint, tapRadius: Constants.tapRadius) + } + + /// - Parameter point: A tap point in *pixel* (native scale) cooordinates. + /// - Parameter tapRadius: Radius around tap point in which objects will + /// be detected. + private func tap(point: ScreenPoint, tapRadius: ScreenDistance) { + self.hideSelectedMarker() + self.getRenderedObjectsCancellable?.cancel() + + let cancel = self.map.getRenderedObjects(centerPoint: point, radius: tapRadius).sink( + receiveValue: { + infos in + // The first object is the closest one to the tapped point. + guard let info = infos.first else { return } + DispatchQueue.main.async { + [weak self] in + self?.handle(selectedObject: info) + } + }, + failure: { error in + print("Failed to fetch objects: \(error)") + } + ) + self.getRenderedObjectsCancellable = cancel + } + private func move(at index: Int) { guard index < self.testPoints.count else { return } @@ -128,4 +185,31 @@ final class RootViewModel { } } } + + private func hideSelectedMarker() { + if let marker = self.selectedMarker { + marker.remove() + } + self.selectedObjectCardViewModel = nil + } + + private func handle(selectedObject: RenderedObjectInfo) { + let mapPoint = selectedObject.closestMapPoint + let markerPoint = GeoPointWithElevation( + latitude: mapPoint.latitude, + longitude: mapPoint.longitude + ) + let markerOptions = MarkerOptions( + position: markerPoint, + icon: self.selectedMarkerIcon + ) + self.selectedMarker = self.mapObjectManager.addMarker(options: markerOptions) + self.selectedObjectCardViewModel = MapObjectCardViewModel( + objectInfo: selectedObject, + onClose: { + [weak self] in + self?.hideSelectedMarker() + } + ) + } } diff --git a/app/Route/RouteViewModel.swift b/app/Route/RouteViewModel.swift index c0a1b8b..535b1ab 100644 --- a/app/Route/RouteViewModel.swift +++ b/app/Route/RouteViewModel.swift @@ -35,13 +35,13 @@ final class RouteViewModel: ObservableObject { } func setupPointA() { - _ = self.map.camera.position().sinkOnMainThread { [weak self] position in + _ = self.map.camera.position.sinkOnMainThread { [weak self] position in self?.updatePointA(position.point) } } func setupPointB() { - _ = self.map.camera.position().sinkOnMainThread { [weak self] position in + _ = self.map.camera.position.sinkOnMainThread { [weak self] position in self?.updatePointB(position.point) } } diff --git a/app/Search/DirectoryObjectViewModel.swift b/app/Search/DirectoryObjectViewModel.swift index e3d3599..d407231 100644 --- a/app/Search/DirectoryObjectViewModel.swift +++ b/app/Search/DirectoryObjectViewModel.swift @@ -7,9 +7,9 @@ struct DirectoryObjectViewModel { let address: FormattedAddressViewModel? init(object: DirectoryObject) { - self.navigationTitle = object.title() - self.title = object.title() - self.subtitle = object.subtitle() + self.navigationTitle = object.title + self.title = object.title + self.subtitle = object.subtitle let formattedAddress = object.formattedAddress(type: .full) self.address = formattedAddress.map(FormattedAddressViewModel.init) diff --git a/app/Search/SearchResultItemViewModel.swift b/app/Search/SearchResultItemViewModel.swift index db866f6..9162fc3 100644 --- a/app/Search/SearchResultItemViewModel.swift +++ b/app/Search/SearchResultItemViewModel.swift @@ -9,8 +9,8 @@ struct SearchResultItemViewModel: Identifiable { let object: DirectoryObjectViewModel init(_ item: DirectoryObject) { - self.title = item.title() - self.subtitle = item.subtitle() + self.title = item.title + self.subtitle = item.subtitle self.address = item.formattedAddress(type: .short)?.streetAddress self.object = DirectoryObjectViewModel(object: item) } diff --git a/app/Search/SearchResultViewModel.swift b/app/Search/SearchResultViewModel.swift index 1a0a0ef..53c7708 100644 --- a/app/Search/SearchResultViewModel.swift +++ b/app/Search/SearchResultViewModel.swift @@ -8,8 +8,7 @@ struct SearchResultViewModel { } init(_ result: SearchResult? = nil) { - self.items = result?.firstPage()?.items().compactMap({ $0 }).map(SearchResultItemViewModel.init) - ?? [] + self.items = result?.firstPage?.items.compactMap({ $0 }).map(SearchResultItemViewModel.init) ?? [] } } diff --git a/app/Search/SearchService.swift b/app/Search/SearchService.swift index bfb2cd5..9b50a82 100644 --- a/app/Search/SearchService.swift +++ b/app/Search/SearchService.swift @@ -31,9 +31,9 @@ final class SearchService { debugPrint(handler!) dispatcher(.applyObjectSuggest(suggest)) case .performSearchHandler(let handler): - dispatcher(.searchQuery(handler!.searchQuery()!)) + dispatcher(.searchQuery(handler!.searchQuery)) case .incompleteTextHandler(let handler): - dispatcher(.setQueryText(handler!.queryText())) + dispatcher(.setQueryText(handler!.queryText)) @unknown default: fatalError() } @@ -63,7 +63,7 @@ final class SearchService { let queryText = queryText let builder = SearchQueryBuilder.fromQueryText(queryText: queryText) let query = builder.build() - self.search(query: query!)(dispatcher) + self.search(query: query)(dispatcher) } } @@ -98,7 +98,7 @@ final class SearchService { guard !queryText.isEmpty else { return } let builder = SuggestQueryBuilder.fromQueryText(queryText: queryText) - let query = builder.build()! + let query = builder.build() self.suggest(query: query)(dispatcher) } } @@ -113,7 +113,7 @@ final class SearchService { let cancel = future.sink(receiveValue: { [schedule = self.schedule] result in schedule { - let suggestResultViewModel = self.makeSuggestResultViewModel(result: result!) + let suggestResultViewModel = self.makeSuggestResultViewModel(result: result) dispatcher(.setSuggestResult(suggestResultViewModel)) } }, failure: { diff --git a/app/Search/SuggestResultViewModel.swift b/app/Search/SuggestResultViewModel.swift index ca0032f..1f330e0 100644 --- a/app/Search/SuggestResultViewModel.swift +++ b/app/Search/SuggestResultViewModel.swift @@ -12,7 +12,7 @@ struct SuggestResultViewModel { init( result: SuggestResult? = nil ) { - self.suggests = result?.suggests().compactMap({ $0 }).map(SuggestViewModel.init) ?? [] + self.suggests = result?.suggests.compactMap({ $0 }).map(SuggestViewModel.init) ?? [] } } diff --git a/app/Search/SuggestViewModel.swift b/app/Search/SuggestViewModel.swift index d6d6d40..d72bca8 100644 --- a/app/Search/SuggestViewModel.swift +++ b/app/Search/SuggestViewModel.swift @@ -11,11 +11,11 @@ struct SuggestViewModel: Identifiable, Hashable { let object: DirectoryObjectViewModel? init(suggest: Suggest) { - self.title = suggest.title() - self.subtitle = suggest.subtitle() - self.applyHandler = suggest.handler() - self.icon = makeIcon(for: suggest.handler()) - self.object = suggest.handler().object.map(DirectoryObjectViewModel.init) + self.title = suggest.title + self.subtitle = suggest.subtitle + self.applyHandler = suggest.handler + self.icon = makeIcon(for: suggest.handler) + self.object = suggest.handler.object.map(DirectoryObjectViewModel.init) } static func ==(_ lhs: Self, rhs: Self) -> Bool { @@ -46,7 +46,7 @@ private extension SuggestHandler { var object: DirectoryObject? { switch self { case .objectHandler(let handler): - return handler!.item() + return handler?.item default: return nil } diff --git a/docs/ru/examples.md b/docs/ru/examples.md index 5a4e09c..3492774 100644 --- a/docs/ru/examples.md +++ b/docs/ru/examples.md @@ -153,6 +153,20 @@ let polyline = objectsManager.addPolyline(options: options) // TBD +## Мое местоположение + +### Маркер местоположения на карте +```swift +// создаем источник для отображения маркера на карте +let source = createMyLocationMapObjectSource( + context: sdkContext, + directionBehaviour: MyLocationDirectionBehaviour.followMagneticHeading) + +// добавляем источник в карту +map.addSource(source: source) +``` + + ## Получение информации о точке прикосновения к карте Передаём точку нажатия в пиксельных координатах. Для наиболее подходящего