From 7bd85c7e822dca5449027ada2a5b668fcd9cb978 Mon Sep 17 00:00:00 2001 From: Riaz Hassan Date: Fri, 12 Sep 2025 12:17:58 +0530 Subject: [PATCH 1/7] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 73ffacb..4fef7f0 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3") + .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "1.4.0-beta.4") ], targets: [ .target( From 5ca329fb2f906e172bb8b2e71181bf6a77314867 Mon Sep 17 00:00:00 2001 From: Riaz Hassan Date: Fri, 12 Sep 2025 12:37:18 +0530 Subject: [PATCH 2/7] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4fef7f0..c91f71c 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "1.4.0-beta.4") + .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "1.3.0") ], targets: [ .target( From 24750a3bb8fd960c8c55ab85e3ed3f383d4c1d67 Mon Sep 17 00:00:00 2001 From: Riaz Hassan Date: Fri, 12 Sep 2025 12:45:46 +0530 Subject: [PATCH 3/7] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index c91f71c..93ea6f4 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "1.3.0") + .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", branch: "main"), ], targets: [ .target( From 17240f5c75665df266ff1019294c00ba121e0423 Mon Sep 17 00:00:00 2001 From: Riaz Hassan Date: Fri, 12 Sep 2025 13:04:22 +0530 Subject: [PATCH 4/7] Update Package.swift --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 93ea6f4..73ffacb 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", branch: "main"), + .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3") ], targets: [ .target( From c50ba77bbd742ed09fa5587037d72491bad25d89 Mon Sep 17 00:00:00 2001 From: Riyas Date: Fri, 12 Sep 2025 13:12:25 +0530 Subject: [PATCH 5/7] Updated to lates --- .../xcshareddata/xcschemes/Focuser.xcscheme | 79 +++++++++++++++++++ Example/Example.xcodeproj/project.pbxproj | 25 +----- .../xcshareddata/swiftpm/Package.resolved | 6 +- Package.swift | 2 +- 4 files changed, 84 insertions(+), 28 deletions(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/Focuser.xcscheme diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Focuser.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Focuser.xcscheme new file mode 100644 index 0000000..4233c66 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Focuser.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index b6759c2..6cfa809 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -13,7 +13,6 @@ FFDC73A926DF94DE00C1F0D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFDC73A826DF94DE00C1F0D0 /* Assets.xcassets */; }; FFDC73AC26DF94DE00C1F0D0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFDC73AB26DF94DE00C1F0D0 /* Preview Assets.xcassets */; }; FFDC73AF26DF94DE00C1F0D0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FFDC73AD26DF94DE00C1F0D0 /* LaunchScreen.storyboard */; }; - FFDC73C626DFE61F00C1F0D0 /* Focuser in Frameworks */ = {isa = PBXBuildFile; productRef = FFDC73C526DFE61F00C1F0D0 /* Focuser */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -33,7 +32,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FFDC73C626DFE61F00C1F0D0 /* Focuser in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -96,7 +94,6 @@ ); name = focusstate; packageProductDependencies = ( - FFDC73C526DFE61F00C1F0D0 /* Focuser */, ); productName = focusstate; productReference = FFDC739F26DF94DC00C1F0D0 /* focusstate.app */; @@ -126,7 +123,6 @@ ); mainGroup = FFDC739626DF94DC00C1F0D0; packageReferences = ( - FFDC73C426DFE61F00C1F0D0 /* XCRemoteSwiftPackageReference "swift-focuser" */, ); productRefGroup = FFDC73A026DF94DC00C1F0D0 /* Products */; projectDirPath = ""; @@ -353,25 +349,6 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - FFDC73C426DFE61F00C1F0D0 /* XCRemoteSwiftPackageReference "swift-focuser" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "git@github.com:art-technologies/swift-focuser.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.1.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - FFDC73C526DFE61F00C1F0D0 /* Focuser */ = { - isa = XCSwiftPackageProductDependency; - package = FFDC73C426DFE61F00C1F0D0 /* XCRemoteSwiftPackageReference "swift-focuser" */; - productName = Focuser; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = FFDC739726DF94DC00C1F0D0 /* Project object */; } diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4c428c0..3af87da 100644 --- a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -2,12 +2,12 @@ "object": { "pins": [ { - "package": "Introspect", + "package": "swiftui-introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", "state": { "branch": null, - "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2", - "version": "0.1.3" + "revision": "9bedf1e79c7b7c34b35b74bef126f56d2400ef7f", + "version": "1.4.0-beta.4" } } ] diff --git a/Package.swift b/Package.swift index 93ea6f4..c65be29 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", branch: "main"), + .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "1.4.0-beta.3"), ], targets: [ .target( From 96e1c381e76f0f8d777bf324169121888e540d98 Mon Sep 17 00:00:00 2001 From: Riyas Date: Fri, 12 Sep 2025 13:40:01 +0530 Subject: [PATCH 6/7] Focus user updated --- Example/Example.xcodeproj/project.pbxproj | 25 ++- .../xcshareddata/swiftpm/Package.resolved | 24 ++- Package.resolved | 24 ++- Package.swift | 12 +- Sources/Focuser/ComplianceProtocol.swift | 15 ++ Sources/Focuser/LifeCycleEventHandler.swift | 147 ++++++++++++++++++ Sources/Focuser/TextEditorIntrospect.swift | 119 +++++++++++++- Sources/Focuser/TextField+Extensions.swift | 1 - Sources/Focuser/TextFieldIntrospect.swift | 82 +++++++--- 9 files changed, 390 insertions(+), 59 deletions(-) create mode 100644 Sources/Focuser/LifeCycleEventHandler.swift diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 6cfa809..b6759c2 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 52; objects = { /* Begin PBXBuildFile section */ @@ -13,6 +13,7 @@ FFDC73A926DF94DE00C1F0D0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFDC73A826DF94DE00C1F0D0 /* Assets.xcassets */; }; FFDC73AC26DF94DE00C1F0D0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FFDC73AB26DF94DE00C1F0D0 /* Preview Assets.xcassets */; }; FFDC73AF26DF94DE00C1F0D0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FFDC73AD26DF94DE00C1F0D0 /* LaunchScreen.storyboard */; }; + FFDC73C626DFE61F00C1F0D0 /* Focuser in Frameworks */ = {isa = PBXBuildFile; productRef = FFDC73C526DFE61F00C1F0D0 /* Focuser */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -32,6 +33,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FFDC73C626DFE61F00C1F0D0 /* Focuser in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,6 +96,7 @@ ); name = focusstate; packageProductDependencies = ( + FFDC73C526DFE61F00C1F0D0 /* Focuser */, ); productName = focusstate; productReference = FFDC739F26DF94DC00C1F0D0 /* focusstate.app */; @@ -123,6 +126,7 @@ ); mainGroup = FFDC739626DF94DC00C1F0D0; packageReferences = ( + FFDC73C426DFE61F00C1F0D0 /* XCRemoteSwiftPackageReference "swift-focuser" */, ); productRefGroup = FFDC73A026DF94DC00C1F0D0 /* Products */; projectDirPath = ""; @@ -349,6 +353,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + FFDC73C426DFE61F00C1F0D0 /* XCRemoteSwiftPackageReference "swift-focuser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "git@github.com:art-technologies/swift-focuser.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + FFDC73C526DFE61F00C1F0D0 /* Focuser */ = { + isa = XCSwiftPackageProductDependency; + package = FFDC73C426DFE61F00C1F0D0 /* XCRemoteSwiftPackageReference "swift-focuser" */; + productName = Focuser; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = FFDC739726DF94DC00C1F0D0 /* Project object */; } diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3af87da..b113894 100644 --- a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,16 +1,14 @@ { - "object": { - "pins": [ - { - "package": "swiftui-introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "9bedf1e79c7b7c34b35b74bef126f56d2400ef7f", - "version": "1.4.0-beta.4" - } + "pins" : [ + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" } - ] - }, - "version": 1 + } + ], + "version" : 2 } diff --git a/Package.resolved b/Package.resolved index 4c428c0..b113894 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,16 +1,14 @@ { - "object": { - "pins": [ - { - "package": "Introspect", - "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git", - "state": { - "branch": null, - "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2", - "version": "0.1.3" - } + "pins" : [ + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" } - ] - }, - "version": 1 + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index c65be29..be69175 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "Focuser", platforms: [ - .iOS(.v13), + .iOS(.v14), ], products: [ .library( @@ -14,14 +14,16 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "1.4.0-beta.3"), + .package(url: "https://github.com/siteline/swiftui-introspect", exact: "1.3.0") ], targets: [ .target( name: "Focuser", - dependencies: ["Introspect"]), + dependencies: [.product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect")] + ), .testTarget( name: "FocuserTests", - dependencies: ["Focuser"]), + dependencies: ["Focuser"] + ), ] ) diff --git a/Sources/Focuser/ComplianceProtocol.swift b/Sources/Focuser/ComplianceProtocol.swift index 08cac57..deaace0 100644 --- a/Sources/Focuser/ComplianceProtocol.swift +++ b/Sources/Focuser/ComplianceProtocol.swift @@ -11,3 +11,18 @@ public protocol FocusStateCompliant: Hashable { static var last: Self { get } var next: Self? { get } } + +public extension FocusStateCompliant where Self: CaseIterable, AllCases: BidirectionalCollection { + + static var last: Self { + return Self.allCases.last! //swiftlint:disable:this force_unwrapping + } + + var next: Self? { + let all = Self.allCases + let index = all.firstIndex(of: self)! //swiftlint:disable:this force_unwrapping + let next = all.index(after: index) + return next == all.endIndex ? nil : all[next] + } + +} diff --git a/Sources/Focuser/LifeCycleEventHandler.swift b/Sources/Focuser/LifeCycleEventHandler.swift new file mode 100644 index 0000000..4f97bb2 --- /dev/null +++ b/Sources/Focuser/LifeCycleEventHandler.swift @@ -0,0 +1,147 @@ +// +// LifeCycleEventHandler.swift +// +// +// Created by Tarek Sabry on 03/02/2023. +// + +import UIKit +import SwiftUI + +public typealias LifeCycleEventHandler = ((LifeCycleEvent) -> Void) + +public enum LifeCycleEvent { + case viewWillAppear + case viewDidAppear + case viewWillDisappear + case viewDidDisappear +} + +struct ViewControllerLifeCycleHandler: UIViewControllerRepresentable { + + private let onLifeCycleEvent: LifeCycleEventHandler + + init(onLifeCycleEvent: @escaping LifeCycleEventHandler) { + self.onLifeCycleEvent = onLifeCycleEvent + } + + func makeUIViewController(context: Context) -> UIViewController { + LifeCycleViewController(onLifeCycleEvent: onLifeCycleEvent) + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} + + private class LifeCycleViewController: UIViewController { + private let onLifeCycleEvent: ((LifeCycleEvent) -> Void) + + init(onLifeCycleEvent: @escaping LifeCycleEventHandler) { + self.onLifeCycleEvent = onLifeCycleEvent + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + onLifeCycleEvent(.viewWillAppear) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + onLifeCycleEvent(.viewDidAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + onLifeCycleEvent(.viewWillDisappear) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + onLifeCycleEvent(.viewDidDisappear) + } + } +} + +struct ViewDidLoadModifier: ViewModifier { + + @State private var didLoad = false + private let action: (() -> Void)? + + init(perform action: (() -> Void)? = nil) { + self.action = action + } + + func body(content: Content) -> some View { + content.onAppear { + if didLoad == false { + didLoad = true + action?() + } + } + } + +} + + +public extension View { + + func onLifeCycleEvent(perform: @escaping LifeCycleEventHandler) -> some View { + background(ViewControllerLifeCycleHandler(onLifeCycleEvent: perform)) + } + + func onDidLoad(perform: @escaping (() -> Void)) -> some View { + modifier(ViewDidLoadModifier(perform: perform)) + } + + func onWillAppear(perform: @escaping (() -> Void)) -> some View { + onLifeCycleEvent { event in + switch event { + case .viewWillAppear: + perform() + + default: + break + } + } + } + + func onDidAppear(perform: @escaping (() -> Void)) -> some View { + onLifeCycleEvent { event in + switch event { + case .viewDidAppear: + perform() + + default: + break + } + } + } + + func onWillDisappear(perform: @escaping (() -> Void)) -> some View { + onLifeCycleEvent { event in + switch event { + case .viewWillDisappear: + perform() + + default: + break + } + } + } + + func onDidDisappear(perform: @escaping (() -> Void)) -> some View { + onLifeCycleEvent { event in + switch event { + case .viewDidDisappear: + perform() + + default: + break + } + } + } + +} diff --git a/Sources/Focuser/TextEditorIntrospect.swift b/Sources/Focuser/TextEditorIntrospect.swift index 3b9402f..8c5f3f9 100644 --- a/Sources/Focuser/TextEditorIntrospect.swift +++ b/Sources/Focuser/TextEditorIntrospect.swift @@ -6,21 +6,128 @@ // import SwiftUI +@_spi(Advanced) import SwiftUIIntrospect + +class TextViewObserver: NSObject, UITextViewDelegate, ObservableObject { + var onDidBeginEditing: () -> () = { } + weak var forwardToDelegate: UITextViewDelegate? + weak var ownerTextView: UITextView? + + @available(iOS 2.0, *) + func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + forwardToDelegate?.textViewShouldBeginEditing?(textView) ?? true + } + + @available(iOS 2.0, *) + func textViewShouldEndEditing(_ textView: UITextView) -> Bool { + forwardToDelegate?.textViewShouldEndEditing?(textView) ?? true + } + + + @available(iOS 2.0, *) + func textViewDidBeginEditing(_ textView: UITextView) { + onDidBeginEditing() + forwardToDelegate?.textViewDidBeginEditing?(textView) + } + + @available(iOS 2.0, *) + func textViewDidEndEditing(_ textView: UITextView) { + forwardToDelegate?.textViewDidEndEditing?(textView) + } + + + @available(iOS 2.0, *) + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + forwardToDelegate?.textView?(textView, shouldChangeTextIn: range, replacementText: text) ?? true + } + + @available(iOS 2.0, *) + func textViewDidChange(_ textView: UITextView) { + forwardToDelegate?.textViewDidChange?(textView) + } + + + @available(iOS 2.0, *) + func textViewDidChangeSelection(_ textView: UITextView) { + forwardToDelegate?.textViewDidChangeSelection?(textView) + } + + @available(iOS 10.0, *) + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + forwardToDelegate?.textView?(textView, shouldInteractWith: URL, in: characterRange, interaction: interaction) ?? true + } + + @available(iOS 10.0, *) + func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + forwardToDelegate?.textView?(textView, shouldInteractWith: textAttachment, in: characterRange, interaction: interaction) ?? true + } + + + @available(iOS, introduced: 7.0, deprecated: 10.0) + func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool { + forwardToDelegate?.textView?(textView, shouldInteractWith: URL, in: characterRange) ?? true + } + + @available(iOS, introduced: 7.0, deprecated: 10.0) + func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool { + forwardToDelegate?.textView?(textView, shouldInteractWith: textAttachment, in: characterRange) ?? true + } + + @available(iOS 16.0, *) + func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { + forwardToDelegate?.textView?(textView, editMenuForTextIn: range, suggestedActions: suggestedActions) + } + + @available(iOS 16.0, *) + func textView(_ textView: UITextView, willPresentEditMenuWith animator: UIEditMenuInteractionAnimating) { + forwardToDelegate?.textView?(textView, willPresentEditMenuWith: animator) + } + + @available(iOS 16.0, *) + func textView(_ textView: UITextView, willDismissEditMenuWith animator: UIEditMenuInteractionAnimating) { + forwardToDelegate?.textView?(textView, willDismissEditMenuWith: animator) + } +} public struct FocusModifierTextEditor: ViewModifier { @Binding var focusedField: Value? var equals: Value - @State var observer = TextFieldObserver() + @StateObject var observer = TextViewObserver() public func body(content: Content) -> some View { content - .introspectTextView { tv in + .introspect(.textEditor, on: .iOS(.v14...)) { textView in + if !(textView.delegate is TextViewObserver) { + observer.forwardToDelegate = textView.delegate + observer.ownerTextView = textView + textView.delegate = observer + } + + observer.onDidBeginEditing = { + DispatchQueue.main.async { + focusedField = equals + } + } + if focusedField == equals { - tv.becomeFirstResponder() + DispatchQueue.main.async { + if textView.isUserInteractionEnabled { + textView.becomeFirstResponder() + } else { + focusedField = focusedField?.next + } + } + } + } + .onChange(of: focusedField) { focusedField in + if focusedField == nil { + observer.ownerTextView?.resignFirstResponder() + } + } + .onWillDisappear { + if focusedField != nil { + focusedField = nil } } - .simultaneousGesture(TapGesture().onEnded { - focusedField = equals - }) } } diff --git a/Sources/Focuser/TextField+Extensions.swift b/Sources/Focuser/TextField+Extensions.swift index 763d957..3699226 100644 --- a/Sources/Focuser/TextField+Extensions.swift +++ b/Sources/Focuser/TextField+Extensions.swift @@ -14,7 +14,6 @@ public extension View { } } -@available(iOS 14.0, *) public extension View { func focusEditor(_ focusedField: Binding, equals: T) -> some View { modifier(FocusModifierTextEditor(focusedField: focusedField, equals: equals)) diff --git a/Sources/Focuser/TextFieldIntrospect.swift b/Sources/Focuser/TextFieldIntrospect.swift index daf78ec..4b86760 100644 --- a/Sources/Focuser/TextFieldIntrospect.swift +++ b/Sources/Focuser/TextFieldIntrospect.swift @@ -6,10 +6,12 @@ // import SwiftUI -import Introspect +@_spi(Advanced) import SwiftUIIntrospect -class TextFieldObserver: NSObject, UITextFieldDelegate { - var onReturnTap: () -> () = {} +class TextFieldObserver: NSObject, UITextFieldDelegate, ObservableObject { + var onReturnTap: () -> () = { } + var onDidBeginEditing: () -> () = { } + weak var ownerTextField: UITextField? weak var forwardToDelegate: UITextFieldDelegate? @available(iOS 2.0, *) @@ -19,6 +21,7 @@ class TextFieldObserver: NSObject, UITextFieldDelegate { @available(iOS 2.0, *) func textFieldDidBeginEditing(_ textField: UITextField) { + onDidBeginEditing() forwardToDelegate?.textFieldDidBeginEditing?(textField) } @@ -57,39 +60,78 @@ class TextFieldObserver: NSObject, UITextFieldDelegate { onReturnTap() return forwardToDelegate?.textFieldShouldReturn?(textField) ?? true } + + @available(iOS 16.0, *) + func textField(_ textField: UITextField, editMenuForCharactersIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? { + forwardToDelegate?.textField?(textField, editMenuForCharactersIn: range, suggestedActions: suggestedActions) + } + + @available(iOS 16.0, *) + func textField(_ textField: UITextField, willPresentEditMenuWith animator: UIEditMenuInteractionAnimating) { + forwardToDelegate?.textField?(textField, willPresentEditMenuWith: animator) + } + + @available(iOS 16.0, *) + func textField(_ textField: UITextField, willDismissEditMenuWith animator: UIEditMenuInteractionAnimating) { + forwardToDelegate?.textField?(textField, willDismissEditMenuWith: animator) + } } public struct FocusModifier: ViewModifier { @Binding var focusedField: Value? var equals: Value - @State var observer = TextFieldObserver() + @StateObject var observer = TextFieldObserver() public func body(content: Content) -> some View { content - .introspectTextField { tf in - if !(tf.delegate is TextFieldObserver) { - observer.forwardToDelegate = tf.delegate - tf.delegate = observer + .introspect(.textField, on: .iOS(.v14...)) { textField in + if !(textField.delegate is TextFieldObserver) { + observer.forwardToDelegate = textField.delegate + observer.ownerTextField = textField + textField.delegate = observer + } + + observer.onDidBeginEditing = { + DispatchQueue.main.async { + focusedField = equals + } } - /// when user taps return we navigate to next responder observer.onReturnTap = { - focusedField = focusedField?.next ?? Value.last + DispatchQueue.main.async { + focusedField = focusedField?.next + + if focusedField == nil { + textField.resignFirstResponder() + } + } } - - /// to show kayboard with `next` or `return` + + if focusedField == equals { + DispatchQueue.main.async { + if textField.isEnabled { + textField.becomeFirstResponder() + } else { + focusedField = focusedField?.next + } + } + } + if equals.hashValue == Value.last.hashValue { - tf.returnKeyType = .done + textField.returnKeyType = .done } else { - tf.returnKeyType = .next + textField.returnKeyType = .next } - - if focusedField == equals { - tf.becomeFirstResponder() + } + .onChange(of: focusedField) { focusedField in + if focusedField == nil { + observer.ownerTextField?.resignFirstResponder() + } + } + .onWillDisappear { + if focusedField != nil { + focusedField = nil } } - .simultaneousGesture(TapGesture().onEnded { - focusedField = equals - }) } } From f5ca67ace01f30a2dcfe1191502e1df010360b70 Mon Sep 17 00:00:00 2001 From: Riaz Hassan Date: Thu, 25 Sep 2025 11:58:12 +0530 Subject: [PATCH 7/7] Update Package.swift --- Package.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index be69175..efb90b3 100644 --- a/Package.swift +++ b/Package.swift @@ -14,12 +14,17 @@ let package = Package( targets: ["Focuser"]), ], dependencies: [ - .package(url: "https://github.com/siteline/swiftui-introspect", exact: "1.3.0") + .package(url: "https://github.com/siteline/swiftui-introspect", from: "1.4.0-beta.3") ], targets: [ .target( name: "Focuser", - dependencies: [.product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect")] + dependencies: [ + .product(name: "SwiftUIIntrospect", package: "swiftui-introspect") + ], + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency") + ] ), .testTarget( name: "FocuserTests",