WKWebView crashing with certain queries (iOS Swift) - ios

I'm developing a tinderlike swipe app for babynames. I've created a method that, when you click a button besides one of your favorite names, it pops up a window (A UIViewController with a WKWebView) that shows a google search with the meaning of this particular name.
The problem is that with some names, the app crashes because the URL returns nil. I don't understand what's happening, because other names are working just fine. I've found out it happens with Scandinavian names like "OddVeig" and "OddLaug" .
Below is my UIViewController class that pops up with the search result (I've put all the initializing code in the layoutSubViews(), because I had some trouble in iOS 12, where the WKWebView wouldn't resize properly if I put it in the viewDidLoad() ) :
import UIKit
import WebKit
class WebViewController: UIViewController, WKNavigationDelegate {
#IBOutlet weak var webContainer: UIView!
var webView : WKWebView!
var query : String?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
webView = WKWebView()
webView.navigationDelegate = self
webContainer.addSubview(webView)
self.view.alpha = 0.9
webView.frame = self.view.frame
webView.layer.position.y += 20
let slideDownImage = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 30))
slideDownImage.setImage(UIImage(named: "slideIconWhite"), for: .normal)
slideDownImage.imageView?.contentMode = .scaleAspectFit
if let n = query {
let url = URL(string: "https://www.google.com/search?q=\(n)")! // Causing error: Found nil
webView.load(URLRequest(url: url))
webView.allowsBackForwardNavigationGestures = true
}
webContainer.addSubview(slideDownImage)
slideDownImage.frame = CGRect(x: 0, y: 0, width: self.webView.frame.size.width, height: 20)
slideDownImage.backgroundColor = .gray
slideDownImage.alpha = 0.7
slideDownImage.addTarget(self, action: #selector(slideDown), for: .touchUpInside)
}
#objc func slideDown() {
self.dismiss(animated: true, completion: nil)
}
}
I use a segue in the FavoritesViewController class like this :
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "favoritesToWeb" {
let destination = segue.destination as! WebViewController
destination.query = "\(searchName)+\(NSLocalizedString("meaning", comment: "meaning"))"
}
}
Hopefully someone can tell me what I'm doing wrong.
Thanks in advance.
Patrick

query variable need to be percentage encoded, I guess this must be crashing while you have whitespace characters in query string. use addingPercentEncoding for adding percentage encoding to query string.
Reference to API:
https://developer.apple.com/documentation/foundation/nsstring/1411946-addingpercentencoding
Related Question:
Swift. URL returning nil

Related

Swift White Screen upon returning from SafariWebController

When I open a safari view controller and then return to my app (when "Done" is pressed), my app renders a blank/white screen instead of the content for that view.
The code below is the code I have tried using in an empty view - the issue happens no matter where I try in my app.
import UIKit
import SafariServices
class SavedViewController: UIViewController, SFSafariViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let link = URL(string: "https://google.com"){
let myrequest = SFSafariViewController(url: link)
myrequest.delegate = self
present(myrequest, animated:true)
}
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
dismiss(animated:true, completion: nil)
}
}
The website loads fine, its when I return to my app that the blank screen appears. Am I doing something wrong?
It works fine.
You just created new UIViewController this way, UIViewController by default have black background. So when you press Done you just come back from SafariViewController to you SavedViewController (UIViewController).
Probably You are looking for UIWebView solution https://developer.apple.com/documentation/uikit/uiwebview.
If u want only display SafariViewController do it as function from your ViewController, You don't need to create new File with UIViewController class to do it.
import UIKit
import SafariServices
class SavedViewController: UIViewController, SFSafariViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .gray
setupViews()
}
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
dismiss(animated:true, completion: nil)
}
private func setupViews(){
view.addSubview(openSafariButton)
openSafariButton.addTarget(self, action: #selector(handleOpenSafariButtonTap), for: .touchUpInside)
openSafariButton.frame = CGRect(x: 0, y: 100, width: view.frame.width, height: 60)
}
#objc func handleOpenSafariButtonTap(){
if let link = URL(string: "https://google.com"){
let myrequest = SFSafariViewController(url: link)
myrequest.delegate = self
present(myrequest, animated:true)
}
}
let openSafariButton: UIButton = {
let button = UIButton()
button.setTitle("openSafari", for: .normal)
button.setTitleColor(.black, for: .normal)
button.backgroundColor = .red
return button
}()
}
That is what i mean.
You can add function:
private func openSafariLink(link: String){
if let link = URL(string: link){
let myrequest = SFSafariViewController(url: link)
present(myrequest, animated:true)
}
}
And call it from any place like that:
openSafariLink(link: "https://google.com")
This way fits more for Your solution.

