SKStoreProductViewController's cancel button leaving a white, blank screen - ios

I made a quick sample project with an Action Extension for iOS 9. It works properly in my containing app, but it does NOT in my extension. I used the same code in both view controllers.
The button that presents a store product view controller:
#IBAction func storebutton(sender: AnyObject) {
let storeViewController = SKStoreProductViewController()
let parameters = [SKStoreProductParameterITunesItemIdentifier: NSNumber(integer: 377298193)]
storeViewController.delegate = self
storeViewController.loadProductWithParameters(parameters, completionBlock: nil)
self.presentViewController(storeViewController, animated: true) { () -> Void in }
}
Delegate Method:
//MARK: SKStoreProductViewController Delegate
func productViewControllerDidFinish(viewController: SKStoreProductViewController) {
print("Delegate")
viewController.dismissViewControllerAnimated(true,completion: nil)
}
When I tap the Cancel button on the Store Product View that was from my Action Extension, the store view is dismissed.
But its delegate method never gets called and the whole screen remains totally white - never gets back to Action Extension's view.
Can anyone help me please?

Never mind.
I found out the delegate method was outside of the Class.
I did not know Xcode does not warn about it.

it happens when the SKStoreProductViewController has no delegate set.
SKStoreProductViewController * vc = [SKStoreProductViewController new];
[vc loadProductWithParameters:#{SKStoreProductParameterITunesItemIdentifier:#"00000"} completionBlock:^(BOOL result, NSError * _Nullable error) {
}];
vc.delegate = self;

Related

present a ViewController from a Swift class derived from an NSObject?

This project was written in Objective C and a bridging header and Swift files were added so they can be used at run time. When the app starts, Initializer called in Mock API client is printed in the debugger. Is it possible to present a ViewController from the initializer?
Xcode Error:
Value of type 'MockApiClient' has no member 'present'
//MockApiclient.Swift
import Foundation
class MockApiClient: NSObject
{
override init ()
{
print("Initializer called in Mock API client")
if isLevelOneCompleted == false
{
print("It's false")
let yourVC = ViewController()
self.present(yourVC, animated: true, completion: nil)
} else
{
print("It's true")
}
}
var isLevelOneCompleted = false
#objc func executeRequest()
{
print("The execute request has been called")
isLevelOneCompleted = true
if isLevelOneCompleted {
print("It's true")
} else {
//do this
}
}
}
Update - ViewController.m
// prints "The execute request has been called" from the debugger window
- (void)viewDidLoad {
[super viewDidLoad];
MockApiClient *client = [MockApiClient new];
[client executeRequest];
}
You can't call present(_:animated:completion) because it is a method of UIViewController, not NSObject.
Why not pass a viewController reference to the MockApiClient to present on instead like so. Be sure to check Leaks or Allocations on instruments to avoid the client retaining the controller.
class MockApiClient: NSObject {
var referencedViewController: UIViewController?
override init() {
let presentableViewController = ViewController()
referencedViewController.present(presentableViewController, animated: true, completion: nil)
}
deinit {
referencedViewController = nil
}
}
let apiClient = MockApiClient()
apiClient.referencedViewController = // The view controller you want to present on
Assuming you're using UIKit, you'll have to present the view controller from the nearest available attached view controller. If you know for certain that no other view controllers would currently be presented then you can safely present from the root view controller:
UIApplication.shared.keyWindow?.rootViewController?.present(someViewController, animated: true, completion: nil)
This concept of attached and unattached/detached view controllers is never officially explained but the infamous UIKit warning of presenting view controllers on detached view controllers is real. And the workaround is finding the nearest available attached view controller, which at first (when nothing is currently being presented) is the root view controller (of the window). To then present an additional view controller (while one is currently being presented), you'd have to present from that presented view controller or its nearest parent view controller if it has children (i.e. if you presented a navigation view controller).
If you subclass UIViewController, you can add this functionality into it to make life easier:
class CustomViewController: UIViewController {
var nearestAvailablePresenter: UIViewController {
if appDelegate.rootViewController.presentedViewController == nil {
return appDelegate.rootViewController
} else if let parent = parent {
return parent
} else {
return self
}
}
}
Then when you wish to present, you can simply do it through this computed property:
nearestAvailablePresenter.present(someViewController, animated: true, completion: nil)

Segue from AlertController without action

I have a loading indicator, implemented with UIAlertController. When i send auth request to server - i fire up loading indicator. When request is successful i have to go to another ViewController and hide loading indicator. Before i perform segue i need to wait until AlertController dismiss indication is completed. So i have such a logic:
private var loadingIndicator: UIAlertController?
func navigateToMonitoring() {
DispatchQueue.global(qos: .background).async {
if let indicator = self.loadingIndicator {
while !indicator.isBeingDismissed { continue }
DispatchQueue.main.async {
self.performSegue(withIdentifier: "Monitoring", sender: self)
}
}
}
}
But when this method executed i've got a message - Warning: Attempt to present on whose view is not in the window hierarchy! - and segue does not performed.
How can i fix this?
(Swift 3, Xcode 8)
You can maybe create the alert and store its reference, then show the alert and call the API, and in that API's completion block, you can dismiss it and perform the segue, hope this makes sense to you.

