objcArchiv

Ein WebView-Element ist eine schnelle und einfache Art, sowohl plattformunabhängige als auch dynamische Inhalte in eine gewöhnliche iOS App zu integrieren. Nun ist es nicht schwierig, das WebView-Element zu benutzen, um Webseiten anzuzeigen, aber die Dinge werden komplizierter, wenn eine Interaktion zwischen der Parent App und der Webseite erforderlich ist. Im Moment gibt es keinen gangbaren Weg für eine Webseite, Nachrichten an den WebView Controller zu senden. Dieser Artikel zeigt, wie man trotz dieser fehlenden Funktion eine Zwei-Wege-Kommunikation implementieren kann. Am Ende des Artikels finden Sie einen Download-Link zu einem vollständigen iOS Beispielprojekt.

JavaScript Code von Ihrer App aus aufrufen

Dieser Teil ist einfach: Nehmen wir an, in Ihrem Projekt benutzen Sie ein WebView-Element für die Anzeige der Seiten. Es gibt eine Methode namens stringByEvaluatingJavaScriptFromString, die die gewünschte Funktionalität bereitstellt. Beispiel:

[myWebView stringByEvaluatingJavaScriptFromString:@"alert('Hello World!');"];

Diese Methode – Sie ahnen es – zeigt eine Warnmeldung, die von Ihrer Webseite geschickt wurde. Auf diese Weise können Sie alle JavaScript-Funktionen aufrufen, die Ihrer Webseite von außen zur Verfügung stehen. Damit ist es ein Kinderspiel, Seiten ohne großen Aufwand zu ändern.

In Ihrer App Funktionen vom WebView-Element aus aufrufen

Der umgekehrte Weg ist weitaus komplizierter. Wir müssen eine Methode finden, wie eine Webseite mit einer App kommunizieren kann. Glücklicherweise bietet das WebView-Element einige hilfreiche Attribute und Ereignisse. Zunächst einmal wird ein Ereignis namens shouldStartLoadWithRequest ausgelöst, wenn eine Seite angefordert wird. Hier ist ein Beispiel für die Implementierung:

-(BOOL)myWebView:(UIWebView*)myWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNaviagtionType)navigationType {
   [...]
}

Was diese Funktion so interessant macht ist, dass sie aufgerufen wird, bevor die Seite tatsächlich geladen wird, und dass sie einen booleschen Wert als Rückgabe erwartet. Der Rückgabewert YES veranlasst das WebView-Element, die gewünschte Seite zu laden, während der Rückgabewert NO keine Seite lädt. Der Plan ist wie folgt: Eine JavaScript-Funktion lädt eine fiktive Seite einschließlich einiger GET-Parameter über window.location. Durch das spezielle URL Stichwort ist die App in der Lage herauszufinden, ob eine echte Seite geladen werden soll oder ob eine In-App Funktion aufgerufen wird (App-Call). An diesem Punkt kann die Funktion entweder den Aufruf stoppen und die Aktion durchführen oder die Seite laden. Die Funktion erledigt also zwei Dinge:

  • Sie prüft, ob ein normaler Link oder ein App-Aufruf angefordert wird und gibt den entsprechenden booleschen Wert zurück.
  • Sie holt die gegebenen Parameter und überprüft, ob sie zu den vordefinierten Aktionen passen.

Basierend auf dem Beispiel von tetontech ist dies der Code, über den wir sprechen:

- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType {
 NSString *url = [[request URL] absoluteString];
 NSLog(@"Requesting: %@",url);
 NSArray *urlArray = [url componentsSeparatedByString:@"?"];
 NSString *cmd = @"";
 NSMutableArray *paramsToPass = nil;
 // isolate command and parameters
 if([urlArray count] < 1){
  NSString *paramsString = [urlArray objectAtIndex:1];
  NSArray *urlParamsArray = [paramsString componentsSeparatedByString:@"&"];
  cmd = [[[urlParamsArray objectAtIndex:0] componentsSeparatedByString:@"="] objectAtIndex:1];
  int numCommands = [urlParamsArray count];
  paramsToPass = [[NSMutableArray alloc] initWithCapacity:numCommands-1];
  for(int i = 1; i < numCommands; i++){
  NSString *aParam = [[[urlParamsArray objectAtIndex:i] componentsSeparatedByString:@"="] objectAtIndex:1];
  [paramsToPass addObject:aParam];
  }
  }
  if([cmd compare:@"toggleWorking"] == NSOrderedSame){
  NSLog(@"Turning working indicator...");
  if([UIApplication sharedApplication].networkActivityIndicatorVisible == NO) {
  [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
  NSLog(@"...on");
  } else {
  [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
  NSLog(@"...off");
  }
  } else if([cmd compare:@"logMessage"] == NSOrderedSame) {
  NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingOccurrencesOfString:@"%20" withString:@" "];
  NSString *message = [[paramsToPass objectAtIndex:0] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
  NSLog(@"Received JS message: %@",message);
  }
  // only load the page if it is the initial index.html file
  NSRange aSubStringRange = [url rangeOfString:@"index.html"];
  if(aSubStringRange.length != 0){
  return YES;
  } else {
  NSLog(@"App call found: request cancelled");
  return NO;
  }
 }

Wie schon gesagt, extrahiert und isoliert diese Funktion jeden gegebenen Parameter und Befehl und überprüft mit einigen if-Anweisungen, ob ein Befehl erkannt wird. Wenn das der Fall ist, können zusätzliche Parameter verwendet werden, um eine Aktion auszuführen. In diesem Beispiel gibt es nur zwei mögliche Aktionen:

  • Mit der Anforderung ?cmd=toggleWorking wird in der Tab-Leiste des iPad - abhängig von seinem gegenwärtigen Zustand - ein sich drehendes Rädchen ein- oder ausgeschaltet.
  • Die Aktion logMessage kann aufgerufen werden mit: ?cmd=logMessage¶m=Hello%20World. Es wird eine Nachricht als log-Datei an die Debug-Konsole übermittelt.
NSRange aSubStringRange = [url rangeOfString:@"index.html"];
if(aSubStringRange.length != 0){

Die letzte Überprüfung ist sehr wichtig! Angenommen, Ihre Hauptseite heißt index.html, wird dadurch sicher gestellt, dass die Seite einmal beim Start geladen wird. Weitere Anforderungen sind im Moment blockiert, aber Sie kennen sicherlich andere Ansätze, um sicherzustellen, dass App-Aufrufe nicht geladen werden.

Beispielprojekt herunterladen

Gehen Sie zu GitHub und laden sie das WebViewInterface-Example herunter. Quelle:

Ob man nun einer App sein eigenes Layout zu verleihen will oder Design-Vorgaben einhalten muss, oft ist notwendig, vom Apple-Standard-Design abzuweichen.
In diesem Fall geht es um UITableViewCells mit runden Ecken. Das kann z.B. so aussehen:

UITableViewCells with round corners

So eine Vorgabe lässt sich mit einer eigenen UITableViewCell Klasse und den Core Graphics einfach lösen.

Wir öffnen unser XCode-Projekt und fügen eine  neue Datei vom Typ UITableViewCell hinzu (*.m und *.h) und nennen sie z.B. “RoundedTableViewCell”.

Damit wir die Core Graphics zu Verfügung haben, müssen wir das QuarzCore Framework in der *.h-Datei importieren:

#import 

Nicht vergessen: Das Framework muss auch den “frameworks” des Projekts hinzugefügt werden:

add QuartzCore FRamework to your project add QuartzCore framework

In der RoundedTableViewCell.h-Datei definieren wir ein CALayer für jede Ecke der Zelle. Diese kleinen Quadrate “überdecken” später die abgerundeten Ecken, wo gewünscht.

CALayer* topleft;
CALayer* topright;
CALayer* bottomleft;
CALayer* bottomright;
CALayer* bglayer;
...
@property (nonatomic, retain) CALayer* topleft;
@property (nonatomic, retain) CALayer* topright;
@property (nonatomic, retain) CALayer* bottomleft;
@property (nonatomic, retain) CALayer* bottomright;
@property (nonatomic, retain) CALayer* bglayer;

Außerdem brauchen wir zwei BOOLsche Variablen..

BOOL roundTop;
BOOL roundBottom;
...
@property (nonatomic) BOOL roundTop;
@property (nonatomic) BOOL roundBottom;

… und zwei Methoden, um bei der einzelnen Zelle runde Ecken oben und/ oder unten anzuzeigen:

-(void) drawRoundTop;
-(void) drawRoundBottom;

Das war’s mit der *.h-Datei. In der RoundedTableViewCell.m Datei werden nun die runden Ecken implementiert. Zunächst legen wir einen Eck-Radius und eine Hintergrundfarbe für die Tabellenzelle fest (zwischen #import und @implemetation):

#define rad 15  // radius
#define normalColor [UIColor colorWithRed:0.39 green:0.15 blue:0.24 alpha:1.0]  // cell background color, dark red

Man beachte: Kein Strichpunkt hinter #define-Angaben!

Im @implementation-Block fehlt noch das @synthesize unserer CALayers and BOOLschen Variablen. Das release können wir uns bei diesen Datentypen netterweise sparen.

@synthesize topleft;
@synthesize topright;
@synthesize bottomleft;
@synthesize bottomright;
@synthesize roundTop;
@synthesize bglayer;
@synthesize roundBottom;

In der initWithStyle-Methode definieren wir ein UILabel mit ein paar Style-Attributen:

// add label
 label = [[UILabel alloc] initWithFrame:self.contentView.frame];
 label.textAlignment = UITextAlignmentLeft;
 label.backgroundColor = [UIColor clearColor];
 label.textColor = [UIColor whiteColor];
 label.font = [UIFont fontWithName:@"Zapfino" size:16];
 [self.contentView addSubview:label];

// initial values
 roundTop = NO;
 roundBottom = NO;
// set layer background color (= cell background color)
 self.layer.backgroundColor = normalColor.CGColor;

In der gleichen Methode setzen wir noch die Standardwerte, und zwar ohne runde Ecken, sowie die Hintergrundfarbe:

roundTop = NO;
roundBottom = NO;
self.layer.backgroundColor = normalColor.CGColor;

Um die runden Ecken zu zeichnen, brauchen wir unsere eigene drawRect Methode. Dort werden auch die CALayer für jede Ecke erzeugt, beides abhängig vom oben definierten Radius:

- (void) drawRect:(CGRect)rect{
	CGRect fr = rect;

	fr.size.width = fr.size.width-2*rad;
	fr.size.height = fr.size.height-1;
	fr.origin.x = rad;

	// draw round corners layer
	bglayer = [CALayer layer];
    bglayer.backgroundColor = normalColor.CGColor;
	bglayer.cornerRadius = rad;
	bglayer.frame = fr;
	bglayer.zPosition = -5;	// important, otherwise delete button does not fire / is covered
	[self.layer addSublayer:bglayer];

	// set label size (adjust to heightForRowAtIndexPath..)
	label.frame = CGRectMake(rad, 5, fr.size.width, fr.size.height);

	// corner layer top left
	topleft = [CALayer layer];
    topleft.backgroundColor = normalColor.CGColor;
	CGRect tl = CGRectMake(rad, 0, rad, rad);
	topleft.frame = tl;
	topleft.zPosition = -4;
	if(roundTop){
		topleft.hidden = YES;
	}else {
		topleft.hidden = NO;
	}
    [self.layer addSublayer:topleft];

	// corner layer top right
	topright = [CALayer layer];
    topright.backgroundColor = normalColor.CGColor;
	topright.frame = CGRectMake(fr.size.width, 0, rad, rad);
	topright.zPosition = -3;
	if(roundTop){
		topright.hidden = YES;
	}
	else {
		topright.hidden = NO;
	}
    [self.layer addSublayer:topright];

	// corner layer bottom left
	bottomleft = [CALayer layer];
    bottomleft.backgroundColor = normalColor.CGColor;
	bottomleft.frame = CGRectMake(rad, fr.size.height-rad, rad, rad);
	bottomleft.zPosition = -2;
	if(roundBottom){
		bottomleft.hidden = YES;
	}else {
		bottomleft.hidden = NO;
	}
    [self.layer addSublayer:bottomleft];

	// corner layer bottom right
	bottomright = [CALayer layer];
	bottomright.backgroundColor = normalColor.CGColor;
	bottomright.frame = CGRectMake(fr.size.width, fr.size.height-rad, rad, rad);
	bottomright.zPosition = -1;
	if(roundBottom){
		bottomright.hidden = YES;
	}else {
		bottomright.hidden = NO;
	}
    [self.layer addSublayer:bottomright];

	[super drawRect:rect];

}

Jetzt fehlen nur noch die beiden Methoden, um die Tabellenzelle oben oder unten mit runden Ecken darzustellen:

-(void) drawRoundTop{
roundTop = YES;
topleft.hidden = YES;
topright.hidden = YES;
}

Für die Unterseite:

-(void) drawRoundBottom{
roundBottom = YES;
bottomleft.hidden = YES;
bottomright.hidden = YES;
}

So, unsere UITableViewCell Klasse ist jetzt gebrauchsfertig. Also öffnen wir unsere UITableViewController.m-Datei (oder legen eine neue an). In der viewDidLoad Methode muss zumindest die erste  Zeile des folgenden Codes implementiert werden, sonst überdeckt der standardmäßige Tabellenhintergrund unsere neuen Zellen:

// important! without this line it does not work!
[self.tableView setBackgroundView:[[[UIView alloc] init] autorelease]];
// set plain background color
 [self.tableView setBackgroundColor:[UIColor colorWithRed:0.69 green:0.81 blue:0.79 alpha:1.0]];
// remove seperator color
 self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
 [self.tableView setSeparatorColor:[UIColor clearColor]];

Hier im Beispiel wurden zwei Sections definiert, eine mit “normalen” und eine mit runden Tabellenzellen, die wie ein seperater Block erscheinen sollen. Der Zellentyp und die jeweiligen Zellenattribute werden in der cellForRowAtIndexPath Methode festgelegt:

if (indexPath.section == SECTION_NORMAL) {

 static NSString *CellIdentifier = @"Cell";

 NormalTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
 if (cell == nil) {
 cell = [[[NormalTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
 }
 cell.label.text = @"normal cell";
 return cell;

 } else if (indexPath.section == SECTION_ROUNDED) {
// define round cell
 static NSString *CellRoundedIdentifier = @"RoundedTableViewCell";
 RoundedTableViewCell *cell = (RoundedTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellRoundedIdentifier];
 if (cell == nil) {
 cell = [[[RoundedTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellRoundedIdentifier] autorelease];
 }

 // Configure the cell.
 [cell.label setText:[NSString stringWithFormat:@"my round cell No.%d",(indexPath.row+1)]];
 cell.selectionStyle = UITableViewCellSelectionStyleNone;
 // draw round top corners in first row
 if(indexPath.row == 0){
 [cell drawRoundTop];
 }
// draw round corners in last row
if (indexPath.row == [self.tableView  numberOfRowsInSection:indexPath.section]-1) {
 [cell drawRoundBottom];
 }

 return cell;
 }

Um einen Abstand zwischen den einzelnen Tabellenzellen zu erzuegen, verwenden wir die heightForHeaderInSection und die viewForHeaderInSection Methoden:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

 UIView* header = nil;
 if(section == SECTION_ROUNDED){
 CGRect rect = CGRectMake(0, 0, self.tableView.frame.size.width, [self tableView:(tableView) heightForHeaderInSection:section]);
 header = [[UIView alloc] initWithFrame:rect];

 }
 return header;
}

- (CGFloat) tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
 CGFloat h = 0;
 if(section == SECTION_ROUNDED){
 h = 15;
 }
 return h;
}

Das XCode-Beispielprojekt kann als zip-Archiv von github heruntergeladen werden.