UIViewController not accessible after consumable IAP purchase success - ios

I have a consumable IAP that seems to be working but I cannot seem to access the ViewController after the purchase.
extension IAPService: SKPaymentTransactionObserver
{
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction])
{
for transactions in transactions
{
print(transactions.transactionState.status(), transactions.payment.productIdentifier)
if transactions.transactionState.status() == SKPaymentTransactionState.purchased.status()
{
if transactions.payment.productIdentifier == IAPProduct.Consumable.rawValue
{
print("\n\n\n\n\n\n\nCONSUMABLE")
let vc = ViewController()
vc.purchaseTenGold()
SKPaymentQueue.default().finishTransaction(transactions)
}
}
}
}
}
I've tried to add a green button to see if it can access the viewController but the button won't appear.
I've tried to push the view to the viewController but nothing happens.
func purchaseTenGold()
{
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute:
{
let pButton = UIButton()
pButton.translatesAutoresizingMaskIntoConstraints = false
pButton.backgroundColor = .green
self.view.addSubview(pButton)
NSLayoutConstraint.activate([
pButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
pButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
pButton.widthAnchor.constraint(equalToConstant: 200),
pButton.heightAnchor.constraint(equalToConstant: 200),
])
print("\n\n\n\n\n\n\n\n\n\nPURCHASED 10 YEAH????WTF???")
})
}
I just want to add some game coins but can't access the viewController, so I've been trying to add something to the view or make something unhidden that has already loaded to the view but can't access it.
Everything prints out perfectly.
I have another published app where this all works well and I can add the game coins, but this does not work.
The one difference between that app and this one is that with the other one I have a navigation controller with a few view controllers, this app only has the one vc.
Thanks in advance for any help.
Just added this to access the view but I'm still not able to access the viewController to make any changes...
let alert = UIAlertController(title: "", message: "YOU SUCCESSFULLY\nCREATED A NEW\nALERT CONTROLLER", preferredStyle: .alert)
func okAlert(alert: UIAlertAction!)
{
}
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: okAlert))
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
var rootVC = window?.rootViewController
if var topController = rootVC
{
while let presentedViewController = topController.presentedViewController
{
topController = presentedViewController
}
rootVC = topController
}
rootVC?.present(alert, animated: true, completion: nil)

Related

pushing ViewController over current ViewController from deeplink swift

