본문 바로가기
아이폰 개발/ios 개념&튜토리얼

iOS Geofencing 예제 - 특정위치 안에 들어갔을때 알림(NSCoding, UserNotification)

by 인생여희 2021. 2. 15.

iOS Geofencing 예제 - 특정위치 안에 들어갔을때 알림(NSCoding, UserNotification)

 

 

✅ 구현 기능

 

1.알림을 받을 위치 지정하기(반경 + note 지정)

2.알림 받을 위치 어노테이션(마커) 표시하기

3.지도에서 어노테이션 클릭시 정보 표출 + 삭제가능

4.지정한 위치에 들어갔을때 or 벗어났을때 알림 기능

 

 

 

 

0.StoryBoard

 

 

 

 

✅ 유틸 클래스 구현

 

1. 커스텀 Alert 알림창 유틸

2. 지도 확대 유틸

 

Utilities.h

 

#import <Foundation/Foundation.h>

@import UIKit;
@import MapKit;

@interface Utilities : NSObject

//1.커스텀 알림창 띄우기
+ (void)showSimpleAlertWithTitle:(NSString *)title message:(NSString *)message viewController:(UIViewController *)viewController;

//2.지도 확대
+ (void)zoomToUserLocationInMapView:(MKMapView *)mapView;

@end

 

 

Utilities.m

 

#import "Utilities.h"

@implementation Utilities

//1.커스텀 알림창 띄우기
+ (void)showSimpleAlertWithTitle:(NSString *)title message:(NSString *)message viewController:(UIViewController *)viewController{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil];
    [alert addAction:action];
    [viewController presentViewController:alert animated:YES completion:nil];
}


//2.지도 확대
+ (void)zoomToUserLocationInMapView:(MKMapView *)mapView{
    CLLocation *location = mapView.userLocation.location;
    if (location) {

        CLLocationCoordinate2D coordinate = location.coordinate;

        MKCoordinateRegion region = MKCoordinateRegionMakeWithDistance(coordinate, 10000.0f, 10000.0f);
        
        //위에서 설정한 비율로 현재 사용자위치에서 지도확대
        [mapView setRegion:region animated:YES];

    }
}

@end

 

 

✅ 지오펜스 모델 (Geofencing Data Model)클래스 구현

 

LocationManager 가 구성되면 이제 앱에서 모니터링을 위해 사용자가 지오 펜스를 등록하도록 허용해야한다.

 

@지오 펜싱 작동 원리

Apple 용어로 알려진 Geofencing 또는 지역 모니터링에는 모니터링 할 원을 설정해야한다.

이것은 중심 좌표와 반지름 (미터 단위) 이 있는 CLCircularRegion 로 정의된다.

 

디바이스는 사용자가 원형 펜스 영역에 들어오고 나가는 것을 모니터링 할 수 있다.

사용자가 원 외부에서 원 내부로 이동할 때 입장 이벤트가 발생한다.

 

 

 @다음 조건이 충족되면 위치 관리자 콜백이 호출된다.

 

- 디바이스는 위치를 모니터링 가능상태

- 모니터링 조건은 GPS, Wi-Fi, 충분한 배터리.

- 사용자가 위치 서비스를 활성화.

- 사용자는 정확한 모니터링과 함께 항상 위치 권한을 부여.

- 앱이 20 개 이상의 지역을 모니터링하지 않는다.

 

 

앱은 사용자지정 Geotification 모델 클래스 내에 사용자 지오펜스 정보를 저장한다.

그러나 지오 펜스를 모니터링하려면 Core Location에서 각각을 CLCircularRegion 인스턴스로 나타내야 한다.

이를 처리하기 위해주어진 Geotification 객체에서  CLCircularRegion를 반환하는 Helper 메서드를 만든다.

 

 

Geotification.h

 

#import <Foundation/Foundation.h>

@import MapKit;
@import CoreLocation;

//지오팬싱 이벤트 타입 이넘
typedef enum : NSInteger {
    OnEntry = 0,
    OnExit
} EventType;


//NSCoding  프로토콜 채택
@interface Geotification : NSObject <NSCoding>

