15 April 2013

在iOS 6中iPad开发101: UIPopoverController 使用教程

Finished Popover Changing Text Color
更新日期 3/7/2013: 全面更新以支持iOS6 (原帖发布者是 Ray Wenderlich, 更新作者是 Ellen Shapiro).

这是四篇文章中的第二篇:帮助iPhone开发者快速的掌握几个新的类(至少对于我来说):: UISplitView, UIPopoverController和定制输入视图.

在本系列的第一篇中(first part of the series),我创建了一个程序,使用split view在左边显示了一列怪物,选中怪物后,会在右边显示详细的内容。

在本文中,我将显示一个popover view:添加一个popover,让用户可以在列表中选择颜色,以修改怪物名字的颜色。(第三篇传送门:Part 3–定制输入视图)

这里我从上次写的工程开始,如果你还没有这个工程的话,这里下载: grab a copy .
继续阅读!

标签: , , , ,

27 March 2013

使用MapKit叠加图片和视图教程(2)

如果你喜欢的话那就在地图上放置一个Pin — 注解

如果你用Maps程序搜索过位置信息,那么你肯定看到过在地图上出现的许多Pin。这可以理解为注解(annotation),它是用 MKAnnotationView创建的。你也可以在你的程序中使用注解 — 并使用你想要的任何图片,不仅仅是pin!

在程序中使用注解来标出具体的某个景点,这对游客来说非常有用。注解对象的使用方法跟MKOverlay 和 MKOverlayView非常类似, 只不过需要使用的类是MKAnnotation 和 MKAnnotationView.

在Annotations群组中创建一个名为 PVAttractionAnnotation 新的类,并继承自 NSObject.

然后用下面的代码替换 PVAttractionAnnotation.h 文件中的内容:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
 
typedef NS_ENUM(NSInteger, PVAttractionType) {
    PVAttractionDefault = 0,
    PVAttractionRide,
    PVAttractionFood,
    PVAttractionFirstAid
};
 
@interface PVAttractionAnnotation : NSObject 
 
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *subtitle;
@property (nonatomic) PVAttractionType type;
 
@end

上面的代码中,首先是import MapKit,然后为PVAttractionType.定义了一个枚举。这个枚举列出了注解的类型: 游乐设施,食物,急救和默认。

接着让这个类遵循 MKAnnotation Protocol. 跟MKOverlay类似, MKAnnotation 有一个 required coordinate 属性. 最后是定义了一些属性。

OK, 下面我们来看看PVAttractionAnnotation的实现。

将 PVAttractionAnnotation.m 按照如下修改:

#import “PVAttractionAnnotation.h”

@implementation PVAttractionAnnotation
 
@end

这可能是本文中最简单的实现了!在里面不需要实现任何内容;只需要了解在头文件定义的一些属性即可!

现在需要创建一个MKAnnotation 实例来使用你的注解了。

在Annotation群组中创建另外一个类:PVAttractionAnnotationView 继承自MKAnnotationView. 头文件中不需要添加任何内容。

用下面的代码替换PVAttractionAnnotationView.h 中的内容:

#import <Foundation/Foundation.h>
#import <MapKit/MapKit.h>
 
@interface PVAttractionAnnotationView : MKAnnotationView
 
@end

将下面的代码添加到 PVAttractionAnnotationView.m:

#import "PVAttractionAnnotationView.h"
#import "PVAttractionAnnotation.h"
 
@implementation PVAttractionAnnotationView
 
- (id)initWithAnnotation:(id)annotation reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
    if (self) {
        PVAttractionAnnotation *attractionAnnotation = self.annotation;
        switch (attractionAnnotation.type) {
            case PVAttractionFirstAid:
                self.image = [UIImage imageNamed:@"firstaid"];
                break;
            case PVAttractionFood:
                self.image = [UIImage imageNamed:@"food"];
                break;
            case PVAttractionRide:
                self.image = [UIImage imageNamed:@"ride"];
                break;
            default:
                self.image = [UIImage imageNamed:@"star"];
                break;
        }
    }
 
    return self;
}
 
@end

上面重载了方法 initWithAnnotation:reuseIdentifier:; 根据注解不同的type属性,为注解设置不同的image属性。

非常棒! 现在你创建好了注解和与其相关的view,下面可以将它们添加到map view中了!

首先,你需要准备在 initWithAnnotation:reuseIdentifier:方法中用到的一些资源,以及经典相关的位置信息(MagicMountainAttractions.plist). 这些资源包含在 resources for this tutorial – 将它们拷贝到工程的Images群组中。

在plist文件中包含了坐标信息以及其它与公园景点相关的一些详细信息,如下所示:

<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE plist PUBLIC ”-//Apple//DTD PLIST 1.0//EN” ”http://www.apple.com/DTDs/PropertyList-1.0.dtd”>
<plist version=”1.0″>
<array>
<dict>
<key>name</key>
<string>Goliath</string>
<key>location</key>
<string>{34.42635,-118.59712}</string>
<key>type</key>
<string>1</string>
<key>subtitle</key>
<string>Intensity: 8/10</string>
</dict>
<dict>
<key>name</key>
<string>Batman</string>
<key>location</key>
<string>{34.42581,-118.60089}</string>
<key>type</key>
<string>1</string>
<key>subtitle</key>
<string>Intensity: 6/10</string>
</dict>
<dict>
<key>name</key>
<string>Ridler’s Revenge</string>
<key>location</key>
<string>{34.42430,-118.60074}</string>
<key>type</key>
<string>1</string>
<key>subtitle</key>
<string>Intensity: 6/10</string>
</dict>
<dict>
<key>name</key>
<string>X2</string>
<key>location</key>
<string>{34.42156,-118.59556}</string>
<key>type</key>
<string>1</string>
<key>subtitle</key>
<string>Intensity: 10/10</string>
</dict>
<dict>
<key>name</key>
<string>Tatsu</string>
<key>location</key>
<string>{34.42150,-118.59741}</string>
<key>type</key>
<string>1</string>
<key>subtitle</key>
<string>Intensity: 7/10</string>
</dict>
<dict>
<key>name</key>
<string>Panda Express</string>
<key>location</key>
<string>{34.42126,-118.595637}</string>
<key>type</key>
<string>2</string>
<key>subtitle</key>
<string>Cost: $$</string>
</dict>
<dict>
<key>name</key>
<string>Cold Stone</string>
<key>location</key>
<string>{34.42401,-118.59495}</string>
<key>type</key>
<string>2</string>
<key>subtitle</key>
<string>Cost: $</string>
</dict>
<dict>
<key>name</key>
<string>First Aid</string>
<key>location</key>
<string>{34.42640,-118.59918}</string>
<key>type</key>
<string>3</string>
<key>subtitle</key>
<string>Call 911 For Emergency</string>
</dict>
</array>
</plist>

现在你以及拥有上面这些资源了,当然你也可以使用新的注解!

回到 PVParkMapViewController.m 并import MKAnnotation 和 MKAnnotationView两个类, 如下所示:

#import "PVAttractionAnnotation.h"
#import "PVAttractionAnnotationView.h"

下面定义一个方法,将景点的注解添加到map view中:

添加如下方法(还是在PVParkMapViewController.m):

- (void)addAttractionPins {
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"MagicMountainAttractions" ofType:@"plist"];
    NSArray *attractions = [NSArray arrayWithContentsOfFile:filePath];
    for (NSDictionary *attraction in attractions) {
        PVAttractionAnnotation *annotation = [[PVAttractionAnnotation alloc] init];
        CGPoint point = CGPointFromString(attraction[@"location"]);
        annotation.coordinate = CLLocationCoordinate2DMake(point.x, point.y);
        annotation.title = attraction[@"name"];
        annotation.type = [attraction[@"type"] integerValue];
        annotation.subtitle = attraction[@"subtitle"];
        [self.mapView addAnnotation:annotation];
    }
}

上面的个方法读取 MagicMountainAttractions.plist 文件,并对字典数组进行枚举. 针对每个条目,都使用相关的景点信息来创建一个 PVAttractionAnnotation 实例,并将它们添加到map view中。

现在需要更新一下 loadSelectedOptions 方法,当被选中时,以匹配新的选项,并执行新的方法。

将下面的代码添加到 loadSelectedOptions 中(仍然在 PVParkMapViewController.m):

- (void)loadSelectedOptions {
    [self.mapView removeAnnotations:self.mapView.annotations];
    [self.mapView removeOverlays:self.mapView.overlays];
    for (NSNumber *option in self.selectedOptions) {
        switch ([option integerValue]) {
            case PVMapOverlay:
                [self addOverlay];
                break;
            case PVMapPins:
                [self addAttractionPins];
                break;
            default:
                break;
        }
    }
}

现在快完成了!不过还差一点,你需要实现另外一个delegate方法,这个delegate方法给map view提供一个MKAnnotationView 实例,这样才能够进行渲染。

将下面的代码添加到 PVParkMapViewController.m:

- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id)annotation {
    PVAttractionAnnotationView *annotationView = [[PVAttractionAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"Attraction"];
    annotationView.canShowCallout = YES;
    return annotationView;
}

上面的方法会接受一个 MKAnnotation ,并用此来创建一个 PVAttractionAnnotationView. 它的canShowCallout 属性设置为YES,这样当用户触摸到这个注解时,会显示出更多的信息,最后将这个注解返回!

启动景点Pin,看看结果是什么样,如下图:

景点pin在这里看起来有点不圆滑!:]

至此,已经介绍了MapKit许多复杂的知识了,包括overlays和annotation。不过如果你希望绘制一些基本的图形呢:线条、形状和圆等?

使用MapKit framework同样可以在map view上进行绘制!MapKit提供的MKPolylineMKPolygon 和 MKCircle 就可以做到.

我走的线路 – MKPolyline

如果你去过魔山,你应该知道 Goliath hypercoaster 是一个非常令人难以置信的旅程, 当骑手们跨进了大门,就想一路狂奔!:]

为了帮助这些骑手们,你绘制了一条从公园入口到Goliath的路径。

通过MKPolyline 可以非常方便的画一条连接多个点的线,比如绘制一条从A到B的非直线路径。在程序中用 MKPolyline 绘制路径,Goliath的粉丝们会更加快捷的骑行。!需要一系列的经纬度坐标信息,这些坐标将用来按顺序的进行绘制。在这里顺序非常重要 — 否则,绘制出来的线路会是无用的,对骑手们来说毫无意义!

resources for this tutorial 中包含了一个叫 EntranceToGoliathRoute.plist 的文件,里面有路径信息,将这个文件添加到工程中。

这个plist文件中的内容如下:

<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE plist PUBLIC ”-//Apple//DTD PLIST 1.0//EN” ”http://www.apple.com/DTDs/PropertyList-1.0.dtd”>
<plist version=”1.0″>
<array>
<string>{34.42367,-118.594836}</string>
<string>{34.423597,-118.595205}</string>
<string>{34.423004,-118.59537}</string>
<string>{34.423044,-118.595806}</string>
<string>{34.423419,-118.596126}</string>
<string>{34.423569,-118.596229}</string>
<string>{34.42382,-118.596192}</string>
<string>{34.42407,-118.596283}</string>
<string>{34.424323,-118.596534}</string>
<string>{34.42464,-118.596858}</string>
<string>{34.42501,-118.596838}</string>
<string>{34.42537,-118.596688}</string>
<string>{34.425690,-118.596683}</string>
<string>{34.42593,-118.596806}</string>
<string>{34.42608,-118.597101}</string>
<string>{34.42634,-118.597094}</string>
</array>
</plist>

如上,属性列表是一个简单的字符串数组,字符串中包含了路径中每个点的经度和纬度信息。

现在,你需要写一个方法来读取plist文件中的内容,并为骑手们创建出一条路。

将下面的代码添加到 PVParkMapViewController.m:

- (void)addRoute {
    NSString *thePath = [[NSBundle mainBundle] pathForResource:@"EntranceToGoliathRoute" ofType:@"plist"];
    NSArray *pointsArray = [NSArray arrayWithContentsOfFile:thePath];
 
    NSInteger pointsCount = pointsArray.count;
 
    CLLocationCoordinate2D pointsToUse[pointsCount];
 
    for(int i = 0; i &lt; pointsCount; i++) {
        CGPoint p = CGPointFromString(pointsArray[i]);
        pointsToUse[i] = CLLocationCoordinate2DMake(p.x,p.y);
    }
 
    MKPolyline *myPolyline = [MKPolyline polylineWithCoordinates:pointsToUse count:pointsCount];
 
    [self.mapView addOverlay:myPolyline];
}

上面这个方法会读取 EntranceToGoliathRoute.plist, 并遍历其中包含的内容,然后将信息单独坐标字符串转换为 CLLocationCoordinate2D 结构.

很明显,在程序中绘制折线是很简单的;你创建一个数组,数组里面包含所有的点,并将这个数组传递给 MKPolyline! 没有比这更简单的了。

下面,你需要添加一个选项,允许用户打开或者关闭折线路径。

用下面的代码更新一下 loadSelectedOptions 方法:

- (void)loadSelectedOptions {
    [self.mapView removeAnnotations:self.mapView.annotations];
    [self.mapView removeOverlays:self.mapView.overlays];
    for (NSNumber *option in self.selectedOptions) {
        switch ([option integerValue]) {
            case PVMapOverlay:
                [self addOverlay];
                break;
            case PVMapPins:
                [self addAttractionPins];
                break;
            case PVMapRoute:
                [self addRoute];
                break;
            default:
                break;
        }
    }
}

最后,为了将所有的这些内容结合在一起,需要更新一下delegate方法—返回在map view中实际需要渲染的view。

更新一下 mapView:viewForOverlay: 方法,以能够处理折线图层的情况,如下:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id)overlay {
    if ([overlay isKindOfClass:PVParkMapOverlay.class]) {
        UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"];
        PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage];
 
        return overlayView;
    } else if ([overlay isKindOfClass:MKPolyline.class]) {
        MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay];
        lineView.strokeColor = [UIColor greenColor];
 
        return lineView;
    }
 
    return nil;
}

折线图层显示的处理跟之前的overlay view十分类似。只不过,绘制折线的时候,不需要提供自己的view对象。只需要使用 MKPolyLineView 即可—用overlay初始化一个新的实例即可.

MKPolyLineView 同样提供可以修改折线的一些属性。比如,可以把折线的颜色修改为绿色。

编译并运行程序,将route选项打开,出现在屏幕中的内容如下图所示:

Goliath的粉丝们现在可以利用这个绘制的路径去创造新的记录啦!:]

要是能够实际的显示出公园的边界范围就太棒了——因为公园实际上并不是完整的占据整个屏幕。

虽然可以使用 MKPolyline 来绘制公园的边界,不过MapKit提供了另外一个类专门来绘制封闭的多边形:MKPolygon.

不要围着我 – MKPolygon

MKPolygon 跟MKPolyline非常相似(坐标集合中除了第一个点和最后一个点是连接以外)

可以创建一个 MKPolygon ,将其当做显示公园边界的一个overylay。公园的边界坐标已经定义在the MagicMountain.plist中了; 可以返回之前的内容看看 initWithFilename: 方法是如何从plist文件中读取出边界点的。

