WorkflowSchema 是一个 iOS 框架,允许你在 XML 中定义应用程序的工作流程。
将 WorkflowSchema 添加到项目的最快方法是使用 CocoaPods。将以下内容添加到您的 Podfile 中
pod 'WorkflowSchema', '~> 0.2.0'
然后
$ pod install
如果您不想使用 CocoaPods 添加 WorkflowSchema,您需要安装 https://github.com/kstenerud/iOS-Universal-Framework。然后检出源并拖动 WorkflowSchema.xcodeproj 进入项目的框架组
在“构建阶段”下,将框架添加到“目标依赖项”中,并将它添加到“与二进制链接的库”中
无论哪种方式,您现在应该可以从自己的源代码中访问 WorkflowSchema。在您的应用程序代理(或您想使用 WorkflowSchema 的任何地方),加载一个方案
#import <WorkflowSchema/WorkflowSchema.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
NSError *error = nil;
NSURL *xmlURL = [NSURL fileURLWithPath:@"/path/to/your/file.xml"];
WFSSchema *schema = [[[WFSXMLParser alloc] initWithContentsOfURL:xmlURL] parse:&error];
现在创建一个上下文并创建方案的根对象
WFSContext *context = [WFSContext contextWithDelegate:self];
UIViewController *controller = (UIViewController *)[schema createObjectWithContext:context error:&error];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
return YES;
}
您需要实现 WFSContextDelegate
- (void)context:(WFSContext *)contect didReceiveWorkflowError:(NSError *)error
{
[[[UIAlertView alloc] initWithTitle:@"Workflow error"
message:error.localizedDescription
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
- (BOOL)context:(WFSContext *)contect didReceiveWorkflowMessage:(WFSMessage *)message
{
return NO;
}
现在您可以开始使用了。
工作流程方案封装了一个控制器及其视图,以及响应用户输入可执行的操作。
一个简单的方案可能看起来像这样
<workflow>
<navigation>
<screen>
<title>Example</title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
</view>
<actions>
<showAlert name="exampleButtonTapped">Example button tapped!</showAlert>
</actions>
</screen>
</navigation>
</workflow>
这表示一个导航控制器及其内容,可能像这样显示给用户
它还指定了当按钮被点击时会发生什么
方案的基本结构是对象标签包含参数标签,这些参数标签又可以包含对象标签。对象标签指示框架创建一个对象,而参数标签指示框架在对象上设置属性。
因此,在我们的示例中,<screen>
标签代表一个 WFSScreenController
对象,该对象有三个参数:title
、view
和 actions
。标题参数包含一个字符串("Example"),view
包含一个 WFSContainerView
,而 actions
包含一个 WFSShowAlertAction
。
为了保持文件大小较小,框架支持默认参数。这意味着一个对象标签可以出现在另一个对象标签内,框架会确定它所属的参数。在我们的示例中,这发生了几次:首先,<navigation>
标签代表一个WFSNavigationController
对象,正如之前提到的,<screen>
标签代表一个WFSScreenController
对象。WFSNavigationController
有一个名为viewControllers
的参数,这是从UIViewController
继承的对象的默认参数;WFSScreenController
是UIViewController
的子类,因此屏幕控制器被分配给了导航控制器的viewControllers
属性。同样的事情也发生在容器的views
属性、标签的text
属性和警报操作的message
属性上。这个示例XML也可以用等效的方式编写
<workflow>
<navigation>
<viewControllers>
<screen>
<title>Example</title>
<view>
<container>
<views>
<label>
<text>Example label</text>
</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</views>
</container>
</view>
<actions>
<showAlert name="exampleButtonTapped">
<message>Example button tapped!</message>
</showAlert>
</actions>
</screen>
</viewControllers>
</navigation>
</workflow>
或者,由于view
是视图对象的默认参数,而actions
是动作对象的默认参数,
<workflow>
<navigation>
<screen>
<title>Example</title>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
<showAlert name="exampleButtonTapped">Example button tapped!</showAlert>
</screen>
</navigation>
</workflow>
对象可以有自己的名称。在我们的示例中,showAlert
标签有一个名称exampleButtonTapped
。
当用户与视图交互时,它可以告知控制器执行一个动作。再看我们的第一个例子
<workflow>
<navigation>
<screen>
<title>Example</title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
</view>
<actions>
<showAlert name="exampleButtonTapped">Example button tapped!</showAlert>
</actions>
</screen>
</navigation>
</workflow>
按钮有一个message
属性,它指定了当按钮被点击时向控制器发送的消息。当控制器接收到这个消息时,它会尝试执行与消息同名的操作 - 在这种情况下是exampleButtonTapped
,它会显示一个警报。
不同的动作可以完成不同的事情。例如,我们可以将另一个屏幕推到导航堆栈上
<workflow>
<navigation>
<screen>
<title>Example</title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
</view>
<actions>
<pushController name="exampleButtonTapped">
<screen>
<title>Example 2</title>
<view>
<container>
<label>This controller gets pushed!</label>
</container>
</view>
</screen>
</pushController>
</actions>
</screen>
</navigation>
</workflow>
当用户点击按钮时,他们会看到这个
假设我们修改我们的屏幕以添加另一个按钮,并将一个动作添加到导航控制器中
<workflow>
<navigation>
<screen>
<title>Example<title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button 1</title>
<message>exampleButton1Tapped</message>
</button>
<button>
<title>Example button 2</title>
<message>exampleButton2Tapped</message>
</button>
</container>
</view>
<actions>
<showAlert name="exampleButton1Tapped">Example button 1 tapped!</showAlert>
</actions>
</screen>
<actions>
<showAlert name="exampleButton2Tapped">Example button 2 tapped!</showAlert>
</actions>
</navigation>
</workflow>
假设用户点击第二个按钮,但没有发生任何事情。这是因为按钮告诉屏幕控制器执行名为exampleButton2Tapped
的操作,但没有任何这样的操作。请求没有被传递到导航控制器,所以什么也没有发生。
如果我们想将按钮连接到导航控制器的动作,我们需要发送另一条消息。我们使用一个特别的动作叫做<sendMessage>
或WFSSendMessageAction
<workflow>
<navigation>
<screen>
<title>Example<title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button 1</title>
<message>exampleButton1Tapped</message>
</button>
<button>
<title>Example button 2</title>
<message>exampleButton2Tapped</message>
</button>
</container>
</view>
<actions>
<showAlert name="exampleButton1Tapped">Example button 1 tapped!</showAlert>
<sendMessage name="exampleButton2Tapped">
<message>exampleMessage</message>
</sendMessage>
</actions>
</screen>
<actions>
<showAlert name="exampleMessage">Example button 2 tapped!</showAlert>
</actions>
</navigation>
</workflow>
现在当用户点击第二个按钮时,它告诉屏幕控制器执行名为exampleButton2Tapped
的操作;然后这个动作发送一个名为exampleMessage
的消息。导航控制器接收这个消息并尝试执行一个具有相同名称的操作。有一个这样的操作,因此它会执行。
默认情况下,消息会被传递到控制器的创建者 - 在这种情况下,屏幕控制器的创建者是导航控制器 - 并且不会被传递。可以通过指定一个目的地来将它们发送到其他地方。例如,您可以使用'this'目的地类型来在同一个控制器上触发其他操作
<workflow>
<navigation>
<screen>
<title>Example</title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
</view>
<actions>
<sendMessage name="exampleButtonTapped">
<message>
<name>exampleMessage</name>
<destinationType>self</destinationType>
</message>
</sendMessage>
<showAlert name="exampleMessage">Message received and understood!</showAlert>
</actions>
</screen>
</navigation>
</workflow>
在这种情况下,当按钮被点击时,屏幕控制器接收到的exampleButtonTapped
消息会执行匹配的操作。这会将exampleMessage
消息发送给自己,因此它会再次执行匹配的操作,在这种情况下显示一个警报。
您可以使用'descendant'目的地类型将消息发送给子控制器。例如
<workflow>
<navigation>
<screen>
<title>Example</title>
<navigationItem>
<rightBarButtonItem>
<barButtonItem>
<title>Submit</title>
<message>submitBarButtonTapped</message>
</barButtonItem>
</rightBarButtonItem>
</navigationItem>
<form name="theForm">
<container>
<label>Example text</label>
<textField name="exampleText"></textField>
</container>
</view>
<actions>
<sendMessage name="leftBarButtonTapped">
<message>
<name>submit</name>
<destinationType>descendant</destinationType>
<destinationName>theForm</destinationName>
</message>
</sendMessage>
</actions>
</screen>
</navigation>
</workflow>
在这种情况下,当工具栏按钮被点击时,屏幕控制器接收到的submitBarButtonTapped
消息;然后它会将submit
消息发送给它的子控制器,其名称为theForm
。
第四种目的地是 rootDelegate
。具有此目的地类型的消息将被传递到您的应用程序代理那里,并且将调用 context:didReceiveWorkflowMessage:
代理消息。这就是您实现类似 API 调用的方式。
注意动作名称与消息名称之间的区别
<sendMessage name="exampleButton2Tapped">
<message>
<name>exampleMessageName</name>
</message>
</sendMessage>
动作被命名为 exampleButton2Tapped
,但发送的消息被命名为 exampleMessageName
。
之前我们说“默认情况下,消息将被传递到控制器创建者”。为了解释实际上发生了什么,我们需要介绍工作流程上下文的概念。我们已经在设置工作流程时看到过了。
WFSContext *context = [WFSContext contextWithDelegate:self];
上下文封装了框架为了从模式创建对象和确定消息应该发送到哪里所需的信息。每个上下文都有一个代表对象,该对象实现了 context:didReceiveWorkflowMessage:
以处理消息。每次创建对象或执行动作时,都会向其传递一个上下文。当对象作为控制器的参数创建,或控制器执行动作时,它会创建用于创建控制器的上下文的副本,并将新上下文的代表设置为自身。然后它实现了 context:didReceiveWorkflowMessage:
来过滤出针对该控制器的消息,然后将其他消息传递给自己所在的上下文。因此,控制器有一系列代表可以响应消息,一直到最后您在创建第一个上下文时提供的代表。
上下文还有一个包含动作结果的 userInfo 字典。这用于,例如,允许模式加载其他模式。
我们看到了一个示例模式,它将新的控制器推送到导航堆栈中,但如果我们能够从另一个文件中加载该控制器,那会很好。我们使用 loadSchema
动作来完成这个操作。
<workflow>
<navigation>
<screen>
<title>Example</title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
</view>
<actions>
<loadSchema name="exampleButtonTapped">
<path>/path/to/other/schema.xml</path>
<successAction>
<pushController />
</successAction>
<failureAction>
<showAlert>Failed to load schema</showAlert>
</failureAction>
</loadSchema>
</actions>
</screen>
</navigation>
</workflow>
现在当用户点击按钮时,会执行 loadSchema
动作。这发送一个名为“loadSchema”的消息,其中包含其上下文的 userInfo 中的“/path/to/other/schema.xml”,并沿代表链等待响应。收到响应后,它会查看是否成功,如果成功,则会查看带有响应的上下文的 userInfo。如果“schema”键的值包含一个模式,则将其添加到其原始上下文中,并执行成功动作。
在这种情况下,成功动作是 pushController
动作,它知道如果没有参数,它应该查看其上下文中的“schema”键,如果找到该键,则创建该模式并检查它是否是 UIViewController
。如果是,则将其推送到堆栈上。
这里缺少的一部分是:实际读取文件。框架对您想要如何存储工作流程文件没有做出任何假设;它们可能在设备上,也可能在某个远程服务器上。实际加载和解析是原始上下文代表的职责。一种可能的实现方法是将 context:didReceiveWorkflowMessage:
实现如下
- (BOOL)context:(WFSContext *)contect didReceiveWorkflowMessage:(WFSMessage *)message
{
if ([message.name isEqualToString:WFSLoadSchemaActionMessageName])
{
NSError *error = nil;
WFSSchema *schema = nil;
WFSResult *result = nil;
NSString *path = message.context.userInfo[WFSLoadSchemaActionPathKey];
if (path)
{
NSURL *xmlURL = [NSURL fileURLWithPath:message.name];
schema = [[[WFSXMLParser alloc] initWithContentsOfURL:xmlURL] parse:&error];
}
if (schema)
{
WFSMutableContext *successContext = [message.context mutableCopy];
successContext.userInfo = @{ WFSLoadSchemaActionSchemaKey : schema };
result = [WFSResult successResultWithContext:successContext];
}
else
{
NSLog(@"Error loading schema at %@: %@", path, error);
result = [WFSResult failureResultWithContext:message.context];
}
[message respondWithResult:result];
return YES; // The message has been handled, whether successfully or not
}
return NO; // The message has not been handled
}
所有这些操作的结果是,当用户点击按钮时,将从给定路径加载一个模式,并且表示该模式的控制器将被推送到导航堆栈上。
现在我们可以在多个文件中拆分我们的工作流程,如果能以不同的方式重用这些文件那就很棒了。看起来会很困难,因为我们不知道我们正在编写一个要推入现有导航堆栈的控制器,还是它将以模态方式呈现,因此需要自己的堆栈。我们希望的是进行展示的控制器能够提供导航堆栈,然后我们就可以在不包含任何外部控制器的情况下编写除了第一个以外的所有文件。
幸运的是,我们可以使用参数代理来实现这一功能。参数代理是一个应该在上下文的userInfo中提取,而不是直接创建的对象。当一个对象标签使用keyPath
属性时,它代表了一个参数代理,如下所示
<workflow>
<navigation>
<screen>
<title>Example</title>
<view>
<container>
<label>Example label</label>
<button>
<title>Example button</title>
<message>exampleButtonTapped</message>
</button>
</container>
</view>
<actions>
<loadSchema name="exampleButtonTapped">
<path>/path/to/other/schema.xml</path>
<successAction>
<presentController>
<navigation>
<screen keyPath="schema" />
</navigation>
</presentController>
</successAction>
<failureAction>
<showAlert>Failed to load schema</showAlert>
</failureAction>
</loadSchema>
</actions>
</screen>
</navigation>
</workflow>
现在这告诉框架,当按钮被按下时,它应该加载给定的模式;这个模式将进入用于成功的上下文的userInfo的"schema"键中,该上下文是以模态方式呈现的导航控制器。该控制器创建了一个屏幕控制器,但不是基于它查找的XML,而是在"schema"键中查找,然后创建它找到的由模式定义的对象。
换句话说,它从给定的文件中加载模式,将找到的控制器包裹在导航控制器中,然后以模态方式呈现它。
现在我们可以孤立地编写所有的工作流程模式文件,除了用户看到的第一份文件。如果我们也想在那个可重用组件中这样做怎么办?到目前为止,我们只能对用户输入做出响应执行操作。然而,也可能会对视图生命周期事件做出响应执行操作。当控制器调用了tab栏、导航或屏幕控制器的viewDidLoad、viewWillAppear、viewDidAppear、viewWillDisappear和viewDidDisappear方法时,控制器会查找相应命名的动作(动作名称省略了方法名称中的冒号,对于那些有冒号的方法)。因此,我们可以将我们的第一个工作流程文件设置为以下方式
<workflow>
<tabs>
<navigation>
<tabItem>
<title>Tab 1</title>
<image>tab1</image>
</tabItem>
<screen>
<title>Loading...</title>
<view>
<label>Loading....</label>
</view>
</screen>
<loadSchema name="viewDidLoad">
<path>/path/to/schema1.xml</path>
<successAction>
<replaceRootController />
</successAction>
<failureAction>
<showAlert>Failed to load schema</showAlert>
</failureAction>
</loadSchema>
</navigation>
<navigation>
<tabItem>
<title>Tab 2</title>
<image>tab2</image>
</tabItem>
<screen>
<title>Loading...</title>
<view>
<label>Loading....</label>
</view>
</screen>
<loadSchema name="viewDidLoad">
<path>/path/to/schema2.xml</path>
<successAction>
<replaceRootController />
</successAction>
<failureAction>
<showAlert>Failed to load schema</showAlert>
</failureAction>
</loadSchema>
</navigation>
</tabs>
</workflow>
这将给我们一个初始窗口,其中包含一个具有两个导航控件的标签控制器,每个导航控制器都包含一个标题为"Loading..."的临时屏幕和一个也表示"Loading..."的标签。当屏幕加载时,加载并使用新的模式来替换临时屏幕。
通过这种方式,应用程序中的每个屏幕都可以写成顶级控制器。
样式由iOS内建的UIAppearance协议处理。这可以通过代码或使用出色的UISS项目来实现,该项目允许你在集中式的JSON文件中定义样式。
UIAppearance按类别进行操作,但常常希望不同类别的对象有不同的样式。WorkflowSchema在某种程度上支持这一点,通过允许用户在对象标签上设置class
属性,这会修改创建的对象的类别。例如
<button>This is a normal button</button>
<button class="submit">This is a submit button</button>
<button class="cancel">This is a cancel button</button>
第一个标签指示框架创建一个WFSButton
类的对象。第二个指示框架动态创建一个名为WFSButton.submit
的WFSButton
类的子类(如果尚未创建),然后创建该类的对象。第三个为WFSButton.cancel
做同样的事。这意味着你可以设置UISS规则,如下所示
"WFSButton" : {
"backgroundImage" : [["button_bg", 11, 4, 12, 4], "normal"],
"contentEdgeInsets" : [[12, 4, 12, 4]],
},
"WFSButton.submit" : {
"backgroundImage" : [["submit_button_bg", 11, 4, 12, 4], "normal"],
},
"WFSButton.cancel" : {
"backgroundImage" : [["cancel_button_bg", 11, 4, 12, 4], "normal"],
},
...并且它将正确设置背景图像。
使用此功能有一些注意事项:首先,如果您正在使用UISS(您应该使用它),那么每次加载一个模式后,您都需要调用configureWithDefaultJSONFile
,因为类是动态创建的,且UISS不会为还不存在的类设置样式。
其次,尽管它看起来非常像CSS类,当您只有一个时它的表现也像CSS类,但它不是CSS类。您不能仅仅基于类属性进行匹配,并且不能有多个类。因此,这样做是不行的
<button class="big cancel">This is a big cancel button</button>
<button class="big submit">This is a big submit button</button>
"WFSButton.big" : {
"contentEdgeInsets" : [[24, 8, 24, 8]],
},
"WFSButton.cancel" : {
"backgroundImage" : [["cancel_button_bg", 11, 4, 12, 4], "normal"],
},
"WFSButton.big.submit" : {
"backgroundImage" : [["big_submit_button_bg", 11, 4, 12, 4], "normal"],
},
...因为您创建了一个名为WFSButton.big cancel
的类,它既不是从WFSButton.big
继承,也不是从WFSButton.cancel
继承,还创建了一个名为WFSButton.big submit
的类,它不是从WFSButton.big.submit
继承的(这样的类不存在)。
这样也是不行的
<button class="big">This is a big button</button>
<label class="big">This is a big label</button>
".big" : {
"font" : "Big-font",
},
...因为您创建了两个类,分别是WFSButton.big
和WFSLabel.big
,它们都没有继承自.big
(这样的类不存在)。