Skip to content

Commit cb8eb65

Browse files
committed
Fixed: suggestion + autocorrection + reentrance selection flash. Added: Lock (again).
1 parent 1a88752 commit cb8eb65

File tree

3 files changed

+206
-15
lines changed

3 files changed

+206
-15
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//=----------------------------------------------------------------------------=
2+
// This source file is part of the DiffableTextViews open source project.
3+
//
4+
// Copyright (c) 2022 Oscar Byström Ericsson
5+
// Licensed under Apache License, Version 2.0
6+
//
7+
// See http://www.apache.org/licenses/LICENSE-2.0 for license information.
8+
//=----------------------------------------------------------------------------=
9+
10+
//*============================================================================*
11+
// MARK: * Lock
12+
//*============================================================================*
13+
14+
@MainActor public final class Lock {
15+
16+
//=------------------------------------------------------------------------=
17+
// MARK: State
18+
//=------------------------------------------------------------------------=
19+
20+
@usableFromInline private(set) var count: UInt = 0
21+
22+
//=------------------------------------------------------------------------=
23+
// MARK: Initializers
24+
//=------------------------------------------------------------------------=
25+
26+
@inlinable public init() { }
27+
28+
//=------------------------------------------------------------------------=
29+
// MARK: Accessors
30+
//=------------------------------------------------------------------------=
31+
32+
@inlinable @inline(__always)
33+
public var isLocked: Bool {
34+
self.count != 0
35+
}
36+
37+
//=------------------------------------------------------------------------=
38+
// MARK: Transformations
39+
//=------------------------------------------------------------------------=
40+
41+
@inlinable @inline(__always) func lock() {
42+
self.count += 1
43+
}
44+
45+
@inlinable @inline(__always) func open() {
46+
self.count -= 1
47+
}
48+
49+
//=------------------------------------------------------------------------=
50+
// MARK: Utilities
51+
//=------------------------------------------------------------------------=
52+
53+
@inlinable public func perform(action: () throws -> Void) {
54+
self.lock(); try? action(); self.open()
55+
}
56+
57+
@inlinable public func task(operation: @escaping () async throws -> Void) {
58+
_ = asynchronous(operation: operation)
59+
}
60+
61+
@inlinable public func task(operation: @escaping () async throws -> Void) async {
62+
await asynchronous(operation: operation).value
63+
}
64+
65+
@inlinable @inline(__always) @discardableResult func asynchronous(
66+
operation: @escaping () async throws -> Void) -> Task<Void, Never> {
67+
self.lock(); return Task { try? await operation(); self.open() }
68+
}
69+
}

