A reactive, card-based UI framework built on UIKit for iOS developers.

Overview

Mint Logo

Build Status Version License Platform

MintSights by CardParts CardParts in Mint CardParts in Turbo

CardParts - made with ❤️ by Intuit:

Example

To run the example project, clone the repo, and run pod install from the Example directory first.

In ViewController.swift you will be able to change the cards displayed and/or their order by commenting out one of the loadCards(cards: ) functions. If you want to change the content of any of these cards, you can look into each of the CardPartsViewController you pass into the function such as: TestCardController, Thing1CardController, Thing2CardController, etc.

Requirements

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

Installation

CardParts is available through CocoaPods. You can install it with the following command:

$ gem install cocoapods

To add CardParts to your project, simply add the following line to your Podfile:

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

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

Then, run the following command:

$ pod install

Communication and Contribution

  • If you need help, open an issue and tag as help wanted.
  • If you found a bug, open an issue and tag as bug.
  • If you have a feature request, open an issue and tag as feature.
  • If you want to contribute, submit a pull request.
    • In order to submit a pull request, please fork this repo and submit a PR from your forked repo.
    • Have a detailed message as to what your PR fixes/enhances/adds.
    • Each PR must get two approvals from our team before we will merge.

Overview

CardParts is the second generation Card UI framework for the iOS Mint application. This version includes many updates to the original card part framework, including improved MVVM, data binding (via RxSwift), use of stack views and self sizing collection views instead sizing cells, 100% swift and much more. The result is a much simpler, easier to use, more powerful, and easier to maintain framework. This framework is currently used by the iOS Mint application and the iOS Turbo application.

CardPart Example in Mint

Quick Start

See how quickly you can get a card displayed on the screen while adhering to the MVVM design pattern:

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!")
    }
}

Note: RxCocoa is required for BehaviorRelay, thus you must import it wherever you may find yourself using it.

Architecture

There are two major parts to the card parts framework. The first is the CardsViewController which will display the cards. It is responsible for displaying cards in the proper order and managing the lifetime of the cards. The second major component is the cards themselves which are typically instances of CardPartsViewController. Each instance of CardPartsViewController displays the content of a single card, using one or more card parts (more details later).

CardsViewController

The CardsViewController uses a collection view where each cell is a single card. The cells will render the frames for the cards, but are designed to have a child ViewController that will display the contents of the card. Thus CardsViewController is essentially a list of child view controllers that are rendered in special cells that draw the card frames.

To use a CardsViewController, you first need to subclass it. Then in the viewDidLoad method call the super class loadCards method passing an array of CardControllers. Each instance of a CardController will be rendered as a single card. The loadCards method does this by getting the view controller for each CardController and adding them as child view controllers to the card cells. Here is an example:

class TestCardsViewController: CardsViewController {

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

    override func viewDidLoad() {
        super.viewDidLoad()

        loadCards(cards: cards)
    }
}

Each card must implement the CardController protocol (note that CardPartsViewController discussed later implements this protocol already). The CardController protocol has a single method:

protocol CardController : NSObjectProtocol {

    func viewController() -> UIViewController

}

The viewController() method must return the viewController that will be added as a child controller to the card cell. If the CardController is a UIViewController it can simply return self for this method.

Load specific cards

While normally you may call loadCards(cards:) to load an array of CardControllers, you may want the ability to load reload a specific set of cards. We offer the ability via the loadSpecificCards(cards: [CardController] , indexPaths: [IndexPath]) API. Simply pass in the full array of new cards as well as the indexPaths that you would like reloaded.

Custom Card Margins

By default, the margins of your CardsViewController will match the theme's cardCellMargins property. You can change the margins for all CardsViewControllers in your application by applying a new theme or setting CardParts.theme.cardCellMargins = UIEdgeInsets(...). Alternatively, if you want to change the margins for just one CardsViewController, you can set the cardCellMargins property of that CardsViewController. To change the margin for an individual card see CustomMarginCardTrait. This property will default to use the theme's margins if you do not specify a new value for it. Changing this value should be done in the init of your custom CardsViewController, but must occur after super.init because it is changing a property of the super class. For example:

class MyCardsViewController: CardsViewController {

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

	...
}

If you use storyboards with CardsViewController subclasses in your storyboard, the cardCellMargins property will take the value of the CardParts.theme.cardCellMargins when the required init(coder:) initializer is called. If you are trying to change the theme for your whole application, you will need to do so in this initializer of the first view controller in your storyboard to be initialized, and changes will take effect in all other view controllers. For example:

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

Card Traits

The Card Parts framework defines a set of traits that can be used to modify the appearance and behavior of cards. These traits are implemented as protocols and protocol extensions. To add a trait to a card simply add the trait protocol to the CardController definition. For example:

class MyCard: UIViewController, CardController, TransparentCardTrait {

}

MyCard will now render itself with a transparent card background and frame. No extra code is needed, just adding the TransparentCardTrait as a protocol is all that is necessary.

Most traits require no extra code. The default protocol extensions implemented by the framework implement all the code required for the trait to modify the card. A few traits do require implementing a function or property. See the documentation for each trait below for more information.

NoTopBottomMarginsCardTrait

By default each card has margin at the top and bottom of the card frame. Adding the NoTopBottomMarginsCardTrait trait will remove that margin allowing the card to render to use the entire space inside the card frame.

TransparentCardTrait

Each card is rendered with a frame that draws the border around the card. Adding TransparentCardTrait will not display that border allowing the card to render without a frame.

EditableCardTrait

If the EditableCardTrait trait is added, the card will be rendered with an edit button in upper right of the card. When user taps in the edit button, the framework will call the cards onEditButtonTap() method. The EditableCardTrait protocol requires the CardController to implement the onEditButtonTap() method.

HiddenCardTrait

The HiddenCardTrait trait requires the CardController to implement an isHidden variable:

    var isHidden: BehaviorRelay<Bool> { get }

The framework will then observe the isHidden variable so that whenever its value is changed the card will be hidden or shown based upon the new value. This allows the CardController to control its visibility by simply modifying the value of its isHidden variable.

ShadowCardTrait

The ShadowCardTrait protocol requires CardController to implement shadowColor(), shadowRadius(), shadowOpacity() and shadowOffset() methods.

    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

