Главная iPhone Mac OS X Форум О себе

Пишем правильный текстовый редактор

Мне всегда нравилось приложение Заметки в моем iPod Touch. Но оно имеет небольшой недостаток, который не позволяет использовать программу с максимальной эффективностью — она не работает в ландшафтном режиме, что не позволяет использовать более крупную клавиатуру и дальше разнесенные клавиши, которые позволяют уменьшить количество ошибок и обеспечивают больше удобства при наборе текста. Предлагаю исправить этот недостаток и написать правильный текстовый редактор. Статья на этот раз получилась очень большая, но согласно моей задумке она и не могла быть меньше, чтобы реализовать самый минимум необходимых нам функций. Возможно, вам придется читать пост по частям или в несколько подходов, но так даже лучше — вы не торопясь пройдете по каждому пункту, успеете осмыслить полученные знания и приобретете понимание того, как устроена несложная на первый взгляд программа изнутри.

Итак, наши задачи на сегодня:

  • знакомство с новыми для нас классами UIKit: UIViewController, UINavigationController, UINavigationBar, UINavigationBarItem
  • отслеживание ландшафтного и портретного положения iPhone и поворот интерфейса нашей программы
  • сохранение данных при выходе из программы с использованием property list

Начните новый проект на основе уже знакомого нам шаблона “Window-based Application” и назовите его “txtEdit”. В конце моего повествования вы поймете, что могли бы использовать при создании проекта другой шаблон (попробуйте сами догадаться какой), но я предлагаю вам учиться на основе минимально подготовленной базы, чтобы вы поняли как строится даже самое сложное приложение с нуля.

Знакомство с новыми классами UIKit

Откройте файл интерфейса и попробуйте представить, что бы вы сделали, основываясь на знаниях, полученных в предыдущих постах, для создания интерфейса описанного выше приложения. Правильно, добавили бы на форму объект UIView, поверх него UITextView. Сделайте так, сохранитесь и запустите ваше приложение.

Interface Builder первый вариант интерфейса txtEdit

Ну не красота ли? Ни единой строчки кода, а у нас уже готова половина приложения: работает редактор, выезжает клавиатура и даже появляется при длительном нажатии на тексте лупа для точного позиционирования курсора при редактировании текста.

txtEdit на iPhone Simulator

Но нам не хватает двух вещей:

  1. не работает вращение интерфейса при повороте аппарата (повернуть iPhone в симуляторе можно кнопками Cmd+стрелки вправо/влево)
  2. отсутствует кнопка окончания редактирования, которая убирает клавиатуру с экрана

В этом месте следовало бы включить фанфары и поприветствовать два новых класса, которые помогут нам в решении создавшихся проблем: для решения первого вопроса нам понадобится класс UIViewController, для второго - UINavigationController.

Удалите из вашего интерфейса объекты UIVew и UITextView, будто мы ничего еще не делали. Перетяните в окно nib-файла объект UINavigationController из библиотеки компонентов, раскройте дерево подчинения для этого объекта в том же окне и немного передохните, пока я расскажу о нашем новом знакомом и всех его родственниках.

UINavigationController

Начну я свой рассказ с UIViewController. Как вы уже догадались из названия, этот класс создан для контроля работы объекта UIView. В наших целях будет пригодна одна из его функций — вращение интерфейса вслед за аппаратом. Кроме того, у объекта UIViewController есть несколько необязательных Outlet переменных для привязки к объектам навигации: tabBarItem и navigationItem. О применении последнего я расскажу чуть позже, а пока дам пояснение с какой целью эти переменные используются.

Элементы навигации в приложении

Каждое приложение может использовать Navigation Bar и/или Tab Bar для навигации между окнам (говорю “окна” — подразумеваю View). UIViewController содержит для каждого такого “окна” связанные с ним значения View, NavigationItem и TabBarItem. Последние два элементам могут присутствовать опционально. В нашем случае используется только NavigationItem.

Над UIViewController стоит UINavigationController, который используется для перемещения вперед-назад по окнам приложения и отвечает за анимацию при смене окон. Для выполнения этих целей у UINavigationController имеется стек из объектов UIViewController, и в момент работы приложения демонстрируется только самый верхний View от ViewController из данного стека. Такой подход к хранению элементов значительно облегчает возврат к предыдущему состоянию.

Например, когда мы нажимает на кнопку “+” в Заметках, метод, обрабатывающий нажатие, отправляет сообщение pushViewController:animated: в объект UINavigationController, который добавляет на вершину стека полученный объект UIViewController, и подменяет значения активного View, TabBarItem и NavigationItem на значения из полученного объекта. Когда мы нажимаем кнопку возврата к списку заметок, метод, обрабатывающий нажатие на эту кнопку, отправляет сообщение popViewController в объект UINavigationController, который снимает верхний элемент стека из ViewController и возвращает нас к предыдущему состоянию окна приложения.

Подробнее о работе UINavigationController мы поговорим в следующей статье, а пока добавление данного объекта представляет собой заготовку на будущее, чтобы не калечить интерфейс программы в следующий раз.

