A Swift library for creating user interfaces using reusable components.
Blocks is a Swift library designed to simplify the creation of user interfaces in iOS applications. It allows developers to use both SwiftUI components and traditional UITableView cells, headers, and footers within the same UITableView. This approach facilitates a declarative way of building UIs while maintaining the flexibility to write custom code when necessary. Inspired by React, Blocks encourages the development of reusable components, promoting more abstract and modular code, which is especially beneficial for large and complex projects. The library leverages UIKit's UITableView as container to render components, and it supports the MVVM design pattern.
The following video presents how UIKit, SwiftUI views are rendered in the same UITableView that acts as a container (renderer)
blocks.mov
You can install Blocks using one of the following ways...
Add the following line to your Podfile and run pod install in your terminal:
pod 'Blocks', '~> 0.1.0'
Add the following line to your Carthage and run carthage update in your terminal:
github "billp/Blocks" ~> 0.1.0
Go to File > Swift Packages > Add Package Dependency and add the following URL :
https://github.com/billp/Blocks
Create a new TableViewRenderer instance by passing an table view instance in its initializer.
// Create a lazy var for UITableView. You can also create the TableView in any way you want (Storyboard, Nib, etc.)
lazy var tableView: UITableView = {
let tableView = UITableView()
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: tableView.topAnchor),
view.leftAnchor.constraint(equalTo: tableView.leftAnchor),
view.bottomAnchor.constraint(equalTo: tableView.bottomAnchor),
view.rightAnchor.constraint(equalTo: tableView.rightAnchor),
])
return tableView
}()
// Create the Table View renderer and pass it a UITableView instance.
lazy var renderer = TableViewRenderer(tableView: tableView)
Blocks enables the creation of flexible and reusable UI components by defining view models that conform to the Component
protocol. These components can be rendered using various view types, including traditional nib files (for UITableViewCell
and UITableViewHeaderFooterView
), class-based views (for nibless initializations), or SwiftUI class types. This hybrid approach allows for the integration of both UIKit and SwiftUI elements within your application's UI, providing a versatile toolset for UI development.
Define a view model that conforms to the Component
protocol.
struct EmptyResultsComponent: Component {
var title: String
}
Implement a UITableViewCell
subclass that conforms to ComponentViewConfigurable
for configuring the cell with a view model.
class EmptyResultsViewCell: UITableViewCell, ComponentViewConfigurable {
@IBOutlet weak var resultLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
selectionStyle = .none
}
func configure(with viewModel: any Component) {
guard let component = viewModel.as(EmptyResultsComponent.self) else { return }
resultLabel.text = component.title
}
}
Associated xib file: EmptyResultsViewCell.xib
.
Register the component with the renderer to connect the view model with its corresponding view.
renderer.register(viewModelType: EmptyResultsComponent.self, nibName: String(describing: EmptyResultsViewCell.self))
This registration method applies similarly for headers and footers using nib files.
Create a view model that conforms to the Component
protocol, containing properties utilized by the SwiftUI view.
class TodoComponent: ObservableObject, Component {
var id: UUID = .init()
@Published var title: String
init(title: String) {
self.title = title
}
}
Construct a SwiftUI view that complies with ComponentSwiftUIViewConfigurable
, using the view model for UI configuration.
import SwiftUI
struct TodoView: View, ComponentSwiftUIViewConfigurable {
@ObservedObject private var viewModel: TodoComponent
init(viewModel: any Component) {
self.viewModel = viewModel.as(TodoComponent.self)
}
var body: some View {
// SwiftUI view layout using viewModel properties
}
}
SwiftUI components are registered with the renderer to link the view model with its SwiftUI view, ensuring proper management and rendering within the UIKit-based table or collection view.
renderer.register(viewModelType: TodoComponent.self, viewType: TodoView.self)
This strategy for defining and registering components offers a modular and reusable approach for constructing your app's UI, leveraging the best of both UIKit and SwiftUI frameworks.
To update the UI using renderer.updateSections, incorporating TodoComponent with sample data and handling empty states with EmptyResultsComponent, you can follow this streamlined approach:
// Example method to update sections with todos and handle empty states
private func updateUI(withActiveTodos activeTodos: [TodoComponent], completedTodos: [TodoComponent]) {
let activeSectionRows: [any Component] = activeTodos.isEmpty ? [EmptyResultsComponent(title: "No active todos.")] : activeTodos
let completedSectionRows: [any Component] = completedTodos.isEmpty ? [EmptyResultsComponent(title: "No completed todos.")] : completedTodos
let sections = [
Section(id: "activeTodos", rows: activeSectionRows),
Section(id: "completedTodos", rows: completedSectionRows)
]
renderer.updateSections(sections, animation: .fade)
}
// Sample usage with active and completed todos
private func sampleUpdate() {
let activeTodos = [
TodoComponent(title: "Buy groceries"),
TodoComponent(title: "Read a book")
]
let completedTodos = [
TodoComponent(title: "Workout"),
TodoComponent(title: "Call mom")
]
updateUI(withActiveTodos: activeTodos, completedTodos: completedTodos)
}
We welcome contributions to Blocks! If you're looking to contribute, here's how you can help.
Before submitting a bug report, please check the issue tracker to avoid duplicates. When filing an issue, include:
- A clear and descriptive title
- Steps to reproduce the bug
- Expected behavior
- Actual behavior
- Screenshots (if applicable)
We love to hear about new features or improvements! For feature requests, please provide:
- A clear and concise description of what you want to happen
- Any additional context or screenshots about the feature request
Want to make a direct contribution? Great! Here's how:
- Fork the repository and create your branch from
main
. - Write clear, commented code.
- Ensure your changes pass any tests.
- Update the README.md with details of changes, if applicable.
- Submit a pull request with a comprehensive description of changes.
Please note we have a Code of Conduct, please follow it in all your interactions with the project.
If you have any questions or need further clarification, feel free to open an issue or contact a project maintainer.
Thank you for your interest in contributing to Blocks! We look forward to your contributions.
Blocks is available under the MIT license. See the LICENSE file for more info.