CardParts 4.0.0

CardParts 4.0.0

Chase RoossinLucien DupontBadarinath Venkatnarayansetty 维护。



 
依赖
RxSwift~> 6.2
RxCocoa~> 6.2
RxDataSources~> 5.0
RxGesture~> 4.0
 

CardParts 4.0.0

  • Chase Roossin,Bharath Urs,Lucien Dupont 和 Badarinath Venkatnarayansetty 编写

Mint Logo

Build Status Version License Platform

MintSights by CardParts CardParts in Mint CardParts in Turbo

CardParts - 由❤️Intuit 制作

示例

要运行示例项目,请克隆仓库,然后首先从示例目录中运行 pod install

ViewController.swift 中,您可以通过取消注释其中一个 loadCards(cards: ) 函数来更改显示的卡片及其顺序。如果您想更改任何这些卡片的文字内容,您可以考虑每个传递给函数的 CardPartsViewController,例如:TestCardControllerThing1CardControllerThing2CardController 等。

需求

  • iOS 10.0+
  • Xcode 10.2+
  • Swift 5.0+
  • CocoaPods 1.6.1+

安装

CardParts 通过 CocoaPods 提供。你可以使用以下命令安装

$ gem install cocoapods

要将 CardParts 添加到你的项目中,只需将以下行添加到你的 Podfile 中

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'CardParts'
end

然后,运行以下命令

$ pod install

沟通和贡献

  • 如果 你需要帮助,请创建一个 issue 并标记为 help wanted
  • 如果 你发现了一个错误,请创建一个 issue 并标记为 bug
  • 如果你有 功能请求,请创建一个 issue 并标记为 feature
  • 如果你想 做出贡献,请提交一个 pull request。
    • 为了提交 pull request,请fork 此仓库,并从你的 forked 仓库提交 PR。
    • 在你的 PR 中提供关于此 PR 修复/增强/添加的详细信息。
    • 每个 PR 必须得到我们团队的两个批准后,我们才会合并。

概览

CardParts 是 iOS Mint 应用程序的下一代 Card UI 框架。此版本对原始的 Card 部分框架进行了许多更新,包括改进的 MVVM、数据绑定(通过 RxSwift)、使用堆叠视图和自适应集合视图替代固定尺寸的 cell、100% Swift 等等。结果是框架更简单、更容易使用、更强大,且更容易维护。该框架目前由 iOS Mint 应用程序和 iOS Turbo 应用程序使用。

CardPart Example in Mint

快速开始

了解如何在遵循MVVM设计模式的同时快速在屏幕上显示卡片。

import RxCocoa

class MyCardsViewController: CardsViewController {

    let cards: [CardController] = [TestCardController()]

    override func viewDidLoad() {
        super.viewDidLoad()

        loadCards(cards: cards)
    }
}

class TestCardController: CardPartsViewController  {

    var viewModel = TestViewModel()
    var titlePart = CardPartTitleView(type: .titleOnly)
    var textPart = CardPartTextView(type: .normal)

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.title.asObservable().bind(to: titlePart.rx.title).disposed(by: bag)
        viewModel.text.asObservable().bind(to: textPart.rx.text).disposed(by: bag)

        setupCardParts([titlePart, textPart])
    }
}

class TestViewModel {

    var title = BehaviorRelay(value: "")
    var text = BehaviorRelay(value: "")

    init() {

        // When these values change, the UI in the TestCardController
        // will automatically update
        title.accept("Hello, world!")
        text.accept("CardParts is awesome!")
    }
}

注意: 使用 BehaviorRelay 需要 RxCocoa,因此您必须在其使用的任何地方导入它。

架构

卡片框架包含两个主要部分。第一部分是 CardsViewController,它负责显示卡片,并按正确顺序显示卡片并管理卡片的生存周期。第二大部分是卡片本身,它们通常是 CardPartsViewController 的实例。每个 CardPartsViewController 实例显示一张单独卡片的内容,使用一个或多个卡片部件(更多详情稍后说明)。

CardsViewController

CardsViewController 使用一个集合视图,其中每个单元格代表一个数字。单元格将渲染卡片的框架,但设计为具有子 ViewController,它将显示卡片的内容。因此,CardsViewController 实质上是一个子 ViewController 的列表,这些控制器以特殊单元格的形式渲染,这些单元格绘制卡片框架。

要使用 CardsViewController,您首先需要对其进行子类化。然后在 viewDidLoad 方法中调用基类 loadCards 方法,并传入一个 CardController 数组。每个 CardController 实例将被渲染为单独的卡片。通过为每个 CardController 获取视图控制器并将它们作为子视图控制器添加到卡单元格中,loadCards 方法完成这项任务。以下是一个示例

class TestCardsViewController: CardsViewController {

    let cards: [CardController] = [TestCardController(), AnotherTestCardController()]

    override func viewDidLoad() {
        super.viewDidLoad()

        loadCards(cards: cards)
    }
}

每个卡片都必须实现 CardController 协议(注意,稍后讨论的 CardPartsViewController 已经实现了此协议)。CardController 协议有单一方法。

protocol CardController : NSObjectProtocol {

    func viewController() -> UIViewController

}

viewController() 方法必须返回将要作为子控制器添加到卡片单元格中的 viewController。如果 CardController 是 UIViewController,它可以简单地为此方法返回 self。

加载特定卡片

虽然通常你可以通过调用loadCards(cards:)来加载一组CardControllers,但你可能希望能够重新加载特定的卡片集。我们通过APIloadSpecificCards(cards: [CardController] , indexPaths: [IndexPath])提供了这种能力。只需传递完整的卡片数组以及你想重新加载的indexPaths。

自定义卡片外边距

