@@ -41,35 +41,50 @@ struct UXCodeTextViewRepresentable : UXViewRepresentable {
4141 * the opening character. For example: `[ "<": ">" ]` would
4242 * automatically insert the closing ">" if the user enters
4343 * "<".
44+ * - autoscroll: If enabled, the editor automatically scrolls to the respective
45+ * region when the `selection` is changed programatically.
4446 */
4547 public init ( source : Binding < String > ,
48+ selection : Binding < Range < String . Index > > ? ,
4649 language : CodeEditor . Language ? ,
4750 theme : CodeEditor . ThemeName ,
4851 fontSize : Binding < CGFloat > ? ,
4952 flags : CodeEditor . Flags ,
5053 indentStyle : CodeEditor . IndentStyle ,
5154 autoPairs : [ String : String ] ,
52- inset : CGSize )
55+ inset : CGSize ,
56+ autoscroll : Bool )
5357 {
5458 self . source = source
59+ self . selection = selection
5560 self . fontSize = fontSize
5661 self . language = language
5762 self . themeName = theme
5863 self . flags = flags
5964 self . indentStyle = indentStyle
6065 self . autoPairs = autoPairs
6166 self . inset = inset
67+ self . autoscroll = autoscroll
6268 }
6369
6470 private var source : Binding < String >
71+ private var selection : Binding < Range < String . Index > > ?
6572 private var fontSize : Binding < CGFloat > ?
6673 private let language : CodeEditor . Language ?
6774 private let themeName : CodeEditor . ThemeName
6875 private let flags : CodeEditor . Flags
6976 private let indentStyle : CodeEditor . IndentStyle
7077 private let inset : CGSize
7178 private let autoPairs : [ String : String ]
72-
79+ private let autoscroll : Bool
80+
81+ // The inner `value` is true, exactly when execution is inside
82+ // the `updateTextView(_:)` method. The `Coordinator` can use this
83+ // value to guard against update cycles.
84+ // This needs to be a `State`, as the `UXCodeTextViewRepresentable`
85+ // might be destructed and recreated in between calls to `makeCoordinator()`
86+ // and `updateTextView(_:)`.
87+ @State private var isCurrentlyUpdatingView = ReferenceTypeBool ( value: false )
7388
7489 // MARK: - TextView Delegate Coordinator
7590
@@ -92,15 +107,66 @@ struct UXCodeTextViewRepresentable : UXViewRepresentable {
92107 assertionFailure ( " unexpected notification object " )
93108 return
94109 }
95- parent . source . wrappedValue = textView. string
110+ textViewDidChange ( textView : textView)
96111 }
97112 #elseif os(iOS)
98113 public func textViewDidChange( _ textView: UITextView ) {
99- parent. source. wrappedValue = textView. string
114+ textViewDidChange ( textView: textView)
115+ }
116+ #else
117+ #error("Unsupported OS")
118+ #endif
119+
120+ private func textViewDidChange( textView: UXTextView ) {
121+ // This function may be called as a consequence of updating the text string
122+ // in UXCodeTextViewRepresentable/updateTextView(_:)`.
123+ // Since this function might update the `parent.source` `Binding`, which in
124+ // turn might update a `State`, this would lead to undefined behavior.
125+ // (Changing a `State` during a `View` update is not permitted).
126+ guard !parent. isCurrentlyUpdatingView. value else {
127+ return
128+ }
129+
130+ parent. source. wrappedValue = textView. string
131+ }
132+
133+ #if os(macOS)
134+ public func textViewDidChangeSelection( _ notification: Notification ) {
135+ guard let textView = notification. object as? UXTextView else {
136+ assertionFailure ( " unexpected notification object " )
137+ return
138+ }
139+
140+ textViewDidChangeSelection ( textView: textView as! UXCodeTextView )
141+ }
142+ #elseif os(iOS)
143+ public func textViewDidChangeSelection( _ textView: UITextView ) {
144+ textViewDidChangeSelection ( textView: textView as! UXCodeTextView )
100145 }
101146 #else
102147 #error("Unsupported OS")
103148 #endif
149+
150+ private func textViewDidChangeSelection( textView: UXCodeTextView ) {
151+ // This function may be called as a consequence of updating the selected
152+ // range in UXCodeTextViewRepresentable/updateTextView(_:)`.
153+ // Since this function might update the `parent.selection` `Binding`, which in
154+ // turn might update a `State`, this would lead to undefined behavior.
155+ // (Changing a `State` during a `View` update is not permitted).
156+ guard !parent. isCurrentlyUpdatingView. value else {
157+ return
158+ }
159+
160+ guard let selection = parent. selection else {
161+ return
162+ }
163+
164+ let range = textView. swiftSelectedRange
165+
166+ if selection. wrappedValue != range {
167+ selection. wrappedValue = range
168+ }
169+ }
104170
105171 var allowCopy : Bool {
106172 return parent. flags. contains ( . selectable)
@@ -113,6 +179,11 @@ struct UXCodeTextViewRepresentable : UXViewRepresentable {
113179 }
114180
115181 private func updateTextView( _ textView: UXCodeTextView ) {
182+ isCurrentlyUpdatingView. value = true
183+ defer {
184+ isCurrentlyUpdatingView. value = false
185+ }
186+
116187 if let binding = fontSize {
117188 textView. applyNewTheme ( themeName, andFontSize: binding. wrappedValue)
118189 }
@@ -136,6 +207,25 @@ struct UXCodeTextViewRepresentable : UXViewRepresentable {
136207 }
137208 }
138209
210+ if let selection = selection {
211+ let range = selection. wrappedValue
212+
213+ if range != textView. swiftSelectedRange {
214+ let nsrange = NSRange ( range, in: textView. string)
215+ #if os(macOS)
216+ textView. setSelectedRange ( nsrange)
217+ #elseif os(iOS)
218+ textView. selectedRange = nsrange
219+ #else
220+ #error("Unsupported OS")
221+ #endif
222+
223+ if autoscroll {
224+ textView. scrollRangeToVisible ( nsrange)
225+ }
226+ }
227+ }
228+
139229 textView. isEditable = flags. contains ( . editable)
140230 textView. isSelectable = flags. contains ( . selectable)
141231 }
@@ -197,28 +287,44 @@ struct UXCodeTextViewRepresentable : UXViewRepresentable {
197287 #endif // iOS
198288}
199289
290+ extension UXCodeTextViewRepresentable {
291+ // A wrapper around a `Bool` that enables updating
292+ // the wrapped value during `View` renders.
293+ private class ReferenceTypeBool {
294+ var value : Bool
295+
296+ init ( value: Bool ) {
297+ self . value = value
298+ }
299+ }
300+ }
301+
200302struct UXCodeTextViewRepresentable_Previews : PreviewProvider {
201303
202304 static var previews : some View {
203305
204306 UXCodeTextViewRepresentable ( source : . constant( " let a = 5 " ) ,
307+ selection : nil ,
205308 language : nil ,
206309 theme : . pojoaque,
207310 fontSize : nil ,
208311 flags : [ . selectable ] ,
209312 indentStyle : . system,
210313 autoPairs : [ : ] ,
211- inset : . init( width: 8 , height: 8 ) )
314+ inset : . init( width: 8 , height: 8 ) ,
315+ autoscroll : false )
212316 . frame ( width: 200 , height: 100 )
213317
214318 UXCodeTextViewRepresentable ( source: . constant( " let a = 5 " ) ,
319+ selection : nil ,
215320 language : . swift,
216321 theme : . pojoaque,
217322 fontSize : nil ,
218323 flags : [ . selectable ] ,
219324 indentStyle : . system,
220325 autoPairs : [ : ] ,
221- inset : . init( width: 8 , height: 8 ) )
326+ inset : . init( width: 8 , height: 8 ) ,
327+ autoscroll : false )
222328 . frame ( width: 200 , height: 100 )
223329
224330 UXCodeTextViewRepresentable (
@@ -228,13 +334,15 @@ struct UXCodeTextViewRepresentable_Previews: PreviewProvider {
228334 \bye
229335 """#
230336 ) ,
337+ selection : nil ,
231338 language : . tex,
232339 theme : . pojoaque,
233340 fontSize : nil ,
234341 flags : [ . selectable ] ,
235342 indentStyle : . system,
236343 autoPairs : [ : ] ,
237- inset : . init( width: 8 , height: 8 )
344+ inset : . init( width: 8 , height: 8 ) ,
345+ autoscroll : false
238346 )
239347 . frame ( width: 540 , height: 200 )
240348 }
0 commit comments