Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Keyboard Glitch / Add automatic conformance to enums that are CaseIterable / Ignore disabled Textfields #10

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
6 changes: 3 additions & 3 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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": "7dc5b287f8040e4ad5038739850b758e78f77808",
"version": "1.1.4"
}
}
]
Expand Down
12 changes: 7 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
// 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

let package = Package(
name: "Focuser",
platforms: [
.iOS(.v13),
.iOS(.v14),
],
products: [
.library(
name: "Focuser",
targets: ["Focuser"]),
],
dependencies: [
.package(name: "Introspect", url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.3")
.package(url: "https://github.com/siteline/SwiftUI-Introspect", exact: "1.1.4")
],
targets: [
.target(
name: "Focuser",
dependencies: ["Introspect"]),
dependencies: [.product(name: "SwiftUIIntrospect", package: "SwiftUI-Introspect")]
),
.testTarget(
name: "FocuserTests",
dependencies: ["Focuser"]),
dependencies: ["Focuser"]
),
]
)
15 changes: 15 additions & 0 deletions Sources/Focuser/ComplianceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}

}
147 changes: 147 additions & 0 deletions Sources/Focuser/LifeCycleEventHandler.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}

}
119 changes: 113 additions & 6 deletions Sources/Focuser/TextEditorIntrospect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,128 @@
//

import SwiftUI
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<Value: FocusStateCompliant & Hashable>: 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, .v15, .v16, .v17)) { 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
})
}
}
1 change: 0 additions & 1 deletion Sources/Focuser/TextField+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ public extension View {
}
}

@available(iOS 14.0, *)
public extension View {
func focusEditor<T: FocusStateCompliant>(_ focusedField: Binding<T?>, equals: T) -> some View {
modifier(FocusModifierTextEditor(focusedField: focusedField, equals: equals))
Expand Down
Loading