Swift 3. Can present a UIViewController with UIWebViewDelegate but not push it - uiviewcontroller

I have a UIViewController with UIWebViewDelegate. I found that I can present it just fine but not push it. If I try to push it the target view controller loads and executes any variables set but does not fire viewDidLoad() and instead just returns to my calling menu view controller without error.
I set a breakpoint on the first var declaration after the class and stepped through the code. When it got to webView init it flashed up the home (not target) view controller in the simulator and then continued though the var declarations before returning back to my home view controller.
Here is my menu code
works:
let myWebViewController = MyWebViewController()
myWebViewController.urlString = "myUrl"
present(myWebViewController, animated: true, completion: nil)
does not work
let myWebViewController = MyWebViewController()
myWebViewController.urlString = "myUrl"
let navController = (UIApplication.shared.delegate as! AppDelegate).navController
navController!.pushViewController(myWebViewController, animated: true)
Here is my redacted target view controller code. The URL to load is set in the menu.
import UIKit
class MyWebViewController: UIViewController, UIWebViewDelegate {
var urlString: String = "invalid url"
var url: URL {
return URL(string: urlString)!
}
var urlRequest: URLRequest {
var urlRequest = URLRequest(url: url)
return urlRequest
}
let webView: UIWebView = {
let view = UIWebView()
view.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
// declare various buttons and stack views
override func viewDidLoad() {
super.viewDidLoad()
webView.delegate = self
contentStackView.addArrangedSubview(webView)
mainStackView.addArrangedSubview(contentStackView)
view.addSubview(mainStackView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
webView.loadRequest(urlRequest as URLRequest)
}

The problem has nothing to do with the web view, the web view controller, or anything else except where you're pushing the view controller. You are saying:
let navController = (UIApplication.shared.delegate as! AppDelegate).navController
navController!.pushViewController(myWebViewController, animated: true)
That is highly unusual. The usual thing is that we are inside a navigation interface already, and we push onto its stack:
let navController = self.navigationController
navController!.pushViewController(myWebViewController, animated: true)
But you are not saying that. Why not? I'm guessing it's because self does not have a navigationController. So you are successfully pushing onto the navigation controller, all right, but you are not seeing anything happening because the navigation controller is behind the scenes — the self view controller's view is blocking it or has replaced it.
And that explains why you never detect the url loading. Your call to webView.loadRequest is in viewWillAppear. But this view will not appear; it is behind the scenes. viewWillAppear is never called. Similarly viewDidLoad is not called, because this view is not going to load; the navigation controller is not occupying the interface — self is.

I could possibly think of one scenario why the webViewController is not being pushed.
The UIViewController from which you are trying to make the push might not be part of the navigation controller hierarchy.
Why don't you try using the code instead of getting the navController from AppDelegate
self.navigationController?.pushViewController(myWebViewController, animated: true)
If this doesn't work out then your are certainly doing something wrong.

Related

Updating an #EnvironmentObject var to pass data to PageViewController in SwiftUI results in loss of swiping between ViewControllers

In my SwiftUI app, I currently have a PageViewController implemented using UIKit. It follows the traditional SwiftUI - UIKit implementation outlined in Apple's SwiftUI UIKit integration tutorials.
I have the data that populates a UIViewController, within the controllers array that is passed to the PageViewController, provided by an #Environment variable. In a different screen in the app, you can issue an action that causes the Environment object to update, triggering a re-render of a ViewController that lives within PageViewController.
This re-render, however, causes an issue as the ViewController is re-made with a new identifier and so the index of the viewController cannot be found in the parent.controller array within the Class Coordinator pageViewController function. This causes the index to default to nil and disables any swiping on the updated viewController. I am still able to navigate between viewControllers using the page control dots but I would like to identify how I can update the parent.controller array to include the new ViewController and to discard the old one.
After hours of searching, debugging and tinkering around I've not been able to find a way how I can reset the parents controller array with the new ViewController replacing the old view which has been discarded. Below is the code for the PageViewController.
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.view.backgroundColor = UIColor.clear
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: false)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(parent.controllers)
print(viewController)
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
After updating the environment state that populates the data for a given ViewController, causing a re-render, the coordinator class can print an array of controllers that include the old ViewController and also the new ViewController in the annotated code below but I have not yet been able to find a reliable way to ensure that the new ViewController effectively replaces the old one.
struct PageViewController: UIViewControllerRepresentable {
...
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
...
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(parent.controllers)
// prints out array of ViewControllers including old ViewController that has now been
// discarded
// [<_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c93de0>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c94be0>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c96830>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c976b0>]
print(viewController)
// prints out new ViewController that does not exist within the parent.controllers array
// and hence nil is returned from the guard
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd593721710>
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
...
}
Any help or guidance with this issue would be greatly appreciated!
Having the same issue, for few last hours, I found a work around.
It seems that it is a bug. If you look at the UIViewControllerRepresentable cycle, it should go from the Init > Coordinator > makeUIViewController > updateUIViewController.
But if the pages in the PageViewController get updated (our case). The pages get re-created by SwiftUI, great, but the PageViewController get an internal wrong creation cycle going from Init > updateUIViewController. Without makeUIViewController neither Coordinator. A new UIPageViewController is somehow created (How?) Leading to the discrepancy you noticed.
To solve the issue and force a proper recreation of the PageViewController, add .id(UUID) in your page view code, such as :
struct SGPageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#Binding var currentPage:Int
init(_ views: [Page], currentPage:Binding<Int>) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
self._currentPage = currentPage
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage).id(UUID())
}
}
It will work, but you will notice that the scroll position of the pages are reset. Another bug ;)
think the problem stays in the Coordinator:
when you update controllers in pageviewcontroller, the parent property of Coordinator remains the same (and because it is a struct, all his properties). So you can simply add this line of code in your updateUIViewController method:
context.coordinator.parent = self
also, remember that animation of pageViewController.setViewControllers will occur, so you have to set animated to false, or handle it properly.
There are many other ways to solve this (I wrote the most intuitive solution),
the important thing is to know where the error comes from.