Если UINavigationController управляет стеком UIViewController, то объект UINavigationBar, манипулирует стеком UINavigationItem. Принцип работы у него тот же самый: он получает новые значения NavigationItem либо из активного ViewController, либо вручную, отправкой сообщения pushNavigationItem:animated:, а может и не получать совсем. Дело в том, что использование UINavigationItem элементов не обязательно. В случае отсутствия такого объекта у активного UIViewController’а, NavigationBar создаст строку навигации согласно своему усмотрению: если это не первое окно, то добавится кнопка Back, нажатие на которую автоматически приведет к вызову метода popViewController для объекта UINavigationController.

Вот такие непростые взаимоотношения между нашими новыми знакомыми. Если какой-то аспект работы описанных выше классов будет для вас неясен, просьба не стесняться обратиться с вопросом в комментариях.

Возвратимся к нашему приложению и добавим объект UIView, а поверх него UITextView в окно ViewController’а. После добавления очистите значение TextView. Должна получиться следующая картина:

Окончательный вариант интерфейса txtEdit в окне Interface Builder

Теперь в закладке Identity инспектора свойств измените класс объекта UIViewController на txtEditViewController и создайте файлы нового класса, вызовом пункта меню Write Class Files как в прошлой статье. После чего откройте заголовочный файл txtEditViewController.h и укажите в качестве родительского класса UIViewController.

Вращение интерфейса программы вслед за положением iPhone

Пришло время развлечений — давайте заставим интерфейс нашей программы крутиться! Но вам придется сильно напрячь мозги, ибо код, который я вам продемонстрирую, требует чрезвычайно высокой концентрации внимания, чтобы понять как он действует. Впишите в файл txtEditViewController.m реализацию следующего метода:


-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    return YES;
}

Это самый сложный код для такого простого действия за всю мою жизнь :)

Если серьезно, этот метод возвращает YES или NO в зависимости от желания программы изменять положение своего интерфейса в зависимости от поворота iPhone. Вы можете отслеживать специфические положения аппарата, анализируя переменную interfaceOrientation, принимающая четыре различных значения по углу поворота.

Но прежде чем опробовать в деле этот код, нам потребуется добавить в класс txtEditAppDelegate переменную navController и связать ее значение с объектом UINavigationContoller в нашем интерфейсе. Надеюсь, что вам, моим читателям, подготовленным на предыдущих статьях, эта задача не покажется сложной :) После связывания и добавления свойства, не забудьте вызвать директиву @syntesize в файле txtEditAppDelegate.m.

Мы с вами ввели новую переменную navController для того, чтобы получить ссылку на View, который находится под неуспыпным контролем UINavigationController. Для того, чтобы мы не городили лишнего кода, выбирая из стека NavigationController верхний элемент UIViewController, а затем получали из него значение View, класс UINavigationController имеет переменную view, которой мы воспользуемся для того, чтобы добавить его в окно нашего приложения. Или вы уже забыли про UIWindow? А ведь именно оно, а ничего-то другое показывается при запуске приложения. Исправьте код txtEditAppDelegate.m в соответствии с примером:


#import "txtEditAppDelegate.h"

@implementation txtEditAppDelegate

@synthesize window;
@synthesize navController;

— (void)applicationDidFinishLaunching:(UIApplication *)application {
    [window addSubview:navController.view];
   
    // Override point for customization after app launch   
    [window makeKeyAndVisible];
}

— (void)dealloc {
    [navController release];
    [window release];
    [super dealloc];
}

@end

Нанесите последний штрих для нашего приложения, исправив значение Title для объекта UINavigationItem на “Txt Edit”, и запустите программу. Если вы все сделали правильно, то по окончании компиляции вы увидите и сможете сделать следующее:

Вращение интерфейса программы в зависимости от положения iPhone

Кнопка завершения редактирования

Все хорошо, но чего-то не хватает. Ну конечно же! Хотелось бы закончить редактирование текста, убрать клавиатуру с экрана и придаться чтению своих, без сомнения, гениальных мыслей, изложенных на экране iPhone. Для того, чтобы это сделать, нужно снять с объекта UITextView ответственность за обработку ввода пользователя. Помните, в предыдущей статье я говорил о First Responser, о том, что этот объект постоянно меняется во время выполнения программы. Так вот, когда вы нажимаете на поле TextView, чтобы начать ввод текста, этим объектом становится объект UITextView. Чтобы сбросить с себя полномочия First Responder’а для всех визуальных объектов существует метод resignFirstResponder, который объект должен вызвать для самого себя. Для этого мы добавим кнопку “Done” на наш Navigation Bar, которая будет выполнять отправку данного сообщения в Text View.

Самый догадливые уже поняли, что нам потребуется еще одна Outlet переменная, назовем ее txtView, которая будет ссылаться на объект UITextView. Самостоятельно добавьте ее в класс txtEditViewController, свяжите с объектом на форме и не забудьте освободить память из под нее в dealloc (не забывайте про [super dealloc], когда будете писать реализацию деструктора).

