Maintain custom UIView that is added to KeyWindow - ios

I am working on a sync feature for my app.
What I want to achieve is to display a custom UIView that will serve as an indicator to the user, wherever screen the user at (such as navigate to dashboard, settings, profile page, etc), that the synchronization is in progress. In short term, it will stick to a position within the app statically.
After done some researches from the web, I come to a conclusion to use keyWindow and add a subview to it.
Here is my code
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let window = UIApplication.shared.keyWindow {
window.windowLevel = .statusBar
let uiView = UIView()
uiView.backgroundColor = .green
window.addSubview(uiView)
uiView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
uiView.heightAnchor.constraint(equalToConstant: 150),
uiView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25),
uiView.topAnchor.constraint(equalTo: view.topAnchor, constant: 150),
uiView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4)
])
}
}
Above code is working fine, I can attach a custom view to the keyWindow. However, when the user navigate to another screen, the added customView will dissappear, and only show when the user goes back to the previous screen.
Can someone guide on what part I am missing? On which part I did wrong?
Thanks

As per comments I am adding a minimum requirement to create a new window which will can now always be on top of your view controller or even other windows:
class OverlayViewController: UIViewController {
private var myWindow: UIWindow? // Our own window needs to be retained
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
view.addSubview({
let label = UILabel(frame: .zero)
label.text = "This is an overlay."
label.sizeToFit()
label.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
return label
}())
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap() {
myWindow?.isHidden = true
myWindow?.removeFromSuperview()
myWindow = nil
}
static func showInNewWindow() {
let window = UIWindow(frame: UIScreen.main.bounds)
// window.windowLevel = .alert
// window.isUserInteractionEnabled = false
window.rootViewController = {
let controller = OverlayViewController() // OR UIStoryboard(name: "<#FileName#>", bundle: nil).instantiateViewController(withIdentifier: "<#Storyboard Identifier#>")
controller.myWindow = window
return controller
}()
window.makeKeyAndVisible()
}
}
So this is just all-in-code simple view controller example which can be used anywhere in the project by simply calling OverlayViewController.showInNewWindow(). The view controller can be from storyboard though.
The interesting part is the showInNewWindow which creates a new window and an instance of this view controller that is set as a root view controller. To show the new window all you need to call is makeKeyAndVisible. And to remove it simply hid it and remove it from superview as done in onTap. In this case the window is referenced by the view controller which should create a retain cycle. It seems that this is mandatory. It also means that we need to do some cleanup once window is dismissed by calling myWindow = nil.
There are some settings possible as well like you can disable user interaction and enable user to still use the window below this one (window.isUserInteractionEnabled = false). And you can set the level of window as to where is it displayed (window.windowLevel = .alert). You can play around a bit with these values. Which to set depends on what level you want your window to be.

Related

messageInputBar not dismissing when I dismiss my MessagesViewController (MessageKit, begging for help!)

I am using MessageKit. I've created a MessagesViewController. I add messageInputBar as a subview from viewDidLoad along with a navigational bar that includes a back button. Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController. If I don't tap on the messageInputBar when i first enter the MessagesViewController and press the back button, the messageInputBar properly is dismissed. Below is my code
override func viewDidLoad() {
super.viewDidLoad()
setUpNavBar()
navigationItem.largeTitleDisplayMode = .never
maintainPositionOnKeyboardFrameChanged = true
scrollsToLastItemOnKeyboardBeginsEditing = true
messageInputBar.inputTextView.tintColor = .systemBlue
messageInputBar.sendButton.setTitleColor(.systemTeal, for: .normal)
messageInputBar.delegate = self
messagesCollectionView.messagesDataSource = self
messagesCollectionView.messagesLayoutDelegate = self
messagesCollectionView.messagesDisplayDelegate = self
loadChat()
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
}
func setUpNavBar() {
let navBar = UINavigationBar()
self.view.addSubview(navBar)
navBar.items?.append(UINavigationItem(title: (selectedUser?.userFirstName)!))
let backButton = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(backButtonTapped))
navBar.topItem?.leftBarButtonItem = backButton
navBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
navBar.heightAnchor.constraint(equalToConstant: 44),
navBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
}
#IBAction func backButtonTapped(_ sender: Any) {
let transition = CATransition()
self.view.window!.layer.add(transition.segueLeftToRight(), forKey: kCATransition)
self.dismiss(animated: false)
}
As you have created MessagesViewController, you don't need to explicitly add messageInputBar to the bottom of the view.
Let's look at the source of MessageKit
private func setupInputBar(for kind: MessageInputBarKind) {
inputContainerView.subviews.forEach { $0.removeFromSuperview() }
func pinViewToInputContainer(_ view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
inputContainerView.addSubviews(view)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: inputContainerView.topAnchor),
view.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor),
view.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor),
view.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor),
])
}
switch kind {
case .messageInputBar:
pinViewToInputContainer(messageInputBar)
case .custom(let view):
pinViewToInputContainer(view)
}
}
The following code section should be removed from your source as the messageInputBar has already been set up in the library.
self.view.addSubview(messageInputBar)
messageInputBar.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
messageInputBar.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
messageInputBar.widthAnchor.constraint(equalToConstant: self.view.bounds.width)
])
NSLayoutConstraint.activate([
messagesCollectionView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
])
Now, your scenario
Whenever I am in this view controller and I tap on the messageInputBar's text field and then tap the back button, the messageInputBar stays on the screen when the app goes back to the previous UIViewController.
Whenever, there is an object that you interact with (eg. messageInputBar), and it is not deallocated (stays in view) after you dismissed the view controller, there is a memory leak.
If you repeatedly enter and dismiss the view controller, you will observe a rise in the memory usage of the app. So, finding out which object is creating this retain cycle, should solve this issue.
I installed MessageKit using Cocoapods later to find out they dropped support for Cocoapods. So I completely migrated my entire project over to Swift Package Manager to get the latest MessageKit which includes the setup for the inputbar in their code. No idea why they would release a version that didn't have this initially? Anyways, solved my problem!