Is there an easy solution for parsing html in swift to get individual elements into their own variable?

I have code which I found online that pulls the HTML off a website and then prints it out. I need to save these into a variable to I can display/ use these in my app.
I am fairly new to this kind of thing and really just need pointers, I don't mind researching! I just need to know what steps I need to be looking into!
import UIKit
// run asynchronously in a playground
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
// create a url
let url = URL(string: "https://www.stackoverflow.com")
// create a data task
let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
if error != nil {
print("there's a problem")
}
print(String(data: data!, encoding: String.Encoding.utf8) ?? "")
}
//running the task w/ resume
task.resume()
This (in Xcode playground) takes the HTML and prints it out using:
print(String(data: data!, encoding: String.Encoding.utf8) ?? "")
Can anyone please help me out getting maybe the <title>...</title> element into its own variable?
Parsing HTML without a third party is not achievable without a WebView, BUT YOU CAN easily use a webView and run a getElementsByTagName with JS on it to get anything from the HTML code like this:
1- Define the js code:
let js = "document.getElementsByTagName("title")[0].innerHTML"
2- Import WebKit and load the html into a webView
class MyViewController : UIViewController {
let html = """
<#the HTML code, can be loaded from anywhere#>
"""
override func loadView() {
let webView = WKWebView()
webView.navigationDelegate = self // Here is the Delegate
webView.loadHTMLString(html, baseURL: nil)
self.view = webView
}
}
3- Take the delegation and implement this method:
extension MyViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.evaluateJavaScript(js) {(result, error) in
guard error == nil else {
print(error!)
return
}
print(String(describing: result))
}
}
}
Note 1: remember getElementsByTagName returns an array and you must pass the index you want the get like [0]
Note 2: since it use JavaScriptCore, it can't be done without webView, and it must be run on mainThread. Only safari can do this off main thread, because it has V8 engine.
Note 3: You must wait for delegate to be completed even if you pass the HTML statically
Note 4: you can use a third party framework like SwiftSoap to do this.

How to pre-fill a text box on a website in a WKWebView using swift

I'm relatively new to coding and extremely new to using swift. I'm trying to create something where my users can set a user name and password in settings and have that information pre-fill the login page of a WKWebView I have set up. Currently I'm just trying to test getting the swift code to send the information over to the text field.
I couldn't find much information on this other then one or two stack posts from a few years ago but they didn't seem to work for me.
According to what I've read, I need to get some Javascript to execute to fill in the areas but nothing I'm doing seems to be working. I'll post the small bit of testing code I'm using in swift. If anyone can tell me what I'm doing wrong or if I'm just missing something obvious it would be greatly appreciated.
import UIKit
import WebKit
class ViewController: UIViewController {
#IBOutlet weak var WebView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = URL (string: "https://www.google.com")
let request = URLRequest(url: url!)
WebView.load(request)
}
func webViewDidFinishLoad(webView: WKWebView!) {
webView.evaluateJavaScript("document.getElementById('q').value = 'Hello' ;")
}
}
Recommend you can use like this ,
// let source = "document.body.style.background = \"#777\";"
let source = "document.getElementById('q').value = 'Hello' ;"
let userScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
let userContentController = WKUserContentController()
userContentController.addUserScript(userScript)
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
webView = WKWebView(frame: self.view.bounds, configuration: configuration)
view.addSubview(webView)
let url = URL (string: "https://www.google.com")
let request = URLRequest(url: url!)
webView.load(request)
as Apple said , A WKUserContentController object provides a way for JavaScript to post messages and inject user scripts to a web view.