将下面的代码添加到 PVParkMapViewController.m:

- (void)addBoundary {
    MKPolygon *polygon = [MKPolygon polygonWithCoordinates:self.park.boundary
                                                     count:self.park.boundaryPointsCount];
    [self.mapView addOverlay:polygon];
}

addBoundary 方法的实现非常简单。从park实例中制定边界数组和边界点数,这样就可以很快捷的创建一个新的 MKPolygon 实例了!

你能猜猜下一步要做什么吗?跟上面的MKPolyline 操作非常类似.

更新一下 loadSelectedOptions 方法,以能够显示或者隐藏公园的边界,如下所示:

- (void)loadSelectedOptions {
    [self.mapView removeAnnotations:self.mapView.annotations];
    [self.mapView removeOverlays:self.mapView.overlays];
    for (NSNumber *option in self.selectedOptions) {
        switch ([option integerValue]) {
            case PVMapOverlay:
                [self addOverlay];
                break;
            case PVMapPins:
                [self addAttractionPins];
                break;
            case PVMapRoute:
                [self addRoute];
                break;
            case PVMapBoundary:
                [self addBoundary];
                break;
            default:
                break;
        }
    }
}

MKPolygon 跟MKPolyline一样,遵循 MKOverlay ,因此也需要更新一下相关的delegate方法。

PVParkMapViewController.m中更新一下delegate方法:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id)overlay {
    if ([overlay isKindOfClass:PVParkMapOverlay.class]) {
        UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"];
        PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage];
 
        return overlayView;
    } else if ([overlay isKindOfClass:MKPolyline.class]) {
        MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay];
        lineView.strokeColor = [UIColor greenColor];
 
        return lineView;
    } else if ([overlay isKindOfClass:MKPolygon.class]) {
        MKPolygonView *polygonView = [[MKPolygonView alloc] initWithOverlay:overlay];
        polygonView.strokeColor = [UIColor magentaColor];
 
        return polygonView;
    }
 
    return nil;
}

如上所示,delegate的更新方式跟之前一样简单。创建一个MKPolygonView实例,并将其颜色设置为magenta.

编译并运行程序,将看到公园的边界!

上面就是折线和多边形的绘制。下面我将介绍最后一种绘制——使用MKCircle绘制圆

沙滩上的圈圈 – MKCircle

MKCircle 跟 MKPolyline 和 MKPolygon,同样非常相似。只不过它是根据给定的中心坐标和半径来绘制一个圆。

可以想象一下,在公园里,用户可能希望在地图上做一下标注,跟将这个标注与他人分享。此时,就可以使用一个圆来代表用户的标注。

在本文中,你不会走的很远,不过,最起码的,你可以从文件中加载一些坐标数据,并在地图上绘制出一些圆,来当做用户在地图上的一些标注。

MKCircle overlay 可以很容易的就实现这个功能.

resources for this tutorial 包含了相关标注的位置信息,只需要将其添加到工程中。

每个文件包含了一些坐标信息。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
        <string>{34.42481,-118.596914}</string>
        <string>{34.423383,-118.596101}</string>
        <string>{34.423628,-118.595197}</string>
        <string>{34.421832,-118.595404}</string>
</array>
</plist>

我将用 PVCharacter代表用户的标注。下面就在Models群组中创建一个新类 PVCharacter ,并继承自 MKCircle.

然后用下面的代码替换 PVCharacter.h 中的内容:

#import &lt;Foundation/Foundation.h&gt;
#import &lt;MapKit/MapKit.h&gt;
 
@interface PVCharacter : MKCircle 
 
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) UIColor *color;
 
@end

新的这个类遵循 MKOverlay 协议, 并定义了两个属性: name和 color.

这个类的实现非常简单 — 不需要添加任何内容.

#import "PVCharacter.h"
 
@implementation PVCharacter
 
@end

PVParkMapViewController.m 中import PVCharacter.h。如下代码:

#import "PVCharacter.h"

现在,需要一个方法,将plist文件中的数据加载到程序中。将如下代码添加到PVParkMapViewController.m:

- (void)addCharacterLocation {
    NSString *batmanFilePath = [[NSBundle mainBundle] pathForResource:@"BatmanLocations" ofType:@"plist"];
    NSArray *batmanLocations = [NSArray arrayWithContentsOfFile:batmanFilePath];
    CGPoint batmanPoint = CGPointFromString(batmanLocations[rand()%4]);
    PVCharacter *batman = (PVCharacter *)[PVCharacter circleWithCenterCoordinate:CLLocationCoordinate2DMake(batmanPoint.x, batmanPoint.y)
                                                                       radius:MAX(5, rand()%40)];
    batman.color = [UIColor blueColor];
 
    NSString *tazFilePath = [[NSBundle mainBundle] pathForResource:@"TazLocations" ofType:@"plist"];
    NSArray *tazLocations = [NSArray arrayWithContentsOfFile:tazFilePath];
    CGPoint tazPoint = CGPointFromString(tazLocations[rand()%4]);
    PVCharacter *taz = (PVCharacter *)[PVCharacter circleWithCenterCoordinate:CLLocationCoordinate2DMake(tazPoint.x, tazPoint.y)
                                                                       radius:MAX(5, rand()%40)];
    taz.color = [UIColor orangeColor];
 
    NSString *tweetyFilePath = [[NSBundle mainBundle] pathForResource:@"TweetyBirdLocations" ofType:@"plist"];
    NSArray *tweetyLocations = [NSArray arrayWithContentsOfFile:tweetyFilePath];
    CGPoint tweetyPoint = CGPointFromString(tweetyLocations[rand()%4]);
    PVCharacter *tweety = (PVCharacter *)[PVCharacter circleWithCenterCoordinate:CLLocationCoordinate2DMake(tweetyPoint.x, tweetyPoint.y)
                                                                       radius:MAX(5, rand()%40)];
    tweety.color = [UIColor yellowColor];
 
    [self.mapView addOverlay:batman];
    [self.mapView addOverlay:taz];
    [self.mapView addOverlay:tweety];
}

上面的这个方法对每个标注都做了相同的操作。首先,从plist中读出数据,然后随即选取出一个位置。下一步,根据选取出的位置,创建一个 PVCharacter 实例。随即选取一个半径值。

最后,给每个标注都设置一个颜色,并添加到map view中。

现在基本算是完成了 — 你还记得最后几步是如何操作的吗?

没错 — 你同样还需要通过delegate方法,给map view提供 MKOverlayView.

更新一下 PVParkMapViewController.m 中的delegate方法:

- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id)overlay {
    if ([overlay isKindOfClass:PVParkMapOverlay.class]) {
        UIImage *magicMountainImage = [UIImage imageNamed:@"overlay_park"];
        PVParkMapOverlayView *overlayView = [[PVParkMapOverlayView alloc] initWithOverlay:overlay overlayImage:magicMountainImage];
 
        return overlayView;
    } else if ([overlay isKindOfClass:MKPolyline.class]) {
        MKPolylineView *lineView = [[MKPolylineView alloc] initWithOverlay:overlay];
        lineView.strokeColor = [UIColor greenColor];
 
        return lineView;
    } else if ([overlay isKindOfClass:MKPolygon.class]) {
        MKPolygonView *polygonView = [[MKPolygonView alloc] initWithOverlay:overlay];
        polygonView.strokeColor = [UIColor magentaColor];
 
        return polygonView;
    } else if ([overlay isKindOfClass:PVCharacter.class]) {
        MKCircleView *circleView = [[MKCircleView alloc] initWithOverlay:overlay];
        circleView.strokeColor = [(PVCharacter *)overlay color]];
 
        return circleView;
    }
 
    return nil;
}

最后,更新一下 loadSelectedOptions ,让用户可以打开或者隐藏标注信息。

如下是更新后的代码:

- (void)loadSelectedOptions {
    [self.mapView removeAnnotations:self.mapView.annotations];
    [self.mapView removeOverlays:self.mapView.overlays];
    for (NSNumber *option in self.selectedOptions) {
        switch ([option integerValue]) {
            case PVMapOverlay:
                [self addOverlay];
                break;
            case PVMapPins:
                [self addAttractionPins];
                break;
            case PVMapRoute:
                [self addRoute];
                break;
            case PVMapBoundary:
                [self addBoundary];
                break;
            case PVMapCharacterLocation:
                [self addCharacterLocation];
                break;
            default:
                break;
        }
    }
}

编译并运行程序,然后打开character,将看到如下内容:

何去何从?

上面的内容就是本文要介绍的了 — 至此,你已经知道如何使用MapKit绘制自己的overylay image和overlay view。

这里是本文最后完成工程代码: final example project .

恭喜你 — 你已经使用了MapKit提供的许多重要的功能了。并完成了一个地图程序最基本的功能,实现了标注,卫星视图和定制图层!

这个程序的另一个方向就是研究一下创建地图图层的其它方法。

实际上,从简单到复杂的,有许多不同的方法来创建图层。本文使用的方法是图片: overlay_park .

为了生成图层,将卫星视图的截图当做公园的底层。然后绘制一些过山车,树,停车点,以及其它的一些新图层。

当获得了卫星截图后,需要给这个截图的4个角落确定一下经纬度。这些经纬度将用来创建公园的属性列表 — 用在map view上的位置图层信息。

这里还有很多高级的方法来创建图层 — 或许更加高效. 一些可选的方法是使用 KML 文件, MapBox tiles,或者其它第三方提供的资源。

本文的主要目的是介绍关于MapKit framework APIs,因此没有深入介绍overylay类型。如果你是一名地图高级开发者,你可以更加深入的研究一下其它方面的主题。

希望本文对你有用,我也希望能够看到你在程序中使用MapKit overylay。如果你有任何问题或者建议,请反馈给我!


本文是由iOS Tutorial 组员 Chris Wagner编写的,是 一名狂热的开发者!

使用MapKit叠加图片和视图教程(1)

Learn how to add an overlay image using MapKit!

学习如何使用MapKit在地图上添加一个叠加图层

本文是由iOS Tutorial小组成员Chris Wagner撰写,他是一名软件工程爱好者,一直在努力做一名技术前沿的开发者。

使用MapKit在程序中添加一个地图是非常容易的事。不过,如果你希望使用自己的注解和图片来装饰或者定制苹果提供的地图呢?
非常幸运的是苹果提供了非常容易的方法来完成这样的需求:自定义叠加图层。
在本文中,你将为六七魔术山(Six Flags Magic Mountain)游乐园创建一个程序。如果你在洛杉矶是一个做过山车的粉丝,你会喜欢这个程序的 :]
想想,一个来到游乐园的游人都会喜欢这个程序的一些功能:例如具体景点的位置,各种过山车的路线,以及公园的一些特点。这些内容的显示通过定制一个叠加图片是非常好的选择——这真是本文要介绍的内容。

注意: 根据你的经验水平,学习本文,你有两种选择:

  1. 对MapKit已经熟悉了? 如果你对MapKit已经熟悉了,并且你想要马上学习叠加图层的内容,你可以忽略(或略读)掉前面的内容,直接跳到“是什么样的一个view”小节—在这里我为你准备了一个启动项目。
  2. 对MapKit还是一个新手?如果你对MapKit还一无所知,那么请继续往下阅读,我将从最基础的内容开始在程序中添加一个地图!

继续阅读!

标签: , , , ,

26 March 2013

如何升级程序以适应4英寸的iPhone5显示

本文是由 iOS Tutorial 小组成员 Adam Burkepile撰写,他是一名全职的软件咨询师和iOS独立开发者。 这里可以看到他最近开发的程序: Pocket No Agenda, 还可以在 Twitter中follow他。

iPhone 5配备了更大的屏幕——你的程序有更多的使用空间:额外增加了88个点(176像素)。

这跟之前的Retina显示屏一样,你需要对程序做一些修改,以利用更大的屏幕带来的好处。

很幸运的是很容易就能够利用上新的屏幕尺寸。然后稍微做一些设计,你的程序就可以用有趣的方式使用新增的空间。

如果你的程序还没有更新至适配新的4英寸屏幕,那么通过本文,你就可以很容的进行适配并提升你的应用体验!

 

 

 

开始

首先,你的Xcode版本需要支持iPhone 5的开发——也就是Xcode版本至少是4.5。通过这样的操作你可以查看Xcode的版本: XcodeAbout Xcode. 如果你的版本早于4.5,那么需要到Mac App Store下载或者升级到最新版本。

注意:Xcode 4.5中,苹果放弃了对armv6指令集的支持。也就意味着你用Xcode 4.5以及更新的版本创建的程序,将不能在最初的iPhone,iPhone 3G,或者第一代iPod Touch和第二代iPod Touch上运行了。

 

 

本文的工程代码:如果你是这个网站的忠实读者,那么可能你以及下载过了—— How to Use Blocks in iOS 5 – 我在这个网站写的第一篇文章!

虽然Ray的妻子Vicki为这个iOS Diner项目创建出了非常可爱的画面,但是为了将这些画面放适当的屏幕中,这闲得太拥挤——从来没有让我舒服过。现在本文介绍的内容是个非常好的机会来解决这个问题。

从以前的教程中下载完整的工程代码(修改了一些bug): here .

打开工程文件,然后立马编译并运行它。确保运行在 iPhone 6.1 模拟器中, 因为所有的iPhone 5s都将至少运行在iOS 6中。如果程序没有运行在 iPhone 5 4-inch 模拟器中, 那么在Hardware菜单中的Device里面,修改一下。接着再编译运行程序。如下图所示:


噢! iPhone 5 Simulator 是个4英寸的家伙!

iPhone 4 Inch Screen with Wasted Space

Oh,稍等。在模拟器两边难看的黑边(红色圆圈标注)是什么东西?这是由于你的程序还没有对iPhone 5适配。现在你需要对其进行修改!

Hey, 你获得了更多的空间!

实际上让你的程序兼容4英寸的屏幕非常简单。

如果之前你一直在做相关的适配工作,并设置过启动画面,那么现在你应该有这样的两个图片: Default.png 和 Default@2x.png. 现在针对启动画面你所需要做的就是添加一个名为 Default–568h@2x.png 的启动图片! 因为所有4英寸的屏幕都是retina,所你这里不需要相应的non-@2x 版本图片。很简单,不是吗?

这里现在这个新的启动图(this new launch image )名字叫做 Default–568h@2x.png 并将其拖拽到工程中,如下图操作所示.

 

编译并运行程序.

首先,程序加载过程中是正确的 — 一个大的启动图片:


但是后来呢…

 

嗯, 画面并没有完全自动的与屏幕尺寸匹配上。从上图,可以看到你还有多少屏幕没有利用上, 现在你必须要做一点处理,让其与屏幕相适配。

出现上图中的这个原因是因为当初为这个程序所做的设计是针对通用屏幕尺寸的。并没有使用自动调整大小(autosizing),自动布局(Auto Layout),或者其它一些技术,现在就来修正一下这个问题吧!

使用约束(Constraints)

这里有几种不同的方法可以根据不同的屏幕尺寸对视图大小进行调整。在本文中,我将使用iOS 6中引入的新技术: Auto Layout system . 通过Auto Layout,可以把view固定到边缘上,并可以把view设置为自动扩大或者缩小。

注意: 本文只涉及到Auto Layout中的初级知识。相关更深入的知识,可以查看本网站上iOS 6系列文章:Beginning Auto Layout . 这里的链接可以看到第一部分: here.

首先,开启 Auto Layout. 必须在每个storyboard中都要进行开启Auto Layout功能。

在本工程中,只有一个名为storyboardMainStoryboard.storyboard的文件。选中这个文件,然后打开file inspector,并勾选上 Use Autolayout, 如下图.


现在,在Diner程序的用户界面中,注意到向右的这个箭头,它的作用是让用户可以选择可用项中的下一项。

Right facing arrow problems with Auto Layout
 

当frame变得更大时,这个向右的箭头看起来是固定到右边缘的. 现在来对这个箭头做一些修改,让它与别的view一样,固定到左边缘上。

选中这个右箭头,然后选择Size Inspector. 可以看到view中已有的Auto Layout相关的约束。

在storyboard画布底部 ,选择这样一个外形切换按钮图标 (看起来有点像横着的 “I”) ,然后选择Leading Space to Superview, 如下图所示.

 


现在马上就可以看到新选择的约束。


在不需要重新编译并生成整个工程的情况下,就可以通过在storyboard中修改屏幕尺寸来测试这个约束在4英寸屏幕上的效果。修改屏幕尺寸的操作可以这样:菜单中,选择 EditorApply Retina 4 Form Factor, 也可以通过选择画布底部的这个外形尺寸按钮(这个按钮是一个矩形框,顶部和底部分别有一个箭头),如下图。


嗯, 如下图,看起来有点跟预期的不一样!重新选择右箭头和Size Inspector ,并查看是什么原因。

 

哇, 如上图右边,现在这个右箭头有3个约束了。一个是与顶部其它的按钮对齐,这是正确的。还有一个是刚刚添加的。

但是中间这个:Trailing Space to: Superview, 这是将这个箭头按钮的右边固定到父视图(superview)的右边。因此,结合新的约束是固定到左边,而旧的约束是固定到右边,这样的话当屏幕增大的时候,Auto Layout除了将这个按钮拉伸,就没有别的选择了。!

要修正这个问题很容易,不过在做任何操作之前,确定切回到更小的屏幕尺寸,否则将不会像预期的一样正常工作。(要明白为什么,请看下面的注意部分。)

切回到更小的屏幕尺寸之后,选中 Trailing Space to: Superview 约束的齿轮图标,然后选择 Delete. 这里不再需要将右箭头按钮固定到superview的右边。


当你再切回到大屏幕尺寸时,图片看起来是下面这样:


嗯, 现在好多了.

为什么要切回到更小的屏幕尺寸? 如果在显示大屏幕内容时,删除约束,Xcode会添加新的宽度约束,以强制右箭头跟以前一样是拉伸的。因此要避免这个问题,你需要分别对待两种屏幕尺寸!

出现上面这种情况,是因为当Auto Layout开启时,你对view或者约束做任何的修改,都会引起Xcode重新计算出当前的约束内容。Xcode为了在当前屏幕尺寸中正确显示视图,总是会默认的添加相关约束。所以,当箭头处于拉伸状态时,如果你删除了这个约束,Xcoe会添加另外一个约束,以让这个箭头保持拉伸状态。

这就是为什么在结合使用Auto Layout时,你会经常看到一些你不想看到的一些约束。当发生这种情况时,简单的将其删除是不行的,因为Xcode会再次添加约束。相反,你需要创建不同的约束,直到Xcode认为你删除的这个约束不再需要。

 

难以执行的伸缩Diner

现在,你需要对diner本身进行拉伸 – 包括背景和计数图片。

切回更小的屏幕尺寸 然后同时选中背景和计数视图: 分别是Image View – bg_wall.png 和 Image View – bg_counter.png. 它们已经有约束了,当屏幕增大时,这些视图会被拉伸。下面我们再来选中pin图标…


添加 Trailing Space to Superview 约束,如下图所示…


然后再点击外形切换按钮,来看看具体效果.

 

如上图,背景图片如预期的一样被扩大了,但是图片不够大,所以被居中显示了。当程序运行在4英寸的屏幕上时,你需要修改图片,但是这里没有这样的一种机制:“针对3.5英寸屏幕使用这个图片,而针对4英寸屏幕则使用别的图片”。因此,在这里需要通过编写代码进行处理。

在storyboard中,打开assistant view,通过control-drag counter和background UIView来创建IBoutlets,名字分别为 ibCounter 和 ibBackgroundImage.

 

 


接下来,在Supporting Files 群组中选择 iOSDiner-Prefix.pch 文件,并在底部的#endif 上面添加如下代码 (归功于StackOverflow):

#define IS_IPHONE5 (([[UIScreen mainScreen] bounds].size.height-568)?NO:YES)

 

通过上面这行代码你可以知道程序现在是否运行在大尺寸屏幕中。代码实际上就是通过当前屏幕的尺寸,减去iPhone 5的屏幕尺寸,如果结果是0,那么说明设备是iPhone 5。

下面在 IODViewController.m文件中使用这个宏。 将如下代码添加到viewWillAppear:最后:

if (IS_IPHONE5) {
    self.ibBackgroundImage.image = [UIImage imageNamed:@"bg_wall-568h.png"];
    self.ibCounter.image = [UIImage imageNamed:@"bg_counter-568h.png"];
}

 

上面的代码首先检查当前程序是否运行在iPhone 5上(大屏幕);如果是的话,设置一个新的,大版本的图片。这里的图片已经存在于之前下载的starter工程中了。

在这里,可能你要问“不应该是bg_wall–568h@2x.png吗?嗯, 不是的. 请记住UIImage会自动的添加“@2x” 来寻找针对retina的图片。因此即使你工程中包含的是 bg_wall–568h@2x.png 图片, 最终选择使用图片时也不需要@“2x”。

编译并运行程序,效果如下图所示:

 

移开点,你挤到我了!

 

现在,程序有一个更大的背景了,但是右边的空间还是没有利用上。而这时,所有的东西都挤在左边了。下面就来对收银员位置重新定位一下,以利用额外的空间。

跟之前一样,确保使用3.5英寸的外形尺寸来编辑storyboard。现在选中Bill(视图的名字是 Image View – person.png) ,并添加一个新的 Trailing Space to SuperView pin 约束.


跟右箭头一样,Bill已经有一个Leading 约束了, 所以,如果现在切到4英寸的屏幕上,Bill将被拉伸。你知道的,Bill虽然是工作在一个快餐厅,但是他并没有那么胖!选中 Leading Space to: Image View 约束并将其删除掉。


再次切换一下外形尺寸.


非常棒!Bill现在被固定到右边,并利用上了更大的屏幕空间了。

 

注意: 描述区域(如上图现在是“Hamburger”)同样被固定到右边了。但是你并没有对其进行修改呀。这是怎么发生的呢?

这是因为这个图片被固定到文本标签上,而这个文本标签是被固定到Bill图片上的。因此当Bill的位置发生改变时,也会对描述文本控件和图片进行更新。Cool,不是吗?

 

等等… Auto Layout必须在iOS 6 中吗?

 

现在如果你将这个iOS Diner程序运行在iPhone5.1 Simulator上,程序在启动的时候就会crash(奔溃)掉,实际上, 实际上,只要你在storyboard上开启Auto Layout,那么就会发生这个错误。这也是为什么Auto Layout必须要在iOS 6中才支持。

对于这个程序可能不是一个大问题,因为此时大约有87%的用户已经在使用iOS 6了( are using iOS 6),但是需要知道这点。

当你发布一个程序时,你需要决定程序将支持iOS最低的版本。针对全新的程序,你可能希望选择iOS 6,但是在本文中,是对已有的程序进行升级。如果你提高了支持的最低版本,可能当前的用户并没有升级到最新的iOS版本,那么就会遇到问题哦。

通常,你有两个选择:

  1. 你可以修改工程的部署目标(deployment target),指定你的程序运行所需要的iOS版本。
  2. 你可以让你的程序向后兼容。

下面我将对这两个选择都进行介绍。在本文中,你也可以只做一个,当然两个也行 — 如果你愿意的话,也可以忽略掉本节的全部内容!

修改部署目标( deployment target)

要修改部署目标,可以这样操作:在工程导航面板中,选中 iOSDiner 工程,然后选中顶部的iOSDiner(在Project下面). 接着选择 Info 选项卡,然后将 iOS Deployment Target 修改至6.0 (最新).


注意,你也可以直接在Targets列表中修改iOSDiner 生成的target,如下所示:

 

上面两种方法的区别是target中设置的值会替代project中设置的。如果你要生成多个target以部署到不同的平台版本上,这个方法非常有用。

使程序向后兼容

如果你希望让程序向后兼容,以支持更早的iOS版本,那么你将必须做更多的工作。如果你使用的是Storyboard(跟IOSDiner一样),那么最简单的方法就是创建2个不同的Storyboard,一个是使用Auto Layout,另外一个不使用。然后在程序启动的时候,告诉程序使用哪个Storyboard。

现在程序被设置为启动的时候自动加载一个storyboard。这涉及到工程的Main Storyboard.

要根据具体的iOS版本使用不同的storyboard,那么就不能在程序启动的时候自动加载一个storyboard了。要停止自动加载storyboard,可以这样操作:清除target中Summary 设置选项里面的 Main Storyboard 内容. 只需要清除当前的值就可以,如下图所示:


接着,选中 MainStoryboard.storyboard 然后选择 FileDuplicate…. 将新的文件命名为MainStoryboard-legacy.storyboard 并将其保存.


现在选中 MainStoryboard-legacy.storyboard, 将显示出来的编辑器设置为更小的外观尺寸,然后取消勾选 Use Autolayout.

 

现在打开 IODAppDelegate.m 文件,然后按照如下进行修改(来自 this 文章)

将下面的代码添加到 @implementation IODAppDelegate上面:

#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:(v) options:NSNumericSearch] != NSOrderedAscending)

 

上面的代码定义了一个宏,当你在调用这个宏的时候,可以根据用当前设备中iOS版本跟指定的版本进行比较,以判断当前的设备版本是否比制定的版本还要新。

用下面的代码替换application:didFinishLaunchingWithOptions: :

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  // 1
  UIStoryboard *mainStoryboard = nil;
  if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"6.0")) {
    mainStoryboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
  } else {
    mainStoryboard = [UIStoryboard storyboardWithName:@"MainStoryboard-legacy" bundle:nil];
  }
 
  // 2
  self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  self.window.rootViewController = [mainStoryboard instantiateInitialViewController];
  [self.window makeKeyAndVisible];
 
  return YES;
}

 

上面的代码会发生如下情况:

  1. 检查设备中运行的iOS版本。如果是最新的6.0,则加载开启了Auto Layout功能的storyboard。否则,加载没有开启Auto Layout功能的storyboard。
  2. 然后把storyboard创建的view controller设置给程序的window。

现在,你针对Auto Layout处理和设备的兼容,你已经有两个很好的选择了 – 但是现在还有一个大问题。

如何利用程序中额外多出来的空间呢?

有了新的界面空间,屏幕将不在像之前那样拥挤了。 针对多出来的空间最好在美学上加强设计,而不是利用多出来的空间增加功能,如果你愿意的话,本文到现在已经完成了4英寸屏幕的匹配工作,你可以停止下来了。不过增加出来的空间你可以做什么呢?

 

如何使用新增的空间呢?一般来说你有两个选择:

首先,如果你的程序是数据驱动型(data-driven),你可以扩大视图,以显示更多的数据。在使用list或grid时,这非常游泳,比如Twitter程序。每屏中你可以添加另外的1到2个列表项。


另外一个选择就是利用额外的空间为程序增加功能。再看看Twitter程序,你可以在底部增加一个tab bar,通过这个tab bar可以访问一些经常需要访问或者难以访问到的功能。

 

现在,这里有一个很好的例子 djay for iPhone.:这个例子在额外的空间中增加了功能。

在老的3.5英寸用户界面上,开发者只能在屏幕中放置两个唱盘。用户要想访问类似查看BMP或者修改拍子速度(tempo)时,必须要修改程序的界面。

而针对新的iPhone 5,可以对程序显示的内容进行优化,一些基本的BMP信息和一个拍子速度可以全部放在一个视图中。

类似上面这样增强用户界面功能是很棒的,但是这样会引入一个非常棘手的问题:如何设计或者升级一个已有的程序,以适配老的3.5英寸和新的4英寸屏幕呢?

部分答案是取决于你的程序使用的是Storyboard还是.xib文件。如果都不是,而是使用代码创建UI,那么你可能需要使用类似之前IS_IPHONE5 这样的宏来获得屏幕的尺寸,然后对UI元素做出相应的调整。

如果你使用的是Storyboard,那么让程序向后兼容是非常容易的事情 — 创建多个Storyboard,然后在代码中使用像IS_IPHONE5这样的宏来判断当前设备需要加载哪个Storyboard。

何去何从?

恭喜! 你已经成功的将一个已经存在的程序升级到适配新的iPhone 5屏幕了,并对一些界面进行了调整,以利用上额外的空间,并为程序做了向后兼容处理,以适配早期的屏幕尺寸和iOS版本。如果现在之前的大屏幕吓到你了,那么现在你应该感觉良好啦!

你可以在这里下载到完整的工程: here.

如果你希望做一些额外的练习,那么看看下面这些:

  • 利用Auto Layout将iOS Diner中的选中项窗口和箭头居中显示在屏幕中。
  • 在iOS Diner storyboard中增加一些额外的数据(4英寸屏幕可用时) — 例如,给当前食物添加一个描述。
  • 从本网站下载其它已经完成的教程工程,并把它们升级一下,以利用4英寸的屏幕 — 或者你自己的程序!

注意: 特别感谢James Prete修正了原来iOS Block程序中的一个bug。

感谢你关注本文,如果你有任何问题,那么可以根据下面的连接加入我们的论坛的讨论!

本文是由 iOS Tutorial 小组成员 Adam Burkepile撰写,他是一名全职的软件咨询师和iOS独立开发者。 这里可以看到他最近的程序: Pocket No Agenda, 还可以在 Twitterfollow他。

标签: , , , ,

AFNetworking速成教程(1)

Learn how to use AFNetworking: an easy to use network API for iOS!

Learn how to use AFNetworking: an easy to use network API for iOS!

本文是由 iOS Tutorial 小组成员 Scott Sherwood撰写,他是一个基于位置动态加载(Dynamically Loaded)的软件公司(专业的混合定位)的共同创办人。

网络 — 你的程序离开了它就不能生存下去!苹果的Foundation framework中的NSURLConnection又非常难以理解, 不过这里有一个可以使用的替代品:AFNetworking.

AFNetworking 非常受开发者欢迎 – 它赢得了我们读者的青睐:2012年最佳的iOS Library奖(2012 Best iOS Library Award.) 所以现在我就写这篇文章来向你介绍如何在程序中有效的使用它。

AFNetworking 包括了所有你需要与在线资源交互的内容,从web services到文件下载。当你的程序在下载一个大文件期间,AFNetworking还能确保你的UI是可以响应的。