Issue performing a segue when function called from another class

I have an Intro video section for an app. The storyboard has a segue "toMainMenu" that links to another storyboard view controller.
The IntroVideoVC has two classes inside it:-
class IntroVideoVC: UIViewController {
var videoView: IntroVideoView!
override func viewDidLoad() {
videoView = IntroVideoView(controller: self)
self.view.addSubview(videoView)
let tap = UITapGestureRecognizer(target: self, action: #selector(tappedVideo))
view.addGestureRecognizer(tap)
}
#objc func tappedVideo() {
videoFinished();
}
func videoFinished() {
self.performSegue(withIdentifier: "toMainMenu", sender: self)
}
}
class IntroVideoView: UIView {
init(controller: UIViewController) {
super.init(frame: CGRect(x: 0, y: 0, w: 0, h: 0))
self.controller = controller
...
}
func handleVideo(videoPlayer: AVPlayer) {
...
IntroVideoVC().videoFinished()
}
}
If i tap the video it correctly performs the segue, however if i let the video end which in-turn calls IntroVideoViews -> handleVideo() method i get a crash stating it has no segue with identifier "toMainMenu".
I think i understand why but unsure how to get around the issue being new to swift. I believe it is because handleVideo() is using IntroVideoVC as a singleton and so is losing reference to IntroVideoVC's controller! I maybe (read: likely) be wrong but that is what i feel may be causing this issue.
Just looking for a nudge in the right direction
thanks in advance
You could use protocol to solve that, it would be something like this:
protocol IntroVideoDelegate {
func videoFinished()
}
class IntroVideoVC: UIViewController, IntroVideoDelegate {
var videoView: IntroVideoView!
override func viewDidLoad() {
videoView = IntroVideoView(controller: self)
videoView.videoDelegate = self
self.view.addSubview(videoView)
let tap = UITapGestureRecognizer(target: self, action: #selector(tappedVideo))
view.addGestureRecognizer(tap)
}
#objc func tappedVideo() {
videoFinished();
}
func videoFinished() {
self.performSegue(withIdentifier: "toMainMenu", sender: self)
}
}
class IntroVideoView: UIView {
videoDelegate: IntroVideoDelegate?
init(controller: UIViewController) {
super.init(frame: CGRect(x: 0, y: 0, w: 0, h: 0))
self.controller = controller
...
}
func handleVideo(videoPlayer: AVPlayer) {
...
videoDelegate.videoFinished()
}
}
Remember that the videoView is still a subview to IntroVideoVC, so if the app goes back to this VC the user will see it, so if you don't want that to happen, be sure to remove it with:
videoView.removeFromSuperview()
In the videoFinished() func
If you want to do running a VC func from the view you should get the parent of IntroVideoView, here is how: Given a view, how do I get its viewController? But i'm pretty sure it breaks MVC

How to open any URL clicked within my iOS app in the in-app browser?

How can I set a rule such that when my user clicks any web link within my app, it opens in the in-app browser instead of Safari?
Context: I'm building an app that has several places where links are either embedded by me or are loaded through various user interactions, e.g. a link leading to a page that houses another link. I want to ensure that the user seldom leaves my app and hence want to open all external web links in an in-app browser that has already been developed
Target Build: iOS 11.
Environment/ Language: Swift 4
Simple solution using SFSafariViewController which available in a separate package
import SafariServices
Swift 4, iOS 9.0+
let url = URL(string: "your URL here")
let vc = SFSafariViewController(url: url)
present(vc, animated: true)
If you need more control - use Configuration parameter (Swift 4, iOS 11.0+):
Example:
let config = SFSafariViewController.Configuration()
config.entersReaderIfAvailable = true
let url = URL(string: "your URL here")
let vc = SFSafariViewController(url: url, configuration: config)
present(vc, animated: true)
...it opens in the in-app browser instead of Safari?
In that case, you would need your own WebView. Here's a quick snippet of a simple webView inside a controller:
import UIKit
import WebKit
/// The controller for handling webviews.
class WebViewController: UIViewController {
// MARK: - Properties
internal lazy var button_Close: UIButton = {
let button = UIButton(type: .custom)
button.setImage(.close, for: .normal)
button.imageEdgeInsets = UIEdgeInsets.init(top: 0, left: -30, bottom: 0, right: 0)
button.addTarget(self, action: #selector(back(_:)), for: .touchUpInside)
return button
}()
public var urlString: String! {
didSet {
if let url = URL(string: urlString) {
let urlRequest = URLRequest(url:url)
self.webView.load(urlRequest)
}
}
}
private lazy var webView: WKWebView = {
let webView = WKWebView()
webView.navigationDelegate = self
return webView
}()
// MARK: - Functions
// MARK: Overrides
override func viewDidLoad() {
super.viewDidLoad()
let barButton = UIBarButtonItem(customView: self.button_Close)
self.button_Close.frame = CGRect(x: 0, y: 0, width: 55.0, height: 44.0)
let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.fixedSpace, target: nil, action: nil)
if #available(iOS 11.0, *) {
negativeSpacer.width = -30
}
self.navigationItem.leftBarButtonItems = [negativeSpacer, barButton]
self.view.addSubview(self.webView)
self.webView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
}
#objc func back(_ sender: Any) {
self.dismiss()
}
}
extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
// Show here a HUD or any loader
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Dismiss your HUD
}
}
and presenting such webViewController, like so (passing a URLString):
let webViewVC = WebViewController()
webViewVC.urlString = "https://www.glennvon.com/"
let navCon = UINavigationController(rootViewController: webViewVC)
self.navigationController?.present(navCon, animated: true, completion: nil)
If you're using storyboard, then simply drag a webview in your controller and setup the delegate. This should help you out :)

iOS Swift - Multiple activity indicator (spinners) appear in webview

I'm working on a webView based screen and I want to have an activity indicator to spin while the web content is loading.
Since I have to use the same activity indicator in another screen of the app, I've put the code in static functions in a specific file.
The activity indicator seems to work fine for some web (simple) web pages but I have an issue when I load more complex pages. The activity indicators gets duplicate several times. (See screenshot below)
On the screenshot, the first activity indicator has the correct layout but the one below is darker which implies that several other activity indicators have been overlaid on top of each other. And then they never disappears.
When it comes to code:
I have a webView and two delegate methods controlling the activity indicators.
func webViewDidStartLoad(_ webView: UIWebView) {
DispatchQueue.main.async {
EBUtil.startActivityIndicator(self)
}
}
func webViewDidFinishLoad(_ webView: UIWebView) {
DispatchQueue.main.async {
EBUtil.stopActivityIndicator(self)
}
}
I guess it comes from the fact that webViewDidStartLoad gets called several times. Any idea how I can prevent this behaviour to happen?
Thanks in advance.
Edouard
EDIT:
Here is the full code for my VC.
class NewsDetailsViewController: UIViewController, UIWebViewDelegate {
//MARK: - #IBOUTLETS
#IBOutlet weak var webView: UIWebView!
//MARK: - VARIABLES
var url: String!
//MARK: - APP LIFE CYCLE
override func viewDidLoad() {
super.viewDidLoad()
if url != nil {
let urlToLoad = NSURL(string: url)
webView.loadRequest(NSURLRequest(url: urlToLoad as! URL) as URLRequest)
}
}
func webViewDidStartLoad(_ webView: UIWebView) {
DispatchQueue.main.async {
EBUtil.startActivityIndicator(self)
}
}
func webViewDidFinishLoad(_ webView: UIWebView) {
DispatchQueue.main.async {
EBUtil.stopActivityIndicator(self)
}
}
}
And code for activity indicator start and stop:
static func startActivityIndicator(_ sender: UIViewController) {
print("START ANIMATING")
if let view = sender.view {
activityIndicatorBackground = UIView(frame: CGRect(x: view.center.x - 25, y: view.center.y - 25, width: 50, height: 50))
activityIndicatorBackground.backgroundColor = UIColor.black
activityIndicatorBackground.layer.zPosition = CGFloat(MAXFLOAT-1)
activityIndicatorBackground.alpha = 0.6
activityIndicatorBackground.layer.cornerRadius = 5
view.addSubview(activityIndicatorBackground)
activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
activityIndicator.center = view.center
activityIndicator.hidesWhenStopped = true
activityIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.white
activityIndicator.layer.zPosition = CGFloat(MAXFLOAT)
view.addSubview(activityIndicator)
activityIndicator.startAnimating()
UIApplication.shared.beginIgnoringInteractionEvents()
}
}
static func stopActivityIndicator(_ sender: UIViewController) {
print("STOP ANIMATING")
activityIndicatorBackground.removeFromSuperview()
activityIndicator.stopAnimating()
UIApplication.shared.endIgnoringInteractionEvents()
}
try this -
add below line in your viewController class -
var activityIndicator: UIActivityIndicatorView?
And update your startActivityIndicator stopActivityIndicator method like below-
func startActivityIndicator(_ sender: UIViewController) {
print("START ANIMATING")
if let view = sender.view {
if activityIndicator != nil {
activityIndicator?.removeFromSuperview()
}
activityIndicator = UIActivityIndicatorView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
activityIndicator?.center = view.center
activityIndicator?.layer.cornerRadius = 10
activityIndicator?.backgroundColor = UIColor.gray
activityIndicator?.hidesWhenStopped = true
activityIndicator?.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.white
activityIndicator?.layer.zPosition = CGFloat(MAXFLOAT)
view.addSubview(activityIndicator!)
activityIndicator?.startAnimating()
UIApplication.shared.beginIgnoringInteractionEvents()
}
}
func stopActivityIndicator(_ sender: UIViewController) {
print("STOP ANIMATING")
activityIndicator?.stopAnimating()
activityIndicator?.removeFromSuperview()
UIApplication.shared.endIgnoringInteractionEvents()
}
Hope it will work for you :)

