Basic: Connecting multiple (View-)Controllers the right way - swift

I'm trying to set up a login screen (ViewController) that leads - after a successful login - to a user list (UserTableViewController) that is itself part of a navigation controller. On the subsequent screens like the UserTableViewController it should be possible to logout. This would bring the user back to the initial login screen ViewController. I'm really struggling connecting those screens the right way. It must be said that I don't have a lot of experience with the different kinds of segues and/or delegates so, with some research done, I went for some trials:
A successful login on ViewController triggers a modal-segue to the navigation controller (that itself leads to the UserTableViewController)
The UserTableViewController has a logout button that triggers another modal-segue back to the ViewController. I took these modal segues because first, I didn't want to have a hierarchy that leads to an automatically created back-button or similar and second, I didn't want to have any "troubles" between these two screens, one having a navigation controller while the other one doesn't.
...it looks like that's not a way to go. Some things get mixed up after one loop and screens are changing the wrong way. I guess a modal-segue-circle is not possible as there has to be parent and a child at least. Next trial:
A successful login on ViewController triggers a modal-segue / push-segue to the navigation controller (that itself leads to the UserTableViewController)
To return to the login screen I implemented a delegate instead of another segue, triggered when tapping "logout" - here I'm facing the problem that I can't set up the UserTableViewController's delegate in preparingForSegue on ViewController properly. At this point segue.destinationViewController cannot be downcasted to UserTableViewController but only to NavigationController what doesn't allow me to set up the delegate at the destination (UserTableViewController).
The third trial was to do the same like in the second approach but implementing a segue from ViewController
to UserTableViewController directly. That may work but now I don't have any navigation bar anymore at my UserTableViewController...!
Of course I could go for a manual fix in the third solution like inserting a stand-alone navigation bar but neither way seems to be efficient. Therefore I'd very very thankful for some hints, highlighting what I misunderstood (completely) on one side and showing a good way of doing it on the other side. Thanks a lot for any help!
EDIT:
I could try to set the navigation controller as initial view controller and then just let the login screen ViewController being presented/dismissed by the UserTableViewController - is that a practical way or are there widely known best practices for those login view scenario?
Just a visual help:

Please consider using an "unwind" segue. In the ViewController create a function as follows:
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
yourLogoutCode()
}
In UserTableViewController control-drag from the Logout button to the exit symbol of UserTableViewController which is the third button at the top, and select unwindToThisViewController (or whatever name you selected in ViewController).
Now when you click on the Logout button you will return to ViewController and execute the unwindToThisViewController in which you can put your logout code.
Here is an example:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func yourLogoutCode() {
println("Logging Out ...")
}
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
self.yourLogoutCode()
}
}
And the UserTableViewController:
import UIKit
var selectedRow = -1
class UserTableViewController: UITableViewController {
var firstArray = ["Item1","Item2","Item3","Item4"]
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return firstArray.count
}
let nameOfCell = "Cell"
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(nameOfCell, forIndexPath: indexPath) as UITableViewCell
cell.textLabel!.text = firstArray[indexPath.row]
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRow = indexPath.row
}
}
Notice that the whole connection from the Logout button to the unwind is buried inside the Storyboard. You can confirm that all this is happening by reviewing the Connections Inspector.

This post appears to address exactly the issue you are facing, if I understand correctly.
Also, not meaning to nitpick, but it's "modal", not "modular" :)

I think that could be an acceptable way of doing it:
Login view (ViewController) is initial View Controller
In case of successful login: Modal-segue from login view to the navigation controller that handles subsequent views
Enabling dismiss-segues in Swift by implementing a sub class of UIStoryboardSegue (needed to be written like this: #objc(DismissSegue) class DismissSegue: UIStoryboardSegue {
) that is overriding its perform() to (sourceViewController.presentingViewController!!).dismissViewControllerAnimated(true, completion: nil)
Dismiss-segue from subsequent views (in my case UserTableViewController) back to the login view
Implement the logout-action within prepareForSegue on each of the View Controllers which allows the user to logout
Still, if anyone knows a better practice, I'm happy to hear!

Related

Safari app extension popover not calling table view notification methods

I want to include a view-based NSTableView in the popover of a Safari App Extension.
Starting with the default project in Xcode, I made the SFSafariExtensionViewController the delegate and datasource for the table view as it is the only content on the popover, and mostly this works.
I can populate the table and implement methods like tableView(_:shouldSelectRow:), yet methods which return a notification object such as tableViewSelectionDidChange(_:) do not get called.
Whilst those methods show a cludgy way of knowing when a row is selected, I am left with no way of knowing when a cell is edited.
As I had to connect the delegate outlet of the NSTableView to the File Owner to allow the delegated methods to work, I also tried connecting the dataSource outlet too, but this rightly did not help.
Here is the essence of my code (which for now includes returning dummy table data to test editing):
class SafariExtensionViewController: SFSafariExtensionViewController {
#IBOutlet weak var tableView: NSTableView!
static let shared: SafariExtensionViewController = {
let shared = SafariExtensionViewController()
shared.preferredContentSize = NSSize(width:445, height:421)
return shared
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
func textDidEndEditing(_ notification: Notification) {
NSLog("I will NEVER appear in the console")
}
}
extension SafariExtensionViewController: NSTableViewDataSource {
func numberOfRows(in tableView: NSTableView) -> Int {
return 5
}
}
extension SafariExtensionViewController: NSTableViewDelegate {
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cellView = tableView.makeView(withIdentifier: tableColumn!.identifier, owner: self) as? NSTableCellView
cellView?.textField?.stringValue = "Blah"
return cellView
}
func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool {
NSLog("I will appear in the console")
return true
}
func tableViewSelectionDidChange(_ notification: Notification) {
NSLog("I will NEVER appear in the console")
}
func controlTextDidEndEditing(_ obj: Notification) {
NSLog("I will NEVER appear in the console")
}
}
(Obviously I do not need both textDidEndEditing(_:) and controlTextDidEndEditing(_:) but I am just trying everything.)
I am guessing the problem is something to do with the table view not being registered for notifications within a SFSafariExtensionViewController? That object inherits from NSViewController, though, so I would have thought these methods should work automatically.
This is my first time using swift, and it is a long time since I wrote a Mac app. But the actual functionality of the extension works, now I just want to have the ability to customize the settings through the UI.
However there seems to be very little written about Safari app extension programming, Apple's documentation is sparse, and I have not even been able to find any code examples featuring a table view in a popover to learn from.
I am probably missing something very obvious, but I have run out of searches to try on here and the web in general, so any help will be appreciated.
UPDATE:
I think I have an answer, by explicitly linking the NSTextFields in the table to the File's Owner as a delegate, the tableViewSelectionDidChange(_:) and controlTextDidEndEditing(_:) methods are now working. There must have been something else wrong causing the former to not work that I accidentally broke and fix, but it makes some sense for the latter.
That is all I need for the functionality to work, however I am still confused why the textDidEndEditing(_:) is still not working when I am led believe it should.
And in Apple's documentation, textDidEndEditing(_ :) is a method of an NSTextField, which links to a page saying controlTextDidEndEditing(_ :) is deprecated
And I misunderstanding anything?
I think you are not setting up the outlet properly please confirm this. Also check you setting up reusable identifier? identifier. for me all delegate calling without no issue after that.

Swift add button to UICollectionViewCell that opens new controller

I have a UICollectionViewCell header on a UICollectionViewController, and I've added a button to it. I would like for the button, when clicked, to push a new view controller atop the current one. The problem is that the button doesn't have access to the navigation controller of the UICollectionViewController, so I there's no way to directly push a controller from, say, a connector to the buttn (that I know of). Is there any way to achieve this? Maybe something can be overriden, such as a collectionView function. Thanks!
If you just want to process the cell selection there is a handy method in the UICollectionViewDelegate that you can implement to get the index path of the pressed cell.
If your goal is to have a custom button inside the cell (or maybe even several) you can use delegation pattern to retrieve user actions to your controller to than process in any way, including pushing/presenting new controllers. Assign the controller's instance (the one managing the collection view) to the delegate member of your cell.
Define a protocol that I would call something like MyCustomCellDelegate (replace MyCustomCell with a more appropriate name for your case). Something like MyCustomCellDelegate: class { func didPressButtonX() }
Declare an optional delegate property in your cell subclass. weak var delegate: MyCustomCellDelegate?
Implement your delegate protocol by the class you want to respond to button presses (or any other interactions defined by your protocol).
Every time you create/dequeue a cell for your UICollectionView to use you set the delegate property to the view controller managing the collection view. cell.delegate = self (if done inside the view controller itself).
After receiving the UI event inside your custom cell use your delegate property to retrieve the action to the controller (or with ever object you used when assigning the property). Something like: delegate?.didPressButtonX()
In your class that implements MyCustomCellDelegate use the method to push the new controller.
Below I will provide sample code that should give more details on the implementation of the proposed solution:
// In your UICollectionViewCell subclass file
protocol MyCustomCellDelegate: class {
func didPressButtonX()
func didPressButtonY()
}
MyCustomCell: UICollectionViewCell {
weak var delegate: MyCustomCellDelegate?
#IBOutlet var buttonX: UIButton!
#IBOutlet var buttonY: UIButton!
#IBAction func didPressButtonX(sender: Any) {
delegate?.didPressButtonX()
}
#IBAction func didPressButtonY(sender: Any) {
delegate?.didPressButtonY()
}
}
// Now in your UICollectionViewController subclass file
MyCustomCollectionViewController: UICollectionViewController {
// ...
override func collectionView(UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier identifier: "YourCellIdentifierGoesHere", for indexPath: indexPath) as! MyCustomCell
// In here we assign the delegate member of the cell to make sure once
// an UI event occurs the cell will call methods implemented by our controller
cell.delegate = self
// further cell setup if needed ...
return cell
}
}
// In order for the instance of our controller to be used as cell's delegate
// we implement the protocol that we defined earlier in the cell file
extension MyCustomCollectionViewController: MyCustomCellDelegate {
func didPressButtonX() {
print("X button was pressed")
// now lets finally push some new controller
let yourNextCoolViewController = UIViewController()
self.push(yourNextCoolViewController, animated: true)
// OR if you are using segues
self.performSegue(withIdentifier: "YourSegueIdentifierGoesHere", sender: self)
}
func didPressButtonY() {
print("Y button was pressed")
}
}

unwind segue not triggering

I have been learning swift and have made the foundation of most of my app. I have the following storyboard
app storyboard
Everything works fine. For example, I have an unwind segue on the add course view controller that triggers when you press save and you are returned to the 'your courses' view controller.
When you are on the my courses view controller, you can select a course and the topics are displayed, you can then select a topic and you are taken to an update score view controller, this all works fine.
However, my problem is this. I want to make it so that when you select save in the updatescore view controller, an unwind segue is triggered (the same as in the add course) and you are returned to the list of topics in the topics view controller.
However, I have followed many tutorials and obviously got it working before. (My action method for the unwind segue is in the correct topics view controller) but when i press save, the unwind segue is not returning me to the topics view controller.
Could anyone suggest a reason for this? I have spent a lot of time trying to find an answer and gone through many tutorials but have not managed to solve it.
I have also included a screen shot of the connections of the triggered segues for my save button to show that it is set up. Showing triggered segue for save button
i have the following code in the update score view controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if saveButton === sender {
print("save button selected")
}
}
But even this is not getting triggered when I click on save.
Many thanks
UPDATE:
After following Ronatorys advice My view controller for the update score is as follows but it is still not working:
import UIKit
class UpdateScoreTableViewController: UITableViewController {
#IBOutlet weak var topicGettingUpdated: UITextField!
#IBOutlet weak var newScore: UITextField!
#IBOutlet weak var saveButton: UIBarButtonItem!
var index:Int?
var Topics:[String]!
var TopicToUpdate:String?
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// 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?) {
guard let uiBarButtonItem = sender as? UIBarButtonItem else {
print("There is no UIBarButtonItem sender")
return
}
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 && indexPath.row == 0 {
newScore.becomeFirstResponder()
}
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
But the prepare for segue is not even getting triggered.
Like Craig in the comments said, it's not that easy to find the problem. So I just build a simple app where you can follow the steps as guide and see if you forgot something to setup the functionality right. Hope it will help you. Note: Code is in Swift 3.0, but should be easy to adopt to 2.*
1. Storyboard with two View Controllers:
2. Declare the action method for the unwind segue in the FirstViewController.swift:
class FirstViewController: UIViewController {
// action method for the unwind segue
#IBAction func updateScore(_ segue: UIStoryboardSegue) {
print("Back in the FirstViewController")
}
}
3. Connect the Save button in the Storyboard with the action method (with ctrl + drag):
4. Connect your Save button with the SecondViewController.swift file, to use it for checking in your prepareSegue method (with ctrl + drag):
5. Add the prepare(for:sender:) method to your SecondViewController.swift:
class SecondViewController: UIViewController {
#IBOutlet weak var saveButtonPressed: UIBarButtonItem!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// check safely with guard that your save button is the sender and you can use it
// if not print message
guard let uiBarButtonItem = sender as? UIBarButtonItem else {
print("There is no UIBarButtonItem sender")
return
}
// check if you selected the save button
if saveButtonPressed == uiBarButtonItem {
print("save button selected")
}
}
}
Result:
The sample app you can find here
I did not manage to get the unwind segue to work but instead used
navigationController!.popViewControllerAnimated(true)
as a work around and this works fine.

how to create xib subview at the center of tableview swift

I have tableview and I created the custom xib uiview as "detailview" for it. I want to show this detailview at the center of scrolled area when tapped to tableview cell. I can show this view but cannot centralized it. when I set value to frame manually, subview will be at center (approximately) but when I tap the cell which is at the bottom, the subview is appearing at the top of page and also it is moving when i scroll the tableview.
Please help me to show this view at the center of the scrolled area and be fixed
Here is my codes;
Detail View :
class TopTenDetailView: UIView {
var screenWidth:CGFloat = UIScreen.mainScreen().bounds.width*0.08
var screenHeight :CGFloat = UIScreen.mainScreen().bounds.height*0.08
class func instanceFromNib() -> UIView {
return UINib(nibName: "TopTenDetail", bundle: nil).instantiateWithOwner(nil, options: nil)[0] as! UIView
}
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius=10
let testFrame : CGRect = CGRectMake(screenWidth,screenHeight,320,480)
self.frame = testFrame
self.userInteractionEnabled=true
}
#IBAction func close(sender: UIButton) {
self.hidden=true
}
}
And TableViewController's method ;
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var detailView = TopTenDetailView.instanceFromNib
self.view.addSubview(detailView())
}
The way this is setup has many problems and i would be surprised if it actually ever works as intended.
A much better, simpler setup uses a OverFullScreen presentation style and it goes like this:
Create a separate UIViewController for your detail view, let's call it DetailViewController use Interface Builder. Make sure to set the background color to CLEAR
Wire up a segue from the "base" UIViewController that holds your UITableView to DetailViewController and give the segue a name. Let's call it 'detailSegue' , basically drag from one view controller to the other. Make sure that you are not dragging from the view but from the yellow icon at the top of the view controller. You are done in Interface Builder.
Ok, now for the code:
// MARK : - UITableViewDelegate
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("detailSegue", sender: self)
}
// MARK: - segues
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let vc = segue.destinationViewController as? UIViewController{
vc.modalPresentationStyle = .OverFullScreen
}
}
The OverFullScreen presentation style uses a proper UIViewController modal segue but leaves the presenting UIViewController visible under the presented one.
You can then just layout whatever you want on DetailViewController using Interface Builder and autolayout without having to do hacky match calculations on the layout at runtime.
Hope it helps!

Fill a textField with a selected TableViewCell?

In my initial view there is a blank textField (with an invisible button over it) that segues to a TableView when you click it. I want to send the text data from my TableViewCell selection to the blank textField in the original view.
I've made a View2.swift file, which is a replica of my original View. I was trying to edit the source code in there to push the textData by sending the .text from the indexPath of the TableView.
This video shows how to send data from a first view to a second view, but I'm trying to send data from my second view (TableView) back to my first view (View).
Here is my code:
ViewController.swift
import UIKit
class ViewController: UITableViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var DestViewController : View2 = segue.destinationViewController as! View2
DestViewController.formulaSelectionText = ViewController.
}
}
View2.swift:
import Foundation
import UIKit
class View2: UIViewController {
override func viewDidLoad(){
formulaSelection.text = indexPathForCell(Cell: UITableView)
}
}
I assume it would be best to just update the value in the initial view. I'm thinking with a UITextFieldDelegate?
It is easy to pass back UITableViewCell Selected value back to a controller. Simply on selection of a cell, update the textfield's value in previous controller and pop the controller/dismiss it if you are using navigation controller / modal controller.
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
//get last controller in hierarchy
let viewControllers: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
var prevController : PrevViewController = viewControllers[viewControllers.count - 1] as PrevViewController;
prevController.textfield.text = dataSource[indexPath.row]; //update textfield here
self.navigationController!.popViewControllerAnimated(true);
}
Hope it helps!