本文将介绍AFNetworking框架主要的组成部分。一路上,你将使用World Weather Online提供的咨询(Feeds)来创建一个天气(Weather)程序。刚开始使用的天气数据是静态的,不过在学完本文内容之后,程序将连接到实时的天气咨询。

今日预计:一个很酷的开发者学习所有关于AFNetworking知识,并在他的程序中使用AFNetworking。我们开始忙活吧!

开始

首先来这里(here)下载本文的启动项目。这个工程提供了一个基本的UI — AFNetworking相关代码还没有添加。

打开MainStoryboard.storyboard文件,将看到3个view controller:


从左到右,分别是:

  • 顶级(top-level)的导航控制器;
  • 用来显示天气的一个table view controller,每天一行;
  • 一个自定义的view controller (WeatherAnimationViewController) 当用户点击某个table view cell时,这个view controller将显示某一天的天气咨询。

生成并运行项目,你将看到相关的UI出现,但是什么都没有实现!因为程序需要从网络中获取到所需要的数据,而相关代码还没有添加。这也是本文中你将要实现的!

首先,你需要将AFNetworking 框架包含到工程中。如果你还没有AFNetworking的话,在这里下载最新的版本:GitHub.

当你解压出下载的文件后,你将看到其中有一个AFNetworking子文件夹,里面全是.h 和 .m 文件, 如下高亮显示的:


AFNetworking拖拽到Xcode工程中.


当出现了添加文件的选项时,确保勾选上Copy items into destination group’s folder (if needed) 和 Create groups for any added folders.

要完成相关配置,请在工程的Supporting Files群组中打开预编译头文件Weather-Prefix.pch. 然后在别的import后面添加如下一行代码:

#import "AFNetworking.h"

将AFNetworking添加到预编译头文件,意味着这个框架会被自动的添加到工程的所有源代码文件中。

很容易,不是吗?现在你已经准备好“天气”程序代码了!

操作JSON

AFNetworking通过网络来加载和处理结构化的数据是非常智能的,普通的HTTP请求也一样。尤其是它支持JSON, XML 和 Property Lists (plists).

你可以下载一些JSON数据,然后用自己的解析器来解析,但这何必呢?通过AFNetworking就可以完成这些操作!

首先,你需要测试脚本(数据)所需的一个基本URL。将下面的这个静态NSString声明到WTTableViewController.m顶部,也就是所有#import下面:

static NSString *const BaseURLString = @"http://www.raywenderlich.com/downloads/weather_sample/";

这个URL是一个非常简单的“web service”,在本文中我特意为你创建的。如果你想知道它看起来是什么样,可以来这里下载代码:download the source.

这个web service以3种不同的格式(JSON, XML 和 PLIST)返回天气数据。你可以使用下面的这些URL来看看返回的数据:

第一个数据格式使用的是JSON. JSON 是一种常见的JavaScript派生类对象格式。看起来如下:

{
    "data": {
        "current_condition": [
            {
                "cloudcover": "16",
                "humidity": "59",
                "observation_time": "09:09 PM",
            }
        ]
    }
}

注意: 如果你想要结更多关于JSON内容,请参考:Working with JSON in iOS 5 Tutorial.

当用户点击程序中的JSON按钮时,你希望对从服务中获得的JSON数据进行加载并处理。在WTTableViewController.m中,找到jsonTapped: 方法 (现在应该是空的) ,并用下面的代码替换:

- (IBAction)jsonTapped:(id)sender {
    // 1
    NSString *weatherUrl = [NSString stringWithFormat:@"%@weather.php?format=json", BaseURLString];
    NSURL *url = [NSURL URLWithString:weatherUrl];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
 
    // 2
    AFJSONRequestOperation *operation =
    [AFJSONRequestOperation JSONRequestOperationWithRequest:request
        // 3
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
            self.weather  = (NSDictionary *)JSON;
            self.title = @"JSON Retrieved";
            [self.tableView reloadData];
        }
        // 4
        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
            UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                         message:[NSString stringWithFormat:@"%@",error]
                                                        delegate:nil
                                               cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [av show];
        }];
 
    // 5
    [operation start];
}

这是你的第一个AFNetworking代码!因此,这看起来是全新的,我将对这个方法中代码进行介绍。

  1. 根据基本的URL构造出完整的一个URL。然后通过这个完整的URL获得一个NSURL对象,然后根据这个url获得一个NSURLRequest.
  2. AFJSONRequestOperation 是一个功能完整的类(all-in-one)— 整合了从网络中获取数据并对JSON进行解析。
  3. 当请求成功,则运行成功块(success block)。在本示例中,把解析出来的天气数据从JSON变量转换为一个字典(dictionary),并将其存储在属性 weather 中.
  4. 如果运行出问题了,则运行失败块(failure block),比如网络不可用。如果failure block被调用了,将会通过提示框显示出错误信息。

如上所示,AFNetworking的使用非常简单。如果要用苹果提供的APIs(如NSURLConnection)来实现同样的功能(下载和解析JSON数据),则需要许多代码才能做到。

现在天气数据已经存在于self.weather中,你需要将其显示出来。找到tableView:numberOfRowsInSection: 方法,并用下面的代码替换:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
 
    if(!self.weather)
        return 0;
 
    switch (section) {
        case 0:{
            return 1;
        }
        case 1:{
            NSArray *upcomingWeather = [self.weather upcomingWeather];
            return [upcomingWeather count];
        }
        default:
            return 0;
    }
}

table view有两个section:第一个用来显示当前天气,第二个用来显示未来的天气。

等一分钟,你可能正在思考。这里的 [self.weather upcomingWeather]是什么? 如果self.weather是一个普通的NSDictionary, 它是怎么知道 “upcomingWeather” 是什么呢?

为了更容易的解析数据,在starter工程中,有一对NSDictionary categories:

  • NSDictionary+weather.m
  • NSDictionary+weather_package.m

这些categories添加了一些方便的方法,通过这些方法可以很方便的对字典中的数据元素进行访问。这样你就可以专注于网络部分,而不是NSDictionary中数据的访问。对吧?

回到 WTTableViewController.m, 找到 tableView:cellForRowAtIndexPath: 方法,并用下面的实现替换:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"WeatherCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    NSDictionary *daysWeather;
 
    switch (indexPath.section) {
        case 0:{
            daysWeather = [self.weather currentCondition];
            break;
        }
        case 1:{
            NSArray *upcomingWeather = [self.weather upcomingWeather];
            daysWeather = [upcomingWeather objectAtIndex:indexPath.row];
        }
        default:
            break;
    }
 
    cell.textLabel.text = [daysWeather weatherDescription];
 
    // maybe some code will be added here later...
 
    return cell;
}

跟tableView:numberOfRowsInSection: 方法一样,在这里使用了便利的NSDictionary categories来获得数据。当前天的天气是一个字典,而未来几日的天气则存储在一个数组中。

生成并运行工程,然后点击JSON按钮. 这将会动态的获得一个AFJSONOperation对象, 并看到如下画面内容:


JSON 操作成功!

操作Property Lists(plists)

Property lists (或简称为 plists) 是以确定的格式(苹果定义的)构成的XML文件。苹果一般将plists用在用户设置中。看起来如下:

<dict>
  <key>data</key>
  <dict>
    <key>current_condition</key>
      <array>
      <dict>
        <key>cloudcover</key>
        <string>16</string>
        <key>humidity</key>
        <string>59</string>

上面的意思是:

  • 一个字典中有一个名为“data”的key,这个key对应着另外一个字典。
  • 这个字典有一个名为 “current_condition” 的key,这个key对应着一个array.
  • 这个数组包含着一个字典,字典中有多个key和values。比如cloudcover=16和humidity=59.

现在是时候加载plist版本的天气数据了!找到plistTapped: 方法,并用下面的实现替换:

 -(IBAction)plistTapped:(id)sender{
    NSString *weatherUrl = [NSString stringWithFormat:@"%@weather.php?format=plist",BaseURLString];
    NSURL *url = [NSURL URLWithString:weatherUrl];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
 
    AFPropertyListRequestOperation *operation =
    [AFPropertyListRequestOperation propertyListRequestOperationWithRequest:request
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, id propertyList) {
            self.weather  = (NSDictionary *)propertyList;
            self.title = @"PLIST Retrieved";
            [self.tableView reloadData];
        }
        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id propertyList) {
            UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                              message:[NSString stringWithFormat:@"%@",error]
                                                        delegate:nil
                                               cancelButtonTitle:@"OK"
                                               otherButtonTitles:nil];
            [av show];
    }];
 
    [operation start];
}

注意到,上面的代码几乎与JSON版的一致,只不过将操作(operation)的类型从AFJSONOperation 修改为 AFPropertyListOperation. 这非常的整齐:你才程序只需要修改一丁点代码就可以接收JSON或plist格式的数据了!

生成并运行工程,然后点击PLIST按钮。将看到如下内容:


如果你需要重置所有的内容,以重新开始操作,导航栏顶部的Clear按钮可以清除掉title和table view中的数据。

操作XML

AFNetworking处理JSON和plist的解析使用的是类似的方法,并不需要花费太多功夫,而处理XML则要稍微复杂一点。下面,就根据XML咨询构造一个天气字典(NSDictionary)。

iOS提供了一个帮助类:NSXMLParse (如果你想了解更多内容,请看这里的链接:SAX parser).

还是在文件WTTableViewController.m, 找到 xmlTapped: 方法,并用下面的实现替换:

- (IBAction)xmlTapped:(id)sender{
    NSString *weatherUrl = [NSString stringWithFormat:@"%@weather.php?format=xml",BaseURLString];
    NSURL *url = [NSURL URLWithString:weatherUrl];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
 
    AFXMLRequestOperation *operation =
    [AFXMLRequestOperation XMLParserRequestOperationWithRequest:request
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
            //self.xmlWeather = [NSMutableDictionary dictionary];
            XMLParser.delegate = self;
            [XMLParser setShouldProcessNamespaces:YES];
            [XMLParser parse];
        }
        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, NSXMLParser *XMLParser) {
            UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                         message:[NSString stringWithFormat:@"%@",error]
                                                        delegate:nil
                                               cancelButtonTitle:@"OK"
                                               otherButtonTitles:nil];
            [av show];
    }];
 
    [operation start];
}

到现在为止,这看起来跟之前处理JSON和plist很类似。最大的改动就是在成功块(success block)中, 在这里不会传递给你一个预处理好的NSDictionary对象. 而是AFXMLRequestOperation实例化的NSXMLParse对象,这个对象将用来处理繁重的XML解析任务。

NSXMLParse对象有一组delegate方法是你需要实现的 — 用来获得XML数据。注意,在上面的代码中我将XMLParser的delegate设置为self, 因此WTTableViewController将用来处理XML的解析任务。

首先,更新一下WTTableViewController.h 并修改一下类声明,如下所示:

@interface WTTableViewController : UITableViewController&lt;NSXMLParserDelegate&gt;

上面代码的意思是这个类将实现(遵循)NSXMLParserDelegate协议. 下一步将下面的delegate方法声明添加到@implementation后面:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict;
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string;
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName;
-(void) parserDidEndDocument:(NSXMLParser *)parser;

为了支持资讯的解析,还需要一些属性来存储相关的数据。将下面的代码添加到@implementatio后面:

@property(strong) NSMutableDictionary *xmlWeather; //package containing the complete response
@property(strong) NSMutableDictionary *currentDictionary; //current section being parsed
@property(strong) NSString *previousElementName;
@property(strong) NSString *elementName;
@property(strong) NSMutableString *outstring;

接着打开WTTableViewController.m,现在你需要一个一个的实现上面所说的几个delegate方法。将下面这个方法粘贴到实现文件中:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict  {
    self.previousElementName = self.elementName;
 
    if (qName) {
        self.elementName = qName;
    }
 
    if([qName isEqualToString:@"current_condition"]){
        self.currentDictionary = [NSMutableDictionary dictionary];
    }
    else if([qName isEqualToString:@"weather"]){
        self.currentDictionary = [NSMutableDictionary dictionary];
    }
    else if([qName isEqualToString:@"request"]){
        self.currentDictionary = [NSMutableDictionary dictionary];
    }
 
    self.outstring = [NSMutableString string];
}

当NSXMLParser发现了新的元素开始标签时,会调用上面这个方法。在这个方法中,在构造一个新字典用来存储赋值给currentDictionary属性之前,首先保存住上一个元素名称。还要将outstring重置一下,这个字符串用来构造XML标签中的数据。

然后将下面这个方法粘贴到上一个方法的后面:

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    if (!self.elementName){
        return;
    }
 
    [self.outstring appendFormat:@"%@", string];
}

如名字一样,当NSXMLParser在一个XML标签中发现了字符数据,会调用这个方法。该方法将字符数据追加到outstring属性中,当XML标签结束的时候,这个outstring会被处理。

继续,将下面这个方法粘贴到上一个方法的后面:

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
 
    // 1
    if([qName isEqualToString:@"current_condition"] ||
       [qName isEqualToString:@"request"]){
        [self.xmlWeather setObject:[NSArray arrayWithObject:self.currentDictionary] forKey:qName];
        self.currentDictionary = nil;
    }
    // 2
    else if([qName isEqualToString:@"weather"]){
 
        // Initalise the list of weather items if it dosnt exist
        NSMutableArray *array = [self.xmlWeather objectForKey:@"weather"];
        if(!array)
            array = [NSMutableArray array];
 
        [array addObject:self.currentDictionary];
        [self.xmlWeather setObject:array forKey:@"weather"];
 
        self.currentDictionary = nil;
    }
    // 3
    else if([qName isEqualToString:@"value"]){
        //Ignore value tags they only appear in the two conditions below
    }
    // 4
    else if([qName isEqualToString:@"weatherDesc"] ||
            [qName isEqualToString:@"weatherIconUrl"]){
        [self.currentDictionary setObject:[NSArray arrayWithObject:[NSDictionary dictionaryWithObject:self.outstring forKey:@"value"]] forKey:qName];
    }
    // 5
    else {
        [self.currentDictionary setObject:self.outstring forKey:qName];
    }
 
	self.elementName = nil;
}

当检测到元素的结束标签时,会调用上面这个方法。在这个方法中,会查找一些标签:

  1. urrent_condition 元素表示获得了一个今天的天气。会把今天的天气直接添加到xmlWeather字典中。
  2. weather 元素表示获得了随后一天的天气。今天的天气只有一个,而后续的天气有多个,所以在此,将后续天气添加到一个数组中。
  3. value 标签出现在别的标签中,所以这里可以忽略掉这个标签。
  4. weatherDesc 和 weatherIconUrl 元素的值在存储之前,需要需要被放入一个数组中 — 这里的结构是为了与JSON和plist版本天气咨询格式相匹配。
  5. 所有其它元素都是按照原样(as-is)进行存储的。

下面是最后一个delegate方法!将下面这个方法粘贴到上一个方法的后面:

-(void) parserDidEndDocument:(NSXMLParser *)parser {
    self.weather = [NSDictionary dictionaryWithObject:self.xmlWeather forKey:@"data"];
    self.title = @"XML Retrieved";
    [self.tableView reloadData];
}