Trying to create an iOS WKWebView with a size smaller than the screen programmatically

Im starting with a simple app that just shows a web view using WKWebView.
Everything builds, and works okay, except that it is always fullscreen. Ideally it would not extend under the iOS status bar at the top. It needs to be lowered by 20px to be below it. I have searched extensively for a solution but nothing changes the size. Nothing is setup in InterfaceBuilder, I'm doing everything programmatically. Using Swift.
This seems like something basic that many apps with a WKWebView would do. It should be simple. I'm probably missing something obvious. Any help is appreciated. Thanks in advance.
This is what I have so far:
import UIKit
import WebKit
class ViewController: UIViewController, UIGestureRecognizerDelegate , UIWebViewDelegate, WKNavigationDelegate {
let url = NSURL(string:"http://stackoverflow.com")
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect( x: 0, y: 20, width: 380, height: 150 ), configuration: WKWebViewConfiguration() )
self.view = webView
self.view.frame = webView.frame
let req = NSURLRequest(URL:url!)
webView.loadRequest(req)
self.webView.allowsBackForwardNavigationGestures = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Many thanks Emilio!! The adjusted code works perfectly!
import UIKit
import WebKit
class ViewController: UIViewController, UIGestureRecognizerDelegate , UIWebViewDelegate, WKNavigationDelegate {
let url = NSURL(string:"http://stackoverflow.com")
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: CGRect( x: 0, y: 20, width: self.view.frame.width, height: self.view.frame.height - 20 ), configuration: WKWebViewConfiguration() )
self.view.addSubview(webView)
let req = NSURLRequest(URL:url!)
webView.loadRequest(req)
self.webView.allowsBackForwardNavigationGestures = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Your problem is that UIViewControllers presented in certain ways will manage the frame of their view. If you present it modally, as the root controller, or in a navigation bar, for example, setting the fame manually won't have an effect. (Those are only a few examples).
If you want to manage the frame of the view, you can add the webView as a subview to your view controller's view. Modifying it's frame will work then.
One thing I'd like to point out is that calling self.view.frame = webView.frame after calling self.view = webView is redundant, because they are both the same object, so what you are effectively doing is:
self.view.frame = self.view.frame
I hat make room for some icon's and spend a lot of time fixing this. the solution was placing the adjustment in the viewDidAppear function.
import UIKit
import WebKit
class ViewController: UIViewController {
let webView = WKWebView()
let url_base = "http://www.ztatz.nl"
override func loadView() {
self.view = webView
webView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress),
options: .new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "estimatedProgress" {
print(Float(webView.estimatedProgress))
}
}
override func viewDidLoad() {
super.viewDidLoad()
webView.load( url_base )
}
override func viewDidAppear(_ animated: Bool) {
self.webView.frame = CGRect(x: 0, y: 0, width: self.view.frame.width,
height: self.view.frame.height - 100)
}
}
extension WKWebView {
func load(_ urlString: String) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
load(request)
}
}
}
The above solutions Didn't Work for me.
So I tried the following solution and it worked.
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView(frame: CGRect(x: 0,y: 0 , width: UIScreen.main.bounds.size.width, height:200))
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
And Call from your struct.
struct TermsOfService: View {
var body: some View {
VStack {
WebView(url: URL(string: "https://in.search.yahoo.com")!)
}
}
}

Resources