默认情况下,你的CardsViewController的外边距将与主题的cardCellMargins属性匹配。你可以通过应用一个新的主题或设置CardParts.theme.cardCellMargins = UIEdgeInsets(...)来改变你应用中所有CardsViewController的外边距。或者,如果你想仅改变一个CardsViewController的外边距,你可以设置该CardsViewControllercardCellMargins属性。要改变单个卡片的外边距,请参阅CustomMarginCardTrait。如果没有指定新值,该属性将默认使用主题的外边距。更改此值应在自定义CardsViewControllerinit中进行,但必须在调用super.init之后进行,因为它是更改基类的属性。例如

class MyCardsViewController: CardsViewController {

	init() {
		// set up properties
		super.init(nibName: nil, bundle: nil)
		self.cardCellMargins = UIEdgeInsets(/* custom card margins */)
	}

	...
}

如果你的Storyboard中使用CardsViewController子类,当调用required init(coder:)初始化器时,cardCellMargins属性将取CardParts.theme.cardCellMargins的值。如果你试图改变整个应用的主题,你需要在Storyboard中第一个将被初始化的视图控制器的这个初始化器中这样做,变化将影响到所有其他视图控制器。例如

required init?(coder: NSCoder) {
	YourCardPartTheme().apply()
	super.init(coder: coder)
}

卡片特征

Card Parts框架定义了一系列 traits,可以用来修改卡片的外观和行为。这些 traits 被实现为协议和协议扩展。要将 trait 添加到卡片中,只需将 trait 协议添加到 CardController 定义中。例如

class MyCard: UIViewController, CardController, TransparentCardTrait {

}

MyCard 现在将以透明卡片背景和边框的形式渲染自己。不需要任何额外的代码,只需添加 TransparentCardTrait 作为协议即可。

大部分 traits 无需额外代码。框架中实现的默认协议扩展实现了特性修改卡片所需的所有代码。少数 traits 需要实现一个函数或属性。请参阅每个 trait 的文档以获取更多信息。

NoTopBottomMarginsCardTrait

默认情况下,每个卡片在其框架顶部和底部都添加了边距。添加 NoTopBottomMarginsCardTrait 特性将移除该边距,允许卡片在其框架内部使用整个空间进行渲染。

TransparentCardTrait

每个卡片都通过绘制边框来渲染框架。添加 TransparentCardTrait 不会显示该边框,将允许卡片无框架进行渲染。

EditableCardTrait

如果添加了 EditableCardTrait 特性,卡片将在上右角渲染一个编辑按钮。当用户点击编辑按钮时,框架将调用卡片的 onEditButtonTap() 方法。EditableCardTrait 协议要求 CardController 实现 onEditButtonTap() 方法。

HiddenCardTrait

HiddenCardTrait 特性要求 CardController 实现一个 isHidden 变量。

    var isHidden: BehaviorRelay<Bool> { get }

然后框架将观察 isHidden 变量,以便在其值更改时根据新值隐藏或显示卡片。这允许 CardController 通过简单地修改其 isHidden 变量的值来控制其可见性。

ShadowCardTrait

ShadowCardTrait 协议要求 CardController 实现 shadowColor()shadowRadius()shadowOpacity()shadowOffset() 方法。

    func shadowColor() -> CGColor {
        return UIColor.lightGray.cgColor
    }

    func shadowRadius() -> CGFloat {
        return 10.0
    }

    // The value can be from 0.0 to 1.0.
    // 0.0 => lighter shadow
    // 1.0 => darker shadow
    func shadowOpacity() -> Float {
        return 1.0
    }

    func shadowOffset() -> CGSize {
    	return CGSize(width: 0, height: 5)
    }

shadowColor: lightGray, shadowRadius: 5.0, shadowOpacity: 0.5
Shadow radius 5.0

shadowColor: lightGray, shadowRadius: 10.0, shadowOpacity: 0.5
Shadow radius 10.0

RoundedCardTrait

使用此协议通过实现 cornerRadius 方法来定义卡片圆角。

    func cornerRadius() -> CGFloat {
        return 10.0
    }

cornerRadius: 10.0
Shadow radius 5.0

GradientCardTrait

使用此协议为卡片添加渐变背景。渐变将垂直从上到下添加。可选地,您可以给渐变应用一个角度。角度以度为单位定义,任何正负值都是有效的。

    func gradientColors() -> [UIColor] {
        return [UIColor.lavender, UIColor.aqua]
    }

    func gradientAngle() -> Float {
        return 45.0
    }

Shadow radius 10.0

BorderCardTrait

使用此协议为卡片添加边框颜色和边框宽度,实现 borderWidthborderColor 方法。

    func borderWidth() -> CGFloat {
        return 2.0
    }

    func borderColor() -> CGColor {
        return UIColor.darkGray.cgColor
    }

border

CustomMarginCardTrait

使用此协议指定卡片的自定义边距,实现 customMargin 方法。返回的值将用于左右边距,从而在父视图中居中卡片。

    func customMargin() -> CGFloat {
        return 42.0
    }

CardPartsViewController

CardPartsViewController 实现了 CardController 协议,并通过使用包含自动数据绑定的 MVVM 模式来显示一个或多个卡片部分视图来构建其卡片 UI。每个 CardPartsViewController 显示其子视图 CardPartView 的列表。每个 CardPartView 都渲染为卡片中的一个行。CardParts 框架实现了几种不同类型的 CardPartView,用于显示基本视图,如标题、文本、图像、按钮、分隔符等。框架中实现的所有 CardPartView 都已经过样式设计,以正确匹配应用的 UI 指引。

除了卡片部件外,CardPartsViewController 还使用视图模型来公开与卡片部件绑定数据属性。视图模型应包含卡片的全部业务逻辑,从而仅将 CardPartsViewController 的角色保留为创建视图部件并将视图模型绑定到卡片部件。一个简单的基于 CardPartsViewController 的实现可能如下所示

