-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathNotesTextView.swift
More file actions
249 lines (189 loc) · 10.7 KB
/
NotesTextView.swift
File metadata and controls
249 lines (189 loc) · 10.7 KB
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//
// NotesTextView.swift
// NotesTextView
//
// Created by Rimesh Jotaniya on 29/05/20.
// Copyright © 2020 Rimesh Jotaniya. All rights reserved.
//
import UIKit
public class NotesTextView: UITextView {
enum TextStyle {
case title
case heading
case body
case serif
}
let textFieldAnimationDuration: TimeInterval = 0.3
let accessaryView = UIView()
var keyboardView: UIView?
// Delay timer to switch between keyboards
var kTimer: Timer?
var isSwitchingKeyboard = false
let styleKeyboard = TextStyleKeyboardView()
let leftIndentButtonMain = UIButton()
let rightIndentButtonMain = UIButton()
var textFormatBarButtonForiPad: UIBarButtonItem!
let accessoryViewHeight: CGFloat = 50
let styleKeyboardHeight: CGFloat = 300
let indentWidth: CGFloat = 20
let minimumIndent: CGFloat = 0
let maximumIndent: CGFloat = 200
struct TextState {
let selectedRange: NSRange
let attributedText: NSAttributedString
}
var keyboardHeight: CGFloat = 0.0
override public var selectedTextRange: UITextRange? {
didSet {
updateVisualForKeyboard()
}
}
public weak var hostingViewController: UIViewController?
public var shouldAdjustInsetBasedOnKeyboardHeight = false
public init() {
super.init(frame: .zero, textContainer: nil)
setupTextView()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
font = NotesFontProvider.shared.bodyFont
setupTextView()
}
override public func becomeFirstResponder() -> Bool {
let isAlreadyFirstResponder = isFirstResponder
let willBecomeFirstResponder = super.becomeFirstResponder()
if !isAlreadyFirstResponder && willBecomeFirstResponder {
if !isSwitchingKeyboard {
var targetLocationForAttributes: Int = 0
if selectedRange.location != NSNotFound && textStorage.length != 0 {
// selectedRange.Location is actually the length of the string so it becomes out of bounds when the selected range is actually at last character
if selectedRange.location == textStorage.length && selectedRange.length == 0 {
let lastChar = text.suffix(1) as NSString
targetLocationForAttributes = max(0, textStorage.length - lastChar.length)
// we don't need to update typing attributes here..
// it creates a problem if the last character is emoji. (emoji length is variable)
} else {
targetLocationForAttributes = selectedRange.location
typingAttributes = textStorage.attributes(at: targetLocationForAttributes, longestEffectiveRange: nil, in: selectedRange)
}
}
updateVisualForKeyboard()
}
}
return willBecomeFirstResponder
}
override public func resignFirstResponder() -> Bool {
let willResignFirstResponder = super.resignFirstResponder()
return willResignFirstResponder
}
override public func paste(_: Any?) {
// Setup code in overridden UITextView.copy/paste
let pb = UIPasteboard.general
// pasting from external source might paste some attributes like images, fonts, colors which are not supported.
// converting it to plain text to remove all the attributes and give it default font of body.
// UTI List
let utf8StringType = "public.utf8-plain-text"
for pbDict in pb.items {
if let pastedString = pbDict[utf8StringType] as? String {
// When pasting apply body font attributes
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.font: NotesFontProvider.shared.bodyFont,
NSAttributedString.Key.foregroundColor: UIColor.label,
]
let attributed = NSAttributedString(string: pastedString, attributes: attributes)
// how many characters to advance?
// string counts emojis as single character so don't use string.count
// convert it to NSString and check its length
let rawString = pastedString as NSString
// Insert pasted string
textStorage.insert(attributed, at: selectedRange.location)
selectedRange.location += rawString.length
selectedRange.length = 0
}
}
}
override public func shouldChangeText(in _: UITextRange, replacementText text: String) -> Bool {
// after the Title or Heading line,
// next line should be body font
if text == "\n" {
if let font = typingAttributes[NSAttributedString.Key.font] as? UIFont {
if font == NotesFontProvider.shared.headingFont || font == NotesFontProvider.shared.titleFont {
typingAttributes[NSAttributedString.Key.font] = NotesFontProvider.shared.bodyFont
updateVisualForKeyboard()
}
}
if typingAttributes[NSAttributedString.Key.backgroundColor] != nil {
typingAttributes[NSAttributedString.Key.backgroundColor] = UIColor.clear
}
}
return true
}
func setupTextView() {
typingAttributes[NSAttributedString.Key.font] = NotesFontProvider.shared.bodyFont
typingAttributes[NSAttributedString.Key.foregroundColor] = UIColor.label
alwaysBounceVertical = true
allowsEditingTextAttributes = true
keyboardDismissMode = .interactive
setupKeyboardActions()
if traitCollection.userInterfaceIdiom == .pad {
let Aa_IconConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .regular, scale: .small)
let textfomratImage = UIImage(systemName: "textformat.size", withConfiguration: Aa_IconConfig)
textFormatBarButtonForiPad = UIBarButtonItem(image: textfomratImage, style: .plain, target: self, action: #selector(showPopOverKeyboardForiPad))
let buttonGroup = UIBarButtonItemGroup(barButtonItems: [textFormatBarButtonForiPad], representativeItem: nil)
inputAssistantItem.trailingBarButtonGroups = [buttonGroup]
} else {
prepareAccessoryView()
}
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextView.textDidChangeNotification, object: nil)
styleKeyboard.delegate = self
}
@objc private func textDidChange(_: Notification) {
updateVisualForKeyboard()
}
private func setupKeyboardActions() {
styleKeyboard.boldButton.addTarget(self, action: #selector(makeTextBold), for: .touchUpInside)
styleKeyboard.italicButton.addTarget(self, action: #selector(makeTextItalics), for: .touchUpInside)
styleKeyboard.underlineButton.addTarget(self, action: #selector(makeTextUnderline), for: .touchUpInside)
styleKeyboard.strikethroughButton.addTarget(self, action: #selector(makeTextStrikethrough), for: .touchUpInside)
styleKeyboard.leftIndentButton.addTarget(self, action: #selector(indentLeft), for: .touchUpInside)
styleKeyboard.rightIndentButton.addTarget(self, action: #selector(indentRight), for: .touchUpInside)
styleKeyboard.titleButton.tapGesture.addTarget(self, action: #selector(useTitle))
styleKeyboard.headingButton.tapGesture.addTarget(self, action: #selector(useHeading))
styleKeyboard.bodyButton.tapGesture.addTarget(self, action: #selector(useBody))
styleKeyboard.serifButton.tapGesture.addTarget(self, action: #selector(useSerif))
styleKeyboard.returnButton.addTarget(self, action: #selector(showDefaultKeyboard), for: .touchUpInside)
styleKeyboard.leftAlignButton.addTarget(self, action: #selector(useLeftAlignment), for: .touchUpInside)
styleKeyboard.centerAlignButton.addTarget(self, action: #selector(useCenterAlignment), for: .touchUpInside)
styleKeyboard.rightAlignButton.addTarget(self, action: #selector(useRightAlignment), for: .touchUpInside)
}
private func prepareAccessoryView() {
accessaryView.frame = .init(origin: .zero, size: CGSize(width: 10, height: accessoryViewHeight))
accessaryView.backgroundColor = .systemGray5
accessaryView.accessibilityIdentifier = "accessoryView"
inputAccessoryView = accessaryView
let Aa_IconConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .regular, scale: .small)
let indent_config = UIImage.SymbolConfiguration(pointSize: 25, weight: .regular, scale: .small)
let textFormatButton = UIButton()
let textfomratImage = UIImage(systemName: "textformat.size", withConfiguration: Aa_IconConfig)
textFormatButton.setImage(textfomratImage, for: .normal)
textFormatButton.addTarget(self, action: #selector(showStyleKeyboard), for: .touchUpInside)
let leftImage = UIImage(systemName: "decrease.indent", withConfiguration: indent_config)
let rightImage = UIImage(systemName: "increase.indent", withConfiguration: indent_config)
leftIndentButtonMain.setImage(leftImage, for: .normal)
rightIndentButtonMain.setImage(rightImage, for: .normal)
leftIndentButtonMain.addTarget(self, action: #selector(indentLeft), for: .touchUpInside)
rightIndentButtonMain.addTarget(self, action: #selector(indentRight), for: .touchUpInside)
leftIndentButtonMain.isEnabled = false
let indentStack = UIStackView(arrangedSubviews: [leftIndentButtonMain, rightIndentButtonMain])
indentStack.spacing = 20
textFormatButton.tintColor = .systemGray
leftIndentButtonMain.tintColor = .systemGray
rightIndentButtonMain.tintColor = .systemGray
accessaryView.addSubview(textFormatButton)
textFormatButton.anchor(top: accessaryView.topAnchor, leading: accessaryView.safeAreaLayoutGuide.leadingAnchor, bottom: accessaryView.bottomAnchor, trailing: nil, padding: .init(top: 0, left: 25, bottom: 0, right: 0))
accessaryView.addSubview(indentStack)
indentStack.anchor(top: accessaryView.topAnchor, leading: nil, bottom: accessaryView.bottomAnchor, trailing: accessaryView.safeAreaLayoutGuide.trailingAnchor, padding: .init(top: 0, left: 0, bottom: 0, right: 20))
}
}