Mobil Programlama

iOS

DERS PROGRAMI
iOS 301 Ders Programı

iOS Haritalar

Lisans: Creative Commons 11.12.2020 tarihinde güncellendi
Bakabileceğiniz Etiketler: Eğitmen: Geleceği Yazanlar Ekibi

Günümüzde konum servisleri artık birçok uygulamanın vazgeçilmezi haline geldi. Konum servisleriyle beraber kullanıcıya bulunduğu yeri ya da aradığı bir mekânı harita üzerinde göstermek için Apple firması uygulama geliştiricilere MapKit adı altında bir harita sistemi sunuyor. iOS 6 öncesine kadar Google Maps'i kullanan bu platform, iOS 6‘dan sonra yoluna Apple Maps ile devam etmeye başladı. Görsel olarak çeşitli farklar içeren iOS 5 ve iOS 6 haritaları, uygulama geliştiriciler açısından herhangi bir SDK farkı yaratmıyor.

Bu bölümde iOS haritalarını nasıl kullanabileceğimizi ve harita üzerinde çeşitli mekânları nasıl gösterebileceğimizi anlatacağız. Bunun yanı sıra kullanıcının konumunun nasıl görüntüleneceğini ve kullanıcı bir mekâna tıkladığında bir aksiyon alınmasını da anlatacağız. Öncelikle yeni bir Single View proje oluşturalım ve ekrana bir MapKit öğesi tanımlayalım.

Ekranı tamamen kaplayan MKMapView iOS haritalarının görüntüleneceği alandır. Sağ yukarıda yer alan seçeneklerden haritanın tipini belirleyebilirsiniz. Burada normal görünüm, uydu görünümü ve karma görünüm seçenekleri arasından size uygun olanı seçebilirsiniz. Shows User Location seçeneği ise harita üzerinde kullanıcının anlık konumunu mavi bir nokta ile görüntülemeyi sağlar.

Haritayı kod içerisinde kullanmadan önce Mapkit Framework’ü projemize ekliyoruz.

Proje özelliklerinde yer alan Yetenekler (Capabilities) ekranı bir Apple servisini uygulamada kolayca aktif hale getirmemize yardım ediyor. iOS harita servislerini kullanmak için de Maps özelliğini ON durumuna getirmemiz, projeye MapKit kütüphanelerinin eklenmesini sağlayacaktır. Framework eklendikten sonra .h dosyasında aşağıdaki tanımları gerçekleştirelim. 

 

#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>

@interface ViewController : UIViewController <MKMapViewDelegate>
@property (nonatomic, retain) IBOutlet MKMapView *mapView;
- (void) zoomUserLocation;
@end

 

Burada MapKit projeye eklenerek (import) framework’ün bize sunduğu özellikler kullanılabilir hale getiriliyor. MKMapViewDelegate ise harita üzerinde kullanıcının yaptığı hareketleri bildirilmesi amacıyla kullanılıyor. zoomUserLocation ise bize kullanıcının bulunduğu noktayı yakınlaştırmamıza yarayacak metodu anlatıyor.

İlk olarak yapacağımız işlem uygulama ekrana geldiğinde kullanıcının anlık konumunun yakınlaştırılması olacaktır. Bunun için yukarıda tanımladığımız zoomUserLocation metodunu aşağıdaki şekilde gerçekleştiriyoruz.

 

- (void) zoomUserLocation {
    MKUserLocation *userLocation = mapView.userLocation;
    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(userLocation.location.coordinate, 5000, 5000);
    [mapView setRegion:region animated:YES];
}

 