class TestCardController: CardPartsViewController  {

    var viewModel = TestViewModel()
    var titlePart = CardPartTitleView(type: .titleOnly)
    var textPart = CardPartTextView(type: .normal)

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.title.asObservable().bind(to: titlePart.rx.title).disposed(by: bag)
        viewModel.text.asObservable().bind(to: textPart.rx.text).disposed(by: bag)

        setupCardParts([titlePart, textPart])
    }
}

class TestViewModel {

    var title = BehaviorRelay(value: "")
    var text = BehaviorRelay(value: "")

    init() {

        // When these values change, the UI in the TestCardController
        // will automatically update
        title.accept("Hello, world!")
        text.accept("CardParts is awesome!")
    }
}

上述示例创建了一个显示两个卡片部件的卡片,分别是标题卡片部件和文本部件。bind 调用设置 View模型属性和卡片部件视图属性之间的自动数据绑定,以便每当视图模型属性更改时,卡片部件视图将自动使用正确数据更新。

调用 setupCardParts 将卡片部件视图添加到卡片中。它接受一个 CardPartView 数组,指定要显示哪些卡片部件及其显示顺序。

CardPartsFullScreenViewController

这样会将卡片变成全屏视图控制器。因此,如果不想使用卡片数组构建,也可以创建一个单独的全屏卡片。

class TestCardController: CardPartsFullScreenViewController  {
    ...
}

CardParts

框架包括一些预定义的卡片部件,可以立即使用。还可以创建自定义卡片部件。以下几节列出了所有预定义卡片部件及其可以绑定到视图模型上的响应式属性。

CardPartTextView

CardPartTextView 用于显示单个文本字符串。这些字符串可以换行。CardPartTextView 的初始化器接受一个类型参数,可以是 normal、title 或 detail。类型用于设置文本的默认字体和 textColor。

CardPartTextView 公开以下可以绑定到视图模型属性的响应式属性

var text: String?
var attributedText: NSAttributedString?
var font: UIFont!
var textColor: UIColor!
var textAlignment: NSTextAlignment
var lineSpacing: CGFloat
var lineHeightMultiple: CGFloat
var alpha: CGFloat
var backgroundColor: UIColor?
var isHidden: Bool
var isUserInteractionEnabled: Bool
var tintColor: UIColor?

CardPartAttributedTextView

CardPartAttributedTextView 与 CardPartTextView 可比较,但它基于 UITextView 而非 UILabel 构建。这使得 CardPartImageViews 能够嵌套在 CardPartAttributedTextView 中,并且文本可以围绕这些嵌套的图片包裹。此外,CardPartAttributedTextView 允许设置和打开链接。CartPartAttributedTextView公开以下可以绑定到视图模型属性的响应式属性:

var text: String?
var attributedText: NSAttributedString?
var font: UIFont!
var textColor: UIColor!
var textAlignment: NSTextAlignment
var lineSpacing: CGFloat
var lineHeightMultiple: CGFloat
var isEditable: Bool
var dataDetectorTypes: UIDataDetectorTypes
var exclusionPath: [UIBezierPath]?
var linkTextAttributes: [NSAttributedString.Key : Any]
var textViewImage: CardPartImageView?
var isUserInteractionEnabled: Bool
var tintColor: UIColor?

CardPartImageView

CardPartImageView显示单个图像。CardPartImageView公开以下可以绑定到视图模型属性的响应式属性

var image: UIImage?
var imageName: String?
var alpha: CGFloat
var backgroundColor: UIColor?
var isHidden: Bool
var isUserInteractionEnabled: Bool
var tintColor: UIColor?

CardPartButtonView

CardPartButtonView显示单个按钮。

CardPartButtonView公开以下可以绑定到视图模型属性的响应式属性

var buttonTitle: String?
var isSelected: Bool?
var isHighlighted: Bool?
var contentHorizontalAlignment: UIControlContentHorizontalAlignment
var alpha: CGFloat
var backgroundColor: UIColor?
var isHidden: Bool
var isUserInteractionEnabled: Bool
var tintColor: UIColor?

CardPartTitleView

CardPartTitleView显示包含标题的可选选项菜单的视图。初始化器需要一个类型参数,可以设置为 titleOnly 或 titleWithMenu。如果类型设置为 titleWithMenu,卡片部分将显示菜单图标,点击时将显示包含在 menuOptions 数组中指定的选项的菜单。可以将 menuOptionObserver 属性设置为在用户从菜单中选择项时被调用的块。

以下是一个带有菜单按钮的标题示例

let titlePart = CardPartTitleView(type: .titleWithMenu)
titlePart.menuTitle = "Hide this offer"
titlePart.menuOptions = ["Hide"]
titlePart.menuOptionObserver  = {[weak self] (title, index) in
    // Logic to determine which menu option was clicked
    // and how to respond
    if index == 0 {
        self?.hideOfferClicked()
    }
}

CardPartButtonView公开以下可以绑定到视图模型属性的响应式属性

var title: String?
var titleFont: UIFont
var titleColor: UIColor
var menuTitle: String?
var menuOptions: [String]?
var menuButtonImageName: String
var alpha: CGFloat
var backgroundColor: UIColor?
var isHidden: Bool
var isUserInteractionEnabled: Bool
var tintColor: UIColor?

CardPartTitleDescriptionView

CardPartTitleDescriptionView允许您有左标题和描述标签,但是您也可以选择右边的标题/描述标签的对齐方式。见下文

let rightAligned = CardPartTitleDescriptionView(titlePosition: .top, secondaryPosition: .right) // This will be right aligned
let centerAligned = CardPartTitleDescriptionView(titlePosition: .top, secondaryPosition: .center(amount: 0)) // This will be center aligned with an offset of 0.  You may increase that amount param to shift right your desired amount

CardPartPillLabel

