Presenting timelines as cards, single or bundled in scrollable feed!

Overview

TimelineCards

Licence Version Swift Version Licence

πŸƒ Autogenerated timelines presented as cards πŸƒ

πŸƒ Single or bundled into feed πŸƒ

Cards Feed Single Card Card Samples


Installation

CocoaPods (wtf is that?)

  1. Add pod 'TimelineCards' to your Podfile;
  2. Then run pod update in Terminal;
  3. Re-open your project using .xcworkspace, put import TimelineCards in the swift files you plan to create awesome cards from (or use bridging in Obj-C projects);
  4. Rebuild and enjoy.

Old School Way

Drop folder with .swift source files to your project. Done.

Usage

TL;DR

Go to Demo project, it has a lot of things demonstrated! If you get confused with anything, you're welcome to continue reading.

Single Card

Cards in TimelineCards kit are highly customizable views that present ordered set of data. They were designed to describe events or milestones, but nothing prevents you from using them for different purposes, given that every element can have unlimited complexity.

Cards support elements grouping, although current version doesn't support recursively nested groups (and probably never will).

Single Card

Creation

If you want to present a single static timeline β€” TimelineCard object is just what you need. Let's create it:

// Let's say you want card to be 80% of its superview's width
let timelineWidth: CGFloat = view.bounds.width * 0.8

let demoCard = TimelineCard(width: timelineWidth)
demoCard.dataProvider = self
demoCard.eventsHandler = self
view.addSubview(demoCard) // Calls `reloadData()` implicitly

// Optionally
demoCard.reloadData()

Height of the card will be calculated automatically based on its data source, and available after reloadData() method execution, or after card is added to superview.

Customization

The creation code above is enough for a simple card with neutral (but nice) appearance to work. Nevertheless, card appearance can be customized in a number of ways. Here are some examples:

demoCard.backgroundColor = .white
demoCard.borderAppearance = (.orange, 2.0) // color and width
demoCard.cornerRadius = 20.0
demoCard.lineColor = .darkGray
demoCard.itemShapeHeight = 30.0 // basically diameter of milestone "circle" thing
demoCard.timelinePathWidth = 2.0 // width of drawn timeline itself
demoCard.margins = (20, 10, 20, 10) // css-like notation

You can also set card header and footer to be any custom UIView you'd like. Card will update its layout accordingly.

let header = UIView(frame: CGRect(x: 0, y: 0, width: detailsCard.bounds.width, height: 60))
header.backgroundColor = .purple
demoCard.headerView = header

let footer = UIView(frame: CGRect(x: 0, y: 0, width: detailsCard.bounds.width, height: 100))
footer.backgroundColor = .purple
demoCard.footerView = footer

As soon as you make any of the above updates, card rebuilds itself automatically. It you want to rule this process manually (saves resources), just turn this feature off:

demoCard.autoreload = false

Data Source and Events Handling

First, make your data provider comply with TimelineCardDataProvider and TimelineCardEventsHandler protocols by adding them to corresponding class declaration.

Implementing TimelineCardDataProvider

Now, let's send some data to your card when it requests so. You do this by creating array consisting of TimelineItem and/or TimelineItemGroup objects, which are the main data units that you use in TimelineCards kit. They both comply with TimelineSourceElement protocol β€” type that you must return in result.

func elementsForTimelineCard(_ timelineCard: TimelineCard, containerWidth: CGFloat) -> [TimelineSourceElement] {
	var cardSource = [] as [TimelineSourceElement]

	for someData in myDataModel.objects {
		if someData.isGroup {
			var childTimelineItems = [TimelineItem]()
			for childData in someData.children {
				let timelineItem = TimelineItem(...)
				childTimelineItems.append(timelineItem)
			}

			let timelineItemGroup = TimelineItemGroup(...)
			cardSource.append(timelineItemGroup)
		} else {
			let timelineItem = TimelineItem(...)
			cardSource.append(timelineItem)
		}
	}

	return cardSource
}

Note: containerWidth gives you info about width of containers that your custom item description views will be added to. Anything beyound this width limit will be invisible.

There are two options of creating TimelineItem and TimelineItemGroup.

  1. Using simple preset with only Title and Subtitle to be shown for item. You can still affect their appearance because you send attributed strings as parameters:
let attrubitedTitle = NSAttributedString(string: "Event title", attributes: [.foregroundColor : UIColor.white])
let attrubitedSubTitle = NSAttributedString(string: "Event subtitle", attributes: [.foregroundColor : UIColor.white])

let simpleItemOne = TimelineItem(title: attrubitedTitle, subtitle: attrubitedSubTitle, icon: UIImage(named: "icon.png"))

let simpleItemTwo = TimelineItem(title: simpleItemOne, subtitle: attrubitedSubTitle, icon: UIImage(named: "icon.png"))

// And, if you want them to be part of the group
let groupItem = TimelineItemGroup(title: attrubitedTitle, subtitle: attrubitedSubTitle, items: [simpleItemOne, simpleItemTwo], icon: UIImage(named: "icon.png"))
  1. Using custom view of any height (but limited to containerWidth) to describe item in the way you want:
let itemDescView = UIView(frame: CGRect(x: 0, y: 0, width: containerWidth, height: 65.0))
itemDescView.backgroundColor = .lightGray
// Customize it the way you want!

let simpleItemOne = TimelineItem.init(customView: itemDescView, icon: UIImage(named: "icon.png"))

let simpleItemTwo = TimelineItem.init(customView: itemDescView, icon: UIImage(named: "sub_icon.png"))

// And, if you want them to be part of the group
let groupItem = TimelineItemGroup(customView: itemDescView, items: [simpleItemOne, simpleItemTwo], icon: UIImage(named: "sub_icon.png"))

This way you build array of uniquely customized items for the card.

Implementing TimelineCardEventsHandler

This one is pretty straight-forward ans self-describing. You just use thise methods to handle events from cards:

func didSelectElement(at index: Int, in timelineCard: TimelineCard)

func didSelectSubElement(at index: (Int, Int), in timelineCard: TimelineCard)

func didTouchHeaderView(_ headerView: UIView, in timelineCard: TimelineCard)

func didTouchFooterView(_ footerView: UIView, in timelineCard: TimelineCard)

Feed of Cards

Cards Feed is represented by TimelineFeed view, which is basically a vertical scroll of TimelineCard objects. It uses UITableView internally to offer memory-efficient reusability, which makes it possible to build feed consisting of large amount of cards.

Card Feed

Creation

Initialize new TimelineFeed object and set its dataSource and delegate:

let timelineWidth: CGFloat = view.bounds.width * 0.8

let timelineFeed = TimelineFeed(frame: CGRect(x: 0, y: 0, width: view.bounds.width * 0.8, height: view.bounds.height))
timelineFeed.center = view.center

timelineFeed.dataSource = self
timelineFeed.delegate = self

// Optional customization options
timelineFeed.paddingBetweenCards = 20.0
timelineFeed.topMargin = 20.0
timelineFeed.bottomMargin = 20.0

view.addSubview(timelineFeed)
timelineFeed.reloadData()

Data Source and Events Handling

Make your data provider comply with TimelineFeedDataSource and TimelineFeedDelegate protocols by adding them to corresponding class declaration.

Implementing TimelineFeedDataSource

Start with method that tells feed how many cards you want it to present:

func numberOfCards(in timelineFeed: TimelineFeed) -> Int {
	return timelinesCollection.items.count
}

Now, let's initialize new card every time feed asks us to for given index:

func card(at index: Int, in timelineFeed: TimelineFeed) -> TimelineCard {
	let timelineCard = TimelineCard(width: timelineFeed.bounds.width)
	// Customize as you'd do with Single Card
	return timelineCard
}

Note: DO NOT set dataProvider or eventHandler for TimelineCard object here. TimelineFeed is responsible for this.

Good! Now, whenever particular card is about to be reused in feed, it will kindly ask you to provide data for it. This is very similar to what we did for a Single Card. Just create some TimelineSourceElements:

func elementsForTimelineCard(at index: Int, containerWidth: CGFloat) -> [TimelineSourceElement] {
	var elements = [] as [TimelineSourceElement]

	// Creating those `TimelineItem` and/or `TimelineItemGroup` objects..

	return elements
}

Ok, cards are set up and running smoothly, but you can also add headers on top of any card, so that we can keep track of this endless scrolling madness. As for many other features, you have two options here.

  1. Keep it simple and use attributed Title and Subtitle preset (or just Title if you want to keep it minimal):
func titleAndSubtitle(at index: Int, in timelineFeed: TimelineFeed) -> (NSAttributedString, NSAttributedString?)? {

	let timelineData = timelinesCollection.items[index]

	let testTitle = NSAttributedString(string: "Timeline Card #\(index)", attributes: [.foregroundColor : UIColor.white, .font : UIFont(name: "HelveticaNeue-Bold", size: 23.0)])

	let testSubtitle = NSAttributedString(string: "Subtitle text", attributes: [.foregroundColor : UIColor.white])

	return (testTitle, testSubtitle)

	// Subtitle is optional
	//return (testTitle, nil)
}
  1. Use custom UIView:
func headerViewForCard(at index: Int, in timelineFeed: TimelineFeed) -> UIView? {
	let customHeader = UIView(frame: CGRect(x: 0, y: 0, width: timelineFeed.bounds.width, height: 60.0))
	customHeader.backgroundColor = .purple
	return customHeader
}
Implementing TimelineFeedDelegate

Fairly simple and similar to event handling for a Single Card. The difference is that you get index of the card where event did occur.

func didSelectElement(at index: Int, timelineCardIndex: Int)

func didSelectSubElement(at index: (Int, Int), timelineCardIndex: Int)

func didTouchHeaderView(_ headerView: UIView, timelineCardIndex: Int)

func didTouchFooterView(_ footerView: UIView, timelineCardIndex: Int)

TODO

  • Support for .square and .diamond milestone shapes
  • Horizontal scrolling/paging
  • Force-touch peek for embedded elements

License

TimelineCards is released under an MIT license. See the LICENSE file.

You might also like...
A SwiftUI view that arranges its children in a whimsical interactive deck of cards, as seen in Big News
A SwiftUI view that arranges its children in a whimsical interactive deck of cards, as seen in Big News

CardStack A SwiftUI view that arranges its children in a whimsical interactive deck of cards. CardStack mimics the behaviour of the photo stack in iMe

Presenting timelines as cards, single or bundled in scrollable feed!
Presenting timelines as cards, single or bundled in scrollable feed!

TimelineCards πŸƒ Autogenerated timelines presented as cards πŸƒ πŸƒ Single or bundled into feed πŸƒ Installation CocoaPods (wtf is that?) Add pod 'Timeli

Essential Feed App – Image Feed Feature

Essential Feed App – Image Feed Feature

An adaptive scrollable graph view for iOS to visualise simple discrete datasets. Written in Swift.
An adaptive scrollable graph view for iOS to visualise simple discrete datasets. Written in Swift.

ScrollableGraphView Announcements 9-7-2017 - Version 4: Version 4 was released which adds multiple plots, dynamic reloading of values, more reference

Scrollable UINavigationBar that follows the scrolling of a UIScrollView
Scrollable UINavigationBar that follows the scrolling of a UIScrollView

A custom UINavigationController that enables the scrolling of the navigation bar alongside the scrolling of an observed content view Versioning notes

COVID Certificate is the official app for storing and presenting COVID certificates issued in Switzerland.
COVID Certificate is the official app for storing and presenting COVID certificates issued in Switzerland.

COVID Certificate is the official app for storing and presenting COVID certificates issued in Switzerland. The certificates are kept and checked locally on the user's phone.

Core Charts | Basic Scrollable Chart Library for iOS
Core Charts | Basic Scrollable Chart Library for iOS

Core Charts | Basic Chart Library for iOS HCoreBarChart VCoreBarChart Requirements Installation Usage Appearance Customization Getting Started You nee

iOS drop-in library presenting a 2048-style game
iOS drop-in library presenting a 2048-style game

