blob: b2de66e86249e606ee328da4dd11e2217740cab4 [file] [log] [blame]
/*
* 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
}
}