[ad_1]
Venture setup
We’ll use a daily storyboard-based Xcode challenge, since we’re working with UIKit.
We’re additionally going to want a desk view, for this objective we might go along with a conventional setup, however since we’re utilizing fashionable UIKit practices we’ll do issues only a bit completely different this time.
It is fairly unlucky that we nonetheless have to offer our personal type-safe reusable extensions for UITableView and UICollectionView lessons. Anyway, here is a fast snippet that we’ll use. ⬇️
import UIKit
extension UITableViewCell {
static var reuseIdentifier: String {
String(describing: self)
}
var reuseIdentifier: String {
kind(of: self).reuseIdentifier
}
}
extension UITableView {
func register<T: UITableViewCell>(_ kind: T.Kind) {
register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
}
func reuse<T: UITableViewCell>(_ kind: T.Kind, _ indexPath: IndexPath) -> T {
dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
}
}
I’ve additionally created a subclass for UITableView, so I can configure every part contained in the initialize perform that we’ll want on this tutorial.
import UIKit
open class TableView: UITableView {
public init(fashion: UITableView.Model = .plain) {
tremendous.init(body: .zero, fashion: fashion)
initialize()
}
@obtainable(*, unavailable)
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been carried out")
}
open func initialize() {
translatesAutoresizingMaskIntoConstraints = false
allowsMultipleSelection = true
}
func layoutConstraints(in view: UIView) -> [NSLayoutConstraint] {
[
topAnchor.constraint(equalTo: view.topAnchor),
bottomAnchor.constraint(equalTo: view.bottomAnchor),
leadingAnchor.constraint(equalTo: view.leadingAnchor),
trailingAnchor.constraint(equalTo: view.trailingAnchor),
]
}
}
We’re going to construct a settings display with a single choice and a a number of choice space, so it is good to have some extensions too that’ll assist us to handle the chosen desk view cells. 💡
import UIKit
public extension UITableView {
func choose(_ indexPaths: [IndexPath],
animated: Bool = true,
scrollPosition: UITableView.ScrollPosition = .none) {
for indexPath in indexPaths {
selectRow(at: indexPath, animated: animated, scrollPosition: scrollPosition)
}
}
func deselect(_ indexPaths: [IndexPath], animated: Bool = true) {
for indexPath in indexPaths {
deselectRow(at: indexPath, animated: animated)
}
}
func deselectAll(animated: Bool = true) {
deselect(indexPathsForSelectedRows ?? [], animated: animated)
}
func deselectAllInSection(besides indexPath: IndexPath) {
let indexPathsToDeselect = (indexPathsForSelectedRows ?? []).filter {
$0.part == indexPath.part && $0.row != indexPath.row
}
deselect(indexPathsToDeselect)
}
}
Now we are able to give attention to making a customized cell, we’re going to use the brand new cell configuration API, however first we’d like a mannequin for our customized cell class.
import Basis
protocol CustomCellModel {
var textual content: String { get }
var secondaryText: String? { get }
}
extension CustomCellModel {
var secondaryText: String? { nil }
}
Now we are able to use this cell mannequin and configure the CustomCell utilizing the mannequin properties. This cell may have two states, if the cell is chosen we’ll show a stuffed verify mark icon, in any other case simply an empty circle. We additionally replace the labels utilizing the summary mannequin values. ✅
import UIKit
class CustomCell: UITableViewCell {
var mannequin: CustomCellModel?
override func updateConfiguration(utilizing state: UICellConfigurationState) {
tremendous.updateConfiguration(utilizing: state)
var contentConfig = defaultContentConfiguration().up to date(for: state)
contentConfig.textual content = mannequin?.textual content
contentConfig.secondaryText = mannequin?.secondaryText
contentConfig.imageProperties.tintColor = .systemBlue
contentConfig.picture = UIImage(systemName: "circle")
if state.isHighlighted || state.isSelected {
contentConfig.picture = UIImage(systemName: "checkmark.circle.fill")
}
contentConfiguration = contentConfig
}
}
Contained in the ViewController class we are able to simply setup the newly created desk view. Since we’re utilizing a storyboard file we are able to override the init(coder:) methodology this time, however if you’re instantiating the controller programmatically then you might merely create your personal init methodology.
By the way in which I additionally wrapped this view controller inside a navigation controller so I am show a customized title utilizing the massive fashion by default and there are some lacking code items that now we have to write down.
import UIKit
class ViewController: UIViewController {
var tableView: TableView
required init?(coder: NSCoder) {
self.tableView = TableView(fashion: .insetGrouped)
tremendous.init(coder: coder)
}
override func loadView() {
tremendous.loadView()
view.addSubview(tableView)
NSLayoutConstraint.activate(tableView.layoutConstraints(in: view))
}
override func viewDidLoad() {
tremendous.viewDidLoad()
title = "Desk view"
navigationController?.navigationBar.prefersLargeTitles = true
tableView.register(CustomCell.self)
tableView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
tremendous.viewDidAppear(animated)
reload()
}
func reload() {
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
}
}
We do not have to implement the desk view knowledge supply strategies, however we’ll use a diffable knowledge supply for that objective, let me present you the way it works.
Diffable knowledge supply
I’ve already included one instance containing a diffable knowledge supply, however that was a tutorial for creating fashionable assortment views. A diffable knowledge supply is actually an information supply tied to a view, in our case the UITableViewDiffableDataSource generic class goes to behave as an information supply object 4 our desk view. The great take into consideration these knowledge sources is which you can simply manipulate the sections and rows contained in the desk view with out the necessity of working with index paths.
So the primary concept right here is that we might wish to show two sections, one with a single choice choice for choosing a quantity, and the second choice group goes to comprise a multi-selection group with some letters from the alphabet. Listed here are the info fashions for the part objects.
enum NumberOption: String, CaseIterable {
case one
case two
case three
}
extension NumberOption: CustomCellModel {
var textual content: String { rawValue }
}
enum LetterOption: String, CaseIterable {
case a
case b
case c
case d
}
extension LetterOption: CustomCellModel {
var textual content: String { rawValue }
}
Now we must always have the ability to show this stuff contained in the desk view, if we implement the common knowledge supply strategies, however since we’ll work with a diffable knowledge supply we’d like some further fashions. To remove the necessity of index paths, we are able to use a Hashable enum to outline our sections, we’ll have two sections, one for the numbers and one other for the letters. We’ll wrap the corresponding kind inside an enum with type-safe case values.
enum Part: Hashable {
case numbers
case letters
}
enum SectionItem: Hashable {
case quantity(NumberOption)
case letter(LetterOption)
}
struct SectionData {
var key: Part
var values: [SectionItem]
}
We’re additionally going to introduce a SectionData helper, this fashion it’ll be simpler to insert the mandatory sections and part objects utilizing the info supply.
remaining class DataSource: UITableViewDiffableDataSource<Part, SectionItem> {
init(_ tableView: UITableView) {
tremendous.init(tableView: tableView) { tableView, indexPath, itemIdentifier in
let cell = tableView.reuse(CustomCell.self, indexPath)
cell.selectionStyle = .none
swap itemIdentifier {
case .quantity(let mannequin):
cell.mannequin = mannequin
case .letter(let mannequin):
cell.mannequin = mannequin
}
return cell
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection part: Int) -> String? {
let id = sectionIdentifier(for: part)
swap id {
case .numbers:
return "Choose a quantity"
case .letters:
return "Choose some letters"
default:
return nil
}
}
func reload(_ knowledge: [SectionData], animated: Bool = true) {
var snapshot = snapshot()
snapshot.deleteAllItems()
for merchandise in knowledge {
snapshot.appendSections([item.key])
snapshot.appendItems(merchandise.values, toSection: merchandise.key)
}
apply(snapshot, animatingDifferences: animated)
}
}
We are able to present a customized init methodology for the info supply, the place we are able to use the cell supplier block to configure our cells with the given merchandise identifier. As you’ll be able to see the merchandise identifier is definitely the SectionItem enum that we created a couple of minutes in the past. We are able to use a swap to get again the underlying mannequin, and since these fashions conform to the CustomCellModel protocol we are able to set the cell.mannequin property. It’s also potential to implement the common titleForHeaderInSection methodology and we are able to swap the part id and return a correct label for every part.
The ultimate methodology is a helper, I am utilizing it to reload the info supply with the given part objects.
import UIKit
class ViewController: UIViewController {
var tableView: TableView
var dataSource: DataSource
required init?(coder: NSCoder) {
self.tableView = TableView(fashion: .insetGrouped)
self.dataSource = DataSource(tableView)
tremendous.init(coder: coder)
}
override func loadView() {
tremendous.loadView()
view.addSubview(tableView)
NSLayoutConstraint.activate(tableView.layoutConstraints(in: view))
}
override func viewDidLoad() {
tremendous.viewDidLoad()
title = "Desk view"
navigationController?.navigationBar.prefersLargeTitles = true
tableView.register(CustomCell.self)
tableView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
tremendous.viewDidAppear(animated)
reload()
}
func reload() {
dataSource.reload([
.init(key: .numbers, values: NumberOption.allCases.map { .number($0) }),
.init(key: .letters, values: LetterOption.allCases.map { .letter($0) }),
])
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
}
}
So contained in the view controller it’s potential to render the desk view and show each sections, even the cells are selectable by default, however I would like to point out you how one can construct a generic method to retailer and return chosen values, in fact we might use the indexPathsForSelectedRows property, however I’ve just a little helper software which is able to enable single and a number of choice per part. 🤔
struct SelectionOptions<T: Hashable> {
var values: [T]
var selectedValues: [T]
var multipleSelection: Bool
init(_ values: [T], chosen: [T] = [], a number of: Bool = false) {
self.values = values
self.selectedValues = chosen
self.multipleSelection = a number of
}
mutating func toggle(_ worth: T) {
guard multipleSelection else {
selectedValues = [value]
return
}
if selectedValues.accommodates(worth) {
selectedValues = selectedValues.filter { $0 != worth }
}
else {
selectedValues.append(worth)
}
}
}
By utilizing a generic extension on the UITableViewDiffableDataSource class we are able to flip the chosen merchandise values into index paths, it will assist us to make the cells chosen when the view masses.
import UIKit
extension UITableViewDiffableDataSource {
func selectedIndexPaths<T: Hashable>(_ choice: SelectionOptions<T>,
_ remodel: (T) -> ItemIdentifierType) -> [IndexPath] {
choice.values
.filter { choice.selectedValues.accommodates($0) }
.map { remodel($0) }
.compactMap { indexPath(for: $0) }
}
}
There is just one factor left to do, which is to deal with the only and a number of choice utilizing the didSelectRowAt and didDeselectRowAt delegate strategies.
import UIKit
class ViewController: UIViewController {
var tableView: TableView
var dataSource: DataSource
var singleOptions = SelectionOptions<NumberOption>(NumberOption.allCases, chosen: [.two])
var multipleOptions = SelectionOptions<LetterOption>(LetterOption.allCases, chosen: [.a, .c], a number of: true)
required init?(coder: NSCoder) {
self.tableView = TableView(fashion: .insetGrouped)
self.dataSource = DataSource(tableView)
tremendous.init(coder: coder)
}
override func loadView() {
tremendous.loadView()
view.addSubview(tableView)
NSLayoutConstraint.activate(tableView.layoutConstraints(in: view))
}
override func viewDidLoad() {
tremendous.viewDidLoad()
title = "Desk view"
navigationController?.navigationBar.prefersLargeTitles = true
tableView.register(CustomCell.self)
tableView.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
tremendous.viewDidAppear(animated)
reload()
}
func reload() {
dataSource.reload([
.init(key: .numbers, values: singleOptions.values.map { .number($0) }),
.init(key: .letters, values: multipleOptions.values.map { .letter($0) }),
])
tableView.choose(dataSource.selectedIndexPaths(singleOptions) { .quantity($0) })
tableView.choose(dataSource.selectedIndexPaths(multipleOptions) { .letter($0) })
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let sectionId = dataSource.sectionIdentifier(for: indexPath.part) else {
return
}
swap sectionId {
case .numbers:
guard case let .quantity(mannequin) = dataSource.itemIdentifier(for: indexPath) else {
return
}
tableView.deselectAllInSection(besides: indexPath)
singleOptions.toggle(mannequin)
print(singleOptions.selectedValues)
case .letters:
guard case let .letter(mannequin) = dataSource.itemIdentifier(for: indexPath) else {
return
}
multipleOptions.toggle(mannequin)
print(multipleOptions.selectedValues)
}
}
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
guard let sectionId = dataSource.sectionIdentifier(for: indexPath.part) else {
return
}
swap sectionId {
case .numbers:
tableView.choose([indexPath])
case .letters:
guard case let .letter(mannequin) = dataSource.itemIdentifier(for: indexPath) else {
return
}
multipleOptions.toggle(mannequin)
print(multipleOptions.selectedValues)
}
}
}
Because of this we have created the choice helper strategies to start with of the article. It’s comparatively simple to implement a single and multi-selection part with this system, however in fact these items are much more easy when you can work with SwiftUI.
Anyway, I hope this tutorial helps for a few of you, I nonetheless like UIKit loads and I am glad that Apple provides new options to it. Diffable knowledge sources are glorious approach of configuring desk views and assortment views, with these little helpers you’ll be able to construct your personal settings or picker screens simply. 💪
[ad_2]