А сейчас я предлагаю вам поступить мудро с точки зрения построения грамотного пользовательского интерфейса, и показывать кнопку завершения редактирования только когда TextView является First Responder. В решении этой задачи нам поможет метод textViewDidBeginEditing протокола UITextViewDelegate, который вызывается как только поле ввода становится активным. Нам потребуется написать реализация протокола, но предлагаю для уменьшения количества файлов в проекте назначить в качестве делегата…txtEditViewController. Почему нет? Вы можете назначать для каждого класса реализацию не одного, а даже нескольких протоколов, если потребуется.

Измените заголовочный файл txtEditViewController.h, указав протокол после родительского класса:


@interface txtEditViewController : UIViewController <UITextViewDelegate>

Теперь в Interface Builder свяжите предопределенную переменную delegate объекта UITextView с объектом класса txtEditViewController, и приступим к реализации метода:


-(void)textViewDidBeginEditing:(UITextView *)textView {
    UIBarButtonItem *buttonDone = [[UIBarButtonItem alloc]
                                    initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                                    target:self
                                    action:@selector(doneAction:)];
    self.navigationItem.rightBarButtonItem = buttonDone;
    [buttonDone release];
}

Вначале мы создаем новую кнопку на основе предопределенного набора. Вы можете поглядеть список возможных вариантов UIBarButtonSystemItem в документации XCode и убедиться своими глазами, что набор настолько широк, что врядли вам когда-либо потребуется создание кнопки вручную. К слову, у данного класса есть еще 3 варианта инициализации: initWithTitle:style:target:action: — кнопка с указанной надписью, initWithImage:style:target:action: — кнопка с изображением и initWithCustomView — кнопка с указанным при инициализации View. В параметре target указывается объект, метод которого обрабатывает нажатие на кнопку, а параметр action — непосредственно ссылка на на этот метод.

Переменная navigationItem нашего ViewContoller содержит ссылку на объект UINavigationItem, содержимое которого определяется только надписью “Txt Edit” в верхней части окна. Свойство rightBarButtonItem определяет кнопку, расположенную в правой части Navigation Bar. В целом код достаточно прозрачен, главное не бояться лезть в документацию и пробовать экспериментировать. Например, попробуйте создать различные варианты кнопок или поменять расположение кнопки на строке навигации — это просто.

Теперь пришло время описать метод doneAction:


-(void)doneAction:(id)sender {
    [txtView resignFirstResponder];
   
    self.navigationItem.rightBarButtonItem = nil;
}

Параметр sender содержит ссылку на объект, из которого произошел вызов метода. В следующей строке мы, как и планировали, снимаем с поля TextView полномочия First Responder’а, а затем убираем со строки навигации кнопку завершения редактирования. Теперь пришло время запустить программу и радоваться результатам совместного творчества.

Работа кнопки завершения редактирования

Сохранение и восстановление сеанса работы приложения

Всем наш редактор хорош, но стоит выйти из него (нажать на кнопку Home), как все данные теряются. Нам потребуется написать несколько методов, отвечающих за сохранение и восстановление данных. Но прежде чем мы приступим к усиленному кодированию, в очередной раз сделаю небольшое отступление и расскажу о том, как работает приложение на iPhone, в каких каталогах и в каком окружении.

Каждое приложения в iPhone располагается в отдельном каталоге и доступ к чтению и записи файлов ограничен только этой и вложенными папками. После установки приложения в аппарат автоматически создаются несколько каталогов:

  • <Application_Home> — каталог отданный под наше приложение. Путь к нему может быть вида /var/mobile/Applications/30B51836-D2DD-43AA-BCB4-9D4DADFED6A2/ (последние цифры для каждого приложения уникальные). Учтите, что путь к папке приложения в симуляторе сильно отличается от пути на аппарате.
  • <Application_Home>/AppName.app — bundle directory. Пишу специально без перевода, ибо именно на такое название существует множество ссылок в описании методов в Cocoa Touch. Так как каждое приложение для iPhone должно быть подписано перед тем как будет установлено на аппарат, вы не должны вносить никакие изменения в этом каталоге. Возможно для вас будет откровением, но что на компьютере, что на телефоне приложение представляет собой не один файл. Если вы установите себе mucommander и пойдете в папку Applications на вашем Маке, то увидите, что каждое приложение представляет собой отдельную папку, содержащую множество файлов: ресурсов, файлов конфигурации, библиотек. Но для простоты установки и удаления программ структура папок скрыта от пользователя, за что я Mac OS благодарен.
  • <Application_Home>/Documents/ — вы можете использовать данную папку для хранения любых данных.
  • <Application_Home>/Library/Preferences/ — эта директория содержит файлы настроек вашего приложения. Для доступа к ним существует отдельный класс NSUserDefaults, работу которого мы рассмотрим в следующий раз.
  • <Application_Home>/tmp/ — этот каталог используется для хранения временных файлов, причем ваше приложение в ответе за очистку этой папки, т.к. у iPhone нет автоматической очистки временных каталогов.

