iOS. Приемы программирования Нахавандипур Вандад
NSError *error = nil;
NSURL *documentFolderUrl = [fileManager URLForDirectory: NSDocumentDirectory
inDomain: NSUserDomainMask
appropriateForURL: nil
create: YES
error:&error];
if (error == nil && documentFolderUrl!= nil){
NSString *fileName = @"MyFile.txt";
NSString *filePath = [documentFolderUrl.path
stringByAppendingPathComponent: fileName];
return filePath;
}
return nil;
}
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
/*
Предпосылки:
1) подписать приложение валидным профилем инициализации;
2) в вашем профиле должна быть активизирована полная защита файла;
3) добавить в проект разрешения на подписывание кода.
*/
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([self filePath]!= nil){
NSData *dataToWrite = [@"Hello, World"
dataUsingEncoding: NSUTF8StringEncoding];
NSDictionary *fileAttributes = @{
NSFileProtectionKey: NSFileProtectionComplete
};
BOOL wrote = [fileManager createFileAtPath: [self filePath]
contents: dataToWrite
attributes: fileAttributes];
if (wrote){
NSLog(@"Successfully and securely stored the file");
} else {
NSLog(@"Failed to write the file");
}
}
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Обсуждение
Пользователи доверяют вашим приложениям. Поэтому, когда вы запрашиваете у пользователя определенную личную информацию, например имя и фамилию, пользователь рассчитывает, что эта информация будет храниться в хорошо защищенном месте, где до нее не доберутся хакеры или кто-нибудь, кто получает временный доступ к пользовательскому устройству с iOS.
Предположим, вы пишете приложение для редактирования фотографий. Работая с этим приложением, пользователь может подключить камеру к своему устройству с iOS, импортировать свои фотографии в ваше приложение, а потом пользоваться им для редактирования, сохранения фотографий и раздачи их друзьям. Вы могли бы поступить так, как делают очень многие разработчики приложений: импортировать эти фотографии в папку Documents (Документы), где они сразу будут готовы к редактированию. Но с таким подходом связана одна проблема: любой файловый менеджер для iOS, который можно свободно скачать в Интернете, может считывать содержимое папки Documents (Документы) в любом приложении, даже если устройство заблокировано. Чтобы защитить пользовательские данные, следует активизировать защиту тех файлов, которые вы храните в песочнице приложения. Защита файлов — неотъемлемая часть безопасности пользовательского устройства, в частности, пароля к этому устройству. Допустим, пользователь установил на устройстве пароль, без которого устройство нельзя разблокировать (пусть даже этот пароль совсем простой), и такая блокировка произошла. В таком случае после того, как устройство будет заблокировано, все файлы, сохраненные в песочнице вашего приложения и обладающие ключом NSFileProtectionComplete, будут недоступны для посторонних. Прочитать такие файлы не сможет даже файловый менеджер.
Итак, при релизе вашего приложения и даже на этапе его разработки задавайте профили для разработки и распространения программы так, чтобы в них действовала необходимая защита файлов, распространяющаяся на конфиденциальные файлы, находящиеся на диске. Сами определите, какие файлы нуждаются в защите от непрошеных зрителей. Остальные файлы на диске можно и не защищать.
См. также
Разделы 8.0 и 8.6.
8.9. Защита пользовательского интерфейса
Постановка задачи
Необходимо гарантировать, что пользовательский интерфейс соответствует наиболее распространенным правилам безопасности, действующим в iOS.
Решение
Следуйте приведенным далее указаниям.
• Обеспечьте, чтобы вся информация, вводимая пользователем в поля паролей и другие защищенные поля, попадала в экземпляры UITextField, чьим свойствам secureTextEntry присвоено значение YES.
• Если пользователь находится на экране, содержащем персональную информацию, например номер кредитной карточки или домашний адрес, присвойте свойству hidden главного окна вашего приложения значение YES (в методе applicationWillResignActive: делегата вашего приложения). Чтобы само окно отображалось на экране, тому же самому свойству нужно присвоить значение NO в методе applicationDidBecomeActive: делегата приложения. Так вы гарантируете, что на скриншоте пользовательского интерфейса (iOS снимает такой скриншот, когда приложение переходит в фоновый режим) не будет отражаться никакое содержимое вашего окна. Apple рекомендует действовать именно так.
• Обязательно валидируйте пользовательский ввод в текстовых полях/видах перед отправкой этой информации на сервер.
• Пользуясь механизмами, изученными в этой главе, защищайте пользовательские записи, если храните их в файлах на диске или в связке ключей.
• На тех экранах, где вы принимаете пароль или числовой код для аутентификации, очищайте такие поля с кодом/паролем, как только контроллер вида перестает отображаться на экране. Если вы не будете уступать и высвобождать эти контроллеры видов, их содержимое будет сохраняться в памяти. В частности, в памяти будут находиться записи из защищенных текстовых полей, находящихся в этих контроллерах видов. Целесообразно высвобождать память, содержащую конфиденциальные данные, сразу же после того, как исчезает необходимость в этих данных.
Обсуждение
В этом списке лишь второй элемент требует дополнительного объяснения. Когда пользователь видит окно приложения на экране своего устройства с iOS и переводит это приложение в фоновый режим, возвращаясь на главный экран (нажимая Home), iOS помещает приложение в неактивное состояние. Когда приложение в таком состоянии переведено в фоновый режим, iOS делает скриншот пользовательского интерфейса этого приложения (в точном соответствии с изображением на экране) и сохраняет этот файл в каталоге Library/Caches/Snapshots/. Данный каталог находится в песочнице вашего приложения. Как только пользователь вновь переводит приложение в приоритетный режим, iOS сразу же отображает этот скриншот, и он остается на экране до тех пор, пока приложение не «оживет» окончательно и не примет управление экраном. Поэтому переход из фонового в приоритетный режим в iOS получается очень плавным. Но хотя такая практика и очень положительна с точки зрения удобства использования (UX), она привносит определенную проблему в области безопасности. Дело в том, что если на скриншоте была зафиксирована конфиденциальная информация, то она будет сохранена и на диске. Мы не можем полностью отключить эту функцию в iOS, однако можем нейтрализовать ее негативное влияние на безопасность приложения. Чтобы это сделать (кстати, такая практика рекомендуется Apple), нужно накрыть основное окно нашего приложения другим видом либо скрыть содержимое этого окна, устанавливая свойство hidden окна приложения в значение YES, когда приложение становится неактивным. При переходе приложения в активное состояние мы вновь присваиваем этому свойству значение NO (и окно снова становится видимым).
iOS-разработчики, стремящиеся выполнять это требование безопасности, часто совершают одну ошибку. Они пытаются устанавливать в YES или NO значение свойства hidden экземпляра keyWindow. Даже хотя окно keyWindow экземпляра приложения будет валидным окном в момент, когда приложение становится неактивным, в момент «оживления» приложения keyWindow равно nil — то есть указывает в никуда. Следовательно, во избежание возможных ошибок просто пользуйтесь свойством window делегата приложения, отображая или скрывая окно.
Другая проблема, касающаяся безопасности приложения, связана с «оседанием» персональных данных в контроллерах видов. Предположим, у вас есть контроллер вида для входа в систему. Здесь пользователь вводит свои имя и пароль. Как только будет нажата кнопка, которая зачастую называется Login (Вход в систему), вы отправляете учетные данные пользователя на сервер по сетевому HTTPS-соединению. Когда произойдет аутентификация пользователя, вы выдвигаете на экран другой контроллер вида. Проблема, связанная с таким подходом, заключается в том, что имя и пароль, введенные пользователем на предыдущем экране, по-прежнему остаются в памяти (как и сам контроллер вида). Как вы помните, навигационный контроллер включает целый стек контроллеров видов.
Чтобы справиться с этой проблемой и повысить безопасность пользовательского интерфейса, можно присвоить свойству text защищенных текстовых полей значение nil. Это делается в тот самый момент, когда на экран выдвигается второй контроллер вида. Другой способ — переопределить метод экземпляра viewWillDisappear: контроллера вида для входа в систему, а также установить свойство text текстовых полей в nil прямо здесь. Тем не менее такой подход требует известной осторожности, так как упомянутый метод экземпляра контроллера вида вызывается всякий раз, когда контроллер исчезает с экрана — например, когда пользователь покидает вкладку с контроллером вида, уходит на другую вкладку, а потом возвращается на первую. Действительно, при таком переходе контроллер вида успевает и исчезнуть с экрана, и вновь там появиться. Поэтому если пользователь всего лишь перешел на соседнюю вкладку, а вы уже стерли всю информацию, введенную в поля, то при возвращении на первую вкладку пользователь найдет там лишь пустые окошки и будет вынужден заносить всю информацию заново. Разработка всегда ведется в соответствии с поставленными бизнес-требованиями, поэтому не существует однозначного ответа на вопрос о том, как справиться с такой ситуацией.
См. также
Разделы 8.2 и 8.8.
Глава 9. Core Location и карты
9.0. Введение
Фреймворки Core Location и Map Kit можно применять для создания приложений, приспособленных для обработки геолокационной информации (информации о местоположении) и картографических приложений. Фреймворк Core Location использует оборудование устройства для определения актуального местонахождения этого устройства. Фреймворк Map Kit, в свою очередь, позволяет программе отображать для пользователя карты, снабжать карту определенными аннотациями и т. д. С чисто программистской точки зрения доступность геолокационных сервисов зависит от наличия на устройстве необходимого оборудования; если оборудование имеется, то оно должно быть активизировано и подключено для работы с фреймворком Core Location или Map Kit. Устройство с операционной системой iOS, оснащенное службами GPS (системы глобального позиционирования), позволяет работать с технологиями 2G, EDGE, 3G, 4G и другими, которые помогают определять местоположение пользователя. В настоящее время практически на любых устройствах с iOS поддерживаются геолокационные службы, но программисту рекомендуется проверять доступность таких сервисов и приступать к работе с ними, лишь убедившись в их наличии. Ведь мы и в самом деле не можем знать наверняка, не будет ли в будущем Apple выпускать какое-либо устройство, на котором не будет всего оборудования, необходимого для обеспечения геолокационных функций.
В новом компиляторе LLVM, предоставляемом в Xcode для iOS 7, Apple реализовала концепцию модулей. В более ранних версиях SDK и Xcode для использования фреймворков Core Location и Map Kit требовалось вручную импортировать эти фреймворки в целевой проект. Но с появлением модулей для добавления этих фреймворков требуется всего лишь импортировать их заголовочные файлы в классы проекта, вот так:
#import <CoreLocation/CoreLocation.h>
#import <MapKit/MapKit.h>
И все. Фреймворки Core Location и Map Kit окажутся в ваших проектах.
9.1. Создание картографического вида
Постановка задачи
Необходимо инстанцировать и отобразить карту в экранном виде.
Решение
Создайте экземпляр класса MKMapView, после чего добавьте его к виду либо присвойте подвиду контроллера вашего вида. Вот пример. h-файла такого контроллера вида, в котором создается экземпляр MKMapView, после чего этот вид отображается в полноэкранном режиме:
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface ViewController ()
@property (nonatomic, strong) MKMapView *myMapView;
@end
@implementation ViewController
Это обычный корневой контроллер вида, содержащий переменную MKMapView. В следующем коде в реализации данного контроллера вида (.m-файле) мы инициализируем карту и зададим для нее тип Satellite:
— (void)viewDidLoad{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.myMapView = [[MKMapView alloc]
initWithFrame: self.view.bounds];
/* Задаем Satellite в качестве типа карты. */
self.myMapView.mapType = MKMapTypeSatellite;
self.myMapView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
/* Добавляем карту к нашему виду. */
[self.view addSubview: self.myMapView];
}
Обсуждение
Создать экземпляр класса MKMapView довольно легко. Можно просто присвоить ему рамку, воспользовавшись его же конструктором, а после того как карта будет создана, добавить ее в качестве подвида к виду, который в настоящий момент отображается на экране. И все, мы сможем просматривать карту.
MKMapView — это подкласс UIView. Таким образом, можно манипулировать любым картографическим видом тем же способом, каким вы работаете с экземпляром UIView. К примеру, мы пользуемся свойством UIView для того, чтобы вставить в вид его свойство backgroundColor.
Вы, наверное, уже заметили, что у класса MKMapView есть свойство под названием mapType, характеризующее тип карты. Карта может быть спутниковой, стандартной или гибридной. В примере мы пользуемся картой спутникового типа (рис. 9.1).
Рис. 9.1. Вид карты, выполненной со спутника
Можно изменить визуальное представление карты определенного типа, воспользовавшись свойством mapType экземпляра MKMapView. Это свойство может принимать следующие значения:
• MKMapTypeStandard — применяется для отображения стандартной карты (задается по умолчанию);
• MKMapTypeSatellite — позволяет отобразить вид карты, выполненной со спутника (как показано на рис. 9.1);
• MKMapTypeHybrid — дает возможность накладывать стандартную карту на спутниковую.
9.2. Обработка событий картографического вида
Постановка задачи
Необходимо обрабатывать различные события, которые картографический вид может посылать своему делегату.
Решение
Присвойте объект делегата, соответствующий протоколу MKMapViewDelegate, свойству delegate, которое относится к экземпляру класса MKMapView:
— (void)viewDidLoad{
[super viewDidLoad];
/* Создаем карту размером с наш вид. */
self.myMapView = [[MKMapView alloc]
initWithFrame: self.view.bounds];
/* Задаем Satellite в качестве типа карты. */
self.myMapView.mapType = MKMapTypeSatellite;
self.myMapView.delegate = self;
self.myMapView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
/* Добавляем карту к нашему виду. */
[self.view addSubview: self.myMapView];
}
Этот код легко запустить в методе viewDidLoad, относящемся к объекту контроллера вида, если объект имеет свойство MapView типа MKMapView:
#import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface ViewController () <MKMapViewDelegate>
@property (nonatomic, strong) MKMapView *myMapView;
@end
@implementation ViewController
Обсуждение
Объект, являющийся делегатом экземпляра класса MKMapView, должен реализовывать методы, описанные в протоколе MKMapViewDelegate. Эти методы необходимы для получения различных сообщений от картографического вида и, как будет показано позднее, для предоставления информации картографическому виду. В протоколе MKMapViewDelegate определяются различные методы, в том числе метод mapViewWillStartLoadingMap:, вызываемый в объекте делегата всякий раз, когда начинается процесс загрузки карты. Не забывайте, что делегат для картографического вида не является обязательным объектом, то есть картографические виды можно создавать и не присваивая им делегатов. Просто картографические виды, лишенные делегатов, не будут реагировать на действия пользователя.
Вот список некоторых методов, объявляемых в протоколе MKMapViewDelegate (здесь также рассказано, о чем они должны сообщать объекту-делегату экземпляра MKMapView):
• mapViewWillStartLoadingMap: — вызывается применительно к объекту делегата всякий раз, когда картографический вид начинает загружать данные, обеспечивающие визуальное представление карты пользователю;
• mapView: viewForAnnotation: — вызывается применительно к объекту делегата всякий раз, когда картографический вид требует от экземпляра MKAnnotationView снабдить карту визуальными аннотациями. Подробнее об этом механизме будет рассказано в разделе 9.4;
• mapViewWillStartLocatingUser: — как понятно из названия, метод вызывается применительно к объекту делегата всякий раз, когда картографический вид приступает к определению местоположения пользователя. Подробнее о том, как сделать это, будет рассказано в разделе 9.3;
• mapView: regionDidChangeAnimated: — вызывается применительно к объекту делегата всякий раз, когда изменяется регион, отображаемый на карте.
См. также
Разделы 9.3 и 9.4.
9.3. Отметка местоположения устройства
Постановка задачи
Необходимо найти широту и долготу той точки, в которой находится устройство.
Решение
Воспользуйтесь классом CLLocationManager:
— (void)viewDidLoad {
[super viewDidLoad];
if ([CLLocationManager locationServicesEnabled]){
self.myLocationManager = [[CLLocationManager alloc] init];
self.myLocationManager.delegate = self;
[self.myLocationManager startUpdatingLocation];
} else {
/* Геолокационные службы не активизированы.
Попробуйте исправить ситуацию: например предложите пользователю
включить геолокационные службы. */
NSLog(@"Location services are not enabled");
}
}
В данном коде myLocationManager — это свойство типа CLLocationManager. В приведенном примере кода данный класс также является делегатом диспетчера местоположения (Location Manager).
Обсуждение
Фреймворк Core Location, входящий в состав комплекта SDK, предоставляет программисту функционал, который позволяет определять актуальное положение устройства с системой iOS в пространстве. Поскольку в iOS пользователь может отключать определение местоположения в разделе Settings (Настройки), то мы перед тем, как инстанцировать объект типа CLLocationManager, проверим, работают ли на устройстве геолокационные службы.
Объект, являющийся делегатом CLLocationManager, должен соответствовать протоколу CLLocationManagerDelegate.
Вот как мы объявим объект нашего диспетчера местоположения в. h-файле контроллера вида (создавать экземпляр CLLocationManager может и объект, не являющийся контроллером вида):
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface ViewController () <CLLocationManagerDelegate>
@property (nonatomic, strong) CLLocationManager *myLocationManager;
@end
@implementation ViewController
Контроллер нашего вида будет иметь следующую реализацию:
— (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation{
/* Получена информация о новом местоположении. */
NSLog(@"Latitude = %f", newLocation.coordinate.latitude);
NSLog(@"Longitude = %f", newLocation.coordinate.longitude);
}
— (void)locationManager:(CLLocationManager *)manager
didFailWithError:(NSError *)error{
/* Не удалось получить информацию о местоположении пользователя. */
}
— (void)viewDidLoad {
[super viewDidLoad];
if ([CLLocationManager locationServicesEnabled]){
self.myLocationManager = [[CLLocationManager alloc] init];
self.myLocationManager.delegate = self;
[self.myLocationManager startUpdatingLocation];
} else {
/* Геолокационные службы не активизированы.
Попробуйте исправить ситуацию: например предложите пользователю
включить геолокационные службы. */
NSLog(@"Location services are not enabled");
}
}
Метод экземпляра startUpdateLocation, относящийся к классу CLLocationManager, сообщает делегату о том, удалось или нет получить информацию о местоположении пользователя. Это делается с помощью методов location Manager: didUpdateToLocation: fromLocation: и locationManager: didFailWithError: объекта делегата, именно в таком порядке.
9.4. Отображение маркеров в картографическом виде
Постановка задачи