@property (nonatomic, assign) CLLocationCoordinate2D coordinate;        //중심좌표
@property (nonatomic, assign) CLLocationDistance radius;              //반경
@property (nonatomic, strong) NSString *identifier;                  //식별자
@property (nonatomic, strong) NSString *note;                       //노트
@property (nonatomic, assign) EventType eventType;                   //이벤트 타입


//지정 초기자
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate radius:(CLLocationDistance)radius identifier:(NSString *)identifier note:(NSString *)note eventType:(EventType)eventType;

@end

 

 

Geotification.m

 

#import "Geotification.h"

static NSString * kGeotificationLatitudeKey = @"latitude";
static NSString * kGeotificationLongitudeKey = @"longitude";
static NSString * kGeotificationRadiusKey = @"radius";
static NSString * kGeotificationIdentifierKey = @"identifier";
static NSString * kGeotificationNoteKey = @"note";
static NSString * kGeotificationEventTypeKey = @"eventType";

@interface Geotification ()

@end

@implementation Geotification 

// - 오브라이드 메소드 : 제목
- (NSString *)title{
    if (self.note.length==0) {
        return @"No Note";
    }
    return self.note;
}

// - 오브라이드 메소드 : 설명박스
- (NSString *)subtitle{
    NSString *eventTypeString = self.eventType == OnEntry ? @"On Entry" : @"On Exit";
    return [NSString stringWithFormat:@"Radius: %fm - %@", self.radius, eventTypeString];
}


//- 지정 초기자 메소드 : 지오팬싱이 생성 될때마다 호출
- (instancetype)initWithCoordinate:(CLLocationCoordinate2D)coordinate radius:(CLLocationDistance)radius identifier:(NSString *)identifier note:(NSString *)note eventType:(EventType)eventType{
    self = [super init];
    if (self) {
        
        NSLog(@"initWithCoordinate 호출 - - - ");
        
        self.coordinate = coordinate;  //중심좌표
        self.radius = radius;         //반경
        self.identifier = identifier;  //식별자
        self.note = note;
        self.eventType = eventType;
    }
    return self;
}


- (id)initWithCoder:(NSCoder *)aDecoder{
    self = [super init];
    if (self) {
        
        NSLog(@"initWithCoder 호출 - - - ");
        
        CGFloat latitude = [aDecoder decodeDoubleForKey:kGeotificationLatitudeKey];
        CGFloat longitude = [aDecoder decodeDoubleForKey:kGeotificationLongitudeKey];
        self.coordinate = CLLocationCoordinate2DMake(latitude, longitude);
        
        self.radius = [aDecoder decodeDoubleForKey:kGeotificationRadiusKey];
        self.identifier = [aDecoder decodeObjectForKey:kGeotificationIdentifierKey];
        self.note = [aDecoder decodeObjectForKey:kGeotificationNoteKey];
        self.eventType = [aDecoder decodeIntegerForKey:kGeotificationEventTypeKey];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder{
    
    NSLog(@"encodeWithCoder 호출 - - - ");
    [aCoder encodeDouble:self.coordinate.latitude forKey:kGeotificationLatitudeKey];
    [aCoder encodeDouble:self.coordinate.longitude forKey:kGeotificationLongitudeKey];
    [aCoder encodeDouble:self.radius forKey:kGeotificationRadiusKey];
    [aCoder encodeObject:self.identifier forKey:kGeotificationIdentifierKey];
    [aCoder encodeObject:self.note forKey:kGeotificationNoteKey];
    [aCoder encodeInteger:self.eventType forKey:kGeotificationEventTypeKey];
}

@end

 

 

✅ 알림을 받을 Geofencing (특정위치)를 추가하는 컨트롤러 클래스 구현

 

 

 

AddGeotificationViewController.h

 

#import <UIKit/UIKit.h>
#import "Geotification.h"

@import MapKit;

//1. 프토토콜 선언
@protocol AddGeotificationsViewControllerDelegate;

@interface AddGeotificationViewController : UITableViewController

@property (nonatomic, strong) id <AddGeotificationsViewControllerDelegate> delegate;

@end

@protocol AddGeotificationsViewControllerDelegate <NSObject>

- (void)addGeotificationViewController:(AddGeotificationViewController *)controller didAddCoordinate:(CLLocationCoordinate2D)coordinate radius:(CGFloat)radius identifier:(NSString *)identifier note:(NSString *)note eventType:(EventType)eventType;

@end

 

 

AddGeotificationViewController.m

 

#import "AddGeotificationViewController.h"
#import "Utilities.h"

@interface AddGeotificationViewController ()


//1.지오 팬싱 추가 버튼
@property (weak, nonatomic) IBOutlet UIBarButtonItem *addButton;

//2.현재 사용자 위치 줌 버튼
@property (weak, nonatomic) IBOutlet UIBarButtonItem *zoomButton;

//3.지오펜싱 이벤트 타입 선택 :  (들어올때 / 나갈때)
@property (weak, nonatomic) IBOutlet UISegmentedControl *eventTypeSegmentedControl;

//4.반경 지정 텍스트 필드
@property (weak, nonatomic) IBOutlet UITextField *radiusTextField;

//5.노트 설명 지정 텍스트 필드
@property (weak, nonatomic) IBOutlet UITextField *noteTextField;

//6.맵뷰
@property (weak, nonatomic) IBOutlet MKMapView *mapView;

@end

@implementation AddGeotificationViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    //1.추가 및 줌 버튼 추가
    self.navigationItem.rightBarButtonItems = @[self.addButton, self.zoomButton];
    
    //2.추가 버튼은 비활성화 처리
    self.addButton.enabled = false;
    
    //3.테이블 뷰 하단 흰색 처리
    self.tableView.tableFooterView = [UIView new];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

//반경 입력 텍스트 필드 + 메모 텍스트 필드에 값이 있을때 <추가> 버튼 활성화
- (IBAction)textFieldEditingChanged:(UITextField *)sender{
    self.addButton.enabled = !(self.radiusTextField.text.length==0) && !(self.noteTextField.text.length==0);
}


//취소 버튼 클릭시
- (IBAction)onCancel:(id)sender{
    [self dismissViewControllerAnimated:YES completion:nil];
}

//지오팬싱 추가 버튼 이벤트
- (IBAction)onAdd:(id)sender{
    
    //현재
    CLLocationCoordinate2D coordinate = self.mapView.centerCoordinate;
    
    //반경
    CGFloat radius = self.radiusTextField.text.floatValue;
    
    //식별자
    NSString *identifier = [[NSUUID new] UUIDString];
    
    //설명
    NSString *note = self.noteTextField.text;
    
    //지오펜싱 이벤트 타입 . 지오팬싱으로 들어갈때 : 나갈때
    EventType eventType = (self.eventTypeSegmentedControl.selectedSegmentIndex == 0) ? OnEntry : OnExit;
    
    //이 클래스의 델리게이트를 자신으로 설정한 클래스에서 호출 예정
    [self.delegate addGeotificationViewController:self didAddCoordinate:coordinate radius:radius identifier:identifier note:note eventType:eventType];
}

//줌 기능 이벤트
- (IBAction)onZoomToCurrentLocation:(id)sender{
    [Utilities zoomToUserLocationInMapView:self.mapView];
}

@end

 

 

✅ MapView 위, 알림받을 위치에 어노테이션 꽂아서 보여주는 클래스 구현

 

 

GeotificationsViewController.h

 

#import <UIKit/UIKit.h>

static NSString *kSavedItemsKey = @"savedItems";

@interface GeotificationsViewController : UIViewController

@end

 

 

GeotificationsViewController.m

 

#import "GeotificationsViewController.h"
#import "AddGeotificationViewController.h"
#import "Geotification.h"
#import "Utilities.h"

@import MapKit;


//1. 맵뷰, 2.코어 로케이션 , 3.AddGeotificationsViewControllerDelegate 프로토콜 채택
@interface GeotificationsViewController () <MKMapViewDelegate, AddGeotificationsViewControllerDelegate, CLLocationManagerDelegate>

//2.맵뷰 변수
@property (weak, nonatomic) IBOutlet MKMapView *mapView;

//3. 지오팬스 위치 담을 배열
@property (nonatomic, strong) NSMutableArray *geotifications;

//4.로케이션 매니져
@property (nonatomic, strong) CLLocationManager *locationManager;

@end

@implementation GeotificationsViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //1.로케이션 매니져 초기화
    self.locationManager = [CLLocationManager new];
    
    
    //2.이 컨트롤러가 locationManager 의 대리자로 작동하도록 지정
    //이 컨트롤러가 관련된 델리게이트 메소드를 호출받는다.
    [self.locationManager setDelegate:self];
    
/*
      3.지오팬스는 앱이 작동중이 아닐때도 사용자의 위치를 모니터링 해야 하기때문에, 항상 사용자의 위치 접근을 사용해야 한다.
    
   
     사용자의 개인 정보를 보호하기 위해 <항상 사용 권한> 을 요청하더라도
     앱은 첫 번째 인스턴스에서 앱을 사용하는 동안 허용을 묻는 메시지만 표시합니다.
     이렇게하면 백그라운드 위치에 대한 임시 액세스 권한이 부여되고
     나중에 백그라운드 위치 이벤트가 발생하면 사용자에게 백그라운드 위치 액세스를 계속 허용할지 확인하라는 메시지가 표시됩니다.

     이 확인은 사용자가 제어 할 수 없는 시간에, 아마도 앱이 포 그라운드에 있지 않을 때 발생합니다.
     이러한 이유로 항상 위치 액세스가 필요한 이유를 명확하게 설명하는 것이 중요합니다.
     Info.plist에
     NSLocationAlwaysAndWhenInUseUsageDescription 및 NSLocationWhenInUseUsageDescription 내용을 설명해야함.
*/
    
    [self.locationManager requestAlwaysAuthorization];
    
/*
     LoadAllGeotifications를 호출하면 UserDefaults에 저장된 Geotification 목록을 역직렬화하고
     로컬 Geotifications 배열에 로드합니다. 이 메서드는 또한 지도보기에 위치 정보를 주석으로 추가합니다.
*/
    [self loadAllGeotifications];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

#pragma mark - Loading and saving functions

- (void)loadAllGeotifications{
    
    //1.배열 초기화
    self.geotifications = [NSMutableArray array];
    
    //2.NSUserDefaults 에서 특정 key 로 배열 객체 반환
    NSArray *savedItems = [[NSUserDefaults standardUserDefaults] arrayForKey:kSavedItemsKey];
    
    
    if (savedItems) {
        
        for (id savedItem in savedItems) {
    
            //3.NSKeyedUnarchiver 의 언아카이브 메소드로 데이터 압축해제
            Geotification *geotification = [NSKeyedUnarchiver unarchiveObjectWithData:savedItem];
            
            //4.Geotification 클래스의 자식 클래스이거나 객체이면
            if ([geotification isKindOfClass:[Geotification class]]) {
            
                //5.Geotification 추가
                [self addGeotification:geotification];
            
            }
            
        }
        
    }
}

//Geotification 객체들 NSUserDefaults에 저장
- (void)saveAllGeotifications{
    
    //1.배열 선언
    NSMutableArray *items = [NSMutableArray array];
    
    for (Geotification *geotification in self.geotifications) {
            
        //2.geotification 객체 데이터 압축
        id item = [NSKeyedArchiver archivedDataWithRootObject:geotification];
        
        //3. 배열에 압축한 geotification 객체 추가
        [items addObject:item];
    }
    
    //4.NSUserDefaults 에 배열 값 추가
    [[NSUserDefaults standardUserDefaults] setObject:items forKey:kSavedItemsKey];
    
    [[NSUserDefaults standardUserDefaults] synchronize];
}


#pragma mark - Geotification 변경으로 모델/관련 뷰를 업데이트하는 기능

//Geotification 추가
- (void)addGeotification:(Geotification *)geotification{
    
    //1.배열에 Geotification 객체 삽입
    [self.geotifications addObject:geotification];
    
    //2. 맵뷰에 geotification 위치 표시
    [self.mapView addAnnotation:geotification];
    
    //3.
    [self addRadiusOverlayForGeotification:geotification];
    
    //4.타이틀에 지오팬싱 등록된 개수 표시
    [self updateGeotificationsCount];
}


//Geotification 삭제
//이렇게하면 Geotification을 UserDefaults에서 제거하고 변경 사항을에 저장하기 전에 Geotification과 관련된 지오 펜스 모니터링이 중지됩니다
 
- (void)removeGeotification:(Geotification *)geotification{
    
    //1.배열에서 삭제
    [self.geotifications removeObject:geotification];
    
    //2.맴 뷰에서 어노테이션 삭제
    [self.mapView removeAnnotation:geotification];
    
    //3.반경 오버레이 삭제
    [self removeRadiusOverlayForGeotification:geotification];
    
    //4.타이틀 카운트 업데이트
    [self updateGeotificationsCount];
}


//앱이 지오팬싱 등록 개수가 20개 이상일때 탐색모음에서 추가 버튼을 비활성화 시킨다..
- (void)updateGeotificationsCount{
    self.title = [NSString stringWithFormat:@"Geotifications (%lu)", (unsigned long)self.geotifications.count];
    [self.navigationItem.rightBarButtonItem setEnabled:self.geotifications.count<20];
}

#pragma mark - AddGeotificationViewController 델리게이트 메소드

//Geotification 추가
- (void)addGeotificationViewController:(AddGeotificationViewController *)controller didAddCoordinate:(CLLocationCoordinate2D)coordinate radius:(CGFloat)radius identifier:(NSString *)identifier note:(NSString *)note eventType:(EventType)eventType{
    
    
    //1.AddGeotificationViewController 닫기
    [controller dismissViewControllerAnimated:YES completion:nil];
    
    //2.반경 크기 체크
    //반경 값이 지오 펜스의 가장 큰 반경 (미터)을 정의하는 locationManager 의 maximumRegionMonitoringDistance 속성을 초과하지 않아야함.
    //이 최대 값을 초과하면 모니터링이 실패.
    CGFloat clampedRadius =
    (radius > self.locationManager.maximumRegionMonitoringDistance)
    ? self.locationManager.maximumRegionMonitoringDistance : radius;
    
    
    //3.Geotification 객체 생성
    Geotification *geotification =
            [[Geotification alloc]
                     initWithCoordinate:coordinate
                     radius:clampedRadius
                     identifier:identifier
                     note:note
                     eventType:eventType];
    
    
    //4.Geotification 추가
    [self addGeotification:geotification];
    
    //모니터링을 위해 새로 추가 된 Geotification을 Core Location에 등록하도록 호출 합니다.
    [self startMonitoringGeotification:geotification];
    
    
    /*
         이 시점에서 앱은 모니터링을 위해 새 지오 펜스를 등록 할 수 있습니다.
         그러나 제한이 있습니다.
         지오 펜스는 공유 시스템 리소스이므로 Core Location은 등록 된 지오 펜스 수를 앱당 최대 20 개로 제한합니다.
         해결 방법이 있지만이 자습서에서는 사용자가 추가 할 수있는 위치 정보의 수를 제한하는 방법을 사용합니다.
     */
    [self saveAllGeotifications];
}

#pragma mark - MKMapViewDelegate

//맵뷰에 어노테이션(마커) 세팅
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation{
    static NSString *identifier = @"myGeotification";
    
    if ([annotation isKindOfClass:[Geotification class]]) {
        
        //1.어노테이션 뷰 객체 생성
        MKPinAnnotationView *annotationView = (MKPinAnnotationView *)[mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
        
        if (![annotation isKindOfClass:[MKPinAnnotationView class]] || annotationView == nil) {
        
            //2.어노테이션 설정
            annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
            [annotationView setCanShowCallout:YES];
            
            //3.어노테이션에 삭제 버튼 추가
            UIButton *removeButton = [UIButton buttonWithType:UIButtonTypeCustom];
            removeButton.frame = CGRectMake(.0f, .0f, 23.0f, 23.0f);
            [removeButton setImage:[UIImage imageNamed:@"DeleteGeotification"] forState:UIControlStateNormal];
            [annotationView setLeftCalloutAccessoryView:removeButton];
        } else {
            annotationView.annotation = annotation;
        }
        return annotationView;
        
    }
    return nil;
}

//맵뷰에 반경 색깔 설정
- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay{
    
    if ([overlay isKindOfClass:[MKCircle class]]) {
        
        MKCircleRenderer *circleRenderer = [[MKCircleRenderer alloc] initWithOverlay:overlay];
        circleRenderer.lineWidth = 1.0f;
        circleRenderer.strokeColor = [UIColor purpleColor];
        circleRenderer.fillColor = [[UIColor purpleColor] colorWithAlphaComponent:.4f];
        return circleRenderer;
    
    }
    
    return nil;
}

//맵뷰의 어노테이션의 악세서리 클릭했을때 호출
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control{
    
    //1.어노테이션 객체 반환
    Geotification *geotification = (Geotification *) view.annotation;
    
    //2.Geotification 객체의 모니터링 종료
    [self stopMonitoringGeotification:geotification];
    
    //3.배열에서 삭제
    [self removeGeotification:geotification];
    
    //4.저장
    [self saveAllGeotifications];
}


#pragma mark - Map overlay functions

//Geotification 객체의 coordinate 속성값과 radius 속성값을 이용해서 반경 표시
- (void)addRadiusOverlayForGeotification:(Geotification *)geotification{
    if (self.mapView) [self.mapView addOverlay:[MKCircle circleWithCenterCoordinate:geotification.coordinate radius:geotification.radius]];
}


//반경 삭제
- (void)removeRadiusOverlayForGeotification:(Geotification *)geotification{
    if (self.mapView){
        NSArray *overlays = self.mapView.overlays;
        for (MKCircle *circleOverlay in overlays) {
            if ([circleOverlay isKindOfClass:[MKCircle class]]) {
                CLLocationCoordinate2D coordinate = circleOverlay.coordinate;
                if (coordinate.latitude == geotification.coordinate.latitude && coordinate.longitude == geotification.coordinate.longitude && circleOverlay.radius == geotification.radius) {
                    [self.mapView removeOverlay:circleOverlay];
                    break;
                }
            }
        }
    }
}

#pragma mark - Other mapview functions

- (IBAction)zoomToCurrentLocation:(id)sender{
    [Utilities zoomToUserLocationInMapView:self.mapView];
}

#pragma mark - CLLocationManagerDelegate

//이 메소드는 위치 인증 상태가 변경 될 때마다 그리고 위치 관리자가 처음 생성 될 때 호출됩니다.
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status{
   
    /*
      사용자의 현재 위치가지도보기에 표시되지 않습니다!
     기본적으로 맵뷰는 이 기능을 비활성화 하므로 탐색 표시 줄의 왼쪽 상단 모서리에있는 확대/축소 버튼이 작동하지 않습니다.
     */
    
    //사용자가 앱을 승인 한 후에 사용자의 현재 위치를 활성화합니다.
    [self.mapView setShowsUserLocation:status==kCLAuthorizationStatusAuthorizedAlways];
 
    
    /*
     // 3 kCLAuthorizationStatusAuthorizedAlways 상태가 아닐경우 alert 창으로 알림
     if status! = .authorizedAlways {
        let message = "" "
       Geotification은 저장되지만 승인 후에 만 ​​활성화됩니다.
       장치 위치에 액세스 할 수있는 권한을 Geotify합니다.
       "" "
       showAlert (withTitle : "Warning " , 메시지 : 메시지)
     }
     */
    
}


/*
 위치 오류 처리
 오류 처리를 쉽게하기 위해 몇 가지 대리자 메서드를 구현하는 것으로 시작.
 문제가 발생할 경우 추가하는 것이 중요.
 */
- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error{
    NSLog(@"ID가 있는 지역 모니터링 실패: %@", region.identifier);
}

- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error{
    NSLog(@"위치 관리자가 다음 오류로 인해 실패했습니다 error: %@", error);
}



#pragma mark - Geotifications

//CLCircularRegion 객체 생성
- (CLCircularRegion *)regionWithGeotification:(Geotification *)geotification{
    
    // 중심좌표 , 반경, 식별자 값을이용해서 초기화
    CLCircularRegion *region = [[CLCircularRegion alloc]
                                initWithCenter:geotification.coordinate
                                radius:geotification.radius
                                identifier:geotification.identifier];
    
    
    [region setNotifyOnEntry:geotification.eventType==OnEntry];
    [region setNotifyOnExit:!region.notifyOnEntry];
    
    return region;
}


//모니터링 영역
- (void)startMonitoringGeotification:(Geotification *)geotification{
    
    
    //isMonitoringAvailableForClass :
    //디바이스에 지오 펜스 모니터링을 지원하는데 필요한 하드웨어가 있는지 확인
    if (![CLLocationManager isMonitoringAvailableForClass:[CLCircularRegion class]]) {
        [Utilities showSimpleAlertWithTitle:@"Error" message:@"Geofencing is not supported on this device!" viewController:self];
        return;
    }
    
    
    //위치권한 확인 : kCLAuthorizationStatusAuthorizedAlways
    if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedAlways) {
        [Utilities showSimpleAlertWithTitle:@"Warning" message:@"Your geotification is saved but will only be activated once you grant GeofencesTest permission to access the device location." viewController:self];
    }
    
    /*
        위에서 정의한 Helper 메소드를 이용해서 CLCircularRegion 객체 생성.
     */
    CLCircularRegion *region = [self regionWithGeotification:geotification];
    
    
    /*
        모니터링 시작 메소드
        CLLocationManager를 통해 모니터링하기 위해 Core Location에 CLCircularRegion 인스턴스를 등록합니다.
     */
    [self.locationManager startMonitoringForRegion:region];
    
    
    
    /*
         참고 : iOS는 경계 교차를 감지하면 지오 펜스 이벤트를 트리거합니다.
         사용자가 등록 시점에 이미 지오 펜스 내에있는 경우 iOS는 이벤트를 생성하지 않습니다.
         장치 위치가 지정된 지오 펜스 내부 또는 외부에 있는지 쿼리해야하는 경우
         CLLocationManager에 requestStateForRegion(_:) 라는 메서드가를 사용하면 된다.
     */
}

/*
 모니터링 시작 메소드가 완료되면 사용자가 앱에서 제거 할 때 지정된 Geotification 모니터링을 중지.
 이 메소드는 locationManager에 지정된 Geotification과 연관된 CLCircularRegion 모니터링을 중지하도록 지시합니다.
 */
- (void)stopMonitoringGeotification:(Geotification *)geotification{
    

    for (CLCircularRegion *circularRegion in self.locationManager.monitoredRegions) {
        
        if ([circularRegion isKindOfClass:[CLCircularRegion class]]) {
            
            if ([circularRegion.identifier isEqualToString:geotification.identifier]) {
                [self.locationManager stopMonitoringForRegion:circularRegion];
            }
            
        }
        
    }
}

#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"addGeotification"]) {
        UINavigationController *navigationController = segue.destinationViewController;
        AddGeotificationViewController *vc = navigationController.viewControllers.firstObject;
        [vc setDelegate:self];
    }
}

