Home telegram - searchBarTextField
Post
Cancel

telegram - searchBarTextField

SearchBarTextField

class SearchBarTextField: UITextField

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public var didDeleteBackwardWhileEmpty: (() -> Void)?

// placeholder
let placeholderLabel: ASTextNode
var placeholderString: NSAttributedString? {
    didSet {
        self.placeholderLabel.attributedText = self.placeholderString
    }
}

// prefix
let prefixLabel: ASTextNode
var prefixString: NSAttributedString? {
    didSet {
        self.prefixLabel.attributedText = self.prefixString
    }
}

override init(frame: CGRect) {
    self.placeholderLabel = ASTextNode()
    self.placeholderLabel.isUserInteractionEnabled = false
    self.placeholderLabel.displaysAsynchronously = false
    self.placeholderLabel.maximumNumberOfLines = 1
    self.placeholderLabel.truncationMode = .byTruncatingTail
    
    self.prefixLabel = ASTextNode()
    self.prefixLabel.isUserInteractionEnabled = false
    self.prefixLabel.displaysAsynchronously = false
    self.prefixLabel.truncationMode = .byTruncatingTail
    
    super.init(frame: frame)
    // view hierarchy
    self.addSubnode(self.placeholderLabel)
    self.addSubnode(self.prefixLabel)
}

// keyboardAppearance
override var keyboardAppearance: UIKeyboardAppearance {
    get {
        return super.keyboardAppearance
    }
    set {
        let resigning = self.isFirstResponder
        if resigning {
            self.resignFirstResponder()
        }
        super.keyboardAppearance = newValue
        if resigning {
            self.becomeFirstResponder()
        }
    }
}

// override textRext
override func textRect(forBounds bounds: CGRect) -> CGRect {
    if bounds.size.width.isZero {
        return CGRect(origin: CGPoint(), size: CGSize())
    }
    var rect = bounds.insetBy(dx: 4.0, dy: 4.0)
    
    let prefixSize = self.prefixLabel.measure(bounds.size)
    if !prefixSize.width.isZero {
        let prefixOffset = prefixSize.width
        rect.origin.x += prefixOffset
        rect.size.width = max((rect.size.width - prefixOffset), 0)
    }
    return rect
}

override func layoutSubviews() {
    super.layoutSubviews()
    
    let bounds = self.bounds
    if bounds.size.width.isZero {
        return
    }
    
    var textOffset: CGFloat = 1.0
    if bounds.height >= 36.0 {
        textOffset += 5.5
    }
    
    let textRect = self.textRect(forBounds: bounds)
    let labelSize = self.placeholderLabel.measure(textRect.size)
    self.placeholderLabel.frame = CGRect(origin: CGPoint(x: textRect.minX, y: textRect.minY + textOffset), size: labelSize)
    
    let prefixSize = self.prefixLabel.measure(bounds.size)
    let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0)
    self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + textOffset), size: prefixSize)
}

// override delete backward
override func deleteBackward() {
    if self.text == nil || self.text!.isEmpty {
        self.didDeleteBackwardWhileEmpty?()
    }
    super.deleteBackward()
}

SearchBarNode

class SearchBarNode: ASDisplayNode, UITextFieldDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
public var cancel: (() -> Void)?
public var textUpdated: ((String) -> Void)?
public var textReturned: ((String) -> Void)?
public var clearPrefix: (() -> Void)?

private let backgroundNode: ASDisplayNode
private let separatorNode: ASDisplayNode
private let textBackgroundNode: ASDisplayNode
private var activityIndicator: ActivityIndicator?
private let iconNode: ASImageNode
private let textField: SearchBarTextField
private let clearButton: HighlightableButtonNode
private let cancelButton: HighlightableButtonNode

public var placeholderString: NSAttributedString? {
    get {
        return self.textField.placeholderString
    } set(value) {
        self.textField.placeholderString = value
    }
}

public var prefixString: NSAttributedString? {
    get {
        return self.textField.prefixString
    } set(value) {
        let previous = self.prefixString
        let updated: Bool
        if let previous = previous, let value = value {
            updated = !previous.isEqual(to: value)
        } else {
            updated = (previous != nil) != (value != nil)
        }
        if updated {
            self.textField.prefixString = value
            self.textField.setNeedsLayout()
            self.updateIsEmpty()
        }
    }
}

public var text: String {
    get {
        return self.textField.text ?? ""
    } set(value) {
        if self.textField.text ?? "" != value {
            self.textField.text = value
            self.textFieldDidChange(self.textField)
        }
    }
}