userLocation kullanıcı konum servisleri için uygulamaya izin verdiği takdirde kullanıcının konumunu okuyabileceğimiz değişkendir. MKCoordinateRegionWithDistance metodu ise belirtilen bir koordinattan belirli uzaklıkta bir alanı belirler ve MKCoordinateRegion tipinde döner. Bu şekilde koordinatını bildiğimiz bir alanı ve etrafındaki uzaklığı belirleyebiliriz. setRegion ise haritanın gösterilen alanını belirlenen alanla değiştirir. Eğer bir animasyon olmasını istersek animated özelliğini YES dönmemiz gerekir.

 

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self performSelector:@selector(zoomUserLocation) withObject:nil afterDelay:1.0];
}

 

zoomUserLocation metodunu viewDidLoad metodu içerisinde 1 saniye gecikmeyle çağırdığımızda, kullanıcı konumu kısa bir animasyonla ekrana getirilir.

NOT : Simulatör içerisinde sahte konum vererek istediğiniz konumu kullanıcı konumu olarak belirtebilirsiniz. İstanbul için 41,29 değerini verdiğinizde yukarıdaki görüntüyü elde edersiniz.

Bir sonraki adımda ekrana rasgele konumlar belirteceğiz ve bunları iOS haritalarda sıkça gördüğümüz iğnelerle (pin) göstereceğiz. İlk olarak MapPin isimli bir sınıf tanımlıyoruz.

 

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>

@interface MapPin : NSObject  <MKAnnotation>
@property (nonatomic, copy) NSString *title;
@property (nonatomic, assign) CLLocationCoordinate2D coordinate;
- (id)initWithName:(NSString*)name coordinate:(CLLocationCoordinate2D)coordinate;
@end

 

MapPin isimli bu sınıf bir MKAnnotation olarak tanımlanıyor ve title ile coordinate değişkenlerini içeriyor. MKAnnotation sınıfından tanımlanan konumlar MapKit tarafından otomatik olarak yönetilir ve haritaya eklenirler. MapPin sınıfı ile ilgili kodlar ise aşağıdaki şekildedir;

 



import "MapPin.h"

@implementation MapPin
@synthesize title,coordinate;
- (id)initWithName:(NSString*)pinName coordinate:(CLLocationCoordinate2D)pinCoordinate {
    if ((self = [super init])) {
        self.title = pinName;
        self.coordinate = pinCoordinate;
    }
    return self;
}
@end

 

Bir konumu kolaylıkla tanımlamak için initWithName metodunu ekliyoruz. Bu metodun işlemi iç değişkenleri hızlıca tanımlamak olacaktır. title tanımlamasının sebebi kullanıcı iğnelerden birine bastığında iğne üzerinde bir ekran çıkarıp (AccessoryView) kullanıcıya konumla ilgili kısa bilgi sunmaktır.

Şimdi ViewController dosyamıza geri dönelim ve fillPois adında bir metod üretelim.

 

- (void) fillPois {
    MKUserLocation *userLocation = mapView.userLocation;
    for(int i = 1; i < 11; i++) {
        NSNumber *latitude = [NSNumber numberWithFloat:arc4random() % i * .1 + userLocation.coordinate.latitude];
        NSNumber *longitude = [NSNumber numberWithFloat:arc4random() % i * 1 + userLocation.coordinate.longitude];
        NSString *name = [NSString stringWithFormat:@"Poi %d",i];
        NSLog(@"%@ %@ %@",name,latitude,longitude);        
        CLLocationCoordinate2D coordinate;
        coordinate.latitude = latitude.doubleValue;
        coordinate.longitude = longitude.doubleValue;
        MapPin *annotation = [[MapPin alloc] initWithName:name coordinate:coordinate];
        [mapView addAnnotation:annotation];
    }
}

 

Bu metodun görevi rasgele koordinatlarda yer alan 10 adet konum (poi - point of interest) oluşturmaktır. arc4random fonksiyonu bir C fonksiyonudur ve rasgele sayılar yaratılmasını sağlar. Oluşturduğumuz rasgele koordinatlar kullanıcının konumunun kuzeydoğusunda yer alacaktır (enleme ve boylama eklenen değerler pozitif olduğundan). Koordinatları belirledikten sonra her konum için bir isim verip (Poi x) bir MapPin oluşturuyoruz ve addAnnotation metoduyla bunları harita üzerinde tanımlıyoruz. viewDidLoad içerisinde fillPois metodunu çağırdığımızda aşağıdaki görüntüyle karşılaşırız.