How can I refresh tableView and JSON datas with button or pull to refresh ?

I'm new at swift and i can not find how to how to refresh TableView and JSON datas same time. First refresh Json after refresh TableView. How can I do this ?
You need to add pull to refresh class to your UITableView
private let refreshControl = UIRefreshControl()
var tableView: UITableView!
Then in ViewDidLoad add it as a subview to your tableView and you can even change its color and text.
override func viewDidLoad() {
if #available(iOS 10.0, *) {
tableView.refreshControl = refreshControl
} else {
tableView.addSubview(refreshControl)
}
refreshControl.tintColor = UIColor.blue
refreshControl.attributedTitle = NSAttributedString(string: "Fetching Data ...", attributes: [NSAttributedStringKey.foregroundColor : UIColor(red:0/255, green:168/255, blue:225/255, alpha:1.0)])
refreshControl.addTarget(self, action: #selector(refreshData(_:)), for: .valueChanged)
}
Add a target to it with a selector function when value changes as shown above.
#objc private func refreshData(_ sender: Any) {
//load the data from the server and
//parse and put it in an array then reload your table view
}

Add "quick actions" to my iOS 9 app

I would like to add the quick actions of iOS 9 to my app.
I put this code in my app delegate:
import UIKit
enum ShortcutType: String {
case NewScan = "QuickAction.NewScan"
case Settings = "QuickAction.Settings"
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let applicationShortcutUserInfoIconKey = "applicationShortcutUserInfoIconKey"
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
UIViewController.prepareInterstitialAds()
if(UIApplication.instancesRespondToSelector(Selector("registerUserNotificationSettings:"))) {
UIApplication.sharedApplication().registerUserNotificationSettings(UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil))
}
// QUICK ACTIONS
var launchedFromShortCut = false
if #available(iOS 9.0, *) {
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
launchedFromShortCut = true
handleShortCutItem(shortcutItem)
}
} else {
return true
}
return !launchedFromShortCut
}
/**************** QUICK ACTIONS ****************/
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
let handledShortCutItem = handleShortCutItem(shortcutItem)
completionHandler(handledShortCutItem)
}
#available(iOS 9.0, *)
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) {
let rootNavigationViewController = window!.rootViewController as? UINavigationController
let rootViewController = rootNavigationViewController?.viewControllers.first as UIViewController?
rootNavigationViewController?.popToRootViewControllerAnimated(false)
switch shortcutType {
case .NewScan:
rootViewController?.performSegueWithIdentifier("goToCamera", sender: nil)
handled = true
case.Settings:
rootViewController?.performSegueWithIdentifier("goToSettings", sender: nil)
handled = true
}
}
return handled
}
}
Now I can make a force touch on my app icon > quick actions will be shown > I select the Quick Action "New Scan" > the app will open and show me the last view, which I have leave.
But the segue will not be execute.
Here is a part of my storyboard:
Explanation:
A: Navigation Controller and initiale Controller
B: ViewController, after a check this will make a segue to navigation Controller C
C: Navigation Controller
D: Table View Controller
E: ViewController
If I select New Scan with quick actions - I would like to show ViewController E.
It appears that you're doing things correctly based on the example code in the documentation. However, you've got a lot of optional chaining in your handleShortCutItem: implementation. Have you used the debugger to verify none of those expression have nil values? Also, from what I can see (although the image is blurry), the root view controller of the first nav controller in that storyboard does not have a segue to E. So I'm not sure how you intend to get there.
I would suggest that you set a breakpoint in your handleShortCutItem: implementation to first validate that the values you're working with are not nil and the code is actually executing. Once you've done this, you can use your storyboard to instantiate the view controls you want and just create an array of them as you want your view controller hierarchy to be in your navigation controller and set the navigation controller's viewControllers property to this array. Again, it's hard to tell exactly what you want from your image, but perhaps something like this:
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
guard let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) else {
return false
}
guard let rootNavigationController = window?.rootViewController as? UINavigationController else {
return false
}
guard let rootViewController = rootNavigationController?.viewControllers.first else {
return false
}
guard let storyboard = rootNavigationController.storyboard else {
return false
}
var viewControllers = [rootViewController]
switch shortcutType {
case .NewScan:
// Instantiate the necessary view controllers for this case
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some view controller#>")]
...
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some other view controller#>")]
case.Settings:
// Instantiate the necessary view controllers for this case
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some view controller#>")]
...
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some other view controller#>")]
}
// Set the new view controllers array
rootNavigationController.setViewControllers(viewControllers, animated: false)
return true
}
Note: Since you tagged this question with Swift2, I've taken the liberty of adjusting the code to use guard statements.