CardPartPillLabel为您提供了圆角、文本水平居中对齐以及垂直和水平填充的功能。

var verticalPadding:CGFloat
var horizontalPadding:CGFloat

pillLabel

请查看示例应用以获取实际使用示例。

CardPartIconLabel

CardPartIconLabel提供添加图像并支持左、右和中心文本对齐以及图标关联功能。

    let iconLabel = CardPartIconLabel()
    iconLabel.verticalPadding = 10
    iconLabel.horizontalPadding = 10
    iconLabel.backgroundColor = UIColor.blue
    iconLabel.font = UIFont.systemFont(ofSize: 12)
    iconLabel.textColor = UIColor.black
    iconLabel.numberOfLines = 0
    iconLabel.iconPadding = 5
    iconLabel.icon = UIImage(named: "cardIcon")

cardPartIconLabel

CardPartSeparatorView

CardPartSeparatorView显示分割线,未为CardPartSeparatorView定义响应式属性。

CardPartVerticalSeparatorView

正如其名所示,它显示的是垂直分割视图而不是水平分割视图。

CardPartStackView

CardPartStackView显示一个可以包含其他card parts,甚至是其他CardPartStackViews的UIStackView。使用CardPartStackView可以创建card parts的自定义布局。通过嵌套CardPartStackViews,您可以创建几乎任何布局。

要将card part添加到Stack视图中,请调用其addArrangedSubview方法,指定card part的view属性为要添加到stack视图中的视图。例如

horizStackPart.addArrangedSubview(imagePart)

还提供了option来为stack视图圆角

let roundedStackView = CardPartStackView()
roundedStackView.cornerRadius = 10.0
roundedStackView.pinBackground(roundedStackView.backgroundView, to: roundedStackView)

roundedStackView

未为CardPartStackView定义响应式属性。但是您可以使用默认的UIStackView属性(分布、对齐、间距和轴)来配置stack视图。

CardPartTableView

CardPartTableView(卡片部分表格视图)会将表格视图呈现为一个卡片部分,使得表格视图中的所有项目都在卡片部分中显示(即表格视图不滚动)。CardPartTableView利用了Bond的响应式数据源支持,允许将可变观察数组绑定到表格视图中。

要设置数据源绑定,视图模型类应该暴露包含表格视图数据的可变观察数组属性。例如:

var listData = MutableObservableArray(["item 1", "item 2", "item 3", "item 4"])

然后在视图控制器中,可以按照以下方式设置数据源绑定:

viewModel.listData.bind(to: tableViewPart.tableView) { listData, indexPath, tableView in

    guard let cell = tableView.dequeueReusableCell(withIdentifier: tableViewPart.kDefaultCellId, for: indexPath) as? CardPartTableViewCell else { return UITableViewCell() }

    cell.leftTitleLabel.text = listData[indexPath.row]

    return cell
}

bind调用中的最后一个参数是当表格视图的cellForRowAt数据源方法被调用时将被调用的块。块的第一个参数是正在绑定的可变观察数组。

CardPartTableView注册了一个默认的单元格类(CardPartTableViewCell),无需额外工作即可使用。CardPartTableViewCell包含4个标签,一个左对齐的标题,一个左对齐的描述,一个右对齐的标题和一个右对齐的描述。每个标签都可以选择使用,如果标签中没有指定文本,单元格的布局代码将正确地排列剩余的标签。

您还可以通过在tableViewPart.tableView上调用register方法来注册自己的自定义单元格。

您还可以访问tableView调用如下两种代理方法:

@objc public protocol CardPartTableViewDelegate {
	func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
	@objc optional func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
}

CardPartTableViewCell

CardPartTableViewCell是CardPartTableView的默认注册单元格。该单元格包含以下属性:

var leftTitleLabel: UILabel
var leftDescriptionLabel: UILabel
var rightTitleLabel: UILabel
var rightDescriptionLabel: UILabel
var rightTopButton: UIButton
var shouldCenterRightLabel = false
var leftTitleFont: UIFont
var leftDescriptionFont: UIFont
var rightTitleFont: UIFont
var rightDescriptionFont: UIFont
var leftTitleColor: UIColor
var leftDescriptionColor: UIColor
var rightTitleColor: UIColor
var rightDescriptionColor: UIColor

CardPartTableViewCardPartsCell

这将使您能够从CardParts创建自定义UITableView单元格。以下代码允许您创建一个单元格:

class MyCustomTableViewCell: CardPartTableViewCardPartsCell {

    let bag = DisposeBag()

    let attrHeader1 = CardPartTextView(type: .normal)
    let attrHeader2 = CardPartTextView(type: .normal)
    let attrHeader3 = CardPartTextView(type: .normal)

    override public init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        selectionStyle = .none

        setupCardParts([attrHeader1, attrHeader2, attrHeader3])
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setData(_ data: MyCustomStruct) {
        // Do something in here
    }
}

如果您创建了自己的单元格,则必须将其注册到CardPartTableView中。

tableViewCardPart.tableView.register(MyCustomTableViewCell.self, forCellReuseIdentifier: "MyCustomTableViewCell")

然后,您就像平常一样,绑定到viewModel的数据。

viewModel.listData.bind(to: tableViewPart.tableView) { tableView, indexPath, data in

    guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyCustomTableViewCell", for: indexPath) as? MyCustomTableViewCell else { return UITableViewCell() }

    cell.setData(data)

    return cell
}

CardPartCollectionView

CardPartCollectionView底层的引擎是RxDataSource。你可以查看它们的文档以深入了解,但下面是它工作的大致方法。

首先使用自定义的UICollectionViewFlowLayout初始化一个CardPartCollectionView