Dismissing a modal view but keeping the data

I'm trying to dismiss a modal view and return back to the view controller that was "sent" from, while keeping the data that was entered in the modal view. If I understand correctly I need to use delegates/protocols for this but I'm having a lot of trouble understanding how to actually implement it in this situation.
Basically a user can call a modal view to enter some information in text fields, and when they hit save this function is called:
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
// Call save function in view controller to save new product to core data
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
// Present reloaded view controller with new product added
let cc = UINavigationController()
let pController = ProductController()
productController = pController
cc.viewControllers = [pController]
present(cc, animated: true, completion: nil)
}
Which calls the self.productController?.save function to save the newly entered values into core data, and reloads the productController table view with the new product.
However the issue I'm running into, is that the productController table view is dynamically set depending on some other factors, so I just want to dismiss the modal view once the user has entered the data, and return back to the page the modal view was called from.
EDIT: attempt at understanding how to implement the delegate -
ProductController is the parent class that the user gets to the modal view from:
protocol ProductControllerDelegate: class {
func getData(sender: ProductController)
}
class ProductController: UITableViewController, NSFetchedResultsControllerDelegate, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
weak var delegate:ProductControllerDelegate?
}
func getData(sender: ProductController) {
}
And AddProductController is the modally presented controller where the user enters in the data then handleSave is called and I want to dismiss and return to the ProductController tableview it was called from:
class AddProductController: UIViewController, ProductControllerDelegate {
override func viewDidDisappear(_ animated: Bool) {
// error on this line
getData(sender: productController)
}
If the sole purpose of your protocol is to return the final state of the view controller its usually easier and clearer to use an unwind segue instead of a protocol.
Steps:
1) In the parent VC you make a #IBAction unwind(segue: UIStoryboardSegue) method
2) In the storyboard of the presented ViewController you control drag from either the control you want to trigger the exit or from the yellow view controller itself(if performing the segue in code) on to the orange exit icon.
your code should look like:
#IBAction func unwind(segue: UIStoryboardSegue) {
if let source = segue.source as? MyModalViewController {
mydata = source.data
source.dismiss(animated: true, completion: nil)
}
}
see apple documentation
Edit here is the hacky way to trigger and unwind from code without storyboard; I do not endorse doing this:
guard let navigationController = navigationController,
let presenter = navigationController.viewControllers[navigationController.viewControllers.count - 2] as? MyParentViewController else {
return
}
presenter.unwind(UIStoryboardSegue(identifier: String(describing: self), source: self, destination: presenter))
Basically you need to create a delegate into this modal view.
Let's say you have ParentViewController which creates this Modal View Controller. ParentViewController must implement the delegate method, let´s say retrieveData(someData).
On the Modal View Controller, you can use the method viewWillDisappear() to trigger the delegate method which the data you want to pass to the parent:
delegate.retrieveData(someData)
If you have issues understanding how to implement a delegate you can check this link

Welcome Screen on Launch

I want a way for the user to have a welcome screen / tutorial on the first launch of the app. If it isn't the first launch of the app, then it open as it usually would.
I already have the welcome screen tied to a button function if the app opens normally. I'm using BWWalkThroughViewController. Here's my code for the button function:
#IBAction func showWalkThroughButtonPressed() {
// Get view controllers and build the walkthrough
let stb = UIStoryboard(name: "MainStoryboard", bundle: nil)
let walkthrough = stb.instantiateViewControllerWithIdentifier("walk0") as! BWWalkthroughViewController
let page_one = stb.instantiateViewControllerWithIdentifier("walk1") as UIViewController
let page_two = stb.instantiateViewControllerWithIdentifier("walk2") as UIViewController
let page_three = stb.instantiateViewControllerWithIdentifier("walk3") as UIViewController
let page_four = stb.instantiateViewControllerWithIdentifier("walk4") as UIViewController
let page_five = stb.instantiateViewControllerWithIdentifier("walk5") as UIViewController
// Attach the pages to the master
walkthrough.delegate = self
walkthrough.addViewController(page_one)
walkthrough.addViewController(page_two)
walkthrough.addViewController(page_three)
walkthrough.addViewController(page_four)
walkthrough.addViewController(page_five)
self.presentViewController(walkthrough, animated: true, completion: nil)
}
func walkthroughCloseButtonPressed() {
self.dismissViewControllerAnimated(true, completion: nil)
}
That code is located in the MyTableViewController.swift file.
Here's what I can't figure out:
I want the view controllers to show on first launch. Once the user finishes the tutorial, they can press the Close button and it will close. I have the code to check if it's the app's first launch. It's located in the AppDelegate.swift file. Here's that code:
// First Launch Check
let notFirstLaunch = NSUserDefaults.standardUserDefaults().boolForKey("FirstLaunch")
if notFirstLaunch {
print("First launch, setting NSUserDefault")
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "FirstLaunch")
}
else {
print("Not first launch.")
}
return true
So how do I get the welcome screen to launch on first launch? Do I have to create a function in AppDelegate to handle that, and if so what do I have to do to make the tutorial the initial view controller for just the first launch?
I believe what you need to do is already covered here: Programmatically set the initial view controller using Storyboards. If that doesn't work for you add more notes on why the implementation failed. A google search on "programatically change uiviewcontroller on launch ios" will yield other similar links.

