TextBlock是用于显示文本信息的对象。

通过设置TexkBlock.text属性来显示文本信息,这也是唯一的一个方法。因为TexkBlock继承自GraphObject,所以一些GraphObject的属性也有可能对文本有影响。

字体和颜色

可以通过TexkBlock.font属性设置文本的字体,该属性的值可以使用CSS来设置。

可以通过TextBlock.stroke属性设置文本字体的颜色,同样可以使用CSS来设置。

因为TexkBlock继承自GraphObject,所以GraphObject.background属性也可以作用于TextBlock,可以通过该属性设置文本背景色。

diagram.add(
$(go.Part, "Vertical",
  $(go.TextBlock, { text: "a Text Block" }),
  $(go.TextBlock, { text: "a Text Block", stroke: "red" }),
  $(go.TextBlock, { text: "a Text Block", background: "lightblue" }),
  $(go.TextBlock, { text: "a Text Block", font: "bold 14pt serif" })
));

结果:
textblock

尺寸和裁剪

TexkBlock的自然尺寸是会自适应设置文本的字体以及文本长度的。但是实际上它的尺寸是可大可小的。

下面的例子中首先展示了自然尺寸的TextBlock,然后对其进行明确的尺寸设置,并给与绿色背景:

diagram.add(
$(go.Part, "Vertical",
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2 }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 100, height: 33 }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 60, height: 33 }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 50, height: 22 }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 40, height: 9 })
));

结果:
textblock

文本自适应

TextBlock也可以使文本信息在规定的尺寸内自动换行,以达到自适应尺寸。可以通过TextBlock.wrap属性来设置,该属性不能为空,必须对其进行属性设置。

下面的例子中,第一个使用自然尺寸,第二个规定了宽度,第三第四个在规定相同宽度的基础上设置了TextBlock.wrap属性:

diagram.add(
$(go.Part, "Vertical",
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2 }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 50, wrap: go.TextBlock.None }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 50, wrap: go.TextBlock.WrapDesiredSize }),
  $(go.TextBlock, { text: "a Text Block", background: "lightgreen", margin: 2,
                    width: 50, wrap: go.TextBlock.WrapFit })
));

结果:
textblock

文本对齐

使用TextBlock.textAlign属性可以设置文本的对齐方式。

这里注意TextBlock.textAlignGraphObject.alignment是不同的,前者是针对文本的对齐方式,后者是针对所在容器的对齐方式:

diagram.add(
$(go.Part, "Horizontal",
  $(go.Panel, "Vertical",
    { width: 150, defaultStretch: go.GraphObject.Horizontal },
    $(go.TextBlock, { text: "textAlign: 'left'", background: "lightgreen", margin: 2,
                      textAlign: "left" }),
    $(go.TextBlock, { text: "textAlign: 'center'", background: "lightgreen", margin: 2,
                      textAlign: "center" }),
    $(go.TextBlock, { text: "textAlign: 'right'", background: "lightgreen", margin: 2,
                      textAlign: "right" })
  ),
  $(go.Panel, "Vertical",
    { width: 150, defaultStretch: go.GraphObject.None },
    $(go.TextBlock, { text: "alignment: Left", background: "lightgreen", margin: 2,
                      alignment: go.Spot.Left }),
    $(go.TextBlock, { text: "alignment: Center", background: "lightgreen", margin: 2,
                      alignment: go.Spot.Center }),
    $(go.TextBlock, { text: "alignment: Right", background: "lightgreen", margin: 2,
                      alignment: go.Spot.Right })
  )
));

结果:
textblock

对齐方式、换行、自适应

TextBlock.textAlign不管在自然尺寸中处理多行还是在规定尺寸中处理多行都很好用。

TextBlock.isMultiline属性用于设置是否开启内嵌文本中的换行符作用。

TextBlock.wrap属性在处理换行时就更加游刃有余,它会根据TexkBlock的尺寸自动对文本进行换行。

diagram.add(
$(go.Part, "Vertical",
  $(go.TextBlock, { text: "a Text Block\nwith three logical lines\nof text",
                    background: "lightgreen", margin: 2,
                    isMultiline: false }),
  $(go.TextBlock, { text: "a Text Block\nwith three logical lines\nof text",
                    background: "lightgreen", margin: 2,
                    isMultiline: true }),
  $(go.TextBlock, { text: "a Text Block\nwith three logical lines\nof centered text",
                    background: "lightgreen", margin: 2,
                    isMultiline: true, textAlign: "center" }),
  $(go.TextBlock, { text: "a single line of centered text that should wrap because we will limit the width",
                    background: "lightgreen", margin: 2, width: 80,
                    wrap: go.TextBlock.WrapFit, textAlign: "center" })
));

结果:
textblock

编辑文本

GOJS也提供了对TextBlock文本的编辑功能,只需要设置TextBlock.editabletrue即可。
如果你想对TextBlock中的文本进行某种规则的验证,那么可以设置TextBlock.textValidation属性,该属性的值为function,你可以自行编写验证规则。你甚至可以更换文本编辑器,设置TextBlock.textEditor属性即可。

diagram.add(
$(go.Part,
  $(go.TextBlock,
    { text: "select and then click to edit",
      background: "lightblue",
      editable: true, isMultiline: false })
));
diagram.add(
$(go.Part,
  $(go.TextBlock,
    { text: "this one allows embedded newlines",
      background: "lightblue",
      editable: true })
));

结果:
textblock

Parts是一个图表对象,它继承自Panel。它是所有用户操作级别对象的基类。因为它继承自Panel,所以它实际上也是其他GraphObject可视对象的容器。也同样因为继承自Panel,所以它的超类是GraphObject,因此Parts也拥有GraphObject的属性,比如GraphObject.actualBoundsGraphObject.contextMenuGraphObject.visible

如果你只想创建一个用户可以选中并拖拽移动的可视对象,那么创建Parts就可以满足你。但通常情况下Parts并不能满足我们,所以我们一般创建的是NodeLink,它们都继承自Parts
gojs框架提供两种构建对象的语法,即普通的JavaScript语法和gojs的进阶语法,下面我们来一一了解一下。

传统构建语法

Node是gojs中最低级的GraphObject对象容器,它只能包含继承自GraphObject对象的TextBlockShape对象。Node的父类Part可包含NodeLink对象,Node的超类Panel就能包含更多的GraphObject对象了。 Diagram是gojs中最大的GraphObject对象容器,Palette次之。
一个最简单的Node是由TextBlockShape组成的:

var node = new go.Node(go.Panel.Auto);
var shape = new go.Shape();
shape.figure = "RoundedRectangle";
shape.fill = "lightblue";
node.add(shape);
var textblock = new go.TextBlock();
textblock.text = "Hello!";
textblock.margin = 5;
node.add(textblock);
diagram.add(node);

结果:
diagram

上面的图片就是通过上述代码创建的Diagram,虽然这里只是图片展示,但实际上他是有交互行为的,你可以选中Node,并拖拽移动。

虽然通过这种语法构建Diagram没有问题,但是如果工程量比较大的时候,这种方式会使代码的可读性变的很差,维护起来也很困难。幸好,gojs为我们提供了另一种构建Diagram的语法。

gojs构建语法

gojs定义了一个用于创建GraphObject对象的静态函数GraphObject.make。如果使用该方法创建对象,我们就不用再关心要创建什么样的对象变量名,什么地方该使用什么变量等。该方法还支持内嵌模式,这样的好处就是代码不会是一行一行罗列下来,而是能够表达出层级关系和所属关系的结构体。

GraphObject.make函数的参数量是可变的,但是第一个参数必须是一个GraphObject对象的子类。其他的参数一般有这么几种:

  • 普通JavaScript的键值对象,用于设置构建类型的相关属性比如{ figure:"RoundedRectangle" }
  • 构建对象的子元素,比如说给Node里添加一个Shape
  • 一个枚举类型的参数,一般该参数用于设定构建对象在Diagram中的排列方式,比如平铺排列、垂直排列等,以及嵌套元素的自适应方法。
  • 一个字符串,这个参数一般作为第二个参数,用于设置某个GraphObject子类型的第一属性,比如设置TextBlocktextShapefigurePicturesourcePaneltype等。
  • 如果构建的是一个Table Panel,那么有一个RowCoumnDefinition属性,用于描述rowcolumn的信息。不过有一点要注意的是,如果构建的是一个Diagram,那么这个字符串必须是DIVid
  • 一个JavaScript数组,它的作用有点像元组,如果过一个函数的返回类型由多个的时候,用它来储存。

下面我们用go.GraphObject.make语法来构建一个与上述例子相同的Diagram:

// 注意,这里的$可以更换,随你喜好    
var $ = go.GraphObject.make;
diagram.add(
$(go.Node, go.Panel.Auto,
  $(go.Shape,
    { figure: "RoundedRectangle",
      fill: "lightblue" }),
  $(go.TextBlock,
    { text: "Hello!",
      margin: 5 })
));

结果:
diagram

