Welcome, Guest User :: Click here to login

Logo 67443

Lab 7A: SimpleBrowser (Android)

Due Date: October 29

Objectives

  • teach students about using Android's WebKit WebChromeClient
  • have students interact with WebView and related classes
  • have students practice implementing methods from design patterns
  • reinforce previous Android development lessons
  • give students an opportunity to work with Web Fragments

README.md

Simple Browser

To get started visit the Web-based Content Android Developer Guide.


In this lab, we will use WebKit to help us build our own browser for Android. It’s a scaled down browser, but it should have a navigation bar so we can enter in URLs, a reload and stop Botton, and a set of back and forward buttons for simple navigation. Here is what the app might look like when finished:

Under Construction

I. Setup

  1. Start a new project called SimpleBrowser as a Single View Application. This will be our first lab using SwiftUI, so make sure to select SwiftUI from the User Interface dropdown.
  2. Now let's work on the UI in the ContentView.swift file. There are 3 main components of the UI—the url navigation bar, the web view, and the bottom bar. The navigation bar and the bottom bar both use HStack. Let’s work on those first, as they are significantly more simple than the web view.

II. The URL Navigation Bar

  1. The 2 elements of the URL Navigation bar are Text element for the URL label, and a TextField element for typing in the URL. Create a new SwiftUI view called SearchBar.swift. Create a struct like so:

    struct SearchBar: View {
      var body: some View { }
    }
    
  2. Inside the body, add the two necessary elements for the search bar like so:

        HStack {
          Text("URL:")
          TextField("URL", text: $viewModel.urlString)
        }
    

    Notice the $viewModel.urlString. This is the binding for URL the user will type in. We will need to create a view model to keep this value consistent throughout the app.

  3. Create a file called ViewModel.swift and create a class called ViewModel and make it an ObservableObject like so:

    class ViewModel: ObservableObject { }
    
  4. Add an published variable for the urlString so the class now looks like this:

    class ViewModel: ObservableObject {
      @Published var urlString: String = ""
    }
    
  5. Then, add a reference to a ViewModel to the SearchBar struct.

    @ObservedObject var viewModel: ViewModel
    
  6. Back to SearchBar.swift. To make the TextField more user friendly for typing in URLs, here are some options you can chain to the end of it:

    TextField("URL", text: $viewModel.urlString)
      .keyboardType(.URL)
      .autocapitalization(.none)
      .disableAutocorrection(true)
    
  7. Now to add the SearchBar to your ContentView. First, store an instance of the ViewModel as a constant in your ContentView struct. Then, add the SearchBar inside a VStack to the body variable, giving it that instance of the ViewModel:

      var body: some View {
        VStack {
          SearchBar(viewModel: viewModel)
        }
    
  8. Now when you run your app, you should be able to see the URL navigation bar.

III. The Navigation Options Bar

Now let’s create the bottom navigation option button bar.

  1. Let’s create a new SwiftUI view called BottomBar.swift and create a struct called BottomBar just like we did for SearchBar.

  2. Let’s create a HStack inside the body for our buttons.

  3. Inside the HStack, let’s add 5 buttons like so:

    Button(action: { }) {
      Image(systemName: "chevron.left")
    }
    

    Add Spacer() in between the buttons to distribute them evenly.

    The systemNames of the Images we’ll be using are:

    • "chevron.left"
    • "chevron.right"
    • "square.and.arrow.up"
    • "arrow.clockwise"
    • "xmark"

    You can learn more about SFSymbols here.

  4. These buttons don’t yet have actions associated with them, but let’s integrate it into our ContentView.swift like we did with the URL navigation bar.

  5. Now if you run your app, you should see both the top and bottom navigation bars.

IV. ViewModel

  1. Let’s create empty methods for our button actions in our ViewModel. Create 5 empty methods for our five empty actions (you can add print statements to these methods to ensure they are working/linked correctly). These methods are:

    • goBack()
    • goForward()
    • share()
    • refresh()
    • stop()
  2. Now, let’s link them to our buttons. First, in your BottomBar struct, create an @ObservedObject for your ViewModel.

    Now, in your ContentView where you create your BottomBar you will need to pass the instance of ViewModel.

  3. Then, add the view model methods to your button actions.

    You can either add them in the action closure of the buttons like this:

    Button(action: {
      self.viewModel.goBack()
    }) {
      Image(systemName: "chevron.left")
    }
    

    Or pass in the function as a parameter like this:

    Button(action: viewModel.goBack) {
      Image(systemName: "chevron.left")
    }
    

    I personally prefer the latter, but it’s up to you!

  4. Now these buttons still don’t do anything, but make sure they’re linked to your ViewModel by using breakpoints or print statements.

V. The WebView

Now, let’s move to the WebView!

  1. Create a new SwiftUI view and within it, create a struct called WebView.

  2. Make sure you import WebKit, import Combine , and import SwiftUI.

  3. Make this conform to UIViewRepresentable instead of having it be an instance of View (like the top and bottom bars were).

  4. Create 2 methods in this WebView struct:

    1. func makeUIView(context: Context) -> WKWebView
    2. func updateUIView(_ webView: WKWebView, context: Context)

    Fill them out like so:

    func makeUIView(context: Context) -> WKWebView {
      return WKWebView()
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
      if let url = URL(string: "https://\(viewModel.urlString)") {
        webView.load(URLRequest(url: url))
      }
    }
    
  5. You will need an instance of ViewModel for the above code. Add a constant for this just as you did for BottomBar and SearchBar

  6. Now, in your ContentView add an instance of WebView between your SearchBar and your BottomBar.

  7. Try this out! You should be able to open web pages using your app, but navigation will not work yet.

  8. We will come back to these methods later when we implement our navigations actions, but before that, within the WebView struct, let’s create a class called Coordinator, which will be an NSObject and conform to WKNavigationDelegate.

  9. Give this class 2 variables:

    var parent: WebView
    var webViewOptionsSubscriber: AnyCancellable?
    
  10. Create an init function for this class, make the init take a WebView object and set the parent variable equal to this WebView.

  11. Also create a deinit like this:

    deinit {
      webViewOptionsSubscriber?.cancel()
    }
    
  12. Finally, let’s create the most important method of this class:

    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { }
    

    This method is what is going to handle our navigation buttons and utilize Combine to do so.

  13. Let’s go back to our pretty empty ViewModel and create an enum (outside of the class) for our web view button action options. Enums are great and used heavily in iOS development. I would suggest getting very comfortable with enums. Read more about them here.

    enum WebViewOptions {
      case back
      case forward
      case share
      case refresh
      case stop
    }
    
  14. Now within the ViewModel, let’s create a published variable for which option the user has just tapped:

    @Published var webViewOptionsPublisher = PassthroughSubject<WebViewOptions, Never>()
    

    You will need to import Combine at the top of your ViewModel in order to use the PassthroughSubject.

    Learn more about a PassthroughSubject here.

  15. This will allow us to respond to the user’s selections in our WebView. But to do that, we need to send these values in our View Model’s methods. For example, our goBack() method will look like this:

    func goBack() {
      webViewOptionsPublisher.send(.back)
    }
    

    Fill out the rest of the methods accordingly.

    This will send the value from the publisher to the subscriber, which we will set up in the WebView so the WebView can respond to the user’s selection. Let’s do that next.

  16. Start filling out the Coordinator.webView(_:didStartProvisionalNavigation:) method in WebView.swift.
    This is what it should look like before you fill it out:

        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
          webViewOptionsSubscriber = parent.viewModel.webViewOptionsPublisher.sink(receiveValue: { webViewOption in
            // create a switch statement for each of the different options
          })
       }
    

    Make sure you understand what .sink() is doing. It is receiving the value we sent using the webViewOptionsPublisher in the ViewModel and we can respond to it in our WebView.

    Let’s create a switch statement to respond to the different navigation options. This goes inside the .sink closure.

    switch webViewOption {
      case .back: return
      case .forward: return
      case .share: return
      case .refresh: return
      case .stop: return
    }
    
  17. Right now we’re just returning, but let’s fill each option out:

    switch webViewOption {
    case .back:
      if webView.canGoBack {
        webView.goBack()
      }
    case .forward:
      if webView.canGoForward {
        webView.goForward()
      }
    case .share: return // we'll fill this out later
    case .refresh:
      webView.reload()
    case .stop:
      webView.stopLoading()
    }
    
  18. Now let’s make sure our WebView respond to these actions. In your WebView struct, add this method:

    func makeCoordinator() -> Coordinator {
      Coordinator(webView: self)
    }
    
  19. And update the WebView.makeUIView(context:) function to look like this:

    func makeUIView(context: Context) -> WKWebView {
      let webView = WKWebView()
      webView.navigationDelegate = context.coordinator
      return webView
    }
    

    This method allows our WebView to use the custom Coordinator we defined.

  20. Now, all buttons on your app should be working except for the share button - try them out!