Sources/DiffableTextKitXUIKit/DiffableTextField.swift

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public struct DiffableTextField<Style: DiffableTextStyle>: UIViewRepresentable {
9494
// MARK: State
9595
//=--------------------------------------------------------------------=
9696

97+
@usableFromInline let lock = Lock()
98+
9799
@usableFromInline var cache: Cache!
98100
@usableFromInline var context: Context!
99101
@usableFromInline var update = Update()
@@ -174,7 +176,7 @@ public struct DiffableTextField<Style: DiffableTextStyle>: UIViewRepresentable {
174176
shouldChangeCharactersIn nsrange: NSRange,
175177
replacementString text: String) -> Bool {
176178
//=----------------------------------=
177-
// Lock
179+
// Wait
178180
//=----------------------------------=
179181
guard !update else { return false }
180182
//=----------------------------------=
@@ -205,9 +207,21 @@ public struct DiffableTextField<Style: DiffableTextStyle>: UIViewRepresentable {
205207

206208
@inlinable @inline(never) public func textFieldDidChangeSelection(_ view: UITextField) {
207209
//=----------------------------------=
210+
// Locked
211+
//=----------------------------------=
212+
guard !lock.isLocked else { return }
213+
//=----------------------------------=
214+
// Reentrance, Known Due To Push Lock
215+
//=----------------------------------=
216+
if !update.isEmpty {
217+
//=------------------------------=
218+
// Push
219+
//=------------------------------=
220+
self.push([.text, .selection])
221+
//=----------------------------------=
208222
// Marked
209223
//=----------------------------------=
210-
if let _ = view.markedTextRange {
224+
} else if let _ = view.markedTextRange {
211225
//=------------------------------=
212226
// Push
213227
//=------------------------------=
@@ -216,10 +230,6 @@ public struct DiffableTextField<Style: DiffableTextStyle>: UIViewRepresentable {
216230
// Normal
217231
//=----------------------------------=
218232
} else {
219-
//=------------------------------=
220-
// Lock
221-
//=------------------------------=
222-
guard !update else { return }
223233
//=------------------------------=
224234
// Pull
225235
//=------------------------------=
@@ -290,16 +300,21 @@ public struct DiffableTextField<Style: DiffableTextStyle>: UIViewRepresentable {
290300
return
291301
}
292302
//=----------------------------------=
293-
// Text
294-
//=----------------------------------=
295-
if let _ = self.update.remove(.text) {
296-
self.downstream.text = context.text
297-
}
298-
//=----------------------------------=
299-
// Selection
303+
// Lock
300304
//=----------------------------------=
301-
if let _ = self.update.remove(.selection) {
302-
self.downstream.selection = context.selection()
305+
self.lock.perform {
306+
//=------------------------------=
307+
// Text
308+
//=------------------------------=
309+
if let _ = self.update.remove(.text) {
310+
self.downstream.text = context.text
311+
}
312+
//=------------------------------=
313+
// Selection
314+
//=------------------------------=
315+
if let _ = self.update.remove(.selection) {
316+
self.downstream.selection = context.selection()
317+
}
303318
}
304319
}
305320
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//=----------------------------------------------------------------------------=
2+
// This source file is part of the DiffableTextViews open source project.
3+
//
4+
// Copyright (c) 2022 Oscar Byström Ericsson
5+
// Licensed under Apache License, Version 2.0
6+
//
7+
// See http://www.apache.org/licenses/LICENSE-2.0 for license information.
8+
//=----------------------------------------------------------------------------=
9+
#if DEBUG
10+
11+
@testable import DiffableTextKit
12+
13+
import XCTest
14+
15+
//*============================================================================*
16+
// MARK: * Lock x Tests
17+
//*============================================================================*
18+
19+
@MainActor final class LockTests: XCTestCase {
20+
21+
//=------------------------------------------------------------------------=
22+
// MARK: State
23+
//=------------------------------------------------------------------------=
24+
25+
var lock = Lock()
26+
27+
//=------------------------------------------------------------------------=
28+
// MARK: State x Setup
29+
//=------------------------------------------------------------------------=
30+
31+
override func setUp() {
32+
lock = Lock()
33+
}
34+
35+
//=------------------------------------------------------------------------=
36+
// MARK: Tests x State
37+
//=------------------------------------------------------------------------=
38+
39+
func testIsNotLockedByDefault() {
40+
XCTAssertFalse(lock.isLocked)
41+
}
42+
43+
func testIsLockedIsSameAsCountIsZero() {
44+
XCTAssertFalse(lock.isLocked)
45+
XCTAssertEqual(lock.count, 0)
46+
47+
lock.lock()
48+
XCTAssert(lock.isLocked)
49+
XCTAssertEqual(lock.count, 1)
50+
}
51+
52+
//=------------------------------------------------------------------------=
53+
// MARK: Tests x Transformations
54+
//=------------------------------------------------------------------------=
55+
56+
func testLockIncrementsCountOpenDecrementsCount() {
57+
XCTAssertEqual(lock.count, 0)
58+
59+
lock.lock()
60+
lock.lock()
61+
lock.lock()
62+
63+
XCTAssertEqual(lock.count, 3)
64+
65+
lock.open()
66+
lock.open()
67+
lock.open()
68+
69+
XCTAssertEqual(lock.count, 0)
70+
}
71+
72+
//=------------------------------------------------------------------------=
73+
// MARK: Tests x Utilities
74+
//=------------------------------------------------------------------------=
75+
76+
func testSynchronousActionLocksUntilCompletion() {
77+
XCTAssertFalse(lock.isLocked)
78+
//=--------------------------------------=
79+
// Start
80+
//=--------------------------------------=
81+
lock.perform {
82+
XCTAssert(self.lock.isLocked)
83+
}
84+
//=--------------------------------------=
85+
// End
86+
//=--------------------------------------=
87+
XCTAssertFalse(lock.isLocked)
88+
}
89+
90+
func testAsynchronousOperationLocksUntilCompletion() async {
91+
XCTAssertFalse(lock.isLocked)
92+
//=--------------------------------------=
93+
// Start
94+
//=--------------------------------------=
95+
let task = lock.asynchronous {
96+
XCTAssert(self.lock.isLocked) // 2nd
97+
}; XCTAssert(self.lock.isLocked) // 1st
98+
99+
await task.value
100+
//=--------------------------------------=
101+
// End
102+
//=--------------------------------------=
103+
XCTAssertFalse(lock.isLocked)
104+
}
105+
}
106+
107+
#endif

0 commit comments

Comments
 (0)