Содержимое всех папок, кроме tmp, автоматически резервируются при синхронизации с iTunes каждый раз, когда вы подключаете аппарат к компьютеру.

Самая подходящая папка для хранения данных нашего приложения — Documents. Полный путь к папке Application_Home возвращает метод NSHomeDirectory, входящий в состав библиотеки Foundation. Чтобы избежать проблем с жестко привязанными путями, мы будем использовать метод NSSearchPathInDirectoriesInDomains, чтобы получить путь непосредственно к папке Documents. Этот метод используется для создания списка папок, согласно указанному в параметрах вызова фильтру. Более подробную информацию вы сможете найти в документации, а мы разберем параметры нашего вызова:


NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

Метод возвращает массив, содержащий список папок Documents (NSDocumentDirectory), поиск производится в папке пользователя (NSUserDomainMask) и все пути преобразуются в полные (третий параметр указывает необходимость преобразования путей вида ~/ к полному пути). Так как папка Documents для нашего приложения существует всего в одном экземпляре, то первый элемент массива будет содержать искомый нами путь.


NSString *documentsDirectory = [paths objectAtIndex:0];

Пришло время рассказать о вариантах хранения данных в iPhone:

  • Foundation представляет следующие классы работы с файлами:
    • вы можете преобразовать данные вашего приложения в объект NSData, используя класс NSPropertyListSerialization, а затем записать на диск полученный property list, используя втроенные методы NSData
    • если наше приложение реализует протокол NSCoding, то можно использовать соответствующие методы для сериализации объектов нашей программы и восстановление их обратно
    • класс NSFileHandle представляет доступ к записи и чтению файла, а класс NSFileManager — манипуляции файлами и директориями на диске
  • Core OS представляет два варианта:
    • использование функций fopen, fread и fwrite
    • работа с крупными файлами посредством методов mmap и munmap

Конечно, я не буду рассматривать работу каждого из этих вариантов, просто имейте ввиду, что следует использовать один из перечисленных выше способов доступа к файлам в зависимости от потребностей вашей программы.

Мы же с вами будет использовать самый первый вариант. Более опытные из вас могут сказать, что это не самый оптимальный вариант, но я хочу показать работу с property list и использования их для сохранения данных при выходе из приложения.

Property list представляют собой объект класса NSData, преобразование к которому можно произвести при помощи класса NSPropertyListSerialization. В качестве входных параметров могут выступать объекты классов NSData, NSString, NSNumber, NSDate, NSArray или NSDictionary. Также не запрещено использовать объект (например, NSDictionary, как мы будем это делать чуть ниже) для хранения других данных, но они также должны представлять собой объекты классов, перечисленных выше.

При создании property list обязательно указывайте двоичный формат хранения данных. Это позволит сэкономить место на диске и время на чтение и запись данных. Также учтите, что property list не могут содержать данные объемом свыше нескольких сотен килобайт. Для подобных случаев вам потребуется рассмотреть другие варианты хранения временных данных.

И последнее, с чем нам требуется определиться, — выбор места, где будет производится сохранение состояния приложения в момент выхода и место восстановления данных. Первым на ум приходит готовый метод applicationWillTerminate для сохранения данных и метод applicationDidFinishLaunching для восстановления данных, которые реализует наш класс txtEditAppDelegate в рамках протокола UIApplicationDelegate. Но в этом случае нам понадобится дополнительная Outlet переменная для связи с ViewController’ом, в котором находится поле TextView, данные которого мы собираемся хранить. Может нам стоит расположить процедуры сохранения сеанса в классе txtEditViewController?

Класс UIViewController содержит пару методов, которые вызываются в момент открытия и сокрытия View, который он контролирует — это viewWillDisappear:animated: и viewDidAppear:animated:. Первый из них вызывается в момент, когда View еще присутствует на экране, второй — когда View уже загрузилось на экран. Похожие на них методы viewDidDisappear и viewWillAppear нам не подходят, так как в момент их вызова View, а следовательно и TextView на нем, еще не существуют.

Давайте уже приступим от слов к делу и напишем реализацию этих методов:


-(void)viewWillDisappear:(BOOL)animated {
    NSDictionary *pData = [[NSDictionary alloc] initWithObjectsAndKeys:
                           txtView.text, @"textData",
                           nil];
   
    [self writeApplicationList:pData toFile:@"data.plist"];
   
    [pData release];
}

-(void)viewDidAppear:(BOOL)animated {
    NSDictionary *pData = [self applicationPlistFromFile:@"data.plist"];
   
    if (pData != nil) {
        txtView.text = [pData objectForKey:@"textData"];
    }
}

В качестве методов, реализующих запись и чтение property list будут выступать еще не описанные writeApplicationList:toFile: и applicationPlistFromFile, код которых я наглым образом скоммуниздил из iPhone OS Programming Guide, в одном месте подкорректировав :)

