Short lag when setting label text using storyboards - ios

In my application, I have a mainViewController with some content on it. At some points, I load an overlay view controller from storyboard. The overlay view controller is smaller than the screen and is presented on top of the mainViewController. I initialize it the following way:
class MyOverlayViewController {
#IBOutlet var textLabel: UILabel!
#IBOutlet var countLabel: UILabel!
static let storyboard = UIStoryboard(name: "...", bundle: nil)
// Return a new view controller
class func newViewControllerWithData(data: AnyObject) -> UIViewController {
let vc = storyboard.instantiateViewControllerWithIdentifier("MyOverlayViewController") as! MyOverlayViewController
Timing.performAfterDelay(0) {
vc.titleLabel.text = data[...] // Load title label text
vc.countLabel.text = data[...] // Load count label text
}
return vc
}
}
I cannot set the text of the labels immediately in the method newViewControllerWithData, because that produces the following error: fatal error: unexpectedly found nil while unwrapping an Optional value. So the labels are nil when accessing them immediately in this method.
It seems like the two label outlets are not loaded immediately when the view controller is instantiated from storyboard, because this takes a (very) short time.
Therefore, I use my method Timing.performAfterDelay(0) which executes the code after the next run-loop cycle (it starts a timer with duration 0 and executes the code as callback). The code is (I have checked that) executed on the main thread.
The problem is the following:
Sometimes (not always, and not reproducible!), when loading the overlay view controller, for a fraction of a second the labels are empty (like I have defined them in storyboard) before they are showing the text.
So the user sees empty labels for a short moment before the actual data is loaded into the labels.
How can I fix this behavior?
Is it possible somehow to access the outlets immediately after instantiating the view controller from storyboard, without using Timing.performAfterDelay(0)?
Help would be appreciated.

Outlets are set after view is loaded i.e. when viewDidLoad gets called on the view controller. However, calling it directly like vc.viewDidLoad() will not work, you have to access the view controller's view like let dummyVariable = vc.view instead. Here's the code that force loads the view and then sets the label values.
class func newViewControllerWithData(data: AnyObject) -> UIViewController {
let vc = storyboard.instantiateViewControllerWithIdentifier("MyOverlayViewController") as! MyOverlayViewController
let _ = vc.view // force load the view
// now set your outlets as you please
vc.titleLabel.text = data[...] // Load title label text
vc.countLabel.text = data[...] // Load count label text
return vc
}
NOTE: This is not really a good practice though. MyOverlayViewController should be responsible for setting its label values instead of these being set from the outside. You could pass it the required data via a property or argument to a method, etc.

Related

Passing data in a segmented Controller?

I got stuck because one ViewController trying to pass data from one ViewController to another. To explain my code is that I have label in mainViewController named name1 with data inside of it and I am trying to pass that data that is in text format to the name2 label in the firstViewController of name2 label, Can you please help me with it.
Thank you in advance
class mainViewController: UIViewController {
//its inside a segment controller
#objc func Seg(sender: UISegmentedControl) {
switch sender.selectedSegmentIndex {
case 0:
let firstViewController = SharedViewController()
print(self.name1.text!)
firstViewController.name2.text = self.name1.text!
self.addChild(firstViewController)
self.bottomContainer.addSubview(firstViewController.view)
firstViewController.didMove(toParent: self)
default:
let secondViewController = Shared2ViewController()
self.addChild(secondViewController)
self.bottomContainer.addSubview(secondViewController.view)
secondViewController.didMove(toParent: self)
}
Don't try to manipulate another view controller's views directly. That violates the principle of encapsulation and often doesn't work (as in this case.)
You should add a string property to the other view controller, and install that string into the label text in viewWillAppear.
Also note that creating a view controller with an init like SharedViewController() doesn't usually do what you want. (You won't get the view controller's views loaded from it's storyboard/xib.) You usually want to use the Storyboard instantiate method, or to load it from the XIB.

Collection View returns nil when called in function

I want to run a function which involves adding a sublayer to a collection view. However when I run the function the app crashes saying Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value When I print the collection view it shows up in the log as none so I know the collection view is the problem. The view has already been loaded when I call the function and I can see all of it's cells. The function is being called from another class, which I think might have something to do with the problem.
Here is the function that I am calling...
func displayCircle() {
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.green.cgColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.green.cgColor
//you can change the line width
shapeLayer.lineWidth = 3.0
print(shapeLayer)
print(collectionView)
collectionView!.layer.addSublayer(shapeLayer)
}
Here is how I am calling this function from another class...
ViewController().displayCircle()
EDIT: This is my storyboard layout...
What could the problem be?
As you can see, I am using a page view controller. Hope this helps
There's likely a few issues here.
If you wanted to say ViewController.displayCircle() then displayCircle would need to be a static function. But I don't think that was your intention, you probably don't want to do that in this case, and also your static function syntax is wrong (ViewController().displayCircle() is wrong). But moving on... :)
ViewController().displayCircle() isn't how you properly reference the collectionView. First you need a reference to the other view controller. Then inside displayCircle you need to grab a reference to the collectionView if it's in another View Controller. So that would be otherViewController.collectionView provided the collectionView is public of course, and provided you have a reference to that other view controller somehow. Note that you can't just make a new reference of the other view controller, otherwise you'll be adjusting the layer on the new instance, not the original.
Last but not least, you're force unwrapping the collectionView - don't do that. Your app will crash if it's ever nil. Instead, take advantage of Swift's paradigms:
if let collView = collectionView {
collView.layer...// etc
}
This last bit isn't the issue, but just good practice.
If the collectionView is part of this same viewController, use self:
self.collectionView.layer.addSublayer(shapeLayer)
If the collectionView is in a different viewController, as it seems to be, you would need to get a reference to that view controller. How you do this depends on the structure of your app. You mention using a UIPageViewController and presumably both the view controllers are presented on it.
From one view controller, you can refer to another like this:
let pvc = self.parent as? UIPageViewController // Or your custom class
let targetViewController = pvc.viewControllers[index] as? YourTargetViewControllerClass
You might need to figure out what index you need. An alternative is to make sure each child view controller of the UIPageViewController has its own subclass, then find the one you want like this:
let pvc = self.parent as? UIPageViewController
let viewControllers = pvc.viewControllers.filter { $0 is CustomSubclass }
if let viewController = viewControllers.first as? CustomSubclass {
viewController.displayCircle()
}
As the other answer states, using ViewController() creates a brand new view controller instance, not the one that already exists.
This
ViewController()
creates a new instance other than the presented one ,when you reference the collectionView from it it's actually nil as you have to load the VC either from storyboard / xib , so you have to use delegate to reference the current VC that contains the collectionView

Updating a view controller's content from another view controller without opening it

In the app I am developing I need to update 2 view controllers when a button is tapped. I managed to update the view controller which contain the button, but i could not update the content of the other view controller.
I tried to do so by a couple of ways: first, I created a delegate variable (using a protocol) in the view controller of the button and set its value to the other view controller:
var delegate: WeatherServiceDelegateForcast? = ViewController2()
as ViewController2 is the view controller I am trying to update using the delegate.
the protocol:
protocol WeatherServiceDelegateForcast{
func SetForcast(forcast: ForcastInfo)
}
The SetForcast func in viewController2
func SetForcast(forcast: ForcastInfo) {
self.day1Label.text = ....
}
the problem is that when executing the function SetForcast, the IBOutlet of day1label is nil and as a result it cannot be updated. I suppose that is happens because the view controller has not yet been loaded, but i could not figure out a way to load it.
secondly, I tried to use segue (without delegate) to pass the data to viewcontroller2. That worked but only when I used type show for the segue and in my app it is critical that it won't open. When i tried to use segue without opening viewcontroller2 (by type custom) it once again did not work and could not identify the IBOutlets.
Any suggestions how to pass the data to viewcontroller2 without opening it?
edit:
this is the code i used:
when the button in viewcontroller1 is tapped it takes the data from textfield and call the function from its property weather service (that works).
self.weatherService.GetForcast(textField.text!)
GetWeather gets the weather of a city, creates a variable from type forecast and then:
if self.delegate2 != nil
{
dispatch_async(dispatch_get_main_queue(), {
self.delegate2?.SetForecast(forecast)
})
}
as delegate2 is the property of weatherService that refers to viewcontroller2 and this is how it is defined in the beginning of weatherService class:
var delegate2: WeatherServiceDelegateForecast? = ViewController2()
and WeatherServiceForcast is the protocol:
protocol WeatherServiceDelegateForcast
{
func SetForecast(forecast: ForecastInfo)
}
then, according to the debugger, it executes SetForecast of viewcontroller2 with the right forecast variable:
func SetForcast(forcast: ForcastInfo) {
self.day1 = String(round(forcast.day[0])) + "-" + String(round(forcast.night[0]))
self.day2 = String(round(forcast.day[1])) + "-" + String(round(forcast.night[1]))
self.day3 = String(round(forcast.day[2])) + "-" + String(round(forcast.night[2]))
self.day4 = String(round(forcast.day[3])) + "-" + String(round(forcast.night[3]))
self.day5 = String(round(forcast.day[4])) + "-" + String(round(forcast.night[4]))
self.day6 = String(round(forcast.day[5])) + "-" + String(round(forcast.night[5]))
self.day7 = String(round(forcast.day[6])) + "-" + String(round(forcast.night[6]))
}
as day1-7 are variables in viewController2 class from which i want to update the labels of viewController2 afterwards, and this is how these variables are defined:
var day1 = String()
var day2 = String()
var day3 = String()
var day4 = String()
var day5 = String()
var day6 = String()
var day7 = String()
in viewDidLoad i put the code to update the labels into the strings of the variables day1-7 but it seems it firstly calls that function and only then it calls set forecast and updates the variables.
override func viewDidLoad() {
// Do any additional setup after loading the view.
self.day1Label.text = self.day1
self.day2Label.text = self.day2
self.day3Label.text = self.day3
self.day4Label.text = self.day4
self.day5Label.text = self.day5
self.day6Label.text = self.day6
self.day7Label.text = self.day7
}
Why not update the content of your viewController2 in viewWillAppear or viewDidLoad? A view controller won't load its view and subviews until it needs to display them on screen.
Therefore, Views of your viewController2 is not loaded if you are not going to present it, that's why you got nil and you can't access it.
A clean way to achieve what you want is:
Have a separate property in viewController2 to store the data
Set content of the property in viewController1
Then update the view of viewController2 in viewDidLoad or viewWillAppear according to the property you just set.
Edit
Try change this:
if self.delegate2 != nil
{
dispatch_async(dispatch_get_main_queue(), {
self.delegate2?.SetForecast(forecast)
})
}
to
if self.delegate2 != nil
{
self.delegate2?.SetForecast(forecast)
}
Code inside a async block will be executed asynchronously , so viewWillAppear or viewDidload may be called before these properties are set.
That's why these values are "". Also, no need to use async block here, what you are doing in SetForcast is just set values of properties, they are finished almost immediately.
You simply can't do this.
If the ViewController2 is not loaded it won't never take the data.
You first have to initialize it, then you can give it the data, without displaying it.
You simply have to do an "alloc-init" to load it inside a variable. The you give the data.

Why does initializing a view controller with nibName not set the new instance as file owner?

I am trying to display one view or another view inside the detail view of a master/detail based on a conditional.
These views will contain outlets and elements, so I would like to have view controllers for each that I can play with.
So I created a new UIViewController called AddPhotoViewController. This is how I add AddPhotoViewController.xib inside DetailViewController:
let photoVC = AddPhotoViewController(nibName: "AddPhotoViewController", bundle: nil)
let photoView = photoVC.view
photoVC.delegate = self
photoView.autoresizingMask = UIViewAutoresizing.FlexibleWidth
photoView.frame = area.bounds
area.addSubview(photoView)
The view loads properly in the detail view and looks like this:
AddPhotoViewController.xib's owner class has been set as well here:
When I tap the button, though the action is set properly in AddPhotoViewController to print a message, Xcode crashes.
Am I doing this correctly? Is there a more common practice for loading view X or view Y inside a view controller depending on user data?
Button action:
#IBAction func ButtonPressed(sender: AnyObject) {
println("worked!")
}
Button connection:
Console output:
I think you need to add the viewController:
addChildViewController(PhotoVC)
//and then
PhotoVC.didMoveToParentViewController(self)

iOS using xib + global methods for overlay UIView (Swift)

I'm writing an app that should present overlays in specific situations, like for example the lack of location services enabled for the app.
Overlay is a UIView with a UIImageView (background) a UILabel (title) and a UIButton calling a specific action. I want to use Interface Builder to set up the overlay UI but I would like to recall the overlay and show it on different UIViewControllers, depending on when the lack of location services is detected.
I have set up a custom class (subclass of UIView) to link a xib file. Code below:
class LaunchCustomScreen: UIView
{
#IBOutlet var title: UILabel!
#IBOutlet var enableLocationButton: UIButton!
#IBOutlet var waitingIndicator: UIActivityIndicatorView!
#IBOutlet var bckgroundImage: UIImageView!
func setupDefault()
{
title.text = "Location Services Required"
enableLocationButton.setTitle("Enable Location Services", forState: UIControlState.Normal)
enableLocationButton.addTarget(self,
action: "promptUserForLocation",
forControlEvents: UIControlEvents.TouchUpInside)
hideLocButton()
}
func hideLocButton()
{
enableLocationButton.hidden = true
}
func showLocButton()
{
enableLocationButton.hidden = false
}
}
Then I have created the xib file which is of Class LaunchCustomScreen and I linked the IBOutlets to all the objects in it (UILabels, UIBUtton, UIImageView)
Then I have set some global functions to be called from any other UIViewController in order to show/hide the overlay on the specific view controller and configure it with UIButton hidden or visible (it will be hidden with a waiting indicator when user location is still loading). Below related code:
func setupLaunchDefault(vc: UIViewController) -> LaunchCustomScreen
{
for aSubview in vc.view.subviews
{
if aSubview.isKindOfClass(LaunchCustomScreen)
{
NSLog("Found already a launch screen. Removing")
aSubview.removeFromSuperview()
}
}
var screen: LaunchCustomScreen = LaunchCustomScreen()
screen.setupDefault()
return screen
}
func showLaunchAskLocation(vc:UIViewController)
{
var screen = setupLaunchDefault(vc)
screen.bounds = vc.view.bounds
screen.showLocButton()
vc.view.addSubview(screen)
}
Now I'm trying if the solution works and it crashes on the setupLaunchDefault function. Reason is that even if an instance of LaunchCustomSCreen is created, the variables (title, enableLocationButton) are still nil. I though they should be non-nil thanks to the IBOutlet to the xib... what am I missing?
Thank you in advance for your help!
I have set up a custom class (subclass of UIView) to link a xib file
No, you haven't. No such "link" is possible.
what am I missing?
You're not missing anything, because you've already figured it out!
Merely creating a LaunchCustomScreen instance out of thin air (i.e. by saying LaunchCustomScreen(), as you are doing) merely creates an instance of this class. It has nothing whatever to do with the .xib (nib) file! There is no magic "link" whatever between the class and the nib! Thus, nothing happens that would cause these properties to get any value. They are, as you have rightly explained, nil.
You have designed and configured one special particular instance of LaunchCustomScreen in the nib. That is the instance whose outlets are hooked up, within the same nib. So if you want an instance of LaunchCustomScreen with hooked-up outlets, you must load the nib! Loading the nib is exactly equivalent to making an instance of what's in the nib - it is a form of instantiation. And here, it's the form of instantiation you want, because this instance is the instance you want.
So, the answer is: do not say LaunchCustomScreen() to get your LaunchCustomScreen instance (screen). Instead, load the nib to get your LaunchCustomScreen instance - and all will be well.
So, let's say your .xib file is called LaunchCustomScreen.xib. You would say:
let arr = NSBundle.mainBundle().loadNibNamed("LaunchCustomScreen", owner: nil, options: nil)
let screen = arr[0] as UIView
The first result, arr, is an array of top-level objects instantiated from the nib. The first of those objects (probably the only member of the array) is the view you are after! So you cast it to a UIView and you are ready to stick it into your interface. Since the view comes from the nib, its outlets are set, which is what you're after. You can do this as many times as you need to, to get as many "copies" of this view as you like.

Resources