ToastWindow.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import UIKit
  2. open class ToastWindow: UIWindow {
  3. // MARK: - Public Property
  4. public static let shared = ToastWindow(frame: UIScreen.main.bounds, mainWindow: UIApplication.shared.keyWindow)
  5. override open var rootViewController: UIViewController? {
  6. get {
  7. guard !self.isShowing else {
  8. isShowing = false
  9. return nil
  10. }
  11. guard !self.isStatusBarOrientationChanging else { return nil }
  12. guard let firstWindow = UIApplication.shared.delegate?.window else { return nil }
  13. return firstWindow is ToastWindow ? nil : firstWindow?.rootViewController
  14. }
  15. set { /* Do nothing */ }
  16. }
  17. override open var isHidden: Bool {
  18. willSet {
  19. if #available(iOS 13.0, *) {
  20. isShowing = true
  21. }
  22. }
  23. didSet {
  24. if #available(iOS 13.0, *) {
  25. isShowing = false
  26. }
  27. }
  28. }
  29. /// Don't rotate manually if the application:
  30. ///
  31. /// - is running on iPad
  32. /// - is running on iOS 9
  33. /// - supports all orientations
  34. /// - doesn't require full screen
  35. /// - has launch storyboard
  36. ///
  37. var shouldRotateManually: Bool {
  38. let iPad = UIDevice.current.userInterfaceIdiom == .pad
  39. let application = UIApplication.shared
  40. let window = application.delegate?.window ?? nil
  41. let supportsAllOrientations = application.supportedInterfaceOrientations(for: window) == .all
  42. let info = Bundle.main.infoDictionary
  43. let requiresFullScreen = (info?["UIRequiresFullScreen"] as? NSNumber)?.boolValue == true
  44. let hasLaunchStoryboard = info?["UILaunchStoryboardName"] != nil
  45. if #available(iOS 9, *), iPad && supportsAllOrientations && !requiresFullScreen && hasLaunchStoryboard {
  46. return false
  47. }
  48. return true
  49. }
  50. // MARK: - Private Property
  51. /// Will not return `rootViewController` while this value is `true`. Or the rotation will be fucked in iOS 9.
  52. private var isStatusBarOrientationChanging = false
  53. /// Will not return `rootViewController` while this value is `true`. Needed for iOS 13.
  54. private var isShowing = false
  55. /// Returns original subviews. `ToastWindow` overrides `addSubview()` to add a subview to the
  56. /// top window instead itself.
  57. private var originalSubviews = NSPointerArray.weakObjects()
  58. private weak var mainWindow: UIWindow?
  59. // MARK: - Initializing
  60. public init(frame: CGRect, mainWindow: UIWindow?) {
  61. super.init(frame: frame)
  62. self.mainWindow = mainWindow
  63. self.isUserInteractionEnabled = false
  64. self.gestureRecognizers = nil
  65. #if swift(>=4.2)
  66. self.windowLevel = .init(rawValue: .greatestFiniteMagnitude)
  67. let willChangeStatusBarOrientationName = UIApplication.willChangeStatusBarOrientationNotification
  68. let didChangeStatusBarOrientationName = UIApplication.didChangeStatusBarOrientationNotification
  69. let didBecomeActiveName = UIApplication.didBecomeActiveNotification
  70. let keyboardWillShowName = UIWindow.keyboardWillShowNotification
  71. let keyboardDidHideName = UIWindow.keyboardDidHideNotification
  72. #else
  73. self.windowLevel = .greatestFiniteMagnitude
  74. let willChangeStatusBarOrientationName = NSNotification.Name.UIApplicationWillChangeStatusBarOrientation
  75. let didChangeStatusBarOrientationName = NSNotification.Name.UIApplicationDidChangeStatusBarOrientation
  76. let didBecomeActiveName = NSNotification.Name.UIApplicationDidBecomeActive
  77. let keyboardWillShowName = NSNotification.Name.UIKeyboardWillShow
  78. let keyboardDidHideName = NSNotification.Name.UIKeyboardDidHide
  79. #endif
  80. self.backgroundColor = .clear
  81. self.isHidden = false
  82. self.handleRotate(UIApplication.shared.statusBarOrientation)
  83. NotificationCenter.default.addObserver(
  84. self,
  85. selector: #selector(self.statusBarOrientationWillChange),
  86. name: willChangeStatusBarOrientationName,
  87. object: nil
  88. )
  89. NotificationCenter.default.addObserver(
  90. self,
  91. selector: #selector(self.statusBarOrientationDidChange),
  92. name: didChangeStatusBarOrientationName,
  93. object: nil
  94. )
  95. NotificationCenter.default.addObserver(
  96. self,
  97. selector: #selector(self.applicationDidBecomeActive),
  98. name: didBecomeActiveName,
  99. object: nil
  100. )
  101. NotificationCenter.default.addObserver(
  102. self,
  103. selector: #selector(self.keyboardWillShow),
  104. name: keyboardWillShowName,
  105. object: nil
  106. )
  107. NotificationCenter.default.addObserver(
  108. self,
  109. selector: #selector(self.keyboardDidHide),
  110. name: keyboardDidHideName,
  111. object: nil
  112. )
  113. }
  114. required public init?(coder aDecoder: NSCoder) {
  115. fatalError("init(coder:) has not been implemented: please use ToastWindow.shared")
  116. }
  117. // MARK: - Public method
  118. override open func addSubview(_ view: UIView) {
  119. super.addSubview(view)
  120. self.originalSubviews.addPointer(Unmanaged.passUnretained(view).toOpaque())
  121. self.topWindow()?.addSubview(view)
  122. }
  123. open override func becomeKey() {
  124. super.becomeKey()
  125. mainWindow?.makeKey()
  126. }
  127. // MARK: - Private method
  128. @objc private func statusBarOrientationWillChange() {
  129. self.isStatusBarOrientationChanging = true
  130. }
  131. @objc private func statusBarOrientationDidChange() {
  132. let orientation = UIApplication.shared.statusBarOrientation
  133. self.handleRotate(orientation)
  134. self.isStatusBarOrientationChanging = false
  135. }
  136. @objc private func applicationDidBecomeActive() {
  137. let orientation = UIApplication.shared.statusBarOrientation
  138. self.handleRotate(orientation)
  139. }
  140. @objc private func keyboardWillShow() {
  141. guard let topWindow = self.topWindow(),
  142. let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
  143. for subview in subviews {
  144. topWindow.addSubview(subview)
  145. }
  146. }
  147. @objc private func keyboardDidHide() {
  148. guard let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
  149. for subview in subviews {
  150. super.addSubview(subview)
  151. }
  152. }
  153. private func handleRotate(_ orientation: UIInterfaceOrientation) {
  154. let angle = self.angleForOrientation(orientation)
  155. if self.shouldRotateManually {
  156. self.transform = CGAffineTransform(rotationAngle: CGFloat(angle))
  157. }
  158. if let window = UIApplication.shared.windows.first {
  159. if orientation.isPortrait || !self.shouldRotateManually {
  160. self.frame.size.width = window.bounds.size.width
  161. self.frame.size.height = window.bounds.size.height
  162. } else {
  163. self.frame.size.width = window.bounds.size.height
  164. self.frame.size.height = window.bounds.size.width
  165. }
  166. }
  167. self.frame.origin = .zero
  168. DispatchQueue.main.async {
  169. ToastCenter.default.currentToast?.view.setNeedsLayout()
  170. }
  171. }
  172. private func angleForOrientation(_ orientation: UIInterfaceOrientation) -> Double {
  173. switch orientation {
  174. case .landscapeLeft: return -.pi / 2
  175. case .landscapeRight: return .pi / 2
  176. case .portraitUpsideDown: return .pi
  177. default: return 0
  178. }
  179. }
  180. /// Returns top window that isn't self
  181. private func topWindow() -> UIWindow? {
  182. if let window = UIApplication.shared.windows.last(where: {
  183. // https://github.com/devxoul/Toaster/issues/152
  184. KeyboardObserver.shared.didKeyboardShow || $0.isOpaque
  185. }), window !== self {
  186. return window
  187. }
  188. return nil
  189. }
  190. }