当NSXMLParser解析到document的尾部时,会调用这个方法。在此,xmlWeather字典已经构造完毕,table view可以重新加载了。

在上面代码中将xmlWeather添加到一个字典中,看起来是冗余的, 不过这样可以确保与JSON和plist版本的格式完全匹配。这样所有的3种数据格式(JSON, plist和XML)都能够用相同的代码来显示!

现在所有的delegate方法和属性都搞定了,找到xmlTapped: 方法,并取消注释成功块(success block)中的一行代码:

-(IBAction)xmlTapped:(id)sender{
    ...
    success:^(NSURLRequest *request, NSHTTPURLResponse *response, NSXMLParser *XMLParser) {
        // the line below used to be commented out
        self.xmlWeather = [NSMutableDictionary dictionary];
        XMLParser.delegate = self;
    ...
}

生成和运行工程,然后点击XML按钮,将看到如下内容:


一个小的天气程序

嗯, 上面的这个程序看起来体验不太友好,有点像整周都是阴雨天。如何让table view中的天气信息体验更好点呢?

再仔细看看之前的JSON格式数据:JSON format from before,你会看到每个天气项里面都有一个图片URLs。 将这些天气图片显示到每个table view cell中,这样程序看起来会更有意思。

AFNetworking给UIImageView添加了一个category,让图片能够异步加载,也就是说当图片在后台下载的时候,程序的UI界面仍然能够响应。为了使用这个功能,首先需要将这个category import到WTTableViewController.m文件的顶部:

#import "UIImageView+AFNetworking.h"
找到tableView:cellForRowAtIndexPath: 方法,并将下面的代码粘贴到最后的return cell; 代码上上面(这里应该有一个注释标记)
__weak UITableViewCell *weakCell = cell;
 
[cell.imageView setImageWithURLRequest:[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:daysWeather.weatherIconURL]]
                      placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                               success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image){
                                   weakCell.imageView.image = image;
 
                                   //only required if no placeholder is set to force the imageview on the cell to be laid out to house the new image.
                                   //if(weakCell.imageView.frame.size.height==0 || weakCell.imageView.frame.size.width==0 ){
                                   [weakCell setNeedsLayout];
                                   //}
                               }
                               failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error){
 
                               }];

首先创建一个弱引用(weak)的cell,这样就可以在block中使用这个cell。如果你直接访问cell变量,Xcode会提示一个关于retain循环和内存泄露的警告。

UIImageView+AFNetworking category定义了一个setImageWithURLRequest… 方法. 这个方法的参数包括:一个指向图片URL的请求,一个占位符图片,一个success block和一个failure block。

当cell首次被创建的时候,cell中的UIImageView将显示一个占位符图片,直到真正的图片被下载完成。在这里你需要确保占位符的图片与实际图片尺寸大小相同。

如果尺寸不相同的话,你可以在success block中调用cell的setNeedsLayout方法. 上面代码中对两行代码进行了注释,这是因为这里的占位符图片尺寸正好合适,留着注释,可能在别的程序中需要用到。

现在生成并运行工程,然后点击之前添加的3个操作中的任意一个,将看到如下内容:


很好! 异步加载图片从来没有这么简单过。

一个RESTful类

到现在你已经使用类似AFJSONRequestOperation这样的类创建了一次性的HTTP请求。另外,较低级的AFHTTPClient类是用来访问单个的web service终端。 对这个AFHTTPClient一般是给它设置一个基本的URL,然后用AFHTTPClient进行多个请求(而不是像之前的那样,每次请求的时候,都创建一个AFHTTPClient)。

AFHTTPClient同样为编码参数、处理multipart表单请求body的构造、管理请求操作和批次入队列操作提供了很强的灵活性,它还处理了整套RESTful (GET, POST, PUT, 和 DELETE), 下面我们就来试试最常用的两个:GET 和 POST.

注意: 对REST, GET和POST不清楚?看看这里比较有趣的介绍 – 我如何给妻子解释REST(How I Explained REST to My Wife.)

WTTableViewController.h 顶部将类声明按照如下修改:

@interface WTTableViewController : UITableViewController

在 WTTableViewController.m中,找到httpClientTapped: 方法,并用下面的实现替换:

- (IBAction)httpClientTapped:(id)sender {
    UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"AFHTTPClient" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitles:@"HTTP POST",@"HTTP GET", nil];
    [actionSheet showFromBarButtonItem:sender animated:YES];
}

上面的方法会弹出一个action sheet,用以选择GET和POST请求。粘贴如下代码以实现action sheet中按钮对应的操作:

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{
    // 1
    NSURL *baseURL = [NSURL URLWithString:[NSString stringWithFormat:BaseURLString]];
    NSDictionary *parameters = [NSDictionary dictionaryWithObject:@"json" forKey:@"format"];
 
    // 2
    AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:baseURL];
    [client registerHTTPOperationClass:[AFJSONRequestOperation class]];
    [client setDefaultHeader:@"Accept" value:@"application/json"];
 
    // 3
    if (buttonIndex==0) {
        [client postPath:@"weather.php"
              parameters:parameters
                 success:^(AFHTTPRequestOperation *operation, id responseObject) {
                     self.weather = responseObject;
                     self.title = @"HTTP POST";
                     [self.tableView reloadData];
                 }
                 failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                     UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                                  message:[NSString stringWithFormat:@"%@",error]
                                                                 delegate:nil
                                                        cancelButtonTitle:@"OK" otherButtonTitles:nil];
                     [av show];
 
                 }
         ];
    }
    // 4
    else if (buttonIndex==1) {
        [client getPath:@"weather.php"
             parameters:parameters
                success:^(AFHTTPRequestOperation *operation, id responseObject) {
                    self.weather = responseObject;
                    self.title = @"HTTP GET";
                    [self.tableView reloadData];
                }
                failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                    UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                                 message:[NSString stringWithFormat:@"%@",error]
                                                                delegate:nil
                                                       cancelButtonTitle:@"OK" otherButtonTitles:nil];
                    [av show];
 
                }
         ];
    }
}

上面的代码作用如下:

  1. 构建一个baseURL,以及一个参数字典,并将这两个变量传给AFHTTPClient.
  2. 将AFJSONRequestOperation注册为HTTP的操作, 这样就可以跟之前的示例一样,可以获得解析好的JSON数据。
  3. 做了一个GET请求,这个请求有一对block:success和failure。
  4. POST请求跟GET一样。

在这里,将请求一个JSON回应,当然也可以使用之前讨论过的另外两种格式来代替JSON。

生成并运行工程,点击HTTPClient按钮,然后选择GET 或 POST按钮来初始化一个相关的请求。之后会看到如下内容:


至此,你已经知道AFHTTPClient最基本的使用方法。不过,这里还有更好的一种使用方法,它可以让代码更加干净整齐,下面我们就来学习一下吧。

连接到Live Service

到现在为止,你已经在table view controller中直接调用了AFRequestOperations 和 AFHTTPClient. 实际上,大多数时候不是这样的,你的网络请求会跟某个web service或API相关。

AFHTTPClient已经具备与web API通讯的所有内容。AFHTTPClient在代码中已经把网络通讯部分做了解耦处理,让网络通讯的代码在整个工程中都可以重用。

下面是两个关于AFHTTPClient最佳实践的指导:

  1. 为每个web service创建一个子类。例如,如果你在写一个社交网络聚合器,那么可能就会有Twitter的一个子类,Facebook的一个子类,Instragram的一个子类等等。
  2. 在AFHTTPClient子类中,创建一个类方法,用来返回一个共享的单例,这将会节约资源并省去必要的对象创建。

当前,你的工程中还没有一个AFHTTPClient的子类,下面就来创建一个吧。我们来处理一下,让代码清洁起来。

首先,在工程中创建一个新的文件:iOSCocoa TouchObjective-C Class. 命名为WeatherHTTPClient 并让其继承自AFHTTPClient.

你希望这个类做3件事情:

A:执行HTTP请求

B:当有新的可用天气数据时,调用delegate

C:使用用户当前地理位置来获得准确的天气。

用下面的代码替换WeatherHTTPClient.h:

#import "AFHTTPClient.h"
 
@protocol WeatherHttpClientDelegate;
 
@interface WeatherHTTPClient : AFHTTPClient
 
@property(weak) id delegate;
 
+ (WeatherHTTPClient *)sharedWeatherHTTPClient;
- (id)initWithBaseURL:(NSURL *)url;
- (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(int)number;
 
@end
 
@protocol WeatherHttpClientDelegate 
-(void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)weather;
-(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error;
@end

在实现文件中,你将了解头文件中定义的更多相关内容。打开WeatherHTTPClient.m 并将下面的代码添加到@implementation下面:

+ (WeatherHTTPClient *)sharedWeatherHTTPClient
{
    NSString *urlStr = @"http://free.worldweatheronline.com/feed/";
 
    static dispatch_once_t pred;
    static WeatherHTTPClient *_sharedWeatherHTTPClient = nil;
 
    dispatch_once(&amp;pred, ^{ _sharedWeatherHTTPClient = [[self alloc] initWithBaseURL:[NSURL URLWithString:urlStr]]; });
    return _sharedWeatherHTTPClient;
}
 
- (id)initWithBaseURL:(NSURL *)url
{
    self = [super initWithBaseURL:url];
    if (!self) {
        return nil;
    }
 
    [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
    [self setDefaultHeader:@"Accept" value:@"application/json"];
 
    return self;
}

sharedWeatherHTTPClient 方法使用Grand Central Dispatch(GCD)来确保这个共享的单例对象只被初始化分配一次。这里用一个base URL来初始化对象,并将其设置为期望web service响应为JSON。

将下面的方法粘贴到上一个方法的下面:

- (void)updateWeatherAtLocation:(CLLocation *)location forNumberOfDays:(int)number{
    NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
    [parameters setObject:[NSString stringWithFormat:@"%d",number] forKey:@"num_of_days"];
    [parameters setObject:[NSString stringWithFormat:@"%f,%f",location.coordinate.latitude,location.coordinate.longitude] forKey:@"q"];
    [parameters setObject:@"json" forKey:@"format"];
    [parameters setObject:@"7f3a3480fc162445131401" forKey:@"key"];
 
    [self getPath:@"weather.ashx"
       parameters:parameters
          success:^(AFHTTPRequestOperation *operation, id responseObject) {
            if([self.delegate respondsToSelector:@selector(weatherHTTPClient:didUpdateWithWeather:)])
                [self.delegate weatherHTTPClient:self didUpdateWithWeather:responseObject];
        }
        failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            if([self.delegate respondsToSelector:@selector(weatherHTTPClient:didFailWithError:)])
                [self.delegate weatherHTTPClient:self didFailWithError:error];
        }];
}

这个方法调用World Weather Online接口,以获得具体位置的天气信息。

非常重要!本实例中的API key仅仅是为本文创建的。如果你创建了一个程序,请在World Weather Online创建一个账号,并获得你自己的API key!

一旦对象获得了天气数据,它需要一些方法来通知对此感兴趣的对象:数据回来了。这里要感谢WeatherHttpClientDelegate 协议和它的delegate方法,在上面代码中的success 和 failure blocks可以通知一个controller:指定位置的天气已经更新了。这样,controller就可以对天气做更新显示。

现在,我们需要把这些代码片段整合到一起!WeatherHTTPClient希望接收一个位置信息,并且WeatherHTTPClient定义了一个delegate协议,现在对WTTableViewControlle类做一下更新,以使用WeatherHTTPClient.

打开WTTableViewController.h 添加一个import,并用下面的代码替换@interface声明:

#import "WeatherHTTPClient.h"
 
@interface WTTableViewController : UITableViewController

另外添加一个新的Core Location manager 属性:

@property(strong) CLLocationManager *manager;

在 WTTableViewController.m中,将下面的代码添加到viewDidLoad:的底部:

    self.manager = [[CLLocationManager alloc] init];
    self.manager.delegate = self;

上面这两行代码初始化了Core Location manager,这样当view加载的时候,用来确定用户的当前位置。Core Location然后会通过delegate回调以传回位置信息。将下面的方法添加到实现文件中:

- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation{
 
    //if the location is more than 5 minutes old ignore
    if([newLocation.timestamp timeIntervalSinceNow]&lt; 300){
        [self.manager stopUpdatingLocation];
 
        WeatherHTTPClient *client = [WeatherHTTPClient sharedWeatherHTTPClient];
        client.delegate = self;
        [client updateWeatherAtLocation:newLocation forNumberOfDays:5];  
    }
}

现在,当用户的位置有了变化时,你就可以使用WeatherHTTPClient单例来请求当前位置的天气信息。

记住,WeatherHTTPClient有两个delegate方法需要实现。将下面两个方法添加到实现文件中:

-(void)weatherHTTPClient:(WeatherHTTPClient *)client didUpdateWithWeather:(id)aWeather{
    self.weather = aWeather;
    self.title = @"API Updated";
    [self.tableView reloadData];
}
 
-(void)weatherHTTPClient:(WeatherHTTPClient *)client didFailWithError:(NSError *)error{
    UIAlertView *av = [[UIAlertView alloc] initWithTitle:@"Error Retrieving Weather"
                                                 message:[NSString stringWithFormat:@"%@",error]
                                                delegate:nil
                                       cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [av show];
}

上面的两个方法,当WeatherHTTPClient请求成功, 你就可以更新天气数据并重新加载table view。如果网络错误,则显示一个错误信息。

找到apiTapped: 方法,并用下面的方法替换:

-(IBAction)apiTapped:(id)sender{
    [self.manager startUpdatingLocation];
}

生成并运行程序,点击AP按钮以初始化一个WeatherHTTPClient 请求, 然后会看到如下画面:


希望在这里你未来的天气跟我的一样:晴天!

我还没有死!

你可能注意到了,这里调用的外部web service需要花费一些时间才能返回数据。当在进行网络操作时,给用户提供一个信息反馈是非常重要的,这样用户才知道程序是在运行中或已奔溃了。

很幸运的是,AFNetworking有一个简便的方法来提供信息反馈:AFNetworkActivityIndicatorManager.

在 WTAppDelegate.m中,找到application:didFinishLaunchingWithOptions: 方法,并用下面的方法替换:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [AFNetworkActivityIndicatorManager sharedManager].enabled = YES;
    return YES;
}

让sharedManager可以自动的显示出网络活动指示器( network activity indicator)— 无论射门时候,只要有一个新的网络请求在后台运行着。 这样你就不需要每次请求的时候,都要单独进行管理。

生成并运行工程,无论什么时候,只要有网络请求,都可以在状态栏中看到一个小的网络风火轮:


现在,即使你的程序在等待一个很慢的web service,用户都知道程序还在运行着!

下载图片

如果你在table view cell上点击,程序会切换到天气的详细画面,并且以动画的方式显示出相应的天气情况。

这非常不错,但目前动画只有一个背景图片。除了通过网络来更新背景图片,还有更好的方法吗!

下面是本文关于介绍AFNetworking的最后内容了:AFImageRequestOperation. 跟AFJSONRequestOperation一样, AFImageRequestOperation封装了HTTP请求:获取图片。

WeatherAnimationViewController.m 中有两个方法需要实现. 找到updateBackgroundImage: 方法,并用下面的代码替换:

- (IBAction)updateBackgroundImage:(id)sender {
 
    //Store this image on the same server as the weather canned files
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.scott-sherwood.com/wp-content/uploads/2013/01/scene.png"]];
    AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request
        imageProcessingBlock:nil
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
            self.backgroundImageView.image = image;
            [self saveImage:image withFilename:@"background.png"];
        }
        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
            NSLog(@"Error %@",error);
    }];
    [operation start];
}

这个方法初始化并下载一个新的背景图片。在结束时,它将返回请求到的完整图片。

在WeatherAnimationViewController.m中, 你将看到两个辅助方法:imageWithFilename: 和 saveImage:withFilename:, 通过这两个辅助方法,可以对下载下来的图片进行存储和加载。updateBackgroundImage: 将通过辅助方法把下载的图片存储到磁盘中。

接下来找到deleteBackgroundImage: 方法,并用下面的代码替换:

- (IBAction)deleteBackgroundImage:(id)sender {
    NSString *path;
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
	path = [[paths objectAtIndex:0] stringByAppendingPathComponent:@"WeatherHTTPClientImages/"];
 
    NSError *error;
    [[NSFileManager defaultManager] removeItemAtPath:path error:&amp;error];
 
    NSString *desc = [self.weatherDictionary weatherDescription];
    [self start:desc];
}

这个方法将删除已经下载的图片,这样在测试程序的时候,你可以再次下载图片。

最后一次:生成并运行工程,下载天气数据,并点击某个cell,以打开详细天气画面。在详细天气画面中,点击Update Background 按钮. 如果你点击的是晴天cell,将会看到如下画面:


以上就是本文的第一部分内容,第二部分内容在这里:AFNetworking速成教程(2)

标签: , , , , ,

AFNetworking速成教程(2)

何去何从?

你可以在这里下载到完整的工程:here.

你所想到的所有方法,都可以使用AFNetworking来与外界通讯:

  • AFJSONOperation, AFPropertyListOperation 和 AFXMLOperation用来解析结构化数据。
  • UIImageView+AFNetworking用来快捷的填充image view。
  • AFHTTPClient用来进行更底层的请求。
  • 用自定义的AFHTTPClient子类来访问一个web service。
  • AFNetworkActivityIndicatorManager用来给用户做出网络访问的提示。
  • AFImageRequestOperation用来加载图片。

AFNetworking可以帮助你进行网络开发!

如果你有任何相关问题,请访问我们的网站以获得帮助。我也很乐意看到你的回复!

本文是由 iOS Tutorial 小组成员 Scott Sherwood撰写,他是一个动态加载(Dynamically Loaded)(基于位置)软件公司(专业的混合定位)的共同创办人。

23 March 2013

Unity3D for iOS初级教程:Part 3/3(上)

Learn how to use Unity to make a simple 3D iOS game!

Learn how to use Unity to make a simple 3D iOS game!

这份教程是由教程团队成员 Christine Abernathy, 他是Facebook的开发支持团队的工程师。
欢迎来到第三部分,这是Unity 3D for iOS初级系列教程的最后一个部分!
在这个系列的第一部分,你参观了基本的Unity工具,创建了带有一个简单角色控制机制的游戏,并且学习了如何部署你的项目到iOS上。

然后在这个系列的第二部分,你增强了你英勇的小方块的移动,并且给这个世界带来一些生命,它拥有了天空,草地和一个起伏的地形。

在第三部分,也是最后一部分,你将要添加游戏玩法到项目中。替代简单的绕着场景乱转,你英勇的小方块需要在一个给定的时间里,冲刺穿过一个终点线。
要给你的角色一些挑战,在方块以z字路线通向终点的路上,障碍物会像雨点一样落向方块。一个倒计时的时钟添加到剧本中。成功会伴随欢呼 – 失败则是震耳欲聋的沉默.:]
你也几乎接近终点线了,所以记住 – 我行我素!(“Huey Lewis And The News”中的一首歌名)
继续阅读!

标签: , , , , ,

22 March 2013

Unity3D for iOS初级教程:Part 3/3(下)

消息不会自动消除

你基本的游戏功能已经完成了,但是显示一些关于游戏的信息当它首次加载的时候是一个好的亮点。你现在所有显示的是一个play按钮。用户不知道他们要做啥。
添加一些欢迎的文字和关于游戏的超级简短的介绍会让它更加用户友好.:]欢迎的文字会使用你导入变形金刚字体。对于介绍文字。你会使用和Unity一起打包的Arial字体。 
创建一个空的游戏对象,命名它为Intro,然后设置 Transform Position为0,0,0. 
选择GameObjectCreate Other3D Text来创建一个3D文字GameObject,然后命名它为Description Text。设置InspectorText MeshText属性为"Get to the finish line before time runs out".设置初始的Transform Position为-10, 1, 12.

Description text adjustments.

用Unity编辑器试玩游戏,然后调整物件的位置让它水平居中,让你可以看的清楚一些。

提示:你会不得不与x和z位置打交道:x用于中间对齐物件,z用于放大和缩小。

<a href="http://www.raywenderlich.com/wp-content/uploads/2012/11/part3_39b_desc_text_adjusted.png"><img title="Description text position adjustments." src="http://www.raywenderlich.com/wp-content/uploads/2012/11/part3_39b_desc_text_adjusted-700x297.png" alt="Description text position adjustments." width="700" height="297" /></a>
用Unity Remote检查游戏,确保文字在iOS设备上显示的出来。对一些必要的调整。
将Description Text放到Intro GameObject下。你这样做的目的是你可以通过简单的代码在后面显示或隐藏显示菜单信息。

Description text parented.

创建第二个3D文字GameObject,然后命名它为Welcome Text。这个文字应该出现在描述文字的上面,所以设置初始的Transform Position为 -6,5,10.设置InspectorText MeshText属性为"Welcome".
通过拖动项目视图中的字体资源到Inspector中的Font属性来设置字体属性为Transformers Movie(或者通过点击Font边上带有小点的圆圈,然后选择字体从弹出的列表中):
<a href="http://www.raywenderlich.com/wp-content/uploads/2012/11/part3_42b_welcome_text_transform_text_font.png"><img title="Welcome text transform and material modifications." src="http://www.raywenderlich.com/wp-content/uploads/2012/11/part3_42b_welcome_text_transform_text_font-244x500.png" alt="Welcome text transform and material modifications." width="244" height="500" /></a>
调整欢迎文字的位置,当你在Unity编辑器和Unity Remote中测试游戏的时候,让你可以看的清楚。
<a href="http://www.raywenderlich.com/wp-content/uploads/2012/11/part3_43b_welcome_text_adjusted.png"><img title="Welcome text position adjustments." src="http://www.raywenderlich.com/wp-content/uploads/2012/11/part3_43b_welcome_text_adjusted-700x315.png" alt="Welcome text position adjustments." width="700" height="315" /></a>
将Welcome Text物件放到Intro GameObject下面。
当游戏开始运行的时候,你想要隐藏IntroGameObject(和它的子物件)。打开GameController脚本,然后做下面的修改:
var intro : Transform;
...
function startGame() {
...
    // Turn off the intro text
    for (var child : Transform in intro ) {
        child.gameObject.renderer.enabled = false;
    }
 
    // Clean out any enemy objects
...
这里你添加一个新的公共变量来得到Intro GameObject的句柄。然后你修改startGame()来让Intro GameObject不可见,通过关闭它的子GameObject的渲染。
现在通过选择主摄像头然后从Hierarchy视图中拖动Intro GameObject到InspectorGame ControllerIntro变量上来设置Intro变量。或者使用圆圈点图标,因为它更加容易.:]

Description text parented.

创建第二个3D文字GameObject,然后命名它为Welcome Text。这个文字应该出现在描述文字的上面,所以设置初始的Transform Position为 -6,5,10.设置InspectorText MeshText属性为”Welcome”.

通过拖动项目视图中的字体资源到Inspector中的Font属性来设置字体属性为Transformers Movie(或者通过点击Font边上带有小点的圆圈,然后选择字体从弹出的列表中):

Welcome text transform and material modifications.

调整欢迎文字的位置,当你在Unity编辑器和Unity Remote中测试游戏的时候,让你可以看的清楚。

Welcome text position adjustments.

将Welcome Text物件放到Intro GameObject下面。

当游戏开始运行的时候,你想要隐藏IntroGameObject(和它的子物件)。打开GameController脚本,然后做下面的修改:

var intro : Transform;
...
function startGame() {
...
    // Turn off the intro text
    for (var child : Transform in intro ) {
        child.gameObject.renderer.enabled = false;
    }

    // Clean out any enemy objects
...

这里你添加一个新的公共变量来得到Intro GameObject的句柄。然后你修改startGame()来让Intro GameObject不可见,通过关闭它的子GameObject的渲染。

现在通过选择主摄像头然后从Hierarchy视图中拖动Intro GameObject到InspectorGame ControllerIntro变量上来设置Intro变量。或者使用圆圈点图标,因为它更加容易.:]

Intro transform assigned to Game Controller script.

试玩游戏来测试当play按钮被点击,游戏开始后文字隐藏起来了。

每个勇敢的小方块都需要一个音轨。

音乐在游戏的体验中扮演一个重要的角色,及提供感官的反馈也创造情绪。你将要添加音频来增强游戏性。

声音效果会被触发,当玩家按时通过终点线的时候,或者他们的任务失败,或者障碍物撞击到地上,或者碰到了任何东西。当然,游戏也需要一些背景音乐!:]

用Unity添加音频涉及关联一个Audio Source组件到一个GameObject上。这个Audio Source组件有一个Audio Clip属性让你可以分配你想要播放的声音。这个组件有其他附加的属性来控制声音是可以播放,也可以循环。支持音频格式包括.AIF, .WAV, .MP3, and .OGG.

下面两个网站提供免费的音乐,可以被用在这份教程中:

你先前下载的Resources.zip文件包括了所有你会用到的音频文件了。你可以自由创建你自己的音效来替代那些我提供给你的. :]

作为你的参考(并且为了找到原始的链接),在Resources.zip文件中包含的音频文件的原始链接如下:

注意,在Resources.zip文件,为了清晰和简洁,文件已被重新命名。到你最初解压Resources.zip的文件夹下,然后通过拖动它们到你的Project ViewAssets文件夹下来导入音频文件。

Victory audio import settings.

当音频文件导入到Unity中,你可以指定是否压缩或保持现状,例如,自然的状态(但是注意MP3和Ogg Vorbis音频总是以压缩的格式导入的)

为什么会这样呢?压缩文件会更加小,但是它们需要在游戏运行的时候解压,会消耗CPU周期。你通常想要解压背景音乐。对于短的音效,自然状态会更好,并且可以提供更好的声音效果。

如果音频格式是压缩的,你可以选择是否通过硬件来处理解压,例如,Apple的硬件解码器,如果运行在iOS设备上。硬件会更快,但是硬件只能同时处理一个压缩的文件。

你也可以让声音有3D效果。意思是当播放声音的时候,效果会与GameObject的3D位置有相关性。举例来说,如果GameObject远离了你,声音会变得轻一些。

Background audio import settings.

在项目视图中选择背景音乐,显示导入的设置。不要勾选3D Sound选项。选择 Hardware decoding。点击Apply来保存改变的设置。

另外一些音频文件是.WAV 文件,你不需要改变缺省的导入设置,也就是应该设置为3D和自然的音频格式。

对于被听到的声音,你的场景需要一个Audio Listener组件添加到一个GameObject中。场景里只有一个Audio Listener。这个收听者会从最靠近它的声音源捡起声音,然后把它发送到设备的喇叭上。

缺省情况下,Audio Listener关联到主摄像头。你可以把他放在那里或者关联它到一个不同的GameObject:例如,你的角色上。

在这个游戏中,你保留Audio Listener在主摄像头上,但是你可以体会一下不同的选择当你构建你自己的游戏的时候。

胜利和失败的音效

你打算关联音频到Goal GameObject上来模拟一个在终点线的观众欢呼或嘲笑。

在Hierarchy视图中选中Goal GameObject,然后通过选择ComponentAudioAudio Source来添加一个音频源。设置victory audio资源到InspectorAudio SourceAudio Clip属性。不要勾选在Awake播放的选项。

Audio source added to Goal object.

现在创建一个新的JavaScript资源用作播放一个胜利的声音或失败的声音。命名这个新的脚本为FanReaction。打开这个新的脚本,移除自动添加的函数,然后添加下面的代码:

var audioVictory : AudioClip;
var audioDefeat : AudioClip;
var volumeVictory : float = 2.0;
var volumeDefeat : float = 2.0;

function playSoundOfVictory(isVictory : boolean) {
    // Stop any current audio
    if (audio.isPlaying)
        audio.Stop();

    // Play either the sound of victory or defeat.
    audio.clip = isVictory ? audioVictory : audioDefeat;
    audio.volume = isVictory ? volumeVictory : volumeDefeat;
    audio.Play();
}

function resetGame() {
    // Reset to original state, stop any audio
    if (audio.isPlaying)
        audio.Stop();
}

@script RequireComponent(AudioSource)

这个脚本处理两段音频,一个是为胜利准备的,另一个是为失败准备的。playSoundOfVictory函数首先停止播放任何当前播放的声音,然后根据isVictory输入,播放需要的声音。

resetGame()函数会停止播放正在播放的任何声音。你会想起GameController在游戏每次重新开始的时候调用resetGame()。

关联这个新的脚本到Goal GameObject上。设置 victory audio资源到Audio Victory变量。设置defeat audio资源到Audio Defeat变量。

Fan Script added to Goal object.

编辑GameController脚本,并做下面这些修改:

var fanReactionScript : FanReaction;
...
function Update() { 
    if (!gameRunning)
        return; 

    // Keep track of time and display a countdown
    gameTimeRemaining -= Time.deltaTime;
    if (gameTimeRemaining <= 0) {
        timedOut = true; 
        gameRunning = false;

        // Play the sound of defeat
        fanReactionScript.playSoundOfVictory(false);
    }
}
...
function MissionComplete() { 
    if (!gameRunning)
        return;

    missionCompleted = true; 
    gameRunning = false;

    // Play the sound of victory
    fanReactionScript.playSoundOfVictory(true);

    missionCompleteTime =  gameTimeAllowed - gameTimeRemaining;
}

代码定义了一个新的公共变量来引用FanReaction脚本。你修改MissionComplete()来调用playSoundOfVictory,传递true来播放胜利的声音。你也要修改Update()来调用playSoundOfVictory(),传递false来播放失败的声音。

在主摄像头的GameController脚本组件的Goal GameObject带有一个变量来连接FanReaction脚本。在Inspector中选择主摄像头,然后点击GameController组件下面的fanReactionScript变量边上的圆圈点。在弹出的对话框下,选择Goal GameObject,然后关闭弹出的对话框。

Fan Reaction script assigned to Game Controller script.

要调用FanReaction中的resetGame(),选择主摄像头物件。在Inspector的Game Controller组件栏目,增加Game Object到Reset数组,从2到3.设置Goal GameObejct到Element 2.

Goal added to Game Objects To Reset array.

试玩游戏,并且测试胜利和失败的场景,来确认游戏播放了正确的声音。检查声音是否停止,当你再次点击了Play按钮。

在3D环境下的轰的一声

当障碍物撞击到地面的时候,有某种声音也是不错的。要实现这点,你会关联一个音频源到Obstacle预制件,然后当障碍物落下或碰撞到任何其他东西的时候,你可以通过监测碰撞来播放碰撞的声音。

添加一个音频员组件到Obstacle预制件。分配impact audio到Audio Clip属性。

Impact audio added to Obstacle Prefab.

创建一个新的JavaScript脚本资源,然后命名它为ObjectCollision.编辑这个脚本,删除预先添加的函数,然后添加下面这些代码:

var impact : AudioClip;
function OnCollisionEnter () {
    audio.PlayOneShot(impact);
}

@script RequireComponent(AudioSource)

这段代码实现了预先定义的OnCollisionEnter事件函数,它调用audio.PlayOneShot()函数来播放撞击的音频。audio.PlayOneShot()展示了播放音频的另一种方式,允许你传递你想要播放的音频。

关联脚本到Obstacle预制件上。设置impact audio资源到脚本中的Impact变量。

Object Collision script and audio variable added.

试玩游戏,并检查当障碍物撞击地面或其他物件时你是否听到一个可喜的砰的一声。注意离障碍物玩家越近,声音越大。

小方块的音乐

你的游戏几乎完成了。但是那里还有一件事漏掉了 – 一些背景音乐。

对于游戏音乐制造了很多气氛。它可以让用户的肾上腺素流动,并且通过如鸟的声音或狼的嚎叫,帮助他们提供游戏环境的线索。所以添加一些音乐吧!

添加一个Audio Source组件到主摄像头。设置background audio资源到Audio Clip属性。

勾选Play on Awake和Loop属性。这些确保当游戏开始后背景音乐就开始播放,并会持续的播放。

调整volume从0.1直到它没有淹没了其他声音。如果你使用你自己的声音,根据你音乐缺省的音量级别微调音量级别来完成同样的目标。用户应该可以通道所有音效当背景音乐播放的时候。

Background audio source added.

在Unity编辑器里试玩这个项目。当完全满意后,部署这个项目到你的iOS设备上。你需要添加Level_3场景在 Build Settings中。

Build settings when adding final level.

在你的iOS设备上测试整个游戏,直到你满意你添加的声音。

Completed game running on iOS.

你英勇的小方块有了背景音乐来鼓舞它的士气。

向哪里进发?

恭喜,你已经到达到了Unity基础旋风演戏的结尾!你已经展示了你英勇小方块的活力和生机。这或许是Unity游戏精彩之旅的开始。

这里是这个系列教程的所有代码的源文件: Unity ProjectXcode Project.

相信或不相信,你只是知道了一些皮毛而已 – 那里还有很多东西要学。敬请关注马上就会来的中级教程系列,那带你到Unity一个下的级别。

在此期间,记得要通过论坛提出你的问题和反馈,来构建充满乐趣的精彩游戏吧!

这份博文是由教程的团队成员 Christine Abernathy,来自Facebook的开发支持团队的工程师。

21 March 2013

Unity3D for iOS初级教程:Part 1/3(上)

Learn how to use Unity to make a simple 3D iOS game!

Learn how to use Unity to make a simple 3D iOS game!

这篇教材是来自教程团队成员 Christine Abernathy, 他是Facebook的开发支持团队的工程师。
Unity是最为流行的游戏引擎之一。这是有充分缘由的:Unity有一个让它非常容易上手的强大的可视化编辑器,功能强大,好的社区支持,有吸引力的价格,等等!
如果你渴望尝试Unity,没有比这篇教程更适合你的了!在这篇教程中,你会学习如何用Unity创建一个简单的iOS游戏 – 并且之前不需要有任何经验!:]
在这篇教程中,你要创建的这个简单游戏是让玩家在限定的时间内,绕过沿途的障碍物顺利达到终点。
在过程中,会向你介绍Unity的开发环境和工作流程,并且学习游戏设计的基本概念。
这篇份教程将会分为三个部分:
在第一部分中,通过创建一个游戏,这个游戏具有一个带有控制机制的简单角色,通过这个游戏使你学会使用Unity的基本操作。你也会学习如何将的项目部署到iOS平台上。
在第二部分中,你会掌握如何使用内建的控制器对象和标准库中的脚本来增强角色的移动;如何用Unity Remote来调试;如何美化游戏的场景。
在第三部分中,你将会添加游戏规则到你的项目中,并且建立起游戏设计的概念,这些概念包括添加预制件,计时器,菜单和音效来增强游戏的体验。

准备好了添加一个强大的新框架到你的技能栏里了吗?请继续阅读!:]
继续阅读!

标签: , , , , ,

20 March 2013

Unity3D for iOS初级教程:Part 2/3

Learn how to use Unity to make a simple 3D iOS game!

Learn how to use Unity to make a simple 3D iOS game!

这篇教材是来自教程团队成员 Christine Abernathy, 他是Facebook的开发支持团队的工程师。
欢迎来到Unity3D for iOS系列教程的第二部分!
在这个系列的第一部分中,你已经学习了Unity的一些基本技能来构建一个非常简单的项目,并把它部署到iOS设备上。在继续这篇教程之前,你要确认已阅读过第一部分。
现在进入第二部分,你要添加一些功能来增强我们的项目,这些功能包括更好的角色移动,和更好的游戏背景。你也会学习如何使用Unity Remote来调试。
游戏再次开启!
继续阅读!

标签: , , , , ,

13 March 2013

iOS应用崩溃日志揭秘

Learn how to make sense of crash logs!

Learn how to make sense of crash logs!

本文作者是 Soheil Moayedi Azarpour, 他是一名独立iOS开发者。


作为一名应用开发者,你是否有过如下经历?
为确保你的应用正确无误,在将其提交到应用商店之前,你必定进行了大量的测试工作。它在你的设备上也运行得很好,但是,上了应用商店后,还是有用户抱怨会闪退 !
如果你跟我一样是个完美主义者,你肯定想将应用做到尽善尽美。于是你打开代码准备修复闪退的问题……但是,从何处着手呢?
这时iOS崩溃日志派上用场了。在大多数情况下,你能从中了解到关于闪退的详尽、有用的信息。
通过本教程,你将学习到一些常见的崩溃日志案例,以及如何从开发设备和iTunes Connect上获取崩溃日志文件。你还将学习到符号化( symbolication),从日志追踪到代码 。你还将学习调试一个在待定情况下会闪退的应用。
让我们开始动手吧! 继续阅读!

标签: , , ,

9 February 2013

iOS 5 Turn-Based游戏入门第2部分—1

Learn how to make a turn-based game with Game Center in iOS 5!

Learn how to make a turn-based game with Game Center in iOS 5!

来自Ray的注释 这是iOS 5 Feast的第六篇!同时是我们的新书iOS 5 By Tutorials中相应章节的免费预览版。希望你喜欢!

本篇教程是由iOS教程组成员Jacob Gundersen发布的,Jacob是一位独立游戏开发者,经营着Indie Ambitions。请看看他最新的app Factor Samurai!

这是“如何使用iOS5 Game Center新API制作一款turn-based游戏”系列教程的第2部分。

上一篇中,我们完成了基础部分,包括配置app并创建了一个turn-based比赛。

在第2部分也是最后一部分中,我们将要完成剩下的有趣的部分。其中包括允许玩家完成他们的回合、为app加入更多的细节并最终完善此游戏!

让我们马上进入教程一起来完成这个游戏吧! :]

继续阅读!

标签: , , , , ,

iOS 5 Turn-Based游戏入门第2部分—2

完成Matchmaker View Controller代理方法

我们已经完成了didFindMatch方法。但是,我们仍然需要做一些工作来完善其他代理方法。

didCancel方法只需要收起view controller,不需要对其修改。error方法也不需要修改,虽然更好的实现方式是针对每一种错误进行不同的处理,但我们的目的很明确,所以只打印出错误即可。

确实需要修改的是playerDidQuit方法,所以在GCTurnBasedMatchHelper.m中对其如下更新:

 
-(void)turnBasedMatchmakerViewController: 
  (GKTurnBasedMatchmakerViewController *)viewController 
  playerQuitForMatch:(GKTurnBasedMatch *)match {
    NSUInteger currentIndex = 
      [match.participants indexOfObject:match.currentParticipant];
    GKTurnBasedParticipant *part;
 
    for (int i = 0; i < [match.participants count]; i++) {
        part = [match.participants objectAtIndex:
          (currentIndex + 1 + i) % match.participants.count];
        if (part.matchOutcome != GKTurnBasedMatchOutcomeQuit) {
            break;
        } 
    }
    NSLog(@"playerquitforMatch, %@, %@", 
      match, match.currentParticipant);
    [match participantQuitInTurnWithOutcome:
      GKTurnBasedMatchOutcomeQuit nextParticipant:part 
      matchData:match.matchData completionHandler:nil];
}

如果玩家当前拿着指挥棒而且退出了比赛,那么比赛就会卡住,因为只有当前玩家可以提交回合给下一个玩家,而当前玩家退出了游戏。所以当玩家在本回合时退出游戏,我们需要针对此种情况将指挥棒从他们的手中取回,这就是以上方法所做的。

此方法当我们在本回合并从view controller中退出时被调用。如果不是本回合并退出,那么另一个方法playerQuitOutOfTurn会被调用,所有的方法调用都是自动的。

在此例中,我们遍历了participants数组并找出那个matchOutcome不为GKTurnBasedMatchOutcomeQuit的participant。我们不会将比赛传递给已经退出的玩家。如果我们尝试将比赛传递给已经退出的玩家,那么将会得到一个错误,同时此回合也不会被记录。

当我们从数组中找到了下一个没有退出游戏的participant后,就调用participantQuitInMatchWithOutcome:nextParticipant:matchData:completionHandler:方法,此方法会为已退出玩家赋予一个outcome,并将比赛传递给下一个玩家,然后结束回合。

在本游戏中,我们不需要对matchData做任何修改,传递它即可。在其他场景中,游戏可能需要在数据被传递前对游戏状态做一些改变。

当我们修正此问题时,记得对sendTurn方法也做相同的改动。具体到游戏中,我们想遍历participants并确保我们将要传递给的那个玩家没有退出游戏。事实上,如果你现在就编译并运行,开始一个三人的比赛(或是其中一个玩家退出的两人游戏),玩儿一段时间,让其中一个玩家退出,然后尝试将比赛传递给那个退出的玩家,你将会看到如下内容:

Current turn isn't as expected error

所以如下修改ViewController.m中的sendTurn方法:

 
- (IBAction)sendTurn:(id)sender {
    GKTurnBasedMatch *currentMatch = 
      [[GCTurnBasedMatchHelper sharedInstance] currentMatch];
    NSString *newStoryString;
    if ([textInputField.text length] > 250) {
        newStoryString = [textInputField.text substringToIndex:249];
    } else {
        newStoryString = textInputField.text;
    }
    NSString *sendString = [NSString stringWithFormat:@"%@ %@", 
      mainTextController.text, newStoryString];
    NSData *data = [sendString 
      dataUsingEncoding:NSUTF8StringEncoding ];
    mainTextController.text = sendString;
 
    NSUInteger currentIndex = [currentMatch.participants 
      indexOfObject:currentMatch.currentParticipant];
    GKTurnBasedParticipant *nextParticipant;
 
    NSUInteger nextIndex = (currentIndex + 1) % 
      [currentMatch.participants count];
    nextParticipant = 
      [currentMatch.participants objectAtIndex:nextIndex];
 
    for (int i = 0; i < [currentMatch.participants count]; i++) {
        nextParticipant = [currentMatch.participants 
          objectAtIndex:((currentIndex + 1 + i) % 
          [currentMatch.participants count ])];
        if (nextParticipant.matchOutcome != 
            GKTurnBasedMatchOutcomeQuit) {
            break;
        } 
    }
 
    [currentMatch endTurnWithNextParticipant:nextParticipant 
      matchData:data completionHandler:^(NSError *error) {
        if (error) {
            NSLog(@"%@", error);
            statusLabel.text = 
              @"Oops, there was a problem.  Try that again.";
        } else {
            statusLabel.text = @"Your turn is over.";
            textInputField.enabled = NO;
        }
    }];
    NSLog(@"Send Turn, %@, %@", data, nextParticipant);
    textInputField.text = @"";
    characterCountLabel.text = @"250";
}

首先我们添加遍历所有participants的代码,找到第一个没有退出的玩家(通常来说一般是遍历中的第一个)则退出循环。这样一来就可以跳过所有已经退出游戏的参与者。

这里添加的其他内容是两部分没关联的代码。首先,遇到错误或者没遇到错误时更新状态。其次,如果回合发送过程没有问题,我们就禁用textInputField,这样就不会立刻发送另一个回合给游戏了。

如果你现在编译并运行,会发现在发送回合后状态会相应更新!

Updating the status label when the turn is complete

事件处理代理

我们的游戏到目前为止还不错,但还有一个重要的问题没有解决 —— 当其他玩家开始回合时,我们并没有更新状态!每次都从Game Center UI中查看状态会让人非常不爽。

随着你对此游戏的逐步了解,你也许已经注意到了我们时不时地会收到游戏中的横幅或是系统的notification提醒。这些工作是由GKTurnBasedEventHandler对象完成的。此对象会在不同事件发生时发送notification并在我们的app中横幅提醒,比如当到了我们玩家的回合时。

当某些通知进入时,有三个我们需要处理的代理回调方法。一个方法在游戏中开始一个邀请时被调用,一个在回合前进时(即便不是我们的回合,也会在每次回合改变时被调用),还有一个当游戏结束时被调用。

为了接收并处理这些事件,我们首先需要让自己成为GKTurnBasedEventHandler的代理。此对象是一个单例,我们只会在设置自己为它的代理时才会接触到它。

正如view controller代理协议一样,我们将要使用GCTurnBasedMatchHelper来充当所有消息的媒介。所以,这就是我们需要的设置代理的步骤。我们需要在登录Game Center成功后设置代理,否则它将不起作用。如下修改authenticateUser方法:

- (void)authenticateLocalUser { 
 
    if (!gameCenterAvailable) return;
 
    void (^setGKEventHandlerDelegate)(NSError *) = ^ (NSError *error)
    {
        GKTurnBasedEventHandler *ev = 
          [GKTurnBasedEventHandler sharedTurnBasedEventHandler];
        ev.delegate = self;
    };
 
    NSLog(@"Authenticating local user...");
    if ([GKLocalPlayer localPlayer].authenticated == NO) {     
        [[GKLocalPlayer localPlayer] 
         authenticateWithCompletionHandler:
          setGKEventHandlerDelegate];        
    } else {
        NSLog(@"Already authenticated!");
        setGKEventHandlerDelegate(nil);
    }
}

这里我们设置了一个block(这样就可以将其传递给authenticate方法中的completionHandler)。此block仅仅取得单例的指针并将此指针的delegate设置为self。很简单。

然后,我们将其作为验证方法中completionHandler的参数。如果我们不需要验证那么就会调用此block。

