/* * Copyright (C) 2020 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ import SwiftUI import _WebKit_SwiftUI struct BrowserTab : View { @StateObject private var state = WebViewState(initialURL: URL(string: "https://webkit.org/")!) var body: some View { let content = WebView(state: state) .webViewNavigationPolicy(onAction: decidePolicy(for:state:)) .alert(item: $externalNavigation, content: makeExternalNavigationAlert(_:)) #if os(macOS) return content .edgesIgnoringSafeArea(.all) .navigationTitle(state.title.isEmpty ? "MiniBrowserSwiftUI" : state.title) .toolbar { ToolbarItemGroup(placement: .navigation) { backItem forwardItem } ToolbarItem(placement: .principal) { urlField } } #else return content // FIXME: This should be `.all`, but on iOS safe area insets do not seem to be respected // correctly when embedded in a `NavigationView`. .edgesIgnoringSafeArea(.bottom) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { backItem.labelStyle(IconOnlyLabelStyle()) forwardItem.labelStyle(IconOnlyLabelStyle()) } ToolbarItem(placement: .principal) { urlField } } #endif } private var urlField: some View { URLField( url: state.url, isSecure: state.hasOnlySecureContent, loadingProgress: state.estimatedProgress, onNavigate: onNavigate(to:) ) { if state.isLoading { Button(action: state.stopLoading) { Label("Stop Loading", systemImage: "xmark") } } else { Button(action: state.reload) { Label("Reload", systemImage: "arrow.clockwise") } .disabled(state.url == nil) } } } private var backItem: some View { Button(action: state.goBack) { Label("Back", systemImage: "chevron.left") .frame(minWidth: 20) } .disabled(!state.canGoBack) } private var forwardItem: some View { Button(action: state.goForward) { Label("Forward", systemImage: "chevron.right") .frame(minWidth: 20) } .disabled(!state.canGoForward) } private func onNavigate(to string: String) { switch UserInput(string: string) { case .search(let term): state.load(URL(searchTerm: term)) case .url(let url): state.load(url) case .invalid: break } } @State private var externalNavigation: ExternalURLNavigation? @Environment(\.openURL) private var openURL private func decidePolicy(for action: NavigationAction, state: WebViewState) { if let externalURL = action.request.url, !WebView.canHandle(externalURL) { externalNavigation = ExternalURLNavigation( source: state.url ?? .aboutBlank, destination: externalURL) action.decidePolicy(.cancel) } else { action.decidePolicy(.allow) } } private func makeExternalNavigationAlert(_ navigation: ExternalURLNavigation) -> Alert { Alert(title: Text("Allow “\(navigation.source.highLevelDomain)” to open “\(navigation.destination.scheme ?? "")”?"), primaryButton: .default(Text("Allow"), action: { openURL(navigation.destination) }), secondaryButton: .cancel()) } } private enum UserInput { case search(String) case url(URL) case invalid init(string _string: String) { let string = _string.trimmingCharacters(in: .whitespaces) if string.isEmpty { self = .invalid } else if !string.contains(where: \.isWhitespace), string.contains("."), let url = URL(string: string) { self = .url(url) } else { self = .search(string) } } } private struct ExternalURLNavigation : Identifiable, Hashable { var source: URL var destination: URL var id: Self { self } } struct BrowserTab_Previews: PreviewProvider { static var previews: some View { #if os(iOS) NavigationView { BrowserTab() } .navigationViewStyle(StackNavigationViewStyle()) #endif } }