VI. Share Sheet

Finally, let’s create the share sheet!

  1. Create a Swift file called ShareSheet.swift:

    import SwiftUI
    import UIKit
    
    struct ShareSheet: UIViewControllerRepresentable {
      typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
    
      let activityItems: [Any]
      let applicationActivities: [UIActivity]? = nil
      let excludedActivityTypes: [UIActivity.ActivityType]? = nil
      let callback: Callback? = nil
    
      func makeUIViewController(context: Context) -> UIActivityViewController {
        let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
        controller.excludedActivityTypes = excludedActivityTypes
        controller.completionWithItemsHandler = callback
        return controller
      }
    
      func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
    }
    
  2. Add a new variable to the ViewModel: @Published var shouldShowShareSheet: Bool = false

    We will set this to true in WebView.Coordinator.webView(_:didStartProvisionalNavigation:) like so:

    case .share:
      self.parent.viewModel.shouldShowShareSheet = true
    
  3. To present the share sheet, add the following code to the end of the VStack in the ContentView.

    Note: due to improvements in how SwiftUI handles if statements the code can be written more concisely in XCode 12 than it can in XCode 11. Please use the appropriate version.

    XCode 12 and above

    .sheet(isPresented: $viewModel.shouldShowShareSheet) {
      if let url: URL = URL(string: "https://\(viewModel.urlString)") {
        ShareSheet(activityItems: [url])
      }
    }
    

    The ContentView.swift should look like this now:

    import SwiftUI
    
    struct ContentView: View {
      @ObservedObject var viewModel = ViewModel()
    
      var body: some View {
        VStack {
          SearchBar(viewModel: viewModel)
          WebView(viewModel: viewModel)
          BottomBar(viewModel: viewModel)
        }
        .sheet(isPresented: $viewModel.shouldShowShareSheet) {
          if let url: URL = URL(string: "https://\(viewModel.urlString)") {
            ShareSheet(activityItems: [url])
          }
        }
      }
    }
    

    XCode 11 and below

    .sheet(isPresented: $viewModel.shouldShowShareSheet) {
      if URL(string: "https://\(self.viewModel.urlString)") != nil {
        ShareSheet(activityItems: [URL(string: "https://\(self.viewModel.urlString)")!])
      }
    }
    

    The ContentView.swift should look like this now:

    struct ContentView: View {
      @ObservedObject var viewModel: ViewModel = ViewModel()
    
      var body: some View {
        VStack {
          SearchBar(viewModel: viewModel)
          WebView(viewModel: viewModel)
          BottomBar(viewModel: viewModel)
        }
        .sheet(isPresented: $viewModel.shouldShowShareSheet) {
          if URL(string: "https://\(self.viewModel.urlString)") != nil {
            ShareSheet(activityItems: [URL(string: "https://\(self.viewModel.urlString)")!])
          }
        }
      }
    }
    
  4. And now all your actions should work! And you have your own simple browser :)