此block如果被completionHandler执行,那么它会包含一个NSError类型的参数。如果我们直接调用它,则error输入nil即可(因为此error有可能是authenticate方法中任何问题导致的,但我们并没有运行它)。

很好,我们已经通过设置GKTurnBasedEventHandlerDelegate协议而准备好接收回调了。我们还需要设置我们的对象为GKTurnBasedEventHandlerDelegate并实现此方法。这就实现它:

在GCTurnBasedMatchHelper.h中,修改@interface行为:

@interface GCTurnBasedMatchHelper : NSObject 
  <GKTurnBasedMatchmakerViewControllerDelegate, 
  GKTurnBasedEventHandlerDelegate> {

并添加如下代码到GCTurnBasedMatchHelper.m:

#pragma mark GKTurnBasedEventHandlerDelegate
 
-(void)handleInviteFromGameCenter:(NSArray *)playersToInvite {
    NSLog(@"new invite");
}
 
-(void)handleTurnEventForMatch:(GKTurnBasedMatch *)match {
    NSLog(@"Turn has happened");
}
 
-(void)handleMatchEnded:(GKTurnBasedMatch *)match {
    NSLog(@"Game has ended");
}

编译并运行。你可以通过让其他玩家开始回合来测试handleTurn事件,你将能够看到log信息。哇奥!

测试handleMatchEnded需要我们先实现一个结束游戏的方法。handleInviteFromGameCenter只在从game center app中启动游戏时被调用。

如果你从game center中发送了一个邀请,此回调需要初始化一个新的GKMatchRequest,然后自定义实现或是使用view controller(GKTurnBasedMatchmakerViewController)来设置新的比赛。app内发送的邀请不会调用此方法。

以下是handleTurn事件的输出:

Console output demonstrating handleTurn is called

处理邀请

接下来完成handleInviteFromGameCenter。你可能和我曾经一样错误地假设过此方法是在接收到一个进入游戏的邀请后触发的。其实,此方法与此毫无关联!

实际上,并没有方法会接收并处理上述消息,一个从游戏内发送的邀请只会出现在可用比赛列表中。而上述方法处理的是,当你为比赛创建一个向你其中一个好友的邀请时收入的数据。所以,当你在game center中切换游戏时,在回调(playersToInvite)中包含有你想邀请谁加入新游戏的信息。此回调是在发出邀请的玩家端被调用,而不是被邀请者。我详细解释了此错误,因为我并不是唯一对此感到困惑的人。

如果我们从game center收到了一个新的邀请,我们需要用一个GKMatchRequest对象初始化GKTurnBasedMatchmakerViewController。此方法会提供给我们将要出现比赛中的玩家的数组。我们将使用次对象来设置GKMatchRequest。以下是代码例子:

-(void)handleInviteFromGameCenter:(NSArray *)playersToInvite {
    [presentingViewController 
      dismissModalViewControllerAnimated:YES];
    GKMatchRequest *request = 
      [[[GKMatchRequest alloc] init] autorelease]; 
    request.playersToInvite = playersToInvite;
    request.maxPlayers = 12;
    request.minPlayers = 2;
    GKTurnBasedMatchmakerViewController *viewController =
      [[GKTurnBasedMatchmakerViewController alloc] 
        initWithMatchRequest:request];
    viewController.showExistingMatches = NO;
    viewController.turnBasedMatchmakerDelegate = self;
    [presentingViewController 
      presentModalViewController:viewController animated:YES];
}

首先,我们收起任何的在屏幕上的modal view controller。然后设置GKMatchRequest和GKTurnBasedMatchmakerViewController。注意showExistingMatches被设置为NO。我们只关心比比赛的新游戏界面。最后,我们设置代理并将此view controller弹出到屏幕上。

编译并运行,尝试从Game Center中开始一个新比赛(如果是在游戏内部发起的邀请,将不会调用此方法)并发送邀请。有一点需要注意的是,模拟器不会接收到任何与GKTurnBasedEventHandlerDelegate有关的通知,所以你不得不在设备上测试。

Inviting friends in Game Center

处理回合事件

handleTurn被调用有很多种情况,可能是比赛从一个玩家移动到了另一个,而并不是我们的回合,也可能是回合移动到了我们的玩家。另外,当前游戏状态下被加载进来的比赛,并不一定是收到handleTurn回调的比赛。我们需要针对这些情形进行区分并分别处理它们。

以下是代码:

-(void)handleTurnEventForMatch:(GKTurnBasedMatch *)match {
    NSLog(@"Turn has happened");
    if ([match.matchID isEqualToString:currentMatch.matchID]) {
        if ([match.currentParticipant.playerID 
          isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
            // it's the current match and it's our turn now
            self.currentMatch = match;
            [delegate takeTurn:match];
        } else {
            // it's the current match, but it's someone else's turn
            self.currentMatch = match;
            [delegate layoutMatch:match];
        }
    } else {
        if ([match.currentParticipant.playerID 
          isEqualToString:[GKLocalPlayer localPlayer].playerID]) {
            // it's not the current match and it's our turn now
            [delegate sendNotice:@"It's your turn for another match" 
              forMatch:match];
        } else {
            // it's the not current match, and it's someone else's 
            // turn
        }
    }
}

我们处理了4种情况。对其中的3个发送代理方法,第4个我们会忽略掉,但是此处留空,以备你在其他游戏中使用。

前两种情况的条件是当前比赛和我们加入的比赛相同。此时,如果当前是我们的回合,我们将会发送takeTurn代理方法,如果不是,就发送layoutMatch代理方法。我们仍旧将currentMatch属性设置为传入的match变量,因为即使matchID是相同的,新传入的match也会有些不同的状态更新数据(match.matchData),而且currentParticipant也已改变。

如果当前比赛不是我们加入的比赛,而正好轮到我们的回合时,我们则发送sendNotice代理方法。我们将使用此方法来显示一个提示框以警示用户。

如果当前比赛不是我们加入的比赛,而也没有轮到我们的回合时,我们就不做任何事情。玩家可以通过GKTurnBasedMatchmakerViewController来加载比赛并观察它们,我们不需要当每场比赛状态变化时都打断玩家。对其他类型游戏来说,我们可能需要处理此种情况,所以此处留着以后扩展。

handleTurn方法已经就绪,你现在就可以运行游戏试试看了。每当一个新回合被发送,你都会看到你的游戏界面的更新,而且statusLabel会告知你现在是谁的回合!

Status label showing the current player's turn

根据你的游戏状态的不同,你也许会得到一个提示sendNotice回调没实现的错误。这是我们的下一步工作,我们这就来实现ViewController.m中的sendNotice方法。

-(void)sendNotice:(NSString *)notice forMatch:
  (GKTurnBasedMatch *)match {
    UIAlertView *av = [[UIAlertView alloc] initWithTitle:
      @"Another game needs your attention!" message:notice 
      delegate:self cancelButtonTitle:@"Sweet!" 
      otherButtonTitles:nil];
    [av show];
    [av release];
}

以上内容很直观。我们只是让玩家知道其他比赛需要它们的关注。当玩家准备好后,我们使用GKTurnBasedMatchmakerViewController来加载比赛。你如果参与了多个游戏,那么就有可能收到此提示:

Showing an alert view when another game is ready

结束游戏

我们真的只差一步就完成了,这就是结束游戏。但是,我们可能还需要提供给玩家一些高级消息来提示玩家还有多少个剩余回合。

我们通过检查NSData的长度来实现一个新方法。当字符数达到3800个时我们就结束游戏,但是我们首先需要让玩家知道什么时候游戏会达到3000个字符。

此新方法在每次比赛被加载时被调用,所以我们会将其放置到takeTurn和layoutMatch方法中。如果比赛快结束了,此方法会对statusLabel添加更多的信息,比如,还差200字符就满了。

在ViewController.m中的dealloc方法后添加以下方法:

-(void)checkForEnding:(NSData *)matchData {
    if ([matchData length] > 3000) {
        statusLabel.text = [NSString stringWithFormat:
          @"%@, only about %d letter left", statusLabel.text, 
            4000 - [matchData length]];
    }
}

然后在layoutMatch和takeTurn方法的末端添加以下内容:

[self checkForEnding:match.matchData];

编译并运行,你将会得到类似以下截图中的内容(需要你写很长的一段内容)。

Ending game based on character count

这里我们做的是当字符数超过3800个后就结束游戏。我们不想让NSData超过4096字节的原因是,如果matchData太长,Game Center就不会结束回合。

所以,最后一次修改sendTurn方法,如下所示:

- (IBAction)sendTurn:(id)sender {
    GKTurnBasedMatch *currentMatch = 
      [[GCTurnBasedMatchHelper sharedInstance] currentMatch];
    NSString *newStoryString;
    if ([textInputField.text length] > 250) {
        newStoryString = [textInputField.text substringToIndex:249];
    } else {
        newStoryString = textInputField.text;
    }
 
    NSString *sendString = [NSString stringWithFormat:@"%@ %@", 
      mainTextController.text, newStoryString];
    NSData *data = [sendString dataUsingEncoding:NSUTF8StringEncoding ];
    mainTextController.text = sendString;
 
    NSUInteger currentIndex = [currentMatch.participants 
      indexOfObject:currentMatch.currentParticipant];
    GKTurnBasedParticipant *nextParticipant;
 
    NSUInteger nextIndex = (currentIndex + 1) % 
      [currentMatch.participants count];
    nextParticipant = [currentMatch.participants objectAtIndex:nextIndex];
 
    for (int i = 0; i < [currentMatch.participants count]; i++) {
        nextParticipant = [currentMatch.participants 
          objectAtIndex:((currentIndex + 1 + i) % 
            [currentMatch.participants count ])];
        if (nextParticipant.matchOutcome != GKTurnBasedMatchOutcomeQuit) {
            NSLog(@"isnt' quit %@", nextParticipant);
            break;
        } else {
            NSLog(@"nex part %@", nextParticipant);
        }
    }
 
    if ([data length] > 3800) {
        for (GKTurnBasedParticipant *part in currentMatch.participants) {
            part.matchOutcome = GKTurnBasedMatchOutcomeTied;
        }
        [currentMatch endMatchInTurnWithMatchData:data 
          completionHandler:^(NSError *error) {
            if (error) {
                NSLog(@"%@", error);
            }
        }];
        statusLabel.text = @"Game has ended";
    } else {
 
        [currentMatch endTurnWithNextParticipant:nextParticipant 
          matchData:data completionHandler:^(NSError *error) {
            if (error) {
                NSLog(@"%@", error);
                statusLabel.text = 
                   @"Oops, there was a problem.  Try that again.";
            } else {
                statusLabel.text = @"Your turn is over.";
                textInputField.enabled = NO;
            }
        }];
    }
    NSLog(@"Send Turn, %@, %@", data, nextParticipant);
    textInputField.text = @"";
    characterCountLabel.text = @"250";
    characterCountLabel.textColor = [UIColor blackColor];
}

我们只是将当前的endTurn方法调用包含在一个if语句中。如果我们已经多余3800个字符,就会取代endTurn的调用,而调用endMatch。一旦我们结束了游戏,notification通知会通过handleMatchEnded方法传递给游戏中的其他玩家。

我们会立刻处理它,但首先测试一些结束游戏。确保handleEndMatch方法中还有log语句,然后在持续玩儿游戏以达到结束游戏的条件(达成3000个字符真的是很费时间呢)。

一旦达成,你会得到如下结果:

A game that has ended

很好,马上就大功告成了。现在我们需要回过头来处理GCTurnBasedMatchHelper.m中的handleMatchEnded方法:

-(void)handleMatchEnded:(GKTurnBasedMatch *)match {
    NSLog(@"Game has ended");
    if ([match.matchID isEqualToString:currentMatch.matchID]) {
        [delegate recieveEndGame:match];
    } else {
        [delegate sendNotice:@"Another Game Ended!" forMatch:match];
    }
}

如果不是我们当前的比赛我们仅仅发送一个通知,这会弹出一个UIAlert提示框,否则我们就发送receiveEndGame方法。同样也在ViewController.m中实现它:

-(void)recieveEndGame:(GKTurnBasedMatch *)match {
    [self layoutMatch:match];
}

在此可以做更多的事儿,但因为我们在layoutMatch会设置statusLabel,所以这样做就足够了。但是有些更好的方式来处理游戏结束的事件,比如通过email将我们的故事发送出去,或是邀请同样的玩家再次进行游戏,我们可以在此处处理它们。

以下是当设备处于锁定状态时接收到游戏结束消息的样子:

A system notice when a turn based game ends

何去何从?

这里是本篇教程系列完整的示例工程

Turn-Based Gaming API是对策略,棋牌,桌面,和其他异步玩法的多人游戏来说,都是很棒的工具,我期待着你使用它制作的作品!

想学习更多有关Game Center API的内容,可以参考我们的新书iOS 5 By Tutorials。在此书中我们有额外的一章来讲述Game Center,其中包含了如何自定义内置的乏味的Turn-Based游戏界面!这不仅仅只是表面功夫,它还增加了用户体验,因为你可以在此显示更多有关游戏和参与玩家的信息。

如果你对iOS 5 turn-based游戏有任何问题或评论的话,欢迎来本篇教程对应的论坛板块儿来参与讨论!

本篇教程是由iOS教程组成员Jacob Gundersen发布的,Jacob是一位独立游戏开发者,经营着Indie Ambitions。请看看他最新的app Factor Samurai!

8 February 2013

iOS 5 Turn-Based游戏入门第1部分

Learn how to make a turn-based game with Game Center in iOS 5!

Learn how to make a turn-based game with Game Center in iOS 5!

更新 2012/10/24: 如果你想要本教程的iOS 6和Xcode 4.5的更新版本,请参考iOS 5 by Tutorials Second Edition

来自Ray的注释 这是iOS 5 Feast的第六篇!同时是我们的新书iOS 5 By Tutorials中相应章节的免费预览版。希望你喜欢!

本篇教程是由iOS教程组成员Jacob Gundersen发布的,Jacob是一位独立游戏开发者,经营着Indie Ambitions。请看看他最新的app Factor Samurai!

在iOS 5中,Game Center引入了全新的API,可以用来制作一种类型的游戏:turn-based games(回合制游戏)!

这些新API非常适合棋牌类游戏,战棋类游戏,猜字游戏,以及其他常见的回合制游戏。在游戏中,你可以在你的回合中行动,或者等待你的朋友行动,然后当你的回合到来时收到提醒。

在本篇教程中我们会制作一个简单的基于UIKit的游戏“Spinning Yarn”,在这个游戏里,你可以和你的朋友们一起编写一个故事。

这是一个两部分的教程,在第一部分中,我们要做一些基础工作并实现自动匹配游戏。在第二部分中,我们要完成回合并实现简单的回合制游戏!

注意,为了达到本教程最好的效果,你需要有两台不同的设备和两个不同的Game Center沙盒帐号。我知道这对于测试来说比较麻烦,但是对于Game Center开发来说,这确实是个不幸的事实。

现在轮到你的回合了:继续阅读来制作一个很cool的turn-based游戏吧!

继续阅读!

标签: , , , , , ,