I can't figure out a way to set the navigation bar to be opaque black...
All the related hacks don't seem to work if the navigation view is presented modally...
This is how I present my webView:
Button(action: { self.showFAQ.toggle() }) {
Text("Frequently Asked Questions").foregroundColor(.orange)
}.sheet(isPresented: $showFAQ) {
WebView(isPresented: self.$showFAQ, url: self.faqURL)
}
This is my webView wrapper:
struct WebView: View {
let url: URL
#Binding var isPresented: Bool
var body: some View {
NavigationView {
WebViewRepresentable(url: url)
.navigationBarTitle("", displayMode: .inline)
.navigationBarItems(trailing: Button(action: {
self.isPresented.toggle()
}, label: { Text("Done") } ))
}
}
init(isPresented: Binding<Bool>, url: URL) {
self.url = url
self._isPresented = isPresented
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
}
struct WebViewRepresentable: UIViewRepresentable {
let url: URL
// Creates a UIKit view to be presented.
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.isOpaque = false
webView.backgroundColor = .systemBackground
return webView
}
// Updates the presented UIKit view (and its coordinator)
// to the latest configuration.
func updateUIView(_ uiView: WKWebView, context: Context) {
let req = URLRequest(url: url)
uiView.load(req)
}
}
}
UINavigationBarAppearance() is ignored... UINavigationBar.appearance() is also ignored...
A possible solution is to avoid using a NavigationView and simply add a Done button to achieve the same result:
struct WebView: View {
let url: URL
#Binding var isPresented: Bool
var body: some View {
VStack {
HStack {
Spacer()
Button(action: {
self.isPresented.toggle()
}) {
Text("Done").padding(.all, 20)
}
}
WebViewRepresentable(url: url)
}.background(Color.black.opacity(1.0))
.edgesIgnoringSafeArea(.all)
}
init(isPresented: Binding<Bool>, url: URL) {
self.url = url
self._isPresented = isPresented
let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
}
struct WebViewRepresentable: UIViewRepresentable {
let url: URL
// Creates a UIKit view to be presented.
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.isOpaque = false
webView.backgroundColor = .systemBackground
return webView
}
// Updates the presented UIKit view (and its coordinator)
// to the latest configuration.
func updateUIView(_ uiView: WKWebView, context: Context) {
let req = URLRequest(url: url)
uiView.load(req)
}
}
}
Related
I have a UIViewRepresentable and need to dismiss when some value has changed.I used .onChange method and it is not working. But onChange method called successfully.
Main View
class ViewModel:ObservableObject {
#Published var urlHasChanged:Bool = false
#Published var isShowWebView:Bool = false
}
struct MainView : View {
#ObservedObject var viewModel = ViewModel()
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
Button {
viewModel.isShowWebView = true
} label: {
Text("show web view")
}
.background(NavigationLink( destination:
WebView(viewModel: viewModel)
.onChange(of: viewModel.urlHasChanged, perform: { newValue in
print("called")
self.presentationMode.wrappedValue.dismiss()
})
,isActive: $viewModel.isShowWebView, label: {
EmptyView()
}).opacity(0))
}
}
}
}
UIViewRepresentable
struct WebView: UIViewRepresentable {
var viewModel : ViewModel
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
context.coordinator.viewModel = viewModel
webView.navigationDelegate = context.coordinator
webView.load(URLRequest(url: URL(string: "https://www.google.com/")!))
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: URL(string: "https://www.google.com/")!)
webView.load(request)
}
class Coordinator : NSObject, WKNavigationDelegate {
var viewModel : ViewModel?
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
//print("webview url \(webView.url)")
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let host = navigationAction.request.url?.absoluteString{
if host.contains("google.com") {
decisionHandler(.allow)
return
}else{
viewModel?.urlHasChanged = true
decisionHandler(.cancel)
return
}
}else{
decisionHandler(.cancel)
}
}
}
}
It is not that presentation, instead turn activating flag back, like
.onChange(of: viewModel.urlHasChanged, perform: { newValue in
print("called")
viewModel.isShowWebView = false // << here !!
})
I would like to make a very simple browser in a View using SwiftUI.
What to expect:
In my ContentView, there is a WKWebView, and a "Go Back" button on the left of the Navigation Bar. If "Go Back" button is pressed, the webView will go back to previous page.
ContentView.swift:
struct ContentView: View {
let defaultUrl = "https://www.apple.com"
#State var needsGoBack = false
var body: some View {
WebView(urlString: defaultUrl, needsGoBack: $needsGoBack)
.navigationBarItems(leading:
Button(action: {
print("button pressed...set needsGoBack = true")
needsGoBack = true
}) {
Text("Go Back")
})
}
}
WebView.swift
struct WebView: UIViewRepresentable {
let urlString: String
let navigationHelper = WebViewHelper()
#State var myWebView = WKWebView()
#Binding var needsGoBack: Bool
func makeUIView(context: Context) -> WKWebView {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
myWebView.load(request)
}
myWebView.navigationDelegate = navigationHelper
myWebView.uiDelegate = navigationHelper
return myWebView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
print("webview updateUIView")
print("needsGoBack", needsGoBack)
if needsGoBack {
myWebView.goBack()
needsGoBack = false // this line has problem
}
}
typealias UIViewType = WKWebView
}
// https://gist.github.com/joshbetz/2ff5922203240d4685d5bdb5ada79105
class WebViewHelper: NSObject, WKNavigationDelegate, WKUIDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("webview didFinishNavigation")
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("didStartProvisionalNavigation")
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
print("webviewDidCommit")
}
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
print("didReceiveAuthenticationChallenge")
completionHandler(.performDefaultHandling, nil)
}
}
What actually happens:
In the updateUIView function of the WebView.swift, there is a caution message "Modifying state during view update, this will cause undefined behavior." for this line of code: "needsGoBack = false". And the "needsGoBack" variable will not be set to "false". The "Go Back" button works for the first time. However, because "needsGoBack" was always "true" afterward, the child view (WebView) will not be notified (updateUIView method called) for the second time.
By keeping your WKWebView stored in a separate object (in this case, NavigationState) that is accessible to both your ContentView, you can access the goBack() method directly. That way, you avoid the tricky problem with trying to use a Bool to signify a one-time event, which not only doesn't work in practice (as you've found), but is also semantically a little funny to think about.
class NavigationState : NSObject, ObservableObject {
#Published var currentURL : URL?
#Published var webView : WKWebView
override init() {
let wv = WKWebView()
self.webView = wv
super.init()
wv.navigationDelegate = self
}
func loadRequest(_ urlRequest: URLRequest) {
webView.load(urlRequest)
}
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.currentURL = webView.url
}
}
struct WebView : UIViewRepresentable {
#ObservedObject var navigationState : NavigationState
func makeUIView(context: Context) -> WKWebView {
return navigationState.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack {
Text(navigationState.currentURL?.absoluteString ?? "(none)")
WebView(navigationState: navigationState)
.clipped()
HStack {
Button("Back") {
navigationState.webView.goBack()
}
Button("Forward") {
navigationState.webView.goForward()
}
}
}.onAppear {
navigationState.loadRequest(URLRequest(url: URL(string: "https://www.google.com")!))
}
}
}
I have a problem where my webView doesn't load on build but in preview it works as it should. What I mean by not loading is that the webView is just white.
What I first thought was that the simulated iphones network settings made it so it didn't allow URLRequests for some reason but I disputed this quickly when I temporarily changed the url to "https://google.com" and it loaded as it should.
Here is my code:
//
// ContentView.swift
// spotifystats
//
// Created by bappo on 2021-08-15.
//
import SwiftUI
import WebKit
struct SpotifyConstants {
static let CLIENT_ID = "***************"
static let SESSION_KEY = "spotifySessionKey"
static let REDIRECT_URI = "spotifystats://"
static let SCOPE = "user-read-email"
}
struct WebView : UIViewRepresentable {
let request: URLRequest
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
}
struct ContentView: View {
#State var isLogginIn = false
let authURLFull = "https://accounts.spotify.com/authorize?response_type=token&client_id=" + SpotifyConstants.CLIENT_ID + "&scope=" + SpotifyConstants.SCOPE + "&redirect_uri=" + SpotifyConstants.REDIRECT_URI + "&show_dialog=false"
var body: some View {
Button("Spotify Login") {
isLogginIn = true
}
.padding()
.foregroundColor(.white)
.background(Color.green)
.clipShape(Capsule())
.sheet(isPresented: $isLogginIn) {
WebView(request: URLRequest(url: URL(string: authURLFull)! ))
}
}
}
extension ContentView {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
RequestForCallbackURL(request: navigationAction.request)
}
func RequestForCallbackURL(request: URLRequest) {
let requestURLString = (request.url?.absoluteString)! as String
print(requestURLString)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am trying to build a new tab function but I am not too sure how I can accomplish this. I am having trouble setting a new or previous WKWebView. And also how do I display an errorView if the url is invalid?
This is what I have so far.
EDIT: I wasn't too sure how to initialize or how to create a invalidurl view. This is kind of like whats going on through my mind
class NavigationState : NSObject, ObservableObject {
#Published var url : URL?
let webView = WKWebView()
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
self.url = webView.url
}
}
struct WebView : UIViewRepresentable {
let request: URLRequest
var navigationState : NavigationState
func makeUIView(context: Context) -> WKWebView {
let webView = navigationState.webView
webView.navigationDelegate = navigationState
webView.load(request)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) { }
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
#State var tablist = [NavigationState]
#State var validurl = true;
init(){
//does not work currently
navigationState.createNewWebView(withRequest: URLRequest(url: URL(string: "https://www.google.com")!))
}
var body: some View {
VStack(){
Button("create new tab"){
tablist.append(navigationState)
//create and set new webview
}
Text(navigationState.url?.absoluteString ?? "(none)")
if(validUrl){
WebView(request: URLRequest(url: URL(string: "https://www.google.com")!), navigationState: navigationState)
} else{InvalidURL()}
HStack {
Button("Back") {
navigationState.webView.goBack()
}
Button("Forward") {
navigationState.webView.goForward()
}
TextField(){onCommit: {
navigationState.selectedWebView?.load(URLRequest(url: URL(string: urlInput)!))
}}
}
}
List {
ForEach(tabs, id: \.self) { tab in
Button(action: {
//set to current webview
}, label: {
Text(tab.webView.url)
})
}.onDelete(perform: delete)
}
}
}
EDIT for the initlization
I added this block of code underneath the NavigationState but I keep getting a blank screen.
override init(){
super.init()
let wv = WKWebView()
wv.navigationDelegate = self
self.webViews.append(wv)
self.selectedWebView = wv
wv.load(URLRequest(url: URL(string: "https://www.google.com")!))
}
Here's a relatively simple implementation (code first, then explanation):
class NavigationState : NSObject, ObservableObject {
#Published var currentURL : URL?
#Published var webViews : [WKWebView] = []
#Published var selectedWebView : WKWebView?
#discardableResult func createNewWebView(withRequest request: URLRequest) -> WKWebView {
let wv = WKWebView()
wv.navigationDelegate = self
webViews.append(wv)
selectedWebView = wv
wv.load(request)
return wv
}
}
extension NavigationState : WKNavigationDelegate {
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
if webView == selectedWebView {
self.currentURL = webView.url
}
}
}
struct WebView : UIViewRepresentable {
#ObservedObject var navigationState : NavigationState
func makeUIView(context: Context) -> UIView {
return UIView()
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let webView = navigationState.selectedWebView else {
return
}
if webView != uiView.subviews.first {
uiView.subviews.forEach { $0.removeFromSuperview() }
webView.frame = CGRect(origin: .zero, size: uiView.bounds.size)
uiView.addSubview(webView)
}
}
}
struct ContentView: View {
#StateObject var navigationState = NavigationState()
var body: some View {
VStack(){
Button("create new tab"){
navigationState.createNewWebView(withRequest: URLRequest(url: URL(string: "https://www.google.com")!))
}
Text(navigationState.currentURL?.absoluteString ?? "(none)")
WebView(navigationState: navigationState)
.clipped()
HStack {
Button("Back") {
navigationState.selectedWebView?.goBack()
}
Button("Forward") {
navigationState.selectedWebView?.goForward()
}
}
List {
ForEach(navigationState.webViews, id: \.self) { tab in
Button(action: {
navigationState.selectedWebView = tab
}) {
Text(tab.url?.absoluteString ?? "?")
}
}
}
}
}
}
Instead of trying to store an array of NavigationStates, I refactored NavigationState to hold an array of web views. The current URL and selected web view are #Published values so that the parent view can see the URL, the selected view, etc.
WebView had to be changed significantly since it had to update which WKWebView is being shown at any given time.
This is pretty rough-around-the edges code. I'd do more refactoring if it were my own project, but this should get you started.
Regarding showing errors with invalid URLs, that's really a second question and probably needs more clarity (what constitutes an invalid URL? Where is it coming from? Do you mean just if the user enters one (in some part of the UI that you're not describing) or also if they click on an invalid link on the page?)
How can I add an activity indicator in WKWebView which will display the indicator while the webpage is loading and disappears when loaded ?
I've looked at some of the old posts but could not figure out how to do it in SwiftUI
see link to one of the old solutions below
How to add Activity Indicator to WKWebView (Swift 3)
Use UIViewRepresentable to create a UIActivityIndicatorView:
You control when an activity indicator animates by calling the startAnimating() and stopAnimating() methods. To automatically hide the activity indicator when animation stops, set the hidesWhenStopped property to true.
You can set the color of the activity indicator by using the color property.
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Create a LoadingView to allow you to wrap around your views:
This allows you to style the activity views content.
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2, height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.red)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to be able to update the LoadingView(...) status you'll need to introduce a view model that inherits from ObservableObject:
Based on this answer: https://stackoverflow.com/a/58825642/264802
class WebViewModel: ObservableObject {
#Published var url: String
#Published var isLoading: Bool = true
init (url: String) {
self.url = url
}
}
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeCoordinator() -> Coordinator {
Coordinator(self.viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.viewModel.isLoading = false
}
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<WebView>) { }
func makeUIView(context: Context) -> UIView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: self.viewModel.url) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
}
Then to use it inside your views you would do the following:
struct ContentView: View {
#StateObject var model = WebViewModel(url: "http://www.google.com")
var body: some View {
LoadingView(isShowing: self.$model.isLoading) {
WebView(viewModel: self.model)
}
}
}
Using 3 Steps I do it in my project.
Step 1: Create a Loading View
import SwiftUI
import UIKit
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
// Main View
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
let message: String
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text(self.message)
.bold()
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
// Mark: Testing
struct LoadingIndicator: View {
var body: some View {
LoadingView(isShowing: .constant(true), message: "Loading...") {
NavigationView {
List(["1", "2", "3", "4", "5"], id: \.self) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}
}
}
}
struct ActivityIndicatorView_Previews: PreviewProvider {
static var previews: some View {
LoadingIndicator()
}
}
Step 2: Create a WebView and WebViewModel
import SwiftUI
import WebKit
class WebViewModel: ObservableObject {
#Published var isLoading: Bool = false
}
struct WebView: UIViewRepresentable {
#ObservedObject var webViewModel: WebViewModel
let urlString: String
func makeUIView(context: Context) -> WKWebView {
let wkWebView = WKWebView()
if let url = URL(string: urlString) {
let urlRequest = URLRequest(url: url)
wkWebView.load(urlRequest)
}
return wkWebView
}
func updateUIView(_ wkWebView: WKWebView, context: Context) {
// do nothing
}
class Coordinator: NSObject, WKNavigationDelegate {
let webViewModel: WebViewModel
init(_ webViewModel: WebViewModel) {
self.webViewModel = webViewModel
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
webViewModel.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webViewModel.isLoading = false
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(webViewModel)
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(webViewModel: WebViewModel(),
urlString: "https://instagram.com/mahmudahsan/")
}
}
Step 3: In your main view use the following code to show indicator and webview
ZStack {
WebView(webViewModel: webViewModel, urlString: "http://ithinkdiff.net")
.frame(height: 1000)
if webViewModel.isLoading {
LoadingView(isShowing: .constant(true), message: "Loading...") {
EmptyView()
}
}
}