上面的代码还可以进行简化,将枚举类型参数替换为字符串,可参阅上文中提到的参数类型:

var $ = go.GraphObject.make;
diagram.add(
$(go.Node, "Auto",
  $(go.Shape, "RoundedRectangle", { fill: "lightblue" }),
  $(go.TextBlock, "Hello!", { margin: 5 })
));

结果:
diagram

使用go.GraphObject.make不仅可以构建可视对象,还可以方便的实例化gojs中的装饰对象,可以动态的创建某些作为参数或属性的对象。比如上述例子中的Node是蓝色,我们可以使用样式刷对象(Brush)对其颜色进行修饰:

diagram.add(
$(go.Node, "Auto",
  $(go.Shape, "RoundedRectangle",
    { fill: $(go.Brush, go.Brush.Linear,
              { 0.0: "Violet", 1.0: "Lavender" }) }),
  $(go.TextBlock, "Hello!",
    { margin: 5 })
));

结果:
diagram
下面我们再来看一个构建Diagram的例子:

var myDiagram =
$(go.Diagram, "myDiagramDiv",  // must name or refer to the DIV HTML element
  {
    // any initial diagram is centered in the viewport
    initialContentAlignment: go.Spot.Center,

    // don't initialize some properties until after a new model has been loaded
    "InitialLayoutCompleted": loadDiagramProperties,  // this DiagramEvent listener is defined below

    // have mouse wheel events zoom in and out instead of scroll up and down
    "toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,

    // specify a data object to copy for each new Node that is created by clicking
    "clickCreatingTool.archetypeNodeData": { text: "new node" }
  });

function loadDiagramProperties(e) { . . . }

从上面的例子中可以看出,第二个参数为DIVid,第三个参数就是键值对象了,其实就是json字符串,用于配置Diagram的相关属性。
我们也可以单独创建能够复用的对象,比如样式刷(Brush):

var violetbrush = $(go.Brush, go.Brush.Linear, { 0.0: "Violet", 1.0: "Lavender" });

diagram.add(
$(go.Node, "Auto",
  $(go.Shape, "RoundedRectangle",
    { fill: violetbrush }),
  $(go.TextBlock, "Hello!",
    { margin: 5 })
));

diagram.add(
$(go.Node, "Auto",
  $(go.Shape, "Ellipse",
    { fill: violetbrush }),
  $(go.TextBlock, "Goodbye!",
    { margin: 5 })
));

结果:
diagram

gojs是一个能够让我们很容易的实现基于html5浏览器绘制具有交互性的图形图表的JavaScript框架。
gojs采用了面向对象的编程模式。以图形对象表示绘图模板。以用普通js对象存储数据作为数据模型,然后赋值给图形对象的属性作为数据绑定的模式。gojs同样提供了大量工具类来代表我们的交互行为。我们需要做的就是创建图形对象、构建数据模型、设置属性、绑定数据模型、使用工具类添加行为即可创建出具有丰富交互性能的各种图表。

一个简单的gojs图表

下面的代码定义了一个node模板和数据模型,并它们构建了一个简单的图表:

// For conciseness. See the "Building Parts" intro page for more
var $ = go.GraphObject.make;

// the node template describes how each Node should be constructed
diagram.nodeTemplate =
  $(go.Node, "Auto",  // the Shape automatically fits around the TextBlock
    $(go.Shape, "RoundedRectangle",
      // bind Shape.fill to Node.data.color
      new go.Binding("fill", "color")),
    $(go.TextBlock,
      { margin: 3 },  // some room around the text
      // bind TextBlock.text to Node.data.key
      new go.Binding("text", "key"))
  );

// the Model holds only the essential information describing the diagram
diagram.model = new go.GraphLinksModel(
[ // a JavaScript Array of JavaScript objects, one per node
  { key: "Alpha", color: "lightblue" },
  { key: "Beta", color: "orange" },
  { key: "Gamma", color: "lightgreen" },
  { key: "Delta", color: "pink" }
],
[ // a JavaScript Array of JavaScript objects, one per link
  { from: "Alpha", to: "Beta" },
  { from: "Alpha", to: "Gamma" },
  { from: "Beta", to: "Beta" },
  { from: "Gamma", to: "Delta" },
  { from: "Delta", to: "Alpha" }
]);

diagram.initialContentAlignment = go.Spot.Center;
// enable Ctrl-Z to undo and Ctrl-Y to redo
diagram.undoManager.isEnabled = true;

结果:
Diagram

虽然上面展示的是图片,但是实际上我们可以在这个图表上用鼠标中键滚动、选中、删除、重做、撤销等等交互。

