PresentationFramework 1.15.1

PresentationFramework 1.15.1

Måns BernhardtMåns BernhardtNataliya PatsovskaiZettle iOS OSS 维护。



  • PayPal Inc.

Build Status Platforms Carthage Compatible

Presentation是一个iOS Swift库,用于更结构化的处理UI展示,侧重于

  • 从模型到结果的驱动过程形式化。
  • 引入工具和模式以改善关注点分离。
  • 提供展示视图控制器及其生命周期管理的便捷性。

Presentation虽然灵活,但也有自己的观点,并有建设展示的首选方式

  • 明确关于模型数据和结果。
  • 以编程方式构建和布局UI。
  • 使用响应式编程处理事件。
  • 更喜欢小型可重用组件和扩展,而不是子类化。

Presentation框架在处理事件,异步流程和生命周期管理方面大量依赖于 Flow框架

示例用法

// Declare the model and data needed to present the messages UI
struct Messages {
  var messages: ReadSignal<Message>
}

// Conform to Presentable and implement materialize to produce  
// a UIViewController and a Disposable for life-time management
extension Messages: Presentable {
  func materialize() -> (UIViewController, Disposable) { 
    // Setup viewController and views
    let viewController = UITableViewController()

    // Set up event handlers
    let bag = DisposeBag()
    bag += messages.atOnce().onValue { // update table view }
    
    return (viewController, bag)
  }
}   

/// Create an instance and present it
let messages = Messages(...)
presentingViewController.present(messages)

内容

需求

  • Xcode 9.3+
  • Swift 4.1
  • iOS 9.0+

安装

Carthage

github "iZettle/Presentation" >= 1.0

Cocoa Pods

platform :ios, '9.0'
use_frameworks!

target 'Your App Target' do
  pod 'PresentationFramework', '~> 1.0'
end

用法

展示基于几种核心类型和方法

  • Presentable:将展示模型实体化,以及展示的结果。
  • present():展示模型的视图控制器方法。
  • PresentationStyle:展示风格,如 modal 或 popover。
  • Presentation:将展示模型与展示参数和动作包装在一起。
  • AnyPresentable:类型擦除的 Presentable。

Presentable

展示鼓励你提前声明一个展示模型,在这里所有数据都提供用于设置、更新和完成展示。

struct Messages {
  var messages: ReadSignal<Message>
  var filter: (String?) -> ()
  var refresh: () -> Future<()>
}

将这个模型遵循 Presentable 协议来描述如何将模型实体化成展示内容以及展示的结果。

extension Messages: Presentable {
  func materialize() -> (UIViewController, Disposable) { 
    // Setup view controller and views
    let viewController = UITableViewController()

    // Set up event handlers
    let bag = DisposeBag()
    bag += messages.atOnce().onValue { // update table view }
    
    return (viewController, bag)
  }
}   

Presentable 协议定义为

public protocol Presentable {
  associatedtype Matter
  associatedtype Result

  func materialize() -> (Matter, Result)
}

在这里 Matter 通常是一个 UIViewController 或者是其子类,展示的结果通常是 Flow 的三种核心类型之一。

