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.
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.0.1'
Add the following line to your Carthage and run carthage update in your terminal:
github "billp/Blocks" ~> 0.0.1
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)
}