import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import ComponentFlow import TelegramPresentationData import AccountContext import BundleIconComponent final class SearchBarContentComponent: Component { public typealias EnvironmentType = BrowserNavigationBarEnvironment let theme: PresentationTheme let strings: PresentationStrings let performAction: ActionSlot init( theme: PresentationTheme, strings: PresentationStrings, performAction: ActionSlot ) { self.theme = theme self.strings = strings self.performAction = performAction } static func ==(lhs: SearchBarContentComponent, rhs: SearchBarContentComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } return true } final class View: UIView, UITextFieldDelegate { private final class SearchTextField: UITextField { override func textRect(forBounds bounds: CGRect) -> CGRect { return bounds.integral } } private struct Params: Equatable { var theme: PresentationTheme var strings: PresentationStrings var size: CGSize static func ==(lhs: Params, rhs: Params) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings !== rhs.strings { return false } if lhs.size != rhs.size { return false } return true } } private let queryPromise = ValuePromise() private var queryDisposable: Disposable? private let backgroundLayer: SimpleLayer private let iconView: UIImageView private let clearIconView: UIImageView private let clearIconButton: HighlightTrackingButton private let cancelButtonTitle: ComponentView private let cancelButton: HighlightTrackingButton private var placeholderContent = ComponentView() private var textFrame: CGRect? private var textField: SearchTextField? private var tapRecognizer: UITapGestureRecognizer? private var params: Params? private var component: SearchBarContentComponent? init() { self.backgroundLayer = SimpleLayer() self.iconView = UIImageView() self.clearIconView = UIImageView() self.clearIconButton = HighlightableButton() self.clearIconView.isHidden = true self.clearIconButton.isHidden = true self.cancelButtonTitle = ComponentView() self.cancelButton = HighlightTrackingButton() super.init(frame: CGRect()) self.layer.addSublayer(self.backgroundLayer) self.addSubview(self.iconView) self.addSubview(self.clearIconView) self.addSubview(self.clearIconButton) self.addSubview(self.cancelButton) self.clipsToBounds = true let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.tapRecognizer = tapRecognizer self.addGestureRecognizer(tapRecognizer) self.cancelButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { cancelButtonTitleView.layer.removeAnimation(forKey: "opacity") cancelButtonTitleView.alpha = 0.4 } } else { if let cancelButtonTitleView = strongSelf.cancelButtonTitle.view { cancelButtonTitleView.alpha = 1.0 cancelButtonTitleView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } } self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), for: .touchUpInside) self.clearIconButton.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.clearIconView.layer.removeAnimation(forKey: "opacity") strongSelf.clearIconView.alpha = 0.4 } else { strongSelf.clearIconView.alpha = 1.0 strongSelf.clearIconView.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } self.clearIconButton.addTarget(self, action: #selector(self.clearPressed), for: .touchUpInside) let throttledSearchQuery = self.queryPromise.get() |> mapToSignal { query -> Signal in if !query.isEmpty { return (.complete() |> delay(0.6, queue: Queue.mainQueue())) |> then(.single(query)) } else { return .single(query) } } self.queryDisposable = (throttledSearchQuery |> deliverOnMainQueue).start(next: { [weak self] query in if let self { self.component?.performAction.invoke(.updateSearchQuery(query)) } }) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.activateTextInput() } } private func activateTextInput() { if self.textField == nil, let textFrame = self.textFrame { let backgroundFrame = self.backgroundLayer.frame let textFieldFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textFrame.minX - 32.0, height: backgroundFrame.height)) let textField = SearchTextField(frame: textFieldFrame) textField.clipsToBounds = true textField.autocorrectionType = .no textField.returnKeyType = .search self.textField = textField self.insertSubview(textField, belowSubview: self.clearIconView) textField.delegate = self textField.addTarget(self, action: #selector(self.textFieldChanged(_:)), for: .editingChanged) } guard !(self.textField?.isFirstResponder ?? false) else { return } self.textField?.becomeFirstResponder() } @objc private func cancelPressed() { self.clearIconView.isHidden = true self.clearIconButton.isHidden = true let textField = self.textField self.textField = nil self.component?.performAction.invoke(.updateSearchActive(false)) if let textField { textField.resignFirstResponder() textField.removeFromSuperview() } } @objc private func clearPressed() { guard let textField = self.textField else { return } textField.text = "" self.textFieldChanged(textField) } func deactivate() { if let text = self.textField?.text, !text.isEmpty { self.textField?.endEditing(true) } else { self.cancelPressed() } } public func textFieldDidBeginEditing(_ textField: UITextField) { } public func textFieldDidEndEditing(_ textField: UITextField) { } public func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.endEditing(true) return false } @objc private func textFieldChanged(_ textField: UITextField) { let text = textField.text ?? "" self.clearIconView.isHidden = text.isEmpty self.clearIconButton.isHidden = text.isEmpty self.placeholderContent.view?.isHidden = !text.isEmpty self.queryPromise.set(text) if let params = self.params { self.update(theme: params.theme, strings: params.strings, size: params.size, transition: .immediate) } } func update(component: SearchBarContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize { self.component = component self.update(theme: component.theme, strings: component.strings, size: availableSize, transition: transition) self.activateTextInput() return availableSize } public func update(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ComponentTransition) { let params = Params( theme: theme, strings: strings, size: size ) if self.params == params { return } let isActiveWithText = true if self.params?.theme !== theme { self.iconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate) self.iconView.tintColor = theme.rootController.navigationSearchBar.inputIconColor self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate) self.clearIconView.tintColor = theme.rootController.navigationSearchBar.inputClearButtonColor } self.params = params let sideInset: CGFloat = 10.0 let inputHeight: CGFloat = 36.0 let topInset: CGFloat = (size.height - inputHeight) / 2.0 let sideTextInset: CGFloat = sideInset + 4.0 + 17.0 self.backgroundLayer.backgroundColor = theme.rootController.navigationSearchBar.inputFillColor.cgColor self.backgroundLayer.cornerRadius = 10.5 let cancelTextSize = self.cancelButtonTitle.update( transition: .immediate, component: AnyComponent(Text( text: strings.Common_Cancel, font: Font.regular(17.0), color: theme.rootController.navigationBar.accentTextColor )), environment: {}, containerSize: CGSize(width: size.width - 32.0, height: 100.0) ) let cancelButtonSpacing: CGFloat = 8.0 var backgroundFrame = CGRect(origin: CGPoint(x: sideInset, y: topInset), size: CGSize(width: size.width - sideInset * 2.0, height: inputHeight)) if isActiveWithText { backgroundFrame.size.width -= cancelTextSize.width + cancelButtonSpacing } transition.setFrame(layer: self.backgroundLayer, frame: backgroundFrame) transition.setFrame(view: self.cancelButton, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX, y: 0.0), size: CGSize(width: cancelButtonSpacing + cancelTextSize.width, height: size.height))) let textX: CGFloat = backgroundFrame.minX + sideTextInset let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height)) self.textFrame = textFrame if let image = self.iconView.image { let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + 5.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) transition.setFrame(view: self.iconView, frame: iconFrame) } let placeholderSize = self.placeholderContent.update( transition: transition, component: AnyComponent( Text(text: strings.Common_Search, font: Font.regular(17.0), color: theme.rootController.navigationSearchBar.inputPlaceholderTextColor) ), environment: {}, containerSize: size ) if let placeholderContentView = self.placeholderContent.view { if placeholderContentView.superview == nil { self.addSubview(placeholderContentView) } let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: backgroundFrame.midY - placeholderSize.height / 2.0), size: placeholderSize) transition.setFrame(view: placeholderContentView, frame: placeholderContentFrame) } if let image = self.clearIconView.image { let iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size) transition.setFrame(view: self.clearIconView, frame: iconFrame) transition.setFrame(view: self.clearIconButton, frame: iconFrame.insetBy(dx: -8.0, dy: -10.0)) } if let cancelButtonTitleComponentView = self.cancelButtonTitle.view { if cancelButtonTitleComponentView.superview == nil { self.addSubview(cancelButtonTitleComponentView) cancelButtonTitleComponentView.isUserInteractionEnabled = false } transition.setFrame(view: cancelButtonTitleComponentView, frame: CGRect(origin: CGPoint(x: backgroundFrame.maxX + cancelButtonSpacing, y: floor((size.height - cancelTextSize.height) / 2.0)), size: cancelTextSize)) } if let textField = self.textField { textField.textColor = theme.rootController.navigationSearchBar.inputTextColor transition.setFrame(view: textField, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideTextInset, y: backgroundFrame.minY - UIScreenPixel), size: CGSize(width: backgroundFrame.width - sideTextInset - 32.0, height: backgroundFrame.height))) } } } func makeView() -> View { return View() } func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } }