iPhone OS provides a very nice structure and some templates or tools for creating views. For example, a UIViewController may contains several controls such as labels and text boxes. You can use XCode's templates to create xib file for views and .m class for code behind the view. Then use Interface Builder tool to layout the view in a visual designer environment.
The control variables in the view controller class are defined by IBOutlet key work as indicator so that in the Interface Builder you can link the visual controls to the field variables in the class. In many example codes, including Apple examples, I have seen that those class level variables are also linked to their corresponding properties. Variables can be private, protected or public, but property in Objective-C are public. In my projects, I have never have any usages to access or set those field variables through properties outside the class. Why the properties are defined? I have tried to not define properties, and my codes do working well. I am confused, what is the reason doing that?
I posted a question to SO. Soon I got an answer saying that this is a duplicated question. From the link to the duplicated question, I found the explanation from Apple's documentaion: Resource Programming Guide.
Actually, my question was not exactly a duplicated one, just similar topic. In my case, the field variable name is different from its related property name, because OS 2.0 allows it. This brings my question into a further interesting one. In short, in case of IBOutlet, property and xib loading, it is better to keep the variable name and property name the same. I tried several tests and I found some further in depth issues. Then I posted my finding and analysis as an answer to my SO question. I would not repeat the answer here again.
Saturday, June 19, 2010
IBOutlet and Property in iPhone OS
Posted by D Chu at 8:36 PM 0 comments
Labels: iPhone Development
Detect Window Form Application Status
Last month, I was working, actually fixing bugs to resolve exceptions, and to improve performance, on an application which is a Windows Form application. One feature client wants is to query data from a SQL server periodically and refresh the UI with the retrieved data. The data may not be changed, but there was no inelegance to know if there was change. Therefore, the UI refresh was enforced because clients want to get the most recent data.
During the process to refactory codes to enhance/optimize SQL queries, I realized that clients wants the more recent data because they want to see any changes when the application is in running. There are many cases this kind of refreshing is actually not necessary. For example, when the window form is minimized, or the OS Windows is locked. In many cases, clients just left the application running after their work. Windows is locked, but the application had still been pulling data from the SQL server.
Then I proposed a optimized strategy to stop the data pulling when the UI is not available, either minimized or Widnows is locked. My suggestion was accepted.
Checking Application Status
To find out the Window from's status is very easy. This can be done by checking form's property WindowState
. Here are the codes in a Timer's cycle event:
private void timer1_Tick(object sender, EventArgs e) {
if (IsWindowFormActive()) {
// set interval to max
timer1.Interval = int.MaxValue;
Cursor c = this.Cursor;
this.Cursor = Cursors.WaitCursor;
RefreshDataAndViews();
this.Cursor = c;
// save the last update timestamp to timer's tag
timer1.Tag = DateTime.Now;
// if the window is locked, min or inactive, OnPaint() will not be called.
this.Invalidate();
}
else
{
timer1.Interval = int.MaxValue;
}
}
private bool IsWindowFormActive() {
bool bRet = ( !_windowIsLocked && this.WindowState !=
FormWindowState.Minimized &&
this.Visible && this.Enabled);
return bRet;
}
When the Window Form's state is minimized, Windows is locked or the form is not visible, the timer's interval is set to the maximum number of integer. All the status checking is done within the method
IsWindowsActive
.Find out If Windows is Locked
There is one class level variable
_windowsIsLocked
as a flag for Windows lock status. To set this flag, an event is added within the form's CTOR:class MainForm {
private bool _windowsIsLocked;
...
// CTOR
public MainForm()
{
...
SystemEvents.SessionSwitch +=
new SessionSwitchEventHandler(SystemEvents_SessionSwitch);
...
}
...
void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{
if (e.Reason == SessionSwitchReason.SessionLock)
{
_windowIsLocked = true;
}
else if (e.Reason == SessionSwitchReason.SessionUnlock)
{
// cause OnPaint event called where timer is reset
_windowIsLocked = false;
}
}
Enable Timer When Application Back to Normal Running Status
When the timer's interval is set to the maximum value of integer, how to bring the timer back to the required cycle interval, such as 2 minutes(120000 milliseconds)? The trick is to overwrite form's OnPaint method.
Make sure the base method is called first. This even is the most intensively called event. Whenever the form is activated, clicked, re-sized or moved, OnPaint will be called. Therefore, it is recommended to avoid any lengthy codes in this method. For my case, what I need to do is to reset the timer's interval if the application is back to normal:
protected override void OnPaint(PaintEventArgs e)
{
try
{
base.OnPaint(e);
int interval = NORMAL_REFRESH_INTERVAL;
object timerTag = Timer1.Tag;
// check if the tag was set?
if (timerTag != null && timerTag.ToString().Length > 0)
{
DateTime dt = (DateTime)timerTag;
long ticks = DateTime.Now.Ticks - dt.Ticks;
TimeSpan ts = new TimeSpan(ticks);
// Is this time interval since last refreshing too long?
if (ts.TotalMilliseconds > 1.5 * (double)interval)
{
// secons to start the next timer cycle
interval = QUICKREFRESH_INTERVAL_AFTERAWAKE;
}
// Reset the timer's interval
Timer1.Interval = interval;
}
}
catch { }
}
There is a try and catch blocks and exceptions are ignored. I had several cases of crashes when
OnPaint
was introduced. Since there are not critical codes there, I just ignore any exceptions from this event.
Posted by D Chu at 2:05 PM 0 comments
Sunday, June 13, 2010
MyLogger Class in Objective-C (2)
Let's continue MyLogger class. In this l'll discuss the structure of MyLogger class. As I mentioned before, this class is based on Objectuser: A Single Class Logger in Objective C. To further enhance the logger class, I also wanted to add indention feature, which I have used in .Net: Debug class method Indent in System.Dianostics namespace.
Stucture of MyLogger Class
The structure of MyLogger
class contains class level methods and properties, and instance level methods and properties. The following sections will discuss them in detail.
Class Level Methods and Properties
There actually only one class level method: Initialize
. I think this is the default class initializer when any reference to MyLogger class is called or set. In theory, this method is not need to be called explicitly. As Objectuser's suggested, this method just creates a template, as a static instance of MyLogger
, with default property settings.
All the class level properties are used for customized settings in all MyLogger instances. For example, defaultLogginLevel
is used as default logger level. It is set only once and all the instances of MyLogger
have the same logger level. Therefore, you can set this once at the begging of an application so that debug level is enabled or disabled. Other settings are very straightforward, such as indentChar
, numberOfIndentChars
, and format
.
For the property format
, it may need further explanation. The format should contain four place holders to be filed: indent, log level, context, and message. The default format is:
"%@[%@] %@ - %@"
where the first one is for indention, second for log level name (DEBUG, WARNING, ERROR, or INFO), third for context and last for log message.
Here are some codes:
// Define a static instnace as sort of prototype static MyLogger* gInstance; static int gIndent = 0; static NSString* gFormat = @"%@[%@] %@ - %@"; // indent[logLevel] context - message static char gIndentChar = ' '; static int gIndentChars = 2; ... #pragma mark - #pragma mark MyLogger class methods + (void) initialize { // here self is class level self refert to the class ie MyLogger, best practice. gInstance = [[self alloc] init]; gInstance.level = LogLevelError; } + (int)defaultLoggingLevel { return gInstance.level; } + (void)setDefaultLoggingLevel:(MyLoggerLevel)defaultLevel { gInstance.level = defaultLevel; } + (char) indentChar { return gIndentChar; } + (void) setIndentChar:(char)aChar { gIndentChar = aChar; } + (int) numberOfIndentChars { return gIndentChars; } + (void) setNumberOfIndentChars:(int)numberOfChars { gIndentChars = numberOfChars; } + (NSString*) format { return gFormat; } + (void)setFormat:(NSString *)aFormat { [gFormat autorelease]; gFormat = [aFormat retain]; }
Instance Level Properties and NSObject Methods
There are two instance level properties:
level
and context
. The level
is corresponding to class property defaultLoggingLevel
, but it provides flexibility for instance to set instance logging level. Those two properties are straightforward:#pragma mark - #pragma mark MyLogger properties @synthesize level=mLevel; @synthesize context = mContext;
I divide instance level methods into two groups:
NSObject
methods and MyLogger
instance level methods. The NSObject
methods are override methods, such as init
and description
. The init
methods in MyLogger are actually two customized CTORs(constructors).#pragma mark - #pragma mark NSObject methods - (id) initWithContext:(NSString*)loggingContext { if (self = [super init]) { self.level = gInstance.level; self.context = loggingContext; } return self; } - (id) initWithContext:(NSString*)loggingContext logLevel:(MyLoggerLevel)intentLoggingLevel { if (self = [self initWithContext:loggingContext]) { self.level = intentLoggingLevel; } return self; } - (NSString*) description { NSString* s = [[NSString alloc] initWithFormat:@"Mylogger instance - level: %@, context: %@", nameOfLevel(mLevel), mContext]; [s autorelease]; return s; }
The
description
method may not be required. It just occurred to me with crashing within MyLogger class once and the description provides me some help to identity MyLogger class with its internal private data member information.There is another one NSObject related method:
dealloc
. I called it as memory management. It contains very simple codes:#pragma mark - #pragma mark Memory management -(void) dealloc { self.context = nil; [super dealloc]; }
My next blog will focuse on MyLogger instance methods.
Reference
Posted by D Chu at 7:58 PM 0 comments
Labels: iPhone Development
Saturday, June 05, 2010
Reset Keyboard for an Entry Field (iPhone)
During my first iPhone application development, I have tried to learn the Objective-C, Cocoa framework, the best practice to make my application in a well design structure. It's been very slow, but I think it worth every minutes and efforts I invested. After about one week hard working and struggling, today, I found a way to resolve an issue to reset keyboard for an entry field as specified in xib.
I have seem some applications have very nice feature to use iPhone keyboard's enter or next key switch an input to the next entry field if available. The issue I had is that bother fields in sequence have the same keyboard but with different mode, one in numeric and the next in alphabet, then the keyboard would not switch back to the required alphabet mode. Use is required to press ABC key to go back numeric mode if he/she needs.
The problem is caused by the setting of the field as responder within the event of textFieldShouldReturn:
. The next field's textFieldDidBeginEditing:
is called with the ...Return event. This nested call prevent the keyboard to change the default mode as required. Here is an example:
Input in Rate field. Press Next key to the Note field.
The keyboard says in the numeric mode.
I used my NSLog wrapper class to debug the sequence of calls and state messages.
[DEBUG] EditTaxViewController - textFieldDidBeginEditing
[DEBUG] EditTaxViewController - textFieldDidBeginEditing DONE
[DEBUG] EditTaxViewController - textFieldShouldReturn
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder:
----[DEBUG] EditTaxViewController - textViewDidBeginEditing
----[DEBUG] EditTaxViewController - textViewDidBeginEditing DONE
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder: return: true
[DEBUG] EditTaxViewController - textFieldShouldReturn DONE
As seem in the log, The
event textFieldDidBeginEditing:
was fired with the event textFiledDidEntEditing
. The keyboard mode is not changed, since the keyboard was not resigned yet. What I need is to find a way to fire the textFieldDidBeginEditing
afterwards. I have tried some many different ways, finally, with the debug message help, I realized this point and I found iPhone Notification framework is the way to resolve the issue.I have created a helper class to encapsulate all the logic. Here is the way I use the helper class:
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[[mLogger debug:@"textFieldShouldReturn"] indent:YES];
// No more inputs?
if (![mEntryFieldHelper findNextEntryFieldAsResponder:textField])
{
...
}
[[mLogger indent:NO] debug:@"textFieldShouldReturn DONE"];
return YES;
}
- (void)viewDidLoad {
[[mLogger debug:@"viewDidLoad"] indent:YES];
[super viewDidLoad];
...
// Get all entry fields
if (!mEntryFieldHelper)
{
mEntryFieldHelper = [[EntryFieldHelper alloc] initWithView:self.view];
[mEntryFieldHelper registerForKeyboardNotifications];
// the first two fields (tax and rate) are OK, but the note
// field's keyboard needs to be reset
mEntryFieldHelper.inputFieldsWithKeyboardToBeReset =
[NSArray arrayWithObjects: [NSNumber numberWithInt:3], nil];
}
[[mLogger indent:NO] debug:@"viewDidLoad DONE"];
}
...
// In .h file:
@class EntryFieldHelper;
@interface EditTaxViewController : UIViewController <UINavigationBarDelegate,
UITextFieldDelegate, UITextViewDelegate>
{
...
@private
MyLogger* mLogger;
EntryFieldHelper* mEntryFieldHelper;
}
In the
viewDidLoad
event, the helper class instance is created with view, next register keyboard notifications and last set an array of integers for input fields which need reset keyboard.In the event of
textFieldShouldReturn:
, the method of findNextEntryFieldAsResponder:
is called to set the next responder. Within the helper class, all the logic auto-jump to next fields is implemented. The only requirement for the view class or parent is that all the input fields should have tag value set in sequence in xib file. As in this example, three fields in the view: name, rate and note.The header file defines the variables, properties and methods:
@class MyLogger;
@interface EntryFieldHelper : NSObject {
@private
MyLogger* mLogger;
NSArray* mEntryFields;
NSArray* mInputFieldsWithKeyboardToBeReset;
}
@property (nonatomic, retain) NSArray* inputFieldsWithKeyboardToBeReset;
-(id) initWithView:(UIView*) aView;
-(BOOL) findNextEntryFieldAsResponder:(UIControl*)field;
@end
And here are implementations, first the
initialization
and dealloc
:import "EntryFieldHelper.h"
#import "MyLogger.h"
#define kInputFieldKey @"MyInputFields"
@implementation EntryFieldHelper
@synthesize inputFieldsWithKeyboardToBeReset = mInputFieldsWithKeyboardToBeReset;
-(id)initWithView:(UIView *)theView {
if (self = [super init]) {
mLogger = [[MyLogger alloc] initWithContext:@"EntryFieldHelper"];
[[mLogger debug:@"InitWithView:"] indent:YES];
NSMutableArray* entryFields = [[NSMutableArray alloc] init];
NSInteger tag = 1;
UIView* aView;
while (aView = [theView viewWithTag:tag] ) {
if (aView && [[aView class] isSubclassOfClass:[UIResponder class]]) {
[entryFields addObject:aView];
}
tag++;
}
mEntryFields = entryFields;
[mEntryFields retain];
[entryFields release];
[[mLogger indent:NO] debug:@"initWithView: DONE"];
}
return self;
}
-(void) dealloc {
[[mLogger debug:@"dealloc"] indent:YES];
// Remove the subscription of all notifications from the notification center.
[[NSNotificationCenter defaultCenter] removeObserver:self];
if ([mEntryFields retainCount] > 0) {
[mEntryFields release];
}
[mInputFieldsWithKeyboardToBeReset release];
[[mLogger indent:NO] debug:@"dealloc"];
[mLogger release];
[super dealloc];
}
@end
The class level
NSArray mEntryFields
is populated based on the input view's UIResponder type controls. The following is the method of findNextEntryFieldAsResponder:
. A text field or text view is an input. Within the method, the field next to the input is searched within mEntryFields
. If the found one does not require reset keyboard, it will become the next responder; if keyboard reset is required, then an notification is created and registered.- (BOOL)findNextEntryFieldAsResponder:(UIControl *)field {
[[mLogger debug:@"findNextEntryFieldAsResponder:"] indent:YES];
BOOL retVal = NO;
for (UIView* aView in mEntryFields) {
// Find the entry field
if (aView.tag == (field.tag + 1)) {
NSNumber* tag = [NSNumber numberWithInt:aView.tag];
if ([self.inputFieldsWithKeyboardToBeReset containsObject:tag]) {
NSNotification* notification = [NSNotification notificationWithName:kInputFieldKey
object:aView];
[[NSNotificationQueue defaultQueue]
enqueueNotification:notification postingStyle:NSPostWhenIdle
coalesceMask:NSNotificationCoalescingOnName forModes:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardShowNotification:)
name:kInputFieldKey object:nil];
}
else {
[aView becomeFirstResponder];
}
retVal = YES;
break;
}
}
[[mLogger indent:NO] debug:@"findNextEntryFieldAsResponder: return: %@",
retVal ? @"true" : @"false"];
return retVal;
}
Last the async notification is received to this method:
- (void) keyboardShowNotification:(NSNotification*) notification {
[[mLogger debug:@"keyboardShowNotification:"] indent:YES];
UIResponder* responder = [notification object];
if (responder) {
[responder becomeFirstResponder];
}
// Notification received. Remove the notification.
[[NSNotificationCenter defaultCenter] removeObserver:self name:kInputFieldKey object:nil];
[[mLogger indent:NO] debug:@"keyboardShowNotification: DONE"];
}
As expected here is the snap-shot of screen from rate to note:
hand here is the log:
[DEBUG] EditTaxViewController - textFieldDidBeginEditing
[DEBUG] EditTaxViewController - textFieldDidBeginEditing DONE
[DEBUG] EditTaxViewController - textFieldShouldReturn
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder:
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder: return: true
[DEBUG] EditTaxViewController - textFieldShouldReturn DONE
[DEBUG] EntryFieldHelper - keyboardShowNotification:
--[DEBUG] EditTaxViewController - textViewDidBeginEditing
--[DEBUG] EditTaxViewController - textViewDidBeginEditing DONE
--[DEBUG] EntryFieldHelper - <UITextView: 0x3d09bd0; frame = (20 143; 287 77); text = 'Test'; clipsToBounds = YES; opaque = NO; autoresize = W; tag = 3; layer = <CALayer: 0x3d09db0>> is responder
[DEBUG] EntryFieldHelper - keyboardShowNotification: DONE
Posted by D Chu at 4:11 PM 0 comments
Labels: iPhone Development