public var activity: Bool = false {
    didSet {
        if self.activity != oldValue {
            if self.activity {
                if self.activityIndicator == nil, let theme = self.theme {
                    let activityIndicator = ActivityIndicator(type: .custom(theme.inputIcon, 13.0, 1.0, false))
                    self.activityIndicator = activityIndicator
                    self.addSubnode(activityIndicator)
                    if let (boundingSize, leftInset, rightInset) = self.validLayout {
                        self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
                    }
                }
            } else if let activityIndicator = self.activityIndicator {
                self.activityIndicator = nil
                activityIndicator.removeFromSupernode()
            }
            self.iconNode.isHidden = self.activity
        }
    }
}

public var hasCancelButton: Bool = true {
    didSet {
        self.cancelButton.isHidden = !self.hasCancelButton
        if let (boundingSize, leftInset, rightInset) = self.validLayout {
            self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
        }
    }
}

private var validLayout: (CGSize, CGFloat, CGFloat)?

private let fieldStyle: SearchBarStyle
private var theme: SearchBarNodeTheme?
private var strings: PresentationStrings?

public init(theme: SearchBarNodeTheme, strings: PresentationStrings, fieldStyle: SearchBarStyle = .legacy) {
    self.fieldStyle = fieldStyle
    
    self.backgroundNode = ASDisplayNode()
    self.backgroundNode.isLayerBacked = true
    
    self.separatorNode = ASDisplayNode()
    self.separatorNode.isLayerBacked = true
    
    self.textBackgroundNode = ASDisplayNode()
    self.textBackgroundNode.isLayerBacked = false
    self.textBackgroundNode.displaysAsynchronously = false
    self.textBackgroundNode.cornerRadius = self.fieldStyle.cornerDiameter / 2.0
    
    self.iconNode = ASImageNode()
    self.iconNode.isLayerBacked = true
    self.iconNode.displaysAsynchronously = false
    self.iconNode.displayWithoutProcessing = true
    
    self.textField = SearchBarTextField()
    self.textField.autocorrectionType = .no
    self.textField.returnKeyType = .search
    self.textField.font = self.fieldStyle.font
    
    self.clearButton = HighlightableButtonNode()
    self.clearButton.imageNode.displaysAsynchronously = false
    self.clearButton.imageNode.displayWithoutProcessing = true
    self.clearButton.displaysAsynchronously = false
    self.clearButton.isHidden = true
    
    self.cancelButton = HighlightableButtonNode()
    self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
    self.cancelButton.displaysAsynchronously = false
    
    super.init()
    
  // build the view hierarchy
    self.addSubnode(self.backgroundNode)
    self.addSubnode(self.separatorNode)
    
    self.addSubnode(self.textBackgroundNode)
    self.view.addSubview(self.textField)
    self.addSubnode(self.iconNode)
    self.addSubnode(self.clearButton)
    self.addSubnode(self.cancelButton)
    
    self.textField.delegate = self
    self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
    
    self.textField.didDeleteBackwardWhileEmpty = { [weak self] in
        self?.clearPressed()
    }
    
    self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
    self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
    
    self.updateThemeAndStrings(theme: theme, strings: strings)
}


// should change characters
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if string.range(of: "\n") != nil {
        return false
    }
    return true
}

// should return
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    self.textField.resignFirstResponder()
    if let textReturned = self.textReturned {
        textReturned(textField.text ?? "")
    }
    return false
}

// text updated
@objc private func textFieldDidChange(_ textField: UITextField) {
    self.updateIsEmpty()
    if let textUpdated = self.textUpdated {
        textUpdated(textField.text ?? "")
    }
}

// select all
public func selectAll() {
    self.textField.becomeFirstResponder()
    self.textField.selectAll(nil)
}

// update empty
private func updateIsEmpty() {
    let isEmpty = !(self.textField.text?.isEmpty ?? true)
    if isEmpty != self.textField.placeholderLabel.isHidden {
        self.textField.placeholderLabel.isHidden = isEmpty
    }
    self.clearButton.isHidden = !isEmpty && self.prefixString == nil
}

// cancel
@objc private func cancelPressed() {
    if let cancel = self.cancel {
        cancel()
    }
}

// clear
@objc private func clearPressed() {
    if (self.textField.text?.isEmpty ?? true) {
        if self.prefixString != nil {
            self.clearPrefix?()
        }
    } else {
        self.textField.text = ""
        self.textFieldDidChange(self.textField)
    }
}

SearchDisplayControllerContentNode

classSearchDisplayControllerContentNode: ASDisplayNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public final var dismissInput: (() -> Void)?
public final var cancel: (() -> Void)?
public final var setQuery: ((NSAttributedString?, String) -> Void)?
public final var setPlaceholder: ((String) -> Void)?

open var isSearching: Signal<Bool, NoError> {
    return .single(false)
}

override public init() {
    super.init()
}

open func updatePresentationData(_ presentationData: PresentationData) {
}

open func searchTextUpdated(text: String) {
}

open func searchTextClearPrefix() {
}

open func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}

