How to fix Cyclonemtic Complexity? - RxDataSource RxSwift - SwiftLint - ios

I met warning like this "Cyclomatic Complexity Violation: Function should have complexity 10 or less: currently complexity equals 14 (cyclomatic_complexity)" when I used RxDataSource.
My code structure like this:
struct ItemDetailDataSource {
typealias DataSource = RxTableViewSectionedReloadDataSource
static func dataSource() -> DataSource<ItemDetailTableViewSection> {
return .init(configureCell: { (dataSource, tableView, indexPath, _) -> UITableViewCell in
switch dataSource[indexPath] {
case .itemInfoTopItem(let info):
guard let cell = tableView.dequeueReusableCell(withIdentifier: ConstantsForCell.infoTopTableViewCell,
for: indexPath)
as? InfoTopTableViewCell else {
return UITableViewCell()
}
cell.configure(info)
return cell
case .itemHintItem(let hint):
...
case .itemManaColdownItem(let manacd):
case .itemNotesItem(let notes):
case .itemAttribItem(let attrib):
case .itemLoreItem(let lore):
case .itemComponentsItem(let components):
}
Can anyone help me fix this? Thanks very much.

The solution here is to not use an enum for your cell items. A possible solution is as follows:
struct DisplayableItem {
let makeCell: (UITableView, IndexPath) -> UITableViewCell
}
struct ItemDetailDataSource {
typealias DataSource = RxTableViewSectionedReloadDataSource
static func dataSource() -> DataSource<ItemDetailTableViewSection> {
.init { _, tableView, indexPath, item in
item.makeCell(tableView, indexPath)
}
}
}
Each DisplayableItem is given the means for making a UITableViewCell. You could do it with a closure like above, or with a protocol and a bunch of sub-classes.

Related

How to handle different data models with collectionView compositional layout?

Im updating an app which had many collectionViews embedded inside tableview so I decided to use UICollectionViewCompositionalLayout instead. I find them very flexible and I was able to design Home page for my app using this layout.
I followed some tutorials which just showed same data types, like Integers in multiple sections or photos app showing photos in different sections. In those tutorials they will use dataSource like this:
var dataSource: UICollectionViewDiffableDataSource<Section, PhotosItem>! = nil
or
var dataSource: UICollectionViewDiffableDataSource<Section, MoviesEntity>!
As my homepage consisted of many types of data, I simply used:
var dataSource: UICollectionViewDiffableDataSource<Section, Int>! = nil
And simply used numberOfSections, numberOfItemsInSection and cellForItemAt methods. I used this approach as my homepage will get data from multiple api's and some sections are static.
In UICollectionView, I would simply hit an api and update my data model and reload collectionView and do same for different collectionViews handling different data models in same page but now there is only one CollectionView so how do I handle this?
Do I just hit 7-8 API's and reload collectionView everytime? What I would like to do is use snapshot feature of feeding CollectionView with data coming from multiple API's.
This is what I have done to create compositional layout:
func generateLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let isWideView = layoutEnvironment.traitCollection.horizontalSizeClass == .regular
let sectionLayoutKind = Section.allCases[sectionIndex]
switch (sectionLayoutKind) {
case .firstSection:
return self.generateFirstLayout(isWide: isWideView)
case .secondSection:
return self.generateCategoriesLayout(isWide: isWideView)
case .services:
return self.generateServicesLayout(isWide: isWideView)
case .spotlight:
return self.generateSpotlightLayout(isWide: isWideView)
case .offers:
return self.generateOffersLayout(isWide: isWideView)
case .reviews:
return self.generateReviewLayout(isWide: isWideView)
case .logo:
return self.generateLogoLayout(isWide: isWideView)
}
}
}
And im simply using these functions to generate sections and adding Dummy items in them:
func numberOfSections(in collectionView: UICollectionView) -> Int {
return Section.allCases.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch section {
case 0 :
return 1
case 1 :
return categories.count
case 2 :
return services.count
case 3:
return 2
case 4:
return 5
case 5:
return offers.count
case 6:
return 4
default:
return 10
}
}
So far this is all dummy data and now I want to feed live data and this is not same as what those tutorials did but my sections are different and I could not create something like "PhotosItem" to feed into dataSource method like in those tutorials.
Those tutorials used something like this:
func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource
<Section, AlbumItem>(collectionView: albumsCollectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, albumItem: AlbumItem) -> UICollectionViewCell? in
let sectionType = Section.allCases[indexPath.section]
switch sectionType {
case .featuredAlbums:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: FeaturedAlbumItemCell.reuseIdentifer,
for: indexPath) as? FeaturedAlbumItemCell else { fatalError("Could not create new cell") }
cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
cell.title = albumItem.albumTitle
cell.totalNumberOfImages = albumItem.imageItems.count
return cell
case .sharedAlbums:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: SharedAlbumItemCell.reuseIdentifer,
for: indexPath) as? SharedAlbumItemCell else { fatalError("Could not create new cell") }
cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
cell.title = albumItem.albumTitle
return cell
case .myAlbums:
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: AlbumItemCell.reuseIdentifer,
for: indexPath) as? AlbumItemCell else { fatalError("Could not create new cell") }
cell.featuredPhotoURL = albumItem.imageItems[0].thumbnailURL
cell.title = albumItem.albumTitle
return cell
}
}
dataSource.supplementaryViewProvider = { (
collectionView: UICollectionView,
kind: String,
indexPath: IndexPath) -> UICollectionReusableView? in
guard let supplementaryView = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: HeaderView.reuseIdentifier,
for: indexPath) as? HeaderView else { fatalError("Cannot create header view") }
supplementaryView.label.text = Section.allCases[indexPath.section].rawValue
return supplementaryView
}
let snapshot = snapshotForCurrentState()
dataSource.apply(snapshot, animatingDifferences: false)
}
But i don't have one AlbumItem but like ReviewItem, OfferItem and many more. So I was wondering do I stick to my way and just call the api and reload my collectionView?
I have seen many apps that probably have same situation as me. So can anyone help me how to deal with this?
Im using SwiftyJSON to feed my data into my data models. Just telling this as I have seen in most examples they use Hashable.
I found answer by making my Model Hashable and had to switch from SwiftyJSON to Codable completely which I was avoiding for months. When your model is Hashable, your can pass it as item for your section. You can create different data models and add them to any section as required.
So for above, if I want to add PhotoItems or MovieItems or anything else, first I need to create Sections which can be done by creating an Enum like this:
enum Section {
case Section1
case SEction2
}
And then create another enum for items for each section like this:
enum Items {
case Section1Item(PhotoItem)
case Section2Item(MovieItem)
}
then define your dataSource like this:
var dataSource: UICollectionViewDiffableDataSource<Section, Items>!
As you can see now we can add items of any type as we please instead of defining Item type like in question.
Finally just create a function to define snapshot:
var snapshot = Snapshot()
snapshot.appendItems(ArrayOfItemType1.map(PhotosItem.photos), toSection: Section.first)
snapshot.appendItems(ArrayOfItemType2.map(MovieItem.movie), toSection: Section.first)
datasource.apply(snapshot, animatingDifferences: true)

Configuring UITableViewCell instances for various models without using conditionals

I'd like to handle many different data types each of which has an associated UITableViewCell class and dedicated configure(for:) function.
Currently my view controller has something like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func failure() {
fatalError("The row type was unknown, or the row item couldn't be cast as expected, or the specified cell couldn't be dequeued, or the dequeued cell couldn't be cast")
}
let section = sections[indexPath.section]
let item = section.rowItems[indexPath.row]
switch item.moduleType {
case .message:
guard let message = item as? CardModules.Message,
let messageCell = tableView.dequeueReusableCell(withIdentifier: CardCell_Message.identifier) as? CardCell_Message
else { failure(); break }
messageCell.configure(for: message)
return messageCell
case .availability:
guard let availability = item as? CardModules.Availability,
let availabilityCell = tableView.dequeueReusableCell(withIdentifier: CardCell_Availability.identifier) as? CardCell_Availability
else { failure(); break }
availabilityCell.configure(for: availability)
return availabilityCell
// etc, for many data types
I'd prefer a system where any model can have it's cell class instantiated with a single call in the view controller, and have written the following solution
protocol DataForCell {
associatedtype Cell: CellForData
func configuredCell(from tableView: UITableView) -> Cell
}
extension DataForCell {
func cell(from tableView: UITableView) -> Cell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell
return cell
}
}
protocol CellForData {
associatedtype Data: DataForCell
static var identifier: String { get }
func configure(for data: Data)
}
Which allows the cellForRowAt to just call configuredCell(from: tableView) on each item, as the array of items is an array of DataForCell
The question is, how can I improve further on this? It would be great if the configuredCell function could be moved into a protocol extension to avoid the need to keeping it as boilerplate in every instance of DataForCell
Finally, does this approach violate any important principles?
Update
Based on Sweepers suggestion, the improved protocols are this:
// MARK: - Data Protocol
protocol DataForCell {
associatedtype Cell: CellForData
}
extension DataForCell where Cell.Data == Self {
func configuredCell(from tableView: UITableView) -> Cell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell
cell.configure(for: self)
return cell
}
}
// MARK: - Cell Protocol
protocol CellForData {
associatedtype Data: DataForCell
static var identifier: String { get }
func configure(for data: Data)
}
Use different cell type all of them based on a common cell class.
class BaseCellType : UITableviewCell {
func configure(…) {
print(“Not implemented”)
}
}
class MessageCell : BaseCellTyoe {
override func configure(…) {
… configure the message cell
}
}
Then create a dictionary of cell identifiers in the TableViewDataSource :
let cellId : [ModuleType:String] = [.message : CardModule.message, …]
Then in cellForRow , dequeue cell with correct identifier for type BaseCellType and call configure.
In storyboard set the final cell type for each cell.
Note : you may also do this with a protocole.

Generic function to return a UITableViewCell subclass type

I've written an extension to easily dequeue table view cells of a certain type:
class RedCell: UITableViewCell { }
class BlueCell: UITableViewCell { }
extension UITableView {
func dequeueReusableCell<T: UITableViewCell>(_ type: T.Type, for indexPath: IndexPath) -> T {
let identifier = String(describing: T.self) // Must set on Storyboard
return dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! T
}
}
This makes dequeuing cells of the correct type very easy:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(RedCell.self, for: indexPath) // type: RedCell
}
Now, rather than typing RedCell.self into that function call, I would like to store this SomeCell.self on a variable so that each of the enum cases can pass their own custom cell subclass to the table view:
enum Color {
case red, blue
// ...?
// func cellType<T: UITableViewCell>() -> T.Type {
// func cellType<T>() -> T.Type where T: UITableViewCell {
func cellType<T>() -> T.Type {
switch self {
case .red: return RedCell.self // Cannot convert return expression of type 'RedCell.Type' to return type 'T.Type'
case .blue: return BlueCell.self // Cannot convert return expression of type 'BlueCell.Type' to return type 'T.Type'
}
}
}
Desired result is to construct the cell via the enum case:
let color = Color.red
let cell = tableView.dequeueReusableCell(color.cellType(), for: indexPath) // type: UITableViewCell
It's fine for the return type of the above call to be upcast to the in-common UITableViewCell, rather than a subclass. But the cellType() should dequeue the proper cell subclass from the Storyboard, as shown in the first code block, which is based on a String of the class name.
Is this possible? Xcode gives the errors above for my attempts to write the function.
What is the correct syntax for the generic function I'm attempting to write?
No need for generics; just return UITableViewCell.Type:
enum Color {
case red, blue
func cellType() -> UITableViewCell.Type {
switch self {
case .red: return RedCell.self
case .blue: return BlueCell.self
}
}
}
Im not sure I'm a fan of the idea but have a look at this, just use a private static variable and set it the desired type.
This way you avoid the inferring error from the return statement, and still change the type according to your enum case.
enum Color {
private static var cellType = UITableViewCell.self
case red, blue
private func setCellType(){
switch self{
case .red:
Color.cellType = RedCell.self
case .blue:
Color.cellType = BlueCell.self
}
}
func cellType<T: UITableViewCell>() -> T.Type {
setCellType()
return Color.cellType as! T.Type
}
}
class Test {
init(){
print(String(describing: Color.red.cellType()))
}
}
let test = Test()

Generic UITableview Cell

I want to create a tableview with customs cells, each cell is different. But i want the implementation to be so smart.
I thought instead of making if else check in cellForRow:atIndex: for every cell like this
if indexpath.section == 0 {
// Create first cell
} else if indexpath.section == 1 {
// Create second cell
}
and so on..
I decided to create a protocol like so:
protocol tableviewProtocol {
func cellForAt(index: IndexPath, object: ObjectModel) -> UItableviewCell
}
and for every cell i want to show i create a class like so:
class firstCellClass: tableviewProtocol {
func cellForAt(index: IndexPath, object: ObjectModel) -> UItableviewCell {
// create and return the cell
}
}
and created this array,
var tableviewDataSource: [tableviewProtocol] = []
and then fill the array like so:
tabletableviewDataSource = [firstCellClass(), secondCellClass(), thirdCellClass()]
and in tableView cellForRow:atIndex:
return tableviewDataSource[indexpath.section].cellForAt(indexpath, object: object)
And that was fine until i have to create different cell with different object model type instead of ObjectModel
So how can i make it more generic that can accept any type of objects ?
You can make it more generic by subclassing UITableViewCell
class firstCellClass: UITableViewCell {
}
class secondCellClass: UITableViewCell {
}
class thirdCellClass: UITableViewCell {
}
let arrayOfCells = [UITableViewCell]()
Something like this should point you in the right direction. There are many ways to achieve this goal
I realize this isn’t the most elegant way to do it but hopefully it helps you.
Let’s say you have three types:
struct ModelA { ... }
struct ModelB { ... }
struct ModelC { ... }
So you can make the array of type Any:
var model = [[Any]]() // append data
Then in cellForRow:
var cell: UITableViewCell
let value = model[indexPath.section][indexPath.row]
switch val {
case is ModelA:
cell = tableView.dequeueReusableCell(withIdentifier: "cellA", for: indexPath) as! CellClassA
case is ModelB:
cell = tableView.dequeueReusableCell(withIdentifier: "cellB", for: indexPath) as! CellClassB
case is ModelC:
cell = tableView.dequeueReusableCell(withIdentifier: "cellC", for: indexPath) as! CellClassC
default:
cell = UITableViewCell()
}
return cell
Nighttalker had a good answer to just use "Any" but you could also try 2 other ways:
Make ObjectModel a protocol, and make all objects you want to pass in implement that protocol
protocol ObjectModel{
}
class SomeObject: ObjectModel{}
Use generics
protocol tableviewProtocol {
associatedtype ObjectModel
func cellForAt(index: IndexPath, object: ObjectModel) -> UITableViewCell
}
class SomeObject: tableviewProtocol{
typealias ObjectModel = String //whatever type you want
func cellForAt(index: IndexPath, object: ObjectModel) -> UITableViewCell{
//return your cell
}
}

How to create dynamic views programmatically with a good OO-design