I am using deeplink in my application and from the central area, I am trying push a screen in another ViewController based on my deeplink.
The issues I have is that when I cancel the pushed ViewController, the entire application stack is dismissed and I just want to pop back to the presenting viewcontroler.
func getCurrentNanvigationController() -> UINavigationController? {
//targeted UINavigationController
var navigationController: UINavigationController? = nil
if let nav = window?.rootViewController as? UINavigationController {
if let topNav = window?.topViewController()?.navigationController {
navigationController = topNav
}
else{
navigationController = nav
}
}
// Wallet Module is the stand alone module, which means its not embeded in the Navigator app
else if let tabBar = window?.rootViewController as? UITabBarController,
let nav = tabBar.selectedViewController as? UINavigationController {
navigationController = nav
}
else {
//should not happen, window root controller shouldbe be either UINavigationController or UITabBarController
}
return navigationController
}
public extension UIWindow {
func topViewController() -> UIViewController? {
var top = self.rootViewController
while true {
if let presented = top?.presentedViewController {
top = presented
} else if let nav = top as? UINavigationController {
top = nav.visibleViewController
} else if let tab = top as? UITabBarController {
top = tab.selectedViewController
} else {
break
}
}
return top
}
}
This is the currentViewController that I am trying to push another controller over from the deeplink
#objc func addNavBarItemTapped() {
let storyBoard = UIStoryboard(storyboard: .addVC, bundle: .main)
let controller = storyBoard.instantiateViewController(withIdentifier: "AddViewController")
self.navigationController?.pushViewController(controller, animated: true)
}
How can I effectively push over the AddViewController because that is the presenting ViewController and when I dismiss the presented ViewController from the navigation stack, I am not removing the entire app navigation but instead returning back to AddViewController
How the dismissal of this view controller is achieved is like this.
public extension UIViewController {
func alert(title: String,
message: String? = nil ,
attributedMessage: NSMutableAttributedString? = nil,
okAction: AlertActionButton = ("ok_button".fpxLocalizedText, .default, nil),
cancelAction: AlertActionButton = (nil, .cancel, nil),
complete: (() -> Void)? = nil) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
if let attributedMessage = attributedMessage {
alertController.setValue(attributedMessage, forKey: "attributedMessage")
}
let oKAction = UIAlertAction(title: okAction.0, style: okAction.1, handler: okAction.2)
if let cancelButtonTitle = cancelAction.0 {
let cancelAction = UIAlertAction(title: cancelButtonTitle, style: cancelAction.1, handler: cancelAction.2)
alertController.addAction(cancelAction)
}
alertController.addAction(oKAction)
self.present(alertController, animated: true, completion: complete)
}
}
In the presented Viewcontroller
public func declineButtonClicked() {
alert(title: "decline_warning_title".fpxLocalizedText,
message: "decline_warning_message".fpxLocalizedText,
okAction: ("yes_button".fpxLocalizedText, .destructive, { _ in self.sendDeclineRequest() }),
cancelAction: ("decline_cancel_button".fpxLocalizedText, .cancel, { _ in self.dismiss(animated: true, completion: nil) }))
}
Also sometimes if I have a presented Viewcontroller and need to show this ViewController, it is often presented in the background of the presented viewcontroller
In iOs push duty perform with UINavigationController
wherever you need to push so you need to UINavigationController.
You should Create another UInavigationController in Deep Link,
let storyBoard = UIStoryboard(storyboard: .addVC, bundle: .main)
let controller = storyBoard.instantiateViewController(withIdentifier: "AddViewController")
let navigationController = UINavigationController(rootViewController:controller)
self.navigationController?.present(navigationController, animated: true, completion: nil)
or
window?.getCurrentNanvigationController().present(navigationController, animated: true, completion: nil)
so now you have a new navigation controller that work stand alone.

How to present UIAlertController in front of modal sheet in Swift

