❤️ Support my apps ❤️
- Push Hero - pure Swift native macOS application to test push notifications
- PastePal - Pasteboard, note and shortcut manager
- Quick Check - smart todo manager
- Alias - App and file shortcut manager
- My other apps
❤️❤️😇😍🤘❤️❤️
Arguably one of the first things we learn about iOS development is rendering a list of items using UITableView, UICollectionView
, and that is also the task we do every day. We mostly do the same task again and again, fetch and parse json, register cells and manually manage data sources.
Using a framework gives you a boost at the start, but it also limits you in many ways. Using plain UITableView
, UICollectionView
is absolutely the right choice as sometimes we need just that. But think about that for a minute, most of the times we just need to do UITableView, UICollectionView
right with little effort, especially when it comes to data with multiple sections and cell types.
Upstream
is lightweight and guides you to implement declarative and type safe data source. It's inspired by React in that the source of truth comes from the model, and the cells are just representation of that.
Upstream
exposes the convenient methods we need the most configure, select, size
methods, while allows us to override or implement methods we want. It provides needed abstraction and does not limit you in any way.
- Works on both
UITableView
andUICollectionView
- Separation of concern
- Type safed
- Just manage Data Source and Delegate, keep your
UITableView
andUICollectionView
intact
Upstream
is based on minion Service
classes that you can use for many boring tasks
- RegistryService: auto checks and registers cells if needed
- DiffService: performs diff on your model changes so
UITablView, UICollectionView
gets reredendered nicely. - PaginationService: handles load more and pagination
- StateService: handles empty, loading, content, error states
- AccordionService: expands, collapses sections and cells
Also, there are some layout that we need often
- CarouselCell: used for carousel inside a list
- PlaceholderCell: show placeholder for remote content
Usually we get json data, parse it to model and use that model to drive the cell. In fact, what we really need is to map model -> cell
, or "I want to show this model using this cell"
The core concept of Upstream
is UI = f(model)
. The model is a collection of sections
public struct Section {
let header: Header?
let items: [Item]
let footer: Footer?
}
Each section can container either header, footer and a collection of items.
public struct Item {
let model: Any
let cellType: UIView.Type
}
Header, Footer, Cell are just UIView
under the hood, and are driven by Model
.
Since Upstream
is a generic, your model can be struct, enum or protocol. Here is an example on how to structure your Profile screen. Each enum case represents the kind of model this screeen can show. And by just looking at the model, you know exactly what your UITableView, UICollectionView
is going to show. You typically should do this inside UIViewController
.
class ProfileViewController: UIViewController {
enum Model {
case header(String)
case avatar(URL)
case name(String)
case location(String)
case skill(String)
}
}
Adapter
is the class that handles Data Source and Delegate methods. It is open
so that you can override and add new stuff if you like
let adapter = Adapter(tableView: tableView)
adapter.delegate = self
tableView.dataSource = adapter
tableView.delegate = adapter
Each section will map to a section in UITableView, UICollectionView
let sections: [Section] = [
Section(
header: Header(model: Model.header("Information"), viewType: HeaderView.self),
items: [
Item(model: Model.avatar(avatarUrl), cellType: AvatarCell.self),
Item(model: Model.name("Thor"), cellType: NameCell.self),
Item(model: Model.location("Asgard"), cellType: NameCell.self)
]
),
Section(
header: Header(model: Model.header("Skills"), viewType: HeaderView.self),
items: [
Item(model: Model.skill("iOS"), cellType: SkillCell.self),
Item(model: Model.skill("Android"), cellType: SkillCell.self)
]
)
]
adapter.reload(sections: sections)
Just call adapter.reload
. Your header, footer, cell types are automatically registered and reloaded.
The 3 methods we need the most are:
configure
: how to configure the view using the modelsize
: what is the size of the viewselect
: what do you when a cell is selected
These are universal for header, footer, cells. Due to generic protocol not be able to declare, we need to use Any
.
If you don't like switch case, you can make your Model
as protocol instead of enum.
extension ProfileViewController: AdapterDelegate {
func configure(model: Any, view: UIView, indexPath: IndexPath) {
guard let model = model as? Model else {
return
}
switch (model, view) {
case (.avatar(let string), let cell as Avatarcell):
cell.configure(string: string)
case (.name(let name), let cell as NameCell):
cell.configure(string: name)
case (.header(let string), let view as HeaderView):
view.configure(string: string)
default:
break
}
}
func select(model: Any) {
guard let model = model as? Model else {
return
}
switch model {
case .skill(let skill):
let skillController = SkillController(skill: skill)
navigationController?.pushViewController(skillController, animated: true)
default:
break
}
}
func size(model: Any, containerSize: CGSize) -> CGSize {
guard let model = model as? Model else {
return .zero
}
switch model {
case .name:
return CGSize(width: containerSize.width, height: 40)
case .avatar:
return CGSize(width: containerSize.width, height: 200)
case .header:
return CGSize(width: containerSize.width, height: 30)
default:
return .zero
}
}
}
Upstream
is meant to help you with the basic most common things. In case you need some more customisations, you can just override the behaviors, since Manager
is open
Below is how you implement your own accordion UITableView, UICollectionView
that expands and collapses sections
class AccordionManager<T>: Manager<T> {
private var collapsedSections = Set<Int>()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return collapsedSections.contains(section)
? 0 : sections[section].items.count
}
func toggle(section: Int) {
if collapsedSections.contains(section) {
collapsedSections.remove(section)
} else {
collapsedSections.insert(section)
}
let indexSet = IndexSet(integer: section)
tableView?.reloadSections(indexSet, with: .automatic)
}
}
Upstream is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Upstream'
Upstream is also available through Carthage. To install just write into your Cartfile:
github "hyperoslo/Upstream"
Upstream can also be installed manually. Just download and drop Sources
folders in your project.
- Khoa Pham, onmyway133@gmai.com
- Hyper Interaktiv AS, ios@hyper.no
We would love you to contribute to Upstream, check the CONTRIBUTING file for more info.
Upstream is available under the MIT license. See the LICENSE file for more info.