{"id":2537,"url":"https://github.com/0xNSHuman/TimelineCards","last_synced_at":"2025-08-03T00:32:04.423Z","repository":{"id":39894579,"uuid":"112837606","full_name":"0xNSHuman/TimelineCards","owner":"0xNSHuman","description":"Presenting timelines as cards, single or bundled in scrollable feed!","archived":false,"fork":false,"pushed_at":"2022-05-06T04:28:56.000Z","size":6119,"stargazers_count":429,"open_issues_count":3,"forks_count":52,"subscribers_count":10,"default_branch":"master","last_synced_at":"2024-05-29T04:51:09.690Z","etag":null,"topics":["card","cards","feed","milestones","timeline"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/0xNSHuman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-12-02T11:44:01.000Z","updated_at":"2024-05-17T19:09:32.000Z","dependencies_parsed_at":"2022-08-21T05:20:39.437Z","dependency_job_id":null,"html_url":"https://github.com/0xNSHuman/TimelineCards","commit_stats":null,"previous_names":["vladaverin24/timelinecards"],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xNSHuman%2FTimelineCards","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xNSHuman%2FTimelineCards/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xNSHuman%2FTimelineCards/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/0xNSHuman%2FTimelineCards/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/0xNSHuman","download_url":"https://codeload.github.com/0xNSHuman/TimelineCards/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228510730,"owners_count":17931759,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["card","cards","feed","milestones","timeline"],"created_at":"2024-01-05T20:16:16.244Z","updated_at":"2024-12-06T18:30:34.335Z","avatar_url":"https://github.com/0xNSHuman.png","language":"Swift","funding_links":[],"categories":["UI","Swift"],"sub_categories":["Cards"],"readme":"\u003ch1 align=\"center\"\u003e TimelineCards \u003c/h1\u003e\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://opensource.org/licenses/MIT\"\u003e\u003cimg alt=\"Licence\" src=\"https://img.shields.io/badge/license-MIT-green.svg\" /\u003e\u003c/a\u003e\n\u003ca href=\"\"\u003e\u003cimg alt=\"Version\" src=\"https://img.shields.io/badge/version-1.0.5-blue.svg\" /\u003e\u003c/a\u003e\n\u003ca href=\"\"\u003e\u003cimg alt=\"Swift Version\" src=\"https://img.shields.io/badge/swift_versions-3.2|4.0-orange.svg\" /\u003e\u003c/a\u003e\n\u003ca href=\"https://cocoapods.org/pods/TimelineCards\"\u003e\u003cimg alt=\"Licence\" src=\"https://img.shields.io/badge/pod-TimelineCards-red.svg\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n🃏 Autogenerated timelines presented as cards 🃏\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n🃏 Single or bundled into feed 🃏\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n\u003cimg width=\"30%\" height=\"auto\" alt=\"Cards Feed\" src=\"https://raw.githubusercontent.com/0xNSHuman/TimelineCards/master/Screenshots/timeline_feed.gif\" /\u003e\n\u003cimg width=\"30%\" height=\"auto\" alt=\"Single Card\" src=\"https://raw.githubusercontent.com/0xNSHuman/TimelineCards/master/Screenshots/card_randomized.gif\" /\u003e\n\u003cimg width=\"30%\" height=\"auto\" alt=\"Card Samples\" src=\"https://raw.githubusercontent.com/0xNSHuman/TimelineCards/master/Screenshots/card_samples.gif\" /\u003e\n\u003c/p\u003e\n\n\u003chr\u003e\n\n## Installation\n### CocoaPods ([wtf is that?](https://cocoapods.org/about))\n1. Add `pod 'TimelineCards'` to your `Podfile`;\n2. Then run `pod update` in Terminal;\n3. 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);\n4. Rebuild and enjoy.\n\n### Old School Way\nDrop folder with `.swift` source files to your project. Done.\n\n## Usage\n### TL;DR\n\nGo to Demo project, it has a lot of things demonstrated! If you get confused with anything, you're welcome to continue reading.\n\n### Single Card\n\nCards 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.\n\nCards support elements grouping, although current version doesn't support recursively nested groups (~~and probably never will~~).\n\n\u003cp\u003e\n\u003cimg width=\"30%\" height=\"auto\" alt=\"Single Card\" src=\"https://raw.githubusercontent.com/0xNSHuman/TimelineCards/master/Screenshots/card.PNG\" /\u003e\n\u003c/p\u003e\n\n#### Creation\nIf you want to present a single static timeline — `TimelineCard` object is just what you need. Let's create it:\n\n```Swift\n// Let's say you want card to be 80% of its superview's width\nlet timelineWidth: CGFloat = view.bounds.width * 0.8\n\nlet demoCard = TimelineCard(width: timelineWidth)\ndemoCard.dataProvider = self\ndemoCard.eventsHandler = self\nview.addSubview(demoCard) // Calls `reloadData()` implicitly\n\n// Optionally\ndemoCard.reloadData()\n```\nHeight 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.\n\n#### Customization\n\nThe 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:\n\n```Swift\ndemoCard.backgroundColor = .white\ndemoCard.borderAppearance = (.orange, 2.0) // color and width\ndemoCard.cornerRadius = 20.0\ndemoCard.lineColor = .darkGray\ndemoCard.itemShapeHeight = 30.0 // basically diameter of milestone \"circle\" thing\ndemoCard.timelinePathWidth = 2.0 // width of drawn timeline itself\ndemoCard.margins = (20, 10, 20, 10) // css-like notation\n```\n\nYou can also set card **header** and **footer** to be any custom `UIView` you'd like. Card will update its layout accordingly.\n\n```Swift\nlet header = UIView(frame: CGRect(x: 0, y: 0, width: detailsCard.bounds.width, height: 60))\nheader.backgroundColor = .purple\ndemoCard.headerView = header\n\nlet footer = UIView(frame: CGRect(x: 0, y: 0, width: detailsCard.bounds.width, height: 100))\nfooter.backgroundColor = .purple\ndemoCard.footerView = footer\n```\n\nAs 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:\n\n```Swift\ndemoCard.autoreload = false\n```\n\n#### Data Source and Events Handling\n\nFirst, make your data provider comply with `TimelineCardDataProvider` and `TimelineCardEventsHandler` protocols by adding them to corresponding class declaration.\n\n##### Implementing `TimelineCardDataProvider`\n\nNow, 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.\n\n```Swift\nfunc elementsForTimelineCard(_ timelineCard: TimelineCard, containerWidth: CGFloat) -\u003e [TimelineSourceElement] {\n\tvar cardSource = [] as [TimelineSourceElement]\n\n\tfor someData in myDataModel.objects {\n\t\tif someData.isGroup {\n\t\t\tvar childTimelineItems = [TimelineItem]()\n\t\t\tfor childData in someData.children {\n\t\t\t\tlet timelineItem = TimelineItem(...)\n\t\t\t\tchildTimelineItems.append(timelineItem)\n\t\t\t}\n\n\t\t\tlet timelineItemGroup = TimelineItemGroup(...)\n\t\t\tcardSource.append(timelineItemGroup)\n\t\t} else {\n\t\t\tlet timelineItem = TimelineItem(...)\n\t\t\tcardSource.append(timelineItem)\n\t\t}\n\t}\n\n\treturn cardSource\n}\n```\n\n*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.*\n\nThere are two options of creating `TimelineItem` and `TimelineItemGroup`.\n\n1. 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:\n\n```Swift\nlet attrubitedTitle = NSAttributedString(string: \"Event title\", attributes: [.foregroundColor : UIColor.white])\nlet attrubitedSubTitle = NSAttributedString(string: \"Event subtitle\", attributes: [.foregroundColor : UIColor.white])\n\nlet simpleItemOne = TimelineItem(title: attrubitedTitle, subtitle: attrubitedSubTitle, icon: UIImage(named: \"icon.png\"))\n\nlet simpleItemTwo = TimelineItem(title: simpleItemOne, subtitle: attrubitedSubTitle, icon: UIImage(named: \"icon.png\"))\n\n// And, if you want them to be part of the group\nlet groupItem = TimelineItemGroup(title: attrubitedTitle, subtitle: attrubitedSubTitle, items: [simpleItemOne, simpleItemTwo], icon: UIImage(named: \"icon.png\"))\n```\n\n2. Using custom view of any height (but limited to `containerWidth`) to describe item in the way you want:\n\n```Swift\nlet itemDescView = UIView(frame: CGRect(x: 0, y: 0, width: containerWidth, height: 65.0))\nitemDescView.backgroundColor = .lightGray\n// Customize it the way you want!\n\nlet simpleItemOne = TimelineItem.init(customView: itemDescView, icon: UIImage(named: \"icon.png\"))\n\nlet simpleItemTwo = TimelineItem.init(customView: itemDescView, icon: UIImage(named: \"sub_icon.png\"))\n\n// And, if you want them to be part of the group\nlet groupItem = TimelineItemGroup(customView: itemDescView, items: [simpleItemOne, simpleItemTwo], icon: UIImage(named: \"sub_icon.png\"))\n```\n\nThis way you build array of uniquely customized items for the card.\n\n##### Implementing `TimelineCardEventsHandler`\n\nThis one is pretty straight-forward ans self-describing. You just use thise methods to handle events from cards:\n\n```Swift\nfunc didSelectElement(at index: Int, in timelineCard: TimelineCard)\n\nfunc didSelectSubElement(at index: (Int, Int), in timelineCard: TimelineCard)\n\nfunc didTouchHeaderView(_ headerView: UIView, in timelineCard: TimelineCard)\n\nfunc didTouchFooterView(_ footerView: UIView, in timelineCard: TimelineCard)\n```\n\n\u003chr\u003e\n\n### Feed of Cards\n\nCards 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.\n\n\u003cp\u003e\n\u003cimg width=\"30%\" height=\"auto\" alt=\"Card Feed\" src=\"https://raw.githubusercontent.com/0xNSHuman/TimelineCards/master/Screenshots/feed.PNG\" /\u003e\n\u003c/p\u003e\n\n#### Creation\nInitialize new `TimelineFeed` object and set its `dataSource` and `delegate`:\n\n```Swift\nlet timelineWidth: CGFloat = view.bounds.width * 0.8\n\nlet timelineFeed = TimelineFeed(frame: CGRect(x: 0, y: 0, width: view.bounds.width * 0.8, height: view.bounds.height))\ntimelineFeed.center = view.center\n\ntimelineFeed.dataSource = self\ntimelineFeed.delegate = self\n\n// Optional customization options\ntimelineFeed.paddingBetweenCards = 20.0\ntimelineFeed.topMargin = 20.0\ntimelineFeed.bottomMargin = 20.0\n\nview.addSubview(timelineFeed)\ntimelineFeed.reloadData()\n```\n\n#### Data Source and Events Handling\n\nMake your data provider comply with `TimelineFeedDataSource` and `TimelineFeedDelegate` protocols by adding them to corresponding class declaration.\n\n##### Implementing `TimelineFeedDataSource`\n\nStart with method that tells feed how many cards you want it to present:\n\n```Swift\nfunc numberOfCards(in timelineFeed: TimelineFeed) -\u003e Int {\n\treturn timelinesCollection.items.count\n}\n```\n\nNow, let's initialize new card every time feed asks us to for given index:\n\n```Swift\nfunc card(at index: Int, in timelineFeed: TimelineFeed) -\u003e TimelineCard {\n\tlet timelineCard = TimelineCard(width: timelineFeed.bounds.width)\n\t// Customize as you'd do with Single Card\n\treturn timelineCard\n}\n```\n**Note**: *DO NOT set `dataProvider` or `eventHandler` for `TimelineCard` object here. `TimelineFeed` is responsible for this*.\n\nGood! 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 `TimelineSourceElement`s:\n\n```Swift\nfunc elementsForTimelineCard(at index: Int, containerWidth: CGFloat) -\u003e [TimelineSourceElement] {\n\tvar elements = [] as [TimelineSourceElement]\n\n\t// Creating those `TimelineItem` and/or `TimelineItemGroup` objects..\n\n\treturn elements\n}\n```\n\nOk, 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.\n\n1. Keep it simple and use attributed **Title** and **Subtitle** preset (or just **Title** if you want to keep it minimal):\n\n```Swift\nfunc titleAndSubtitle(at index: Int, in timelineFeed: TimelineFeed) -\u003e (NSAttributedString, NSAttributedString?)? {\n\n\tlet timelineData = timelinesCollection.items[index]\n\n\tlet testTitle = NSAttributedString(string: \"Timeline Card #\\(index)\", attributes: [.foregroundColor : UIColor.white, .font : UIFont(name: \"HelveticaNeue-Bold\", size: 23.0)])\n\n\tlet testSubtitle = NSAttributedString(string: \"Subtitle text\", attributes: [.foregroundColor : UIColor.white])\n\n\treturn (testTitle, testSubtitle)\n\n\t// Subtitle is optional\n\t//return (testTitle, nil)\n}\n```\n\n2. Use custom `UIView`:\n\n```Swift\nfunc headerViewForCard(at index: Int, in timelineFeed: TimelineFeed) -\u003e UIView? {\n\tlet customHeader = UIView(frame: CGRect(x: 0, y: 0, width: timelineFeed.bounds.width, height: 60.0))\n\tcustomHeader.backgroundColor = .purple\n\treturn customHeader\n}\n```\n\n##### Implementing `TimelineFeedDelegate`\n\nFairly simple and similar to event handling for a Single Card. The difference is that you get index of the card where event did occur.\n\n```Swift\nfunc didSelectElement(at index: Int, timelineCardIndex: Int)\n\nfunc didSelectSubElement(at index: (Int, Int), timelineCardIndex: Int)\n\nfunc didTouchHeaderView(_ headerView: UIView, timelineCardIndex: Int)\n\nfunc didTouchFooterView(_ footerView: UIView, timelineCardIndex: Int)\n```\n\n## TODO\n\n- [ ] Support for `.square` and `.diamond` milestone shapes\n- [ ] Horizontal scrolling/paging\n- [ ] Force-touch peek for embedded elements\n\n## License\nTimelineCards is released under an MIT license. See the [LICENSE](https://raw.githubusercontent.com/0xNSHuman/TimelineCards/master/LICENSE.md) file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0xNSHuman%2FTimelineCards","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F0xNSHuman%2FTimelineCards","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F0xNSHuman%2FTimelineCards/lists"}