I am facing a problem with creation of dynamic view in Swift. However, the problem is not directly related to Swift itself, it is rather a Object-Oriented programming problem.
The problem is that I need to be able to add additional view elements to a view dynamically. And I am not sure if I'm doing it correctly. My solutions seems as overkill to me.
To solve the problem I thought Decorator pattern would be a good candidate. Additionally to have more control of the flow, I have introduced Template Method pattern.
I have a number of classes that define default look and feel on certain view controls like Labels, TextFields and Buttons. Here below you can see an approximate idea of how it is.
Here is my code:
class ViewElement{
// this class inherits from default UIKit elemnts and provides default UI view
}
// default cell is the cell that implements default elements layout and margings, etc
class DefaultCell: UITableViewCell {
let mainStack: UIViewStack
func addElement(ViewElement)
}
class BlueCell: DefaultCell {
let textField1: TextField
let label : Label
let button: Button
init(){
textField = TextField()
label = Label()
button = Button()
addElement(textField)
addElement(label)
addElement(button)
}
}
Here is the tableViewDataSource implementation
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
return cell
}
}
As you can see, the greater the number of conditions, the bigger my switch statement.
Additional problem arises from the fact that I will need to access the additional elements that I assign in the condition, like callbacks, tap events etc. Also the fact that those elements in conditional are added via addElement method, means that those elements will be added at the bottom of the mainStack.
In order to have control over the way elements are added to the stack I decided to go with the following solution: Template Method pattern
protocol OrderableElements {
func top()
func middle()
func bottom()
}
extension OrderableElements {
func render() {
top()
middle()
bottom()
}
}
Now the BlueCell implements the protocol and looks like this
class BlueCell: DefaultCell, OrderableElements {
init(){
textField = TextField()
label = Label()
button = Button()
}
func top() {
addElement(textField)
}
func middle() {
addElement(label)
}
func bottom(){
addElement(button)
}
}
The tabledatasource class will then look as follows:
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
...
**cell.render()**
return cell
}
}
Now because I need to add the new view elements in certain location or better said, at certain moments in during the scope of BlueCell, I introduced Decorators for the cell, like this:
class CellDecorator: OrderableElements {
var cell: BlueCell
init(cell: BlueCell){
self.cell = cell
}
func top() {
self.cell.top()
}
func middle(){
self.cell.middle()
}
func bottom(){
self.cell.bottom()
}
func getCell() {
return self.cell
}
}
Here is the concrete implementation
class GoodWeatherDecorator: CellDecorator {
let goodLabel
let goodTextField
let goodButton
override top() {
super.top()
addElement(goodLabel)
}
override middle(){
super.middle()
addElement(goodTextfield)
}
override bottom(){
super.bottom()
addElement(goodButton)
}
}
The final implementation of the cellForRowAt method looks like below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
var decoratedCell = CellDecorator(cell: cell)
switch weather {
case good:
decoratedCell = GoodWeatherDecorator(cell: cell)
case bad:
decoratedCell = BadWeatherDecorator(cell: cell)
}
decoratedCell.configure() // <------------ here is the configure call
cell = decoratedCell.getCell() // <------- here I get cell from the decorator
return cell
}
}
Now I do understand that my implementation of the decorator pattern is not 100% valid, because I don't inherit from the BlueCell class, for example. But that does not bother me that much. The things that bothers me is that I think that this solution to the problem is kind of overkill.
All works the right way, but I can help the feeling of having done too much to solve this trivial problem.
What do you think? How would you solve this kind of problem?
Thanks in advace
Given that you only show two types of cells and your solution doesn't actually get rid of the switch statement, I'd say that your solution counts as "overkill."
You don't show it, but it seems that you have a Weather enum. I'll assume that...
enum Weather: String {
case good
case bad
}
In the table view datasource, my goal would be to have something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weather = weathers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: weather.rawValue, for: indexPath) as! ConfigurableCell
cell.configure(with: weather)
return cell as! UITableViewCell
}
In order to achieve the above, I would have several cells laid out in the storyboard file with different identifiers. I would have a subclass for each type of cell in my code where all of them conform to the ConfigurableCell protocol.
protocol ConfigurableCell {
func configure(with weather: Weather)
}
If you can't conform your Weather enum to the String type, you will need a switch statement to convert a weather object to a string identifier, but otherwise, no switch statements necessary.
You should follow Daniel T.'s answer.
But here's a suggested upgrade that I use on my own projects.
Instead of just using
protocol ConfigurableCell {
func configure(with weather: Weather)
}
I use this for reusability purposes in many different scenarios.
protocol Configurable {
associatedtype Initializables
func configure(_ model: Initializables) -> Self
}
Example use cases:
UIViewController
class SomeViewController: UIViewController {
var someIntProperty: Int?
...
}
extension SomeViewController: Configurable {
struct Initializables {
let someIntProperty: Int?
}
func configure(_ model: SomeViewController.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
return self
}
}
// on some other part of the code.
let someViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SomeViewController
_ = someViewController.configure(SomeViewController.Initializables(someIntProperty: 100))
UITableViewCell
class SomeTableViewCell: UITableViewCell {
var someIntProperty: Int?
var someStringProperty: Int?
...
}
extension SomeTableViewCell: Configurable {
struct Initializables {
let someIntProperty: Int?
let someStringProperty: Int?
}
func configure(_ model: SomeTableViewCell.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
self.someStringProperty = model.someStringProperty
return self
}
}
// on cellForRow
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeTableViewCell", for: indexPath) as! SomeTableViewCell
return cell.configure(SomeTableViewCell.Initializables(someIntProperty: 100, someStringProperty: "SomeString"))
Notes:
As you can see it's very reusable and easy to use and implement. Downside is the code generated could be long when using configure

Resources