// // BrowserView.swift // SBrowser // // Created by James Magahern on 7/21/20. // import Combine import UIKit import WebKit class BrowserView: UIView { let titlebarView = TitlebarView() var findOnPageView: FindOnPageView? { didSet { addSubview(findOnPageView!) } } var toolbarView: ToolbarView? { didSet { addSubview(toolbarView!) } } var tabBarView: TabBarView? { didSet { addSubview(tabBarView!) } } var tabBarViewVisible: Bool = true { didSet { setNeedsLayout() } } var autocompleteView: UICollectionView? { didSet { addSubview(autocompleteView!) if let toolbarView = toolbarView { bringSubviewToFront(toolbarView) } } } var webView: WKWebView? { didSet { oldValue?.removeFromSuperview() if let toolbarView = toolbarView { insertSubview(webView!, belowSubview: toolbarView) } else { addSubview(webView!) } } } 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() } } convenience init() { self.init(frame: .zero) backgroundColor = .systemBackground addSubview(titlebarView) keyboardWillShowObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillShowNotification).sink { [weak self] notification in self?.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!) } keyboardWillHideObserver = NotificationCenter.default.publisher(for: UIWindow.keyboardWillHideNotification).sink { [weak self] notification in self?.adjustOffsetForKeyboardNotification(userInfo: notification.userInfo!) } } private func adjustOffsetForKeyboardNotification(userInfo: [AnyHashable : Any]) { guard var keyboardEndFrame = userInfo[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect else { return } guard let animationDuration = userInfo[UIWindow.keyboardAnimationDurationUserInfoKey] as? TimeInterval else { return } guard let animationCurve = userInfo[UIWindow.keyboardAnimationCurveUserInfoKey] as? Int else { return } // Convert keyboard frame to screen coordinate space keyboardEndFrame = self.convert(keyboardEndFrame, from: UIScreen.main.coordinateSpace) let animationOptions: UIView.AnimationOptions = { curve -> UIView.AnimationOptions in switch UIView.AnimationCurve(rawValue: curve) { case .easeIn: return .curveEaseIn case .easeOut: return .curveEaseOut case .easeInOut: return .curveEaseInOut default: return .init() } }(animationCurve) keyboardLayoutOffset = bounds.height - keyboardEndFrame.minY UIView.animate(withDuration: animationDuration, delay: 0.0, options: animationOptions, animations: { self.layoutIfNeeded() }, completion: nil) } override func layoutSubviews() { super.layoutSubviews() var webViewContentInset = UIEdgeInsets() bringSubviewToFront(titlebarView) var titlebarHeight: CGFloat = 24.0 titlebarHeight += safeAreaInsets.top titlebarView.frame = CGRect(origin: .zero, size: CGSize(width: bounds.width, height: titlebarHeight)) webViewContentInset.top += titlebarView.frame.height if let toolbarView = toolbarView { var toolbarSize = toolbarView.sizeThatFits(bounds.size) // Compact: toolbar is at the bottom if traitCollection.horizontalSizeClass == .compact { var bottomOffset: CGFloat = 0.0 if keyboardLayoutOffset < CGFloat.leastNonzeroMagnitude { toolbarSize.height += safeAreaInsets.bottom } else if toolbarView.urlBar?.textField.isFirstResponder ?? false { bottomOffset = keyboardLayoutOffset } 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) webViewContentInset.top += toolbarView.frame.height } } if let tabBarView = tabBarView { bringSubviewToFront(tabBarView) if tabBarViewVisible { tabBarView.isHidden = false let tabViewSize = tabBarView.sizeThatFits(bounds.size) tabBarView.frame = CGRect( x: 0.0, y: webViewContentInset.top, width: bounds.width, height: tabViewSize.height ) webViewContentInset.top += tabBarView.frame.height } else { tabBarView.isHidden = true } } // Fix web view content insets if let webView = webView { webView.scrollView.layer.masksToBounds = true webView.frame = bounds.inset(by: webViewContentInset) } // Autocomplete view if let autocompleteView = autocompleteView { // Compact: autocomplete view takes the space of the webview autocompleteView.frame = bounds.inset(by: webViewContentInset) autocompleteView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardLayoutOffset, right: 0) autocompleteView.scrollIndicatorInsets = autocompleteView.contentInset if traitCollection.horizontalSizeClass == .regular { // Regular: shows up just underneath the url bar autocompleteView.layer.shadowColor = UIColor.black.cgColor autocompleteView.layer.shadowOffset = CGSize(width: 0.0, height: 1.0) autocompleteView.layer.shadowRadius = 8.0 autocompleteView.layer.shadowOpacity = 0.6 autocompleteView.layer.cornerRadius = 8.0 autocompleteView.layer.masksToBounds = true let shadowPath = UIBezierPath(roundedRect: autocompleteView.bounds, cornerRadius: autocompleteView.layer.cornerRadius) autocompleteView.layer.shadowPath = shadowPath.cgPath if let toolbarView = toolbarView, let urlBar = toolbarView.urlBar { var yOffset = toolbarView.frame.maxY + 3.0 if let tabBarView = tabBarView, tabBarViewVisible { yOffset += tabBarView.frame.height } let urlFrame = self.convert(urlBar.frame, from: urlBar.superview) autocompleteView.frame = CGRect( x: urlFrame.minX, y: yOffset, width: urlFrame.width, height: bounds.height / 2.5 ) } } else { autocompleteView.layer.cornerRadius = 0.0 autocompleteView.layer.shadowOpacity = 0.0 } } // 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) } } } }