In Swift 3, I need to detect if the user touches outside of a UITextField, then check that a particular UITextField is the sender, and then save the text. I have been trying to do this with Notification Center I have found examples in Swift 2, but I am struggling to implement the correct syntax for Swift 3.
let notificationName = Notification.Name("UITextFieldTextDidChange")
NotificationCenter.default.addObserver(self, selector: #selector(self.textFieldDidChange), name: notificationName, object: nil)
NotificationCenter.default.post(name: notificationName, object: nil)
func textFieldDidChange(sender: AnyObject) {
if let notification = sender as? NSNotification,
let textFieldChanged = notification.object as? UITextField
where textFieldChanged == self.myTextField {
storedText = myTextField.text!
}
}
UPDATE
I have found a slightly different way of doing this which works for me:
myTextField.addTarget(self, action: #selector(didChangeText(textField:)), for: .editingChanged)
func didChangeText(textField: UITextField) {
if let textInField = myTextField.text {
myTextField.text = textInField
storedText = myTextField.text!
}
}
Related
I have NSTextView and I want to show autocompletion options using NSTableView (like Xcode autocompletion). The problem is that when textView is the first responder, tableView is shown as unfocused (which is true), but I want to pretend that it's also active. Is there an easy way to achieve this (having firstResponder textView and tableView with active cell selection color)?
So I managed to resolve the issue with lots of experiments.
Here are my solution:
setup NSTableView:
tableView.refusesFirstResponder = true
tableView.selectionHighlightStyle = .none
on NSTableCellView subclass implement following code:
override func awakeFromNib() {
super.awakeFromNib()
registerNotifications()
}
deinit {
unregisterNotifications()
}
private func registerNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(selectionIsChanging),
name: NSNotification.Name.NSTableViewSelectionIsChanging,
object: nil)
}
private func unregisterNotifications() {
NotificationCenter.default.removeObserver(self)
}
#objc private func selectionIsChanging() {
if let row = superview as? NSTableRowView, row.isSelected == true {
self.backgroundColor = NSColor.alternateSelectedControlColor
} else {
self.backgroundColor = NSColor.clear
}
}
I am currently developing an iOS application with login and sign up forms. To make sure that the keyboard does not cover any UITextFields I've implemented the following solution provided by Apple and discussed in this issue.
To briefly sum it up, this solution uses a UIScrollView in which the different UI elements are placed and UIKeyboardDidShowNotification and UIKeyboardDidHideNotification to move the elements up and down when the keyboard appears/disappears so that the UITextFields aren't hidden.
This works like a charm except for one thing: for all my UIViewControllers I have to repeat the same code. To tackle my problem I have tried:
to create a base UIViewController, providing an implementation for the different functions, that can be subclasses by the other UIViewControllers;
to use a protocol and a protocol extension to provide a default implementation for the different functions and make my UIViewControllers conform to it.
Both solutions didn't solve my problem. For the first solution, I wasn't able to connect the UIScrollView of my base class through the Interface Builder although it was declared.
#IBOutlet weak var scrollView: UIScrollView!
When trying to implement the second solution, the UIViewController implementing my protocol somehow did not recognise the declared methods and their implementations.
The protocol declaration:
protocol ScrollViewProtocol {
var scrollView: UIScrollView! { get set }
var activeTextField: UITextField? { get set }
func addTapGestureRecognizer()
func singleTapGestureCaptured()
func registerForKeyboardNotifications()
func deregisterForKeyboardNotifications()
func keyboardWasShown(notification: NSNotification)
func keyboardWillBeHidden(notification: NSNotification)
func setActiveTextField(textField: UITextField)
func unsetActiveTextField()
}
The protocol extension implements all functions expect for the addTapGestureRecognizer() as I would like to avoid using #objc:
extension ScrollViewProtocol where Self: UIViewController {
// The implementation for the different functions
// as described in the provided links expect for the following method
func registerFromKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil, usingBlock: { notification in
self.keyboardWasShown(notification)
})
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidHideNotification, object: nil, queue: nil, usingBlock: { notification in
self.keyboardWillBeHidden(notification)
})
}
}
Does anyone have a good solution to my problem, knowingly how could I avoid repeating the code related to moving the UITextFields up and down when the keyboard appears/disappears? Or does anyone know why my solutions did not work?
I found a solution. I'll post it in case someone once to do the same thing.
So, I ended up deleting the UIScrollView outlet in my base class and replacing it with a simple property that I set in my inheriting classes. The code for my base class look as follow:
import UIKit
class ScrollViewController: UIViewController, UITextFieldDelegate {
// MARK: Properties
var scrollView: UIScrollView!
var activeTextField: UITextField?
// MARK: View cycle
override func viewDidLoad() {
super.viewDidLoad()
let singleTap = UITapGestureRecognizer(target: self, action: #selector(singleTapGestureCaptured))
scrollView.addGestureRecognizer(singleTap)
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
registerForKeyboardNotifications()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
deregisterFromKeyboardNotifications()
}
// MARK: Gesture recognizer
func singleTapGestureCaptured(sender: AnyObject) {
view.endEditing(true)
}
// MARK: Keyboard management
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWasShown), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(keyboardWillBeHidden), name: UIKeyboardWillHideNotification, object: nil)
}
func deregisterFromKeyboardNotifications() {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWasShown(notification: NSNotification) {
scrollView.scrollEnabled = true
let info : NSDictionary = notification.userInfo!
let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue().size
let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize!.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
var aRect : CGRect = self.view.frame
aRect.size.height -= keyboardSize!.height
if let activeFieldPresent = activeTextField {
if (!CGRectContainsPoint(aRect, activeFieldPresent.frame.origin)) {
scrollView.scrollRectToVisible(activeFieldPresent.frame, animated: true)
}
}
}
func keyboardWillBeHidden(notification: NSNotification) {
let info : NSDictionary = notification.userInfo!
let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue().size
let contentInsets : UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, -keyboardSize!.height, 0.0)
scrollView.contentInset = contentInsets
scrollView.scrollIndicatorInsets = contentInsets
view.endEditing(true)
scrollView.scrollEnabled = false
}
// MARK: Text field management
func textFieldDidBeginEditing(textField: UITextField) {
activeTextField = textField
}
func textFieldDidEndEditing(textField: UITextField) {
activeTextField = nil
}
}
And here is the inheriting class code:
class ViewController: ScrollViewController {
#IBOutlet weak var scrollViewOutlet: UIScrollView! {
didSet {
self.scrollView = self.scrollViewOutlet
}
}
// Your view controller functions
}
I hope this will help!
I have a tableView, and when the user taps a cell, it opens an alert box with the options "Close" and "Search". The handler for "Search" is shown below as func searchSongAction.
This presents a new viewcontroller (embedded in a Nav Bar) that searches the song online. In this new viewcontroller, Search, there are 2 functions: one searches the song currently playing and the other searches the song that the user asked to search from the tableView alert.
I am trying to pass the data from the cell into the Search class, but I keep coming up short. I feel like what I have is correct, but that is obviously not he case.
Any ideas?
Ask me if you need any more information.
History.swift
func searchSongAction(alert: UIAlertAction!) {
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("Search") as! Search
let navigationController = UINavigationController(rootViewController: vc)
self.presentViewController(navigationController, animated: true, completion: nil)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destinationNavigationController = segue.destinationViewController as! UINavigationController
let targetController = destinationNavigationController.topViewController as! Search
targetController.searchType = "Previous"
targetController.songNowText = self.songToSearch
targetController.artistNowText = self.artistToSearch
}
Search.swift
class Search: UIViewController {
var songNowText = ""
var artistNowText = ""
var searchType = ""
override func viewDidLoad() {
super.viewDidLoad()
if searchType == "Previous" {
searchSongPrevious()
} else {
searchSongNow()
}
}
}
You're not actually performing a Segue from your searchSongAction: function, so I'm guessing that prepareForSegue:sender: isn't actually being called and therefore the setup is not running.
Try assigning searchType, songNowText and artistNowText on vc inside of searchSongAction::
func searchSongAction(alert: UIAlertAction!) {
let vc = self.storyboard?.instantiateViewControllerWithIdentifier("Search") as! Search
//Setup the properties
vc.searchType = "Previous"
vc.songNowText = self.songToSearch
vc.artistNowText = self.artistToSearch
let navigationController = UINavigationController(rootViewController: vc)
self.presentViewController(navigationController, animated: true, completion: nil)
}
As a side note, typically you'll want to call super's implementation of a function when overriding it (you have neglected to do this in prepareForSegue:sender:)
I have a UIPageViewController and a UIButton under it. Here is the screenshot of my storyboard.
When I build the app, the button is huge:
All of my constraints were set automatically. I tried to specify the height, but it doesn't help. Any ideas?
P.S. I'm using XCode 6.3.
Edit:
ViewController.swift:
import UIKit
class ViewController: UIViewController, UIPageViewControllerDataSource {
#IBOutlet weak var restartButton: UIButton!
var pageViewController: UIPageViewController!
var pageTitles: NSArray!
var pageImages: NSArray!
override func viewDidLoad() {
super.viewDidLoad()
self.pageTitles = NSArray(objects: "Page 1", "Page 2")
self.pageImages = NSArray(objects: "algorithm", "apoint")
self.pageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("PageViewController") as! UIPageViewController
self.pageViewController.dataSource = self
var startVC = self.viewControllerAtIndex(0) as ContentViewController
var viewControllers = NSArray(object: startVC)
self.pageViewController.setViewControllers(viewControllers as [AnyObject], direction: .Forward, animated: true, completion: nil)
self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.size.height - restartButton.frame.height)
self.addChildViewController(self.pageViewController)
self.view.addSubview(self.pageViewController.view)
self.pageViewController.didMoveToParentViewController(self)
self.view.sendSubviewToBack(self.pageViewController.view)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func viewControllerAtIndex(index: Int) -> ContentViewController {
if ((self.pageTitles.count == 0) || (index >= self.pageTitles.count)) {
return ContentViewController()
}
var vc: ContentViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContentViewController") as! ContentViewController
vc.imageFile = self.pageImages[index] as! String
vc.titleText = self.pageTitles[index] as! String
vc.pageIndex = index
return vc
}
#IBAction func restartAction(sender: AnyObject) {
var startVC = self.viewControllerAtIndex(0) as ContentViewController
var viewControllers = NSArray(object: startVC)
self.pageViewController.setViewControllers(viewControllers as [AnyObject], direction: .Forward, animated: true, completion: nil)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var vc = viewController as! ContentViewController
var index = vc.pageIndex as Int
if (index == 0) || (index == NSNotFound) {
return nil
}
index--
return self.viewControllerAtIndex(index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var vc = viewController as! ContentViewController
var index = vc.pageIndex as Int
if index == NSNotFound {
return nil
}
index++
if index == self.pageTitles.count {
return nil
}
return self.viewControllerAtIndex(index)
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return self.pageTitles.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
}
I'm going to guess that the problem is that you have a constraint from the top of the button to something else in the interface. Get rid of that constraint. The only constraints you need for a button at the bottom of the screen are its bottom and its right-or-left-or-center - its width and height are automatic.
You'll want to find your button in your Controller Scene like this, highlight your constraints and delete them.
Personally when I have constraint issues, I start from scratch by deleting all of the constraints across the board. This is just my personal approach.
Next thing I do is start with the item at the top of my storyboard view and set its constraints like this:
Notice in my add constraints window i am only selecting the top and left margins and the width and height. I do this to each of my objects starting from the top of the screen and working my way to the bottom.
Obviously you'll need to play with this feature a bit to get your desired results. Please note that what I've provided is just an example and not a fix-all.
edit:
after reading your comment I am not sure this solution will help you, I didn't realize that you made your button programmatically. I'm going to leave it up for the time being.
I have made a statusBar application with a drop down. I would like to open a settingsWindow from that dropdown. I have made the settings window with its own ViewController.
The issue is that i can't figure out how to instantiate and show the settingsWindow that i have made. I have tried to follow every thread on the internet without any success.
My Viewcontroller:
class SettingsViewController: NSViewController {
#IBOutlet var ipAddress: NSTextField!
#IBOutlet var port: NSTextField!
#IBAction func connect(sender: AnyObject) {}
override func viewDidLoad() {
super.viewDidLoad()
}
}
My AppDelegate:
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet var statusMenu: NSMenu!
var statusItem: NSStatusItem?
var tcpService: TcpService = TcpService()
func applicationDidFinishLaunching(aNotification: NSNotification?) {
let bar = NSStatusBar.systemStatusBar()
statusItem = bar.statusItemWithLength(20)
statusItem!.menu = statusMenu
statusItem!.image = NSImage(byReferencingFile: NSBundle.mainBundle().pathForResource("16*16", ofType: "png"))
statusItem!.highlightMode = true
tcpService.initOutputStream("192.168.1.1", Port: 8888)
}
func applicationWillTerminate(aNotification: NSNotification?) {
// Insert code here to tear down your application
}
#IBAction func openSettings(sender: AnyObject) {
// open settings for ip and port optional port
}
}
in swift 3:
var myWindow: NSWindow? = nil
let storyboard = NSStoryboard(name: "Main",bundle: nil)
let controller: EditorViewController = storyboard.instantiateController(withIdentifier: "editorViewController") as! ViewController
myWindow = NSWindow(contentViewController: controller)
myWindow?.makeKeyAndOrderFront(self)
let vc = NSWindowController(window: myWindow)
vc.showWindow(self)
For 2022
in your normal Main storyboard, tap to add a new window controller.
tap precisely on the red "X", then the blue circle, and then enter "ExampleID" at the green entry.
in your app's ordinary main view controller, add this
variable:
var otherWindow: NSWindowController?
function:
private func otherWindow() {
let sb = NSStoryboard(name: "Main", bundle: nil)
otherWindow = sb.instantiateController(
withIdentifier: "ExampleID") as! NSWindowController
otherWindow?.showWindow(self)
}
That's it.
Call otherWindow when you want to.
Problem:
Inevitably you will want to set up the otherWindow in a certain way, example, transparent, whatever. Unfortunately this is a whole topic in itself, but you do it like this:
private func otherWindow() {
... as above ...
otherWindow?.window?.ExampleSetup()
}
and then
extension NSWindow {
func ExampleSetup() {
self.styleMask = .borderless
self.collectionBehavior = [.fullScreenPrimary]
self.level = .floating
self.isMovable = false
self.titleVisibility = .hidden
// etc etc etc ..
guard let screen = self.screen ?? NSScreen.main else {
print("what the???")
return
}
self.setFrame(screen.frame, display: true)
// consider also .visibleFrame
}
}
enum Storyboards: String {
case main = "Main"
func instantiateVC<T>(_ identifier: T.Type) -> T? {
let storyboard = NSStoryboard(name: rawValue, bundle: nil)
guard let viewcontroller = storyboard.instantiateController(withIdentifier: String(describing: identifier)) as? T else { return nil}
return viewcontroller
}
}
var ssoLoginController: IDSSOLoginViewController?
var myWindow: NSWindow? = nil
ssoLoginController = Storyboards.main.instantiateVC(IDSSOLoginViewController.self)
myWindow = NSWindow(contentViewController: ssoLoginController!)
myWindow?.makeKeyAndOrderFront(self)
let vc = NSWindowController(window: myWindow)
vc.showWindow(self)
I am not 100% that I fully understand your problem, but assuming that you are using a storyboard (you should if you are starting fresh), adding few lines to your applicationDidFinishLaunching method will help:
var myWindow: NSWindow? = nil
let storyboard = NSStoryboard(name: "Main",bundle: nil)
let controller: SettingsViewController = storyboard?.instantiateControllerWithIdentifier("SettingsViewController") as SettingsViewController
myWindow = controller.window
myWindow?.makeKeyAndOrderFront(self)
Do not forget to set the Storyboard ID in IB (in the example above to SettingsViewController)!