Making NavigationBar subview clickable in Swift

I have a View Controller embedded in Navigation Controller. The view has 1 WKWebView, hence, I'm setting view = webView in loadView() override.
So, I'm adding a small little sub navigation bar underneath my navigation controller to allow a user to change their location.I can add the subview to the navigation controller, I'm just not able to make it clickable.
override func loadView() {
let config = WKWebViewConfiguration()
config.processPool = YourModelObject.sharedInstance.processPool
webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = self
self.webView.scrollView.delegate = self
view = webView
..
if let navigationBar = self.navigationController?.navigationBar {
let secondFrame = CGRect(x: 50, y: 44.1, width: navigationBar.frame.width, height: 30)
let secondLabel = UILabel(frame: secondFrame)
secondLabel.textColor = .black
secondLabel.text = "Getting your location..."
secondLabel.isUserInteractionEnabled = true
let guestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(setLocation(_:)))
secondLabel.addGestureRecognizer(guestureRecognizer)
secondLabel.textAlignment = .left
secondLabel.font = secondLabel.font.withSize(14)
secondLabel.tag = 1002
navigationBar.addSubview(secondLabel)
}
}
And then the setLocation function
#objc func setLocation(_ sender: Any) {
print("location label tapped")
}
But when I tap the label, I'm not getting anything printed in console. I don't know if the use of target: self is wrong for the tapGestureRecognizer or what's going on here.
I too am new to Swift, so my answer is far from guaranteed. I just know what it's like to be in your position,
Perhaps try creating a subclass of navigationBar for the sub navigation bar, i.e. mySubNavigationBar. Then in the subclass's code do all the initialization that you need to do. Including the print line so you'll know if you're getting there.
p.s. I would have put this as a comment, but I don't have enough points to add comments.

Accessibility focus order not working as expected [iOS]

OVERVIEW
I'm having trouble getting correct focus order (Accessibility in iOS). It seems like becomeFirstResponder() overwrites my focus order I have specified in the array and causes Voice Over Accessibility functionality to read wrong Accessibility Label first.
DETAILS:
I have a View Controller with containerView. Inside I have UIView of my progress bar image and text input field (placeholder). Both elements have isAccessibilityElement = true attributes and they have been added to my focus order array. However upon screen launch, focus order goes to the input field instead of progress bar UIView.
After extended testing I've noticed that this issue is no longer replicable if I remove below line of code.
otpNumberTextField.becomeFirstResponder()
But this is not a solution. I need cursor in the textfield but Voice Over functionality to read my Progress Bar Accessibility Label first. How to fix it?
SPECIFIC SCENARIO
I've noticed this issue occurs only when I have VC with a last active focus on a Textfield and then transition to the next VC (with a Textfield and a Progress Bar).
Bug is not replicable when I have VC with a last active focus on the Button and then transition to the next VC (with a Textfield and a Progress Bar).
CODE SNIPPET
import UIKit
class MyViewController: UIViewController, UITextFieldDelegate {
var otpNumberTextField = UITextField()
var progressMainDot = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
setupView()
setupBinding()
}
override func viewWillAppear(_ animated: Bool) {
setupView()
textFieldDidChange(otpNumberTextField)
}
func setupView(){
let containerView = UIView()
containerView.backgroundColor = UIColor.init(named: ColourUtility.BackgroundColour)
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
containerView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
//Progress Bar
let progressBarView = UIView()
containerView.addSubview(progressBarView)
progressBarView.isAccessibilityElement = true
progressBarView.accessibilityLabel = "my accessibility label"
progressBarView.translatesAutoresizingMaskIntoConstraints = false
progressMainDot.image = UIImage(named:ImageUtility.progressMain)
progressMainDot.contentMode = .scaleAspectFit
progressBarView.addSubview(progressMainDot)
//Text Field
otpNumberTextField.borderStyle = UITextField.BorderStyle.none
otpNumberTextField.font = UIFontMetrics.default.scaledFont(for: FontUtility.inputLargeTextFieldStyle)
otpNumberTextField.adjustsFontForContentSizeCategory = true
otpNumberTextField.isAccessibilityElement = true
otpNumberTextField.accessibilityLabel = AccessibilityUtility.enterVerificationCode
otpNumberTextField.placeholder = StringUtility.otpPlaceholder
otpNumberTextField.textColor = UIColor.init(named: ColourUtility.TextfieldColour)
otpNumberTextField.textAlignment = .center
otpNumberTextField.keyboardType = .numberPad
otpNumberTextField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
containerView.addSubview(otpNumberTextField)
otpNumberTextField.becomeFirstResponder()
//Accessibility - focus order
view.accessibilityElements = [progressBarView, otpNumberTextField]
}
//... more code goes here ...
}
If you have already set accessibilityElements, then voice over should respects that order but calling becomeFirstResponder() changes the focus to that text field.
You can try below code, which notifies voice over for shifting the focus to new element due to layout changes.
UIAccessibility.post(notification: .layoutChanged, argument: progressBarView)
So now your modified method should be like below:
func setupView(){
.....
otpNumberTextField.becomeFirstResponder()
//Accessibility - focus order
view.accessibilityElements = [progressBarView, otpNumberTextField]
UIAccessibility.post(notification: .layoutChanged, argument: progressBarView)
.....
}