open func ready() -> Signal<Void, NoError> {
    return .single(Void())
}

open func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? {
    return nil
}

open func scrollToTop() {
}

SearchDisplayControllerMode

enum SearchDisplayControllerMode

1
2
case list
case navigation

SearchDisplayController

class SearchDisplayController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
private let searchBar: SearchBarNode
private let mode: SearchDisplayControllerMode
// contentNode承担重要任务(设置作为searchBar的事件代理),展示搜索结果集
public let contentNode: SearchDisplayControllerContentNode


public func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
    let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
    let searchBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0
    let navigationBarOffset: CGFloat
    if statusBarHeight.isZero {
        navigationBarOffset = -20.0
    } else {
        navigationBarOffset = 0.0
    }
    var navigationBarFrame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight))
    if layout.statusBarHeight == nil {
        navigationBarFrame.size.height = 64.0
    }
    navigationBarFrame.size.height += 10.0
    
    let searchBarFrame: CGRect
    if case .navigation = self.mode {
        searchBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0)
    } else {
        searchBarFrame = navigationBarFrame
    }
  // layout searchBar
    transition.updateFrame(node: self.searchBar, frame: searchBarFrame)
    self.searchBar.updateLayout(boundingSize: searchBarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition)
    
    self.containerLayout = (layout, navigationBarFrame.maxY)
    
  // layout contentNode
    transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: layout.size))
    self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: layout.intrinsicInsets, safeInsets:
layout.safeInsets, statusBarHeight: nil, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver),
navigationBarHeight: navigationBarFrame.maxY, transition: transition)
}

// activate
public func activate(insertSubnode: (ASDisplayNode, Bool) -> Void, placeholder: SearchBarPlaceholderNode) {
    guard let (layout, navigationBarHeight) = self.containerLayout else {
        return
    }
    
  // insert contentNode
    insertSubnode(self.contentNode, false)
    
    self.contentNode.frame = CGRect(origin: CGPoint(), size: layout.size)
    self.contentNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: LayoutMetrics(), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: layout.safeInsets,
statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false), navigationBarHeight: navigationBarHeight, transition: .immediate)
    
    let initialTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil)
    
    let contentNodePosition = self.contentNode.layer.position
    self.contentNode.layer.animatePosition(from: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (initialTextBackgroundFrame.maxY + 8.0 - navigationBarHeight)), to: contentNodePosition, duration:
0.5, timingFunction: kCAMediaTimingFunctionSpring)
    self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
    
    self.searchBar.placeholderString = placeholder.placeholderString
    
    let navigationBarFrame: CGRect
    switch self.mode {
        case .list:
            let statusBarHeight: CGFloat = layout.statusBarHeight ?? 0.0
            let searchBarHeight: CGFloat = max(20.0, statusBarHeight) + 44.0
            let navigationBarOffset: CGFloat
            if statusBarHeight.isZero {
                navigationBarOffset = -20.0
            } else {
                navigationBarOffset = 0.0
            }
            var frame = CGRect(origin: CGPoint(x: 0.0, y: navigationBarOffset), size: CGSize(width: layout.size.width, height: searchBarHeight))
            if layout.statusBarHeight == nil {
                frame.size.height = 64.0
            }
            navigationBarFrame = frame
        case .navigation:
            navigationBarFrame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: 54.0)
    }
    
    self.searchBar.frame = navigationBarFrame
  // insert search bar
    insertSubnode(self.searchBar, true)
    self.searchBar.layout()
    
    self.searchBar.activate()
    self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}

// deactivate
public func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) {
    self.searchBar.deactivate()

    if let placeholder = placeholder {
        let searchBar = self.searchBar
      // transition searchBar out
        searchBar.transitionOut(to: placeholder, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate, completion: {
            [weak searchBar] in
            searchBar?.removeFromSupernode()
        })
    }

  // animate contentNode out
    let contentNode = self.contentNode
    if animated {
        if let placeholder = placeholder, let (_, navigationBarHeight) = self.containerLayout {
            let contentNodePosition = self.contentNode.layer.position
            let targetTextBackgroundFrame = placeholder.convert(placeholder.backgroundNode.frame, to: nil)

            self.contentNode.layer.animatePosition(from: contentNodePosition, to: CGPoint(x: contentNodePosition.x, y: contentNodePosition.y + (targetTextBackgroundFrame.maxY + 8.0 - navigationBarHeight)), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
        }
        contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in
            contentNode?.removeFromSupernode()
        })
    } else {
        contentNode.removeFromSupernode()
    }
}

// preview
public func previewViewAndActionAtLocation(_ location: CGPoint) -> (UIView, CGRect, Any)? {
    return self.contentNode.previewViewAndActionAtLocation(location)
}
This post is licensed under CC BY 4.0 by the author.