@end

 

 

 

 

✅ 사용자가 지오팬스 안으로 들어가거나 or 나갈때를 모니터링하는 클래스 

 

@위치 이벤트 처리

 

지오펜스 진입 및 종료 이벤트를 수신하고 이에 반응하는 코드를 추가

 

[!] 왜 앱델리게이트에 작성하나?

 

iOS는 앱이 실행되지 않을 때를 포함하여 항상 앱에 등록 된 지오 펜스를 모니터링한다.

앱이 실행되지 않는 동안, 기기가 지오펜스 이벤트를 트리거하면 iOS가 백그라운드에서 앱을 다시 시작한다.

뷰 컨트롤러가 로드되거나 준비되지 않았을 수 있으므로 appDelegate를 이벤트 처리에 이상적인 진입 점으로 만든다.

     

[!] 새로 생성 된 CLLocationManager 인스턴스가 모니터링되는 지오펜스에 대해 어떻게 알 수 있나?

 

앱에서 모니터링하기 위해 등록 된 모든 지오펜스는 앱의 모든 위치 관리자가 편리하게 액세스 할 수 있으므로 위치 관리자를 초기화하는 위치는 중요하지 않다...

 

 

AppDelegate.h

 

//지오 펜싱 동작은 장치에서 Wi-Fi가 활성화 된 경우 훨씬 더 정확하다.

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@end

 

 