iOS-2048 iOS drop-in library presenting a clean-room Objective-C/Cocoa implementation of the game 2048. Screenshot Instructions The included sample ap

LNPopupController is a framework for presenting view controllers as popups of other view controllers, much like the Apple Music and Podcasts apps.
LNPopupController is a framework for presenting view controllers as popups of other view controllers, much like the Apple Music and Podcasts apps.

LNPopupController LNPopupController is a framework for presenting view controllers as popups of other view controllers, much like the Apple Music and

β›© Presenting custom views as a popup in iOS.
β›© Presenting custom views as a popup in iOS.

FFPopup is a lightweight library for presenting custom views as a popup. Bounce from Top & Bounce to Bottom Bounce from Top & Bounce to Top Bounce in

INTUZ is presenting an interesting a Multilevel Expand/Collapse UITableView App Control to integrate inside your native iOS-based application
INTUZ is presenting an interesting a Multilevel Expand/Collapse UITableView App Control to integrate inside your native iOS-based application

INTUZ is presenting an interesting a Multilevel Expand/Collapse UITableView App Control to integrate inside your native iOS-based application. MultilevelTableView is a simple component, which lets you use the tableview with multilevel tree view in your project.

Light and scrollable view controller for tvOS to present blocks of text
Light and scrollable view controller for tvOS to present blocks of text

TvOSTextViewer Light and scrollable view controller for tvOS to present blocks of text Description TvOSTextViewer is a view controller to present bloc

This is an iOS control for presenting any UIView in an UIAlertController like manner
This is an iOS control for presenting any UIView in an UIAlertController like manner

RMActionController This framework allows you to present just any view as an action sheet. In addition, it allows you to add actions around the present

A tiny Safari Web Extension for presenting highlighted JSON files
A tiny Safari Web Extension for presenting highlighted JSON files

A tiny Safari Web Extension for presenting highlighted JSON files

TomatoVerticalMenuMania lets you create a vertical, scrollable menu to the left of the screen.
TomatoVerticalMenuMania lets you create a vertical, scrollable menu to the left of the screen.

TomatoVerticalMenuMania Framework Development and compatibility Development platform: iOS Language: Swift Compatibility: iOS 13 or greater Description

INTUZ is presenting an interesting a custom alert view in SwiftUI
INTUZ is presenting an interesting a custom alert view in SwiftUI

Introduction INTUZ is presenting an interesting a custom alert view in SwiftUI, App Control to integrate inside your native iOS-based application. Cus

A project that uses the Flickr image search API and shows the results in a 3-column scrollable collection view

FlickrImagesDemo FlickrImagesDemo is a project that uses the Flickr image search API and shows the results in a 3-column scrollable collection view. 


Sample iOS application in SwiftUI presenting Redux architecture
Sample iOS application in SwiftUI presenting Redux architecture

SwiftUI-Redux-Demo Sample iOS application in SwiftUI presenting Redux architecture. My full article about Redux in detail you will find here: Redux ar

A simple User Profile screen with scrollable detail area
A simple User Profile screen with scrollable detail area

User Profile Screen with Scrollable Detail Area This simple user profile screen skeleton project can be used as a starting point for creating more sop

