Subscribing to UIButton tap in a UICollectionViewCell in RxSwift? - ios

I'm new to RxSwift, trying to wrap my head around it. I was having trouble getting a UIButton in a cell to show a UIAlertController when it's pressed.
private func setupCellConfiguration() {
bookListViewModel.data
.bindTo(collectionView.rx.items(cellIdentifier: BookListCell.Identifier, cellType: BookListCell.self)) { [unowned self] (row, element, cell) in
cell.configureForBook(book: element)
cell.moreButton.rx.tap.subscribe { [weak self] in
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {(action) in
self?.dismiss(animated: true, completion: nil)
}
alertController.addAction(cancelAction)
let destroyAction = UIAlertAction(title: "Delete", style: .destructive) { (action) in
}
alertController.addAction(destroyAction)
self?.present(alertController, animated: true)
}
.addDisposableTo(self.disposeBag)
}
.addDisposableTo(disposeBag)
}
Nothing happens when it's pressed. What am I doing wrong here?

I actually prefer to assign cell button action on its subclass. The problem is I think every cell should have it's own disposeBag and it should reinitialize every time it is reused.
Example: Haven't tested on code, if there's any problem let me know
private func setupCellConfiguration() {
bookListViewModel.data
.bindTo(collectionView.rx.items(cellIdentifier: BookListCell.Identifier, cellType: BookListCell.self)) { [unowned self] (row, element, cell) in
cell.delegate = self
cell.configureForBook(book: element)
}
.addDisposableTo(disposeBag)
}
// Your Cell Class
var disposeBag = DisposeBag()
var delegate: UIViewController?
func configureForBook(book: Book) {
self.moreButton.rx.tap.subscribe { [unowned self] in
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) {(action) in
self?.dismiss(animated: true, completion: nil)
}
alertController.addAction(cancelAction)
let destroyAction = UIAlertAction(title: "Delete", style: .destructive) { (action) in
}
alertController.addAction(destroyAction)
self.delegate?.present(alertController, animated: true)
}
.addDisposableTo(self.disposeBag)
}
override func prepareForReuse() {
disposeBag = DisposeBag()
}

Related

Extending UIViewController(s) to show UIAlertController without subclassing

