Swift, for some UIViews to their overall controller when clicked - swift

I have a DotBoss:UIViewController
There are a dozen UIView in the scene, Dot:UIView
(Some are direct subviews, some are further down.)
There are even container views in the chain between the highest controller and the Dot items.
The dozen Dot items know if they are tapped...
class Dot:UIView
{
private var tap:UITapGestureRecognizer? = nil
override func awakeFromNib()
{
tap = UITapGestureRecognizer(target:self,
action: #selector(SnapDot.handleTap(_:)))
self.addGestureRecognizer(tap!)
}
func handleTap(g:UITapGestureRecognizer)
{
print("user tapped on this particular Dot...")
}
}
I want DotBoss to know when one of the Dot is tapped.
class DotBoss:UIViewController
{
func oneDotWasClicked(d:Dot)
{
}
}
How to do this perfectly in Swift?
Note for anyone googling here, this bubbling extension is remarkably useful: https://blog.veloxdb.com/2016/05/12/bubbling-events-using-uiresponder-in-swift/

A Dot (UIView) and its ultimately controlling DotBoss (UIViewController) are both responders, and are links along the responder chain. Therefore, a Dot can call a method in its DotBoss by walking the responder chain until it comes to the DotBoss:
func handleTap(g:UITapGestureRecognizer) {
// ... other stuff can go here if necessary ...
var r : UIResponder = self
repeat { r = r.nextResponder()! } while !(r is DotBoss)
(r as! DotBoss).oneDotWasClicked(self)
}

Related

How can I show/hide a button added to the title bar of an NSWindow?

I have created a method in an NSWindow extension that allows me to add a button next to the text in the title bar. This is similar to the "down chevron" button that appears in the title bar of Pages and Numbers. When the button is clicked, an arbitrary code, expressed as a closure, is run.
While I have that part working fine, I would also like the button to be invisible most of the time and only become visible when the mouse is scrolled into the title bar area. This would be mimicking the way that Pages and Numbers displays the button.
However, I'm having difficulties getting the show/hide to work properly. I believe I can do it if I make it completely custom in the application delegate, and possibly by subclassing NSWindow, but I would really like to keep it as a single method in an NSWindow extension. In this way the code would be easily reusable in multiple applications.
To accomplish this I believe I need to inject an additional handler/listener that will tell me when the mouse enters and leaves the appropriate area. I can define the necessary area using an NSTrackingArea, but I haven't figured out how to "inject" an event listener without the need of subclasses. Does anyone know how (or if) such a thing is possible?
The key to handling the show/hide based on the mouse position was to use an NSTrackingArea to signify the portion that we are interested in, and to handle the mouse enter and mouse exit events. But since this can't be done directly on the title bar view (since we have to subclass the view in order to add the event handlers) we need to create an additional NSView that is invisible but covers the area we want to track.
I'll post the full code below, but the key parts related to this question are the TrackingHelper class defined near the bottom of the file and the way it is added to the titleBarView with its constrains set to be equal to the size of the title bar. The class itself is designed to take three closures, one for the mouse enter event, one for the mouse exit, and one for the action to take when the button is pressed. (Technically the latter doesn't really need to be part of the TrackingHelper, but it is a convenient place to put it to ensure it does not go out of scope while the UI still exists. A more correct solution would be to subclass NSButton to keep the closure, but I have always found subclassing NSButton to be a royal pain.)
Here is the full text of the solution. Note that this has a couple of things that depend on another library of mine - but they are not necessary for the understanding of this problem and are used to deal with the button image. If you wish to use this code you will need to replace the getImage function with one that creates the image you want. (And if you want to see what KSSCocoa is adding, you can obtain it from https://github.com/klassen-software-solutions/KSSCore)
//
// NSWindowExtension.swift
//
// Created by Steven W. Klassen on 2020-02-24.
//
import os
import Cocoa
import KSSCocoa
public extension NSWindow {
/**
Add an action button to the title bar. This will add a "down chevron" icon, similar to the one used in
Numbers and Pages, just to the right of the title in the title bar. When clicked it will run the given
lambda.
*/
#available(OSX 10.14, *)
func addTitleActionButton(_ lambda: #escaping () -> Void) -> NSButton {
guard let titleBarView = getTitleBarView() else {
fatalError("You can only add a title action to an app that has a title bar")
}
guard let titleTextField = getTextFieldChild(of: titleBarView) else {
fatalError("You can only add a title action to an app that has a title field")
}
let trackingHelper = TrackingHelper()
let actionButton = NSButton(image: getImage(),
target: trackingHelper,
action: #selector(trackingHelper.action))
actionButton.setButtonType(.momentaryPushIn)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.isBordered = false
actionButton.isEnabled = false
actionButton.alphaValue = 0
trackingHelper.translatesAutoresizingMaskIntoConstraints = false
trackingHelper.onButtonAction = lambda
trackingHelper.onMouseEntered = {
actionButton.isEnabled = true
actionButton.alphaValue = 1
}
trackingHelper.onMouseExited = {
actionButton.isEnabled = false
actionButton.alphaValue = 0
}
titleBarView.addSubview(trackingHelper)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addSubview(actionButton)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[titleTextField]-[actionButton(==7)]",
options: [], metrics: nil,
views: ["actionButton": actionButton,
"titleTextField": titleTextField]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-1-[actionButton]-3-|",
options: [], metrics: nil,
views: ["actionButton": actionButton]))
DistributedNotificationCenter.default().addObserver(
actionButton,
selector: #selector(actionButton.onThemeChanged(notification:)),
name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"),
object: nil
)
return actionButton
}
fileprivate func getTitleBarView() -> NSView? {
return standardWindowButton(.closeButton)?.superview
}
fileprivate func getTextFieldChild(of view: NSView) -> NSTextField? {
for subview in view.subviews {
if let textField = subview as? NSTextField {
return textField
}
}
return nil
}
}
fileprivate extension NSButton {
#available(OSX 10.14, *)
#objc func onThemeChanged(notification: NSNotification) {
image = image?.inverted()
}
}
#available(OSX 10.14, *)
fileprivate func getImage() -> NSImage {
var image = NSImage(sfSymbolName: "chevron.down")!
if NSApplication.shared.isDarkMode {
image = image.inverted()
}
return image
}
fileprivate final class TrackingHelper : NSView {
typealias Callback = ()->Void
var onMouseEntered: Callback? = nil
var onMouseExited: Callback? = nil
var onButtonAction: Callback? = nil
override func mouseEntered(with event: NSEvent) {
onMouseEntered?()
}
override func mouseExited(with event: NSEvent) {
onMouseExited?()
}
#objc func action() {
onButtonAction?()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
for trackingArea in self.trackingAreas {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways]
let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
}

view move up in particular textfield delegate

I have to move the UIView in only last UITextField in Swift 3.0 on mentioned below delegate method using tag,
func textFieldDidBeginEditing(_ textField: UITextField) {
if (textField.tag == 4){
//UIView Up
}
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
if (textField.tag == 4){
//UIView Down
}
return true
}
I tried many codes but none of them are working like notification,..etc.
You need to add Observers into the NotificationCenter for listening to both when Keyboard goes up and down (i'll assume your textfield outlet is lastTextField for this example to work but this obviously have to be adapted to whatever name you've had provide for it)
IBOutlet weak var passwordTextField: UITextField!
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: nil)
(Code above can be added in viewDidLoad())
Then you add methods to be executed when those notifications arrive, like this:
func keyboardWillShow(_ notification:Notification) {
if view.frame.origin.y >= 0 && lastTextField.isFirstResponder {
view.frame.origin.y -= getKeyboardHeight(notification)
}
}
func keyboardWillHide(_ notification:Notification) {
if view.frame.origin.y < 0 {
view.frame.origin.y += getKeyboardHeight(notification)
}
}
Validations within those methods prevent double execution like moving up/down twice when moving between textfields without resigning first responder which is common in cases like your (i assume your doing this for a form hence the clarification you only need it for the fourth textfield). Notice i'm only doing validation in for the specified textfield (with its outlet lastTextField) in the keyboardWillShow method, this in case you move thor another textfield while the keyboard is shown and resign responder from it in which case, even though it isn't the original place where you started, the view will return to its original place when the keyboard is hidden.
You'll also need a method for getting keyboard's height, this one can help with that:
func getKeyboardHeight(_ notification:Notification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue // of CGRect
return keyboardSize.cgRectValue.height
}
Let me know how it goes but i just tested this same code on my app and it works so you should be fine.
PS: pay close attention to the storyboard (if you're using it) and that delegate for textfields are set up properly.
The problem you are trying to remedy is rather complicated, because it requires you to:
Find the textField which is firstResponder
Calculate where that TextField is relative to it's superViews
Determine the distance for the animation, so that the containing
superview doesnt jump out of the window, or jumps too
much/repeatedly
Animate the proper superView.
As you can see.. it's quite the algorithm. But luckily, I can help. However, this only works for a hierarchy which has the following layout:
superView (view in the case of UIViewController) > (N)containerSubviews > textFields
where N is an integer
or the following:
superView (view in the case of UIViewController) > textFields
The idea is to animate superView, based on which textField is firstResponser, and to calculate if it's position inside of the SCREEN implies that it either partially/totally obstructed by the Keyboard or that it is not positioned the way you want for editing. The advantage to this, over simply moving up the superView when the keyboard is shown in an arbitrary manner, is that your textField might not be positioned properly (ie; obstructed by the statusbar), and in the case where your textfields are in a ScrollView/TableView or CollectionView, you can simply scroll the texfield into the place you want instead. This allows you to compute that desired location.
First you need a method which will parse through a given superView, and look for which of it's subViews isFirstResponder:
func findActiveTextField(subviews : [UIView], textField : inout UITextField?) {
for view in subviews {
if let tf = view as? UITextField {
guard !tf.isFirstResponder else {
textField = tf; break
return
}
} else if !subviews.isEmpty {
findActiveTextField(subviews: view.subviews, textField: &textField)
}
}
}
Second, to aleviate the notification method, also make a method to manage the actual animation:
func moveFromDisplace(view: UIView, keyboardheight: CGFloat, comp: #escaping (()->())) {
//You check to see if the view passed is a textField.
if let texty = view as? UITextField {
//Ideally, you set some variables to animate with.
//Next step, you determine which textField you're using.
if texty == YourTextFieldA {
UIView.animate(withDuration: 0.5, animations: {
self./*the proper superView*/.center.y = //The value needed
})
comp()
return
}
if texty == YourTextFieldB {
// Now, since you know which textField is FirstResponder, you can calculate for each textField, if they will be cropped out by the keyboard or not, and to animate the main view up accordingly, or not if the textField is visible at the time the keyboard is called.
UIView.animate(withDuration: 0.5, animations: {
self./*the proper superView*/.center.y = //The Value needed
})
comp()
return
}
}
}
Finally, the method which is tied to the notification for the keyboardWillShow key; in this case, i have a UIViewController, with an optional view called profileFlow containing a bunch of UITextFields
func searchDisplace(notification: NSNotification) {
guard let userInfo:NSDictionary = notification.userInfo as NSDictionary else { return }
guard let keyboardFrame:NSValue = userInfo.value(forKey: UIKeyboardFrameEndUserInfoKey) as? NSValue else { return }
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
let keybheight = keyboardHeight
var texty : UITextField? //Here is the potential textfield
var search : UISearchBar? //In my case, i also look out for searchBars.. So ignore this.
guard let logProfile = profileFlow else { return }
findActiveTextField(subviews: [logProfile], textField: &texty)
//Check if the parsing method found anything...
guard let text = texty else {
//Found something.. so determine if it should be animated..
moveFromDisplace(view: searchy, keybheight: keybheight, comp: {
value in
search = nil
})
return
}
//Didn't find anything..
}
Finally, you tie in this whole logic to the notification:
NotificationCenter.default.addObserver(self, selector: #selector(searchDisplace(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
I can't provide more content to the code, since it all depends on your view hierarchy, and how you want things to animate. So it's up to you to figure that out.
On a side note, usually, if you have so many textfields that to lay them out properly means they overstep the length of the screen.. it's probable that you could simplify your layout. A way to make this algorithm better would be to make sure you have all your textfields in one containing view, which again can become heavy for when, say, you use AutoLayout constraints. Odds are if you're in this situation, you can probably afford to add a flow of several views etc.
There is also the fact that i've never really needed to use this for iPhone views, more for iPad views, and even then for large forms only (e-commerce). So perhaps if you're not in that category, it might be worth reviewing your layout.
Another approach to this, is to use my approach, but to instead check for specific textFields right in the findActiveTextField() method if you only have a handful of textfields, and to animate things within findActiveTextField() as well if you know all of the possible positions they can be in.
Either way, i use inout parameters in this case, something worth looking into if you ask me.

Where in view lifecycle to update controller after modal UIViewController dismissed

I have a UIViewController with a UILabel that needs to display either "lbs" or "kg". My app has a settings screen (another UIViewController) that is presented modally over the first view controller and the user can select either of the two units and save their preference. If the units are changed and the modal settings screen is dismissed, I of course want the label on the first view controller to be updated with the new units value (but without refreshing the whole view). I thought I knew how to make it work, but evidently I don't.
On my modal settings screen, I have a UISegmentedControl to allow the user to select units. Anytime it's changed, this function updates userDefaults:
func saveUnitsSelection() {
if unitsControl.selectedSegmentIndex == 0 {
UserDefaultsManager.sharedInstance.preferredUnits = Units.pounds.rawValue
} else {
UserDefaultsManager.sharedInstance.preferredUnits = Units.kilograms.rawValue
}
}
Then they would likely dismiss the settings screen. So, I added this to viewDidLoad in my first view controller:
override func viewDidLoad() {
super.viewDidLoad()
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
units.text = preferredUnits
}
That didn't work, so I moved it to viewWillAppear() and that didn't work either. I did some research and some caveman debugging and found out that neither of those functions is called after the view has been loaded/presented the first time. It seems that viewWillAppear will be called a second time if I'm working within a hierarchy of UITableViewControllers managed by a UINavigationController, but isn't called when I dismiss my modal UIViewController to reveal the UIViewController underneath it.
Edit 1:
Here's the view hierarchy I'm working with:
I'm kinda stuck at this point and not sure what to try next.
Edit 2:
The user can tap a 'Done' button in the navigation bar and when they do, the dismissSettings() function dismisses the Settings view:
class SettingsViewController: UITableViewController {
let preferredUnits = UserDefaultsManager.sharedInstance.preferredUnits
// some other variables set here
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.topItem?.title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .Plain, target: self, action: #selector(self.dismissSettings(_:)))
if preferredUnits == Units.pounds.rawValue {
unitsControl.selectedSegmentIndex = 0
} else {
unitsControl.selectedSegmentIndex = 1
}
}
func dismissSettings(sender: AnyObject?) {
navigationController?.dismissViewControllerAnimated(true, completion: nil)
}
}
THE REAL PROBLEM
You misspelled viewWillAppear. You called it:
func viewWillAppear()
As far as Cocoa Touch is concerned, this is a random irrelevant function that hooks into nothing. You meant:
override func viewWillAppear(animated: Bool)
The full name of the first function is: "viewWillAppear"
The full name of the second function is: "viewWillAppear:animated"
Once you get used to this, the extreme method "overloading" that Cocoa Touch uses gets easier.
This is very different in other languages where you might at least get a warning.
The other lesson that everyone needs to learn when posting a question is: Include All Related Code!
Useful logging function I use instead of print or NSLog, to help find these things:
class Util {
static func log(message: String, sourceAbsolutePath: String = #file, line: Int = #line, function: String = #function, category: String = "General") {
let threadType = NSThread.currentThread().isMainThread ? "main" : "other"
let baseName = (NSURL(fileURLWithPath: sourceAbsolutePath).lastPathComponent! as NSString).stringByDeletingPathExtension ?? "UNKNOWN_FILE"
print("\(NSDate()) \(threadType) \(baseName) \(function)[\(line)]: \(message)")
}
}
[Remaining previous discussion removed as it was incorrect guesses]

performSegueWithIdentifier from a NSView subclass?

I have a document window that contains a number of NSView subclasses, switched between using a tab control. Each of the subclasses, and the window's ViewController, support different user actions accessed through menu items tied to the First Responder.
I'd like to perform a segue from one of those views in response to a menu item. However, NSView does not support performSegueWithIdentifier, it appears to be something that is part of NSViewController alone.
Can someone suggest a way around this? I have seen suggestions to pass the VC into the views, but I am not clear how to do that. Or perhaps there is a better way?
view.containingController.performSegue()
note: you have to add containingController to your views
I WOULD add the viewController to the responder chain and then make containingController a computed property in an extension!
e.g. add vc as responder:
override func awakeFromNib() {
super.awakeFromNib()
self.nextResponder = self.view
for subview in self.view.subviews {
subview.nextResponder = self
}
}
e.g. containingController in extension
extension NSView {
var containingController: NSViewController? {
get {
while(self.nextResponder != nil) {
if(self.nextResponder is NSViewController) {
return self.nextResponder
}
}
return nil
}
}
}
You could do that (see Daij-Djan's answer), however it is not what I would recommend, since a hypothetical programmer who will be using your code, but is not familiar with it (let's say, you in a year :) ) might be caught by surprise by such behaviour.
I would recommend you to add a delegate (conforming to your custom protocol, let's call it MyViewDelegate) to your NSView with a method like viewRequiresToPerformTransition(view: YourViewSubclass). Then you implement this method (more generally, you conform to MyViewDelegate protocol) in your view controller and inside its implementation perform any segue you want.

How to slide TabBar up to hide it in tvOS app?

In my tvOS app I have a TabBarController with 3 viewControllers. What I want to do is to automatically hide/change focus of the tabBar when I switch to the next viewController.
I saw some posts here, on SO that suggested to change alfa on the tabBar, but I would like to have a slide up animation, same way as it does when you change focus to something in the viewController.
Any kind of help is highly appreciated.
As Charles said.. Something like this in the derived UITabBarController:
var focusOnChildVC : Bool = false {
didSet {
self.setNeedsFocusUpdate()
}
};
override weak var preferredFocusedView: UIView? {
get {
let v : UIView?;
let focused = UIScreen.mainScreen().focusedView
//A bit of a hack but seems to work for picking up whether the VC is active or not
if (focusOnChildVC && focused != nil) {
v = self.selectedViewController?.preferredFocusedView
} else {
//If we are focused on the main VC and then clear out of property as we're done with overriding the focus now
if (focusOnChildVC) {
focusOnChildVC = false
}
v = super.preferredFocusedView;
}
return v
}
}
The basic idea of the solution described below is to subclass UITabBarController and selectively use the super implementation of weak var preferredFocusedView: UIView? { get } or one that returns selectedViewController?.preferredFocusView along with an implementation of didUpdateFocusInContext(_:withAnimationCoordinator:) that sets up an NSTimer that triggers a focus update and sets a flag that controls the preferredFocusView implementation.
More verbosely, Subclass UITabBarController and override didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator). In your implementation (make sure to call the super implementation) you can inspect the context and determine if a descendent view of the tabBar property is the nextFocusedView or the previousFocusedView (and the nextFocusedView is not a descendent).
If the tab bar is gaining focus you can create an NSTimer for the duration that you want to show the tab bar before hiding it. If the tab bar loses focus before the timer fires, invalidate it. If the timer fires, call setNeedsFocusUpdate() followed by updateFocusIfNeeded().
The last piece you need to get this to work is a flag that is set to true while the timer is set. You then need to override weak var preferredFocusedView: UIView? { get } and call the super implementation if the flag is false and if it is true return selectedViewController?.preferredFocusedView.
You can do it in a UITabBarController subclass:
final class TabBarViewController: UITabBarController {
private(set) var isTabBarHidden = false
func setTabBarHidden(_ isHidden: Bool, animated: Bool) {
guard isTabBarHidden != isHidden else {
return
}
var frame: CGRect
let alpha: CGFloat
if isHidden {
frame = tabBar.frame
frame.origin.y -= frame.height
alpha = 0
} else {
frame = tabBar.frame
frame.origin.y += frame.height
alpha = 1
}
let animations = {
self.tabBar.frame = frame
self.tabBar.alpha = alpha
}
if animated {
UIView.animate(withDuration: 0.3, animations: animations)
} else {
animations()
}
isTabBarHidden = isHidden
}
}