AppDelegate.m

 

#import "AppDelegate.h"
#import "Geotification.h"
#import "GeotificationsViewController.h"
#import "Utilities.h"

@import CoreLocation;

@interface AppDelegate () <CLLocationManagerDelegate>

@property (nonatomic, strong) CLLocationManager *locationManager;

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    
    /*
     @위치 이벤트 처리
     지오펜스 진입 및 종료 이벤트를 수신하고 이에 반응하는 코드를 추가.
     
     [!] 왜 앱델리게이트에 작성하나?
     
     iOS는 앱이 실행되지 않을 때를 포함하여 항상 앱에 등록 된 지오 펜스를 모니터링한다.
     앱이 실행되지 않는 동안, 기기가 지오펜스 이벤트를 트리거하면 iOS가 백그라운드에서 앱을 다시 시작한다.
     뷰 컨트롤러가 로드되거나 준비되지 않았을 수 있으므로 appDelegate를 이벤트 처리에 이상적인 진입 점으로 만든다.
     
     새로 생성 된 CLLocationManager 인스턴스가 모니터링되는 지오펜스에 대해 어떻게 알 수 있나?
     앱에서 모니터링하기 위해 등록 된 모든 지오펜스는 앱의 모든 위치 관리자가 편리하게 액세스 할 수 있으므로 위치 관리자를 초기화하는 위치는 중요하지 않다...
     */
    self.locationManager = [CLLocationManager new];
    [self.locationManager setDelegate:self];
    [self.locationManager requestAlwaysAuthorization]; //항상 허용으로 요청해야함!.
    
    
    /*
     사용자 알림을 사용하고 위치 이벤트가 트리거 될 때 배지, 사운드 및 경고를 표시 할 권한을 요청.
     */
    [application registerUserNotificationSettings:
                [UIUserNotificationSettings settingsForTypes:
                    UIUserNotificationTypeSound |
                    UIUserNotificationTypeAlert |
                    UIUserNotificationTypeBadge categories:nil]
                ];
    
    [[UIApplication sharedApplication] cancelAllLocalNotifications];
    
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}