Bütün pinleri tek ekranda göstermek için MapView içerisinde yer alan showAnnotations:animated: metodundan faydalanabiliriz. Bu metod verilen bir pin dizisini ekran içerisinde uygun büyütme seviyesi ile görüntüleyecektir. Bütün pinleri ekranda görüntülemek için MapView içerisinde yer alan annotations dizisi ile ekrana eklenmeş bütün pinleri seçebiliriz.

 

- (void) fillPois {
    MKUserLocation *userLocation = mapView.userLocation;
    for(int i = 1; i < 11; i++) {
        NSNumber *latitude = [NSNumber numberWithFloat:arc4random() % i * .1 + userLocation.coordinate.latitude];
        NSNumber *longitude = [NSNumber numberWithFloat:arc4random() % i * 1 + userLocation.coordinate.longitude];
        NSString *name = [NSString stringWithFormat:@"Poi %d",i];
        NSLog(@"%@ %@ %@",name,latitude,longitude);        
        CLLocationCoordinate2D coordinate;
        coordinate.latitude = latitude.doubleValue;
        coordinate.longitude = longitude.doubleValue;
        MapPin *annotation = [[MapPin alloc] initWithName:name coordinate:coordinate];
        [mapView addAnnotation:annotation];
    }   
    [mapView showAnnotations:mapView.annotations animated:YES];
}

 

Metodu fillPois metodu içerisinde ekledikten sonra aşağıdaki görüntüyle karşılaşırız;

Bir sonraki adımda ise kullanıcı iğnelerden birine bastığında bir pencere oluşmasını sağlamalıyız. Bunun için temsilci metodlardan birini kullanacağız.

 

- (MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id <MKAnnotation>)annotation
{
    static NSString *viewID = @"annotationViewID";
    if ([annotation isKindOfClass:[MapPin class]]) {
        MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:viewID];
        if (annotationView == nil)
        {
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:viewID];
            annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
            annotationView.animatesDrop = YES;
            annotationView.canShowCallout = YES;
        }        
        annotationView.annotation = annotation;        
        return annotationView;
    }
    return nil;
}

 

viewForAnnotation metodu her iğne için farklı bir tasarım oluşturmamızı sağlar. Burada viewID değişkeni oluşturulan görüntünün tekrar kullanılabilmesi içindir. Tıpkı UITableView’da olduğu gibi her iğne için bu metodun bir View döndürmesi gerekir. isKindOfClass metodunun kullanılma sebebi ise eğer harita kullanıcının konumunu gösteriyorsa (ki bu mavi bir noktadır) bu konumun iğnelerle karıştırılmaması içindir. MKPinAnnotationView öntanımlı bir görünümdür ve iğne görüntüsü verir.

İğne ilk defa tanımlanıyorsa dequeueReusableAnnotationViewWithIdentifier bize nil değer döner ve biz de yeni bir View oluşturmak durumunda kalırız. Burada rightCalloutAccessoryView kullanıcı iğneye bastığında ortaya çıkan detay ekranında bir düğme yer almasını sağlar. animatesDrop özelliği iğnelerin yukarıdan düşme efektiyle oluşturulması içindir. canShowCallout ise iğneye tıklandığında tepesinde açıklama ekranı oluşmasını sağlar.

Son olarak kullanıcı iğnelerden birine tıkladığında alınacak aksiyonu belirtmemiz gerekir. Bu amaçla yine temsilci metodlardan birinin yardımına ihtiyaç duyarız;

 