viewWillDisappear начинается с создания словаря, содержащего под ключом “textData” содержимое нашего объекта TextView. В своем приложении вы можете иметь далеко не один параметр, который следует сохранить перед выходом из нее. Самый удобный вариант — сохранить эти данные с привязкой к ключам в словаре. Вы можете добавить дату последнего редактирования документа или заголовок, главное не забудьте в качестве завершающего элемента словаря указать nil.

Объект pData в конце работы метода viewDidAppear освобождать не требуется — он автоматически освобождается при окончании работы с ним. Остальной код достаточно прозрачен и самодокументируем, поэтому приступим к разобру кода следующих двух методов:


-(BOOL)writeApplicationList:(id)pList toFile:(NSString *)fileName {
    NSString *error;
    NSData *pData = [NSPropertyListSerialization dataFromPropertyList:pList
                                            format:NSPropertyListBinaryFormat_v1_0
                                            errorDescription:&error];
    if (!pData) {
        NSLog(@"%@", error);
        [error release];
        return NO;
    }
   
    return [self writeApplicationData:pList toFile:fileName];
}

Метод сериализует поступающие на вход данные (pList) в любом формате (id) в формат property list в двоичном виде (NSPropertyListBinaryFormat_v1_0). Если во время преобразования произойдет ошибка, подробная информация о ней будет возвращена в переменной error, которую в этом случае нужно будет вручную освободить ([error release]). В последней строке мы вызываем метод, осуществляющий запись данных в файл на диске.


-(id)applicationPlistFromFile:(NSString *)fileName {
    NSData *retData;
    NSString *error;
    id pList;
    NSPropertyListFormat format;
   
    retData = [self applicationDataFromFile:fileName];
    if (!retData) {
        NSLog(@"Data file not returned.");
        return nil;
    }
   
    pList = [NSPropertyListSerialization propertyListFromData:retData
                                             mutabilityOption:NSPropertyListImmutable
                                                       format:&format
                                             errorDescription:&error];
    if (!pList){
        NSLog(@"Plist not returned, error: %@", error);
        [error release];
    }
   
    return pList;
}

В retData возвращается содержимое файла data.plist в сериализованном формате. Мы производим преобразование из данного формата обратно в объект класса id. Дело в том, что это мы с вами знаем, что формат объекта после раскодирования — NSDictionary, но в общем случае там может храниться объект любого типа, именно поэтому pList имеет произвольный формат данных, а уже при вызове метода applicationPlistFromFile мы явно указываем тип данных и свободно работаем с ним.

Параметр mutabilityOption указывает на создание изменяемого объекта. Все объекты: NSArray, NSString, NSDictionary — неизменяемые. Их содержимое задается один раз при создании объекта и не может быть изменено в процессе использования. Объекты NSMutableArray, NSMutableString, NSMutableDictionary в противовес первым изменямые. Судите по ситуации и выбирайте этот параметр исходя из ваших потребностей.


-(BOOL)writeApplicationData:(NSData *)data toFile:(NSString *)fileName {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
   
    if (!documentsDirectory) {
        NSLog(@"Documents directory not found!");
        return NO;
    }
   
    NSString *appFile = [documentsDirectory
                         stringByAppendingPathComponent:fileName];
   
    return ([data writeToFile:appFile atomically:YES]);
}

-(NSData *)applicationDataFromFile:(NSString *)fileName {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *appFile = [documentsDirectory
                         stringByAppendingPathComponent:fileName];
    NSData *myData = [[[NSData alloc] initWithContentsOfFile:appFile]
                      autorelease];
    return myData;
}

А здесь мы уже видим работу описанных выше методов поиска домашней директории и папки Documents. Объект класса NSData содержит встроенные методы записи и чтения файла с диска. А в строке NSData *myData = [[[NSData alloc] initWithContentsOfFile:appFile] autorelease]; мы видим, что объект автоматически будет освобожден, что не требует вызова release в методе viewDidAppear, описанном выше.

Чего вы ждете? Запускайте нашу программу! Наше приложение работает как мы и задумывали. Я очень устал, но чертовски доволен собой, а вы? :)

Скачать архив проекта (16Кб)

Комментарий

Пингбэки

[newbie question] Как по нажатию на кнопку отобразить другой view @pyobjc.ru 5.11.2020 21:47
http://pyobjc.ru/2008/08/10/pishem-pravilnyj-tekstovyj-redaktor/
Создать пользовательское событие @pyobjc.ru 22.10.2020 13:55
смотри примерhttp://pyobjc.ru/2008/08/03/znakomstvo-s-interface-builder-svyazi-mezhdu-obektami/и http://pyobjc.ru/2008/08/10/pishem-pravilnyj-tekstovyj-redaktor/

Комментарии

cDima 11.08.2020 1:12

Уффф, чертовски славный урок! Спасибо. Излагаешь коротко, ясно и лаконично, темп правильный.