Use this protocol to define the roundness for the card by implementing the method cornerRadius().

    func cornerRadius() -> CGFloat {
        return 10.0
    }

cornerRadius: 10.0
Shadow radius 5.0

GradientCardTrait

Use this protocol to add a gradient background for the card. The gradients will be added vertically from top to bottom. Optionally you can apply an angle to the gradient. Angles are defined in degrees, any negative or positive degree value is valid.

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

    func gradientAngle() -> Float {
        return 45.0
    }

Shadow radius 10.0

BorderCardTrait

Use this protocol to add border color and border width for the card, implement borderWidth(), and borderColor() methods.

    func borderWidth() -> CGFloat {
        return 2.0
    }

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

border

CustomMarginCardTrait

Use this protocol to specifiy a custom margin for the card, implement customMargin() method. Value returned will be used for left and right margins thus centering the card in the superview.

    func customMargin() -> CGFloat {
        return 42.0
    }

CardPartsViewController

CardPartsViewController implements the CardController protocol and builds its card UI by displaying one or more card part views using an MVVM pattern that includes automatic data binding. Each CardPartsViewController displays a list of CardPartView as its subviews. Each CardPartView renders as a row in the card. The CardParts framework implements several different types of CardPartView that display basic views, such as title, text, image, button, separator, etc. All CardPartView implemented by the framework are already styled to correctly match the applied s UI guidelines.

In addition to the card parts, a CardPartsViewController also uses a view model to expose data properties that are bound to the card parts. The view model should contain all the business logic for the card, thus keeping the role of the CardPartsViewController to just creating its view parts and setting up bindings from the view model to the card parts. A simple implementation of a CardPartsViewController based card might look like the following:

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!")
    }
}

The above example creates a card that displays two card parts, a title card part and a text part. The bind calls setup automatic data binding between view model properties and the card part view properties so that whenever the view model properties change, the card part views will automatically update with the correct data.

The call to setupCardParts adds the card part views to the card. It takes an array of CardPartView that specifies which card parts to display, and in what order to display them.

CardPartsFullScreenViewController

This will make the card a full screen view controller. So if you do not want to build with an array of Cards, instead you can make a singular card full-screen.

class TestCardController: CardPartsFullScreenViewController  {
    ...
}

CardParts

The framework includes several predefined card parts that are ready to use. It is also possible to create custom card parts. The following sections list all the predefined card parts and their reactive properties that can be bound to view models.

CardPartTextView

CardPartTextView displays a single text string. The string can wrap to multiple lines. The initializer for CardPartTextView takes a type parameter which can be set to: normal, title, or detail. The type is used to set the default font and textColor for the text.

CardPartTextView exposes the following reactive properties that can be bound to view model properties:

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 is comparable to CardPartTextView, but it is built upon UITextView rather than UILabel. This allows for CardPartImageViews to be nested within the CardPartAttrbutedTextView and for text to be wrapped around these nested images. In addition, CardPartAttributedTextView allows for links to be set and opened. CartPartAttributedTextView exposes the following reactive properties that can be bound to view model properties:

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 displays a single image. CardPartImageView exposes the following reactive properties that can be bound to view model properties:

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

CardPartButtonView

CardPartButtonView displays a single button.

CardPartButtonView exposes the following reactive properties that can be bound to view model properties:

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 displays a view with a title, and an optional options menu. The initializer requires a type parameter which can be set to either titleOnly or titleWithMenu. If the type is set to titleWithMenu the card part will display a menu icon, that when tapped will display a menu containing the options specified in the menuOptions array. The menuOptionObserver property can be set to a block that will be called when the user selects an item from the menu.

As an example for a title with menu buttons:

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 exposes the following reactive properties that can be bound to view model properties:

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 allows you to have a left and right title and description label, however, you are able to also choose the alignment of the right title/description labels. See below:

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 provides you the rounded corners, text aligned being at the center along with vertical and horizontal padding capability.

var verticalPadding:CGFloat
var horizontalPadding:CGFloat

pillLabel

See the example app for a working example.

CardPartIconLabel

CardPartIconLabel provides the capability to add images in eithet directions supporting left , right and center text alignments along with icon binding capability.

    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 displays a separator line. There are no reactive properties define for CardPartSeparatorView.

CardPartVerticalSeparatorView

As the name describes, it shows a vertical separator view opposed to a horizontal one

CardPartStackView

CardPartStackView displays a UIStackView that can contain other card parts, and even other CardPartStackViews. Using CardPartStackView allows for creating custom layouts of card parts. By nesting CardPartStackViews you can create almost any layout.

To add a card part to the stack view call its addArrangedSubview method, specifying the card part's view property as the view to be added to the stack view. For example:

horizStackPart.addArrangedSubview(imagePart)

Also,provides an option to round the corners of the stackview

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

roundedStackView

There are no reactive properties defined for CardPartStackView. However you can use the default UIStackView properties (distribution, alignment, spacing, and axis) to configure the stack view.

CardPartTableView

CardPartTableView displays a table view as a card part such that all items in the table view are displayed in the card part (i.e. the table view does not scroll). CardPartTableView leverages Bond's reactive data source support allowing a MutableObservableArray to be bound to the table view.

To setup the data source binding the view model class should expose MutableObservableArray property that contains the table view's data. For example:

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

Then in the view controller the data source binding can be setup as follows:

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
}

The last parameter to the bind call is block that will be called when the tableview's cellForRowAt data source method is called. The first parameter to the block is the MutableObservableArray being bound to.

CardPartTableView registers a default cell class (CardPartTableViewCell) that can be used with no additional work. CardPartTableViewCell contains 4 labels, a left justified title, left justified description, right justified title, and a right justified description. Each label can be optionally used, if no text is specified in a label the cell's layout code will correctly layout the remaining labels.

It is also possible to register your own custom cells by calling the register method on tableViewPart.tableView.

You also have access to two delegate methods being called by the tableView as follows:

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

CardPartTableViewCell

CardPartTableViewCell is the default cell registered for CardPartTableView. The cell contains the following properties:

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

This will give you the ability to create custom tableView cells out of CardParts. The following code allows you to create a cell:

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
    }
}

If you do create a custom cell, you must register it to the CardPartTableView:

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

And then as normal, you would bind to your viewModel's data:

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 underlying engine is RxDataSource. You can look at their documentation for a deeper look but here is an overall approach to how it works:

Start by initializing a CardPartCollectionView with a custom UICollectionViewFlowLayout:

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
}()

Now say you have a custom struct you want to pass into your CollectionViewCell:

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

You will need to create a new struct to conform to 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
    }
}

Next, create a data source that you will bind to you data: Note: You can create a custom CardPartCollectionViewCell as well - see below.

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

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

    return cell
})

Finally, bind your viewModel data to the collectionView and its newly created data source:

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

Note: viewModel.data will be a reactive array of SectionOfCustomStruct:

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

CardPartCollectionViewCardPartsCell

Just how CardPartTableViewCell has the ability to create tableView cells out of CardParts - so do CollectionViews. Below is an example of how you may create a custom 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
    }
}

To use this, you must register it to the CollectionView during viewDidLoad as follows:

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

Then, inside your data source, simply dequeue this 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 present a horizontal bar graph that can be filled to a certain percentage of your choice. Both the color of the fill and the percent is reactive

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

This CardPart allows you to create a horizontal paged carousel with page controls. Simply feed it with your desired height and an array of CardPartStackView:

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

This CardPart also has a delegate:

func didMoveToPage(page: Int)

Which will fire whenever the user swipes to another page

You also have the abililty to automatically move to a specific page by calling the following function on CardPartPagedView

func moveToPage(_ page: Int)

CardPartSliderView

You can set min and max value as well as bind to the current set amount:

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

You can set min and max value as well as tint color and outer track color:

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

Allows you to add a space between card parts in case you need a space larger than the default margin. Initialize it with a specific height:

CardPartSpacerView(height: 30)

CardPartTextField

CardPartTextField can take a parameter of type CardPartTextFieldFormat which determines formatting for the UITextField. You may also set properties such as keyboardType, placeholder, font, text, etc.

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)

The different formats are as follows:

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

CardPartOrientedView

CardPartOrientedView allows you to create an oriented list view of card part elements. This is similar to the CardPartStackView except that this view can orient elements to the top or bottom of the view. This is advantageous when you are using horizontal stack views and need elements to be oriented differently (top arranged or bottom arranged) relative to the other views in the horizontal stack view. To see a good example of this element please take a look at the example application.

The supported orientations are as follows:

public enum Orientation {
    case top
    case bottom
}

To create an oriented view you can use the following code:

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

Add the above orientedView to any list of card parts or an existing stack view to orient your elements to the top or bottom of the enclosing view.

CardPartCenteredView

CardPartCenteredView is a CardPart that fits a centered card part proportionally on the phone screen while allowing a left and right side card part to scale appropriately. To create a centered card part please use the following example:

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])
    }
}

A CardPartCenteredView can take in any card part that conforms to CardPartView as the left, center, and right components. To see a graphical example of the centered card part please look at the example application packaged with this cocoapod.

CardPartConfettiView

Provides the capability to add confetti with various types ( diamonds, star, mixed ) and colors, along with different level of intensity

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

Confetti

CardPartProgressBarView

Provides the capability to configure different colors and custom marker , it's position to indicate the progress based on the value provided.

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

ProgressBarView

CardPartMapView

Provides the capability to display a MapView and reactively configure location, map type, and coordinate span (zoom). You also have direct access to the MKMapView instance so that you can add annotations, hook into it's MKMapViewDelegate, or whatever else you'd normally do with Maps.

By default the card part will be rendered at a height of 300 points but you can set a custom height just be resetting the CardPartMapView.intrensicHeight property.

Here's a small example of how to reactively set the location from a changing address field (See the Example project for a working example):

    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

Provides the capability to add radio buttons with configurable inner/outer circle line width , colors along with tap etc..

    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

Provides the capability to add a switch with configurable colors.

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

RadioButton

CardPartHistogramView

Provides the caoability to generate the bar graph based on the data ranges with customizable bars , lines, colors etc..

    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 provides the capability to show a highly-customizable modal bottom sheet. At its simplest, all you need to do is set the contentVC property to a view controller that you create to control the content of the bottom sheet:

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

bottom sheet

CardPartsBottomSheetViewController also supports being used as a sticky view at the bottom of the screen, and can be presented on any view (default is keyWindow). For example, the following code creates a sticky view that still permits scrolling behind it and can only be dismissed programmatically.

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

sticky bottom sheet