-(void) mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
    MapPin *tapLocation = (MapPin*) view.annotation;
    NSString *googleUrl = [NSString stringWithFormat:@"http://google.com/search?q=%f,%f",tapLocation.coordinate.latitude,tapLocation.coordinate.longitude];
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:googleUrl]];
}

 

Burada kullanıcı bir konuma tıkladığında konumlar ilgili koordinat alınıyor ve Google arama moturunda o konumla ilgili bir arama gerçekleştiriliyor. Bu şekilde de konumla ilgili detaylı bilgi görüntülenmesi sağlanıyor.

Eğer haritanın ekran görüntüsünü alıp göndermek isterseniz MKMapSnapshotter özelliğinden yararlanabilirsiniz. MKMapSnapshotter haritada belirli bir bölgenin ekran görüntüsünü alarak bir imaj dosyasına kayıt yapmanızı sağlayabilir. Bunu denemek için önce Interface Builder yardımıyla uygulamaya bir düğme ekleyelim.

Daha sonra sendScreenshot: adında bir metod oluşturalım ve daha önceki bölümlerde öğrendiğimiz gibi düğmenin Touch Up Inside aksiyonuna bu metodu bağlayalım. Metod aşağıdaki koddan oluşacaktır:

 

- (IBAction) sendScreenshot:(id)sender {
    MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
    options.region = self.mapView.region;
    options.scale = [UIScreen mainScreen].scale;
    options.size = self.mapView.frame.size;    
    MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
    [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
        UIImage *screenshot = snapshot.image;
        MFMailComposeViewController *mailComposer = [[MFMailComposeViewController alloc] init];
mailComposer.mailComposeDelegate = self;
        [mailComposer setSubject:@"MapView öğrendim!"];
        
        [mailComposer addAttachmentData:UIImagePNGRepresentation(screenshot) mimeType:@"image/png" fileName:@"mapview.png"];
        
        NSString *emailBody = @"Harita görüntüsü";
        [mailComposer setMessageBody:emailBody isHTML:NO];
        [self presentViewController:mailComposer animated:YES completion:nil];
    }];
}
- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

 

MKMapSnapshotOptions alınacak ekran görüntüsü ile ilgili bilgiler belirler. Biz burada bütün harita ekranını almak istediğimizden değerleri mapView değişkeni üzerinden aldık. scale değişkeni ise cihazın retina olup olmadığını belirlediğinden, UIScreen içerisinde yer alan scale değişkenini burada kullanmayı uygun gördük. MKMapSnapshotter ise ekrandan görüntü alma işlemini başlatır ve işlem bittiğinde blok metodu içerisinde resimle ilgili işlemleri yapmamızı sağlar.

Resmi e-posta olarak göndereceğimizden MFMailComposeViewController yardımıyla bir e-posta gönderim ekranı oluşturuyoruz. Burada e-postanın konu kısmına “MapView öğrendim!” yazıp MKMapSnapshot içerisinde her alan UIImage sınıfındaki dosyayı UIImagePNGRepresentation metodu ile UIData sınıfına çeviriyoruz. Bu şekilde addAttachmentData:mimType:fileName: metodunu kullanarak resmi e-posta haline getirebiliyoruz. Diğer özellikleri de belirttikten sonra presentViewController:animated: metodu ile e-posta gönderim ekranı görüntülenecektir. mailComposeController:didFinishWithResult:error: temsilci metodu ise kullanıcı e-posta ekranında işlemlerini bitirdikten sonra harekete geçer ve e-posta ekranını kaldırır.

NOT: E-posta gönderimi ile ilgili olarak Mesajlaşma İşlemleri konusuna göz atmanızı tavsiye ederiz. Bu kütüphaneleri kullanmak için öncelikle projeye MessageUI Framework eklenmeli ve ardından gerekli import işlemleri yapılmalıdır. Ayrıca ViewController.h dosyasına MFMailComposeViewControllerDelegate eklenmesi unutulmamalıdır.

