I have a widget and want to open a particular ViewController when clicking on it. I've read all the documentation and questions on SO regarding the topic, and can't figure out why it isn't working. When clicking the widget, it always opens the default ViewController.
Here's the code for the WidgetView.
struct WidgetAdapter : View {
let entry: TimeLine.Entry
#Environment(\.widgetFamily) var family
#ViewBuilder
var body: some View {
switch family {
case .systemSmall:
SmallView(...).widgetURL(URL(string: "fcv://open_activity"))
case .systemMedium:
MediumView(...).widgetURL(URL(string: "fcv://open_activity"))
default:
LargeView(...).widgetURL(URL(string: "fcv://open_activity"))
}
}
}
Here the AppDelegate method for managing URLs.
func application(_ application: UIApplication, open
url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool{
if url.scheme == "fcv"{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "WidgetActivity") as! WidgetActivityController
self.window?.rootViewController = vc
self.window?.makeKeyAndVisible()
}
return true
}
I also tried implementing the respective method for the SceneDelegate, I added the url scheme to the URL Types in project info, I added the LSApplicationQueriesSchemes item to the info.plist, used Link instead of .widgetURL... And it didn't work even once. I also think that the method in the AppDelegate is not being called, however, I checked for the cases were that can happen and they don't come to case.
Any help would be appreciated.
I solved the same problem by finding current rootViewController and pushing the view I need:
let rootViewController = window!.rootViewController as! UINavigationController
let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)let profileViewController = mainStoryboard.instantiateViewController(withIdentifier: "MainVC") as! CombinedMainVC
rootViewController.pushViewController(profileViewController, animated: false)
It is always UINavigationController in my case, so I push a new VC, in your case you could present it.
I use the same method in AppDelegate, so there could be a problem with your if statement. Where do you set a scheme for a widget URL? Maybe you could just check URL string:
if url.absoluteString.prefix(3) == "fcv" { }
Related
I am setting up a notification content extension in IOS and would like to add a button which opens up a specific view in the main app.
I can do this through the action buttons (at the bottom) but would like to add a button to the exentsion view itself.
#IBAction func btn_openapp(_ sender: Any) {
let mystring = "mydrop://" + String(campaign_id)
self.extensionContext?.open(URL(string: mystring)!, completionHandler: nil)
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool
{
d_me["single"] = url.host
print(url.host,url.scheme)
if url.scheme == "mydrop://"
{
let rootViewController = self.window!.rootViewController as! UINavigationController
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let targerView = mainStoryboard.instantiateViewController(withIdentifier: "CampaignDetailViewController")
rootViewController.pushViewController(targerView, animated: false)
}
return true
}
You shouldn't try to add a stored property at the extension. If you need a stored property, in this case a button, you should create a UIView or UIViewController, according to your use case, and associate it to another UIView or UIViewController.
You should also try to share some code, because it is not clear about what you are asking for.
In my Xcode project when a user taps on a notification I want to first send them to a certain item in my tabBar then I want to instantiate a view controller and send an object over to that view controller. I have code the that sends them to the tabBar I want, but I do not know how to instantiate them to the view controller while keeping the tabBar and navigation bar connected to the view controller. All the answers on this require you to change the root view controller and that makes me lose connection to my tabBar and navigation bar when the view controller is called.
A Real Life Example of this: User receives Instagram notification saying "John started following you" -> user taps on notification -> Instagram opens and shows notifications tab -> quickly send user to "John" profile and when the user presses the back button, it sends them back to the notification tab
Should know: The reason why I'm going to a certain tab first is to get that tab's navigation controller because the view controller I'm going to does not have one.
Here's my working code on sending the user to "notifications" tab (I added comments to act like the Instagram example for better understanding):
if let tabbarController = self.window!.rootViewController as? UITabBarController {
tabbarController.selectedViewController = tabbarController.viewControllers?[3] //goes to notifications tab
if type == "follow" { //someone started following current user
//send to user's profile and send the user's id so the app can find all the information of the user
}
}
First of all, you'll to insatiate a TabBarController:
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
And then insatiate all of the viewControllers of TabBarController. If your viewControllers is embedded in to the UINavigationController? If so, you'll to insatiate a Navigation Controller instead:
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
Also you should instantiate your desired ViewController too:
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
Make all of the NavigationControllers as viewControllers of TabBarController:
tabBarController.viewControllers = [first, second, third]
And check: It's about your choice.
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
Make tabBarController as a rootViewController:
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
Finally: It's your completed code:
func openViewController() {
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
tabBarController.viewControllers = [first, second, third]
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
}
If you want to present or push ViewController when the notification is tapped? Try something like that:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
openViewController()
completionHandler()
default:
break;
}
}
}
I can think of two ways to do that:
1) If that view controller is a UINavigationController you can simply push the profile from wherever you are:
if let tabNavigationController = tabbarController.viewControllers?[3] as? UINavigationController {
tabbarController.selectedViewController = tabNavigationController
let profileViewController = ProfileViewController(...)
// ... set up the profile by setting the user id or whatever you need to do ...
tabNavigationController.push(profileViewController, animated: true) // animated or not, your choice ;)
}
2) Alternatively, what I like to do is control such things directly from my view controller subclass (in this case, PostListViewController). I have this helper method in a swift file that I include in all of my projects:
extension UIViewController {
var containedViewController: UIViewController {
if let navController = self as? UINavigationController, let first = navController.viewControllers.first {
return first
}
return self
}
}
Then I would do this to push the new view controller:
if let tabViewController = tabbarController.selectedViewController {
tabbarController.selectedViewController = tabViewController
if let postListViewController = tabViewController.containedViewController as? PostListViewController {
postListViewController.goToProfile(for: user) // you need to get the user reference from somewhere first
}
}
In my last live project, I'm using the same approach like yours. So even though I doubt this method is the correct or ideal for handling a push notification from the AppDelegate (I still got a lot of stuff to learn in iOS 🙂), I'm still sharing it because it worked for me and well I believe the code is still readable and quite clean.
The key is to know the levels or stacks of your screens. The what are childViewControllers, the topMost screen, the one the is in the bottom, etc...
Then if you're now ready to push to a certain screen, you would need of course the navigationController of the current screen you're in.
For instance, this code block is from my project's AppDelegate:
func handleDeeplinkedJobId(_ jobIdInt: Int) {
// Check if user is in Auth or in Jobs
if let currentRootViewController = UIApplication.shared.keyWindow!.rootViewController,
let presentedViewController = currentRootViewController.presentedViewController {
if presentedViewController is BaseTabBarController {
if let baseTabBarController = presentedViewController as? BaseTabBarController,
let tabIndex = TabIndex(rawValue: baseTabBarController.selectedIndex) {
switch tabIndex {
case .jobsTab:
....
....
if let jobsTabNavCon = baseTabBarController.viewControllers?.first,
let firstScreen = jobsTabNavCon.childViewControllers.first,
let topMostScreen = jobsTabNavCon.childViewControllers.last {
...
...
So as you can see, I know the hierarchy of the screens, and by using this knowledge as well as some patience in checking if I'm in the right screen by using breakpoints and printobject (po), I get the correct reference. Lastly, in the code above, I have the topMostScreen reference, and I can use that screen's navigationController to push to a new screen if I want to.
Hope this helps!
I'm creating an iPad app with a SplitViewController, thats supposed to work in 2 ways.
You just open the app, and are presented with a master tableview
showing some assignments. These assignments can be 1 of three
different types. You select an assignment and are presented with a
detail ViewController using a show detail segue
You click a link
from another app, opening the app with an id, and should now only be
presented with the assignment corresponding to the id.
I have the following functions in AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let splitViewController = self.window!.rootViewController as! UISplitViewController
UIApplication.shared.statusBarStyle = .lightContent
splitViewController.delegate = self
splitViewController.preferredDisplayMode = .primaryOverlay
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
if let itemid = getQueryStringParameter(url: url.absoluteString, param: "itemid"){
print(itemid)
NetworkService().getSpecificExercise(id: itemid) { response, error in
let exercise = response! as VoiceExercise
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
switch exercise.type {
case "STRENGTH":
print("Strength")
let initialViewController : StrengthViewController = storyBoard.instantiateViewController(withIdentifier: "StrengthViewController") as! StrengthViewController
initialViewController.exercise = exercise
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
case "RANGE":
print("Range")
let initialViewController : RangeViewController = storyBoard.instantiateViewController(withIdentifier: "RangeViewController") as! RangeViewController
initialViewController.exercise = exercise
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
case "COMBINED":
print("combined")
let initialViewController : CombinedViewController = storyBoard.instantiateViewController(withIdentifier: "CombinedViewController") as! CombinedViewController
initialViewController.exercise = exercise
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
default:
print(exercise.type)
}
}
}
return true
}
My problem is, that if the app has not been started yet (ie not running in background), and I open the app from another app (or a test link on the same ipad through safari), nothing happens from the open url function - it just shows the splitviewcontroller, with the tableview, as if I'd opened the app on its' own. If the app has already been opened, it behaves as expected - showing the assignment I expect.
Right now that is what I want. When you end an assignment, it opens the other app again, if the app has been opened by a link.
If I wanted to (when I click a link to open) show the assignment, but contained in a navigationcontroller, that was contained in the splitviewcontroller, where the splitviewcontroller had its' desiplaymode set to .primaryHidden, how would I go about this. All tutorials I've found only deals with Navigationcontroller, not contained in a splitviewcontroller. (this is still only for open url)
I have an app which starts with different VCs depending whether the user is already logged in or not.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
window = UIWindow.init(frame: UIScreen.mainScreen().bounds)
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let initialViewController: UIViewController
if DataManager.getInstance().getUserInfo() == nil {
initialViewController = storyboard.instantiateViewControllerWithIdentifier("authenticationViewController")
} else {
initialViewController = storyboard.instantiateViewControllerWithIdentifier("locationsNavigationViewController")
}
window!.rootViewController = initialViewController;
window!.makeKeyAndVisible();
return true
}
If the user is not logged in, the app starts with AuthenticationViewController, otherwise it starts with LocationsNavigationViewController, which is a NavigationViewController
In the latter VC, the is a button for logout. The problem is when the user taps on that button, I don't know if I have to dismiss the LocationsNavigationViewController (because AuthenticationViewController is in background) or if I have to dismiss LocationsNavigationViewController and perform a segue for opening the AuthenticationViewController.
So far, I have just covered the first use case. So in LocationsNavigationViewController I call this function
func showAuthentication() {
dismissViewControllerAnimated(true, completion: nil)
}
But when the app starts with LocationsNavigationViewController dismiss the VC is not enough of course, because the the AuthenticationViewController has never been instantiated.
How can I solve this please?
self.navigationController?.viewControllers
This is an array which will contain all your previous view controller. You can enumerate it and check whether your view controller is exist or not.
If you are using UINavigationController then you can check any UIViewController present or not!
let rootViewController = application.windows[0].rootViewController as! UINavigationController
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if !rootViewController.viewControllers.contains(UIViewController_Class()){
let notificationVC = mainStoryboard.instantiateViewControllerWithIdentifier(constInstance.notificationsIdentifier) as! UIViewController_Class_Name
rootViewController.pushViewController(notificationVC, animated: false)
}
Hope this helps!
Thanks to Sohil's answer, I change the showAuthentication function in this way
func showAuthentication() {
//Since the app can start with different VC, I have to check which is the window root VC
if UIApplication.sharedApplication().windows[0].rootViewController is AuthenticationViewController {
dismissViewControllerAnimated(true, completion: nil)
} else {
performSegueWithIdentifier("authenticationSegue", sender: self)
}
}
And added a segue from the NavigationViewController to the AuthenticationViewController, called authenticationSegue
Pretty simple solution.
You can create one function in appdelegate for logout
and in logout function chanege rootviewcontroller of window like
func logoutUser()
{
var login: UIViewController?
login = LoginViewController(nibName : "LoginViewController", bundle : nil)
let nav = UINavigationController(rootViewController: login!)
self.window?.rootViewController = nav
}
I there a simple way to route a user to a view controller only one time? This ViewController could then be an instructional screen that the user only sees once at startup.
I have this now:
let skipScreen = NSUserDefaults.standardUserDefaults()
#IBAction func skipButton(sender: AnyObject) {
let skipNow = true
skipScreen.setObject(skipNow, forKey: "skip")
skipScreen.synchronize()
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject :AnyObject]?) -> Bool {
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var initialViewController = storyboard.instantiateViewControllerWithIdentifier("ViewController") as UIViewController
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
}
override func viewDidLoad() {
super.viewDidLoad()
var skipNow: Bool? = skipScreen.objectForKey("skip") as Bool?
if skipScreen.boolForKey("skip") == true{
var nav = self.view?.rootViewController as UINavigationController
var storyBoard = UIStoryboard(name: "Main", bundle: nil)
nav.pushViewController(storyBoard.instantiateViewControllerWithIdentifier("BookingViewController") as BookingViewController, animated: false)
}
}
But it gives errors with the self.windowparts and says that the viewController does not have a member named 'window'
How could I make this work?
Thanks!
My suggestion for creating tutorial/insturctional screens would be to create images(full screen) with opacity background color and text full visible. Handle showing with NSUserDefaults(after you show one screen, save that to user defaults so you know next time that you shouldn't show that particular image).
Don't forget to create pictures for all screens (for iphone: 4, 5, 6, 6+)
You need to check in your appdelegate's didFinishLaunchingWithOptions method for, lets say a boolean value in your userdefaults, which indicates wether you should start with a "instructional" viewcontroller or the normal viewcontroller.
So you can set the instrucitonal viewcontroller as the default viewcontroller in your storyboard, and you can push the "non-instructional" viewcontroller if it has been shown.
if userDefaults.boolForKey("HasBeenShownInstructions") {
var nav = self.window?.rootViewController as UINavigationController
var storyBoard = UIStoryboard(name: "Main", bundle: nil)
nav.pushViewController(storyBoard.instantiateViewControllerWithIdentifier("BookingViewController") as BookingViewController, animated: false)
}
Dont forget to save the boolean value to userdefaults when the user has dismissed/ended the instructions.
This is just one of many ways to solve the issue. You could also do it the other way around. Depends on what you need.