UIDocumentInteractionController Open Menu Cancelled Callback

I am currently developing an application specifically for iOS7 that utilizes UIDocumentInteractionController open in menu and need a method that notifies me when a user cancels and does not choose an available option.
UIDocumentInteractionControllerDelegate offers:
- (void)documentInteractionControllerDidDismissOptionsMenu:(UIDocumentInteractionController *) controller
but this does not specify whether the user tapped one of the available options or cancel.
Any ideas?
NOTE: This will not work for iOS 8 anymore, only iOS7 and earlier
To determine whether the user has canceled the menu or selected an option, you have to make use of the following delegate methods:
1-
- (void)documentInteractionController:(UIDocumentInteractionController *)controller
didEndSendingToApplication:(NSString *)application
{
//get called only when the user selected an option and then the delegate method bellow get called
// Set flag here _isOptionSelected = YES;
_isOptionSelected = YES;
}
2-
- (void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller
{
//called whether the user has selected option or not
// check your flag here
if(_isOptionSelected == NO) {
//the user has canceled the menu
}
_isOptionSelected = NO;
}
iOS 8
For iOS 8 and above, use this method instead of the one in step 2:
- (void)documentInteractionController:(UIDocumentInteractionController *)controller
didEndSendingToApplication:(NSString *)application
This will work on iOS7 && iOS8
BOOL didSelectOptionFromDocumentController = NO;//**set this to "NO" every time you present your documentInteractionController too
-(void)documentInteractionController:(UIDocumentInteractionController *)controller willBeginSendingToApplication:(NSString *)application {
didSelectOptionFromDocumentController = YES;
}
-(void)documentInteractionControllerDidDismissOpenInMenu:(UIDocumentInteractionController *)controller {
if (didSelectOptionFromDocumentController == NO) {//user cancelled.
}
}
This works for iOS8 & iOS9 for 3rd party apps AND System Apps!
It's not pretty but it works.
Can anyone tell me if this will pass App Review? Not sure as I'm referring to a class name which is not publicly accessible (_UIDocumentActivityViewController).
This is Swift 2.2!
NSObject Extension to get a string of the class name:
extension NSObject {
var theClassName: String {
return NSStringFromClass(self.dynamicType)
}
}
Your Viewcontroller where you're calling the UIDocumentInteractionController from:
var appOpened = false
var presentedVCMonitoringTimer: NSTimer!
var docController: UIDocumentInteractionController!
func openDocController() {
docController = UIDocumentInteractionController(URL: yourURL!)
docController.UTI = "your.UTI"
docController.delegate = self
docController.presentOptionsMenuFromRect(CGRectZero, inView: self.view, animated: true)
// Check the class of the presentedViewController every 2 seconds
presentedVCMonitoringTimer = NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: #selector(self.checkPresentedVC), userInfo: nil, repeats: true)
}
func checkPresentedVC() {
if let navVC = UIApplication.sharedApplication().keyWindow?.rootViewController as? UINavigationController {
print(navVC.presentedViewController?.theClassName)
if navVC.presentedViewController != nil && (navVC.presentedViewController?.theClassName)! != "_UIDocumentActivityViewController" && (navVC.presentedViewController?.theClassName)! != self.theClassName {
// A system App was chosen from the 'Open In' dialog
// The presented ViewController is not the DocumentInteractionController (anymore) and it's not this viewcontroller anymore (could be for example the MFMailComposeViewController if the user chose the mail app)
appOpened = true
presentedVCMonitoringTimer?.invalidate()
presentedVCMonitoringTimer = nil
}
}
}
func documentInteractionControllerDidDismissOptionsMenu(controller: UIDocumentInteractionController) {
print("dismissedOptionsMenu")
presentedVCMonitoringTimer?.invalidate()
presentedVCMonitoringTimer = nil
if appOpened {
// Do your thing. The cancel button was not pressed
appOpened = false
}
else {
// Do your thing. The cancel button was pressed
}
}
func documentInteractionController(controller: UIDocumentInteractionController, willBeginSendingToApplication application: String?) {
// A third party app was chosen from the 'Open In' menu.
appOpened = true
presentedVCMonitoringTimer?.invalidate()
presentedVCMonitoringTimer = nil
}
For Swift 4, use this:
func documentInteractionControllerDidDismissOpenInMenu(_ controller: UIDocumentInteractionController) {
// this function get called when users finish their work,
// either for sharing thing within the same app or exit to other app will do
}
I use it when after users have shared image to Facebook and Instagram.

Resources