lazy var collectionViewCardPart = CardPartCollectionView(collectionViewLayout: collectionViewLayout)
var collectionViewLayout: UICollectionViewFlowLayout = {
    let layout = UICollectionViewFlowLayout()
    layout.minimumInteritemSpacing = 12
    layout.minimumLineSpacing = 12
    layout.scrollDirection = .horizontal
    layout.itemSize = CGSize(width: 96, height: 128)
    return layout
}()

现在假设你有一个自定义的结构体想要传递到你的CollectionViewCell中。

struct MyStruct {
    var title: String
    var description: String
}

你需要创建一个新的结构体以便它遵守SectionModelType

struct SectionOfCustomStruct {
    var header: String
    var items: [Item]
}

extension SectionOfCustomStruct: SectionModelType {

    typealias Item = MyStruct

    init(original: SectionOfCustomStruct, items: [Item]) {
        self = original
        self.items = items
    }
}

接下来,创建一个数据源,你将会将其绑定到你的数据上:注意:你还可以创建一个自定义的CardPartCollectionViewCell - 见下文。

let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfCustomStruct>(configureCell: {[weak self] (_, collectionView, indexPath, data) -> UICollectionViewCell in

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)

    return cell
})

最后,将viewModel数据绑定到collectionView及其新建的数据源。

viewModel.data.asObservable().bind(to: collectionViewCardPart.collectionView.rx.items(dataSource: dataSource)).disposed(by: bag)

注意:viewModel.data将会是一个包含SectionOfCustomStruct的响应式数组。:

typealias ReactiveSection = BehaviorRelay<[SectionOfCustomStruct]>
var data = ReactiveSection(value: [])

CardPartCollectionViewCardPartsCell

正如CardPartTableViewCell能够从CardParts中创建tableView cells一样,这样CollectionView也能做到。下面是一个创建自定义CardPartCollectionViewCardPartsCell的示例。

class MyCustomCollectionViewCell: CardPartCollectionViewCardPartsCell {
    let bag = DisposeBag()

    let mainSV = CardPartStackView()
    let titleCP = CardPartTextView(type: .title)
    let descriptionCP = CardPartTextView(type: .normal)

    override init(frame: CGRect) {

        super.init(frame: frame)

        mainSV.axis = .vertical
        mainSV.alignment = .center
        mainSV.spacing = 10

        mainSV.addArrangedSubview(titleCP)
        mainSV.addArrangedSubview(descriptionCP)

        setupCardParts([mainSV])
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setData(_ data: MyStruct) {

        titleCP.text = data.title
        descriptionCP.text = data.description
    }
}

要使用它,你必须将其在viewDidLoad期间注册到CollectionView中,如下所示:

collectionViewCardPart.collectionView.register(MyCustomCollectionViewCell.self, forCellWithReuseIdentifier: "MyCustomCollectionViewCell")

然后,在你的数据源内部,只需简单地抽取这个cell

let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfSuggestedAccounts>(configureCell: {[weak self] (_, collectionView, indexPath, data) -> UICollectionViewCell in

    guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCustomCollectionViewCell", for: indexPath) as? MyCustomCollectionViewCell else { return UICollectionViewCell() }

    cell.setData(data)

    return cell
})

CardPartBarView

CardPartBarView展示一个可以填充到所选百分比的水平柱状图。填充的颜色和百分比都是响应式的。

let barView = CardPartBarView()
viewModel.percent.asObservable().bind(to: barView.rx.percent).disposed(by:bag)
viewModel.barColor.asObservable().bind(to: barView.rx.barColor).disposed(by: bag)

CardPartPagedView

这个CardPart允许你创建一个带有页控制器的横向分页轮播。只需提供你的期望高度和CardPartStackView的数组即可。

let cardPartPages = CardPartPagedView(withPages: initialPages, andHeight: desiredHeight)
cardPartPages.delegate = self

这个CardPart也有一个代理。

func didMoveToPage(page: Int)

它会在用户滑动到另一个页面时触发。

你还可以通过在CardPartPagedView上调用以下函数来自动移动到特定的页面

func moveToPage(_ page: Int)

CardPartSliderView

你可以设置最小和最大值,并将当前设置的数值绑定上。

let slider = CardPartSliderView()
slider.minimumValue = sliderViewModel.min
slider.maximumValue = sliderViewModel.max
slider.value = sliderViewModel.defaultAmount
slider.rx.value.asObservable().bind(to: sliderViewModel.amount).disposed(by: bag)

CardPartMultiSliderView

可以设置最小和最大值、着色色和轨道外颜色

let slider = CardPartMultiSliderView()
slider.minimumValue = sliderViewModel.min
slider.maximumValue = sliderViewModel.max
slider.orientation = .horizontal
slider.value = [10, 40]
slider.trackWidth = 8
slider.tintColor = .purple
slider.outerTrackColor = .gray

CardPartSpacerView

允许您在卡片部分之间添加空间,如果您需要比默认边距更大的空间。用特定高度初始化它

CardPartSpacerView(height: 30)

CardPartTextField

CardPartTextField可以接收类型为CardPartTextFieldFormat的参数,它决定了UITextField的格式。您还可以设置如keyboardTypeplaceholderfonttext等属性。

let amount = CardPartTextField(format: .phone)
amount.keyboardType = .numberPad
amount.placeholder = textViewModel.placeholder
amount.font = dataFont
amount.textColor = UIColor.colorFromHex(0x3a3f47)
amount.text = textViewModel.text.value
amount.rx.text.orEmpty.bind(to: textViewModel.text).disposed(by: bag)

以下是不用的格式

public enum CardPartTextFieldFormat {
    case none
    case currency(maxLength: Int)
    case zipcode
    case phone
    case ssn
}

CardPartOrientedView

CardPartOrientedView允许您创建具有方向感的卡片部分元素列表视图。这类似于CardPartStackView,但此视图可以将元素定位于视图的顶部或底部。当您在水平堆叠视图中使用时,如果需要元素相对于水平堆叠视图中的其他视图以不同的方式定位(顶部排列或底部排列),则非常有用。要查看该元素的示例,请参阅示例应用程序。

支持的定位方式如下

public enum Orientation {
    case top
    case bottom
}

要创建一个定位视图,可以使用以下代码

let orientedView = CardPartOrientedView(cardParts: [<elements to list vertically>], orientation: .top)

将上述定位视图添加到任何卡片部分列表或现有堆叠视图中,以便将您的元素定位在包含视图的顶部或底部。

CardPartCenteredView

CardPartCenteredView 是一种 CardPart,它在手机屏幕上按照比例适当地显示居中卡片部分,同时允许左右两侧的卡部分相应地进行缩放。要创建居中卡片部分,请参考以下示例

class TestCardController : CardPartsViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let rightTextCardPart = CardPartTextView(type: .normal)
        rightTextCardPart.text = "Right text in a label"

        let centeredSeparator = CardPartVerticalSeparator()

        let leftTextCardPart = CardPartTextView(type: .normal)
        leftTextCardPart.text = "Left text in a label"

        let centeredCardPart = CardPartCenteredView(leftView: leftTextCardPart, centeredView: centeredSeparator, rightView: rightTextCardPart)

        setupCardParts([centeredCardPart])
    }
}

CardPartCenteredView 可以接受任何符合 CardPartView 的卡部分作为左侧、中间和右侧组件。要查看居中卡部分的图形示例,请参阅打包在此 cocoapod 中的示例应用程序。

CardPartConfettiView

提供添加各种类型(菱形、星形、混合)和颜色以及不同强度的彩带(confetti)的功能

    let confettiView = CardPartConfettiView()
    confettiView.type  = .diamond
    confettiView.shape = CAEmitterLayerEmitterShape.line
    confettiView.startConfetti()

Confetti

CardPartProgressBarView

提供配置不同颜色和自定义标记,以及其位置的功能,以显示基于提供值的进度。

    let progressBarView = CardPartProgressBarView(barValues: barValues, barColors: barColors, marker: nil, markerLabelTitle: "", currentValue: Double(720), showShowBarValues: false)
    progressBarView.barCornerRadius = 4.0

ProgressBarView

CardPartMapView

提供显示 MapView 和反应性配置位置、地图类型和坐标跨度(缩放)的功能。您还可以直接访问 MKMapView 实例,以便添加注释、将其挂钩到 MKMapViewDelegate 或执行通常与地图一起完成的任何操作。

默认情况下,卡片部分将以 300 个像素的高度渲染,但您可以通过重置 CardPartMapView.intrensicHeight 属性来设置自定义高度。

以下是一个如何反应性地从变化的地址字段设置位置的简单示例(有关正常工作的示例,请参阅示例项目)。

    let initialLocation = CLLocation(latitude: 37.430489, longitude: -122.096260)
    let cardPartMapView = CardPartMapView(type: .standard, location: initialLocation, span: MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))

    cardPartTextField.rx.text
            .flatMap { self.viewModel.getLocation(from: $0) }
            .bind(to: cardPartMapView.rx.location)
            .disposed(by: bag)

MapView

CardPartRadioButton

提供添加可配置的内外圆线宽、颜色以及触摸等功能的单选按钮能力。

    let radioButton = CardPartRadioButton()
    radioButton.outerCircleColor = UIColor.orange
    radioButton.outerCircleLineWidth = 2.0

    radioButton2.rx.tap.subscribe(onNext: {
        print("Radio Button Tapped")
    }).disposed(by: bag)

RadioButton

CardPartSwitchView

提供添加可配置颜色的开关能力。

    let switchComponent = CardPartSwitchView()
    switchComponent.onTintColor = .blue

RadioButton

CardPartHistogramView

能根据数据范围生成自定义条形图、线条、颜色等。

    let dataEntries = self.generateRandomDataEntries()
    barHistogram.width = 8
    barHistogram.spacing = 8
    barHistogram.histogramLines = HistogramLine.lines(bottom: true, middle: false, top: false)
    self.barHistogram.updateDataEntries(dataEntries: dataEntries, animated: true)

Histogram

CardPartsBottomSheetViewController

CardPartsBottomSheetViewController提供显示高度可定制的模态底卷帘能力。最简单的方式,只需要设置你创建用于控制底卷帘内容的视图控制器的contentVC属性。

    let bottomSheetViewController = CardPartsBottomSheetViewController()
    bottomSheetViewController.contentVC = MyViewController()
    bottomSheetViewController.presentBottomSheet()

bottom sheet

CardPartsBottomSheetViewController还可以作为屏幕底部的粘性视图使用,并且可以在任何视图上显示(默认是keyWindow)。例如,以下代码创建了一个粘性视图,允许在下面滚动,并且只能通过编程方式关闭。

    let bottomSheetViewController = CardPartsBottomSheetViewController()
    bottomSheetViewController.contentVC = MyStickyViewController()
    bottomSheetViewController.configureForStickyMode()
    bottomSheetViewController.addShadow()
    bottomSheetViewController.presentBottomSheet(on: self.view)

sticky bottom sheet