Признаюcь, я немного запутался при выборе места для методов сохранения состояния. Почему txtEditViewController? Это же контроллер первого view? Он может сменяться другими UIViewControllers в стеке, и, наверно, txtEditViewController не будет вызываться при выходе?

Ещё как улучшить статью: — ссылки на документы, в которых ты сам разбирался для написания этой статьи. (если особые выделяются) — схема классов? кто кого контроллирует не совсем ясно, может только для меня.

В целом — статья по уровню не хуже чем в Cocoa Programming for Mac OS X, а может лучше. Респект!

ответить
Daemon 11.08.2020 2:03

Поддерживаю, материал отличный. Однако, мне английские мануалы читать привычней, просто потому, что там знакомая терминология. Тут слова все знакомы, но бывает применяются не так как обычно, что затрудняет понимания текста.

К примеру такие штуки, делают текст более сложным для понимания “Теперь в закладке Identity инспектора свойств измените класс объекта UIViewController на txtEditViewController ” класс объекта? :) Класс — это класс, объект — это инстанс класса. Имелось ввиду видимо все-таки имя класса, ну и в том же духе.

Спасибо за материал. Низкий поклон.

ответить
Evgeniy Krysanov 11.08.2020 10:57

Когда я пишу “объекта класса …” или “объект …” я подразумеваю экземпляр класса. В Interface Builder Каждый элемент на форме уже является экземпляром класса, потому у меня такие формулировки. Постараюсь в дальнейшем выражаться более ясно :)

ответить
Kонстантин 1.10.2020 19:48

В этой строке ошибка return [self writeApplicationData:pList toFile:fileName]; надо заменить pList на pData

ответить
Evgeniy Krysanov 11.08.2020 11:07

Представь ситуацию, когда происходит операция pushViewController для нашего NavigationController’а. В этот момент происходит подмена View нашего VewController’а на View нового, при этом все те данные, что мы вводили в TextView должны быть сохранены. А как это сделать? Именно благодаря существованию метода viewWillDisappear.

Безусловно, это не единственный выход, существует еще множество мест, где можно произвести сохранение и восстановление данных, но для нашей ситуации (а в следующей статье вы поймете почему я выбрал именно такой подход) эти места идеально подходят для данной операции.

Исправлять текст поста уже не буду, чтобы читатели не получили повторно статью в свои RSS-читалки, — я пользуюсь поиском и примерами кода с developer.apple.com, а также встроенной в XCode документацией. Это, собственно, все мои источники с информацией для данной статьи :)

ответить
cDima 11.08.2020 1:20

По поводу “apple дай мне сертификат” — могу помочь, мой новый сверкающий iphone dev certificate работает на пяти девайсах, 4 из которых мне нужны.

ответить
Daemon 11.08.2020 2:04

А как вы его получали, коли не секрет? :)

ответить
cDima 11.08.2020 11:55

Отец живёт в Германии и заплатил.

ответить
Evgeniy Krysanov 11.08.2020 11:10

Присоединяюсь к вопросу :) Как удалось и из какой страны? За предложение поделиться сертификатом спасибо, но я потерплю до появления своего личного.

ответить
тендер 11.08.2020 14:45

Какие прогнозы по датам появления в России

ответить
Evgeniy Krysanov 11.08.2020 14:56

На мои многочисленные письма товарищи из Apple упорно отмалчиваются. Кроме нашей страны еще очень много государств не имеет Apple Store, а со стороны Apple глупо отказываться от денег, которые желающие стать разработчиками готовы отдать хоть сейчас. Поэтому я надеюсь, что вопрос с оплатой разрешится до конца года.

ответить
hardwarrior 24.10.2020 10:22

Вы пробовали, используя этот сертификат, что то публиковать в аппсторе? На сколько я понял, расчет с девелопером будет производиться на карточку, с которой куплен сертификат?..

ответить
Evgeniy Krysanov 24.10.2020 16:41

Нет, не на карту. Перед размещением платных приложений с Apple заключается отдельный контракт, в котором ты указываешь номер своего счета в банке, на который будут поступать деньги с продаж.

ответить
Сергей 12.08.2020 15:00

Статьи невероятно интересные и полезные с практической т.зр.

Но прежде чем опробовать в деле этот код, нам потребуется добавить в класс txtEditAppDelegate переменную navController и связать ее значение с объектом UINavigationContoller в нашем интерфейсе.”

Вроде бы и просто оказалось, но я долго не мог сообразить, что связывать надо через IB.

ответить
the_hamster 20.08.2020 7:01

Я только заинтересовался темой написания приложений под Mac OS. Меня интересует, а для iPod Touch также сужествует эмулятор для разработки? И по идее такое приложение как данный текстовый редактор должно один в один работать на IPod Touch? Просто его попроще и быстрее купить, чем IPhone :)

ответить
Evgeniy Krysanov 20.08.2020 10:13

Среда разработки для iPhone и iPod Touch абсолютно одинаковая. Все что я написал до сих пор одинаково подходит как для того, так и для другого. Разница лишь в отсутствии “звонильной” части у iPod и сопутствующих библиотек, в остальном — два идентичных аппарата.

