I am working on the online Stanford IOS development course and am having a bug I don't know how to deal with. When I segue to the detail view in the split view controller I am able to set parameters of the detail UIView from the detail UIViewController in the didSet of outlet to the view. Later when I try to access or mutate the detail view from the controller the view variable(outlet) is nil even though the view is still on screen and responds to gestures.
This is the prepare method of the master view in the split view controller
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationViewController = segue.destination
if let graphViewController = destinationViewController as? GraphViewController {
if let identifier = segue.identifier{
switch identifier {
case "DisplayGraph":
graphViewController.function = funcForGraphView(_:)
default:
break
}
}
}
}
This is the detail view controller. I commented on the interesting parts.
class GraphViewController: UIViewController {
#IBOutlet weak var graphView: GraphView!{
didSet{
graphView.addGestureRecognizer(UITapGestureRecognizer(target: graphView, action: #selector(graphView.tap(recognizer:))))
graphView.addGestureRecognizer(UIPanGestureRecognizer(target: graphView, action: #selector(graphView.pan(recognizer:))))
graphView.addGestureRecognizer(UIPinchGestureRecognizer(target: graphView, action: #selector(graphView.zoom(recognizer:))))
graphView.function = cos //I can set variables in the view here
// After this didSet I am no longer able to set or read any variables from the graphView
}
}
var function: ((Double)->(Double))? {
didSet{
if let newfunction = function {
graphView.function = newfunction // I can not set or access variables in the view here since for some reason graphView is nil
}
}
}
And this is the relevant variable in the detail view
var function: ((Double) -> Double)? {
didSet{
setNeedsDisplay()
}
}
Storyboard Picture
Any help would be greatly appreciated and hopefully this isn't a stupid question. If more information would be helpful let me know.
Thanks!
It will be nil because of your view not fully loaded while your are push viewcontrolle. so you needs to pass as below :
In GraphViewController
var yourValue : ((Double)->(Double))?
override func viewDidLoad() {
super.viewDidLoad()
self.function = yourValue
}
Set value while navigation
if let identifier = segue.identifier{
switch identifier {
case "DisplayGraph":
graphViewController.yourValue = funcForGraphView(_:)
default:
break
}
}
Related
I'm building an app to keep track of scores i have a struct
*/
var teamA_name: String!
var teamB_name: String!
var teamA_points: [Int] = []
var teamB_points: [Int] = []
/*
- Add points to the teams
*/
mutating func addPoints(teamA: Int, teamB: Int){
self.teamA_points.append(teamA)
self.teamB_points.append(teamB)
}
as you can see i have two int arrays that will hold the user points. I have a controller with two tableviews to show the array of points added by the user, i'm going to skip some of the code since i know is not needed for my problem, this is my Main ViewController where tables will show the points
class GameScoreViewController: UIViewController {
/*
- Properties
*/
var gameScore = GameScore()
override func viewDidLoad() {
super.viewDidLoad()
//- Setup delegates & datasources
teamA_tableview.delegate = self
teamA_tableview.dataSource = self
teamB_tableview.delegate = self
teamB_tableview.dataSource = self
// - Button configuration
addPointsButton.layer.cornerRadius = 5
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "toPopUp"{
let popUpView = segue.destination as! PopUpViewController
// this is where i call my popup view
}
}
}
}
now here is where my problem occurs, when i segue to my pop up and the user enters the score needed and taps done, the data doesn't append to the array and my tableview won't reload, i've tried many different ways, using callbacks, delegates, i tried userdefaults since is not very important data but nothing seems to work, and i'm stuck, this is my pop up view controller button action where it should happen, i left the textfield.text in the parameter for reference
#IBAction func addBtnPressed(_ sender: Any) {
// this is the func to append data to the array
gameScore.addPoints(teamA: Int(pointsTextField.text!)!, teamB: 0)
self.dismiss(animated: true, completion: nil)
//after dismissed it should reload table view or insert row with the user entered score
}
any help will be appreciated, thank you.
It seems that you declare a separate instance inside the popup
gameScore.addPoints(teamA: Int(pointsTextField.text!)!, teamB: 0)
You need to set a delegate to the real object in segue
let popUpView = segue.destination as! PopUpViewController
popUpView.delegate = self
And declare it inside the popup like
var delegate:GameScoreViewController?
Then use it
delegate?.addPoints(teamA: Int(pointsTextField.text!)!, teamB: 0)
Trying to pass data from one view controller MainScreenVC to Another RatesVC with protocol and extension, but that's not working, app crashing everytime . I'm clearly see that problem with code on second VC(because print showing correct data after action on first VC) but not sure where is error.
StoryBoard and 1st VC Example
Second VC
1st View controller
import UIKit
protocol transferNameOfCurrency {
func currencySelected(nameOfCurrency: String)
}
class MainScreenVC: UIViewController {
var transferCurrencyDelegate: transferNameOfCurrency?
var nameOfTheCurrency: String?
#IBAction func updateRates(_ sender: Any) {
nameOfTheCurrency = "EUR"
transferCurrencyDelegate?.currencySelected(nameOfCurrency:
nameOfTheCurrency)
print(nameOfTheCurrency)
}
}
2nd ViewController
import UIKit
class RatesVC: UIViewController {
var currencySelected: String?
override func viewDidLoad() {
super.viewDidLoad()
if let push = self.storyboard?.instantiateViewController(withIdentifier: "MainScreenVC") as? MainScreenVC
{
push.transferCurrencyDelegate = self
}
// Do any additional setup after loading the view.
}
}
extension RatesVC: transferNameOfCurrency {
func currencySelected(nameOfCurrency: String) {
currencySelected = nameOfCurrency
print(currencySelected)
}
}
The most obvious problem lies here:
if let push = self.storyboard?.instantiateViewController(withIdentifier: "MainScreenVC") as? MainScreenVC {
push.transferCurrencyDelegate = self
}
You have to realize that instantiateViewController creates a new view controller - it's not the reference to the view controller presented at the screen. In that code you just created a completely new view controller and then set its delegate to self, but otherwise nothing else.
Without knowing the context it is really hard to suggest anything - prepare(for:) segue might be the place where you want to set the delegate. Anyway, the problem is that you have to obtain a reference to the controller that is presented on the screen, the one that is supposed to be reacting to those events.
Moreover, from the memory management aspect, you should really consider making the delegate property a weak one to prevent memory leaks.
EDIT
So after seeing the minimal working example you provided at link, I think I can provide the solution on how to get that string to the SecondVC.
Your first view controller with comments:
import UIKit
class ViewController: UIViewController {
var newLine: String = "EUR"
#IBAction func push(_ sender: Any) {
// here the secondVC does not exist yet, calling delegate.transferWord() here would have no sense
// performSegue will create that secondVC, but now it does not exist, nor it is set up as the delegate
self.performSegue(withIdentifier: "ViewController", sender: navigationController)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let secondVC = segue.destination as? SecondVC, segue.identifier == "ViewController" {
// at this moment secondVC did not load its view yet, trying to access it would cause crash
// because transferWord tries to set label.text directly, we need to make sure that label
// is already set (for experiment you can try comment out next line)
secondVC.loadViewIfNeeded()
// but here secondVC exist, so lets call transferWord on it
secondVC.transferWord(word: newLine)
}
}
}
No need for delegates here, because your ViewController is the one pushing the SecondVC to the Navigation controller - that means that you can access it directly in prepare(for:), as you can see above.
Now the SecondVC is super simple (I omitted unnecessary code):
import UIKit
class SecondVC: UIViewController {
#IBOutlet weak var label: UILabel!
func transferWord(word: String) {
label.text = word
}
}
Storyboards can stay as they are.
I am pretty new to swift development and have some problems understanding how to pass data between ViewController.
I want to build a simple music player app which has three views (Player, Playlists, Tracks).
At start the Player is shown to the user. From there the user can press a button and the Playlists view come up. Now he can select a playlist and the next view Tracks is displayed.
If he press on a track he gets back to the Player view and the track is playing. So I need to pass my track to my PlayerViewController.
Currently I'm using Segues to display each ViewController.
Player -> Playlists -> Tracks -> Player
But this will initialise the Player again which means that values/variables get reset. How can I avoid this?
If you are getting from view controller B to view controller C by saying present, then view controller C can speak to view controller B as its presentingViewController.
Try using Unwind segues to pass data i guess they can help you out.
An unwind segue (sometimes called exit segue) can be used to navigate
back through push, modal or popover segues (as if you popped the navigation
item from the navigation bar, closed the popover or dismissed the modally
presented view controller). On top of that you can actually unwind through
not only one but a series of push/modal/popover segues, e.g. "go back"multiple steps in your navigation hierarchy with a single unwind action.When you perform an unwind segue, you need to specify an action, which is an action method of the view controller you want to unwind to.
//ViewControllerA:
import UIKit
class ViewControllerA: UIViewController {
var dataRecieved: String? {
willSet {
labelOutlet.text = newValue
}
}
#IBOutlet weak var labelOutlet:UILabel!
#IBOutlet weak var nextButtonOutlet: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
#IBAction func nextButtonAction(_ sender:UIButton) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "ViewControllerB") as! ViewControllerB
controller.dataPassed = labelOutlet.text
self.present(controller, animated: true, completion: nil)
}
// segue ViewControllerB -> ViewControllerA
#IBAction func unwindToThisView(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? ViewControllerB {
dataRecieved = sourceViewController.dataPassed
}
}
}
//ViewControllerB
import UIKit
class ViewControllerB: UIViewController , UITextFieldDelegate{
#IBOutlet weak var textFieldOutlet: UITextField!
var dataPassed : String?
override func viewDidLoad() {
super.viewDidLoad()
textFieldOutlet.text = dataPassed
textFieldOutlet.delegate = self
}
// UITextFieldDelegate
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// User finished typing (hit return): hide the keyboard.
textField.resignFirstResponder()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
dataPassed = textField.text
}
}
//From the Return Button click control and drag to exit of the viewcontrollerB as show in the image.
To pass data between View Controllers, have this block in your code:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let feed = segue.destination as! SecondViewControllerName
feed.variableinsecondviewcontroller = variableincurrentviewcontroller
}
If that didn't help, you might need to elaborate on what exactly you want with your code...
Use struct class and access your object any where using the stuct object
Like this
struct SomeStruct { var name: String init(name: String) { self.name = name } } var aStruct = SomeStruct(name: "Bob") var bStruct = aStruct // aStruct and bStruct are two structs with the same value! bStruct.name = "Sue" println(aStruct.name) // "Bob" println(bStruct.name) // "Sue"
I am re-writing a tutorial converting the code from Objective-C to swift. The app moves from VC one where there is 3 sliders (Red, Green and Blue) that set the background colour, a label of the colour name and a button that links to the second VC. In the second VC the colour from the first VC is used as the background and the user has a chance to name the colour.
When the user enters the colour name it should return the new colour name to the orginal VC and the label that shows the colour name should show the text entered.
The following is the code that is causing issue:
func textFieldShouldReturn(nameEntry: UITextField) -> Bool
{
ViewController().colourLabel.text = nameEntry.text
nameEntry.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
return true
}
The error "fatal error: unexpectedly found nil while unwrapping an Optional value" is generated. However debugging nameEntry.text has a string in it.
I'm a little stumped. I could try and do a prepare for unwind segue but it is meant to be a tutorial app.
Cheers
ViewController() actually creates a new instance of your ViewController. This is not a reference to the already existing ViewController. What you can do is create a weak variable pointing to first ViewController inside the second ViewController and set it at prepareForSegue or when the second View controller is shown.
class SecondViewController : UIViewController {
weak var firstViewController : ViewController?
// Other code
func textFieldShouldReturn(nameEntry: UITextField) -> Bool
{
firstViewController?.colourLabel.text = nameEntry.text
nameEntry.resignFirstResponder()
dismissViewControllerAnimated(true, completion: nil)
return true
}
}
Inside First View Controller prepareForSegue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SecondViewController" {
let secondViewController = segue.destinationViewController as SecondViewController
secondViewController.firstViewController = self
}
}
It's possible that the view controller returned by ViewController() has not yet loaded its views. You could try checking this in a setter function and storing it for later use once the views have been loaded.
class VC : UIViewController {
#IBOutlet weak var colourLabel: UILabel!
var savedLabelText: String?
override func viewDidLoad() {
super.viewDidLoad()
self.colourLabel.text = self.savedLabelText
}
func setColorLabelText(label: String) {
if self.isViewLoaded() {
self.colourLabel.text = label
}
else {
self.savedLabelText = label
}
}
}
I'm doing some drawing on a custom UIView canvas, and rather than having a set of buttons at the bottom of the view to allow the user to select shapes, I'd like to have the user do a long press gesture, then have a popup-type menu appear with different shapes they can choose. I don't see anything like this in xCode, though I'd assume there's something like that in iOS. I don't want the alert popup that shows up when you have low battery and notifications.
I've looked into using a UIPopoverController but I'm a bit confused about some of the other Stack Overflow questions I've read about it, and also about the documentation given by Apple.
I described the steps to achieve floating menu as shown in above image:
Create segue from the barButtonItem to the MenuViewCobtroller of type 'Present as Popover'
In the MenuViewController override the preferredContentSize as:
override var preferredContentSize : CGSize
{
get
{
return CGSize(width: 88 , height: 176)
}
set
{
super.preferredContentSize = newValue
}
}
In my case I am returning CGSize with width 100 and size 200. You can set these values so as to fit your floating menu content properly.
4. In the initial/source view controller, in the prepare(for segue: sender) method set self as popoverPresentationController delegate:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowMenuSegue" {
if let tvc = segue.destination as? MenuViewController
{
tvc.delegate = self
if let ppc = tvc.popoverPresentationController
{
ppc.delegate = self
}
}
}
}
The source view controller must comply to UIPopoverPresentationControllerDelegate and implement following method:
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
}
That's it. You got the floating menu. Hopefully this will be useful.
I used Masture's method above and it worked for me (thank you!), but a couple of notes for other newbies like myself:
Make sure you put "ShowMenuSegue" (or whatever you choose) as the identifier for your segue in the Storyboard, and
I had to add
var delegate: MainViewController!
in the MenuViewController (with MainViewController being your source view controller) in order to get tvc.delegate = self to work
After you make a connection of that button with the viewController and popover as a segue you will need to prepare. Here is the following code in order to prepare for the popover segue.
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
{
if let identifier = segue.identifier
{
switch identifier
{
case History.SegueIdentifier:
if let tvc = segue.destinationViewController as? TextViewController
{
if let ppc = tvc.popoverPresentationController
{
ppc.delegate = self
}
tvc.text = "\(diagnosticHistory)"
}
default: break
}
}
}
Do keep in mind that if you have an iPhone the popover will take full screen, so you can fix that using this for let's say a text that takes some particular elements.
This will fix the popover to be exactly the size of the elements you have in your text.
#IBOutlet weak var textView: UITextView!
{
didSet
{
textView.text = text
}
}
var text : String = ""
{
didSet
{
textView?.text = text
}
}
override var preferredContentSize : CGSize
{
get
{
if textView != nil && presentingViewController != nil
{
return textView.sizeThatFits(presentingViewController!.view.bounds.size)
}
else
{
return super.preferredContentSize
}
}
set {super.preferredContentSize = newValue}
}
}
I have those 2 in different view controllers but I guess it will work. You will also need to implement UIPopoverPresentationControllerDelegate and
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
to your first viewController.