get Nil when calling for IBOutlet properties in function - ios

i’m working in swift and i’m trying to use the .frames to check if 2 objects of type CGRect intersect.
i have my View Controller Class and a CircleClass, the CircleClass creates a circle that has gesture recognition so i can drag the circles that i create where i want to, now i want to add the option that if at the end of the drag the subview intersects my trashimageView (image view that will always be in the low-right corner of the view for all devices it's like a trashcan) it can delete the circle or subView.
the problem is that when i try to call trashImageView.frame in a function “deleteSubView” that i’ve created in the View Controller i get nil and my app crashes.
But if the IBOutlet is in the VC and my function is defined in my VC, also i can call the trashImageView.frame (CGRect Value) in the viewDidLoad and there is fine, but not in my function, why do i get nil for this value??
class ViewController: UIViewController {
#IBOutlet var trashImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
//here i can print the CGRect value just fine
print("my imageView init: \(trashImageView.frame)")
}
func deleteSubView(subView: UIView){
// Here i get nil from the trashImageView.frame
if (subView.frame.intersects(trashImageView.frame)) {
print("intersection")
subView.removeFromSuperview()
}
}
}
i've checked that the Nil value is from the 'trashImageView.frame' and that the connection with the storyboard is good.
i call the function ‘delete subView’ from another class but should that matter? i don’t understand what is the error here, why do i get nil? help please.

Since your UIViewController is declared and instantiated using storyboard my guess is that you are creating the view controller using it's no arg initializer, i.e.: let controller = MyController() if you must create an instance of the controller programmatically do so by obtaining a reference to the Storyboard that contains the controller, i.e like this:
NOTE: Here I'm using "MyController" as the name of the class and the identifier that has been set in the storyboard.
func createMyController() -> MyController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewController(withIdentifier: "MyController")
return controller as! MyController
}
I'd also add a guard for view load state in your deleteSubview(:subView) method, so something like this:
func deleteSubView(subView: UIView) {
guard isViewLoaded else { return }
// Here i get nil from the trashImageView.frame
if (subView.frame.intersects(trashImageView.frame)) {
print("intersection")
subView.removeFromSuperview()
}
}

Related

scrollView setContentOffset crashes when accessing method from another file

I have a HomeView class with scrollView IBOutlet and a function the changes the offset of the scrollView:
scrollView.setContentOffset(CGPoint(x: self.view.frame.width * 2, y: 0), animated: true)
From the FeedView class I attempt:
let Home = HomeView()
Home.ScrollRight()
But I get this error:
fatal error: unexpectedly found nil while unwrapping an Optional value
Because your HomeView created by either XIB or Story board. Thats why below line return nil object.
let Home = HomeView()
If you want to call ScrollRight() method of HomeView from another class then declare global variable of HomeView like
var homeVC = HomeView()
and in viewDidMethod of HomeView
homeVC = self as HomeView
not you can access homeVC from anywhere
and just call method by
homeVC.ScrollRight()
from another viewController.
=======================================
EX
import UIKit
var homeVC = HomeVC()
class HomeVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
homeVC = self as HomeVC
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
Now you can use homeVC object and access property of HomeVC anywhere.
NOTE:
If you are calling function from same viewController (HomeView) then you don't need to create object of that viewController. You can just simply call function by below like directly.
ScrollRight() OR self.ScrollRight()
If your scroll view is set up as an outlet it means it is created within your view controller's main view when said view controller is instantiated from a storyboard.
This code:
let Home = HomeView()
...at most instantiates the view controller itself, but it does not load its view (let alone wire the outlets to the appropriate subviews -in this case, your scroll view).
Please read about view controller view life cycle. It's all in Apple's programming guides and countless tutorials online.

ImageView retuning nil

I have a view controller OtherUserAccountViewController containing a button with a "profile picture" as its background. When this button is tapped, I would like to push a new view controller ImageTappedViewController onto the stack to simply present a bigger ImageView of this said profile picture. Please note the Storyboard identifier for the screenshot seen below is in fact "imageTapped" and the class is ImageTappedViewController
Below is my function for instantiating and pushing the new view controller:
In OtherUserAccountViewController.swift:
#IBAction func profilePicButtonTapped() {
let sb = UIStoryboard(name: "SuccessfulLogin", bundle: nil) //SB name: SuccessfulLogin
let cc = (sb.instantiateViewController(withIdentifier: "imageTapped")) as! ImageTappedViewController
if cc.imageView == nil || cc.imageView == UIImage() {
print("Nil") //<- Nil is printed upon firing this function
} else {
print("not nil")
}
//cc.imageView.image = self.profilePicButton.currentBackgroundImage <- breaks because the imageView is nil
self.navigationController?.pushViewController(cc, animated: true)
}
ImageTappedViewController.swift:
import UIKit
class ImageTappedViewController: UIViewController {
#IBOutlet weak var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
I am totally lost as to why this imageView is returning nil.
I set the imageView to display the lovely Taylor Swift on default as seen below; but regardless, nil is being returned.
Any help is much appreciated!
When view controllers are first initialized their IBOutlets will not be initialized. Only after viewDidLoad will all of their outlets be non-nil. Pass the image as a UIImage then in viewDidLoad set image view's image property.

Pass data to View Controller embedded inside a Container View Controller

My view controller hierarchy is the following:
The entry point is a UINavigationController, whose root view controller is a usual UITableViewController. The Table View presents a list of letters.
When the user taps on a cell, a push segue is triggered, and the view transitions to ContainerViewController. It contains an embedded ContentViewController, whose role is to present the selected letter on screen.
The Content View Controller stores the letter to be shown as a property letter: String, which should be set before its view is pushed on screen.
class ContentViewController: UIViewController {
var letter = "-"
#IBOutlet private weak var label: UILabel!
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
label.text = letter
}
}
On the contrary, the Container View Controller should not know anything about the letter (content-unaware), since I'm trying to build it as reusable as possible.
class ContainerViewController: UIViewController {
var contentViewController: ContentViewController? {
return childViewControllers.first as? ContentViewController
}
}
I tried to write prepareForSegue() in my Table View Controller accordingly :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
// Not executed:
containerViewController.contentViewController?.letter = letter
}
}
but contentViewController is not yet created by the time this method is called, and the letter property is never set.
It is worth mentioning that this does work when the segue's destination view controller is set directly on the Content View Controller -- after updating prepareForSegue() accordingly.
Do you have any idea how to achieve this?
Actually I feel like the correct solution is to rely on programmatic instantiation of the content view, and this is what I chose after careful and thorough thoughts.
Here are the steps that I followed:
The Table View Controller has a push segue set to ContainerViewController in the storyboard. It still gets performed when the user taps on a cell.
I removed the embed segue from the Container View to the ContentViewController in the storyboard, and I added an IB Outlet to that Container View in my class.
I set a storyboard ID to the Content View Controller, say… ContentViewController, so that we can instantiate it programmatically in due time.
I implemented a custom Container View Controller, as described in Apple's View Controller Programming Guide. Now my ContainerViewController.swift looks like (most of the code install and removes the layout constraints):
class ContainerViewController: UIViewController {
var contentViewController: UIViewController? {
willSet {
setContentViewController(newValue)
}
}
#IBOutlet private weak var containerView: UIView!
private var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
setContentViewController(contentViewController)
}
private func setContentViewController(newContentViewController: UIViewController?) {
guard isViewLoaded() else { return }
if let previousContentViewController = contentViewController {
previousContentViewController.willMoveToParentViewController(nil)
containerView.removeConstraints(constraints)
previousContentViewController.view.removeFromSuperview()
previousContentViewController.removeFromParentViewController()
}
if let newContentViewController = newContentViewController {
let newView = newContentViewController.view
addChildViewController(newContentViewController)
containerView.addSubview(newView)
newView.frame = containerView.bounds
constraints.append(newView.leadingAnchor.constraintEqualToAnchor(containerView.leadingAnchor))
constraints.append(newView.topAnchor.constraintEqualToAnchor(containerView.topAnchor))
constraints.append(newView.trailingAnchor.constraintEqualToAnchor(containerView.trailingAnchor))
constraints.append(newView.bottomAnchor.constraintEqualToAnchor(containerView.bottomAnchor))
constraints.forEach { $0.active = true }
newContentViewController.didMoveToParentViewController(self)
}
} }
In my LetterTableViewController class, I instantiate and setup my Content View Controller, which is added to the Container's child view controllers. Here is the code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let containerViewController = segue.destinationViewController as? ContainerViewController {
let indexPath = tableView.indexPathForCell(sender as! UITableViewCell)!
let letter = letterForIndexPath(indexPath)
containerViewController.navigationItem.title = "Introducing \(letter)"
if let viewController = storyboard?.instantiateViewControllerWithIdentifier("ContentViewController"),
let contentViewController = viewController as? ContentViewController {
contentViewController.letter = letter
containerViewController.contentViewController = contentViewController
}
}
}
This works perfectly, with an entirely content-agnostic container view controller. By the way, it used to be the way one instantiated a UITabBarController or a UINavigationController along with its children, in the appDidFinishLaunching:withOptions: delegate method.
The only downside of this I can see: the UI flow ne longer appears explicitly on the storyboard.
The only way I can think of is to add delegation so that your tableViewController implements a protocol with one method to return the letter; then you have containerViewController setting its childViewController (the contentViewController) delegate to its parent. And the contentViewController can finally ask its delegate for the letter.
At your current solution the presenting object itself is responsible for working both with the "container" and the "content", it doesn't have to be changed, but such solution not only has the issues like the one you described, but also makes the purpose of the "container" not very clear.
Look at the UIAlertController: you are not configuring its child view controller directly, you are not even supposed to know it exists when using the alert controller. Instead of configuring the "content", you are configuring the "container" which is aware of the content interfaces, lifecycle and behavior and doesn't expose it. Following this approach you achieve a properly divided responsibility of the container and content, minimal exposure of the "content" allows you to update the "container" without a need to update the way it is used.
In short, instead of trying to configure everything from a single place, make it so you configure only the "container" and let it configure the "content" when and where it is needed. E.g. in the scenario you described the "container" would set data for the "content" whenever it initializes the child controllers. I'm using "container" and "content" instead of ContainerViewController and ContentViewController because the solution is not strictly based on the controllers because you might as well replace it wth NSObject + UIView or UIWindow.

Segue to UIViewController without initializing a new object

I am building in iOS 9 with Swift 2.0. I have my starting UIViewController that is my menu screen. It contains the following code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let id = segue.identifier where id == "GamePlayScene" {
self.gameVC = segue.destinationViewController as? GameViewController
self.gameVC!.delegate = self
if let s = sender as? GKTurnBasedMatch {
self.gameVC!.match = s
}
}
}
When segueing to my GameViewController, the following init runs before that prepareForSegue even gets called:
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
GKLocalPlayer.localPlayer().registerListener(self) // I only want this once
}
In storyboard, my GameViewController has a "Menu" button that is connected to the exit widget for the view controller and it unwinds to the Menu as intended. But whenever I perform the segue again, the init gets called again so I now have multiple GameViewControllers. I think this slows my app down since I am using SKScenes. How do I perform a segue without it creating a new object every time?
func player(player: GKPlayer, receivedTurnEventForMatch match: GKTurnBasedMatch, didBecomeActive: Bool) {
if didBecomeActive {
// This event is what activated the app, so the user wants it right meow
GameKitHelper.sharedInstance.match = match
performSegueWithIdentifier("GamePlayScene", sender: match)
}
}
You can store your GameViewController in a singleton so that it only needs to be created once. Since the view controller is initialized only once it saves processing time. This is probably a good performance optimization for an app using two view controllers, such as your game, that need to frequently be switched back and forth between the two.
The way to do this is to create a new Swift class and have it accessible as a shared instance where the second view controller is stored in a private property. The second view controller is instantiated if it has not yet been initialized. Further retrievals of the view controller return the stored view controller thereby eliminating the need to initialize the view controller. The operations for creating the view controller, storing it, and returning it are handled by the getter of a publically accessible computed property.
Here is the code for the class:
Singleton.swift:
import Foundation
import UIKit
class Singleton {
static let sharedInstance = Singleton()
var gameViewController: GameViewController {
get {
if self.storedViewController == nil {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
self.storedViewController =
storyboard.instantiateViewControllerWithIdentifier("GameViewController") as? GameViewController
}
return self.storedViewController!
}
}
private var storedViewController: GameViewController?
}
To use this, it will be necessary to use the showViewController method of showing the game view controller instead of using a segue.
In the first view controller I have:
#IBAction func buttonWasPressed(sender: AnyObject) {
let vc = Singleton.sharedInstance.gameViewController
navigationController?.showViewController(vc, sender: self)
}
The second controller (GameViewController) will now only be created once and re-used every time the button is pressed on the first view controller.
GKLocalPlayer.localPlayer().registerListener(self) // I only want this once
The way to cause a line of code to run only once over the lifetime of the app is with dispatch_once.

Programmatically modifications will not apply to children class (viewController)

I my iOS application maybe I need to override some viewControllers so I developed the following method to discover if the override ViewController exists I use it otherwise use from the original viewController.
internal class func getOverride<T: RBaseViewController>(id: String, mClass: AnyClass) -> T
{
let storyBoard = UIStoryboard(name: RConstants.Static.MAIN_STORYBOARD_NAME, bundle: nil)
let originalViewController = storyBoard.instantiateViewControllerWithIdentifier(id) as! T
var className = NSStringFromClass(mClass)
// Removing the prefix of class name if exists
if let index: String.Index = className.rangeOfString(".")?.startIndex.successor()
{
className = className.substringFromIndex(index)
}
// Override class name
var overrideClassName = className! + RConstants.Static.CLASSES_OVERRIDE_SUFFIX
if let mClass: AnyClass = NSClassFromString(overrideClassName)
{
RLog.debug("View controller override found")
// ViewController is overrode
if let overeideClass: T.Type = mClass as? T.Type
{
// Create instance and return
let overrodeViewController = overeideClass()
let view = originalViewController.view
originalViewController.view = nil
overrodeViewController.view = view
return overrodeViewController
}
}
return originalViewController
}
The Override viewController extends from the original one and just maybe some methods will change.
But the issue is that the modifications that I applied on the viewController is not affecting the overrode viewController.
For example in the parent viewController class I have the following code :
override func viewDidLoad()
{
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
title = "Map"
}
But in the Child viewController the title is empty, This issue is not just for title, Anything that is changed in the main viewController programmatically will not affect the child viewController.
Any suggestion ?
I found the issue.
By using xibfiles instead of storyboards this issue has fixed.
Seems that this is not possible to change the controller of a storyboard view.
in The above question, When I had set the view of originalViewController to nil all and set the new view to overrode view controller so the new view is not the same as old one, It's just a copy of it, So all the modifications shouldn't apply on the copy of it.

Resources