ответить
cDima 21.08.2020 19:18

И снова здравствуй. Вопрос про delegate. Как я понял, у нас получилась такая иерархия контроллеров и view:

  1. UIApplication

  2. UIWindows

  3. txtEditAppDelegate

  4. navController

  5. txtEditViewController (верхний элемент стека UIView)

  6. UITextView

Итак, посылать сообщения вниз по этой цепочки — вроде легко.

Посылать сообщения вверх — вот в чем вопрос. Например, штуке UITextView нужно послать сообщение “я first responder” своему родителю. Для этого UITextView объявляет prototype UITextViewDelegate и переменную delegate, а его родитель подписывается на эти сообщения. Именно так реализуется посыл сообщений вверх по иерархии?

ответить
Evgeniy Krysanov 22.08.2020 10:32

Во-первых, отправка сообщений. Существует множество способов:
1. Первое что приходит на ум — ввести новый outlet и связать его с нужным объектом
2. У некоторых классов (не могу пока сказать у всех ли) есть переменные, указывающие на родительский класс в иерархии. Например, у UIViewController есть navigationController, ссылающийся на UINavigationController-владелец
3. Получить ссылку на нужный объект через методы класса. Например, чтобы сослаться на делегат приложения делаем следующее:

AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];

Для этого UITextView объявляет prototype UITextViewDelegate и переменную delegate, а его родитель подписывается на эти сообщения

Не совсем верно. Класс, который реализует протокол UITextViewDelegate лишь реагирует на ряд событий, ответные реакции на которые он реализует, имплементируя ряд классов из описания делегата.

штуке UITextView нужно послать сообщение “я first responder” своему родителю

С одной стороны, first responder назначается в зависимости от того, куда пользователь нажал на экране, в какую область. Эта область и будет отвечать на воздействие. Если она не хочет или не может (или у нее не определены методы, реагирующие на нажатия), то событие отправляется выше по цепочке responder chain.

Вообще, про First Responder объекты я сам еще достаточно хорошо не понял. Как разберусь — напишу о них.

ответить
Val 28.11.2020 5:28

Просто огромное человеческое спасибо. Благословит Вас Господь за то, что делитесь Знаниями. Конвертируйте эти знания в прибыль. Предложите издательству небольшую книжку “для чайников”. Материала сайта вполне достаточно.

ответить
butterken 25.08.2020 16:04

Огромное спасибо за блог :)

В этот раз я немного запнулся на том месте где следовало самостоятельно соединить объекты в Interface Builder. Так и не смог доделать.

Если можно то очень бы хотелось туториал со сменой Видов.

ответить
Evgeniy Krysanov 25.08.2020 21:21

В этот раз я немного запнулся на том месте где следовало самостоятельно соединить объекты в Interface Builder. Так и не смог доделать.

Предыдущий мой пост о знакомстве с Interface Builder не помог разобраться со связями? В чем именно запнулся?

Если можно то очень бы хотелось туториал со сменой Видов.

Это ты еще не прочел следующий пост. Там все есть :)

ответить
-=di 26.08.2020 20:13

прожка на 10 баллов!!! все понятно и ясно. тока вот незадачка. у меня не сожхраняется контент страницы. есть подозрения, что виной тому, что у меня не девайс а эмулятор. следствие чему расхождение путей ( например в NSSearchPathForDirectoriesInDomains). плюс я не нашол лог файл, который может бы прояснил ситуацию. где он находиться ???

ответить
Evgeniy Krysanov 26.08.2020 23:36

Какой контент какой страницы и причем тут редактор и эмулятор? Я ничего не понял.

ответить
Val 22.09.2020 14:22

UITextView получается выводит просто текст. Получается аналог TextBox в Визуал Бэйсик. Ещё есть в Визуал RichTextBox для вывода форматированного, разукрашенного и т.д текста. Вероятно есть аналог и для iPhone (UI… какой-то). Если вместо UITextView его поставим, то еще более усложним редактор… Ещё в некоторых языках браузер можно ставлять. Вероятно для АйФона тоже… сможем тогда вообще нтмл документы отображать…

ответить
Evgeniy Krysanov 22.09.2020 15:08

Для вывода и форматированного, и web-документа предусмотрен объект UIWebView. UItextView к сожалению слишком прост и позволяет менять цвет и шрифт только применительно по отношению ко всему тексту, в отличие от UIWebView. Но последний не позволяет вводить текст с клавиатуры.

ответить
Val 5.10.2020 0:49

Вот этот момент не ясен, не получается.

Но прежде чем опробовать в деле этот код, нам потребуется добавить в класс txtEditAppDelegate переменную navController и связать ее значение с объектом UINavigationContoller в нашем интерфейсе. Надеюсь, что вам, моим читателям, подготовленным на предыдущих статьях, эта задача не покажется сложной :) ”