还有二十多项其他属性可以设置,以进一步针对您的需求定制底卷帘。您可以使用以下属性配置颜色、高度、手势识别器、处理外观、动画时间和回调函数。

  • var contentVC: UIViewController?:底卷帘内容的视图控制器。应在显示底卷帘之前设置此参数。
  • var contentHeight: CGFloat?:手动设置内容高度。如果没有设置,高度将尝试从contentVC推断。
  • var bottomSheetBackgroundColor: UIColor:底卷帘的背景颜色。默认为白色。
  • var bottomSheetCornerRadius: CGFloat:底卷帘的圆角。默认为16。
  • var handleVC: CardPartsBottomSheetHandleViewController:底部表的顶部圆形把手。可以配置 handleVC.handleHeighthandleVC.handleWidthhandleVC.handleColor
  • var handlePosition: BottomSheetHandlePosition:把手相对于底部表的位置。选项有 .above(bottomPadding).inside(topPadding).none。默认为上方,并添加8个像素的填充。
  • var overlayColor: UIColor:背景覆盖层的颜色。默认为黑色。
  • var shouldIncludeOverlay: Bool:是否包括背景覆盖层。默认为true。
  • var overlayMaxAlpha: CGFloat:背景覆盖层的最大不透明度。当底部表下拉时,会按比例淡出到0。默认为0.5。
  • var dragHeightRatioToDismiss: CGFloat:用户在放手之前必须将底部表下拉的距离,才能触发关闭。默认为0.4。
  • var dragVelocityToDismiss: CGFloat:必须超过的速度,以便在高度比例大于dragHeightRatioToDismiss时关闭底部表。默认为250。
  • var pullUpResistance: CGFloat:底部表抵抗向上拖动的程度。默认5意味着用户每向上拖动5像素,底部表就上升1像素。
  • var appearAnimationDuration: TimeInterval:底部表出现时的动画时间。默认为0.5。
  • var dismissAnimationDuration: TimeInterval:底部表消失时的动画时间。默认为0.5。
  • var snapBackAnimationDuration: TimeInterval:底部表弹回到其高度的动画时间。默认为0.25。
  • var animationOptions: UIView.AnimationOptions:底部表动画的动画选项。默认为UIView.AnimationOptions.curveEaseIn。
  • var changeHeightAnimationDuration: TimeInterval:底部表调整为新高度时的动画时间。默认为0.25。
  • var shouldListenToOverlayTap: Bool:是否在覆盖层上单击时关闭。默认为true。
  • var shouldListenToHandleDrag: Bool:是否应对把手的拖动做出响应。默认为true。
  • var shouldListenToContentDrag: Bool:是否应对内容中的拖动做出响应。默认为true。
  • var shouldListenToContainerDrag: Bool:是否应对容器中的拖动做出响应。默认为true。
  • var shouldRequireVerticalDrag: Bool:是否要求以垂直方向开始拖动。默认为true。
  • var adjustsForSafeAreaBottomInset: Bool:布尔值,指定底部表是否自动将其高度添加到其高度以补偿底部安全区域内边距。默认为true。
  • var didShow: (() -> Void)?:当底部表完成展示时会被调用的回调函数。
  • var didDismiss: ((_ dismissalType: BottomSheetDismissalType) -> Void)?:当底部表完成自己消失时会被调用的回调函数。参数 dismissalType:关于底部表如何被关闭的信息 - .tapInOverlay.swipeDown.programmatic(info)
  • var didChangeHeight: ((_ newHeight: CGFloat) -> Void)?:当底部表高度因拖动或调用 updateHeight 而更改时会被调用的回调函数。
  • var preferredGestureRecognizers: [UIGestureRecognizer]?:应阻止底部表垂直拖动的手势识别器。如果为nil,将自动查找和使用所有手势识别器;否则,将使用数组中的识别器。默认为空数组。

如果你更改了contentVCcontentHeight属性,底部表将自动更新其高度。你也可以调用updateHeight()来触发高度的更新(这主要用于当contentVC的内容已更改且你希望底部表更新以匹配新内容大小时)。

因为从contentVC访问底部的视图控制器不常见,我们定义了一个带有默认实现的CardPartsBottomSheetDelegate,用于更新新的contentVCcontentHeight,更新高度或通过对底部视图进行程序性操作来关闭底部视图。为了使用此委托及其默认函数实现,请让您的类遵从CardPartsBottomSheetDelegate,并定义一个var bottomSheetViewController: CardPartsBottomSheetViewController。然后,将此类设置为您的视图控制器的委托,并通过委托与底部视图进行交互。

CardPartVideoView

能够将AVPlayer嵌入到卡片视图中。

guard let videoUrl = URL(string: "https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4")  else  { return }
let cardPartVideoView = CardPartVideoView(videoUrl: videoUrl)

如果您需要访问底层的AVPlayerViewController以对其进行进一步自定义或设置其代理,您可以通过CardPartVideoViewviewController属性来完成。例如

guard let controller = cardPartVideoView.viewController as? AVPlayerViewController else { return }
controller.delegate = self
controller.entersFullScreenWhenPlaybackBegins = true

CardPartVideoView

卡片状态

CardPartsViewController可选支持卡片状态的概念,其中卡片可以有三种不同状态:加载中、空和非空。对于每种状态,您可以指定一组唯一的卡片组件来显示。然后,当CardPartsViewController的状态属性更改时,框架将自动切换到显示该状态的卡片组件。通常,您会将状态属性绑定到视图模型中的状态属性上,以便当视图模型状态发生变化时,卡片组件也会更新。一个简单的例子

public enum CardState {
    case none
    case loading
    case empty
    case hasData
    case custom(String)
}

class TestCardController : CardPartsViewController  {

    var viewModel = TestViewModel()
    var titlePart = CardPartTitleView(type: .titleOnly)
    var textPart = CardPartTextView(type: .normal)
    var loadingText = CardPartTextView(type: .normal)
    var emptyText = CardPartTextView(type: .normal)
    var customText = CardPartTextView(type: .normal)

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.title.asObservable().bind(to: titlePart.rx.title).disposed(by: bag)
        viewModel.text.asObservable().bind(to: textPart.rx.text).disposed(by: bag)

        loadingText.text = "Loading..."
        emptyText.text = "No data found."
        customText.text = "I am some custom state"

        viewModel.state.asObservable().bind(to: self.rx.state).disposed(by: bag)

