Implemented find on page UI
This commit is contained in:
@@ -65,6 +65,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
title: "Next Tab",
|
||||
action: #selector(ShortcutResponder.nextTab)
|
||||
),
|
||||
|
||||
// Find on page
|
||||
UIKeyCommand(
|
||||
modifiers: [.command], input: "F",
|
||||
title: "Find on Page",
|
||||
action: #selector(ShortcutResponder.findOnPage)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ class BrowserView: UIView
|
||||
{
|
||||
let titlebarView = TitlebarView()
|
||||
|
||||
var findOnPageView: FindOnPageView? {
|
||||
didSet { addSubview(findOnPageView!) }
|
||||
}
|
||||
|
||||
var toolbarView: ToolbarView? {
|
||||
didSet { addSubview(toolbarView!) }
|
||||
}
|
||||
@@ -38,6 +42,18 @@ class BrowserView: UIView
|
||||
}
|
||||
}
|
||||
|
||||
var findOnPageVisible: Bool = false {
|
||||
didSet { layoutSubviews() }
|
||||
}
|
||||
|
||||
func setFindOnPageVisible(_ visible: Bool, animated: Bool) {
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.33, animations: { self.findOnPageVisible = visible })
|
||||
} else {
|
||||
findOnPageVisible = visible
|
||||
}
|
||||
}
|
||||
|
||||
var keyboardWillShowObserver: AnyCancellable?
|
||||
var keyboardWillHideObserver: AnyCancellable?
|
||||
var keyboardLayoutOffset: CGFloat = 0 { didSet { setNeedsLayout() } }
|
||||
@@ -101,6 +117,11 @@ class BrowserView: UIView
|
||||
toolbarView.bounds = CGRect(origin: .zero, size: toolbarSize)
|
||||
toolbarView.center = CGPoint(x: bounds.center.x, y: bounds.maxY - (toolbarView.bounds.height / 2) - bottomOffset)
|
||||
webViewContentInset.bottom += toolbarView.frame.height
|
||||
|
||||
if findOnPageVisible {
|
||||
// Hide off the bottom
|
||||
toolbarView.center = CGPoint(x: toolbarView.center.x, y: toolbarView.center.y + toolbarView.frame.height)
|
||||
}
|
||||
} else {
|
||||
// Regular: toolbar is at the top
|
||||
toolbarView.frame = CGRect(origin: CGPoint(x: 0.0, y: titlebarView.frame.maxY), size: toolbarSize)
|
||||
@@ -139,5 +160,25 @@ class BrowserView: UIView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find on page view
|
||||
if let findOnPageView = findOnPageView {
|
||||
var bottomOffset: CGFloat = 0.0
|
||||
var findOnPageSize = CGSize(width: bounds.width, height: 54.0)
|
||||
if keyboardLayoutOffset == 0 {
|
||||
findOnPageSize.height += safeAreaInsets.bottom
|
||||
} else if findOnPageView.textField.isFirstResponder {
|
||||
bottomOffset = keyboardLayoutOffset
|
||||
}
|
||||
|
||||
findOnPageView.bounds = CGRect(origin: .zero, size: findOnPageSize)
|
||||
findOnPageView.center = CGPoint(x: bounds.center.x, y: bounds.maxY - (findOnPageView.bounds.height / 2) - bottomOffset)
|
||||
bringSubviewToFront(findOnPageView)
|
||||
|
||||
if !findOnPageVisible {
|
||||
// Hide off the bottom
|
||||
findOnPageView.center = CGPoint(x: findOnPageView.center.x, y: findOnPageView.center.y + findOnPageView.frame.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
|
||||
private let tabController = TabController()
|
||||
private let toolbarController = ToolbarViewController()
|
||||
private let findOnPageController = FindOnPageViewController()
|
||||
|
||||
private let autocompleteViewController = AutocompleteViewController()
|
||||
private let redirectRules = PersonalRedirectRules()
|
||||
@@ -41,6 +42,8 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
addChild(toolbarController)
|
||||
addChild(findOnPageController)
|
||||
|
||||
didChangeTab(tab)
|
||||
}
|
||||
|
||||
@@ -48,6 +51,7 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
|
||||
override func loadView() {
|
||||
browserView.toolbarView = toolbarController.toolbarView
|
||||
browserView.findOnPageView = findOnPageController.findOnPageView
|
||||
|
||||
// Refresh button
|
||||
toolbarController.urlBar.refreshButton.addAction(UIAction(handler: { [unowned self] action in
|
||||
@@ -198,9 +202,19 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
label.text = numberFormatter.string(for: tab.webView._viewScale)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
documentControls.findOnPageControlView.addAction(UIAction(handler: { [unowned self] _ in
|
||||
documentControls.dismiss(animated: true, completion: nil)
|
||||
browserView.setFindOnPageVisible(true, animated: true)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
present(documentControls, animated: true, completion: nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Find on page dismiss
|
||||
findOnPageController.findOnPageView.doneButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
browserView.setFindOnPageVisible(false, animated: true)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
self.view = browserView
|
||||
}
|
||||
|
||||
@@ -234,6 +248,7 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
|
||||
// Change webView
|
||||
browserView.webView = webView
|
||||
findOnPageController.webView = webView
|
||||
|
||||
// Autocomplete view
|
||||
browserView.autocompleteView = autocompleteViewController.view
|
||||
@@ -552,4 +567,9 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func findOnPage(_ sender: Any?) {
|
||||
browserView.setFindOnPageVisible(true, animated: true)
|
||||
findOnPageController.findOnPageView.textField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import UIKit
|
||||
|
||||
class ReliefButton: UIButton
|
||||
{
|
||||
public var constrainedToSquare = true
|
||||
|
||||
internal let shadowView = UIView(frame: .zero)
|
||||
internal let backgroundView = GradientView(direction: .vertical, colors: ReliefButton.gradientColors(inverted: false, darkMode: false))
|
||||
|
||||
@@ -140,9 +142,14 @@ class ReliefButton: UIButton
|
||||
sendSubviewToBack(backgroundView)
|
||||
sendSubviewToBack(shadowView)
|
||||
|
||||
let backgroundDimension = bounds.height
|
||||
backgroundView.frame = CGRect(origin: .zero, size: CGSize(width: backgroundDimension, height: backgroundDimension))
|
||||
backgroundView.frame = backgroundView.frame.centeredX(inRect: bounds)
|
||||
if constrainedToSquare {
|
||||
let backgroundDimension = bounds.height
|
||||
backgroundView.frame = CGRect(origin: .zero, size: CGSize(width: backgroundDimension, height: backgroundDimension))
|
||||
backgroundView.frame = backgroundView.frame.centeredX(inRect: bounds)
|
||||
} else {
|
||||
backgroundView.frame = bounds
|
||||
}
|
||||
|
||||
shadowView.frame = backgroundView.frame
|
||||
}
|
||||
}
|
||||
|
||||
87
App/Document Controls UI/DocumentControlView.swift
Normal file
87
App/Document Controls UI/DocumentControlView.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// DocumentControlView.swift
|
||||
// App
|
||||
//
|
||||
// Created by James Magahern on 9/30/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class DocumentControlView: UIControl
|
||||
{
|
||||
static public let controlHeight = CGFloat(48.0)
|
||||
|
||||
let imageView = UIImageView(frame: .zero)
|
||||
let label = UILabel(frame: .zero)
|
||||
|
||||
var drawsBottomSeparator: Bool = false {
|
||||
didSet { setNeedsLayout() }
|
||||
}
|
||||
|
||||
internal let highlightView = UIView(frame: .zero)
|
||||
internal let separatorView = UIView(frame: .zero)
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
addSubview(highlightView)
|
||||
addSubview(imageView)
|
||||
addSubview(label)
|
||||
addSubview(separatorView)
|
||||
|
||||
tintColor = .label
|
||||
|
||||
label.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
label.textAlignment = .center
|
||||
|
||||
imageView.contentMode = .center
|
||||
|
||||
separatorView.backgroundColor = .secondarySystemFill
|
||||
highlightView.backgroundColor = .secondarySystemFill
|
||||
|
||||
highlightView.isHidden = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
CGSize(width: size.width, height: Self.controlHeight)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
highlightView.frame = bounds
|
||||
|
||||
let padding: CGFloat = 18.0
|
||||
let imageSize: CGFloat = 24.0
|
||||
let bounds = self.bounds.inset(by: layoutMargins)
|
||||
imageView.frame = CGRect(
|
||||
x: bounds.minX, y: 0.0,
|
||||
width: imageSize, height: imageSize
|
||||
).centeredY(inRect: self.bounds)
|
||||
|
||||
label.frame = CGRect(
|
||||
x: imageView.frame.maxX + padding, y: bounds.minY,
|
||||
width: bounds.width - imageView.frame.maxX - padding, height: bounds.height
|
||||
)
|
||||
|
||||
let separatorHeight: CGFloat = 1.0
|
||||
if drawsBottomSeparator {
|
||||
separatorView.isHidden = false
|
||||
separatorView.frame = CGRect(
|
||||
x: self.bounds.minX, y: self.bounds.height - separatorHeight,
|
||||
width: self.bounds.width, height: separatorHeight
|
||||
)
|
||||
} else {
|
||||
separatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
override func setTracking(_ tracking: Bool) {
|
||||
super.setTracking(tracking)
|
||||
highlightView.isHidden = !tracking
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,20 @@ class DocumentControlViewController: UIViewController
|
||||
{
|
||||
let documentControlView = StackView(dimension: .vertical)
|
||||
let fontSizeAdjustView = FontSizeAdjustView()
|
||||
let findOnPageControlView = DocumentControlView()
|
||||
|
||||
static public let preferredWidth = CGFloat(200.0)
|
||||
static public let controlHeight = CGFloat(48.0)
|
||||
|
||||
convenience init() {
|
||||
self.init(nibName: nil, bundle: nil)
|
||||
|
||||
findOnPageControlView.label.text = "Find On Page"
|
||||
findOnPageControlView.imageView.image = UIImage(systemName: "magnifyingglass")
|
||||
|
||||
fontSizeAdjustView.drawsBottomSeparator = true
|
||||
|
||||
documentControlView.addArrangedSubview(fontSizeAdjustView)
|
||||
documentControlView.addArrangedSubview(findOnPageControlView)
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class FontSizeAdjustView: UIView
|
||||
class FontSizeAdjustView: DocumentControlView
|
||||
{
|
||||
let decreaseSizeButton = UIButton(frame: .zero)
|
||||
let increaseSizeButton = UIButton(frame: .zero)
|
||||
let labelView = UILabel(frame: .zero)
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
labelView.textColor = .secondaryLabel
|
||||
labelView.textAlignment = .center
|
||||
@@ -30,13 +30,15 @@ class FontSizeAdjustView: UIView
|
||||
addSubview(labelView)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
CGSize(width: size.width, height: DocumentControlViewController.controlHeight)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
highlightView.isHidden = true
|
||||
|
||||
decreaseSizeButton.frame = CGRect(
|
||||
x: 0.0, y: 0.0,
|
||||
width: bounds.height,
|
||||
|
||||
71
App/Find on Page/FindOnPageView.swift
Normal file
71
App/Find on Page/FindOnPageView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// FindOnPageView.swift
|
||||
// App
|
||||
//
|
||||
// Created by James Magahern on 9/30/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class FindOnPageView: UIView
|
||||
{
|
||||
let textField = UISearchTextField(frame: .zero)
|
||||
|
||||
let doneButton = ReliefButton()
|
||||
let nextResultButton = ReliefButton()
|
||||
let prevResultButton = ReliefButton()
|
||||
|
||||
private let arrowControls = SegmentedReliefButton(children: [])
|
||||
|
||||
let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
|
||||
arrowControls.children = [ prevResultButton, nextResultButton ]
|
||||
|
||||
addSubview(backgroundView)
|
||||
addSubview(textField)
|
||||
addSubview(doneButton)
|
||||
addSubview(arrowControls)
|
||||
|
||||
textField.autocapitalizationType = .none
|
||||
textField.autocorrectionType = .no
|
||||
|
||||
doneButton.setTitle("Done", for: .normal)
|
||||
doneButton.setTitleColor(.label, for: .normal)
|
||||
doneButton.constrainedToSquare = false
|
||||
|
||||
nextResultButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
|
||||
prevResultButton.setImage(UIImage(systemName: "chevron.up"), for: .normal)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
backgroundView.frame = bounds
|
||||
|
||||
let bounds = self.bounds
|
||||
.insetBy(dx: 8.0, dy: 8.0)
|
||||
.inset(by: safeAreaInsets)
|
||||
|
||||
let doneButtonPadding: CGFloat = 8.0
|
||||
let doneButtonSize = doneButton.sizeThatFits(bounds.size)
|
||||
doneButton.frame = CGRect(
|
||||
x: bounds.width - doneButtonSize.width, y: bounds.minY,
|
||||
width: doneButtonSize.width + doneButtonPadding, height: bounds.height
|
||||
)
|
||||
|
||||
let arrowControlsSize = arrowControls.sizeThatFits(bounds.size)
|
||||
arrowControls.frame = CGRect(
|
||||
x: doneButton.frame.minX - doneButtonPadding - arrowControlsSize.width, y: bounds.minY,
|
||||
width: arrowControlsSize.width, height: bounds.height
|
||||
)
|
||||
|
||||
textField.frame = CGRect(
|
||||
x: bounds.minX, y: bounds.minY,
|
||||
width: arrowControls.frame.minX - bounds.minX - doneButtonPadding,
|
||||
height: bounds.height
|
||||
)
|
||||
}
|
||||
}
|
||||
98
App/Find on Page/FindOnPageViewController.swift
Normal file
98
App/Find on Page/FindOnPageViewController.swift
Normal file
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// FindOnPageViewController.swift
|
||||
// App
|
||||
//
|
||||
// Created by James Magahern on 9/30/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class FindOnPageViewController: UIViewController, _WKFindDelegate
|
||||
{
|
||||
let findOnPageView = FindOnPageView()
|
||||
weak var webView: WKWebView? {
|
||||
didSet { webView?._findDelegate = self }
|
||||
}
|
||||
|
||||
private var findString: String?
|
||||
private let findOptions: _WKFindOptions = [
|
||||
.caseInsensitive,
|
||||
.atWordStarts,
|
||||
.treatMedialCapitalAsWordStart,
|
||||
.wrapAround,
|
||||
.showFindIndicator,
|
||||
.showOverlay,
|
||||
.showHighlight,
|
||||
.determineMatchIndex,
|
||||
]
|
||||
|
||||
private let maxCount: UInt = 1000
|
||||
|
||||
convenience init() {
|
||||
self.init(nibName: nil, bundle: nil)
|
||||
|
||||
findOnPageView.textField.addAction(UIAction(handler: { [unowned self] _ in
|
||||
self.findString = findOnPageView.textField.text
|
||||
webView?._find(self.findString, options: self.findOptions, maxCount: self.maxCount)
|
||||
}), for: .editingChanged)
|
||||
|
||||
findOnPageView.prevResultButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
findPrevious(nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
findOnPageView.nextResultButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
findNext(nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
findOnPageView.doneButton.addAction(UIAction(handler: { [unowned self] _ in
|
||||
doneFinding(nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
// Escape to cancel
|
||||
addKeyCommand(UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(Self.doneFinding)))
|
||||
|
||||
// Return goes to next
|
||||
addKeyCommand(UIKeyCommand(input: "\n", modifierFlags: [], action: #selector(Self.findNext)))
|
||||
|
||||
// Cmd+G next
|
||||
addKeyCommand(UIKeyCommand(input: "G", modifierFlags: [.command], action: #selector(Self.findNext)))
|
||||
|
||||
// Shift return goes to prev
|
||||
addKeyCommand(UIKeyCommand(input: "\n", modifierFlags: [.shift], action: #selector(Self.findPrevious)))
|
||||
|
||||
// Shift+Cmd+G prev
|
||||
addKeyCommand(UIKeyCommand(input: "G", modifierFlags: [.command, .shift], action: #selector(Self.findPrevious)))
|
||||
|
||||
self.view = findOnPageView
|
||||
}
|
||||
|
||||
@objc
|
||||
func doneFinding(_ sender: Any?) {
|
||||
findOnPageView.textField.resignFirstResponder()
|
||||
webView?._hideFindUI()
|
||||
}
|
||||
|
||||
@objc
|
||||
func findNext(_ sender: Any?) {
|
||||
webView?._find(self.findString, options: self.findOptions, maxCount: self.maxCount)
|
||||
}
|
||||
|
||||
@objc
|
||||
func findPrevious(_ sender: Any?) {
|
||||
let options: _WKFindOptions = self.findOptions.union(.backwards)
|
||||
webView?._find(self.findString, options: options, maxCount: self.maxCount)
|
||||
}
|
||||
|
||||
func _webView(_ webView: WKWebView!, didFailToFind string: String!) {
|
||||
// ??
|
||||
}
|
||||
|
||||
func _webView(_ webView: WKWebView!, didCountMatches matches: UInt, for string: String!) {
|
||||
// TODO: Update a label
|
||||
}
|
||||
|
||||
func _webView(_ webView: WKWebView!, didFindMatches matches: UInt, for string: String!, withMatch matchIndex: Int) {
|
||||
findOnPageView.nextResultButton.isEnabled = matches > 0
|
||||
findOnPageView.prevResultButton.isEnabled = matches > 0
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,7 @@ protocol ShortcutResponder: class {
|
||||
|
||||
@objc
|
||||
optional func nextTab(_ sender: Any?)
|
||||
|
||||
@objc
|
||||
optional func findOnPage(_ sender: Any?)
|
||||
}
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
// SPI
|
||||
#import <UIKit/UITextField_Private.h>
|
||||
#import <WebKit/WKWebViewPrivate.h>
|
||||
#import <WebKit/_WKFindDelegate.h>
|
||||
|
||||
Reference in New Issue
Block a user