I'm trying to show an alert with a textfield from a modal in swift, but getting an error when I try to show it.
I have to use a UIAlertController because the default alert in Swift doesn't support Textfields currently.
I am calling my modal from my base view with
.sheet(isPresented: $showingLocationSheet) {
LocationSelectionView(selectedLocations: $locations)
}
From my LocationSelectionView, I then call the alert as follows:
let alert = UIAlertController(title: "Add Location", message: "Enter Location Name", preferredStyle: .alert)
alert.addTextField { (name) in
name.placeholder = "Bathroom"
}
let add = UIAlertAction(title: "Add", style: .default) { _ in
print("add Location")
}
let cancel = UIAlertAction(title: "Cancel", style: .destructive)
alert.addAction(add)
alert.addAction(cancel)
DispatchQueue.main.async {
UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true)
}
but then I receive this error:
Attempt to present <UIAlertController: 0x7fc8a78c7c00> on <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7fc8a800b8d0> (from <_TtGC7SwiftUI19UIHostingControllerGVS_15ModifiedContentVS_7AnyViewVS_12RootModifier__: 0x7fc8a800b8d0>) which is already presenting <_TtGC7SwiftUI29PresentationHostingControllerVS_7AnyView_: 0x7fc8a6e51980>.
I'm not quite sure how to get it to show here. I've also tried using an extension on the UIAlertController like this:
public extension UIAlertController {
func show() {
let win = UIWindow(frame: UIScreen.main.bounds)
let vc = UIViewController()
vc.view.backgroundColor = .clear
win.rootViewController = vc
win.windowLevel = UIWindow.Level.alert + 1 // Swift 3-4: UIWindowLevelAlert + 1
win.makeKeyAndVisible()
vc.present(self, animated: true, completion: nil)
}
as well as getting the top most controller like so:
func topmostController() -> UIViewController? {
if var topController = UIApplication.shared.keyWindow?.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
return nil
}
but no luck. Any ideas?

Prevent presenting the UIAlertViewController after navigating to the other view

I have one scenario when the user did not use the application for more than 5 min app will show a popup with session expiration message.
The code for session expiration is added in the appDelegate and from there the popup will be presented on the current view controller.
code is
#objc func applicationDidTimeout(notification: NSNotification) {
if (window?.rootViewController?.isKind(of: UITabBarController.self))! {
for view in window?.rootViewController?.view.subviews ?? [(window?.rootViewController?.view)!] {
if view.isKind(of: MBProgressHUD.self) {
return
}
}
if window?.rootViewController?.presentedViewController != nil {
window?.rootViewController?.dismiss(animated: true, completion: {
self.showMessage(message: Message.sessionTimeout)
})
} else {
self.showMessage(message: Message.sessionTimeout)
}
}
}
fileprivate func showMessage(message: String) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
DispatchQueue.main.async {
UIView.transition(with: self.window!, duration: 0.3, options: UIView.AnimationOptions.transitionCrossDissolve, animations: {
CommonFunctions.setLoginAsRootVC()
}, completion: nil)
}
}
alert.addAction(actionOkay)
self.window?.rootViewController?.present(alert, animated: true, completion: nil)
}
Now if the user is doing some data entry and at that time, if the user leaves application ideal for 5 min or more the keyboard will dismiss and the session expiration message shown there.
But as the text field's delegate method textFieldShouldEndEditing has some validation and if that validation fails it shows a popup with the message and ok button.
So when the user taps on the ok button in the session expiration message popup, it will redirect the user to the login screen but due to the text field's delegate method validation, it shows one pop up in the login screen.
Code for the validation fail message popup is
fileprivate func showErrorMessage(message: String) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
self.txtField.becomeFirstResponder()
}
alert.addAction(actionOkay)
self.present(alert, animated: true, completion: nil)
}
How to prevent the popup from being present in the login screen?
I try to get the proper way to prevent the popup from appearing on the login screen.
But Finally, I found one heck to solve this issue.
I have declared one boolean in AppDelegate and set it's value to false when I want to prevent the popup from appearing and then revert it back to true when I want to show the popup.
I know this is not the elegant or efficient solution for the issue, but it works for now.
If anyone knows the better answer can post here, I'm still open to any better solution.
#objc func applicationDidTimeout(notification: NSNotification)
{
let visibleView : UIViewController = self.getVisibleViewControllerFrom(self.window?.rootViewController)!
self.showMessage(message: Message.sessionTimeout,Controller: visibleView)
}
fileprivate func showMessage(message: String , Controller : UIViewController) {
let alert = UIAlertController(title: appName, message: message, preferredStyle: .alert)
let actionOkay = UIAlertAction(title: "OK", style: .default) { (action) in
//Now apply your code here to set login view controller as rootview
// This controller is for demo
window!.rootViewController = UIStoryboard(name: "Main", bundle:
nil).instantiateViewController(withIdentifier: "loginview")
window!.makeKeyAndVisible()
}
alert.addAction(actionOkay)
Controller.present(alert, animated: true, completion: nil)
}
//MARK:- Supporting method to get visible viewcontroller from window
func getVisibleViewControllerFrom(_ vc: UIViewController?) -> UIViewController? {
if let nc = vc as? UINavigationController {
return self.getVisibleViewControllerFrom(nc.visibleViewController)
} else if let tc = vc as? UITabBarController {
return self.getVisibleViewControllerFrom(tc.selectedViewController)
} else {
if let pvc = vc?.presentedViewController {
return self.getVisibleViewControllerFrom(pvc)
} else {
return vc
}
}
}
Try this code, I've use this code many times may be it's work for you.

How can I restart an application programmatically in Swift 4 on iOS?

I have issues. After language changing, I want to restart my application.
So I want to get an alert message with the text "Do you want to restart app to change language?" "Yes" "No"
And if the user presses YES, how can I restart the app?
My solution:
let alertController = UIAlertController(title: "Language".localized(), message: "To changing language you need to restart application, do you want to restart?".localized(), preferredStyle: .alert)
let okAction = UIAlertAction(title: "Yes".localized(), style: UIAlertActionStyle.default) {
UIAlertAction in
NSLog("OK Pressed")
exit(0)
}
let cancelAction = UIAlertAction(title: "Restart later".localized(), style: UIAlertActionStyle.cancel) {
UIAlertAction in
NSLog("Cancel Pressed")
}
alertController.addAction(okAction)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
After the app will close, the user will manually start the app.
You cannot restart an iOS app. One thing you could do is to pop to your rootViewController.
func restartApplication () {
let viewController = LaunchScreenViewController()
let navCtrl = UINavigationController(rootViewController: viewController)
guard
let window = UIApplication.shared.keyWindow,
let rootViewController = window.rootViewController
else {
return
}
navCtrl.view.frame = rootViewController.view.frame
navCtrl.view.layoutIfNeeded()
UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: {
window.rootViewController = navCtrl
})
}
In one of my apps, I needed to restart. I wrapped all of the loading logic into a LaunchScreenViewController. Above is the piece of code for "restarting the app".
It's not perfect, but I solved this by sending a timed local notification just before exiting the application. Then the user only needs to click on the notification to restart the app so they don't need to look for the app to relaunch it. I also suggest displaying an alert informing the user of the restart:
import UserNotifications
import Darwin // needed for exit(0)
struct RestartAppView: View {
#State private var showConfirm = false
var body: some View {
VStack {
Button(action: {
self.showConfirm = true
}) {
Text("Update Configuration")
}
}.alert(isPresented: $showConfirm, content: { confirmChange })
}
var confirmChange: Alert {
Alert(title: Text("Change Configuration?"), message: Text("This application needs to restart to update the configuration.\n\nDo you want to restart the application?"),
primaryButton: .default (Text("Yes")) {
restartApplication()
},
secondaryButton: .cancel(Text("No"))
)
}
func restartApplication(){
var localUserInfo: [AnyHashable : Any] = [:]
localUserInfo["pushType"] = "restart"
let content = UNMutableNotificationContent()
content.title = "Configuration Update Complete"
content.body = "Tap to reopen the application"
content.sound = UNNotificationSound.default
content.userInfo = localUserInfo
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.5, repeats: false)
let identifier = "com.domain.restart"
let request = UNNotificationRequest.init(identifier: identifier, content: content, trigger: trigger)
let center = UNUserNotificationCenter.current()
center.add(request)
exit(0)
}
}
You can change your root controller and don't need to restart it. Just change the root view controller or update or refresh or recall the root view controller:
let alertController = UIAlertController(title: "Language".localized(), message: "To change language you need to restart the application. Do you want to restart?".localized(), preferredStyle: .alert)
let okAction = UIAlertAction(title: "Yes".localized(), style: UIAlertActionStyle.default) {
UIAlertAction in
// Change update / refresh rootview controller here...
}
You can add to AppDelegate
func resetApp() {
UIApplication.shared.windows[0].rootViewController = UIStoryboard(
name: "Main",
bundle: nil
).instantiateInitialViewController()
}
Call this function where you want
let appDelegate = AppDelegate()
appDelegate.startWith()
Add this to your viewDidLoad for starting the viewcontroller (VC):
override func viewDidLoad() {
super.viewDidLoad()
// Make dismiss for all VC that was presented from this start VC
self.children.forEach({vc in
print("Dismiss \(vc.description)")
vc.dismiss(animated: false, completion: nil)
})
// ....
}
And in the restart initiator:
// ...
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "startVC")
self.present(vc, animated: false, completion: nil)

