UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Need help , Project 4 first challenge.

Forums > 100 Days of Swift

I got to end of project number 4 and iv been trying to solve the first challenge of this project .

Q: users try to visit a URL that isn’t allowed, show an alert saying it’s blocked.

4      

Please post what you are trying to do (code), would be helpful to help you

4      

My temporary "solution" was to add a "unallowedwebsite" string in the websites array and check if the website url contain the "unallowed" word.

Actually there aren't url added by the user, so I think that this could be a good way to make this challenge. In future projects you will learn how to let users insert text but you have to wait some days... ;-)

4      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

I actually tried to add some if else statement in the openPage func , I dont know if i do it correctly .

func openPage(action : UIAlertAction){
        if  let url = URL(string: "http://" + action.title!) {
            webView.load(URLRequest(url: url))
        }else{
            let ac = UIAlertController(title: "Valid URL", message: "wrong url", preferredStyle: .alert)
            ac.addAction(UIAlertAction(title: "Close", style: .cancel))
            present(ac , animated: true)
        }
    }

4      

Hi Eric.

You need to implement the following method:

  func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)

which is a function of the WKNavigationDelegate (which your class needs to conform to). Before loading a page, the web view will call this function so you can decide what to do before the page loads. That's why your logic should be here.

In that function, you will loop through the list of allowed sites and compare them to your URL's host.

  let url = navigationAction.request.url
  let host = url?.host

If it is a match, you'll allow the site to load:

  decisionHandler(.allow)
  return

Otherwise (after the loop), You'll show the error message and send .cancel to the decisionHandler closure.

  decisionHandler(.cancel)

Hope that helps.

5      

Following up with the below:

I actually solved this. all i did was to move the alert one " } " above.. so it would look like this instead, and the alert worked fine.HOWEVER I am actually not sure of the reason why it has to be one " } " above and why it would work now.

 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url

        if let host = url?.host {
            for website in websites{
                if host.contains(website) {
                    decisionHandler(.allow)
                    return
                }
            }  // this is all i did differently

        let denied = UIAlertController(title: "denied", message: "This site is not allowed", preferredStyle: .alert)
            denied.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil))
            present(denied, animated: true)

            }

        decisionHandler(.cancel)

Hi all. I am having another problem with this challange as well

I have put the alert inside of the decisionHandler , and the alert works fine. However the alert shows regardless of the website url. I used the "Open" button to load another site , like apple.com( which is an allowed site) and when i click on apple.com link in my button , the alert shows up and the site still loads.

 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url

        if let host = url?.host {
            for website in websites{
                if host.contains(website) {
                    decisionHandler(.allow)
                    return
                }
            }
        }

        let denied = UIAlertController(title: "denied", message: "This site is not allowed", preferredStyle: .alert)
            denied.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil))
            present(denied, animated: true)

        decisionHandler(.cancel)

        }

6      

Hi teekachu,

I was having the same problem. I added some print statements after the decicionHandler calls, like

                    decisionHandler(.allow)
                    print("allowed:")
                    print(url!)

and

        print("not allowed:")
        print(url!)
        // cancel loading
        decisionHandler(.cancel)

Now you will see that some sites that are referenced try to make additional page loads within the html/css/js. Obviously a lot of sites do this without you being aware of it...

So I guess the coding is functioning ok: in case of the apple site it is allowed to go there and the page starts loading. During that load you get an alert on an attempted load within that page that isn't allowed.

For instance, if you have "hackingwithswift.com" in your allowed websites array, you will see that the pages that have an embedded youtube reference wil load, but with a warning and without the youtube reference visible.

5      

Hi guys,

@offgrid, thanks so much for your explanation. I've been trying to grasp this thing for some time now, and now I fully understand how it works.

What you're saying is true - indeed, the "hackingwithswift.com" subpages with an embedded YT reference do load, with a warning and without the youtube reference visible though. What makes me wonder is why some of the clickable links don't react to my interaction, only printing "allowed:". The links I'm talking about are the ones found in the "100 Days With Swift" days.

For example, if I try to enter Day 24. of "100 Days With Swift (UIKit)", everything loads fine (apart from the things we mentioned above). But when I try to click/tap "Setting Up" or other 2 links, literally NOTHING happens - the link gets an underline, message "allowed:" is printed, but that's all - the page is not loading.

Is it only my case, or do you guys experience the same thing?

4      

Hi @MateusZ ,

I'm experiencing the same. Must be because these links try to open in a new tab using target="blank" in the html code. You would probably need to have some extra swift coding to make this work, like opening the new page in a new WKWebView, but that is beyond my knowledge. So for now I just take it for what it is.

5      

Lovely! Thank you for easing my mind with that.

4      

I am also getting the same problem with apple.com and gmail.com. I, don't know why it is happening . For apple and gmail the alert appears after the website is loaded. I had tried printing statement to check the flow of code but in case of above mentioned sites it is going through both the print statements. Can, anyone help with this one.

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        let url = navigationAction.request.url
        if let host = url?.host {
            for website in websites {
                if host.contains(website) {
                    print("Found")
                    decisionHandler(.allow)
                    return
                }
            }
        }
        print("Ouside")
        let ac = UIAlertController(title: "Blocked", message: "This site isn't allowed to visit", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
        present(ac,animated: true)
        decisionHandler(.cancel)
    }

4      

Hi fellas. I figured out why your block alert would display even if, say, "hackingwithswift.com" is in the websites array. It's because you put it inside the for loop, hence the for loop was only able to iterate just 1 time, at that moment website is being only "apple.com", therefore the alert displays as your for loop was not looping through to websites[1] aka "hackingwithswift.com".

This also took me an entire afternoon to figure out. Below is my code for it. Note that I decided to define a separate showBlockAlert() method, and then call it instead, just for readability's sake.

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
  let url = navigationAction.request.url

  if let host = url?.host {
    for website in websites {
      if host.contains(website) {
        decisionHandler(.allow)
        return
      }
    }
    // If user somehow access a URL that isn't allowed, then show a blocking alert
    // This should be placed right below, and outside of the for loop above
    showBlockAlert()
  }

  decisionHandler(.cancel)
}

// Method to show a blocked website alert
func showBlockAlert() {
  let ac = UIAlertController(title: "Blocked website", message: "Unfortunately, this website is not in the website catalog", preferredStyle: .alert)
  ac.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil))
  present(ac, animated: true)
}

Updated: It seems to me that when a site tries to call - say - an ad with its host different from our preset hosts, the showBlockAlert() is also triggered. At the moment being I'm not able to find a solution to it yet; but as long as the app works properly with "apple.com" and "hackingwithswift.com", I think I'm good

4      

Hello everyone! Thank you for your valuable inputs.

I still have several doubts about this challenge.

I realized that even with the alert that the website wasn't allowed, it kept loading so I placed several prints to check the status of my constants in every step and to check which if statement fails and this is when I noticed that there are several loading attempts for the same "selectedWeb" pointing to different hosts (marketing, video, etc.) which aren't on our array (therefore shouldn't be loaded?). This is an example of my output:

"url: https://www.hackingwithswift.com/speaking"
"host: www.hackingwithswift.com"
"selected web: hackingwithswift.com"
"Host was found"
"url: https://www.youtube.com/embed/aK3TJW_nnio?start=51"
"host: www.youtube.com"
"selected web: hackingwithswift.com"
"Host was NOT found"

I believed that placing the decisionHandler right before the end of the decidePolicyFor method is what's allowing the website to load so I changed it and placed it as a part of an else statement, like so:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

        let url = navigationAction.request.url
        debugPrint("url: \(url!)")

        if let host = url?.host {
            debugPrint("host: \(host)")
            if let selectedWeb = selectedWebsite {
                debugPrint("selected web: \(selectedWeb)")

                if host.contains(selectedWeb) {
                    debugPrint("Host was found")
                    decisionHandler(.allow)
                    return
                } else {
                    let ac = UIAlertController(title: "ERROR", message: "The website you're trying to load is not allowed", preferredStyle: .alert)
                    ac.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
                    present(ac, animated: true)
                    decisionHandler(.cancel)
                }
            }
        }

    }

But guess what? I broke my code with this exception:

Exception   NSException *   "Completion handler passed to -[project4.WebVC webView:decidePolicyForNavigationAction:decisionHandler:] was not called"    0x0000600003fbe370

So I am still confused with the decisionHandler placement and the scope of the challenge. When should I considered it was successfully completed?

P.D.: Please note that there isn't a loop in my decidePolicyFor method because I am selecting the website from a Table View Controller.

Thanks in advanced for your time!

KB

4      

Hi all, as a test I've added an invalid host name at index 4 like this:

var websites = ["apple.com", "botb.com", "youtube.com", "hackingwithswift.com", "twitter.co"]

But when I click to open the "twitter" the error message is not showing at all, I thought this would actually work, as we also checking if it's a valid URL I guess?

I also declared a method then I called this method here:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { let url = navigationAction.request.url // Equals to the current URL of the navigation / check the current requested URL

    if let host = url?.host { // Checks if the URL has a host like "apple.com"

        for website in websites { // Loops through the array of "websites" one by one
            if host.contains(website) { // Checks if each safe website exists somewhere in the host name
                decisionHandler(.allow) // Allows loading the website when the host is found in the current URL
                return

            } 

        }
        showAlert()

    }

    decisionHandler(.cancel) // If no host is found for the current URL than website will not be loaded

}

func showAlert() {

    let ac2 = UIAlertController(title: "", message: "Website blocked!", preferredStyle: .alert)
    ac2.addAction(UIAlertAction(title: "Return", style: .cancel))
    present(ac2, animated: true)

}

}

The alert appears after on "botb.com" is loaded which I'm still trying to figure out why as it's a valid website like "apple.com" and so on. Could someone clarify this please? I see is a lot of confusion about this challange.Thank you!

4      