/*
 사용자가 지오팬스 안으로 들어가거나 or 나갈때를 모니터링
 지오 펜스 이벤트에 반응하는 관련 델리게이트 메서드를 구현.
 */
#pragma mark - CLLocationManagerDelegate

//사용자가 지오팬스에 들어갔을때 호출되는 메소드
- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region{
    if ([region isKindOfClass:[CLCircularRegion class]]) {
        
        [self handleRegionEvent:region];
    
    }
}

//사용자가 지오펜스로부터 나올때 호출되는 메소드
- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region{
    if ([region isKindOfClass:[CLCircularRegion class]]) {
       
        [self handleRegionEvent:region];
    
    }
}

#pragma mark - Helpers

//지오펜스 반경에 들어왔을때
- (void)handleRegionEvent:(CLRegion *)region{
    
    //1.앱이 활성화 상태일때
    if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive) {
        
        //2.식별자를 이용해서 지오펜스의 메시지 반환받기
        NSString *message = [self noteFromRegionIdentifier:region.identifier];
        if (message) {
            UIViewController *viewController = self.window.rootViewController;
            if (viewController) {
                
                //3.alert 로 메시지 띄워주기
                [Utilities showSimpleAlertWithTitle:nil message:message viewController:viewController];
            }
        }
    }else{
        //앱이 비활성화 상태일때
        UILocalNotification *notification = [UILocalNotification new];
        [notification setAlertBody:[self noteFromRegionIdentifier:region.identifier]];
        [notification setSoundName:@"Default"];
        [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
    }
}