I would like to reuse code to show alert when I delete some row in tableview in a few my view controllers:
func confirmDelete(item: String) {
let alert = UIAlertController(title: "Delete Planet", message: "Are you sure you want to permanently delete \(item)?", preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: handleDeleteItem)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: cancelDeleteItem)
alert.addAction(deleteAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
func handleDeleteItem(alertAction: UIAlertAction!) -> Void {
if let indexPath = deletePlanetIndexPath {
presenter?.removePlanet(atIndex: indexPath, completion: { (result) in
switch result {
case .success(_):
self.tableView.deleteRows(at: [indexPath], with: .fade)
break
case let .failure(error):
print(error)
break
}
})
deletePlanetIndexPath = nil
}
}
func cancelDeleteItem(alertAction: UIAlertAction!) {
deletePlanetIndexPath = nil
}
The only one part will be different everytime:
presenter?.removePlanet(atIndex: indexPath, completion: { (result) in
switch result {
case .success(_):
self.tableView.deleteRows(at: [indexPath], with: .fade)
break
case let .failure(error):
print(error)
break
}
})
So as you can see I can simple do subclassing and declare some closure variable which will be triggered each time deleteAction invoked.
It's very simple way, but I am not super fun of subclassing. Maybe there is some help-full extension, protocol based thing or any other suggestions.
You could write a class extension to view controller:
extension UIViewController {
func createAlert(handleDeleteItem: #escaping () -> Void, cancelDeleteItem: #escaping () -> Void) {
let alert = UIAlertController(title: "Delete Planet", message: "Are you sure you want to permanently delete \(item)?", preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: handleDeleteItem)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: cancelDeleteItem)
alert.addAction(deleteAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
}
And then pass in the appropriate delete and cancel functions for each different view controller.
An extension to UIAlertController could work:
class MyVC: UIViewController {
private lazy var alertControllerA: UIAlertController = {
return UIAlertController.customAlertController(/* params */)
}()
}
private extension UIAlertController {
static func customAlertController(_ item: String, handleDeleteItem: #escaping () -> Void, cancelDeleteItem: #escaping () -> Void) -> UIAlertController {
let alertController = UIAlertController(title: "Delete Planet", message: "Are you sure you want to permanently delete \(item)?", preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: handleDeleteItem)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: cancelDeleteItem)
alert.addAction(deleteAction)
alert.addAction(cancelAction)
return alertController
}
}
Thanks to guys who already posted answers, much appreciated. I came up with another solution as well. So in case we use Tob example we extend functionality for all UIViewControllers in case we use h.and.h example we decorate our specific view controller but it force us to write additional lazy code with is not big deal and I mostly like this example which does not make any view controllers to be able using UIAlerController extension but I will follow a solution which is protocol based extension:
import UIKit
protocol UIAlertControllerManageable {
func createAlert(with text: String, handleDeleteItem: #escaping (UIAlertAction) -> Void, cancelDeleteItem: #escaping (UIAlertAction) -> Void)
}
extension UIAlertControllerManageable where Self: UIViewController {
func createAlert(with text: String, handleDeleteItem: #escaping (UIAlertAction) -> Void, cancelDeleteItem: #escaping (UIAlertAction) -> Void) {
let alert = UIAlertController(title: "Delete", message: "Are you sure you want to permanently delete \(text)?", preferredStyle: .actionSheet)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: handleDeleteItem)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: cancelDeleteItem)
alert.addAction(deleteAction)
alert.addAction(cancelAction)
self.present(alert, animated: true, completion: nil)
}
}
Usage:
class HomeViewController: UIViewController, UIAlertControllerManageable {
func confirmDelete(deleteText: String) {
createAlert(with: deleteText) { (action) in
if let indexPath = self.deleteItemIndexPath {
self.presenter?.removeItem(atIndex: indexPath, completion: { (result) in
switch result {
case .success(_):
self.tableView.deleteRows(at: [indexPath], with: .fade)
break
case let .failure(error):
print(error)
break
}
})
self.deleteItemIndexPath = nil
}
} cancelDeleteItem: { (action) in
self.deleteItemIndexPath = nil
}
}
}
Let me know you thoughts, thanks!

Show UIAlertController every time view controller launches

Currently, the UIAlertController appears when the user taps on the HeaderButton. I am trying to make the UIAlertController automatically appear every time the view controller initially launches. Any suggestions?
// MARK: - RestaurantListTableViewHeaderDelegate
extension RestaurantListViewController: RestaurantListTableViewHeaderDelegate {
func didTapHeaderButton(_ headerView: RestaurantListTableViewHeader) {
let locationPicker = UIAlertController(title: "Select location", message: nil, preferredStyle: .actionSheet)
for location in RestaurantListViewController.locations {
locationPicker.addAction(UIAlertAction(title: location, style: .default) { [weak self] action in
guard let `self` = self else { return }
self.currentLocation = action.title
self.tableView.reloadData()
})
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
locationPicker.addAction(cancelAction)
present(locationPicker, animated: true)
}
}
I kept the extension for when the Header Button gets tapped and I added the following to viewDidLoad:
// Code for showing alert
let locationPicker = UIAlertController(title: "Select location", message: nil, preferredStyle: .actionSheet)
for location in RestaurantListViewController.locations {
locationPicker.addAction(UIAlertAction(title: location, style: .default) { [weak self] action in
guard let `self` = self else { return }
self.currentLocation = action.title
self.tableView.reloadData()
})
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
locationPicker.addAction(cancelAction)
present(locationPicker, animated: true)
It's not an elegant solution but it will work:
var alertAlreadyShown = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !alertAlreadyShown {
alertAlreadyShown = true
/* Code for showing alert */
}
}

How to display an Alert from a different class

I created a Utilities class to hold some common functions, one of which is an alertUser function that if called, will display an Alert box to the user with the provided title and message text. In another class file, I am validating some text field entries and if the validation doesn't pass, then I want to use the alertUser function from the Utilities class. However, when I do this, I get the following error message in the Xcode log:
Warning: Attempt to present <UIAlertController: 0x7f9c4be0b140> on <MyAppName.Utilities: 0x7f9c4be1cb60> whose view is not in the window hierarchy!
The calling code is in a UIViewController class file. Here's the code which is in the
class ItemSettingsVC: UIViewController:
private func validateNameField() -> Bool {
var passed = false
if (nameField.hasText) {
passed = true
} else {
Utilities().alertUser(strTitle: "Alert", strMessage: strInvalidNameFieldErrorMsg)
passed = false
}
return passed
}
Here's the alertUser function which is in the
class Utilities: UIViewController:
public func alertUser(strTitle: String, strMessage: String) {
let myAlert = UIAlertController(title: strTitle, message: strMessage, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil)
myAlert.addAction(okAction)
self.present(myAlert, animated: true, completion: nil)
}
This is running on iOS. I'm using Xcode 8 and swift 3. Any help is greatly appreciated. Thanks.
This should do it:
public func alertUser(strTitle: String, strMessage: String) {
let myAlert = UIAlertController(title: strTitle, message: strMessage, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil)
myAlert.addAction(okAction)
UIApplication.shared.delegate?.window??.rootViewController?.present(myAlert, animated: true, completion: nil)
}
You have to add an additional parameter in your alertUser function, which would be the VC that will present the alert controller.
for example:
public func alertUser(strTitle: String, strMessage: String, viewController: UIViewController) {
let myAlert = UIAlertController(title: strTitle, message: strMessage, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil)
myAlert.addAction(okAction)
viewController.present(myAlert, animated: true, completion: nil)
}
But I would recommend that you just make an extension of UIViewController and add your func alertUser()* there because you would surely use this alertUser in different VCs and complexity wise in my opinion, this would be more optimized.
Like this:
extension UIViewController {
func showAlert(title: String, message: String, callback: #escaping () -> ()) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: {
alertAction in
callback()
}))
self.present(alert, animated: true, completion: nil)
}
//add additional functions here if necessary
//like a function showing alert with cancel
}
NOTE : Please don't make your Utilities class a subclass of UIViewController, it would also be better to make it a struct handling static functions and/or variables
Use this class for easy to show Alert or ActionSheet
UIAlertController Extension
public extension UIAlertController {
public func showAlert(animated: Bool = true, completionHandler: (() -> Void)? = nil) {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else {
return
}
var forefrontVC = rootVC
while let presentedVC = forefrontVC.presentedViewController {
forefrontVC = presentedVC
}
forefrontVC.present(self, animated: animated, completion: completionHandler)
}
}
AppAlert Class Create For UIAlertController Show
public class AppAlert {
private var alertController: UIAlertController
public init(title: String? = nil, message: String? = nil, preferredStyle: UIAlertControllerStyle) {
self.alertController = UIAlertController(title: title, message: message, preferredStyle: preferredStyle)
}
public func setTitle(_ title: String) -> Self {
alertController.title = title
return self
}
public func setMessage(_ message: String) -> Self {
alertController.message = message
return self
}
public func setPopoverPresentationProperties(sourceView: UIView? = nil, sourceRect:CGRect? = nil, barButtonItem: UIBarButtonItem? = nil, permittedArrowDirections: UIPopoverArrowDirection? = nil) -> Self {
if let poc = alertController.popoverPresentationController {
if let view = sourceView {
poc.sourceView = view
}
if let rect = sourceRect {
poc.sourceRect = rect
}
if let item = barButtonItem {
poc.barButtonItem = item
}
if let directions = permittedArrowDirections {
poc.permittedArrowDirections = directions
}
}
return self
}
public func addAction(title: String = "", style: UIAlertActionStyle = .default, handler: #escaping ((UIAlertAction!) -> Void) = { _ in }) -> Self {
alertController.addAction(UIAlertAction(title: title, style: style, handler: handler))
return self
}
public func addTextFieldHandler(_ handler: #escaping ((UITextField!) -> Void) = { _ in }) -> Self {
alertController.addTextField(configurationHandler: handler)
return self
}
public func build() -> UIAlertController {
return alertController
}
}
Used For Open AlertBox
AppAlert(title: "Question", message: "Are you sure?", preferredStyle: .alert)
.addAction(title: "NO", style: .cancel) { _ in
// action
}
.addAction(title: "Okay", style: .default) { _ in
// action
}
.build()
.showAlert(animated: true)
Used For ActionSheet Open
if UIDevice.current.userInterfaceIdiom != .pad {
// Sample to show on iPhone
AppAlert(title: "Question", message: "Are you sure?", preferredStyle: .actionSheet)
.addAction(title: "NO", style: .cancel) {_ in
print("No")
}
.addAction(title: "YES", style: .default) { _ in
print("Yes")
}
.build()
.showAlert(animated: true)
} else {
// Sample to show on iPad
AppAlert(title: "Question", message: "Are you sure?", preferredStyle: .actionSheet)
.addAction(title: "Not Sure", style: .default) {
_ in
print("No")
}
.addAction(title: "YES", style: .default) { _ in
print("Yes")
}
.setPopoverPresentationProperties(sourceView: self, sourceRect: CGRect.init(x: 0, y: 0, width: 100, height: 100), barButtonItem: nil, permittedArrowDirections: .any)
.build()
.showAlert(animated: true)
}
First find out the topmost viewController on your window .
Get the top ViewController in iOS Swift
and then present your alert on that viewController.No need to pass any parameter.
public func alertUser(strTitle: String, strMessage: String) {
let myAlert = UIAlertController(title: strTitle, message: strMessage, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil)
myAlert.addAction(okAction)
topmostVC().present(myAlert, animated: true, completion: nil)
}

UICollectionView does not refresh in swift 3

I add UICollectionView in my app and each cell has "Delete" trash button.
When I click on this button, it will show alert message to ask. After clicks on OK button, the item in Collection View will delete. However, the UI does not refresh , the item was not deleted.
My Code is ...
override func viewDidLoad() {
super.viewDidLoad()
myCollectionView.reloadData()
}
override func viewDidAppear(_ animated: Bool) {
debugPrint("viewDidAppear")
callWatchLater()
}
override func viewWillAppear(_ animated: Bool) {
DispatchQueue.main.async(execute: {
debugPrint("Appear")
self.myCollectionView.reloadData()
})
}
override func viewDidLayoutSubviews() {
DispatchQueue.main.async(execute: {
debugPrint("SubViews")
self.myCollectionView.reloadData()
})
}
Code for Alert Controller is
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "watchCell", for: indexPath) as! WatchLaterTableCell
let watchTableList = self.watchLaterTable[indexPath.row]
cell.deleteMovie.addTarget(self, action:#selector(showAlert(sender:)), for: .touchUpInside)
}
return cell
}
func showAlert(sender:UIButton!)
{
debugPrint("Press Button")
let alert = UIAlertController(title: "Are you really want to delete this movie?", message: "", preferredStyle: UIAlertControllerStyle.alert)
// Create the actions
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default) {
UIAlertAction in
self.callRest()
debugPrint("Press OK")
}
let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel) {
UIAlertAction in
_ = self.navigationController?.popViewController(animated: true)
}
// Add the actions
alert.addAction(okAction)
alert.addAction(cancelAction)
// Present the controller
DispatchQueue.main.async(execute: {
self.present(alert, animated: true, completion: nil)
})
}
Code For callRest() is
func callRest(){
SwiftLoading().showLoading()
if Reachability().isInternetAvailable() == true {
rest.auth(auth: access_token)
rest.delete(StringResource().mainURL + "user/" + user_id + "/watch/later/" + String(vedioId) , parma: [:], finished: {(result: NSDictionary, status : Int) -> Void in
debugPrint(result)
if(status == 200){
let data = result["data"] as! NSString
self.messages = String(describing: data)
DispatchQueue.main.sync{[unowned self] in
let alertController = UIAlertController(title: "", message: self.messages , preferredStyle: .alert)
// Create the actions
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default) {
UIAlertAction in
debugPrint("call rest Press OK")
self.callWatchLaterGetAPI()
}
alertController.addAction(okAction)
// Present the controller
DispatchQueue.main.async(execute: {
self.present(alertController, animated: true, completion: nil)
})
}
self.myCollectionView.reloadData()
SwiftLoading().hideLoading()
}else{
let error = result["error"] as! NSArray
for item in 0...(error.count) - 1 {
let device : AnyObject = error[item] as AnyObject
self.messages = device["message"] as! String
DispatchQueue.main.sync{[unowned self] in
let alertController = UIAlertController(title: "", message: self.messages , preferredStyle: .alert)
// Create the actions
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.default) {
UIAlertAction in
debugPrint("Press OK")
}
alertController.addAction(okAction)
// Present the controller
DispatchQueue.main.async(execute: {
self.present(alertController, animated: true, completion: nil)
})
}
SwiftLoading().hideLoading()
}
}
})
}else{
noInternetConnection()
}
}
However, the CollectionView does not refresh . Can anyone help me this, please?