  • Disposable - 对于不能自行完成的展示。
  • Future - 对于只能由自身完成一次的展示。
  • Signal - 对于必须传达完成而不是明确撤销的展示。

当外部的展示对象取消展示,如在一个标签栏中或从菜单中展示时,返回一个Disposable是很常见的。对于上面的Messages模型来说也是如此。

但是,如果展示对象将要驱动完成过程,例如在收集一个表单中的数据时,通常返回一个Future

struct ComposeMessage { }

extension ComposeMessage: Presentable {
  func materialize() -> (UIViewController, Future<Message>) {
    // Setup view controller and views
    let viewController = UIViewController(...)
    viewController.view = ...

    return (viewController, Future { completion in
      // Setup event handlers
      let bag = DisposeBag()
      
      bag += postButton.onValue { 
        completion(.success(Message(...))
      }
      
      return bag
    })
  }
}

尽管我们的消息编写模型没有数据,我们仍然定义一个类型以便引用它,并使其符合Presentable。这里返回的未来将在成功后完成一个编写的消息。

最后,返回一个Signal表示展示可能会多次发出结果,例如当在导航栈上展示时,发出信号将推送下一个视图控制器,但我们可以回退到前一个。

例如,Messages可以潜在地更新为返回一个Signal,以表明应该显示消息的详细信息。

extension Messages: Presentable {
  func materialize() -> (UIViewController, Signal<Message>) { ... }
}   

展示

一旦你的展示模型符合了Presentable,你就可以方便地从另一个视图控制器展示它。

let messages = Messages(...)
presentingController.present(messages)

调用present()将返回一个可以用于终止展示的未来对象。

let future = presentingController.present(messages)

future.cancel() // Abort and dismiss the presentation

你还可以在一个窗口上展示一个展示,以便将其设置为根控制器。

bag += window.present(messages)

如果展示返回一个未来或信号,present()也将返回一个,让你可以检索结果。

let compose = ComposeMessage(...)
presentingController.present(compose).onValue { message in
  // Called with a composed message on dismiss
}

定制

present()方法接受三个默认参数 - styleoptionsconfigure,你可以提供它们来定制展示。

controller.present(messages, style: .modal, options: [ ... ]) { vc, bag in
  // Customize view controller and use bag to add presentation activities 
}

样式

展示提供了几种PresentationStyle,如defaultmodalpopoverembed。但你也可以扩展以添加你自己的样式。

extension PresentationStyle {
  static let customStyle = PresentationStyle(name: "custom") { 
    viewController, presentingController, options in
      // Run and animate presentation and define how to dismiss  
      return (result, dismiss)
    }
}

选项

您还可以提供选项来自定义演示文稿。演示文稿包含几种PresentationOptions,例如embedInNavigationControllerrestoreFirstRespondershowInMaster。您还可以为您的自定义演示文稿样式添加更多选项

extension PresentationOptions {
  static let customOption = PresentationOptions()
}  

配置

最后,您可以可选地提供一个配置闭包,该闭包将一起与要将视图控制器一起呈现的视图控制器以及一个DisposeBag调用,它允许您添加在演示文稿期间保持活动的活动

controller.present(messages) { vc, bag in
  // Customize view controller and use bag to add presentation activities 
}

添加一个关闭按钮来关闭演示文稿很有用,而演示文稿添加了一个便利函数,用于将这些按钮添加到呈现的视图控制器中

let done = UIBarButtonItem(...)
controller.present(messages, configure: installDismiss(done))

演示文稿

Presentation类型将可呈现的内容与演示文稿参数和调用进行了打包,一旦被呈现或取消呈现。一个Presentation可以像Presentable一样呈现,但无需任何显式的演示文稿参数

let compose = Presentation(ComposeMessage(), style: ..., options: ..., configure: ...)

presentingController.present(compose)

通过在我们的可呈现的模型数据中使用Presentation,我们将使它从知道如何构建和呈现其他可呈现的内容中解脱出来

struct Messages {
  let messages: ReadSignal<[Message]>
  let composeMessage: Presentation<ComposeMessage>
}

使用演示文稿是解开两个UI的一种很好的方式,消除了需要转发任何构造和呈现其他演示文稿所需模型数据的需求

到目前为止,我们已经展示了如何定义和实现我们的可呈现内容,但没有展示如何初始化它们。重要的是要认识到,初始化可呈现内容所需的数据并不是可呈现内容本身必须了解的数据。为了使初始化和呈现的解耦更加明显,可以将这些内容分离到不同的文件中,甚至可能分离到不同的模块中。初始化器,例如,可以访问可呈现自身模块中不可用的资源。

extension Messages {
  init(messages: [Message]) {
    let messagesSignal = ReadWriteSignal(messages)
    self.messages = messagesSignal.readOnly()
    
    let compose = Presentation(ComposeMessage(), style: .modal)
    self.composeMessage = compose.onValue { message in
      messagesSignal.value.insert(message, at: 0)
    }
  }
}

在这里,我们可以看到Presentation还允许添加在呈现和取消呈现时调用的操作和转换。在这种情况下,我们将设置一个操作,在成功关闭组合演示文稿时执行,此时将使用新创建的消息。新消息将简单地添加到我们的messagesSignal中,这将向我们的Messages可呈现发出信号以更新其表格视图。

重要的是要认识到,上述对Messages的初始化只是许多潜在实现之一。在更现实的示例中,我们可能会包括网络请求和某种类型的持久存储,如CoreData。但无论我们选择如何实现,都不会影响我们的可呈现类型及其实现。我们甚至可能有针对不同目的的多个初始化器,例如生产、单元测试和示例应用程序。

将可呈现对象视为一种演示某种内容的方法,就好比一份食谱。拥有一个可呈现对象的实例并不意味着已经构建了任何UI。用户可能永远不会选择创建消息,或者创建超过一个。但我们的可创建可呈现对象实例仍然只有一个。

类型擦除的 Presentable

有时匿名化一个可呈现对象是有用的,例如,能够将不同的可呈现对象无差别地传递给API。

let anonymized = AnyPresentable(message) // AnyPresentable<UIViewController, Disposable>

因为 AnyPresentable 只是一个类型擦除的 Presentable,它可以与 Presentation 一起使用。例如,您可以用它来包装一些您认为不需要添加模型的老旧视图控制器。

let presentation = DisposablePresentation {
  let vc = storyboard.instantiateViewController(...)
  let bag = DisposeBag()
  // setup vc and potential activities
  
  return (vc, bag)
}

其中 DisposablePresentation 只是

typealias DisposablePresentation = AnyPresentation<UIViewController, Disposable>

AnyPresentation 作为一个类型别名是

typealias AnyPresentation<Context: UIViewController, Result> = Presentation<AnyPresentable<Context, Result>>

警报

Presentation 内置了一个 Alert 可呈现对象,使得处理警报和操作表更加便捷。

let alert = Alert<Bool>(title: ..., message: ..., actions: 
  Action(title: "Yes") { true }
  Action(title: "No") { false }
  Action(title: "Cancel", style: .cancel) { throw CancelError() },
)

// Display as an alert
presentingController.present(alert).onValue { boolean in
  // Selected yes or no 
}.onError { error in
  // Did cancel
}

// Display as an action sheet
presentingController.present(alert, style: .sheet())

警报还支持字段的编辑。

var add = Alert<URL>.Action(title: "Add") { URL(string: $0[0])! }
add.enabledPredicate = { isValidURL($0[0]) }
let alert = Alert(title: "Add server", fields: [.Field()], actions: add)

presentingController.present(alert).onValue { url in ... }

主-从

Presentation 还内置了一些辅助类型,使处理类似分割视图控制器的主-从UI更加容易。

  • KeepSelection:在一次中保持一个选择。
  • MasterDetailSelection:扩展 KeepSelection 来处理可折叠视图。
  • DualNavigationControllersSplitDelegate:导航控制器之间的协调。

表单框架

我们强烈建议您也了解一下 表单框架。表单和 Presentation 是紧密开发的,它们共享许多相同的设计哲学。

现场测试

该演示已在过去几年中开发和测试,并在 iZettle 的高评价的销售点应用程序中广泛使用。

协作

您可以通过我们的 Slack 工作区与我们协作。提出问题,分享想法,或者可能只是参与正在进行中的讨论。要获取邀请,请给我们写信至 [email protected]