Comments
  • Issue with Layout Constraints

    Issue with Layout Constraints

    Hi,

    i have an issue with the following code in TimelineFeed:

    	private func addCard(_ card: TimelineCard) {
    		self.card = card
            
    		card.reload()
    		
    		card.frame = CGRect(origin: CGPoint(x: 0.0, y: (headerView?.frame.origin.y ?? 0.0) + (headerView?.frame.height ?? 0.0)), size: card.bounds.size)
    		
    		self.insertSubview(card, at: subviews.count)
            
    		let constraints: [NSLayoutConstraint] = [
    			NSLayoutConstraint(item: headerView ?? self, attribute: headerView != nil ? .bottom : .top, relatedBy: .equal, toItem: card, attribute: .top, multiplier: 1, constant: 1),
    			NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: card, attribute: .leading, multiplier: 1, constant: 1),
    			NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: card, attribute: .trailing, multiplier: 1, constant: 1),
    			NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: card, attribute: .bottom, multiplier: 1, constant: bottomPadding),
    			NSLayoutConstraint(item: card, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: card.bounds.size.height)
    		]
    
    		constraints.forEach { $0.isActive = true }
    	}
    

    It generates the following error messages:

    2021-01-06 18:07:02.636373+0100 EinfachLeichterLeben[20536:946407] [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. ( "<NSLayoutConstraint:0x60000250d630 _TtC20EinfachLeichterLebenP33_CD7A8457D9CF40B1C3DACE082F374E5216TimelineFeedCell:0x7fa758d40cc0'TimelineFeedCell'.top == EinfachLeichterLeben.TimelineCard:0x7fa758d414a0.top + 1 (active)>", "<NSLayoutConstraint:0x60000250d270 V:[EinfachLeichterLeben.TimelineCard:0x7fa758d414a0]-(20)-| (active, names: '|':_TtC20EinfachLeichterLebenP33_CD7A8457D9CF40B1C3DACE082F374E5216TimelineFeedCell:0x7fa758d40cc0'TimelineFeedCell' )>", "<NSLayoutConstraint:0x60000250f160 EinfachLeichterLeben.TimelineCard:0x7fa758d414a0.height == 192 (active)>", "<NSLayoutConstraint:0x600002574870 'UIView-Encapsulated-Layout-Height' _TtC20EinfachLeichterLebenP33_CD7A8457D9CF40B1C3DACE082F374E5216TimelineFeedCell:0x7fa758d40cc0'TimelineFeedCell'.height == 211.333 (active)>" )

    Will attempt to recover by breaking constraint <NSLayoutConstraint:0x60000250f160 EinfachLeichterLeben.TimelineCard:0x7fa758d414a0.height == 192 (active)>

    Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

    Latest Xcode, iOS 13 Target

    Do you have any ideas, what i could do? It think it also breaks the delegate methods for the header and the card elements.

    Thanks! Kay.

    opened by kayloehmann 0
  • EXC_BAD_ACCESS during cleanUp() only with Group Items

    EXC_BAD_ACCESS during cleanUp() only with Group Items

    Hi,

    Everything works until I add sub-items to a group, at which point the above error occurs. If I add a Group item without any items, the Group heading displays.

    It's all good, except for the oddity with Group Items.

    Any ideas?

    TIA.

    opened by b0gm0nster 0
Owner
0xNSHuman
Degen armed with math.
0xNSHuman
A navigation controller that displays its view controllers as an interactive stack of cards.

CardNavigation The easiest way to turn a navigation controller into an interactive stack of cards. Highlights βœ… Fully interactive and interruptible βœ…

James Randolph 41 Sep 29, 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
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
A swift SDK to help you scan debit/credit cards.

SKCardReader A swift SDK to help you scan debit/credit cards. Requirements To use the SDK the following requirements must be met: Xcode 11.0 or newer

Syed Kashan 4 Jul 29, 2022
Swift Package to download Transactions for LunchOnUs Cards

LunchOnUs Downloader What This is a small library to download transaction and balance data for LunchOnUs Cards (Giftcards by Eigen Development). How C

Steffen KΓΆtte 0 Jan 15, 2022
Swipe able, customizable card stack view, Tinder like card stack view based on UICollectionView. Cards UI

Swipable, customizable card stack view, Tinder like card stack view based on UICollectionView. Cards UI Π‘ocoapods installation Add in your Podfile: po

Indy 850 Nov 17, 2022
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
Recreation of cards from Apple's AppStore written using SwiftUI.

App Store Cards Animation I tried to reproduce the look and the feeling of the cards from the AppStore. Please note that this repository is a work-in-

Roman 3 Mar 30, 2022
πŸƒ Cardslider is a design UI controller that allows you to swipe through cards with pictures and accompanying descriptions.

CARD SLIDER UI controller that allows you to swipe through cards with pictures. We specialize in the designing and coding of custom UI for Mobile Apps

Ramotion 1.2k Dec 19, 2022