Dissmiss completion block doesn't work. Swift

I have PhotoBrowser (it inherits from UIViewController) as a presentedViewController. I present it from UITableViewController. The PhotoBrowser presents actionMainController (it inherits from UIAlertController) when I tapped on button. I want to call another UIAlertController, when I select one of the actions in the actionMainController and display the second UIAlertController immediately after its dismissed but completion block doesn't work. I want to add that dismiss the actionMainController succeeds, it works
class PhotoBrowser: SKPhotoBrowser {
// MARK: - Controllers
private var actionMainController = UIAlertController()
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - override var
override var prefersStatusBarHidden: Bool {
return true
}
// MARK: - functions
func editAvatar() {
debugPrint("removePhoto")
actionMainController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
actionMainController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
let deleteAction = UIAlertAction(title: "Delete Photo", style: .destructive) { (action) in
self.deletePhoto()
}
let takePhoto = UIAlertAction(title: "Take Photo", style: .default) { (action) in
}
let choosePhoto = UIAlertAction(title: "Choose Photo", style: .default) { (action) in
}
actionMainController.addAction(deleteAction)
actionMainController.addAction(takePhoto)
actionMainController.addAction(choosePhoto)
present(actionMainController, animated: true, completion: nil)
}
private func deletePhoto() {
actionMainController.dismiss(animated: true) {
// TODO: it
// self.showDeleteActionSheet()
}
showDeleteActionSheet()
}
private func showDeleteActionSheet() {
let deleteActionController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
deleteActionController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
let deleteAction = UIAlertAction(title: "Delete Photo", style: .destructive) { (action) in
}
deleteActionController.addAction(deleteAction)
present(deleteActionController, animated: true, completion: nil)
}
}
Your completion handler is not called since your alert has already been dismissed and you are again trying to dismiss the alert.
actionMainController.dismiss(animated: true) {
self.showDeleteActionSheet()
}
Instead you should do some work on the completion handler of Action as
let deleteAction = UIAlertAction(title: "Delete Photo", style: .destructive) { (action) in
self.showDeleteActionSheet()
}

Resources