gojs的基本概念

上面的示例是让我们先在感官上认知一下gojs的产出物,下面我们来阐述一下gojs的基本概念。

gojs绘制的图表(Diagram)具有两个最基本的元素,就是点和线(NodeLink),并且他们可以自由组合组成一个组(Group)。所有的元素都处在图层(Layer)上,并且可以对它们进行布局(Layout)。

每个Diagram都是通过数据模型(Model)来填充和确定Node的信息和Link的所属关系的。并且我们只需要创建好NodeLink的模板以及数据模型,其他的是事情都交给gojs去处理。它会通过Model.nodeDataArray方法和GraphLinksModel.linkDataArray方法自动加载模型并构建元素。

每一个NodeLink都是通过模板来描述他们的文本、形状、颜色等信息以及交互行为。每个模板其实就是一个面板(Panel)(你可以将各种元素自由组合在它里面,也可以在它里面添加各种交互行为),比如说将TextBlockShapePicture等元素添加到这个Panel中,鼠标进入离开的交互行为也可以添加到Panel中,那么这个Panel就是一个模板。

每个Node的位置可以使用Diagram.layoutGroup.layout进行初始化设置,也可以基于交互行为进行拖拽。

gojs里的工具类可以为Diagram添加鼠标、键盘事件。一般情况下Diagram都默认设置了几种交互行为,比如说拖拽、连线。我们也可以通过ToolManager对象来管理工具类,或者说来管理交互行为,比如说可以停止某些交互,或开启某些交互等。

每个Diagram同时也包含CommandHandler对象,它的作用是添加一些键盘命令,比如点Delete键删除元素,Ctrl+C复制、Ctrl+V粘贴、Ctrl+Z撤销等。但是CommandHandler也是被ToolManager管理的。

Diagram也提供通过鼠标中键滚动视图、放大缩小视图。

gojs还提供了图表的预览视图(Overview),用于了解大规模图表的概况,同时还提供了组件管理面板(Palette),用于管理创建的组件,并且支持将组件拖拽到Diagram中。

Diagram中,你可以选中Node或者Link,你会发现他们有少许的变化,比如在Node周围会增加选中框,选中Link会变色等。这些都是由Adornment对象控制的,你还可以用它来增加提示框、右键菜单等。

Diagram默认的交互行为

  • 选中Node或者Link,会出现蓝色选中框,Link也会变为蓝色。
  • 使用Ctrl+鼠标左键可以选中多个元素。
  • Panel中点击鼠标左键移动鼠标可以移动所有元素。
  • 使用Ctrl+A可以选中所有元素。
  • 选中元素,用鼠标可以拖动元素。
  • 使用Ctrl+C/Ctrl+V可以复制粘贴元素。
  • 使用Delete键可以删除元素。
  • 选中多个元素,然后使用Ctrl+G可以将这些选中的元素组合成一个组。
  • 使用Ctrl+Shift+G可以拆散组。
  • 当元素超出Diagram时,可自动出现滚动条。
  • 用鼠标拖拽Node边缘可以与其他Node进行连线。
  • 选中Link,可以重新选择目标Node,即可以重新连线。

创建一个Diagram

每个Diagram都需要依托与一个HTML的DIV元素,虽然由gojs接管了这个DIV,但是DIV本身的属性依然可以由我们通过CSS设置,比如位置、高宽等。gojs实际上是在DIV中创建了一个Canvas元素,Canvas的高宽自动适配DIV的高宽。

<!-- The DIV for a Diagram needs an explicit size or else we won't see anything.
 In this case we also add a border to help see the edges. -->
<div id="myDiagramDiv" style="border: solid 1px blue; width:400px; height:150px"></div>

当然,当你准备开始创建Diagram前,首先要导入gojs的文件。gojs的文件分为Develop(gojs-debug.js)和Product(gojs.js)两种,前者是供开发人员使用的,因为它包含了一些用于调试的代码,后者用于正式发布,速度较快。gojs不依赖与任何JavaScript框架,所以它可以与任何JavaScript框架一起使用,而不会产生冲突。

<!-- Include the GoJS library. -->
<script src="gojs.js"></script>

下面就可以使用gojs将上述的DIV构建为一个Diagram了,这里需要注意的是,使用gojs的api时,都要使用前缀go.

<!-- Create the Diagram in the DIV element using JavaScript. -->
<!-- The "go" object is the "namespace" that holds all of the GoJS types. -->
<script>
var diagram = new go.Diagram("myDiagramDiv");
diagram.model = new go.GraphLinksModel(
    [{ key: "Hello" },   // two node data, in an Array
    { key: "World!" }],
    [{ from: "Hello", to: "World!"}]  // one link, in an Array
);
</script>