Düğmeye bastığımızda ise ekran görüntüsü alınır ve resim dosyası gönderilecek e-postaya eklenir. Gerekli bilgileri yazıp e-postayı gönderdiğimizde temsilci metod harekete geçer ve gönderim ekranını kapatarak haritaya döner.

Aşağıda ViewController.m dosyasının son halini bulabilirsiniz.

// ViewController.m
//  MapsExample
//
//  Created by Ozan Uysal on 4/1/13.
//  Copyright (c) 2013 Turkcell. All rights reserved.
//

#import "ViewController.h"
#import "MapPin.h"
#import <stdio.h>

@interface ViewController ()
@end
@implementation ViewController
@synthesize mapView;
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.    
    [self performSelector:@selector(zoomUserLocation) withObject:nil afterDelay:1.0];    
    [self performSelector:@selector(fillPois) withObject:nil afterDelay:1.5];
}
- (void) fillPois {
    MKUserLocation *userLocation = mapView.userLocation;
    for(int i = 1; i < 11; i++) {
        NSNumber *latitude = [NSNumber numberWithFloat:arc4random() % i * .1 + userLocation.coordinate.latitude];
        NSNumber *longitude = [NSNumber numberWithFloat:arc4random() % i * 1 + userLocation.coordinate.longitude];
        NSString *name = [NSString stringWithFormat:@"Poi %d",i];
        NSLog(@"%@ %@ %@",name,latitude,longitude);        
        CLLocationCoordinate2D coordinate;
        coordinate.latitude = latitude.doubleValue;
        coordinate.longitude = longitude.doubleValue;
        MapPin *annotation = [[MapPin alloc] initWithName:name coordinate:coordinate];
        [mapView addAnnotation:annotation];
    }    
    [mapView showAnnotations:mapView.annotations animated:YES];
}
- (IBAction) sendScreenshot:(id)sender {
    MKMapSnapshotOptions *options = [[MKMapSnapshotOptions alloc] init];
    options.region = self.mapView.region;
    options.scale = [UIScreen mainScreen].scale;
    options.size = self.mapView.frame.size;
    
    MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
    [snapshotter startWithCompletionHandler:^(MKMapSnapshot *snapshot, NSError *error) {
        UIImage *screenshot = snapshot.image;

        MFMailComposeViewController *mailComposer = [[MFMailComposeViewController alloc] init];
mailComposer.mailComposeDelegate = self;
        [mailComposer setSubject:@"MapView öğrendim!"];
        
        [mailComposer addAttachmentData:UIImagePNGRepresentation(screenshot) mimeType:@"image/png" fileName:@"mapview.png"];
        
        NSString *emailBody = @"Harita görüntüsü";
        [mailComposer setMessageBody:emailBody isHTML:NO];
        [self presentViewController:mailComposer animated:YES completion:nil];
    }];
}
- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
{
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (void) zoomUserLocation {
    MKUserLocation *userLocation = mapView.userLocation;
    MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(userLocation.location.coordinate, 5000, 5000);
    [mapView setRegion:region animated:YES];
}
- (MKAnnotationView *)mapView:(MKMapView *)map viewForAnnotation:(id <MKAnnotation>)annotation
{
    static NSString *viewID = @"annotationViewID";
    if ([annotation isKindOfClass:[MapPin class]]) {
        MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:viewID];
        
        if (annotationView == nil)
        {
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:viewID];
            annotationView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
            annotationView.animatesDrop = YES;
            annotationView.canShowCallout = YES;
        }
        annotationView.annotation = annotation;       
        return annotationView;
    }
    return nil;
}
-(void) mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control {
    MapPin *tapLocation = (MapPin*) view.annotation;
    NSString *googleUrl = [NSString stringWithFormat:@"http://google.com/search?q=%f,%f",tapLocation.coordinate.latitude,tapLocation.coordinate.longitude];
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:googleUrl]];
}
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end