Also if you guys, put this code after that function call (which is our "blocked" alert)

} showAlert() debugPrint("(host) URL isn't allowed!") // This will show exactly why the error message is displaying blocked }

    decisionHandler(.cancel) // If no host is found for the current URL than website will not be loaded

I figured out, if I clicked on "botb.com" this was trying to acces "www.facebook.com" which is weird and have no idea what is the connection between these two as they're separate websites.But the bottom line is that you will see why your alert is showing to the user.Hopefully this might help some of you!

4      

Hey guys, I change to navigationRespone and it just work!! :) using .isForMainFrame to ignore advertise request or request that not change the webview


func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        if navigationResponse.isForMainFrame {
            print(navigationResponse)
            let url = navigationResponse.response.url
            print(url!)
            var count = 0
            if let host = url?.host {
                print(host)

                for website in websites {
                    count += 1
                    print(count)
                    if (host.contains(website) ){
                        decisionHandler(.allow)
                        return
                    }
                }

                let alert = UIAlertController(title: "Stop", message: "This link is forbidden! Go back!", preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "Ok", style: .cancel))
                present(alert, animated: true)
            }

        }

        decisionHandler(.cancel)
    }

6      

Using breakpoints to look at what various variables contained, I noticed what was causing the issue was a call to "about:blank", which like others mentioned I suspect is a extrenuous call being made by the page on request, not the application. When I refactored my code using @togahepi solution, the issue did appear to be solved.

4      

Hi everyone! As I was struggling to find the solution as well, I thought it would be nice to share... not sure if it is the best approach, but so far it works!

So basically, I called this method from the WKNavigationDelegate that "Tells the delegate that an error occurred during the early navigation process" and whenever there is an error when trying to access the website it is going to present an alert to the user:

    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        presentErrorAlert()
    }
    func presentErrorAlert() {
        let alert = UIAlertController(title: "Ops... Not allowed to open this website", message: "This current website is blocked", preferredStyle: .alert)
        let dismissAction = UIAlertAction(title: "Dismiss", style: .cancel)
        alert.addAction(dismissAction)
        present(alert, animated: true)
        print("not able to open website")
    }

3      

@togahepi Hey bro, thank you for your solution. It works perfectly! Could you tell me why did you need to keep track of iterations inside loop (var count)?

3      

@Gatch  

Hi, I am joining ths discussion quite late, but happily. I struggled with this challenge and THINK I have come up with atleast a semi-solution.

like many others, I had this code:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

    let url = navigationAction.request.url
    if let host = url?.host {
        for website in websites {
            if host.contains(website) {
                decisionHandler(.allow)
                return

            }

        }
        configError()
    }

    decisionHandler(.cancel)
}

func configError(){let ac = UIAlertController(title: "Denied", message: "This site is not allowed", preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "Cancel", style: .cancel ,handler: nil) )
    present(ac,animated:true)}

} to test it I added an URL in the openTapped func to the action that was not in the websites array:

@objc func openTapped(){ let ac = UIAlertController(title: "Open page ...", message: nil, preferredStyle: .actionSheet) for website in websites { ac.addAction(UIAlertAction(title: website, style: .default, handler: openPage)) ac.addAction(UIAlertAction(title: "https://twitter.com", style: .default, handler: openPage)) }

    ac.addAction(UIAlertAction(title: "Cancel", style: .cancel))
    ac.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
    present(ac,animated: true)

}

Twitter shows up in the list but the Deny Alert displays properly. All internal links to the sites contained in the websites array display without alert.    :(

3      

Your code seems almost correct, but there's a minor issue in the logic. The way you're checking the validity of the URL might cause a crash if the action's title is nil. Here's an improved version of your code with added error handling for nil titles:

swift Copy code func openPage(action: UIAlertAction) { guard let title = action.title, !title.isEmpty else { return // Do nothing if the action title is nil or empty }

if let url = URL(string: "http://" + title) {
    webView.load(URLRequest(url: url))
} else {
    let ac = UIAlertController(title: "Invalid URL", message: "Please enter a valid URL.", preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "Close", style: .cancel))
    present(ac, animated: true)
}

}

This should help prevent potential crashes due to nil titles and provides better user feedback when an invalid URL is provided.

3      

Your code seems almost correct, but there's a minor issue in the logic. The way you're checking the validity of the URL might cause a crash if the action's title is nil. Here's an improved version of your code with added error handling for nil titles:

swift Copy code func openPage(action: UIAlertAction) { guard let title = action.title, !title.isEmpty else { return // Do nothing if the action title is nil or empty }

if let url = URL(string: "http://" + title) {
    webView.load(URLRequest(url: url))
} else {
    let ac = UIAlertController(title: "Invalid URL", message: "Please enter a valid URL.", preferredStyle: .alert)
    ac.addAction(UIAlertAction(title: "Close", style: .cancel))
    present(ac, animated: true)
}

}

This should help prevent potential crashes due to nil titles and provides better user feedback when an invalid URL is provided welpix.com

3      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.