а я и сейчас вдуплиться не могу. Вроде вписал в outlet переменную и связал и всё сделал как надо а программа 7 ошибок выдаёт при попытке выполнить. Расписать бы этот момент для “чайников”, пожалуйста.

ответить
Evgeniy Krysanov 6.10.2020 23:18

Я бы прежде хотел взглянуть на ошибки.

Создание outlet переменной не должно представлять особых трудностей:
1. Объявить переменную в заголовочной файле с модификатором IBOutlet
2. Сгенерировать свойство, если требуется
3. Связать ее через Interface Builder с нужным объектом
4. Сделать @synthesize переменной в .m файле
5. Не забыть освободить ее в dealloc

ответить
Val 9.10.2020 3:03

Вот спасибки. Получился этот момент … с 555й попытки. Терпение и труд всё перетрут. Идём дальше.

ответить
Val 29.11.2020 6:27

Я так понял программа сохраняет в файл то, что мы написали. А вот если мы имеем большой текст для чтения, как сохранить текущую позицию где мы читаем, чтобы следующий раз не искать при запуске программы? Существуют механизмы?

ответить
Evgeniy Krysanov 1.12.2020 18:04

Сохранять позицию, безусловно, лучше не в текстовом файле, а в defaults system. И уже после загрузки перемещать курсор в нужную позицию. Навскидку не вспомню как метод называется, но я игрался с ним — позволяет переместить курсор в произвольное место текста, при этом автоматически делая скроллинг.

ответить
kamat0z 8.12.2020 15:47

А как быть с просмотром с лупой? ведь если с самого начала мы смотрим лупой, выезжает клавиатура, а кнопочки done нет…

видимо это глюк эмулятора.. так как на самом айфоне такого не наблюдается %)

ответить
Alexander 4.01.2020 18:00

Скока крови я потеря пока понял, что у Вас в коде стоит em-dash вместо дефиса. Я-то чтобы не писать руками копипейстил это дело. В xcode не отличить дефис от тире, а с тире он выдаёт ошибку синтаксиса.

— (void)applicationDidFinishLaunching:(UIApplication *)application { [window addSubview:navController.view];

PS: не туда коммент запихнул, ну да не суть) PPS: а ещё хотел сказать спасибо за это. Вместо того чтобы копипастить, я разобрался в каждой строчке кода)

ответить
Sergey Kononov 25.03.2020 11:08

Огромное спасибо за статью. Девайса нет, пользуюсь эмулятором. Вопрос: при вводе текста в нашем редакторе, он заполняет всю видимую часть UITextView, затем заползает за клавиатуру и собссно дальше ничего не видно, что вводишь :), как быть?

ответить
vnizzz 2.05.2020 0:49

Блог просто прекрасный! Жаль только, что не обновлялся давно, но надеюсь всё еще впереди :)

и да, мне всё-таки кажется, что последней строчкой метода writeApplicationList должно быть:

return [self writeApplicationData:pData toFile:fileName];

а не как указано в листинге

return [self writeApplicationData:pList toFile:fileName];


PS мне кажется, я не туда закомментировал, сорри :) Ну да ладно, я думаю Евгений понял, что я ему.

ответить
Артем Куликов 25.09.2020 5:57

Евгений!Доброго времени суток!Огромное спасибо за статью!

Но!У меня появилась проблема в использовании данного примера в моем приложении.

Суть приложения такова

Человек открыл приложение,ему предоставляется два поля куда он может вбить свой логин и пароль.Далее идет проверка на его валидность и после того как его существование подтвердится я в файл записываю некую информацию о нем,номер телефона например.

Есть кнопка по ее нажатию происходит все вышеописанное и записывается(вроде как) номер телефона в файл.код для записи в файл взят с данной статьи.После всего этого управление передается другому контроллеру в котором я открываю файл и считываю номер телефона пользователя.Вот тут-то и загвоздка.Если человек открыл приложение первый раз,то файл соответсенно пустой.и при выводе информации из файла после его “авторизации” ничего не выводится.Стоит закрыть приложение повторить процедуру и все отобразится как надо.А если опять закрыть приложение и снова вбить данные но уже другие выводятся предыдущие и т.д. в чем проблема?Поможете советом?

ответить
Виталий 14.02.2020 22:21

Народ,помогите,не в тему конечно ,мне нужна структура программы на обжектив с,в картинках или диаграммой какой нить че за чем запускаеться и как работает

ответить
Ролишка 30.06.2020 9:44

День добрый) я делала все как здесь написано. в итоге получила следующее: если я указываю в identity класс для NavigationController — UINavigatorController, то у меня текст отображается, но при повороте естественно ничего не работает. если указываю класс txtEditViewController,то при запуске приложения появляется белый экран,то есть UINavigationController вообще не подцепляется((( в чем может быть дело?

ответить

Форма комментирования для «Пишем правильный текстовый редактор»

Обязательное поле. Не больше 30 символов.

Обязательное поле

captcha image Пожалуйста, введите символы, которые вы видите на изображении

.