From b174a26d1c0e9356ebdce06ad48e91a38c510f0d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 28 Sep 2020 15:20:06 -0400 Subject: [PATCH] iOS: Hide/show bars while scrolling --- BrowserCore/BrowserView.swift | 6 +- Gemini-iOS/BrowserViewController.swift | 153 ++++++++++++++++ Gemini-iOS/ContentView.swift | 168 ++++++++++-------- Gemini-iOS/NavigationBar.swift | 48 +++++ Gemini-iOS/SceneDelegate.swift | 11 +- Gemini-iOS/ToolBar.swift | 81 +++++++++ Gemini.xcodeproj/project.pbxproj | 12 ++ .../xcschemes/Gemini-iOS.xcscheme | 90 ++++++++++ .../xcschemes/xcschememanagement.plist | 5 + GeminiRenderer/DocumentView.swift | 25 ++- 10 files changed, 509 insertions(+), 90 deletions(-) create mode 100644 Gemini-iOS/BrowserViewController.swift create mode 100644 Gemini-iOS/NavigationBar.swift create mode 100644 Gemini-iOS/ToolBar.swift create mode 100644 Gemini.xcodeproj/xcshareddata/xcschemes/Gemini-iOS.xcscheme diff --git a/BrowserCore/BrowserView.swift b/BrowserCore/BrowserView.swift index 30b182f..e2735b4 100644 --- a/BrowserCore/BrowserView.swift +++ b/BrowserCore/BrowserView.swift @@ -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) } } diff --git a/Gemini-iOS/BrowserViewController.swift b/Gemini-iOS/BrowserViewController.swift new file mode 100644 index 0000000..e6a38e2 --- /dev/null +++ b/Gemini-iOS/BrowserViewController.swift @@ -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! + private var navBarHost: UIHostingController! + private var toolBarHost: UIHostingController! + + 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() + } + } + +} diff --git a/Gemini-iOS/ContentView.swift b/Gemini-iOS/ContentView.swift index 4475321..107344b 100644 --- a/Gemini-iOS/ContentView.swift +++ b/Gemini-iOS/ContentView.swift @@ -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: {}) diff --git a/Gemini-iOS/NavigationBar.swift b/Gemini-iOS/NavigationBar.swift new file mode 100644 index 0000000..cc84586 --- /dev/null +++ b/Gemini-iOS/NavigationBar.swift @@ -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")!)) + } +} diff --git a/Gemini-iOS/SceneDelegate.swift b/Gemini-iOS/SceneDelegate.swift index 811ebf1..850a907 100644 --- a/Gemini-iOS/SceneDelegate.swift +++ b/Gemini-iOS/SceneDelegate.swift @@ -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() } diff --git a/Gemini-iOS/ToolBar.swift b/Gemini-iOS/ToolBar.swift new file mode 100644 index 0000000..e47f645 --- /dev/null +++ b/Gemini-iOS/ToolBar.swift @@ -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: {}) + } +} diff --git a/Gemini.xcodeproj/project.pbxproj b/Gemini.xcodeproj/project.pbxproj index 5642b16..0a8b7d1 100644 --- a/Gemini.xcodeproj/project.pbxproj +++ b/Gemini.xcodeproj/project.pbxproj @@ -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 = ""; }; D691A64D25217C6F00348C4B /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; D691A66625217FD800348C4B /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; + D691A6762522382E00348C4B /* BrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserViewController.swift; sourceTree = ""; }; + D691A68625223A4600348C4B /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; + D691A69F252242FC00348C4B /* ToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolBar.swift; sourceTree = ""; }; D69F00AB24BE9DD300E37622 /* GeminiDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiDataTask.swift; sourceTree = ""; }; D69F00AD24BEA29100E37622 /* GeminiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiResponse.swift; sourceTree = ""; }; D69F00AF24BEA84D00E37622 /* NavigationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationManager.swift; sourceTree = ""; }; @@ -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 */, ); diff --git a/Gemini.xcodeproj/xcshareddata/xcschemes/Gemini-iOS.xcscheme b/Gemini.xcodeproj/xcshareddata/xcschemes/Gemini-iOS.xcscheme new file mode 100644 index 0000000..86381b6 --- /dev/null +++ b/Gemini.xcodeproj/xcshareddata/xcschemes/Gemini-iOS.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist b/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist index a01464b..904be0e 100644 --- a/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Gemini.xcodeproj/xcuserdata/shadowfacts.xcuserdatad/xcschemes/xcschememanagement.plist @@ -62,6 +62,11 @@ primary + D6E152A124BFFDF500FDF9D3 + + primary + + diff --git a/GeminiRenderer/DocumentView.swift b/GeminiRenderer/DocumentView.swift index 070799b..35b048b 100644 --- a/GeminiRenderer/DocumentView.swift +++ b/GeminiRenderer/DocumentView.swift @@ -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 {