Window hide doesn't deallocate memory - swift

I have presented a custom UIWindow in order to show some screens. When I hide the window, the window instance doesn't free up. The underlying view controller also not getting released.
Custom window logic :
class SDKWindow: UIWindow {
// Unique tag to find the SDKWindow in the containers. Iterate all the windows and find the sdk window to show/hide any controller.
static let SDKWindowTag = 101011
init(rootController: UIViewController) {
if #available(iOS 13.0, *) {
let windowScene = UIApplication.shared
.connectedScenes
.filter { $0.activationState == .foregroundActive }
.first
if let windowScene = windowScene as? UIWindowScene {
super.init(windowScene: windowScene)
} else {
super.init(frame: UIScreen.main.bounds)
}
} else {
super.init(frame: UIScreen.main.bounds)
}
self.rootViewController = rootController
self.windowLevel = .statusBar - 1
self.tag = SDKWindow.SDKWindowTag
self.accessibilityLabel = "SDK.Window"
self.accessibilityViewIsModal = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func show() {
printLog("Show sdkwindow")
self.makeKeyAndVisible()
if self.isHidden == false {
return
}
self.isHidden = false
self.alpha = 1.0
}
func hide() {
if #available(iOS 13.0, *) {
self.windowScene = nil
} else {
self.rootViewController = nil
}
self.alpha = 0.0
self.isHidden = true
self.resignKey()
self.removeFromSuperview()
}
}
When the window presented is hidden
#objc public class MyClass: NSObject {
/// Private variables
private var popupWindow: SDKWindow?
public func showWindow() {
let viewController : CustomController = CustomController.init()
self.popupWindow = SDKWindow.init(rootController: viewController)
self.popupWindow?.show()
}
public func hide() {
self.popupWindow?.hide()
}
}
When hide method is called, CustomViewController Deinit is not called. But when
public func hide() {
self.popupWindow?.hide()
self.popupWindow = nil
}
is performed, the CustomViewController deinit is called and UIWindow instance is also deallocated in memory.

Related

Adding a view modifier inside .onChange

Is it possible to add a view modifier inside .onChange?
Simplified example:
content
.onChange(of: publishedValue) {
content.foregroundColor(.red)
}
I have a theme that when changed needs to change the status bar color. I have a view modifier created for that ( https://barstool.engineering/set-the-ios-status-bar-style-in-swiftui-using-a-custom-view-modifier/ ). The modifier works fine, but I need to update it as the publishedValue changes.
Actual minimal example:
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel: TestViewModel
var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
.onChange(of: viewModel.publishedValue) { newValue in
// Change status bar color
if viewModel.publishedValue % 2 == 0 {
self.body.statusBarStyle(.lightContent)
} else {
self.body.statusBarStyle(.darkContent)
}
}
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.statusBarStyle(.lightContent)
}
}
class TestViewModel: ObservableObject {
#Published var publishedValue: Int
init(publishedValue: Int) {
self.publishedValue = publishedValue
}
}
extension View {
/// Overrides the default status bar style with the given `UIStatusBarStyle`.
///
/// - Parameters:
/// - style: The `UIStatusBarStyle` to be used.
func statusBarStyle(_ style: UIStatusBarStyle) -> some View {
return self.background(HostingWindowFinder(callback: { window in
guard let rootViewController = window?.rootViewController else { return }
let hostingController = HostingViewController(rootViewController: rootViewController, style: style)
window?.rootViewController = hostingController
}))
}
}
fileprivate class HostingViewController: UIViewController {
private var rootViewController: UIViewController?
private var style: UIStatusBarStyle = .default
init(rootViewController: UIViewController, style: UIStatusBarStyle) {
self.rootViewController = rootViewController
self.style = style
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let child = rootViewController else { return }
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return style
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setNeedsStatusBarAppearanceUpdate()
}
}
fileprivate struct HostingWindowFinder: UIViewRepresentable {
var callback: (UIWindow?) -> ()
func makeUIView(context: Context) -> UIView {
let view = UIView()
DispatchQueue.main.async { [weak view] in
self.callback(view?.window)
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
// ...
}
}
GitHub repo for the example project: https://github.com/Iikeli/view-modifier-test
You are way overcomplicating this. Since your viewModel is #ObservedObject and the publishedValue is Published, the body of your View will be recalculated automatically every time publishedValue is updated. There's no need for a manual onChange.
You can simply move the logic into the input argument of statusBarStyle.
var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.statusBarStyle(viewModel.publishedValue % 2 == 0 ? .lightContent : .darkContent)
}
Or even better, move the logic into a separate computed property:
var body: some View {
....
.statusBarStyle(statusBarStyle)
}
private var statusBarStyle: UIStatusBarStyle {
viewModel.publishedValue % 2 == 0 ? .lightContent : .darkContent
}
The short answer is no, but that doesn't mean you can't use it to have views change based on some .onChange(..) action. For example.
#State var somethingChanged = false
Text(somethingChanged ? "First Value" : "Second Value")
// Your code/view
.onChange(..) {
//Some Condition or whatever you want.
somethingChanged = true
}
Your usage might look something like this.
content
.foregroundColor(somethingChanged ? .red : .blue)
.onChange(ofPublishedValue) {
somethingChanged = true
}
First of all, thanks for the help. Neither of the answers helped in my situation, since I couldn't get the modifier to update with the variable change. But with some Googling and trying out different solutions I figured out a working solution for updating the status bar colors.
I needed to update the style variable in the HostingViewController, and then update accordingly. So I added the HostingViewController as a #StateObject and updated the style variable inside the .onChange(). Not quite the solution I was going with to start out, but it does work.
The code:
import SwiftUI
import Introspect
struct ContentView: View {
#ObservedObject var viewModel: TestViewModel
#StateObject var hostingViewController: HostingViewController = .init(rootViewController: nil, style: .default)
var body: some View {
ZStack {
Rectangle().foregroundColor(.mint)
VStack(alignment: .center, spacing: 25) {
Text("Test text \(viewModel.publishedValue)")
.onChange(of: viewModel.publishedValue) { newValue in
// Change status bar color
if viewModel.publishedValue % 2 == 0 {
hostingViewController.style = .lightContent
} else {
hostingViewController.style = .darkContent
}
}
Button("Increment") {
viewModel.publishedValue += 1
}
}
}
.ignoresSafeArea()
.introspectViewController { viewController in
let window = viewController.view.window
guard let rootViewController = window?.rootViewController else { return }
hostingViewController.rootViewController = rootViewController
window?.rootViewController = hostingViewController
}
}
}
class TestViewModel: ObservableObject {
#Published var publishedValue: Int
init(publishedValue: Int) {
self.publishedValue = publishedValue
}
}
class HostingViewController: UIViewController, ObservableObject {
var rootViewController: UIViewController?
var style: UIStatusBarStyle = .default {
didSet {
self.rootViewController?.setNeedsStatusBarAppearanceUpdate()
}
}
init(rootViewController: UIViewController?, style: UIStatusBarStyle) {
self.rootViewController = rootViewController
self.style = style
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let child = rootViewController else { return }
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return style
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
setNeedsStatusBarAppearanceUpdate()
}
}
Big shoutout to this answer for giving me all that I needed to implement the solution. Note: You can override the rootViewController however you like, I used SwiftUI-Introspect as we are already using it in our project anyway.
GitHub branch

End or Dismiss Lottie Animation View if playOnce is finished in Swift

I already tried both loadingview.removeFromSuperView and loadingView.isHidden = true
Yes, it removes or hides the view, but I can't click on my root view anymore.
I also tried animatonview.background = .forceFinish, but doesn't do the job.
import UIKit
import Lottie
class LoadingAnimationView: UIView {
#IBOutlet weak var loadingView: UIView!
let animationView = AnimationView()
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func awakeFromNib() {
super.awakeFromNib()
}
func loadAnimation() {
let animation = Animation.named("success")
animationView.animation = animation
animationView.contentMode = .scaleAspectFill
loadingView.addSubview(animationView)
animationView.backgroundBehavior = .pauseAndRestore
animationView.translatesAutoresizingMaskIntoConstraints = false
animationView.topAnchor.constraint(equalTo: loadingView.layoutMarginsGuide.topAnchor).isActive = true
animationView.leadingAnchor.constraint(equalTo: loadingView.leadingAnchor, constant: 0).isActive = true
animationView.bottomAnchor.constraint(equalTo: loadingView.bottomAnchor).isActive = true
animationView.trailingAnchor.constraint(equalTo: loadingView.trailingAnchor, constant:0).isActive = true
animationView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
animationView.play(fromProgress: 0,
toProgress: 1,
loopMode: .playOnce,
completion: { (finished) in
if finished {
print("Animation Complete")
//please put solution here? dismiss or end loadingView or animationView
} else {
print("Animation cancelled")
}
})
}
EDIT 2:
I'm using the loadingView when the success message is received or 200.
func goOnlineMode(){
APIManager.sharedInstance.fetchServerStatus(completion: { data, error in
if error != nil{
print("Connection Failed")
} else {
if data?.status == 200 || data?.msg == "success" {
print("connected")
loadAnimation(true)
self.setCloudStateValue(value: true)
self.vc.cloudstateChecker()
} else {
print("fail to connect")
}
}
})
}
}
this is my function loading boolean in loadAnimation for Loading the xib.
func loadAnimation(_ display: Bool) {
if (display) {
let window = UIApplication.shared.keyWindow!
if Singleton.animationView == nil {
if let view = Bundle.main.loadNibNamed("LoadingAnimationView", owner: window, options:nil)![0] as? LoadingAnimationView {
Singleton.animationView = view
Singleton.animationView?.frame.size = CGSize(width: window.bounds.width, height: window.bounds.height)
window.addSubview(Singleton.animationView!)
window.layoutIfNeeded()
Singleton.animationView?.loadAnimation()
Singleton.animationView?.translatesAutoresizingMaskIntoConstraints = false
Singleton.animationView?.leftAnchor.constraint(equalTo: window.leftAnchor).isActive = true
Singleton.animationView?.rightAnchor.constraint(equalTo: window.rightAnchor).isActive = true
Singleton.animationView?.topAnchor.constraint(equalTo: window.topAnchor, constant:-60).isActive = true
Singleton.animationView?.bottomAnchor.constraint(equalTo: window.bottomAnchor).isActive = true
window.layoutIfNeeded()
}
}
} else {
if (Singleton.animationView != nil) {
Singleton.animationView?.removeFromSuperview()
Singleton.animationView = nil
}
}
}
Try with this:
Swift 5
animationView.play { (finished) in
animationViewNewOrder!.isHidden = true
}
I solved my problem by using NotificationCenter
Swift 4.2
Add this NotificationCenter Observer in your MainViewController, and also register a Notification.Name to your Constants
NotificationCenter.default.addObserver(self, selector: #selector(removeAnimation(notification:)), name: HIDE_ANIMATION, object: nil)
}
also add this together with your observer
#objc func removeAnimation(notification:NSNotification) {
loadingAnimation(false)
}
I put this Notification Post in my newly created hideAnimation function in LoadingAnimationView.
func hideAnimation() {
NotificationCenter.default.post(name: Notification.Name(HIDE_ANIMATION.rawValue), object: nil)
loadingView.removeFromSuperview()
}
and put the hideAnimation function to your completion finish.

Changing height of a sidemenu

This is my actual view
I want that the sidemenu begins at the first grey line and not on the top of the view. I would be glad if I could get an answer how i can set the height of it but i am also happy if someone could suggest me a workaround like hiding it behind the searchbar and segmented control. Thank you in advance.
This is my ENSideMenu.swift implementation
import UIKit
#objc public protocol ENSideMenuDelegate {
optional func sideMenuWillOpen()
optional func sideMenuWillClose()
optional func sideMenuDidOpen()
optional func sideMenuDidClose()
optional func sideMenuShouldOpenSideMenu () -> Bool
}
#objc public protocol ENSideMenuProtocol {
var sideMenu : ENSideMenu? { get }
func setContentViewController(contentViewController: UIViewController)
}
public enum ENSideMenuAnimation : Int {
case None
case Default
}
/**
The position of the side view on the screen.
- Left: Left side of the screen
- Right: Right side of the screen
*/
public enum ENSideMenuPosition : Int {
case Left
case Right
}
public extension UIViewController {
/**
Changes current state of side menu view.
*/
public func toggleSideMenuView () {
sideMenuController()?.sideMenu?.toggleMenu()
}
/**
Hides the side menu view.
*/
public func hideSideMenuView () {
sideMenuController()?.sideMenu?.hideSideMenu()
}
/**
Shows the side menu view.
*/
public func showSideMenuView () {
sideMenuController()?.sideMenu?.showSideMenu()
}
/**
Returns a Boolean value indicating whether the side menu is showed.
:returns: BOOL value
*/
public func isSideMenuOpen () -> Bool {
let sieMenuOpen = self.sideMenuController()?.sideMenu?.isMenuOpen
return sieMenuOpen!
}
/**
* You must call this method from viewDidLayoutSubviews in your content view controlers so it fixes size and position of the side menu when the screen
* rotates.
* A convenient way to do it might be creating a subclass of UIViewController that does precisely that and then subclassing your view controllers from it.
*/
func fixSideMenuSize() {
if let navController = self.navigationController as? ENSideMenuNavigationController {
navController.sideMenu?.updateFrame()
}
}
/**
Returns a view controller containing a side menu
:returns: A `UIViewController`responding to `ENSideMenuProtocol` protocol
*/
public func sideMenuController () -> ENSideMenuProtocol? {
var iteration : UIViewController? = self.parentViewController
if (iteration == nil) {
return topMostController()
}
repeat {
if (iteration is ENSideMenuProtocol) {
return iteration as? ENSideMenuProtocol
} else if (iteration?.parentViewController != nil && iteration?.parentViewController != iteration) {
iteration = iteration!.parentViewController
} else {
iteration = nil
}
} while (iteration != nil)
return iteration as? ENSideMenuProtocol
}
internal func topMostController () -> ENSideMenuProtocol? {
var topController : UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController
if (topController is UITabBarController) {
topController = (topController as! UITabBarController).selectedViewController
}
while (topController?.presentedViewController is ENSideMenuProtocol) {
topController = topController?.presentedViewController
}
return topController as? ENSideMenuProtocol
}
}
public class ENSideMenu : NSObject, UIGestureRecognizerDelegate {
/// The width of the side menu view. The default value is 160.
public var menuWidth : CGFloat = 90.0 {
didSet {
needUpdateApperance = true
updateFrame()
}
}
private var menuPosition:ENSideMenuPosition = .Right
private var blurStyle: UIBlurEffectStyle = .Light
/// A Boolean value indicating whether the bouncing effect is enabled. The default value is TRUE.
public var bouncingEnabled :Bool = true
/// The duration of the slide animation. Used only when `bouncingEnabled` is FALSE.
public var animationDuration = 0.25
private let sideMenuContainerView = UIView()
private(set) var menuViewController : UIViewController!
private var animator : UIDynamicAnimator!
private var sourceView : UIView!
private var needUpdateApperance : Bool = false
/// The delegate of the side menu
public weak var delegate : ENSideMenuDelegate?
private(set) var isMenuOpen : Bool = false
/// A Boolean value indicating whether the left swipe is enabled.
public var allowLeftSwipe : Bool = true
/// A Boolean value indicating whether the right swipe is enabled.
public var allowRightSwipe : Bool = true
public var allowPanGesture : Bool = true
private var panRecognizer : UIPanGestureRecognizer?
/**
Initializes an instance of a `ENSideMenu` object.
:param: sourceView The parent view of the side menu view.
:param: menuPosition The position of the side menu view.
:returns: An initialized `ENSideMenu` object, added to the specified view.
*/
public init(sourceView: UIView, menuPosition: ENSideMenuPosition, blurStyle: UIBlurEffectStyle = .Light) {
super.init()
self.sourceView = sourceView
self.menuPosition = menuPosition
self.blurStyle = blurStyle
self.setupMenuView()
animator = UIDynamicAnimator(referenceView:sourceView)
animator.delegate = self
self.panRecognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
panRecognizer!.delegate = self
sourceView.addGestureRecognizer(panRecognizer!)
// Add right swipe gesture recognizer
let rightSwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "handleGesture:")
rightSwipeGestureRecognizer.delegate = self
rightSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Right
// Add left swipe gesture recognizer
let leftSwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "handleGesture:")
leftSwipeGestureRecognizer.delegate = self
leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Left
if (menuPosition == .Left) {
sourceView.addGestureRecognizer(rightSwipeGestureRecognizer)
sideMenuContainerView.addGestureRecognizer(leftSwipeGestureRecognizer)
}
else {
sideMenuContainerView.addGestureRecognizer(rightSwipeGestureRecognizer)
sourceView.addGestureRecognizer(leftSwipeGestureRecognizer)
}
}
/**
Initializes an instance of a `ENSideMenu` object.
:param: sourceView The parent view of the side menu view.
:param: menuViewController A menu view controller object which will be placed in the side menu view.
:param: menuPosition The position of the side menu view.
:returns: An initialized `ENSideMenu` object, added to the specified view, containing the specified menu view controller.
*/
public convenience init(sourceView: UIView, menuViewController: UIViewController, menuPosition: ENSideMenuPosition, blurStyle: UIBlurEffectStyle = .Light) {
self.init(sourceView: sourceView, menuPosition: menuPosition, blurStyle: blurStyle)
self.menuViewController = menuViewController
self.menuViewController.view.frame = sideMenuContainerView.bounds
self.menuViewController.view.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
sideMenuContainerView.addSubview(self.menuViewController.view)
}
/*
public convenience init(sourceView: UIView, view: UIView, menuPosition: ENSideMenuPosition) {
self.init(sourceView: sourceView, menuPosition: menuPosition)
view.frame = sideMenuContainerView.bounds
view.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
sideMenuContainerView.addSubview(view)
}
*/
/**
Updates the frame of the side menu view.
*/
func updateFrame() {
var width:CGFloat
var height:CGFloat
(width, height) = adjustFrameDimensions( sourceView.frame.size.width, height: sourceView.frame.size.height)
let menuFrame = CGRectMake(
(menuPosition == .Left) ?
isMenuOpen ? 0 : -menuWidth-1.0 :
isMenuOpen ? width - menuWidth : width+1.0,
sourceView.frame.origin.y,
menuWidth,
height
)
sideMenuContainerView.frame = menuFrame
}
private func adjustFrameDimensions( width: CGFloat, height: CGFloat ) -> (CGFloat,CGFloat) {
if floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_7_1 &&
(UIApplication.sharedApplication().statusBarOrientation == UIInterfaceOrientation.LandscapeRight ||
UIApplication.sharedApplication().statusBarOrientation == UIInterfaceOrientation.LandscapeLeft) {
// iOS 7.1 or lower and landscape mode -> interchange width and height
return (height, width)
}
else {
return (width, height)
}
}
private func setupMenuView() {
// Configure side menu container
updateFrame()
sideMenuContainerView.backgroundColor = UIColor.clearColor()
sideMenuContainerView.clipsToBounds = false
sideMenuContainerView.layer.masksToBounds = false
sideMenuContainerView.layer.shadowOffset = (menuPosition == .Left) ? CGSizeMake(1.0, 1.0) : CGSizeMake(-1.0, -1.0)
sideMenuContainerView.layer.shadowRadius = 1.25
sideMenuContainerView.layer.shadowOpacity = 0.125
sideMenuContainerView.layer.shadowPath = UIBezierPath(rect: sideMenuContainerView.bounds).CGPath
sourceView.addSubview(sideMenuContainerView)
if (NSClassFromString("UIVisualEffectView") != nil) {
// Add blur view
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: blurStyle)) as UIVisualEffectView
visualEffectView.frame = sideMenuContainerView.bounds
visualEffectView.autoresizingMask = [.FlexibleHeight, .FlexibleWidth]
sideMenuContainerView.addSubview(visualEffectView)
}
else {
// TODO: add blur for ios 7
}
}
private func toggleMenu (shouldOpen: Bool) {
if (shouldOpen && delegate?.sideMenuShouldOpenSideMenu?() == false) {
return
}
updateSideMenuApperanceIfNeeded()
isMenuOpen = shouldOpen
var width:CGFloat
var height:CGFloat
(width, height) = adjustFrameDimensions( sourceView.frame.size.width, height: sourceView.frame.size.height)
if (bouncingEnabled) {
animator.removeAllBehaviors()
var gravityDirectionX: CGFloat
var pushMagnitude: CGFloat
var boundaryPointX: CGFloat
var boundaryPointY: CGFloat
if (menuPosition == .Left) {
// Left side menu
gravityDirectionX = (shouldOpen) ? 1 : -1
pushMagnitude = (shouldOpen) ? 20 : -20
boundaryPointX = (shouldOpen) ? menuWidth : -menuWidth-2
boundaryPointY = 20
}
else {
// Right side menu
gravityDirectionX = (shouldOpen) ? -1 : 1
pushMagnitude = (shouldOpen) ? -20 : 20
boundaryPointX = (shouldOpen) ? width-menuWidth : width+menuWidth+2
boundaryPointY = -20
}
let gravityBehavior = UIGravityBehavior(items: [sideMenuContainerView])
gravityBehavior.gravityDirection = CGVectorMake(gravityDirectionX, 0)
animator.addBehavior(gravityBehavior)
let collisionBehavior = UICollisionBehavior(items: [sideMenuContainerView])
collisionBehavior.addBoundaryWithIdentifier("menuBoundary", fromPoint: CGPointMake(boundaryPointX, boundaryPointY),
toPoint: CGPointMake(boundaryPointX, height))
animator.addBehavior(collisionBehavior)
let pushBehavior = UIPushBehavior(items: [sideMenuContainerView], mode: UIPushBehaviorMode.Instantaneous)
pushBehavior.magnitude = pushMagnitude
animator.addBehavior(pushBehavior)
let menuViewBehavior = UIDynamicItemBehavior(items: [sideMenuContainerView])
menuViewBehavior.elasticity = 0.25
animator.addBehavior(menuViewBehavior)
}
else {
var destFrame :CGRect
if (menuPosition == .Left) {
destFrame = CGRectMake((shouldOpen) ? -2.0 : -menuWidth, 0, menuWidth, height)
}
else {
destFrame = CGRectMake((shouldOpen) ? width-menuWidth : width+2.0,
0,
menuWidth,
height)
}
UIView.animateWithDuration(
animationDuration,
animations: { () -> Void in
self.sideMenuContainerView.frame = destFrame
},
completion: { (Bool) -> Void in
if (self.isMenuOpen) {
self.delegate?.sideMenuDidOpen?()
} else {
self.delegate?.sideMenuDidClose?()
}
})
}
if (shouldOpen) {
delegate?.sideMenuWillOpen?()
} else {
delegate?.sideMenuWillClose?()
}
/*let outterView = UIView(frame: CGRectMake(sideMenuContainerView.frame.width, 0,
sourceView.frame.width - sideMenuContainerView.frame.width,
sourceView.frame.height))
outterView.backgroundColor = UIColor.clearColor()
let tapRecognizer = UITapGestureRecognizer(target: self, action: "hideSideMenu")
outterView.addGestureRecognizer(tapRecognizer)
outterView.userInteractionEnabled = false
sourceView.addSubview(outterView)
sourceView.layer.zPosition = 0
outterView.userInteractionEnabled = shouldOpen */
}
public func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UISwipeGestureRecognizer {
let swipeGestureRecognizer = gestureRecognizer as! UISwipeGestureRecognizer
if !self.allowLeftSwipe {
if swipeGestureRecognizer.direction == .Left {
return false
}
}
if !self.allowRightSwipe {
if swipeGestureRecognizer.direction == .Right {
return false
}
}
}
else if gestureRecognizer.isEqual(panRecognizer) {
if allowPanGesture == false {
return false
}
animator.removeAllBehaviors()
let touchPosition = gestureRecognizer.locationOfTouch(0, inView: sourceView)
if menuPosition == .Left {
if isMenuOpen {
if touchPosition.x < menuWidth {
return true
}
}
else {
if touchPosition.x < 25 {
return true
}
}
}
else {
if isMenuOpen {
if touchPosition.x > CGRectGetWidth(sourceView.frame) - menuWidth {
return true
}
}
else {
if touchPosition.x > CGRectGetWidth(sourceView.frame)-25 {
return true
}
}
}
return false
}
return true
}
internal func handleGesture(gesture: UISwipeGestureRecognizer) {
toggleMenu((self.menuPosition == .Right && gesture.direction == .Left)
|| (self.menuPosition == .Left && gesture.direction == .Right))
}
internal func handlePan(recognizer : UIPanGestureRecognizer){
let leftToRight = recognizer.velocityInView(recognizer.view).x > 0
switch recognizer.state {
case .Began:
break
case .Changed:
let translation = recognizer.translationInView(sourceView).x
let xPoint : CGFloat = sideMenuContainerView.center.x + translation + (menuPosition == .Left ? 1 : -1) * menuWidth / 2
if menuPosition == .Left {
if xPoint <= 0 || xPoint > CGRectGetWidth(self.sideMenuContainerView.frame) {
return
}
}else{
if xPoint <= sourceView.frame.size.width - menuWidth || xPoint >= sourceView.frame.size.width
{
return
}
}
sideMenuContainerView.center.x = sideMenuContainerView.center.x + translation
recognizer.setTranslation(CGPointZero, inView: sourceView)
default:
let shouldClose = menuPosition == .Left ? !leftToRight && CGRectGetMaxX(sideMenuContainerView.frame) < menuWidth : leftToRight && CGRectGetMinX(sideMenuContainerView.frame) > (sourceView.frame.size.width - menuWidth)
toggleMenu(!shouldClose)
}
}
private func updateSideMenuApperanceIfNeeded () {
if (needUpdateApperance) {
var frame = sideMenuContainerView.frame
frame.size.width = menuWidth
sideMenuContainerView.frame = frame
sideMenuContainerView.layer.shadowPath = UIBezierPath(rect: sideMenuContainerView.bounds).CGPath
needUpdateApperance = false
}
}
/**
Toggles the state of the side menu.
*/
public func toggleMenu () {
if (isMenuOpen) {
toggleMenu(false)
}
else {
updateSideMenuApperanceIfNeeded()
toggleMenu(true)
}
}
/**
Shows the side menu if the menu is hidden.
*/
public func showSideMenu () {
if (!isMenuOpen) {
toggleMenu(true)
}
}
/**
Hides the side menu if the menu is showed.
*/
public func hideSideMenu () {
if (isMenuOpen) {
toggleMenu(false)
}
}
}
extension ENSideMenu: UIDynamicAnimatorDelegate {
public func dynamicAnimatorDidPause(animator: UIDynamicAnimator) {
if (self.isMenuOpen) {
self.delegate?.sideMenuDidOpen?()
} else {
self.delegate?.sideMenuDidClose?()
}
}
public func dynamicAnimatorWillResume(animator: UIDynamicAnimator) {
print("resume")
}
}
This is my ENSideMenuNavigationController.swift
import UIKit
public class ENSideMenuNavigationController: UINavigationController, ENSideMenuProtocol {
public var sideMenu : ENSideMenu?
public var sideMenuAnimationType : ENSideMenuAnimation = .Default
// MARK: - Life cycle
public override func viewDidLoad() {
super.viewDidLoad()
}
public init( menuViewController: UIViewController, contentViewController: UIViewController?) {
super.init(nibName: nil, bundle: nil)
if (contentViewController != nil) {
self.viewControllers = [contentViewController!]
}
sideMenu = ENSideMenu(sourceView: self.view, menuViewController: menuViewController, menuPosition:.Right)
view.bringSubviewToFront(navigationBar)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
public override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Navigation
public func setContentViewController(contentViewController: UIViewController) {
self.sideMenu?.toggleMenu()
switch sideMenuAnimationType {
case .None:
self.viewControllers = [contentViewController]
break
default:
contentViewController.navigationItem.hidesBackButton = true
self.setViewControllers([contentViewController], animated: true)
break
}
}
}
This is my MyMenuTableViewController:
import UIKit
class MyMenuTableViewController: UITableViewController {
var selectedMenuItem : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Customize apperance of table view
tableView.contentInset = UIEdgeInsetsMake(116.0, 0, 0, 0) //
tableView.separatorStyle = .None
tableView.backgroundColor = UIColor.whiteColor()
tableView.scrollsToTop = false
tableView.allowsSelection = false
// Preserve selection between presentations
self.clearsSelectionOnViewWillAppear = false
tableView.selectRowAtIndexPath(NSIndexPath(forRow: selectedMenuItem, inSection: 0), animated: false, scrollPosition: .Middle)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return 1
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("CELL")
if (cell == nil) {
cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "CELL")
cell!.backgroundColor = UIColor.clearColor()
cell!.textLabel?.textColor = UIColor.darkGrayColor()
let selectedBackgroundView = UIView(frame: CGRectMake(0, 0, cell!.frame.size.width, cell!.frame.size.height))
selectedBackgroundView.backgroundColor = UIColor.grayColor().colorWithAlphaComponent(0.2)
cell!.selectedBackgroundView = selectedBackgroundView
}
//cell!.textLabel?.text = "social Network #\(indexPath.row+1)"
cell!.imageView?.image = UIImage(named: "Facebook")
return cell!
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 45.0
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print("did select row: \(indexPath.row)")
if (indexPath.row == selectedMenuItem) {
return
}
selectedMenuItem = indexPath.row
//Present new view controller
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main",bundle: nil)
var destViewController : UIViewController
switch (indexPath.row) {
case 0:
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController1")
break
case 1:
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController2")
break
case 2:
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController3")
break
default:
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("ViewController4")
break
}
sideMenuController()?.setContentViewController(destViewController)
}
}
This is my MyNavigationController.swift
import UIKit
class MyNavigationController: ENSideMenuNavigationController, ENSideMenuDelegate {
override func viewDidLoad() {
super.viewDidLoad()
sideMenu = ENSideMenu(sourceView: self.view, menuViewController: MyMenuTableViewController(), menuPosition:.Right)
//sideMenu?.delegate = self //optional
sideMenu?.menuWidth = 90.0 // optional, default is 160
// sideMenu?.bouncingEnabled = false
//sideMenu?.allowPanGesture = false
// make navigation bar showing over side menu
view.bringSubviewToFront(navigationBar)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - ENSideMenu Delegate
func sideMenuWillOpen() {
print("sideMenuWillOpen")
}
func sideMenuWillClose() {
print("sideMenuWillClose")
}
func sideMenuDidClose() {
print("sideMenuDidClose")
}
func sideMenuDidOpen() {
print("sideMenuDidOpen")
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
In your menu tableViewController you should implement
let MinHeight: CGFloat = 100.0
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let tHeight = tableView.bounds.height
let temp = tHeight/CGFloat(items.count) //Item size on your side menu
return temp > MinHeight ? temp : MinHeight
}

How to override trait collection for initial UIViewController? (with Storyboard)

I have an app targeted iOS8 and initial view controller is UISplitViewController. I use storyboard, so that it kindly instantiate everything for me.
Because of my design I need SplitViewController to show both master and detail views in portrait mode on iPhone. So I am looking for a way to override trait collection for this UISplitViewController.
I found that I can use
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) { ... }
but, unfortunately, there are only methods to override child controllers traits collections:
setOverrideTraitCollection(collection: UITraitCollection!, forChildViewController childViewController: UIViewController!)
and I can't do so for self in my UISplitViewController subclass.
I checked an example app Adaptive Photos from Apple. And in this app author use special TraitOverrideViewController as root and some magic in his viewController setter to make it all works.
It looks horrible for me. Is there are any way around to override traits? Or If there are not, how can I manage to use the same hack with storyboard? In other words, how to inject some viewController as root one only to handle traits for my UISplitViewController with storyboard?
Ok, I wish there was another way around this, but for now I just converted code from the Apple example to Swift and adjusted it to use with Storyboards.
It works, but I still believe it is an awful way to archive this goal.
My TraitOverride.swift:
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMoveToParentViewController(nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMoveToParentViewController(self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
setForcedTraitForSize(size)
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.Phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
case (.Pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
performSegueWithIdentifier("toSplitVC", sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
if segue.identifier == "toSplitVC" {
let destinationVC = segue.destinationViewController as UIViewController
viewController = destinationVC
}
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}
To make it work you need to add a new UIViewController on the storyboard and made it the initial. Add show segue from it to your real controller like this:
You need to name the segue "toSplitVC":
and set initial controller to be TraitOverride:
Now it should work for you too. Let me know if you find a better way or any flaws in this one.
I understand that you wanted a SWIFT translation here... And you've probably solved that.
Below is something I've spent a considerable time trying to resolve - getting my SplitView to work on an iPhone 6+ - this is a Cocoa solution.
My Application is TabBar based and the SplitView has Navigation Controllers. In the end my issue was that setOverrideTraitCollection was not being sent to the correct target.
#interface myUITabBarController ()
#property (nonatomic, retain) UITraitCollection *overrideTraitCollection;
#end
#implementation myUITabBarController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performTraitCollectionOverrideForSize:self.view.bounds.size];
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
[self performTraitCollectionOverrideForSize:size];
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}
- (void)performTraitCollectionOverrideForSize:(CGSize)size
{
NSLog(#"myUITabBarController %#", NSStringFromSelector(_cmd));
_overrideTraitCollection = nil;
if (size.width > 320.0)
{
_overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:self];
for (UIViewController * view in self.childViewControllers)
{
[self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:view];
NSLog(#"myUITabBarController %# AFTER viewTrait=%#", NSStringFromSelector(_cmd), [view traitCollection]);
}
}
#end
UPDATE:
Apple do not recommend doing this:
Use the traitCollection property directly. Do not override it. Do not
provide a custom implementation.
I'm not overriding this property anymore! Now I'm calling overrideTraitCollectionForChildViewController: in the parent viewControler class.
Old answer:
I know it's more than a year since question was asked, but i think my answer will help someone like me who do not achieved success with the accepted answer.
Whell the solution is really simple, you can just override traitCollection: method. Here is an example from my app:
- (UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
return super.traitCollection;
} else {
switch (self.modalPresentationStyle) {
case UIModalPresentationFormSheet:
case UIModalPresentationPopover:
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
default:
return super.traitCollection;
}
}
}
the idea is to force Compact size class on iPad if controller is presented as popover or form sheet.
Hope it helps.
The extra top level VC works well for a simple app but it won't propagate down to modally presented VC's as they don't have a parentVC. So you need to insert it again in different places.
A better approach I found was just to subclass UINavigationController and then just use your subclass in the storyboard and elsewhere where you would normally use UINavigationController. It saves the additional VC clutter in storyboards and also saves extra clutter in code.
This example will make all iPhones use regular horizontal size class for landscape.
#implementation MyNavigationController
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
UIDevice *device = [UIDevice currentDevice];
if (device.userInterfaceIdiom == UIUserInterfaceIdiomPhone && CGRectGetWidth(childViewController.view.bounds) > CGRectGetHeight(childViewController.view.bounds)) {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
return nil;
}
#end
Yes, it must use custom container View Controller to override the function viewWillTransitionToSize. You use the storyboard to set the container View Controller as initial.
Also, you can refer this good artical which use the program to implement it. According to it, your judgement portait could have some limitations:
var portrait: Bool {
if device == .Phone {
return size.width > 320
} else {
return size.width > 768
}
}
other than
if **size.width > size.height**{
self.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClass.Regular), forChildViewController: viewController)
}
else{
self.setOverrideTraitCollection(nil, forChildViewController: viewController)
}
"
Props To #Ilyca
Swift 3
import UIKit
class TraitOverride: UIViewController {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
}
var forcedTraitCollection: UITraitCollection? {
didSet {
updateForcedTraitCollection()
}
}
override func viewDidLoad() {
setForcedTraitForSize(size: view.bounds.size)
}
var viewController: UIViewController? {
willSet {
if let previousVC = viewController {
if newValue !== previousVC {
previousVC.willMove(toParentViewController: nil)
setOverrideTraitCollection(nil, forChildViewController: previousVC)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
}
}
}
didSet {
if let vc = viewController {
addChildViewController(vc)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
updateForcedTraitCollection()
}
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
setForcedTraitForSize(size: size)
super.viewWillTransition(to: size, with: coordinator)
}
func setForcedTraitForSize (size: CGSize) {
let device = traitCollection.userInterfaceIdiom
var portrait: Bool {
if device == .phone {
return size.width > 320
} else {
return size.width > 768
}
}
switch (device, portrait) {
case (.phone, true):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .regular)
case (.pad, false):
forcedTraitCollection = UITraitCollection(horizontalSizeClass: .compact)
default:
forcedTraitCollection = nil
}
}
func updateForcedTraitCollection() {
if let vc = viewController {
setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
performSegue(withIdentifier: "toSplitVC", sender: self)
}
override var shouldAutomaticallyForwardAppearanceMethods: Bool {
return true
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return true
}
}

How to add a UIView over the Keyboard - iOS

I have been trying to display a toast message on iOS. What I did was when any notification comes, just I took the navigation controller view and added a subview for my toast message and displayed.
UIView *top_view = self.navigationController.view;
[top_view showToast:string];
Everything works fine. However my toast view is not adding over the keyboard(if the keyboard is at the front). Any idea what could be the problem... Little helps may save my time... Thanx..
You can display the toast by adding subview to your main window.
UIWindow *toastDisplaywindow = [[[UIApplication sharedApplication] delegate] window];;
for (UIWindow *testWindow in [[UIApplication sharedApplication] windows])
{
if (![[testWindow class] isEqual:[UIWindow class]])
{
self.toastDisplaywindow = testWindow;
break;
}
}
[toastDisplaywindow showToast:string];
If a keyboard is being displayed, it will be displayed as a separate window, above your usual main window. Hence a check made to find out if the keyboard is being displayed. If it is, then add the toast message on that window, else on the main window.
I found another method in this link, using which you can directly get to the UIView of the keyboard (If required).
You have to add your subview to:
UIWindow *window = [UIApplication sharedApplication].windows.lastObject;
which is on top of the keyboard.
Generally keyboard view is not part of your main window. it appears with new window when you get focused in any text field.
Try the following code to access your keyboard view.
[[[UIApplication sharedApplication] windows] objectAtIndex:1]
Remember, this will only work when you have keyboard on your screen.
Another way is to add a custom UIWindow, then setting it's WindowLevel to +1 of the last window.
Something like this
NSArray *windows = [[UIApplication sharedApplication] windows];
UIWindow *lastWindow = (UIWindow *)[windows lastObject];
myWindow.windowLevel = lastWindow.windowLevel + 1;
Take a look to this topic
https://forums.developer.apple.com/thread/16375
Update for Swift3
UIApplication.shared.windows.last
in iOS9 the answer by Adithya is not work,
UIWindow *window = [UIApplication sharedApplication].windows.lastObject;
work well
Try to add the view as a subview of the following class. This code snippet works for iOS 14 and above. Not sure about older versions. Reference: Toaster Github repo
Use it like:
ToastWindow.shared.addSubView(/your_view/)
public final class ToastWindow: UIWindow {
public static let shared = ToastWindow(frame: UIScreen.main.bounds, mainWindow: UIApplication.shared.connectedScenes.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }.first { $0.isKeyWindow } )
override public var rootViewController: UIViewController? {
get {
guard !self.isShowing else {
isShowing = false
return nil
}
guard let firstWindow = UIApplication.shared.delegate?.window else { return nil }
return firstWindow is ToastWindow ? nil : firstWindow?.rootViewController
}
set { /* Do nothing */ }
}
override public var isHidden: Bool {
willSet { isShowing = true }
didSet { isShowing = false }
}
/// Will not return `rootViewController` while this value is `true`. Needed for iOS 13.
private var isShowing = false
/// Returns original subviews. `ToastWindow` overrides `addSubview()` to add a subview to the
/// top window instead itself.
private var originalSubviews = NSPointerArray.weakObjects()
private weak var mainWindow: UIWindow?
// MARK: - Init
public init(frame: CGRect, mainWindow: UIWindow?) {
super.init(frame: frame)
self.mainWindow = mainWindow
self.isUserInteractionEnabled = false
self.gestureRecognizers = nil
self.windowLevel = .init(rawValue: .greatestFiniteMagnitude)
let keyboardWillShowName = UIWindow.keyboardWillShowNotification
let keyboardDidHideName = UIWindow.keyboardDidHideNotification
self.backgroundColor = .clear
self.isHidden = false
NotificationCenter.default.addObserver( self, selector: #selector(self.keyboardWillShow),
name: keyboardWillShowName,
object: nil )
NotificationCenter.default.addObserver( self, selector: #selector(self.keyboardDidHide),
name: keyboardDidHideName,
object: nil )
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented: please use ToastWindow.shared")
}
override public func addSubview(_ view: UIView) {
super.addSubview(view)
self.originalSubviews.addPointer(Unmanaged.passUnretained(view).toOpaque())
self.topWindow()?.addSubview(view)
}
public override func becomeKey() {
super.becomeKey()
mainWindow?.makeKey()
}
// MARK: - Keyboard methods
#objc private func keyboardWillShow() {
guard let topWindow = self.topWindow(),
let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
for subview in subviews {
topWindow.addSubview(subview)
}
}
#objc private func keyboardDidHide() {
guard let subviews = self.originalSubviews.allObjects as? [UIView] else { return }
for subview in subviews {
super.addSubview(subview)
}
}
/// Returns top window that isn't self
private func topWindow() -> UIWindow? {
if let window = UIApplication.shared.windows.last(where: {
ToastWindowKeyboardObserver.shared.didKeyboardShow || $0.isOpaque
}), window !== self {
return window
}
return nil
}
}
final fileprivate class ToastWindowKeyboardObserver {
static let shared = ToastWindowKeyboardObserver()
private(set) var didKeyboardShow: Bool = false
private(set) var keyboardHeight = 0.0
private init() {
let keyboardWillShowName = UIWindow.keyboardWillShowNotification
let keyboardDidHideName = UIWindow.keyboardDidHideNotification
NotificationCenter.default.addObserver( self, selector: #selector(keyboardWillShow),
name: keyboardWillShowName,
object: nil )
NotificationCenter.default.addObserver( self, selector: #selector(keyboardDidHide),
name: keyboardDidHideName,
object: nil )
}
#objc func keyboardWillShow(_ notification: Notification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
keyboardHeight = keyboardRectangle.height
}
didKeyboardShow = true
}
#objc private func keyboardDidHide() {
didKeyboardShow = false
}
}