ShareLocationViewController.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /**
  2. * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
  3. * SPDX-License-Identifier: GPL-3.0-or-later
  4. */
  5. #import "ShareLocationViewController.h"
  6. #import <CoreLocation/CoreLocation.h>
  7. #import "GeoLocationRichObject.h"
  8. #import "NCAPIController.h"
  9. #import "NCAppBranding.h"
  10. #import "NextcloudTalk-Swift.h"
  11. typedef enum ShareLocationSection {
  12. kShareLocationSectionCurrent = 0,
  13. kShareLocationSectionDropPin,
  14. kShareLocationSectionNearby,
  15. kShareLocationSectionNumber
  16. } ShareLocationSection;
  17. @interface ShareLocationViewController () <CLLocationManagerDelegate, MKMapViewDelegate, UITableViewDelegate, UITableViewDataSource, UISearchControllerDelegate, UISearchResultsUpdating>
  18. {
  19. UISearchController *_searchController;
  20. UITableViewController *_resultTableViewController;
  21. CLLocationManager *_locationManager;
  22. CLLocation *_currentLocation;
  23. NSArray *_nearbyPlaces;
  24. NSArray *_searchedPlaces;
  25. BOOL _hasBeenCentered;
  26. MKPointAnnotation *_dropPinAnnotation;
  27. CLPlacemark *_dropPinPlacemark;
  28. UIView *_dropPinGuideView;
  29. UIImageSymbolConfiguration *_iconsConfiguration;
  30. }
  31. @end
  32. @implementation ShareLocationViewController
  33. - (void)viewDidLoad
  34. {
  35. [super viewDidLoad];
  36. self.navigationItem.title = NSLocalizedString(@"Share location", nil);
  37. [self.navigationController.navigationBar setTitleTextAttributes:
  38. @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]}];
  39. self.navigationController.navigationBar.tintColor = [NCAppBranding themeTextColor];
  40. self.navigationController.navigationBar.barTintColor = [NCAppBranding themeColor];
  41. self.navigationController.navigationBar.translucent = NO;
  42. _iconsConfiguration = [UIImageSymbolConfiguration configurationWithPointSize:20];
  43. _locationManager = [[CLLocationManager alloc] init];
  44. _locationManager.delegate = self;
  45. [_locationManager requestWhenInUseAuthorization];
  46. self.mapView.delegate = self;
  47. self.mapView.showsUserLocation = YES;
  48. self.myLocationButton.layer.cornerRadius = 22;
  49. self.myLocationButton.clipsToBounds = YES;
  50. self.tableView.delegate = self;
  51. self.tableView.dataSource = self;
  52. self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
  53. UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
  54. target:self action:@selector(cancelButtonPressed)];
  55. self.navigationItem.leftBarButtonItem = cancelButton;
  56. _resultTableViewController = [[UITableViewController alloc] init];
  57. _resultTableViewController.tableView.delegate = self;
  58. _resultTableViewController.tableView.dataSource = self;
  59. _resultTableViewController.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
  60. _searchController = [[UISearchController alloc] initWithSearchResultsController:_resultTableViewController];
  61. _searchController.delegate = self;
  62. _searchController.searchResultsUpdater = self;
  63. _searchController.hidesNavigationBarDuringPresentation = NO;
  64. [_searchController.searchBar sizeToFit];
  65. UIColor *themeColor = [NCAppBranding themeColor];
  66. UINavigationBarAppearance *appearance = [[UINavigationBarAppearance alloc] init];
  67. [appearance configureWithOpaqueBackground];
  68. appearance.backgroundColor = themeColor;
  69. appearance.titleTextAttributes = @{NSForegroundColorAttributeName:[NCAppBranding themeTextColor]};
  70. self.navigationItem.standardAppearance = appearance;
  71. self.navigationItem.compactAppearance = appearance;
  72. self.navigationItem.scrollEdgeAppearance = appearance;
  73. self.navigationItem.searchController = _searchController;
  74. self.navigationItem.searchController.searchBar.searchTextField.backgroundColor = [NCUtils searchbarBGColorForColor:themeColor];
  75. if (@available(iOS 16.0, *)) {
  76. self.navigationItem.preferredSearchBarPlacement = UINavigationItemSearchBarPlacementStacked;
  77. }
  78. _searchController.searchBar.tintColor = [NCAppBranding themeTextColor];
  79. UITextField *searchTextField = [_searchController.searchBar valueForKey:@"searchField"];
  80. UIButton *clearButton = [searchTextField valueForKey:@"_clearButton"];
  81. searchTextField.tintColor = [NCAppBranding themeTextColor];
  82. searchTextField.textColor = [NCAppBranding themeTextColor];
  83. dispatch_async(dispatch_get_main_queue(), ^{
  84. // Search bar placeholder
  85. searchTextField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"Search for places", nil)
  86. attributes:@{NSForegroundColorAttributeName:[[NCAppBranding themeTextColor] colorWithAlphaComponent:0.5]}];
  87. // Search bar search icon
  88. UIImageView *searchImageView = (UIImageView *)searchTextField.leftView;
  89. searchImageView.image = [searchImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  90. [searchImageView setTintColor:[[NCAppBranding themeTextColor] colorWithAlphaComponent:0.5]];
  91. // Search bar search clear button
  92. UIImage *clearButtonImage = [clearButton.imageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  93. [clearButton setImage:clearButtonImage forState:UIControlStateNormal];
  94. [clearButton setImage:clearButtonImage forState:UIControlStateHighlighted];
  95. [clearButton setTintColor:[NCAppBranding themeTextColor]];
  96. });
  97. // Place resultTableViewController correctly
  98. self.definesPresentationContext = YES;
  99. }
  100. #pragma mark - CLLocationManagerDelegate
  101. - (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager
  102. {
  103. CLAuthorizationStatus status = manager.authorizationStatus;
  104. _myLocationButton.hidden = YES;
  105. if (status == kCLAuthorizationStatusAuthorizedWhenInUse || status == kCLAuthorizationStatusAuthorizedAlways) {
  106. _myLocationButton.hidden = NO;
  107. } else if (status == kCLAuthorizationStatusNotDetermined) {
  108. [_locationManager requestWhenInUseAuthorization];
  109. } else if (status == kCLAuthorizationStatusDenied) {
  110. [self showAuthorizationStatusDeniedAlert];
  111. }
  112. }
  113. - (void)showAuthorizationStatusDeniedAlert
  114. {
  115. UIAlertController * alert = [UIAlertController
  116. alertControllerWithTitle:NSLocalizedString(@"Could not access your location", nil)
  117. message:NSLocalizedString(@"Location service has been denied. Check your settings.", nil)
  118. preferredStyle:UIAlertControllerStyleAlert];
  119. UIAlertAction* settingsButton = [UIAlertAction actionWithTitle:NSLocalizedString(@"Settings", nil)
  120. style:UIAlertActionStyleDefault
  121. handler:^(UIAlertAction * _Nonnull action) {
  122. [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
  123. }];
  124. [alert addAction:settingsButton];
  125. [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:nil]];
  126. [self presentViewController:alert animated:YES completion:nil];
  127. }
  128. #pragma mark - MKMapViewDelegate
  129. - (void)mapView:(MKMapView *)mapView didUpdateUserLocation:(MKUserLocation *)userLocation
  130. {
  131. // Center the map view the first time the user location is updated
  132. if (!_hasBeenCentered) {
  133. _hasBeenCentered = YES;
  134. [self centerMapViewToUserLocation];
  135. }
  136. dispatch_async(dispatch_get_main_queue(), ^{
  137. self->_currentLocation = mapView.userLocation.location;
  138. [self.tableView reloadSections:[[NSIndexSet alloc] initWithIndex:kShareLocationSectionCurrent] withRowAnimation:UITableViewRowAnimationNone];
  139. });
  140. }
  141. - (void)mapView:(MKMapView *)mapView regionWillChangeAnimated:(BOOL)animated
  142. {
  143. [_mapView removeAnnotation:_dropPinAnnotation];
  144. [self showDropPinGuideView];
  145. }
  146. - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
  147. {
  148. [self hideDropPinGuideView];
  149. _dropPinAnnotation = [[MKPointAnnotation alloc] init];
  150. _dropPinAnnotation.coordinate = CLLocationCoordinate2DMake(_mapView.centerCoordinate.latitude, _mapView.centerCoordinate.longitude);
  151. [_mapView addAnnotation:_dropPinAnnotation];
  152. CLLocation *location = [[CLLocation alloc] initWithLatitude:_dropPinAnnotation.coordinate.latitude longitude:_dropPinAnnotation.coordinate.longitude];
  153. CLGeocoder *geocoder = [[CLGeocoder alloc] init];
  154. [geocoder reverseGeocodeLocation:location completionHandler:^(NSArray *placemarks, NSError *error) {
  155. if (!error) {
  156. dispatch_async(dispatch_get_main_queue(), ^{
  157. self->_dropPinPlacemark = placemarks[0];
  158. [self.tableView reloadSections:[[NSIndexSet alloc] initWithIndex:kShareLocationSectionDropPin] withRowAnimation:UITableViewRowAnimationNone];
  159. });
  160. }
  161. }];
  162. }
  163. - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
  164. {
  165. // If the annotation is the user location, just return nil.
  166. if ([annotation isKindOfClass:[MKUserLocation class]]) {
  167. return nil;
  168. }
  169. if (annotation == _dropPinAnnotation) {
  170. MKPinAnnotationView *pinView = (MKPinAnnotationView*)[mapView dequeueReusableAnnotationViewWithIdentifier:@"SelectedLocationAnnotationView"];
  171. if (!pinView) {
  172. pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"SelectedLocationAnnotationView"];
  173. pinView.pinTintColor = [NCAppBranding elementColor];
  174. pinView.animatesDrop = YES;
  175. pinView.canShowCallout = YES;
  176. } else {
  177. pinView.annotation = annotation;
  178. }
  179. return pinView;
  180. }
  181. return nil;
  182. }
  183. #pragma mark - Actions
  184. - (void)cancelButtonPressed
  185. {
  186. [self dismissViewControllerAnimated:YES completion:nil];
  187. }
  188. - (IBAction)myLocationButtonPressed:(id)sender
  189. {
  190. [self centerMapViewToUserLocation];
  191. }
  192. #pragma mark - Map View
  193. - (void)centerMapViewToUserLocation
  194. {
  195. MKCoordinateRegion mapRegion;
  196. mapRegion.center = self.mapView.userLocation.coordinate;
  197. mapRegion.span = MKCoordinateSpanMake(0.01, 0.01);
  198. [self.mapView setRegion:mapRegion animated: YES];
  199. }
  200. - (void)showDropPinGuideView
  201. {
  202. _dropPinGuideView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 8, 8)];
  203. _dropPinGuideView.backgroundColor = [NCAppBranding placeholderColor];
  204. _dropPinGuideView.layer.cornerRadius = 4;
  205. _dropPinGuideView.clipsToBounds = YES;
  206. _dropPinGuideView.center = _mapView.center;
  207. [self.mapView addSubview:_dropPinGuideView];
  208. [self.mapView bringSubviewToFront:_dropPinGuideView];
  209. }
  210. - (void)hideDropPinGuideView
  211. {
  212. [_dropPinGuideView removeFromSuperview];
  213. _dropPinAnnotation = nil;
  214. }
  215. #pragma mark - Search places
  216. - (void)searchForNearbyPlaces
  217. {
  218. MKLocalPointsOfInterestRequest *request = [[MKLocalPointsOfInterestRequest alloc] initWithCoordinateRegion:self.mapView.region];
  219. MKLocalSearch *search = [[MKLocalSearch alloc] initWithPointsOfInterestRequest:request];
  220. [search startWithCompletionHandler:^(MKLocalSearchResponse * _Nullable response, NSError * _Nullable error) {
  221. if (response) {
  222. dispatch_async(dispatch_get_main_queue(), ^{
  223. self->_nearbyPlaces = response.mapItems;
  224. [self.tableView reloadSections:[[NSIndexSet alloc] initWithIndex:kShareLocationSectionNearby] withRowAnimation:UITableViewRowAnimationNone];
  225. });
  226. }
  227. }];
  228. }
  229. - (void)searchForPlacesWithString:(NSString *)searchString
  230. {
  231. MKLocalSearchRequest *request = [[MKLocalSearchRequest alloc] initWithNaturalLanguageQuery:searchString];
  232. MKLocalSearch *search = [[MKLocalSearch alloc] initWithRequest:request];
  233. [search startWithCompletionHandler:^(MKLocalSearchResponse * _Nullable response, NSError * _Nullable error) {
  234. if (response) {
  235. dispatch_async(dispatch_get_main_queue(), ^{
  236. self->_searchedPlaces = response.mapItems;
  237. [self->_resultTableViewController.tableView reloadData];
  238. });
  239. }
  240. }];
  241. }
  242. #pragma mark - Search controller
  243. - (void)updateSearchResultsForSearchController:(UISearchController *)searchController
  244. {
  245. [self searchForPlacesWithString:_searchController.searchBar.text];
  246. }
  247. #pragma mark - UITableView delegate and data source
  248. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  249. {
  250. if (tableView == _resultTableViewController.tableView) {
  251. return _searchedPlaces.count;
  252. }
  253. if (section == kShareLocationSectionCurrent) {
  254. return _currentLocation ? 1 : 0;
  255. } else if (section == kShareLocationSectionDropPin) {
  256. return 1;
  257. } else if (section == kShareLocationSectionNearby) {
  258. return _nearbyPlaces.count;
  259. }
  260. return 0;
  261. }
  262. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  263. {
  264. if (tableView == _resultTableViewController.tableView) {
  265. return 1;
  266. }
  267. return kShareLocationSectionNumber;
  268. }
  269. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
  270. {
  271. if (section == kShareLocationSectionNearby && _nearbyPlaces.count > 0) {
  272. return NSLocalizedString(@"Nearby places", nil);;
  273. }
  274. return nil;
  275. }
  276. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
  277. {
  278. // Search result table view
  279. if (tableView == _resultTableViewController.tableView) {
  280. MKMapItem *searchedPlace = [_searchedPlaces objectAtIndex:indexPath.row];
  281. UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"SearchedLocationCellIdentifier"];
  282. [cell.imageView setImage:[UIImage systemImageNamed:@"mappin" withConfiguration:_iconsConfiguration]];
  283. cell.imageView.tintColor = [UIColor secondaryLabelColor];
  284. cell.textLabel.text = searchedPlace.name;
  285. NSString *subtitle = nil;
  286. if (searchedPlace.placemark.thoroughfare && searchedPlace.placemark.subThoroughfare) {
  287. subtitle = [NSString stringWithFormat:@"%@ %@, ", searchedPlace.placemark.thoroughfare, searchedPlace.placemark.subThoroughfare];
  288. }
  289. if (searchedPlace.placemark.locality) {
  290. subtitle = [subtitle stringByAppendingString:[NSString stringWithFormat:@"%@, ", searchedPlace.placemark.locality]];
  291. }
  292. if (searchedPlace.placemark.country) {
  293. subtitle = [subtitle stringByAppendingString:[NSString stringWithFormat:@"%@", searchedPlace.placemark.country]];;
  294. }
  295. cell.detailTextLabel.text = subtitle;
  296. return cell;
  297. }
  298. // Main view table view
  299. if (indexPath.section == kShareLocationSectionCurrent) {
  300. UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"UserLocationCellIdentifier"];
  301. [cell.imageView setImage:[UIImage systemImageNamed:@"location.fill" withConfiguration:_iconsConfiguration]];
  302. cell.textLabel.text = NSLocalizedString(@"Share current location", nil);
  303. cell.detailTextLabel.text = [NSString stringWithFormat:@"%@: %.0fm", NSLocalizedString(@"Accuracy", nil), _currentLocation.horizontalAccuracy];
  304. return cell;
  305. } else if (indexPath.section == kShareLocationSectionDropPin) {
  306. UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"DropPinCellIdentifier"];
  307. [cell.imageView setImage:[UIImage systemImageNamed:@"mappin" withConfiguration:_iconsConfiguration]];
  308. cell.textLabel.text = NSLocalizedString(@"Share pin location", @"Share the location of a pin that has been dropped in a map view");
  309. if (_dropPinPlacemark.thoroughfare && _dropPinPlacemark.subThoroughfare) {
  310. cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ %@", _dropPinPlacemark.thoroughfare, _dropPinPlacemark.subThoroughfare];
  311. }
  312. return cell;
  313. } else if (indexPath.section == kShareLocationSectionNearby) {
  314. MKMapItem *nearbyPlace = [_nearbyPlaces objectAtIndex:indexPath.row];
  315. UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"NearbyLocationCellIdentifier"];
  316. [cell.imageView setImage:[UIImage systemImageNamed:@"mappin" withConfiguration:_iconsConfiguration]];
  317. cell.imageView.tintColor = [UIColor secondaryLabelColor];
  318. cell.textLabel.text = nearbyPlace.name;
  319. cell.detailTextLabel.text = nil;
  320. if (nearbyPlace.placemark.thoroughfare && nearbyPlace.placemark.subThoroughfare) {
  321. cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ %@", nearbyPlace.placemark.thoroughfare, nearbyPlace.placemark.subThoroughfare];
  322. }
  323. return cell;
  324. }
  325. return nil;
  326. }
  327. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  328. {
  329. // Search result table view
  330. if (tableView == _resultTableViewController.tableView) {
  331. MKMapItem *searchedPlace = [_searchedPlaces objectAtIndex:indexPath.row];
  332. [self.delegate shareLocationViewController:self didSelectLocationWithLatitude:searchedPlace.placemark.location.coordinate.latitude longitude:searchedPlace.placemark.location.coordinate.longitude andName:searchedPlace.name];
  333. [self dismissViewControllerAnimated:YES completion:nil];
  334. } else {
  335. // Main view table view
  336. if (indexPath.section == kShareLocationSectionCurrent) {
  337. [self.delegate shareLocationViewController:self didSelectLocationWithLatitude:_currentLocation.coordinate.latitude longitude:_currentLocation.coordinate.longitude andName:NSLocalizedString(@"My location", nil)];
  338. } else if (indexPath.section == kShareLocationSectionDropPin) {
  339. NSString *locationName = NSLocalizedString(@"Shared location", nil);
  340. if (_dropPinPlacemark.thoroughfare && _dropPinPlacemark.subThoroughfare) {
  341. locationName = [NSString stringWithFormat:@"%@ %@", _dropPinPlacemark.thoroughfare, _dropPinPlacemark.subThoroughfare];
  342. }
  343. [self.delegate shareLocationViewController:self didSelectLocationWithLatitude:_dropPinAnnotation.coordinate.latitude longitude:_dropPinAnnotation.coordinate.longitude andName:locationName];
  344. } else if (indexPath.section == kShareLocationSectionNearby) {
  345. MKMapItem *nearbyPlace = [_nearbyPlaces objectAtIndex:indexPath.row];
  346. [self.delegate shareLocationViewController:self didSelectLocationWithLatitude:nearbyPlace.placemark.location.coordinate.latitude longitude:nearbyPlace.placemark.location.coordinate.longitude andName:nearbyPlace.name];
  347. }
  348. }
  349. }
  350. @end