//사용자에게 지오 펜스 이벤트 알림
/*
 CLCircularRegion 델리게이트 호출에 의해 반환 된 트리거링과 관련된 메모를 얻으려면
 UserDefaults 에서 해당 Geotification을 검색해야합니다.
 CLCircularRegion 에 등록한 고유 식별자를 사용하여 노트를 찾을 수  있다 .
 */
- (NSString *)noteFromRegionIdentifier:(NSString *)identifier{
    NSArray *savedItems = [[NSUserDefaults standardUserDefaults] arrayForKey:kSavedItemsKey];
    if(savedItems){
        
        for (id savedItem in savedItems) {
            Geotification *geotification = [NSKeyedUnarchiver unarchiveObjectWithData:savedItem];
            if ([geotification isKindOfClass:[Geotification class]]) {
                if ([geotification.identifier isEqualToString:identifier]) {
                    return geotification.note;
                }
            }
        }
        
    }
    return nil;
}

@end

 

프로젝트 예제

drive.google.com/file/d/10SK5CY-qZO9xp4u7eD11IZ9HerFd-qS9/view?usp=sharing

 

 

✅ 참고

NSCoding 

https://baked-corn.tistory.com/60

https://jinios.github.io/ios/2018/03/30/archive/

 

지오팬스 듀토리얼

https://www.raywenderlich.com/17649843-geofencing-with-core-location-getting-started