iOS: Hide/show bars while scrolling
This commit is contained in:
parent
d8e93e481d
commit
b174a26d1c
|
@ -12,11 +12,13 @@ import GeminiProtocol
|
|||
|
||||
public struct BrowserView: View {
|
||||
let navigator: NavigationManager
|
||||
let scrollingEnabled: Bool
|
||||
@State var task: GeminiDataTask?
|
||||
@State var state: ViewState = .loading
|
||||
|
||||
public init(navigator: NavigationManager) {
|
||||
public init(navigator: NavigationManager, scrollingEnabled: Bool = true) {
|
||||
self.navigator = navigator
|
||||
self.scrollingEnabled = scrollingEnabled
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
@ -37,7 +39,7 @@ public struct BrowserView: View {
|
|||
Text(message)
|
||||
}
|
||||
case let .document(doc):
|
||||
DocumentView(document: doc, changeURL: navigator.changeURL)
|
||||
DocumentView(document: doc, scrollingEnabled: scrollingEnabled, changeURL: navigator.changeURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
//
|
||||
// BrowserViewController.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/28/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
class BrowserViewController: UIViewController, UIScrollViewDelegate {
|
||||
|
||||
let navigator: NavigationManager
|
||||
|
||||
private var scrollView: UIScrollView!
|
||||
|
||||
private var browserHost: UIHostingController<BrowserView>!
|
||||
private var navBarHost: UIHostingController<NavigationBar>!
|
||||
private var toolBarHost: UIHostingController<ToolBar>!
|
||||
|
||||
private var prevScrollViewContentOffset: CGPoint?
|
||||
|
||||
private var barAnimator: UIViewPropertyAnimator?
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
scrollView = UIScrollView()
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(scrollView)
|
||||
scrollView.delegate = self
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
browserHost = UIHostingController(rootView: BrowserView(navigator: navigator, scrollingEnabled: false))
|
||||
browserHost.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(browserHost.view)
|
||||
addChild(browserHost)
|
||||
browserHost.didMove(toParent: self)
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: browserHost.view.leadingAnchor),
|
||||
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: browserHost.view.trailingAnchor),
|
||||
scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: browserHost.view.topAnchor),
|
||||
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: browserHost.view.bottomAnchor),
|
||||
browserHost.view.widthAnchor.constraint(equalTo: view.widthAnchor),
|
||||
|
||||
// make sure the browser host view is at least the screen height so the loading indicator appears centered
|
||||
browserHost.view.heightAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor),
|
||||
])
|
||||
|
||||
navBarHost = UIHostingController(rootView: NavigationBar(navigator: navigator))
|
||||
navBarHost.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navBarHost.view)
|
||||
addChild(navBarHost)
|
||||
navBarHost.didMove(toParent: self)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
navBarHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navBarHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
navBarHost.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
])
|
||||
|
||||
toolBarHost = UIHostingController(rootView: ToolBar(navigator: navigator, shareCurrentURL: {
|
||||
let vc = UIActivityViewController(activityItems: [self.navigator.currentURL], applicationActivities: nil)
|
||||
self.present(vc, animated: true)
|
||||
}))
|
||||
toolBarHost.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(toolBarHost.view)
|
||||
addChild(toolBarHost)
|
||||
toolBarHost.didMove(toParent: self)
|
||||
NSLayoutConstraint.activate([
|
||||
toolBarHost.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
toolBarHost.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
toolBarHost.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
let insets = UIEdgeInsets(
|
||||
top: navBarHost.view.bounds.height - view.safeAreaInsets.top,
|
||||
left: 0,
|
||||
bottom: toolBarHost.view.bounds.height - view.safeAreaInsets.bottom,
|
||||
right: 0
|
||||
)
|
||||
scrollView.contentInset = insets
|
||||
scrollView.scrollIndicatorInsets = insets
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
var scrollViewDelta: CGFloat = 0
|
||||
if let prev = prevScrollViewContentOffset {
|
||||
scrollViewDelta = scrollView.contentOffset.y - prev.y
|
||||
}
|
||||
prevScrollViewContentOffset = scrollView.contentOffset
|
||||
|
||||
// When certain state changes happen, the scroll view seems to "scroll" by top the safe area inset.
|
||||
// It's not actually user scrolling, and this screws up our animation, so we ignore it.
|
||||
guard abs(scrollViewDelta) != view.safeAreaInsets.top, scrollViewDelta != 0 else { return }
|
||||
|
||||
let barAnimator: UIViewPropertyAnimator
|
||||
if let animator = self.barAnimator {
|
||||
barAnimator = animator
|
||||
} else {
|
||||
navBarHost.view.transform = .identity
|
||||
toolBarHost.view.transform = .identity
|
||||
barAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .linear) {
|
||||
self.navBarHost.view.transform = CGAffineTransform(translationX: 0, y: -self.navBarHost.view.frame.height)
|
||||
self.toolBarHost.view.transform = CGAffineTransform(translationX: 0, y: self.toolBarHost.view.frame.height)
|
||||
}
|
||||
if scrollViewDelta < 0 {
|
||||
barAnimator.fractionComplete = 1
|
||||
}
|
||||
barAnimator.addCompletion { (_) in
|
||||
self.barAnimator = nil
|
||||
}
|
||||
self.barAnimator = barAnimator
|
||||
}
|
||||
|
||||
let progressDelta = scrollViewDelta / navBarHost.view.bounds.height
|
||||
barAnimator.fractionComplete = max(0, min(1, barAnimator.fractionComplete + progressDelta))
|
||||
}
|
||||
|
||||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
if let barAnimator = barAnimator {
|
||||
if barAnimator.fractionComplete < 0.5 {
|
||||
barAnimator.isReversed = true
|
||||
}
|
||||
barAnimator.startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -8,14 +8,23 @@
|
|||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
// This is not currently used as SwiftUI's ScrollView has no mechanism for detecting when it stops deceleraing,
|
||||
// which is necessary to preven tthe bars from being left in a partially visible state.
|
||||
struct ContentView: View {
|
||||
@ObservedObject private var navigator: NavigationManager
|
||||
@State private var urlFieldContents: String
|
||||
@State private var showPreferencesSheet = false
|
||||
private let shareCurrentURL: () -> Void
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
@State private var prevScrollOffset: CGFloat = 0
|
||||
@State private var scrollOffset: CGFloat = 0 {
|
||||
didSet {
|
||||
prevScrollOffset = oldValue
|
||||
}
|
||||
}
|
||||
@State private var barOffset: CGFloat = 0
|
||||
@State private var navBarHeight: CGFloat = 0
|
||||
@State private var toolBarHeight: CGFloat = 0
|
||||
|
||||
init(navigator: NavigationManager, shareCurrentURL: @escaping () -> Void) {
|
||||
self.navigator = navigator
|
||||
self._urlFieldContents = State(initialValue: navigator.currentURL.absoluteString)
|
||||
|
@ -23,18 +32,57 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
urlBar
|
||||
ZStack {
|
||||
GeometryReader { (outer: GeometryProxy) in
|
||||
ScrollView(.vertical) {
|
||||
Color.clear.frame(height: navBarHeight)
|
||||
|
||||
BrowserView(navigator: navigator, scrollingEnabled: false)
|
||||
.background(GeometryReader { (inner: GeometryProxy) in
|
||||
Color.clear.preference(key: ScrollOffsetPrefKey.self, value: -inner.frame(in: .global).minY + outer.frame(in: .global).minY)
|
||||
})
|
||||
|
||||
Color.clear.frame(height: toolBarHeight)
|
||||
}
|
||||
.onPreferenceChange(ScrollOffsetPrefKey.self) {
|
||||
scrollOffset = $0
|
||||
let delta = scrollOffset - prevScrollOffset
|
||||
|
||||
// When certain state changes happen, the scroll view seems to "scroll" by the top safe area inset.
|
||||
// It's not actually user scrolling, and this screws up our animation, so we ignore it.
|
||||
guard abs(delta) != outer.safeAreaInsets.top else { return }
|
||||
|
||||
if delta != 0 {
|
||||
barOffset += delta
|
||||
}
|
||||
|
||||
barOffset = max(0, min(navBarHeight + outer.safeAreaInsets.top, barOffset))
|
||||
}
|
||||
}
|
||||
|
||||
barBorder
|
||||
|
||||
Spacer(minLength: 0)
|
||||
BrowserView(navigator: navigator)
|
||||
Spacer(minLength: 0)
|
||||
|
||||
barBorder
|
||||
|
||||
bottomBar
|
||||
VStack(spacing: 0) {
|
||||
NavigationBar(navigator: navigator)
|
||||
.background(GeometryReader { (geom: GeometryProxy) in
|
||||
Color.clear.preference(key: NavBarHeightPrefKey.self, value: geom.frame(in: .global).height)
|
||||
})
|
||||
.offset(y: -barOffset)
|
||||
|
||||
Spacer()
|
||||
|
||||
ToolBar(navigator: navigator, shareCurrentURL: shareCurrentURL)
|
||||
.background(GeometryReader { (geom: GeometryProxy) in
|
||||
Color.clear.preference(key: ToolBarHeightPrefKey.self, value: geom.frame(in: .global).height)
|
||||
})
|
||||
.offset(y: barOffset)
|
||||
}
|
||||
.onPreferenceChange(NavBarHeightPrefKey.self) {
|
||||
navBarHeight = $0
|
||||
print("nav bar height: \($0)")
|
||||
}
|
||||
.onPreferenceChange(ToolBarHeightPrefKey.self) {
|
||||
toolBarHeight = $0
|
||||
print("tool bar height: \($0)")
|
||||
}
|
||||
}
|
||||
.onAppear(perform: tweakAppearance)
|
||||
.onReceive(navigator.$currentURL, perform: { (new) in
|
||||
|
@ -45,70 +93,6 @@ struct ContentView: View {
|
|||
})
|
||||
}
|
||||
|
||||
private var barBorder: some View {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75))
|
||||
}
|
||||
|
||||
private var urlBar: some View {
|
||||
TextField("URL", text: $urlFieldContents, onCommit: commitURL)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
}
|
||||
|
||||
private var bottomBar: some View {
|
||||
HStack {
|
||||
// use a group because this exceeds the 10 view limit :/
|
||||
Group {
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.back) {
|
||||
Image(systemName: "arrow.left")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.disabled(navigator.backStack.isEmpty)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.forward) {
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.disabled(navigator.forwardStack.isEmpty)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.reload) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: shareCurrentURL) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showPreferencesSheet = true
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.font(.system(size: 24))
|
||||
})
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
private func tweakAppearance() {
|
||||
UIScrollView.appearance().keyboardDismissMode = .interactive
|
||||
}
|
||||
|
@ -119,6 +103,34 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
fileprivate struct ScrollOffsetPrefKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct NavBarHeightPrefKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ToolBarHeightPrefKey: PreferenceKey {
|
||||
typealias Value = CGFloat
|
||||
static var defaultValue: CGFloat = 0
|
||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||
value += nextValue()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate enum ScrollDirection {
|
||||
case up, down, none
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!), shareCurrentURL: {})
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// NavigationBar.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/28/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
struct NavigationBar: View {
|
||||
@ObservedObject var navigator: NavigationManager
|
||||
@State private var urlFieldContents: String
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
init(navigator: NavigationManager) {
|
||||
self.navigator = navigator
|
||||
self._urlFieldContents = State(initialValue: navigator.currentURL.absoluteString)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
TextField("URL", text: $urlFieldContents, onCommit: commitURL)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75))
|
||||
}
|
||||
.background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.top))
|
||||
}
|
||||
|
||||
private func commitURL() {
|
||||
guard let url = URL(string: urlFieldContents) else { return }
|
||||
navigator.changeURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
struct NavigationBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationBar(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!))
|
||||
}
|
||||
}
|
|
@ -26,19 +26,24 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
if let context = connectionOptions.urlContexts.first {
|
||||
initialURL = context.url
|
||||
} else {
|
||||
initialURL = URL(string: "gemini://gemini.circumlunar.space/")!
|
||||
if ProcessInfo.processInfo.environment.keys.contains("DEFAULT_URL") {
|
||||
initialURL = URL(string: ProcessInfo.processInfo.environment["DEFAULT_URL"]!)!
|
||||
} else {
|
||||
initialURL = URL(string: "gemini://gemini.circumlunar.space/")!
|
||||
}
|
||||
}
|
||||
|
||||
navigationManager = NavigationManager(url: initialURL)
|
||||
navigationManager.delegate = self
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let contentView = ContentView(navigator: navigationManager, shareCurrentURL: self.shareCurrentURL)
|
||||
// let contentView = ContentView(navigator: navigationManager, shareCurrentURL: self.shareCurrentURL)
|
||||
|
||||
// Use a UIHostingController as window root view controller.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
// window.rootViewController = UIHostingController(rootView: contentView)
|
||||
window.rootViewController = BrowserViewController(navigator: navigationManager)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// ToolBar.swift
|
||||
// Gemini-iOS
|
||||
//
|
||||
// Created by Shadowfacts on 9/28/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import BrowserCore
|
||||
|
||||
struct ToolBar: View {
|
||||
@ObservedObject var navigator: NavigationManager
|
||||
let shareCurrentURL: () -> Void
|
||||
@State private var showPreferencesSheet = false
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
Rectangle()
|
||||
.frame(height: 1)
|
||||
.foregroundColor(Color(white: colorScheme == .dark ? 0.25 : 0.75))
|
||||
|
||||
HStack {
|
||||
// use a group because this exceeds the 10 view limit :/
|
||||
Group {
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.back) {
|
||||
Image(systemName: "arrow.left")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.disabled(navigator.backStack.isEmpty)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.forward) {
|
||||
Image(systemName: "arrow.right")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
.disabled(navigator.forwardStack.isEmpty)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: navigator.reload) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: shareCurrentURL) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.system(size: 24))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showPreferencesSheet = true
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.font(.system(size: 24))
|
||||
})
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.background(Color(UIColor.systemBackground).edgesIgnoringSafeArea(.bottom))
|
||||
.sheet(isPresented: $showPreferencesSheet, content: {
|
||||
PreferencesView(presented: $showPreferencesSheet)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ToolBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ToolBar(navigator: NavigationManager(url: URL(string: "gemini://localhost/overview.gmi")!), shareCurrentURL: {})
|
||||
}
|
||||
}
|
|
@ -42,6 +42,9 @@
|
|||
D664673A24BD0B8E00B0B741 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D664673924BD0B8E00B0B741 /* Fonts.swift */; };
|
||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A64D25217C6F00348C4B /* Preferences.swift */; };
|
||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A66625217FD800348C4B /* PreferencesView.swift */; };
|
||||
D691A6772522382E00348C4B /* BrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A6762522382E00348C4B /* BrowserViewController.swift */; };
|
||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A68625223A4600348C4B /* NavigationBar.swift */; };
|
||||
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691A69F252242FC00348C4B /* ToolBar.swift */; };
|
||||
D69F00AC24BE9DD300E37622 /* GeminiDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */; };
|
||||
D69F00AE24BEA29100E37622 /* GeminiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69F00AD24BEA29100E37622 /* GeminiResponse.swift */; };
|
||||
D6E1529824BFAAA400FDF9D3 /* BrowserWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E1529624BFAAA400FDF9D3 /* BrowserWindowController.swift */; };
|
||||
|
@ -276,6 +279,9 @@
|
|||
D664673924BD0B8E00B0B741 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = "<group>"; };
|
||||
D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||
D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = "<group>"; };
|
||||
D691A68625223A4600348C4B /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = "<group>"; };
|
||||
D691A69F252242FC00348C4B /* ToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolBar.swift; sourceTree = "<group>"; };
|
||||
D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiDataTask.swift; sourceTree = "<group>"; };
|
||||
D69F00AD24BEA29100E37622 /* GeminiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = "<group>"; };
|
||||
D69F00AF24BEA84D00E37622 /* NavigationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationManager.swift; sourceTree = "<group>"; };
|
||||
|
@ -526,7 +532,10 @@
|
|||
children = (
|
||||
D6E152A424BFFDF500FDF9D3 /* AppDelegate.swift */,
|
||||
D6E152A624BFFDF500FDF9D3 /* SceneDelegate.swift */,
|
||||
D691A6762522382E00348C4B /* BrowserViewController.swift */,
|
||||
D6E152A824BFFDF500FDF9D3 /* ContentView.swift */,
|
||||
D691A68625223A4600348C4B /* NavigationBar.swift */,
|
||||
D691A69F252242FC00348C4B /* ToolBar.swift */,
|
||||
D691A64D25217C6F00348C4B /* Preferences.swift */,
|
||||
D691A66625217FD800348C4B /* PreferencesView.swift */,
|
||||
D6E152AA24BFFDF600FDF9D3 /* Assets.xcassets */,
|
||||
|
@ -1028,8 +1037,11 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D691A66725217FD800348C4B /* PreferencesView.swift in Sources */,
|
||||
D691A6772522382E00348C4B /* BrowserViewController.swift in Sources */,
|
||||
D6E152A524BFFDF500FDF9D3 /* AppDelegate.swift in Sources */,
|
||||
D691A6A0252242FC00348C4B /* ToolBar.swift in Sources */,
|
||||
D6E152A724BFFDF500FDF9D3 /* SceneDelegate.swift in Sources */,
|
||||
D691A68725223A4700348C4B /* NavigationBar.swift in Sources */,
|
||||
D691A64E25217C6F00348C4B /* Preferences.swift in Sources */,
|
||||
D6E152A924BFFDF500FDF9D3 /* ContentView.swift in Sources */,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1220"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6E152A124BFFDF500FDF9D3"
|
||||
BuildableName = "Gemini-iOS.app"
|
||||
BlueprintName = "Gemini-iOS"
|
||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6E152A124BFFDF500FDF9D3"
|
||||
BuildableName = "Gemini-iOS.app"
|
||||
BlueprintName = "Gemini-iOS"
|
||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "DEFAULT_URL"
|
||||
value = "gemini://gemini.circumlunar.space/docs/specification.gmi"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "DEFAULT_URL"
|
||||
value = "gemini://drewdevault.com"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D6E152A124BFFDF500FDF9D3"
|
||||
BuildableName = "Gemini-iOS.app"
|
||||
BlueprintName = "Gemini-iOS"
|
||||
ReferencedContainer = "container:Gemini.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -62,6 +62,11 @@
|
|||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>D6E152A124BFFDF500FDF9D3</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -12,22 +12,33 @@ public struct DocumentView: View {
|
|||
private let document: Document
|
||||
private let blocks: [RenderingBlock]
|
||||
private let changeURL: ((URL) -> Void)?
|
||||
private let scrollingEnabled: Bool
|
||||
|
||||
public init(document: Document, changeURL: ((URL) -> Void)? = nil) {
|
||||
public init(document: Document, scrollingEnabled: Bool = true, changeURL: ((URL) -> Void)? = nil) {
|
||||
self.document = document
|
||||
self.blocks = document.renderingBlocks
|
||||
self.changeURL = changeURL
|
||||
self.scrollingEnabled = scrollingEnabled
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
public var body: some View {
|
||||
ScrollView(.vertical) {
|
||||
MaybeLazyVStack(alignment: .leading) {
|
||||
ForEach(blocks.indices) { (index) in
|
||||
RenderingBlockView(block: blocks[index], changeURL: changeURL)
|
||||
}
|
||||
}.padding([.leading, .trailing, .bottom])
|
||||
if scrollingEnabled {
|
||||
ScrollView(.vertical) {
|
||||
scrollBody
|
||||
}
|
||||
} else {
|
||||
scrollBody
|
||||
}
|
||||
}
|
||||
|
||||
private var scrollBody: some View {
|
||||
MaybeLazyVStack(alignment: .leading) {
|
||||
ForEach(blocks.indices) { (index) in
|
||||
RenderingBlockView(block: blocks[index], changeURL: changeURL)
|
||||
}
|
||||
}.padding([.leading, .trailing, .bottom])
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentView_Previews: PreviewProvider {
|
||||
|
|
Loading…
Reference in New Issue