Strange Swift Syntax, What's It Doing? - ios

So I'm following this tutorial for In-App-Purchases. Here are a few things I don't get:
For the table, in the rowAtIndexPath they use a handler, what is that?
They put all the table code in an extension. I don't know why.
There's also a weird "buyButtonHandler?(product!)" call on button tap
I'd appreciate any clarification on any of the above points. Below is the table code where they put the table in an extension:
extension MasterViewController {
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return products.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ProductCell
var products = [SKProduct]() //This is actually declared elsewhere
let product = products[(indexPath as NSIndexPath).row]
cell.product = product
cell.buyButtonHandler = { product in
RageProducts.store.buyProduct(product)
}
return cell
}
}
And the above code includes the strange that I'm looking for help understanding:
cell.buyButtonHandler = { product in
RageProducts.store.buyProduct(product)
}
The table cell has a button and in the cell class this is its code:
func buyButtonTapped (_ sender: AnyObject) {
buyButtonHandler?(product!)
}
It references the below line. This button code/reference is gibberish to me:
var buyButtonHandler: ((_ product: SKProduct) -> ())?
I don't get what that buyButtonHandler is doing, it's like 50% parenthesis! Lastly, I'm including the below var declaration, in case it helps for context:
var product: SKProduct? {
didSet {
guard let product = product else { return }
textLabel?.text = product.localizedTitle
if RageProducts.store.isProductPurchased(product.productIdentifier) {
//Setup
} else {
//Alternate setup
}
}
}

The stuff you're seeing is fairly standard Swift.
Bullet #1:
It looks like the table view cells hold a closure, which is a block of code that they save and run later. The IBAction for the cell's button just invokes the handler block. (The term block and closure are interchangeable. Objective-C calls them blocks, Swift calls them closures.)
So the code in cellForRowAtIndexPath is installing a closure into the cell. That lets you configure your cells from outside. It's a neat trick.
Bullet #2:
It's considered good form to place the methods that implement a protocol in an extension. That way they're all grouped together and easy to find. It also makes the extension into a nice modular block of code. The extension is probably for the UITableViewDelegate and/or UITableViewDataSource protocol methods.
Bullet #3:
Same thing as #1. The cell stores a closure (block of code) in a variable, and when the user taps a button, the button's IBAction invokes the stored closure.
Bullet 1 and Bullet 3 mean that in the table view data source's cellForRowAtIndexPath method you can provide a custom block of code for each cell that gets invoked when the cell's button is tapped. The code in the button IBAction invokes the stored closure and passes it the current product.

Related

high performance several horizontal collection views in table view cells with RxSwift/RxDataSources

A famous layout you can find in most apps is having several horizontal lists in a table view cell where each list gets its data from the server. can be found in Airbnb. example below:
Each list has a loading view and an empty state to show when something is wrong.
Each list triggers the network request only when its first time created, so when displayed again by scrolling the table view, it should NOT make another network request to get data.
I tried several approaches and but not yet satisfied. some of which run into memory leaks and performance issues when having several collectionview. currently, I do the network requests in the View controller that holds the table view and passes the data to each cell.
Can anyone share their approach on how to do this? Appreciated!
Example:
This is a huge question with a lot of different possible answers. I recently solved it by using a custom table view data source that reports the first time (and only the first time) an item is displayed by a cell. I used that to trigger when the individual inner network requests should happen. It uses the .distinct() operator which is implemented in RxSwiftExt...
final class CellReportingDataSource<Element>: NSObject, UITableViewDataSource, RxTableViewDataSourceType where Element: Hashable {
let cellCreated: Observable<Element>
init(createCell: #escaping (UITableView, IndexPath, Element) -> UITableViewCell) {
self.createCell = createCell
cellCreated = _cellCreated.distinct()
super.init()
}
deinit {
_cellCreated.onCompleted()
}
func tableView(_ tableView: UITableView, observedEvent: Event<[Element]>) {
if case let .next(sections) = observedEvent {
self.items = sections
tableView.reloadData()
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = items[indexPath.row]
let cell = createCell(tableView, indexPath, item)
_cellCreated.onNext(item)
return cell
}
private var items: [Element] = []
private let createCell: (UITableView, IndexPath, Element) -> UITableViewCell
private let _cellCreated = PublishSubject<Element>()
}
Each table view cell needs its own Observable that emits the results of the network call every time something subscribes to it. I do that by using .scan(into:accumulator:). An example might be something like this:
dataSource.cellCreated
.map { ($0, /* logic to convert a table view item into network call parameters */) }
.flatMap {
Observable.zip(
Observable.just($0.0),
networkCall($0.1)
.map { Result.success($0) }
.catchError { Result.failure($0) }
)
}
.scan(into: [TableViewItem: Result<NetworkResponse, Error>]()) { current, next in
current[next.0] = next.1
}
.share(replay: 1)
Each cell can subscribe to the above and use compactMap to extract it's particular piece of state.

Handle events in subviews in MVVM in Swift

I am trying to get into MVVM in Swift and I am wondering how to handle events in subviews in MVVM, and how these events can travel up the chain of views/viewmodels. I'm talking about pure Swift for now (no SwiftRx etc.).
Example
Say I have a TableViewController with a TableViewModel. The view model holds an array of objects and creates a TableCellViewModel for each one, since each cell represents one of these objects. The TableViewController gets the number of rows to display from its model and also the view model for each cell, so it can pass it along to the cell.
We then have a TableCell and each cell has a TableCellViewModel. The TableCell queries its model for things like user-facing strings etc.
Now let's say TableCell also has a delete button that delete's that row. I'm wondering how to handle that: Usually, the cell would forward the button press to its view model, but this is not where we need it - we eventually need to know about the button press in either TableViewController or TableViewModel, so we can remove the row from the table view.
So the question is:
How does the button event get from a TableCell upwards in the view chain in MVVM?
Code
As requested in the comments, code that goes with the example:
class TableViewController: UIViewController, UITableViewDataSource {
var viewModel: TableViewModel = TableViewModel()
// setup and such
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.viewModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableCell
cell.viewModel = self.viewModel.cellViewModel(at: indexPath.item)
return cell
}
}
class TableViewModel {
// setup, get data from somewhere, ...
var count: Int {
return self.modelObjects.count
}
func cellViewModel(at index: Int) -> TableCellViewModel {
let modelObject = self.modelObjects[index]
let cellViewModel = TableCellViewModel(modelObject: modelObject)
return cellViewModel
}
}
class TableCell {
var viewModel: TableCellViewModel!
// setup UI, do what a cell does
func viewModelChanged() {
self.titleLabel.text = self.viewModel.title()
}
func deleteButtonPressed(_ sender: UIButton) {
// Oh, what to do, what to do?
}
}
class TableCellViewModel {
private var modelObject: ModelObject
init(modelObject: ModelObject) {
self.modelObject = modelObject
}
func title() -> String {
return self.modelObject.title
}
}
TableViewModel is the source of truth, so all global operations should be performed in there. Pressing a button is completely UI operation and viewModel shouldn't handle this in direct way.
So, for now we know two facts:
TableViewModel should delete the cell from array and then viewController should handle the deletion animation process;
Button press shouldn't be handled in child viewModel.
According to this you can achieve it by:
Pass button pressed event up to viewController (use callback or delegate pattern);
Call TableViewModel method to delete specific cell:
viewModel.deleteCell(at: indexPath)
Properly handle deletion animation in viewController.
may be you can use nextResponder util nextResponder is VC, and VC responder to delegate (eg:CellEventDelegate) that handle delete data and cell
UIResponder *nextResponder = pressedCell.nextResponder;
while (nextResponder) {
if ([nextResponder conformsToProtocol:#protocol(CellEventDelegate)]) {
if ([nextResponder respondsToSelector:#selector(onCatchEvent:)]) {
[((id<CellEventDelegate>)nextResponder) onCatchEvent:event];
}
break;
}
nextResponder = nextResponder.nextResponder;
}

How to get the indexPath.row when I click a button on a table view cell?

Hello the image above is the UI of my todo list app, now I just want to show the detail of item (First Item, second Item etc) when I click the detail button in the tableviewcell. So in order to get the property of the item, I need to know the indexPath of the row that I just clicked on the detail button.
I have tried some properties of the tableview like didSelectRowAt, or indexPathForSelectedRow, but both not work. For didSelectRowAt user need to click on the row first then click the detail button, and that's not what I want, and the indexPathForSelectedRow is not working for me.
A common, generalized solution for this type of problem is to connect the #IBAction of the button to a handler in the cell (not in the view controller), and then use a delegate-protocol pattern so the cell can tell the table when the button was tapped. The key is that when the cell does this, it will supply a reference to itself, which the view controller can then use to determine the appropriate indexPath (and thus the row).
For example:
Give your UITableViewCell subclass a protocol:
protocol CustomCellDelegate: class {
func cell(_ cell: CustomCell, didTap button: UIButton)
}
Hook up the #IBAction to the cell (not the view controller) and have that call the delegate method:
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBOutlet weak var customLabel: UILabel!
func configure(text: String, delegate: CustomCellDelegate) {
customLabel.text = text
self.delegate = delegate
}
#IBAction func didTapButton(_ button: UIButton) {
delegate?.cell(self, didTap: button)
}
}
Obviously, when the cell is created, call the configure method, passing, amongst other things, a reference to itself as the delegate:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, delegate: self)
return cell
}
}
Finally, have the delegate method call indexPath(for:) to determine the index path for the cell in question:
extension ViewController: CustomCellDelegate {
func cell(_ cell: CustomCell, didTap button: UIButton) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath.row` here
}
}
The other approach is to use closures, but again using the same general pattern of hooking the button #IBAction to the cell, but have it call a closure instead of the delegate method:
Define custom cell with closure that will be called when the button is tapped:
class CustomCell: UITableViewCell {
typealias ButtonHandler = (CustomCell) -> Void
var buttonHandler: ButtonHandler?
#IBOutlet weak var customLabel: UILabel!
func configure(text: String, buttonHandler: #escaping ButtonHandler) {
customLabel.text = text
self.buttonHandler = buttonHandler
}
#IBAction func didTapButton(_ button: UIButton) {
buttonHandler?(self)
}
}
When the table view data source creates the cell, supply a handler closure:
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { ... }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
let text = ...
cell.configure(text: text, buttonHandler: { [weak self] cell in // the `[weak self]` is only needed if this closure references `self` somewhere
guard let indexPath = tableView.indexPath(for: cell) else { return }
// use `indexPath` here
})
return cell
}
}
I personally prefer the delegate-protocol pattern, as it tends to scale more nicely, but both approaches work.
Note, in both examples, I studiously avoided saving the indexPath in the cell, itself (or worse, “tag” values). By doing this, it protects you from getting misaligned if rows are later inserted and deleted from the table.
By the way, I used fairly generic method/closure names. In a real app, you might give them more meaningful names, e.g., didTapInfoButton, didTapSaveButton, etc.) that clarifies the functional intent.
Implement the delegate method tableView(_:accessoryButtonTappedForRowWith:)
func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
However if you want to navigate to a different controller connect a segue to the accessory view button
If the button is a custom button see my answer in Issue Detecting Button cellForRowAt

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

tableView reloadData off of delegate

Have a right view controller that slides in and out over the main view controller. This right view controller has a table in it to contain the passed information from the main.
I can access and pass the data to the controller from the main without issue but in the right view I need to then bind the data passed to it from the main.
The problem is that even though I try binding the data after the view comes into focus it gives nil on the tableView.reloadData().
RightViewController has 2 functions that are used by the main
func loadAlerts(){
self.tableView.reloadData()
}
func setAlerts(alerts: Alerts){
self.alerts = alerts
}
Alerts is just a custom object. It does contain values. self.alerts is a class variable.
MainViewController calls these 2 functions this way
self.rightViewController = self.storyboard?.instantiateViewController(withIdentifier: "RightViewController") as! RightViewController
Set the data after getting it from the api call
if let count = self.alerts?.Alerts.count {
if count == 0 {
return
}
//set on controller
rightViewController.setAlerts(alerts: self.alerts!)
}
This is defined at class level like
private var rightViewController: RightViewController!
Then I have a delegate defined for when the right controller is opened from a gesture and it calls like this
func rightDidOpen() {
rightViewController.loadAlerts()
}
This works fine for everything but the a tableView. Even by telling the tableView to load on the main thread like so
DispatchQueue.main.async{
self.tableView.reloadData()
}
Didn't change anything. At this point the alerts has values.
I don't mind refactoring the entire thing if need be so any ideas, thoughts or info of how I can get this to work is appreciated. If more info is needed just let me know.
--
Here the table delegate and source defined
class RightViewController : UIViewController, UITableViewDataSource, UITableViewDelegate
and from front end assigned to the uicontroller (its calle Alerts Scene). Forgot to mention that if I do the api call directly in the right controller it works fine but I'm trying to reduce api calls so am refactoring this.
Here are the methods. Sorry for the misunderstanding.
//MARK: Tableview delegates
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let count = alerts?.Alerts.count{
return count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let alertD = alerts?.Alerts[indexPath.row] {
let cell = self.tableView.dequeueReusableCell(withIdentifier: "AlertTableViewCell") as! AlertTableViewCell
cell.name.text = alertD.Summary
cell.icon.image = Helpers.listImage24dp(id: alertD.TOA)
cell.selectionStyle = .none
cell.name.textColor = UIColor.blue
return cell
}
return UITableViewCell()
}

Resources