UTMPatches.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. //
  2. // Copyright © 2022 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import UIKit
  17. /// Handles Obj-C patches to fix SwiftUI issues
  18. final class UTMPatches {
  19. static private var isPatched: Bool = false
  20. /// Installs the patches
  21. /// TODO: Some thread safety/race issues etc
  22. static func patchAll() {
  23. UIViewController.patchViewController()
  24. UIPress.patchPress()
  25. UIWindow.patchWindow()
  26. }
  27. }
  28. fileprivate extension NSObject {
  29. static func patch(_ original: Selector, with swizzle: Selector, class cls: AnyClass?) {
  30. let originalMethod = class_getInstanceMethod(cls, original)!
  31. let swizzleMethod = class_getInstanceMethod(cls, swizzle)!
  32. method_exchangeImplementations(originalMethod, swizzleMethod)
  33. }
  34. }
  35. /// We need to set these when the VM starts running since there is no way to do it from SwiftUI right now
  36. extension UIViewController {
  37. private static var _utm__childForHomeIndicatorAutoHiddenStorage: [UIViewController: UIViewController] = [:]
  38. @objc private dynamic var _utm__childForHomeIndicatorAutoHidden: UIViewController? {
  39. Self._utm__childForHomeIndicatorAutoHiddenStorage[self]
  40. }
  41. @objc dynamic func setChildForHomeIndicatorAutoHidden(_ value: UIViewController?) {
  42. if let value = value {
  43. Self._utm__childForHomeIndicatorAutoHiddenStorage[self] = value
  44. } else {
  45. Self._utm__childForHomeIndicatorAutoHiddenStorage.removeValue(forKey: self)
  46. }
  47. setNeedsUpdateOfHomeIndicatorAutoHidden()
  48. }
  49. private static var _utm__childViewControllerForPointerLockStorage: [UIViewController: UIViewController] = [:]
  50. @objc private dynamic var _utm__childViewControllerForPointerLock: UIViewController? {
  51. Self._utm__childViewControllerForPointerLockStorage[self]
  52. }
  53. @objc dynamic func setChildViewControllerForPointerLock(_ value: UIViewController?) {
  54. if let value = value {
  55. Self._utm__childViewControllerForPointerLockStorage[self] = value
  56. } else {
  57. Self._utm__childViewControllerForPointerLockStorage.removeValue(forKey: self)
  58. }
  59. setNeedsUpdateOfPrefersPointerLocked()
  60. }
  61. /// SwiftUI currently does not provide a way to set the View Conrtoller's home indicator or pointer lock
  62. fileprivate static func patchViewController() {
  63. patch(#selector(getter: Self.childForHomeIndicatorAutoHidden),
  64. with: #selector(getter: Self._utm__childForHomeIndicatorAutoHidden),
  65. class: Self.self)
  66. patch(#selector(getter: Self.childViewControllerForPointerLock),
  67. with: #selector(getter: Self._utm__childViewControllerForPointerLock),
  68. class: Self.self)
  69. }
  70. }
  71. extension UIPress {
  72. @objc static weak var pressResponderOverride: UIResponder?
  73. @objc private dynamic var _utm__responder: UIResponder? {
  74. Self.pressResponderOverride ?? self._utm__responder
  75. }
  76. /// On iOS 15.0, there is a bug where SwiftUI does not propogate the presses event down
  77. /// to a child view controller. This is not seen in iOS 14.5 or iOS 15.1.
  78. fileprivate static func patchPress() {
  79. if #available(iOS 15.0, *) {
  80. if #unavailable(iOS 15.1) {
  81. patch(#selector(getter: Self.responder),
  82. with: #selector(getter: Self._utm__responder),
  83. class: Self.self)
  84. }
  85. }
  86. }
  87. }
  88. private var IndirectPointerTouchIgnoredHandle: Int = 0
  89. /// Patch to allow ignoring indirect touch when capturing pointer
  90. extension UIWindow {
  91. /// When true, `sendEvent(_:)` will ignore any indirect touch events.
  92. @objc var isIndirectPointerTouchIgnored: Bool {
  93. set {
  94. let number = NSNumber(booleanLiteral: newValue)
  95. objc_setAssociatedObject(self, &IndirectPointerTouchIgnoredHandle, number, .OBJC_ASSOCIATION_ASSIGN)
  96. }
  97. get {
  98. let number = objc_getAssociatedObject(self, &IndirectPointerTouchIgnoredHandle) as? NSNumber
  99. return number?.boolValue ?? false
  100. }
  101. }
  102. /// Replacement `sendEvent(_:)` function
  103. /// - Parameter event: The event to dispatch.
  104. @objc private func _utm__sendEvent(_ event: UIEvent) {
  105. if isIndirectPointerTouchIgnored && event.type == .touches {
  106. event.touches(for: self)?.forEach { touch in
  107. if touch.type == .indirectPointer {
  108. // for some reason, if we just ignore the event, future touch events get messed up
  109. // so as an alternative, we still pass the event through but with a modified coordinate
  110. touch.perform(Selector(("_setLocationInWindow:resetPrevious:")),
  111. with: CGPoint(x: -1, y: -1),
  112. with: true)
  113. }
  114. }
  115. }
  116. _utm__sendEvent(event)
  117. }
  118. fileprivate static func patchWindow() {
  119. patch(#selector(sendEvent),
  120. with: #selector(_utm__sendEvent),
  121. class: Self.self)
  122. }
  123. }