Skip to content

Commit 9f94bb6

Browse files
committed
refactor: DispatchQueue보다 안정적으로 KVO 패턴을 채택하여 개선
1 parent b5eae9f commit 9f94bb6

1 file changed

Lines changed: 63 additions & 18 deletions

File tree

DevLog/UI/Common/Component/UIKitTextEditor.swift

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private struct UIKitTextEditorRepresentable: UIViewRepresentable {
135135
if let focusBinding {
136136
if focusBinding.wrappedValue {
137137
if !uiView.isFirstResponder {
138-
context.coordinator.preserveAncestorScrollOffset(for: uiView)
138+
context.coordinator.startTrackingOffset(for: uiView)
139139
uiView.becomeFirstResponder()
140140
}
141141
} else if uiView.isFirstResponder {
@@ -148,15 +148,17 @@ private struct UIKitTextEditorRepresentable: UIViewRepresentable {
148148

149149
final class Coordinator: NSObject, UITextViewDelegate {
150150
var parent: UIKitTextEditorRepresentable
151-
private weak var ancestorScrollView: UIScrollView?
152-
private var preservedContentOffset: CGPoint?
151+
private weak var scrollView: UIScrollView?
152+
private var offsetObservation: NSKeyValueObservation?
153+
private var trackedOffset: CGPoint?
154+
private var isRestoringOffset = false
153155

154156
init(_ parent: UIKitTextEditorRepresentable) {
155157
self.parent = parent
156158
}
157159

158160
func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
159-
preserveAncestorScrollOffset(for: textView)
161+
startTrackingOffset(for: textView)
160162
return true
161163
}
162164

@@ -170,20 +172,16 @@ private struct UIKitTextEditorRepresentable: UIViewRepresentable {
170172
focusBinding.wrappedValue = true
171173
}
172174

173-
restoreAncestorScrollOffsetIfNeeded()
175+
restoreOffsetIfNeeded()
174176

175177
DispatchQueue.main.async { [weak self] in
176-
self?.restoreAncestorScrollOffsetIfNeeded()
178+
self?.restoreOffsetIfNeeded()
177179
self?.updateHeight(for: textView)
178180
}
179-
180-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
181-
self?.restoreAncestorScrollOffsetIfNeeded()
182-
self?.preservedContentOffset = nil
183-
}
184181
}
185182

186183
func textViewDidChange(_ textView: UITextView) {
184+
stopTrackingOffset()
187185
parent.text = textView.text
188186
updateHeight(for: textView)
189187
}
@@ -193,6 +191,7 @@ private struct UIKitTextEditorRepresentable: UIViewRepresentable {
193191
focusBinding.wrappedValue = false
194192
}
195193

194+
stopTrackingOffset()
196195
applyPlaceholderIfNeeded(to: textView)
197196
}
198197

@@ -210,19 +209,65 @@ private struct UIKitTextEditorRepresentable: UIViewRepresentable {
210209
textView.textColor == .placeholderText
211210
}
212211

213-
func preserveAncestorScrollOffset(for textView: UITextView) {
214-
ancestorScrollView = textView.enclosingScrollView
215-
preservedContentOffset = ancestorScrollView?.contentOffset
212+
func startTrackingOffset(for textView: UITextView) {
213+
stopObservingOffset()
214+
scrollView = textView.enclosingScrollView
215+
trackedOffset = scrollView?.contentOffset
216+
observeOffsetIfNeeded()
217+
}
218+
219+
func restoreOffsetIfNeeded() {
220+
guard let scrollView, let trackedOffset else { return }
221+
222+
if scrollView.contentOffset != trackedOffset {
223+
isRestoringOffset = true
224+
scrollView.setContentOffset(trackedOffset, animated: false)
225+
isRestoringOffset = false
226+
}
216227
}
217228

218-
func restoreAncestorScrollOffsetIfNeeded() {
219-
guard let ancestorScrollView, let preservedContentOffset else { return }
229+
func observeOffsetIfNeeded() {
230+
guard let scrollView else { return }
220231

221-
if ancestorScrollView.contentOffset != preservedContentOffset {
222-
ancestorScrollView.setContentOffset(preservedContentOffset, animated: false)
232+
offsetObservation = scrollView.observe(
233+
\.contentOffset,
234+
options: [.new]
235+
) { [weak self] scrollView, _ in
236+
self?.handleOffsetChange(in: scrollView)
223237
}
224238
}
225239

240+
func handleOffsetChange(in scrollView: UIScrollView) {
241+
guard let trackedOffset else {
242+
stopObservingOffset()
243+
return
244+
}
245+
246+
if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating {
247+
stopTrackingOffset()
248+
return
249+
}
250+
251+
if isRestoringOffset {
252+
return
253+
}
254+
255+
if scrollView.contentOffset != trackedOffset {
256+
restoreOffsetIfNeeded()
257+
}
258+
}
259+
260+
func stopObservingOffset() {
261+
offsetObservation?.invalidate()
262+
offsetObservation = nil
263+
}
264+
265+
func stopTrackingOffset() {
266+
stopObservingOffset()
267+
scrollView = nil
268+
trackedOffset = nil
269+
}
270+
226271
func updateHeight(for textView: UITextView) {
227272
textView.layoutIfNeeded()
228273

0 commit comments

Comments
 (0)