这样就构建了一个简单的Diagram
Diagram

方法

Objective-C中的方法有两种:

实例方法

-开头的方法是实例方法。它属于类的某一个或某几个实例对象,即类对象必须实例化后才可以使用的方法,将消息发送给实例对象:

// Deck.h

#import <Foundation/Foundation.h>
#import "Card.h"

@interface Deck : NSObject

@property(nonatomic) int cardNum;

// 实例方法
- (Card *)randomDrawCard;

+ (NSString *)CardKinds;

@end

实例方法中可以使用该类的所有实例变量:

// Deck.m

#import "Card.h"

@implementation Deck

- (Card *)drawCardFromTop
{
    // 实例变量
    _cardNum--;

    //TODO.....
}

+ (NSString *)CardKinds
{
    NSLog("Cards are divided into four kinds: spades, diamonds, clubs and hearts");
}

@end 

类方法

+开头的方法是类方法。Objc中的类方法类似Java中的static静态方法,它是属于类本身的方法,不属于类的某一个实例对象,所以不需要实例化类,用类名即可使用,是将消息发送给类:

// Deck.h

#import <Foundation/Foundation.h>
#import "Card.h"

@interface Deck : NSObject

- (Card *)randomDrawCard;

// 类方法
+ (NSString *)CardKinds;

@end    

类方法不能使用任何实例变量:

// Deck.m

#import "Card.h"

@implementation Deck

- (Card *)drawCardFromTop
{
    // 实例变量
    _cardNum--;

    //TODO.....
}

// 不能使用该类的实例变量_carNum
+ (NSString *)CardKinds
{
    NSLog("Cards are divided into four kinds: spades, diamonds, clubs and hearts");
}

@end 

所以我们使用类方法一般有两种情况:

  • 创建一些事物,比如特殊格式的字符串等。
  • 作为工具方法,比如返回常数等。

类方法和实例方法认知的误区

  • 类方法常驻内存,所以比实例方法效率高。
    事实上,在加载时机和占用内存上,类方法和实例方法是一样的,在类第一次被使用时加载方法,所以在效率上没有什么区别。

  • 类方法分配在堆上,实例方法分配在栈上。
    事实上,所有的方法都不可能分配在堆栈区,方法作为二进制代码是存储在内存的程序代码区,这个内存区域是不可写的。请查看我这篇笔记中的相关概念Objective-C中的Block

总结

实例方法和类方法有大多数的共性,比如都可以有一个或多个参数、都可以继承基类的方法、相同的声明规范等。唯一不同的就是类方法不能使用实例变量,所以导致它只适用于一些特殊的情况。

Category

如果我们想给一个已存在的、很复杂的类添加一个新的方法,应该怎么做?
想翻源码添加?骚年,你太天真,你如果看不到源码呢。即便我们可以看到源码,如果我们新增的逻辑也很复杂,这样就会扩大原始设计的规模,有可能会打乱整个设计的结构。
Category就是Objective-C提供的为我们解决这一问题的方法。它可以让我们动态的在已经存在的类中添加新的行为,即方法。对类进行扩展时不需要访问其源码,也不需要创建子类。

使用方法

Category的实现很简单,我们举例说明。

// Deck.h

#import <Foundation/Foundation.h>
#import "Card.h"

@interface Deck : NSObject

- (Card *)randomDrawCard;

@end

这是类Deck的声明文件,其中包含一个实例方法randomDrawCard,如果我们想在不修改原始类、不增加子类的情况下,为该类增加一个drawCardFromTop方法,只需要定义两个文件Deck+DrawCardFromTop.hDeck+DrawCardFromTop.m,在声明文件和实现文件中用()把Category的名称括起来即可,声明文件如下:

// Deck+DrawCardFromTop.h

#import "Deck.h"
#import "Card.h"

@interface Deck(DrawCardFromTop)

- (Card *)drawCardFromTop;

@end

实现文件如下:

// Deck+DrawCardFromTop.m

#import "Deck+DrawCardFromTop.h"
#import "Card.h"

@implementation Deck(DrawCardFromTop)

- (Card *)drawCardFromTop
{
    //TODO.....
}

@end

DrawCardFromTop是Category的名称。这里一般使用约定俗成的习惯,将声明文件和实现文件统一采用”原类名+Category名”的方式命名。
使用也非常简单,引入Category的声明文件,然后正常调用即可:

// main.m

#import "Deck+DrawCardFromTop.h"
#import "Card.h"

int main(int argc, char * argv[])
{

    Deck *deck = [[Deck alloc] init];
    Card *card = [deck drawCardFromTop];

    return 0;

}

使用场景

  • 需求变更在整个开发周期是司空见惯的事情,那么我们可能就需要对某个或某几个类中添加新的方法以满足需求。
  • 我们在团队协作开发时候,经常需要多个人来实现一个类中的不同方法,在这种情况下采用Category是一个较好的选择。
  • 当一些基础类库满足不了我们的需求时,我们希望能扩展基础类库,这时就需要Category。

需要注意的问题

  • Category可以访问原始类的实例变量,但不能添加变量,如果想添加变量,可以考虑通过继承创建子类。
  • Category可以重载原始类的方法,但不推荐这么做,这么做的后果是你再也不能访问原来的方法。如果确实要重载,正确的选择是创建子类。
  • 和普通接口有所区别的是,在分类的实现文件中可以不必实现所有声明的方法,只要你不去调用它。

总结

掌握并用好Category可以充分利用Objective-C的动态特性,编写出灵活简洁的代码。

Protocol

简单来说,Protocol不属于任何一个类,它只是一个方法列表,任何类都可以对其中声明的方法进行实现。这种设计模式一般称为代理模式(delegation)。你可以通过Protocol定义各种行为,在不同的场景采用不同的实现方式。在iOS和OS X开发中,Apple采用了大量的代理模式来实现MVC中View和Controller的解耦。

使用方法

Protocol有两种声明的方式:

  • 在单独的声明文件(.h文件)中声明。
  • 在某个类的声明的文件中声明。

以上两种方式视具体情况而定,但是在代码规范上都是一致的:

// HandleDeckDelegate.h

@protocol HandleDeckDelegate <NSObject>

@required
- (void)ShuffleDeck;

@optional
- (void)CuttingDeck;

@end

上述代码中有两个关键字,@required@optional,表示如果要实现这个协议,那么ShuffleDeck方法是必须要实现的,CuttingDeck则是可选的,如果不注明,那么方法默认是@required的,必须实现。

那么如何实现这个Protocol呢,很简单,创建一个普通的Objective-C类,如果Protocol使用单独的.h文件声明,那么在该类的.h声明文件中引入包含Protocol的.h文件,如果Protocol是声明在一个相关类中,那么就需要引入该类的.h文件。之后声明采用这个Protocol即可:

// Deck.h

#import <Foundation/Foundation.h>
#import "Card.h"
#import "HandleDeckDelegate.h"

@interface Deck : NSObject<HandleDeckDelegate>

- (Card *)randomDrawCard;

@end

用尖括号(\<…>)括起来的HandleDeckDelegate就是我们创建的Protocol。如果要采用多个Protocol,可以在尖括号内引入多个Protocol名称,并用逗号隔开即可。例如<HandleDeckDelegate,xxxDelegate>

// Deck.m

#import "Card.h"

@implementation Deck

- (Card *)drawCardFromTop
{
    //TODO.....
}

- (void)ShuffleDeck
{
    //TODO.....
}

@end

由于CuttingDeck方法是可选的,所以我们只实现了ShuffleDeck

使用场景

  • Objective-C里的Protocol和Java语言中的接口很类似,如果一些类之间没有继承关系,但是又具备某些相同的行为,则可以使用Protocol来描述它们的关系。
  • 不同的类,可以遵守同一个Protocol,在不同的场景下注入不同的实例,实现不同的功能。

需要注意的问题

  • 根据约定,框架中后缀为Delegate的都是Protocol,例如UIApplicationDelegateUIWebViewDelegate等。
  • Protocol本身是可以继承的,比如:
@protocol A
    -(void)methodA;
@end

@protocol B <A>
    -(void)methodB;
@end

如果你要实现B,那么methodA和methodB都需要实现。

  • Protocol是与任何类都无关的,任何类都可以实现定义好的Protocol,如果我们想知道某个类是否实现了某个Protocol,那么我们可以用conformsToProtocol方法进行判断:
[obj conformsToProtocol:@protocol(ProcessDataDelegate)]

总结

Protocol最常用的就是委托代理模式,Cocoa框架中大量采用了这种模式实现数据和UI的分离。例如UIView产生的所有事件,都是通过委托的方式交给Controller完成。

@property是什么

@Property是声明属性的语法,它可以快速方便的为实例变量创建存取器,并允许我们通过点语法使用存取器。

存取器(accessor):指用于获取和设置实例变量的方法。用于获取实例变量值的存取器是getter,用于设置实例变量值的存取器是setter

创建存取器

手工创建存取器

我们先看两段代码:

// Car.h

#import <Foundation/Foundation.h>

@interface Car : NSObject
{
    // 实例变量
    NSString *carName;
    NSString *carType;
}

// setter                                   
- (void)setCarName:(NSString *)newCarName; 

// getter
- (NSString *)carName;

// setter
- (void)setCarType:(NSString *)newCarType;

// getter
- (NSString *)carType;

@end  

上面的代码中carNamecarType就是Car的实例变量,并且可以看到分别对这两个实例变量声明了get/set方法,即存取器。

#import "Car.h"

@implementation Car

// setter
- (void)setCarName:(NSString *)newCarName
{
    carName = newCarName;
}

// getter
- (NSString *)carName
{
    return carName;
}

// setter
- (void)setCarType:(NSString *)newCarType
{
    carType = newCarType;
}

// getter
- (NSString *)carType
{
    return carType;
}

@end

上面代码是对实例变量存取器的实现。我们可以看到,存取器就是对实例变量进行赋值和取值。按照约定赋值方法以set开头,取值方法以实例变量名命名。
我们看看如何使用:

// main.m

#import "Car.h"

int main(int argc, char * argv[])
{
    @autoreleasepool {
        Car *car = [[Car alloc] init];
        [car setCarName:@"Jeep Cherokee"];
        [car setCarType:@"SUV"];
        NSLog(@"The car name is %@ and the type is %@",[car carName],[car carType]);      
    }
    return 0;
}

上面的代码中我们注意到,对象Car使用了消息语法,也就是使用方括号的语法给存取器发送消息。
返回结果为:

The car name is Jeep Cherokee and the type is SUV

使用@Property创建存取器

// Car.h

#import <Foundation/Foundation.h>

@interface Car : NSObject
{
    // 实例变量
    NSString *carName;
    NSString *carType;
}

@property(nonatomic,strong) NSString *carName;
@property(nonatomic,strong) NSString *carType;

@end

上面代码中,我们使用@property声明两个属性,名称与实例变量名称相同(让我们先忽略nonatomicstrong)。

// Car.m

#import "Car.h"

@implementation Car

@synthesize carName;
@synthesize carType;

@end

在.m文件中我们使用@synthesize自动生成这两个实例变量的存取器,并且隐藏了存取器,虽然我们看不到存取器,但它们确实是存在的。

// main.m

int main(int argc, char * argv[])
{
    @autoreleasepool {
        Car *car = [[Car alloc] init];
        car.carName = @"Jeep Compass";
        car.carType = @"SUV";
        NSLog(@"The car name is %@ and the type is %@",car.carName,car.carType); 
    }
    return 0;
}

在上面的代码中我们可以注意到,Car对象使用点语法给存取器发送消息,并且get与set的语法是相同的,所以这里的点语法可以根据语境判断我们是要赋值还是取值。

当然我们也依然可以使用消息语法来使用:

// main.m

int main(int argc, char * argv[])
{
    @autoreleasepool {
        Car *car = [[Car alloc] init];

        // 点语法
//        car.carName = @"Jeep Compass";
//        car.carType = @"SUV";
//        NSLog(@"The car name is %@ and the type is %@",car.carName,car.carType);

        // 消息语法
        [car setCarName:@"Jeep Compass"];
        [car setCarType:@"SUV"];
        NSLog(@"The car name is %@ and the type is %@",[car carName],[car carType]);       
    }
    return 0;
}

上面两段代码的执行结果都是:

The car name is Jeep Compass and the type is SUV

总结:@property等同于在.h文件中声明实例变量的get/set方法,@synthesize等同于在.m文件中实现实例变量的get/set方法。使用@propertysynthesize创建存取器要比手动声明两个存取方法(gettersetter)更简单。而且我们在使用属性时可以使用点语法赋值或取值,语法更简单,更符合面向对象编程。

不必单独声明示例变量

如果使用@Property,就不必单独声明实例变量了。因为在没有显示提供示例变量声明的前提下,系统会自动帮你生成实例变量。我们通过以下代码来说明:

// Car.h

#import <Foundation/Foundation.h>

@interface Car : NSObject

@property(nonatomic,strong) NSString *carName;
@property(nonatomic,strong) NSString *carType;

- (NSString *)carInfo;

@end

在.h文件中我们并没有声明实例变量,只是声明了carNamecarType两个属性,以及一个carInfo方法,返回值为NSString *

// Car.m

#import "Car.h"

@implementation Car

- (NSString *)carInfo
{
    return [NSString stringWithFormat:@"The car name is %@ and the type is %@",_carName,_carType];
}

@end

在.m文件中我们可以注意到,在carInfo方法中我们使用了_carName_carType实例变量,这就是当我们没有显示声明实例变量时,系统为我们自动生成的。命名规则是以_为前缀,加上属性名,即_propertyName

其实在.m文件中实际是存在@synthesize声明语句的,只是系统将其隐藏了:

@synthesize carName = _carName;
@synthesize carType = _carType;

那么如果我们不喜欢默认的实例变量命名方法,或者我们希望使用更有语义的名称,应该怎么做呢。其实很简单:

// Car.m

#import "Car.h"

@implementation Car

@synthesize carName = i_am_car_name;
@synthesize carType = i_am_car_type;

- (NSString *)carInfo
{
    return [NSString stringWithFormat:@"The car name is %@ and the type is %@",i_am_car_name,i_am_car_type];
}

@end

通过上述代码可以看到,我们只需要通过@synthesize来声明我们希望的实例变量名。

总结:如果我们希望使用默认的实例变量命名方式,那么我们在.m文件中就不需要使用@synthesize声明,系统会帮我们自动完成。如果我们希望自己命名实例变量命,那么我们就使用@synthesize显示声明我们希望的实例变量名。

@property的特性

@property还有一些关键字,它们都是有特殊作用的,比如上述代码中的nonatomicstrong

@property(nonatomic,strong) NSString *carName;
@property(nonatomic,strong) NSString *carType;

我把它们分为三类,分别是:原子性,存取器控制,内存管理。

原子性

  • atomic(默认):atomic意为操作是原子的,意味着只有一个线程访问实例变量。atomic是线程安全的,至少在当前的存取器上是安全的。它是一个默认的特性,但是很少使用,因为比较影响效率,这跟ARM平台和内部锁机制有关。
  • nonatomicnonatomicatomic刚好相反。表示非原子的,可以被多个线程访问。它的效率比atomic快。但不能保证在多线程环境下的安全性,在单线程和明确只有一个线程访问的情况下广泛使用。

存取器控制

  • readwrite(默认):readwrite是默认值,表示该属性同时拥有settergetter
  • readonlyreadonly表示只有getter没有setter

有时候为了语意更明确可能需要自定义访问器的名字:

@property (nonatomic, setter = mySetter:,getter = myGetter ) NSString *name;

最常见的是BOOL类型,比如标识View是否隐藏的属性hidden。可以这样声明:

@property (nonatomic,getter = isHidden ) BOOL hidden;

内存管理

@property有显示的内存管理策略。这使得我们只需要看一眼@property声明就明白它会怎样对待传入的值。

  • assign(默认):assign用于值类型,如intfloatdoubleNSIntegerCGFloat等表示单纯的复制。还包括不存在所有权关系的对象,比如常见的delegate。
@property(nonatomic) int running;
@property(nonatomic,assign) int running;

以上两段代码是相同的。

setter方法中,采用直接赋值来实现设值操作:

-(void)setRunning:(int)newRunning{  
    _running = newRunning;  
} 
  • retain:在setter方法中,需要对传入的对象进行引用计数加1的操作。
    简单来说,就是对传入的对象拥有所有权,只要对该对象拥有所有权,该对象就不会被释放。如下代码所示:
-(void)setName:(NSString*)_name{  
     //首先判断是否与旧对象一致,如果不一致进行赋值。  
     //因为如果是一个对象的话,进行if内的代码会造成一个极端的情况:当此name的retain为1时,使此次的set操作让实例name提前释放,而达不到赋值目的。  
     if ( name != _name){  
          [name release];  
          name = [_name retain];  
     }  
}
  • strongstrong是在IOS引入ARC的时候引入的关键字,是retain的一个可选的替代。表示实例变量对传入的对象要有所有权关系,即强引用。strong跟retain的意思相同并产生相同的代码,但是语意上更好更能体现对象的关系。
  • weak:在setter方法中,需要对传入的对象不进行引用计数加1的操作。
    简单来说,就是对传入的对象没有所有权,当该对象引用计数为0时,即该对象被释放后,用weak声明的实例变量指向nil,即实例变量的值为0。

    注:weak关键字是IOS5引入的,IOS5之前是不能使用该关键字的。delegate 和 Outlet 一般用weak来声明。

  • copy:与strong类似,但区别在于实例变量是对传入对象的副本拥有所有权,而非对象本身。