        setupCardParts([titlePart, textPart], forState: .hasData)
        setupCardParts([titlePart, loadingText], forState: .loading)
        setupCardParts([titlePart, emptyText], forState: .empty)
        setupCardParts([titlePart, customText], forState: .custom("myCustomState"))
    }
}

注意:有一个custom(String)状态,允许您使用我们预定义状态集之外的更多状态。

.custom("myCustomState")

数据绑定

数据绑定通过RxSwift库([https://github.com/ReactiveX/RxSwift](https://github.com/ReactiveX/RxSwift))实现。视图模型应通过Variable类将其数据作为可绑定属性公开。在上面的例子中,视图模型可能看起来像这样

class TestViewModel {

    var title = BehaviorRelay(value: "Testing")
    var text = BehaviorRelay(value: "Card Part Text")
}

稍后,当视图模型的数据发生变化时,它可以通过设置属性的值属性来更新其属性。

title.accept(“Hello”)

视图控制器可以将视图模型的属性绑定到一个视图上

viewModel.title.asObservable().bind(to: titlePart.rx.title).disposed(by: bag)

现在,每次当视图模型的标题属性值发生变化时,它将自动更新标题部分的标题。

RxSwift使用“Disposable”和“DisposeBag”的概念来删除绑定。每次调用bind都返回一个可丢弃的对象,该对象可以被添加到 DisposeBag 中。CardPartsViewController定义了一个名为“bag”的 DisposeBag 实例,您可以使用它来自动删除在 CardPartsViewController 被销毁时创建的所有绑定。有关更多关于可丢弃对象和 DisposeBag 的信息,请参阅 RxSwift 文档。

主题

默认情况下,我们支持两种主题:薄荷和涡轮。这俩都是基于 CardParts 开发的 Intuit 应用程序。在 CardPartsTheme.swift 文件中,我们定义了一个名为 CardPartsTheme 的协议。您可以创建一个遵循 CardPartsTheme 协议的类,并按顺序设置所有属性,以实现您喜欢的任何 CardParts 主题。以下是一些可主题化的属性示例:

// CardPartTextView
var smallTextFont: UIFont { get set }
var smallTextColor: UIColor { get set }
var normalTextFont: UIFont { get set }
var normalTextColor: UIColor { get set }
var titleTextFont: UIFont { get set }
var titleTextColor: UIColor { get set }
var headerTextFont: UIFont { get set }
var headerTextColor: UIColor { get set }
var detailTextFont: UIFont { get set }
var detailTextColor: UIColor { get set }

// CardPartTitleView
var titleFont: UIFont { get set }
var titleColor: UIColor { get set }

// CardPartButtonView
var buttonTitleFont: UIFont { get set }
var buttonTitleColor: UIColor { get set }
var buttonCornerRadius: CGFloat { get set }

应用主题

按照以下方式生成一个类

public class YourCardPartTheme: CardPartsTheme {
    ...
}

然后在您的 AppDelegate 中调用 YourCardPartTheme().apply() 以应用您的主题。如果您在 storyboard 中使用带有 CardsViewController 的 storyboards,那么在调用 AppDelegate 之前会先调用 required init(coder:) 初始化器。在这种情况下,您需要在要初始化的第一个视图控制器的初始化器中应用主题,并且更改将在其他所有视图控制器中生效。例如:

required init?(coder: NSCoder) {
	YourCardPartTheme().apply()
	super.init(coder: coder)
}

可点击卡片

您可以为任何给定卡片的多达 状态 添加一个点击动作。如果卡片的任何部分被点击,将会触发相应的动作

self.cardTapped(forState: .empty) {
    print("Card was tapped in .empty state!")
}

self.cardTapped(forState: .hasData) {
    print("Card was tapped in .hasData state!")
}

// The default state for setupCardParts([]) is .none
self.cardTapped {
    print("Card was tapped in .none state")
}

注意:在闭包中弱引用自我是一个好习惯。

{[weak self] in

}

监听器

CardParts 还支持一个监听器,允许您监听您创建的卡片中的可见性更改。在您的 CardPartsViewController 中,您可以通过实现 CardVisibilityDelegate 来获取您所创建的 CardsViewController 中卡片的可见性信息。此可选代理可以按照以下方式实现:

public class YourCardPartsViewController: CardPartsViewController, CardVisibilityDelegate {
    ...

    /**
    Notifies your card parts view controller of the ratio that the card is visible in its container
    and the ratio of its container that the card takes up.
    */
     func cardVisibility(cardVisibilityRatio: CGFloat, containerCoverageRatio: CGFloat) {
        // Any logic you would like to perform based on these ratios
    }
}

代理

任何继承自CardPartsViewController的视图控制器都支持在视图中长按的手势委托。只需将您的控制器符合CardPartsLongPressGestureRecognizerDelegate协议即可。

当视图被长按时,会调用didLongPress(_:)方法,您可以在其中自定义处理手势。例如:在手势状态开始/结束的时候进行放大和缩小。

    func didLongPress(_ gesture: UILongPressGestureRecognizer) -> Void

您可以设置长按的最短持续时间(minimumPressDuration),以便将其注册为手势开始。该值以秒为单位。默认设置为1秒

    var minimumPressDuration: CFTimeInterval { get } // In seconds

示例

extension MYOwnCardPartController: CardPartsLongPressGestureRecognizerDelegate {
	func didLongPress(_ gesture: UILongPressGestureRecognizer) {
		guard let v = gesture.view else { return }

		switch gesture.state {
		case .began:
			// Zoom in
		case .ended, .cancelled:
			// Zoom out
		default: break
		}
	}
	// Gesture starts registering after pressing for more than 0.5 seconds.
	var minimumPressDuration: CFTimeInterval { return 0.5 }
}

喜欢CardParts的应用

出版

许可

CardParts遵循Apache 2.0许可。有关更多信息,请参阅/LICENSE文件。