There are also over two dozen other properties that you can set to further customize the bottom sheet for your needs. You can configure the colors, height, gesture recognizers, handle appearance, animation times, and callback functions with the following properties.

  • var contentVC: UIViewController?: View controller for the content of the bottom sheet. Should set this parameter before presenting bottom sheet.
  • var contentHeight: CGFloat?: Manually set a content height. If not set, height will try to be inferred from contentVC.
  • var bottomSheetBackgroundColor: UIColor: Background color of bottom sheet. Default is white.
  • var bottomSheetCornerRadius: CGFloat: Corner radius of bottom sheet. Default is 16.
  • var handleVC: CardPartsBottomSheetHandleViewController: Pill-shaped handle at the top of the bottom sheet. Can configure handleVC.handleHeight, handleVC.handleWidth, and handleVC.handleColor.
  • var handlePosition: BottomSheetHandlePosition: Positioning of handle relative to bottom sheet. Options are .above(bottomPadding), .inside(topPadding), .none. Default is above with padding 8.
  • var overlayColor: UIColor: Color of the background overlay. Default is black.
  • var shouldIncludeOverlay: Bool: Whether or not to include a background overlay. Default is true.
  • var overlayMaxAlpha: CGFloat: Maximum alpha value of background overlay. Will fade to 0 proportionally with height as bottom sheet is dragged down. Default is 0.5.
  • var dragHeightRatioToDismiss: CGFloat: Ratio of how how far down user must have dragged bottom sheet before releasing it in order to trigger a dismissal. Default is 0.4.
  • var dragVelocityToDismiss: CGFloat: Velocity that must be exceeded in order to dismiss bottom sheet if height ratio is greater than dragHeightRatioToDismiss. Default is 250.
  • var pullUpResistance: CGFloat: Amount that the bottom sheet resists being dragged up. Default 5 means that for every 5 pixels the user drags up, the bottom sheet goes up 1 pixel.
  • var appearAnimationDuration: TimeInterval: Animation time for bottom sheet to appear. Default is 0.5.
  • var dismissAnimationDuration: TimeInterval: Animation time for bottom sheet to dismiss. Default is 0.5.
  • var snapBackAnimationDuration: TimeInterval: Animation time for bottom sheet to snap back to its height. Default is 0.25.
  • var animationOptions: UIView.AnimationOptions: Animation options for bottom sheet animations. Default is UIView.AnimationOptions.curveEaseIn.
  • var changeHeightAnimationDuration: TimeInterval: Animation time for bottom sheet to adjust to a new height when height is changed. Default is 0.25.
  • var shouldListenToOverlayTap: Bool: Whether or not to dismiss if a user taps in the overlay. Default is true.
  • var shouldListenToHandleDrag: Bool: Whether or not to respond to dragging on the handle. Default is true.
  • var shouldListenToContentDrag: Bool: Whether or not to respond to dragging in the content. Default is true.
  • var shouldListenToContainerDrag: Bool: Whether or not to respond to dragging in the container. Default is true.
  • var shouldRequireVerticalDrag: Bool: Whether or not to require a drag to start in the vertical direction. Default is true.
  • var adjustsForSafeAreaBottomInset: Bool: Boolean value for whether or not bottom sheet should automatically add to its height to account for bottom safe area inset. Default is true.
  • var didShow: (() -> Void)?: Callback function to be called when bottom sheet is done preseting.
  • var didDismiss: ((_ dismissalType: BottomSheetDismissalType) -> Void)?: Callback function to be called when bottom sheet is done dismissing itself. Parameter dismissalType: information about how the bottom sheet was dismissed - .tapInOverlay, .swipeDown, .programmatic(info).
  • var didChangeHeight: ((_ newHeight: CGFloat) -> Void)?: Callback function to be called when bottom sheet height changes from dragging or a call to updateHeight.
  • var preferredGestureRecognizers: [UIGestureRecognizer]?: Gesture recognizers that should block the vertical dragging of bottom sheet. Will automatically find and use all gesture recognizers if nil, otherwise will use recognizers in the array. Default is empty array.
  • var shouldListenToKeyboardNotifications: Bool: If there is a text field in the bottom sheet we may want to automatically have the bottom sheet adjust for the keyboard. Default is false.
  • var isModalAccessibilityElement: Bool: Whether or not to treat the bottom sheet as a modal accessibility element, which will block interaction with views underneath. Default is true. It is not recommended that you override this unless you are using the bottom sheet in sticky mode or otherwise without the overlay.
  • var allowsAccessibilityGestureToDismiss: Bool: Whether or not users can use the accessibility escape gesture to dismiss the bottom sheet. Default is true. It is not recommended that you override this unless you are using the bottom sheet in sticky mode or otherwise disabling dismissal or providing another way for VoiceOver users to dismiss.

If you change the contentVC or contentHeight properties, the bottom sheet will automatically update its height. You can also call updateHeight() to trigger an update of the height (this is mainly for if the content of the contentVC has changed and you want the bottom sheet to update to match the new content size).

Because it is uncommon to have access to the bottom sheet view controller from the contentVC,we define a CardPartsBottomSheetDelegate with default implementations for updating to a new contentVC or contentHeight, updating the height, or dismissing the bottom sheet programmatically. In order to use this delegate and its default function implementations, simply have your class conform to CardPartsBottomSheetDelegate and define a var bottomSheetViewController: CardPartsBottomSheetViewController. Then, set that class to be a delegate for your content view controller and you can interface with the bottom sheet through the delegate.

CardPartVideoView

Provides the capability to embed AVPlayer inside a cardpart view.

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)

If you need to access the underlying AVPlayerViewController to further customize it or set its delegate, you can do so through the CardPartVideoView's viewController property. For example:

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

CardPartVideoView

Card States

CardPartsViewController can optionally support the notion of card states, where a card can be in 3 different states: loading, empty, and hasData. For each state you can specify a unique set of card parts to display. Then when the CardPartsViewController state property is changed, the framework will automatically switch the card parts to display the card parts for that state. Typically you would bind the state property to a state property in your view model so that when the view model changes state the card parts are changed. A simple example:

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"))
    }
}

Note: There is a custom(String) state which allows you to use more than our predefined set of states:

.custom("myCustomState")

Data Binding

Data binding is implemented using the RxSwift library (https://github.com/ReactiveX/RxSwift). View models should expose their data as bindable properties using the Variable class. In the example above the view model might look like this:

class TestViewModel {

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

Later when the view model's data has changed it can update its property by setting the value attribute of the property:

title.accept(“Hello”)

The view controller can bind the view model property to a view:

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

Now, whenever the view model's title property value is changed it will automatically update the titlePart's title.

RxSwift use the concept of "Disposable" and "DisposeBag" to remove bindings. Each call to bind returns a Disposable that can be added to a DisposeBag. CardPartsViewController defines an instance of DisposeBag called "bag" that you can use to automatically remove all your bindings when your CardPartsViewController is deallocated. See the RxSwift documentation for more information on disposables and DisposeBags.

Themes

Out of the box we support 2 themes: Mint and Turbo. These are the 2 Intuit app's that are currently built on top of CardParts. As you can find in the file CardPartsTheme.swift we have a protocol called CardPartsTheme. You may create a class that conforms to CardPartsTheme and set all properties in order to theme CardParts however you may like. Below is an example of some of the themeable properties:

// 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 }

Applying a theme

Generate a class as follows:

public class YourCardPartTheme: CardPartsTheme {
    ...
}

And then in your AppDelegete call YourCardPartTheme().apply() it apply your theme. If you use storyboards with CardsViewControllers in your storyboard, the required init(coder:) initializer gets called prior to AppDelegate. In this case, you will need to apply the theme in this initializer of the first view controller in your storyboard to be initialized, and changes will take effect in all other view controllers. For example:

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

Clickable Cards

You have the ability to add a tap action for each state of any given card. If a part of the card is clicked, the given action will be fired:

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")
}

Note: It is always a good idea to weakify self in a closure:

{[weak self] in

}

Listeners

CardParts also supports a listener that allows you to listen to visibility changes in the cards that you have created. In your CardPartsViewController you may implement the CardVisibilityDelegate to gain insight into the visibility of your card within the CardsViewController you have created. This optional delegate can be implemented as follows:

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
    }
}

Delegates

