Saturday, October 14, 2023
HomeiOS DevelopmentWorking with diffable information sources and desk views utilizing UIKit

Working with diffable information sources and desk views utilizing UIKit


Undertaking setup

We will use an everyday storyboard-based Xcode mission, since we’re working with UIKit.


We’re additionally going to wish a desk view, for this function we may go along with a conventional setup, however since we’re utilizing fashionable UIKit practices we’ll do issues only a bit totally different this time.


It is fairly unlucky that we nonetheless have to supply our personal type-safe reusable extensions for UITableView and UICollectionView lessons. Anyway, this 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.Sort) {
        register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
    }

    func reuse<T: UITableViewCell>(_ kind: T.Sort, _ indexPath: IndexPath) -> T {
        dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
    }
}


I’ve additionally created a subclass for UITableView, so I can configure all the pieces contained in the initialize perform that we’ll want on this tutorial.


import UIKit

open class TableView: UITableView {

    public init(type: UITableView.Model = .plain) {
        tremendous.init(body: .zero, type: type)
        
        initialize()
    }

    @obtainable(*, unavailable)
    required public init?(coder: NSCoder) {
        fatalError("init(coder:) has not been applied")
    }
    
    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 will deal with making a customized cell, we’re going to use the brand new cell configuration API, however first we want 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 will 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 will simply setup the newly created desk view. Since we’re utilizing a storyboard file we will override the init(coder:) methodology this time, however in case you are instantiating the controller programmatically then you could possibly merely create your individual 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 big type by default and there are some lacking code items that we’ve to put in writing.


import UIKit

class ViewController: UIViewController {
    
    var tableView: TableView
    
    required init?(coder: NSCoder) {
        self.tableView = TableView(type: .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 information supply strategies, however we’ll use a diffable information supply for that function, let me present you the way it works.




Diffable information supply


I’ve already included one instance containing a diffable information supply, however that was a tutorial for creating fashionable assortment views. A diffable information 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 nice take into consideration these information sources is that you could simply manipulate the sections and rows contained in the desk view with out the necessity of working with index paths.


So the principle thought right here is that we might prefer 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 below are the information fashions for the part gadgets.


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 be capable to show this stuff contained in the desk view, if we implement the common information supply strategies, however since we’ll work with a diffable information supply we want some extra fashions. To get rid of the necessity of index paths, we will use a Hashable enum to outline our sections, we’ll have two sections, one for the numbers and one other for the letters. We will 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 manner it will be easier to insert the mandatory sections and part gadgets utilizing the information supply.


last 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(_ information: [SectionData], animated: Bool = true) {
        var snapshot = snapshot()
        snapshot.deleteAllItems()
        for merchandise in information {
            snapshot.appendSections([item.key])
            snapshot.appendItems(merchandise.values, toSection: merchandise.key)
        }
        apply(snapshot, animatingDifferences: animated)
    }
}


We will present a customized init methodology for the information supply, the place we will use the cell supplier block to configure our cells with the given merchandise identifier. As you possibly can see the merchandise identifier is definitely the SectionItem enum that we created a couple of minutes in the past. We will use a swap to get again the underlying mannequin, and since these fashions conform to the CustomCellModel protocol we will set the cell.mannequin property. Additionally it is doable to implement the common titleForHeaderInSection methodology and we will 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 information supply with the given part gadgets.


import UIKit

class ViewController: UIViewController {
    
    var tableView: TableView
    var dataSource: DataSource
    
    required init?(coder: NSCoder) {
        self.tableView = TableView(type: .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 doable to render the desk view and show each sections, even the cells are selectable by default, however I would like to indicate you methods to construct a generic method to retailer and return chosen values, after all we may use the indexPathsForSelectedRows property, however I’ve somewhat helper software which can permit 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.comprises(worth) {
            selectedValues = selectedValues.filter { $0 != worth }
        }
        else {
            selectedValues.append(worth)
        }
    }
}


Through the use of a generic extension on the UITableViewDiffableDataSource class we will flip the chosen merchandise values into index paths, this can assist us to make the cells chosen when the view masses.


import UIKit

extension UITableViewDiffableDataSource {

    func selectedIndexPaths<T: Hashable>(_ choice: SelectionOptions<T>,
                                         _ rework: (T) -> ItemIdentifierType) ->  [IndexPath] {
        choice.values
            .filter { choice.selectedValues.comprises($0) }
            .map { rework($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(type: .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)
        }
    }
}


For this reason we have created the choice helper strategies at first of the article. It’s comparatively simple to implement a single and multi-selection part with this method, however after all these items are much more easy for those who can work with SwiftUI.


Anyway, I hope this tutorial helps for a few of you, I nonetheless like UIKit quite a bit and I am glad that Apple provides new options to it. Diffable information sources are wonderful approach of configuring desk views and assortment views, with these little helpers you possibly can construct your individual settings or picker screens simply. 💪





Supply hyperlink

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments