I am new to Swift and iOS development. I have been trying to create an app-wide check for internet connectivity, that allows the user to retry (reload the current view). The checkInternetConnection() function below is called during viewWillAppear() in my BaseUIViewController class.
The Reachability.isConnectedToNetwork() connectivity-check works fine. But so far, I have not been able to figure out a way to reload the current view after the user presses the 'Retry' button in the alert. in fact, I have not been able to figure out a way to reload the current view in any scenario.
One thing I know I am doing wrong in the following attempt (since there is a compile error telling me so), is that I am passing a UIViewController object as a parameter instead of a string on this line: self.performSegueWithIdentifier(activeViewController, sender:self), but I suspect that this is not my only mistake.
func checkInternetConnection() {
if (Reachability.isConnectedToNetwork()) {
print("Internet connection OK")
} else {
print("Internet connection FAILED")
let alert = UIAlertController(title: NSLocalizedString("Error!", comment: "Connect: error"), message: NSLocalizedString("Could not connect", comment: "Connect: error message"), preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: "Connect: retry"), style: .Default, handler: { action in
print("Connection: Retrying")
let navigationController = UIApplication.sharedApplication().windows[0].rootViewController as! UINavigationController
let activeViewController: UIViewController = navigationController.visibleViewController!
self.performSegueWithIdentifier(activeViewController, sender:self)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
}
BaseUIViewController can perform app-wide check for connectivity, so all your ViewControllers will be inherited from BaseUIViewController.
And each ViewController will have different behaviour after connectivity check.
One thing you can do is, in your BaseUIViewController you can define a block that performs action after connectivity check failed.
Here's the example:
class BaseViewUIController: UIViewController {
var connectivityBlock: (() -> Void)?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
checkInternetConnection()
}
func checkInternetConnection() {
if Reachability.isConnectedToNetwork() {
print("Internet connection OK")
} else {
if let b = connectivityBlock {
b()
}
}
}
}
And in your inherited ViewControllers:
class MyViewController: BaseUIViewController {
override func viewDidLoad() {
super.viewDidLoad()
connectivityBlock = {
print("Do View Refresh Here.")
}
}
}
Then After the check in BaseUIViewController, the code in connectivityBlock will be executed so that each VC can deal with it differently.
You can also define a function in BaseUIViewController and each subclass will override that function, after connectivity check failed, call the function.
Try navigate using application's windows
let destinationVC = UIApplication.shared.delegate?.window
destinationVC .present(activeViewController, animated: true, completion: nil)
Related
I'm trying to make a password protected view controller.
so far -
Created storyboard -
on viewcontroller - created hard coded log in -
prints to console if successful or not.
textfields etc...
#IBOutlet weak var untext: UITextField!
#IBOutlet weak var pwtext: UITextField!
let username = "admin"
let password = "adminpw"
override func viewDidLoad() {
super.viewDidLoad()
pwtext.isSecureTextEntry = true
}
#IBAction func loginbtn(_ sender: Any) {
if untext.text == username && pwtext.text == password
{
print("log in succesful")
} else {
print("log in failed")
}
}
The issue I have, once I press the login button, it takes me to the admin page if successful or not.
How can I print a notification - on screen - if unsuccessful and remain on the current view controller, and if successful, take me to admin view controller?
You can either use a segue or instantiateViewController. But in this example I'll use instantiateViewController (Images). (But commented how to use a segue)
Add a class and an identifier to your secondary ViewController
Choose between my Segue or Instantiate. (Check my comments in the code)
If login is succeeded, either perform the segue or navigate using instantiate.
Happy coding. :D
But first off, let's take a look at the code you provided.
#IBAction func loginbtn(_ sender: Any)
{
if untext.text == username && pwtext.text == password
{
print("login succeeded")
//1. using instantiateViewController
if let storyboard = storyboard
{
//Check my image below how to set Identifier etc.
// withIdentifier = Storyboard ID & "ViewController" = Class
let vc = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
self.present(vc, animated: false, completion: nil)
}
//2. Use segue (I'll wrap this with a comment incase you copy)
//self.performSegue(withIdentifier: "SegueID", sender: self)
}
else
{
//Setting up an "AlertController"
let alert = UIAlertController(title: "Login failed", message: "Wrong username / password", preferredStyle: UIAlertController.Style.alert)
//Adding a button to close the alert with title "Try again"
alert.addAction(UIAlertAction(title: "Try again", style: UIAlertAction.Style.default, handler: nil))
//Presentating the Alert
self.present(alert, animated: true, completion: nil)
}
}
Click on the yellow dot on your ViewController (On the ViewController where you want the login-page to take you)
Click on the icon like I've. (Which is blue) and set a Class + Storyboard ID.
NOTE! IF you wanna use a segue, make SURE you have a connection between ViewController(Login) and ViewController1
Assuming you use segues for navigation, you can put a "general purpose" segue (drag from your controller, instead of any controls in it) and assign it an ID (Identifier in attribute inspector of the segue in Storyboard). After that you can conditionally invoke segue from the parent controller class with your code:
if passwordCorrect {
performSegue(withIdentifier: "SegueID", sender: nil)
}
The problem here is that I'm presenting EditCommentVC modally, over the current context of the CommentVC because I want to set the background of the UIView to semi-transparent. Now, on the EditCommentVC I have a UITextView that allows the user to edit their comment, along with 2 buttons - cancel (dismisses the EditCommentVC) and update that updates the new comment and push it to the database.
In term of code, everything is working, except that once the new comment is being pushed and EditCommentVC is being dismissed, the UITableView on CommentsVC with all the comments is not being reloaded to show the updated comments. Tried calling it from viewWillAppear() but it doesn't work.
How can I reload the data in the UITableView in this case?
#IBAction func updateTapped(_ sender: UIButton) {
guard let id = commentId else { return }
Api.Comment.updateComment(forCommentId: id, updatedComment: editTextView.text!, onSuccess: {
DispatchQueue.main.async {
let commentVC = CommentVC()
commentVC.tableView.reloadData()
self.dismiss(animated: true, completion: nil)
}
}, onError: { error in
SVProgressHUD.showError(withStatus: error)
})
}
The code in the CommentVC where it transitions (and passes the id of the comment). CommentVC conforms to a CommentActionProtocol that passes the id of that comment:
extension CommentVC: CommentActionProtocol {
func presentActionSheet(for commentId: String) {
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let editAction = UIAlertAction(title: "Edit", style: .default) { _ in
self.performSegue(withIdentifier: "CommentVCToEditComment", sender: commentId)
}
actionSheet.addAction(editAction)
present(actionSheet, animated: true, completion: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CommentVCToEditComment" {
let editCommentVC = segue.destination as! EditCommentVC
let commentId = sender as! String
editCommentVC.commentId = commentId
}
}
}
I see atleast 2 problems here:
You are creating a new CommentVC which you should not do if you want to update the tableView in the existing view controller.
Since you have mentioned that Api.Comment.updateComment is a an asynchronous call, you need to write the UI code to run on the main thread.
So first you need to have the instance of the commentVC in a variable inside this viewController. You can store the instance of the view controller from where you are presenting this view controller.
class EditCommentVC {
var commentVCdelegate: CommentVC!
// Rest of your code
}
Now you need to pass the reference commentVC in this variable when you are presenting the edit view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "CommentVCToEditComment" {
let editCommentVC = segue.destination as! EditCommentVC
let commentId = sender as! String
editCommentVC.commentId = commentId
editCommentVC.commentVCdelegate = self
}
}
Now you need to use this reference to reload your tableView.
Api.Comment.updateComment(forCommentId: id, updatedComment: editTextView.text!, onSuccess: {
DispatchQueue.main.async {
commentVCdelegate.tableView.reloadData() // - this commentVC must be an instance that you store of the your commentVC that you created the first time
self.dismiss(animated: true, completion: nil)
}
}, onError: { error in
SVProgressHUD.showError(withStatus: error)
})
Well, i had this problem too, and the solution i found was to use Protocol. I would recommend you to search how to send data back to previous ViewController. That way, when you dismiss the EditCommentVC, you then send back a value(in my case i send true) to the previous ViewController(in your case, CommentVC), and then you'll have a function in CommentVC checking if the value is true and if it is, reload the TableView.
Here, let me show you an example of how i used (those are the names of my ViewControllers, functions and protocols, you can use whatever you want and send whatever data you want back):
In your CommentVC, you'll have something like this:
protocol esconderBlurProtocol {
func isEsconder(value: Bool)
}
class PalestranteVC: UIViewController,esconderBlurProtocol {
func isEsconder(value: Bool) {
if(value){
//here is where you can call your api again if you want and reload the data
tableView.reloadData()
}
}
}
Also, dont forget that you have to set the delegate of EditCommentVC, so do it when you're presenting EditCommentVC, like this:
let viewController = (self.storyboard?.instantiateViewController(withIdentifier: "DetalhePalestranteVC")) as! DetalhePalestranteVC
viewController.modalPresentationStyle = .overFullScreen
viewController.delegate = self
self.present(viewController, animated: true, completion: nil)
//replace **DetalhePalestranteVC** with your **EditCommentVC**
And in your EditCommentVC you'll have something like this:
class DetalhePalestranteVC: UIViewController {
var delegate: esconderBlurProtocol?
override func viewWillDisappear(_ animated: Bool) {
delegate?.isEsconder(value: true)
}
}
That way, everything you dismiss EditCommentVC, you'll send back True and reload the tableView.
I attempt to add a function, that is a mail page would pop up after the user touched a row in a table. Namely, it means that the user could activate a "function" (here the name of that function is "orderOfSendAnEmailToReportTheProblem") when the row is tapped. All of my codes were shown below. (This kind of code has been proposed by several genii on Stackoverflow...)
import Foundation
import UIKit
import MessageUI
class ReportProblem : UIViewController, MFMailComposeViewControllerDelegate {
func orderOfSendAnEmailToReportTheProblem() {
let mailComposeViewController = configureMailController()
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: true, completion: nil)
} else {
showMailError()
}
}
//Activate the series of the commands of sending the email.
func configureMailController() -> MFMailComposeViewController {
let mailComposeVC = MFMailComposeViewController()
mailComposeVC.mailComposeDelegate = self
mailComposeVC.setToRecipients(["my email"])
mailComposeVC.setSubject("Yo")
return mailComposeVC
}
//Set the recipient and the title of this email automatically.
func showMailError() {
let sendMailErrorAlert = UIAlertController(title: "Could not sned the email.", message: "Oops, something was wrong, please check your internet connection once again.", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Ok", style: .default, handler: nil)
sendMailErrorAlert.addAction(dismiss)
self.present(sendMailErrorAlert, animated: true, completion: nil) //If you conform the protocol of NSObject instead of UIViewController, you could not finish this line successfully.
}
//Set a alert window so that it would remind the user when the device could not send the email successfully.
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
}
//Set this final step so that the device would go to the previous window when you finish sending the email.
}
However, a problem occurred. When I test it on my real device, and after I tapped that particular row, nothing happened, no any new page pop up... The Xcode only showed that "Warning: Attempt to present on whose view is not in the window hierarchy!" I have tried several ways, such as "view.bringSubview(toFront: mailComposeVC)" or adding the codes shown below at the end of my codes, but nothing worked.
func topMostController() -> UIViewController {
var topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while (topController.presentedViewController != nil) {
topController = topController.presentedViewController!
}
return topController
}
I noticed that some other people also would face similar problems when they want to create the alert window, and the solution of that is to create an independent UIWindow, but I want to use mailComposeController to present the email page instead. Some others also faced some problems about MFMailComposeViewController, but their problems are not concerning to hierarchy. I was a novice of swift, and I was haunted by this problem for a whole day... I used swift 4 to develop my App, is anyone know how to solve this problem here?...
So now I'm writing another way to present which I'm using for generic views.
Have Some code in another class for presentation of view so that you can reuse them throughout the app with these two methods.
func slideInFromRight(parentView:UIView,childView:UIView) {
childView.transform = CGAffineTransform(translationX: parentView.frame.maxX, y: 0)
parentView.addSubview(childView)
UIView.animate(withDuration: 0.25, animations: {
childView.transform = CGAffineTransform(translationX: 0, y: 0)
})
}
func slideOutToRight(view:UIView) {
UIView.animate(withDuration: 0.25, animations: {
view.transform = CGAffineTransform(translationX: view.frame.maxX, y: 0)
},completion:{(completed:Bool) in
view.removeFromSuperview()
})
}
Now use these methods to present and remove custom view controller as follows
let window = UIApplication.shared.keyWindow
let vc = YourViewController().instantiate()
self.addChildViewController(vc)
let view = vc.view
view.frame = CGRect(x: 0, y: 20, width: window!.frame.width, height: window!.frame.height-20)
//Here Animation is my custom presenter class and shared is it's shared instance.
Animation.shared.slideInFromRight(parentView: window!, childView: view)
//Or you can use current View controller's view
Animation.shared.slideInFromRight(parentView: self.view!, childView: view)
Genius Vivek Singh, your way looks good, but it's a little bit tedious. Moreover, it still did not work in my project... (It seems that you used some codes about UIView, such as parentView, childView, and view. However, I used MFMailComposeViewController which seems is a little bit different from original view...I am not sure whether this theory is correct or not...)
However, I have found the solution. I presume that the problem is that after the user clicked the row in another tableViewController (here is SettingTVController), it would activate the function "orderOfSendAnEmailToReportTheProblem( )" which is in "another " viewController (here is ReportProblem). Because there are two different viewController, some kind of conflict occurred.
Therefore, I move my whole codes I posted in the above question to my original tableViewController, so that the user would not go into another viewController when they activate the function, and there's no hierarchy problem anymore.
import UIKit
import StoreKit
import MessageUI
class SettingTVController: UITableViewController, MFMailComposeViewControllerDelegate {
var settingTitleConnection = showData()
override func viewDidLoad() {
//skip
}
override func didReceiveMemoryWarning() {
//skip
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//skip
}
override func numberOfSections(in tableView: UITableView) -> Int {
//skip
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//skip
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if tableView.indexPathForSelectedRow?.row == 2 && tableView.indexPathForSelectedRow?.section == 1 {
orderOfSendAnEmailToReportTheProblem()
} else {
//skip
}
tableView.deselectRow(at: indexPath, animated: true)
}
//-----<The codes below is used to construct the function of reporting problem with email>-----
func orderOfSendAnEmailToReportTheProblem() {
let mailComposeViewController = configureMailController()
self.present(mailComposeViewController, animated: true, completion: nil)
if MFMailComposeViewController.canSendMail() {
self.present(mailComposeViewController, animated: false, completion: nil)
} else {
showMailError()
}
}
//Activate the series of the commands of sending the email.
func configureMailController() -> MFMailComposeViewController {
let mailComposeVC = MFMailComposeViewController()
mailComposeVC.mailComposeDelegate = self
mailComposeVC.setToRecipients(["datototest#icloud.com"])
mailComposeVC.setSubject("Reporting of Problems of Rolling")
return mailComposeVC
}
//Set the recipient and the title of this email automatically.
func showMailError() {
let sendMailErrorAlert = UIAlertController(title: "Could not send the email.", message: "Oops, something was wrong, please check your internet connection once again.", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Ok", style: .default, handler: nil)
sendMailErrorAlert.addAction(dismiss)
self.present(sendMailErrorAlert, animated: true, completion: nil) //If you conform the protocol of NSObject instead of UIViewController, you could not finish this line successfully.
}
//Set a alert window so that it would remind the user when the device could not send the email successfully.
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true, completion: nil)
//UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true, completion: nil)
}
//Set this final step so that the device would go to the previous window when you finish sending the email.
//-----<The codes above is used to construct the function of reporting problem with email>-----
}
I posted my codes above so that it may help others who face similar problem someday. Once again, thanks for your help!!
I don't know why you are facing a view hierarchy issue. But I am able to achieve the mail share option in swift 4. I followed exactly same steps.
Check if mail can be sent:
MFMailComposeViewController.canSendMail()
Configure mail body:
private func configureMailController() -> MFMailComposeViewController {
let mailComposeViewController = MFMailComposeViewController()
mailComposeViewController.mailComposeDelegate = self
mailComposeViewController.setMessageBody("MESSAGE BODY", isHTML: true)
return mailComposeViewController
}
Present mail VC:
present(mailComposeViewController, animated: true)
confirm optional protocol and dismiss the view explicitly:
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true)
}
is there anyway to wait the user to press the button that dismiss the alertController in swift 3, by using DispatchQueue or something else?
Just move all the code you want to execute after the alert is dismissed into a separate method. When you're adding UIAlertActions, make all the actions' handler parameter call that method. This way, whichever button the user presses, your code will always be executed!
You mean something like this?
alertController.displayAndWaitUntilDismissed()
// This line is only reached after the alert controller is dismissed
print("Alert controller dismissed.")
Theoretically, yes, you could use a dispatch semaphore to block until the alert is dismissed. But it’s a bad idea – I can’t even think of a scenario where it would be acceptable. Simply accept that you have to deal with it asynchronously, by executing the desired code in the alert controller action.
You MUST NOT block the main thread and waiting for dismissing your alert so the only way is doing this asynchronously.
For instance you can use next common approach with UIViewControllerTransitioningDelegate:
fileprivate var AssociatedObjectKey: UInt8 = 0
extension UIAlertController : UIViewControllerTransitioningDelegate {
var onDissmiss: (() -> Void)? {
set {
transitioningDelegate = newValue != nil ? self : nil
objc_setAssociatedObject(self, &AssociatedObjectKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, &AssociatedObjectKey) as? (() -> Void)
}
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
onDissmiss?()
return nil
}
}
How to use:
let alert = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default)
alert.addAction(ok)
alert.onDissmiss = {
print("dissmiss")
}
present(alert, animated: true)
The simple way to display a UIAlertView, in swift is:
let alert = UIAlertView()
alert.title = "Alert!"
alert.message = "A wise message"
alert.addButtonWithTitle("Ok, thank you")
alert.show()
But this is now depreciated in iOS 9 and recommends using UIAlertController:
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.presentViewController(myAlert, animated: true, completion: nil)
Which is great, but I'm using SpriteKit and inside a GameScene, which gives an error of Value of type 'GameScene' has no member 'presentViewController'...
Do I need to switch back to my ViewController to present this or is there a way to call it from a GameScene.
I found THIS answer, but it's Objective-C.
There are many ways to handle this situation, I do not recommend Jozemite Apps answer, because this will cause problems on apps with more than 1 view controller.(you want to present the alert on the current view controller, not the root)
My preferred way of doing it is through delegation.
What needs to be done is create a protocol to handle messaging:
import Foundation
protocol ViewControllerDelegate
{
func sendMessage(message:String);
}
In your view controller:
class ViewController : UIViewController, ViewControllerDelegate
{
...
func sendMessage(message:String)
{
//do alert view code here
}
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.delegate = self
}
In your view code:
var delegate : ViewControllerDelegate
Finally in game scene where you want to present:
self.view.delegate?.sendMessage(message)
This way allows limited access to the VC, and can be modified with more options when needed.
Another way is to set up a notification system, and use NSNotificationCenter to pass a message from the scene to the current VC and have it send a message;
in ViewController
func viewDidLoad()
{
NSNotificationCenter.defaultCenter().addObserver(self,selector:"AlertMessage:",name:"AlertMessage",object:nil);
}
func AlertMessage(notification:NSNotification)
{
if(let userInfo = notification.userInfo)
{
let message = userInfo["message"]
....//do alert view call here
}
}
In Game scene code:
...at the spot you want to send a message
let userInfo = ["message":message];
NSNotificationCenter.defaultCenter.postNotificationNamed("AlertMessage",object:nil,userInfo:userInfo)
Another approach is to save the view controller pointer to game scene view:
//in Game Scene View code
var viewController : UIViewController;
//in the view controllers view did load event
func viewDidLoad()
{
var view = self.view as! GameSceneView
view.viewController = self
}
//finally in game scene where you want to present
let myAlert: UIAlertController = UIAlertController(title: "Alert!", message: "Oh! Fancy", preferredStyle: .Alert)
myAlert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
self.view.viewController.presentViewController(myAlert, animated: true, completion: nil)
Yet another way is to make your view controller global.
In view controller code:
private var _instance : UIViewController
class ViewController : UIViewController
{
class var instance
{
get
{
return _instance;
}
}
func viewDidLoad()
{
_instance = self;
}
}
Then just call
ViewController.instance!.
whenever you need access to your view controller.
Each of these methods have there strengths and weaknesses, so choose whatever way works best for you.
Try using this. I work in SpriteKit and I used this code for my in app purchase messages in my game, Chomp'd.
self.view?.window?.rootViewController?.presentViewController(myAlert, animated: true, completion: nil)
The answer from #Knight0fDragon is nice, but I think it's a little long.
I will just put here another solution using Podfile of Cocoapoad for new comers (others guys having the same problem).
first you need to install cocoapod and initiate it for your project (very easy to do; check some YouTube video or this link.
in your obtained Podfile, copy, and paste this: pod 'SIAlertView'. It is "A UIAlertView replacement with block syntax and fancy transition styles". (More details here. Please give credit to the libraries Authors you're using.
Finally, in your GameScene.swift file add the following after the import or after the closing bracket of the class GameScene
private extension GameScene {
func showPauseAlert() {
let alertView = SIAlertView(title: "Edess!!", andMessage: "Congratulations! test testing bla bla bla")
alertView.addButtonWithTitle("OK", type: .Default) { (alertView) -> Void in
print("ok was pushed")
}
alertView.show()
}
}
You can add many button with title if you want, and do whatever action you want. Here I just print "Ok was pushed".
The above cited links plus this one helped me to understand and work with this UIAlertView easily in my multiple-level game, to display alert on the "Pause" button of the game.