Any view controller which is a subclass of CardPartsViewController supports gesture delegate for long press on the view. Just need to conform your controller to CardPartsLongPressGestureRecognizerDelegate protocol.

When the view is long pressed didLongPress(_:) will be called where you can custom handle the gesture. Example: Zoom in and Zoom out on gesture state begin/ended.

    func didLongPress(_ gesture: UILongPressGestureRecognizer) -> Void

You can set the minimumPressDuration for your press to register as gesture began. The value is in seconds. default is set to 1 second.

    var minimumPressDuration: CFTimeInterval { get } // In seconds

Example:

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 }
}

Apps That Love CardParts

Publications

License

CardParts is available under the Apache 2.0 license. See the LICENSE file for more info.

Comments
  • Update RxCocoa/Swift/DataSource/Gesture

    Update RxCocoa/Swift/DataSource/Gesture

    This fixes #239 and #64

    It seems like the only thing that actually needed to be changed was the dependency lines. I recommend an additional PR to bump the Pod version.

    I ran this against a project using RxSwift/RxCocoa 5.1.1, Swift 5, and iOS 13 with successful results

    opened by MatrixSenpai 15
  • Layout issue:

    Layout issue: "contentView of collectionViewCell has translatesAutoresizingMaskIntoConstraints false and is missing constraints to the cell"

    I see this in the console using the code provided (all from the example project):

    [Assert] contentView of collectionViewCell has translatesAutoresizingMaskIntoConstraints false and is missing constraints to the cell, which will cause substandard performance in cell autosizing. Please leave the contentView's translatesAutoresizingMaskIntoConstraints true or else provide constraints between the contentView and the cell. <CardParts.CardCell: 0x10d6a2aa0; baseClass = UICollectionViewCell; frame = (162.5 0; 50 50); layer = <CALayer: 0x107192f00>>

    Code:

    class CardPartTitleViewCardController: CardPartsViewController {
    
    let cardPartTitleView = CardPartTitleView(type: .titleOnly)
    let cardPartTitleWithMenu = CardPartTitleView(type: .titleWithMenu)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        cardPartTitleView.title = "I am a standard .titleOnly CardPartTitleView"
        cardPartTitleWithMenu.title = "I am a .titleWithMenu CardPartTitleView"
        
        setupCardParts([cardPartTitleView, cardPartTitleWithMenu])
    }
    }
    
    class UserAreaViewController: CardsViewController {
    
    
    var cards: [CardController] = [CardPartTitleViewCardController()]
    
     
    override func viewDidLoad() {
        super.viewDidLoad()
       
        loadCards(cards: cards)
    
       }
    }
    

    At first I thought this is a wrong implementation of our team, but then I took 100% code from the example (provided) and still see this warning.

    bug 
    opened by eyzuky 14
  • CardPartTableView doesn't have a top margin, even when manually set

    CardPartTableView doesn't have a top margin, even when manually set

    Hi,

    When working with a single CardPartTableView, I would expect there to be a top margin of about 10.0 or so, but there is no margin. In the documentation it states that cards have a default margin all around them, but that doesn't seem to be true for the top margin of the first card in the view. As a result, this is what it looks like: image

    I've tried implementing the NoTopBottomMarginsCardTrait protocol on my CardPartsViewController subclass, and manually implementing the requiresNoTopBottomMargins() method to return false, but no change. I tried to manually create a CardPartSpacerView(height: 30.0) called aboveSpacerPart and included that in my array passed to setupCardParts([]), but that didn't make any difference either. I even tried to manually set the margins on the CardPartTableView to this: tableViewPart.margins = UIEdgeInsets(top: 10.0, left: 14.0, bottom: 14.0, right: 10.0). For some reason, it doesn't seem like I can get there to be a margin on top of this CardPartTableView. Any reason why that might be so?

    opened by tcheek-bw 12
  • Use of unresolved identifier 'BehaviorRelay

    Use of unresolved identifier 'BehaviorRelay

    Hi, I tried the quick start part. The import of CardParts and everything build fine except it giving me an error on these two which yelling: Use of unresolved identifier 'BehaviorRelay'

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

    In Podfile I have !framework on so it shouldn't be any problem downloading the required frameworks for it. Just curious how to fix this error?

    opened by thunpisit 11
  • Top margins solutions for large title navigationBar

    Top margins solutions for large title navigationBar

    Hi guys,

    Could anyone help me to add an extra space on top of the CardParts? Here is what I dealing with now.

    image

    I use CardPartSpacerView but is not working.

    Any way to fix this?

    opened by vietnguyen09 10
  • Custom margins per CardController

    Custom margins per CardController

    Is your feature request related to a problem? Please describe. Yes, cards within a CardsViewController must all have the same margin.

    Describe the solution you'd like I'd like a way to specify a custom margin for an individual card with a CardsViewController. The main reason is to specify 0 margin for things like transparent cards that need to be the full width of the view.

    Describe alternatives you've considered Instead of allowing for any margin, the solution could just be for cards that need to be the full width of the screen.

    Additional context I have completed the feature on my fork and am ready to submit a PR if this idea is approved.

    enhancement 
    opened by jmjordan 9
  • Add new CardPartMapView

    Add new CardPartMapView

    Related to https://github.com/intuit/CardParts/issues/183

    Alright! Putting myself out there for the first time. Here's my first stab at it. I added three different properties that can be accessed via the RX binding syntax:

    • location
    • map type
    • zoom level (span)

    I also made sure the the underlying MKMapView was available so that users could access its delegate functions or interact with the map directly as needed.

    In my example, I showed how you could bind directly to a CardPartTextField's value and change the location of the map. At the same time, I'm cycling through different "zoom levels".

    I'm totally open to adding more bindable properties if you think it needs more.


    On a personal note, this was a lot of fun and this library is very well organized. Well done to all of the other contributors that have worked on this project.

    enhancement hacktoberfest 
    opened by codethebeard 9
  • Still having issues with layout constraints after #100 closed.

    Still having issues with layout constraints after #100 closed.

    Pulled down version 2.7.4.

    layout constraints still seem to be showing up. But, now the app appears to be be trying to invalidate the layout and retrying. Note in the video that the error messages continue to appear in the console event the ought the app appears to be idle.

    https://www.dropbox.com/s/tc588wc2wtt14dx/CardParts.mp4?dl=0

    opened by jem5519 7
  • Add an method to modify distribution

    Add an method to modify distribution

    The current distribution mode is set to equalSpacing, which is nice for some cards. However, it's preventing me from placing a cardPart at a certain position. Is it possible to add an function like modifyDistribution to switch distribution to .fill, so that cardParts will be placed according to margins?

    opened by Helen-Xu 7
  • CardPartCollectionView binding datasource throws error 'Generic parameter 'Self' could not be inferred'

    CardPartCollectionView binding datasource throws error 'Generic parameter 'Self' could not be inferred'

    I am running into an issue binding my CardPartCollectionView datasource.

    First I set up my model

    struct CollectionViewModel {
        var title: String?
        var image: Storefront.Image?
        var description: String?
        var handle: String?
        var products = Variable([Storefront.Product]())
    }
    
    struct SectionOfCollection {
        var header: String
        var items: [Item]
    }
    
    extension SectionOfCollection: SectionModelType {
        init(original: SectionOfCollection, items: [Item]) {
            self = original
            self.items = items
        }
        
        typealias Item = Storefront.Product
    }
    

    Then I set up my CardPartsFullScreenViewController with an instance of the model, the collectionView CardPart, the layout for the collectionView, then I setup the collectionView with all these components.

    class CollectionCardController: CardPartsFullScreenViewController {
        
        let model = CollectionViewModel()
        lazy var collectionViewCardPart = CardPartCollectionView(collectionViewLayout: layout)
        
        lazy var layout: UICollectionViewFlowLayout = {
            let layout = UICollectionViewFlowLayout()
            layout.minimumInteritemSpacing = 10
            layout.minimumLineSpacing = 10
            layout.scrollDirection = .vertical
            layout.itemSize = CGSize(width: width, height: height)
            return layout
        }()
        lazy var width: CGFloat = {
            return self.view.frame.width / 2 - 20 - 20
        }()
        lazy var height = self.width + 88
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            setupCollectionView()
            collectionViewCardPart.collectionView.register(ProductCollectionViewCell.self, forCellWithReuseIdentifier: "ProductCell")
        }
        
        func setupCollectionView() {
            
            let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfCollection>(configureCell: {[weak self] (_, collectionView, indexPath, product) -> UICollectionViewCell in
                
                print(self?.model.title as Any)
                
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ProductCell", for: indexPath) as? ProductCollectionViewCell else {
                    return UICollectionViewCell()
                }
                
                cell.set(product)
                
                return cell
            })
            
            let section = [ SectionOfCollection(header: "Products", items: model.products.value) ]
            
            // This is where the error appears: "Generic parameter 'Self' could not be inferred"
            model.products.asObservable().bind(to: collectionViewCardPart.collectionView.rx.items(dataSource: dataSource)).disposed(by: bag)
        }
    }
    

    I have been able to get my data from the server and map all the ui components to the cards that I'm using as cells, but the datasource hasn't been set correctly due to this error and nothing ever appears!

    I did find something on this error based in RXDataSources and I was able to get the error to go away and compile with this solution but it did not bind the datasource so data still does not show.

    This one also has some logical explanations but also does not fix the issue either.

    Any help is appreciated!

    help wanted 
    opened by briannadoubt 7
  • Jazzy docs

    Jazzy docs

    Before you make a Pull Request, read the important guidelines:

    Issue Link :link:

    • Is this a bug fix or a feature? **Feature**
    • Does it break any existing functionality? **Unit Tests still pass**

    Goals of this PR :tada:

    • Why is the change important? hacktoberfest
    • What does this fix? https://github.com/intuit/CardParts/issues/58
    • How far has it been tested? N/A
    • [x] Remove docs/ from gitignore
    • This is where the magic happens: .jazzy.yaml
    • Preview: https://cardparts.surge.sh