Swift iOS -View Controller deinit runs when adding it to a new keyWindow

I have a view controller (OrangeVC) that I add to a class that contains a new keyWindow(NewKeyWindowClass). A button in a different vc is tapped and it triggers this new window to get shown over the app's main window and it animates from the right side bottom of the screen to fill to the top. The animation works fine, it starts from the bottom and fills the screen with a new vc with a orange background. The problem is once the OrangeVC is added to the NewKeyWindowClass the orangeVC's deinit keeps getting triggered.
Why is it's deinit running?
Class that goes inside Animator Class:
class OrangeController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
deinit {
print("OrangeVC -Deinit")
}
}
AnimatorClass:
import UIKit
class NewKeyWindowClass: NSObject {
func animateOrangeVCFromBottomToTop() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
let orangeVC = OrangeController()
// 1. starting frame
orangeVC.view.frame = CGRect(x: keyWindow.frame.width - 10, y: keyWindow.frame.height - 10, width: 10, height: 10)
keyWindow.addSubview(orangeVC.view)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// 2. ending frame
orangeVC.view.frame = keyWindow.frame
})
}
}
Button from a different class that triggers the animation:
#IBAction func triggerAnimationButtonPressed(_ sender: UIButton) {
let newKeyWindowClass = NewKeyWindowClass()
newKeyWindowClass.animateOrangeVCFromBottomToTop()
}
I got the answer from this reddit
An iOS application must have a rootViewController, create one and set
the keyWindow.rootViewController property to it. Then present your
view controller from that. Or just the rootViewController to be your
View Controller actually.
The reason the RedVC kept running it's deinit was because the keyWindow didn't have a rootViewController. I added the RedVC's view as a subview to the keyWindow keyWindow.addSubview(orangeVC.view) instead of making it it's rootVC:
keyWindow.rootViewController = redVC
Once I added it that the RedVC's deinit no longer ran when the animation occurred.
It should be noted that although it stopped the deinit from running I lost the animation and it also made the original keyWindow disappear. I should actually add this to a different UIWindow.

How to show a customizable view into all view controller?

the question is little bit elaborate what I really want to know.I want to make an app whose some functionality looks like SoundCloud and gaana. Below here I show some screenshot of gaana.
Here if we click any song they automatically update the above view and play the song and also this view will show into all view controller.Clicking this up arrow button how they showing up a new view where the previous small view is no where and also load a collection view into this new view.how to do all of this?the new view is here.
You can do that by creating an overlay, i.e. you add a UIView as a child element to the app's main window like so:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showOverlay()
}
func showOverlay() {
guard let window = UIApplication.shared.keyWindow else {
//if this block runs we were unable to get a reference to the main window
print("you have probably called this method in viewDidLoad or at some earlier point where the main window reference might be nil")
return
}
//add some custom view, for simplicity I've added a black view
let blackView = UIView()
blackView.backgroundColor = .black
blackView.translatesAutoresizingMaskIntoConstraints = false
blackView.frame = CGRect(x: 0, y: window.frame.height - 100, width: window.frame.width, height: 100.0)
window.addSubview(blackView)
}
Now this view should always be floating above the current UIViewController

Resources