Dispatch asyncAfter for multiple UIAlertControllers

I have a dispatch async where I expect 4 alerts pop up on the screen..and then each get dismissed before a new alert is to be shown. (I've set a 3 second delay in between my Alert)
class ViewController: UIViewController {
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...4{
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0 * Double(counter) , execute: {
print("called")
self.showAlert()
})
}
}
func showAlert(){
let alert = UIAlertController(title: "sampleTitle \(counter)", message: "sampleMessage \(counter)", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default, handler: nil)
alert.addAction(action)
counter += 1
if self.presentedViewController != nil {
dismiss(animated: true, completion: nil)
UIApplication.topViewController()?.present(alert, animated: true, completion: nil)
}else{
UIApplication.topViewController()?.present(alert, animated: true, completion: nil)
}
}
}
Problem1: But for some reason the entire for loop is executed without any delay in between. I'm guessing I'm not understanding something about main queue being a serial queue.
Problem2: And I also get the following logs in my console, even though I'm dismissing the presentedViewController.
called
called
called
called
2017-06-26 11:10:57.000 topViewAndAlertTest[3360:210226] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fe630c03350> while a presentation or dismiss is in progress!
2017-06-26 11:10:57.001 topViewAndAlertTest[3360:210226] Warning: Attempt to present <UIAlertController: 0x7fe630c04180> on <UIAlertController: 0x7fe630f06fe0> while a presentation is in progress!
2017-06-26 11:10:57.001 topViewAndAlertTest[3360:210226] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fe630c03350> while a presentation or dismiss is in progress!
2017-06-26 11:10:57.001 topViewAndAlertTest[3360:210226] Warning: Attempt to present <UIAlertController: 0x7fe630c06b40> on <UIAlertController: 0x7fe630f06fe0> while a presentation is in progress!
FYI My topviewcontroller is using the code from this answer
Problem3: Only 2 alerts pop...I never see the 3rd, 4th alerts!
EDIT:
After rmaddy's suggestion, my errors are slightly changed:
called
called
called
called
2017-06-26 11:59:33.417 topViewAndAlertTest[4834:441163] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fb596d05a30> while a presentation or dismiss is in progress!
2017-06-26 11:59:33.417 topViewAndAlertTest[4834:441163] Warning: Attempt to dismiss from view controller <topViewAndAlertTest.ViewController: 0x7fb596d05a30> while a presentation or dismiss is in progress!
I get 2 less warnings. But still: As soon as alert 1 is on screen, alert 2 dismisses it and that's it! No delay no 3rd,4th alert!
Details
xCode 8.3.2, Swift 3.1
Full Code
import UIKit
class ViewController: UIViewController {
var counter = 1
private var alertViewController: UIAlertController?
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .utility).async {
for _ in 1...4 {
sleep(2)
DispatchQueue.main.async {
print("called")
self.showAlert()
}
}
}
}
private func createAlertView() -> UIAlertController {
let alertViewController = UIAlertController(title: "sampleTitle \(counter)", message: "sampleMessage \(counter)", preferredStyle: .alert)
let action = UIAlertAction(title: "OK", style: .default, handler: nil)
alertViewController.addAction(action)
return alertViewController
}
func showAlert(){
let presentAlert = {
DispatchQueue.main.async { [weak self] in
if let _self = self {
_self.alertViewController = _self.createAlertView()
UIApplication.topViewController()?.present(_self.alertViewController!, animated: true, completion: nil)
}
}
}
DispatchQueue.main.async { [weak self] in
if let alertViewController = self?.alertViewController {
alertViewController.dismiss(animated: true) {
presentAlert()
}
} else {
presentAlert()
}
self?.counter += 1
}
}
}
extension UIApplication {
class func topViewController(base: UIViewController? = (UIApplication.shared.delegate as! AppDelegate).window?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}

Resources