How Has This Been Tested :mag:

Please let us know if you have tested your PR and if we need to reproduce the issues. Also, please let us know if we need any relevant information for running the tests.

  • N/A

Test Configuration :space_invader:

  • Xcode version: 10.3

Things to check on :dart:

  • [x] My Pull Request code follows the coding standards and styles of the project
  • [N/A] I have worked on unit tests and reviewed my code to the best of my ability
  • [x] I have used comments to make other coders understand my code better
  • [x] My changes are good to go without any warnings
  • [N/A] I have added unit tests both for the happy and sad path
  • [x] All of my unit tests pass successfully before pushing the PR
  • [x] I have made sure all dependent downstream changes impacted by my PR are working
opened by xtopolis 6
  • reload specific card inside CardPartsViewController by calling loadSpecificCards

    reload specific card inside CardPartsViewController by calling loadSpecificCards

    I'd like to reload one specific card after I get the data update from an async restful process. However, loadSpecificCards function is not available under the CardPartsViewController. I am wondering what's the correct method to achieve that. Also, I am wondering what the "indexPath" in the function is. Is it possible for someone to give an example? Thank you very much!

    import Foundation
    import CardParts
    import RxSwift
    
    class CardRecommendationController: CardPartsViewController {
        
        private var diseaseClient: DiseaseClient?
        private var progressManager: ProgressManager?
        private var currentPage = 0
        private var timer: Timer?
        
        let cardPartTextView = CardPartTextView(type: .normal)
        var recommendations = BehaviorSubject(value: ["1", "2"])
        var recommendationImages: [UIImage?] = [UIImage(systemName: "heart"), UIImage(systemName: "lungs")]
        let imageRecommendation = CardPartImageView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.progressManager = ProgressManager()
            self.diseaseClient = DiseaseClient()
            
            cardPartTextView.text = "Top Health Threats in the Country"
            cardPartTextView.font = UIFont.systemFont(ofSize: 18)
            cardPartTextView.textColor = .darkGray
            
            self.diseaseClient?.getDiseaseData(country: "US"){[unowned self] (diseaseData, error) in
                      if error != nil {
                        self.progressManager?.stopLoading()
                        return
                      }
                      if let diseaseData = diseaseData {
                        let viewModel3 = DiseaseViewModel(model: diseaseData)
                          viewModel3.cause.asObservable().bind(to: recommendations).disposed(by: bag)
                          var stackViews: [CardPartStackView] = []
                          do {
                              for (index, recommedationString) in try recommendations.value().enumerated() {
                                  
                                  let sv = CardPartStackView()
                                  sv.axis = .vertical
                                  sv.spacing = 8
                                  stackViews.append(sv)
                                  
                                  let strs = recommedationString.components(separatedBy: "|")
                                  
                                  let image = CardPartImageView()
                                  image.image = recommendationImages[index]
                                  image.contentMode = .scaleAspectFit
                                  image.tintColor = .lightGray
                                  sv.addArrangedSubview(image)
                                  
                                  let recom1 = CardPartTextView(type: .normal)
                                  recom1.text = String(index * 5 + 1) + ". " + strs[0]
                                  recom1.textAlignment = .center
                                  sv.addArrangedSubview(recom1)
                                  
                                  let recom2 = CardPartTextView(type: .normal)
                                  recom2.text = String(index * 5 + 2) + ". " + strs[1]
                                  recom2.textAlignment = .center
                                  sv.addArrangedSubview(recom2)
    
                                  let recom3 = CardPartTextView(type: .normal)
                                  recom3.text = String(index * 5 + 3) + ". " + strs[2]
                                  recom3.textAlignment = .center
                                  sv.addArrangedSubview(recom3)
                                  
                                  let recom4 = CardPartTextView(type: .normal)
                                  recom4.text = String(index * 5 + 4) + ". " + strs[3]
                                  recom4.textAlignment = .center
                                  sv.addArrangedSubview(recom4)
    
                                  let recom5 = CardPartTextView(type: .normal)
                                  recom5.text = String(index * 5 + 5) + ". " + strs[4]
                                  recom5.textAlignment = .center
                                  sv.addArrangedSubview(recom5)
                              }
                          } catch {
                              
                          }
                          
                          let cardPartPagedView = CardPartPagedView(withPages: stackViews, andHeight: 200)
                          
                          // To animate through the pages
                          timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: {[weak self] (_) in
                              
                              guard let this = self else { return }
                              do {
                                  let recomSize = try this.recommendations.value().count
                                  this.currentPage = this.currentPage == recomSize - 1 ? 0 : this.currentPage + 1
                              } catch{
                                  
                              }
                              cardPartPagedView.moveToPage(this.currentPage)
                          })
                          
                          setupCardParts([cardPartTextView, cardPartPagedView])
    
                         // I want to call loadSpecificCards(cards: <#T##[CardController]#>, indexPaths: <#T##[IndexPath]#>) method here 
                        //  but it is not available for CardPartsViewController. What should be the best practice?
    
    
                          self.progressManager?.stopLoading()
                      }
            }
            
        }
    }
    
    enhancement 
    opened by zhenyiy 0
  • Fix Array `safeValue(at:)`

    Fix Array `safeValue(at:)`

    Array extension safaValue(at:) method would crash if a negative index is used. https://github.com/intuit/CardParts/blob/3234eda81697e4a2e9e91f0a3673119dca996846/CardParts/src/Extensions/Array.swift#L11-L17

    Though it does not introduce any bug at present as there is only one occurrence of safeValue usage, in CardPartHistogramView, which does not use a negative index, the extension method would crash if a negative index is used. https://github.com/intuit/CardParts/blob/27126a702b8011a274d93a6f0ba5cab18d1bab8c/CardParts/src/Classes/Card%20Parts/CardPartHistogramView.swift#L83-L85

    Could we add a add a check for negative value to ensure that method work for all index values. (fix available in branch: https://github.com/mak-s/CardParts/tree/array_index_out_of_range)

    Also, CardParts code coverage is less. I'd like to add some Unit tests for existing classes available in CardParts.

    • Could you please let me know if I should create a new Issue for adding Unit tests.
    opened by mak-s 0
  • Different themes for different CardViewControllers?

    Different themes for different CardViewControllers?

    See title - ideally, have different themes assigned to CardViewController.

    I can kind of hack it together with inits and tracking when certain view controllers get init and not, but much easier to just be able to assign it rather than as a global.

    Thoughts?

    enhancement 
    opened by wimzee-rcheng 1
  • merge for tests

    merge for tests

    Before you make a Pull Request, read the important guidelines:

    Issue Link :link:

    • Is this a bug fix or a feature?
    • Does it break any existing functionality?

    Goals of this PR :tada:

    • Why is the change important?
    • What does this fix?
    • How far has it been tested?

    How Has This Been Tested :mag:

    Please let us know if you have tested your PR and if we need to reproduce the issues. Also, please let us know if we need any relevant information for running the tests.

    • User Interface Testing
    • Application Testing

    Test Configuration :space_invader:

    • Xcode version:
    • Device/Simulator
    • iOS version || MacOSX version

    Things to check on :dart:

    • [ ] My Pull Request code follows the coding standards and styles of the project
    • [ ] I have worked on unit tests and reviewed my code to the best of my ability
    • [ ] I have used comments to make other coders understand my code better
    • [ ] My changes are good to go without any warnings
    • [ ] I have added unit tests both for the happy and sad path
    • [ ] All of my unit tests pass successfully before pushing the PR
    • [ ] I have made sure all dependent downstream changes impacted by my PR are working
    opened by classicvalues 0
  • Capability to deselect table view row item

    Capability to deselect table view row item

    Before you make a Pull Request, read the important guidelines:

    Issue Link :link:

    • Is this a bug fix or a feature?
    • Adding additional capability to enable deselect functionality on click on TableView row item
    • Does it break any existing functionality?
    • No, as the code marked as optional

    Goals of this PR :tada:

    • Why is the change important?
    • To enable bulk edit functionality the row item need to select/deselect based on the user click.
    • What does this fix?
    • Expose the deselect API
    • How far has it been tested?
    • Didn't impact to any other functionality

    How Has This Been Tested :mag:

    Please let us know if you have tested your PR and if we need to reproduce the issues. Also, please let us know if we need any relevant information for running the tests.

    • User Interface Testing
    • Application Testing

    Test Configuration :space_invader:

    • Xcode version:
    • 12.0.1
    • Device/Simulator
    • 11 Pro
    • iOS version || MacOSX version
    • 10.15.6

    Things to check on :dart:

    • [x] My Pull Request code follows the coding standards and styles of the project
    • [x] I have worked on unit tests and reviewed my code to the best of my ability
    • [x] I have used comments to make other coders understand my code better
    • [x] My changes are good to go without any warnings
    • [x] I have added unit tests both for the happy and sad path
    • [x] All of my unit tests pass successfully before pushing the PR
    • [x] I have made sure all dependent downstream changes impacted by my PR are working
    opened by lsahoo1-intuit 0
  • Added SPM support

    Added SPM support

    Issue Link https://github.com/intuit/CardParts/issues/172

    Closes https://github.com/intuit/CardParts/issues/172

    Goals of this PR :tada:

    Adds support for the Swift Package Manager, as an alternative to CocoaPods.

    How Has This Been Tested :mag:

    Adds a single test CardPartImageViewTests.testAssetResources to check that resources are properly loaded in an SPM environment.

    Test Configuration :space_invader:

    • Xcode version: 12.0 (12A7209)
    • Device/Simulator: N/A
    • iOS version: N/A

    Things to check on :dart:

    • [x] My Pull Request code follows the coding standards and styles of the project
    • [x] I have worked on unit tests and reviewed my code to the best of my ability
    • [x] I have used comments to make other coders understand my code better
    • [ ] My changes are good to go without any warnings
    • [x] I have added unit tests both for the happy and sad path
    • [x] All of my unit tests pass successfully before pushing the PR
    • [x] I have made sure all dependent downstream changes impacted by my PR are working
    opened by calebkleveter 2
  • Releases(4.0.0)
    Owner
    Intuit
    Powering prosperity around the world.
    Intuit
    IOS Card Game - A simple card game using SwiftUI

    IOS_Card_Game A simple card game using Swift UI.

    Md. Masum Musfique 1 Mar 25, 2022
    A SwiftUI based custom sheet card to show information in iOS application.

    A SwiftUI based custom sheet card to show any custom view inside the card in iOS application.

    Mahmud Ahsan 4 Mar 28, 2022
    Card-based view controller for apps that display content cards with accompanying maps, similar to Apple Maps.

    TripGo Card View Controller This is a repo for providing the card-based design for TripGo as well as the TripKitUI SDK by SkedGo. Specs 1. Basic funct

    SkedGo 6 Oct 15, 2022
    An iOS library to create beautiful card transitions.

    CSCardTransition CSCardTransition is a small library allowing you to create wonderful push and pop transition animations like in the App Store. It wor

    Creastel 5 Jan 14, 2022
    A SwiftUI card view, made great for setup interactions.

    SlideOverCard A SwiftUI card design, similar to the one used by Apple in HomeKit, AirPods, Apple Card and AirTag setup, NFC scanning, Wi-Fi password s

    João Gabriel 716 Dec 29, 2022
    This UI attempts to capture the Quibi Card Stack and the associated User Interaction.

    RGStack This UI attempts to capture the Quibi Card Stack and the associated User Interaction. Required A View that conforms to the ConfigurableCard pr

    RGeleta 96 Dec 18, 2022
    Card flip animation by pan gesture.

    CardAnimation Design from Dribble. 实现思路在这里。 Two Solutions At the begin, I didn't encapsulate code, @luxorules refactor code into class and improve it

    null 1.2k Dec 14, 2022
    Awesome looking Dial like card selection ViewController

    KVCardSelectionVC Awesome looking Dial like card selection ViewController An updated Swift 3 working version of : https://github.com/atljeremy/JFCardS

    Kunal Verma 23 Feb 1, 2021
    🃏 Tinder like card interface

    Features Swift 3 Custom views for the card & overlay Generic Dynamically add new cards on top or on the bottom Lazy view loading Setup pod 'DMSwipeCar

    Dylan Marriott 250 Nov 15, 2022
    Cusom CollectionView card layout

    MMCardView Example To run the example project, clone the repo, and run pod install from the Example directory first. Demo 1.Card Requirements iOS 8.0+

    Millman Yang 556 Dec 5, 2022
    :star: Custom card-designed CollectionView layout

    CardsLayout CardsLayout is a lightweight Collection Layout. Installation CocoaPods You can use CocoaPods to install CardsLayout by adding it to your P

    Filipp Fediakov 798 Dec 28, 2022
    🔥 A multi-directional card swiping library inspired by Tinder

    Made with ❤️ by Mac Gallagher Features ?? Advanced swipe recognition based on velocity and card position ?? Manual and programmatic actions ?? Smooth

    Mac Gallagher 754 Dec 28, 2022
    SimpleCardView-SwiftUI is a very simple card view written with SwiftUI

    SimpleCardView-SwiftUI is a very simple card view written with SwiftUI

    Tomortec 3 May 19, 2022
    GLScratchCard - Scratch card effect

    I loved the way payments app's like Google pay and PhonePe used scratch card option to reward it's user. Hence with ?? cloned the same scratch card effect for you guys out there

    Gokul 84 Dec 5, 2022
    A marriage between the Shazam Discover UI and Tinder, built with UICollectionView in Swift.

    VerticalCardSwiper A marriage between the Shazam Discover UI and Tinder, built with UICollectionView in Swift. Project goal and information The goal o

    Joni Van Roost 1.2k Dec 28, 2022
    A easy-to-use SwiftUI view for Tinder like cards on iOS, macOS & watchOS.

    ?? CardStack A easy-to-use SwiftUI view for Tinder like cards on iOS, macOS & watchOS. Installation Xcode 11 & Swift Package Manager Use the package r

    Deniz Adalar 285 Jan 3, 2023
    KolodaView is a class designed to simplify the implementation of Tinder like cards on iOS.

    KolodaView Check this article on our blog. Purpose KolodaView is a class designed to simplify the implementation of Tinder like cards on iOS. It adds

    Yalantis 5.2k Jan 2, 2023
    Awesome iOS 11 appstore cards in swift 5.

    Cards brings to Xcode the card views seen in the new iOS XI Appstore. Getting Started Storyboard Go to main.storyboard and add a blank UIView Open the

    Paolo Cuscela 4.1k Dec 14, 2022
    Reactive WebSockets - A lightweight abstraction layer over Starscream to make it reactive.

    RxWebSocket Reactive extensions for websockets. A lightweight abstraction layer over Starscream to make it reactive. Installation RxWebSocket is avail

    Flávio Caetano 57 Jul 22, 2022
    CreditCardForm is iOS framework that allows developers to create the UI which replicates an actual Credit Card.

    CreditCardForm CreditCardForm is iOS framework that allows developers to create the UI which replicates an actual Credit Card. Fixed typo use CreditCa

    Orazz 1.4k Dec 15, 2022