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

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.

Related

How can an outlet be nil even though it has been set

I have a viewController with two embedded viewControllers through containers. I then created outlets from the parent viewController's containers to the parent class. I want to either hide or show the containers depending on certain conditionals.
But if I simply write:
#IBOutlet var twoArmsContainer: UIView! {
didSet {
print("SETTING TWO ARM")
}
}
override func viewDidLoad() {
super.viewDidLoad()
twoArmsContainer.isHidden = true //container is nil
}
Then it crashes with twoArmsContainer being nil after the print in didSet has been triggered. How is it possible that the outlet is set, but then becomes nil? I have tried hiding it inside didSet and that works fine:
#IBOutlet var twoArmsContainer: UIView! {
didSet {
print("SETTING TWO ARM")
twoArmsContainer.isHidden = true //WORKS
}
}
What else can I say? The class I'm working in inherits from another class so there is a super.viewDidLoad. Not sure if that is relevant. I tried putting the outlets in the super class but with the same results. I also tried removing and readding the outlets again. Have never experienced this problem before. Let me know if I should show more code; perhaps the entire class. Not really sure what's relevant as I'm clueless of where to start.
Ok, so I found out what was wrong; an issue impossible to detect without access to the code, so in retrospect I should have posted both the parent class and container class. Sorry about that.
Anyway the issue was that the container viewController inherited from the parent viewController. This enabled me to share code in the two container viewControllers.
So the structure was basically this:
class WizardChooseArmViewController: WizardViewController {
...
This is the parent viewController which inherits from a base viewController. Then the container viewControllers looked like this:
final class WizardTwoArmsViewController: WizardChooseArmViewController {
...
Apparently it's a bad idea for containers to inherit from its parent, so I refactored and changed it to:
final class WizardTwoArmsViewController: WizardViewController {
...
Not quite sure why its not possible for containers to inherit from its parent. Would be great if someone could brief me.

The proper way to inherit a CustomViewController with "dependencies"

Recently I wrote an app with one single scene and ViewController. I had to set a custom background picture for the View, which the ViewController manages (i.e. my top view contained the UIImageView). Later on I had to implement some logic in ViewController, so that it properly rotates/changes the picture when the screen is rotated. Also I had to overwrite some properties like preferredStatusBarStyle for the ViewController.
Now I have to implement a couple more scenes / screens in my app and it turns out that they all must have the same design as this currently present screen, so I think it makes sense if I create a CommonViewController which contains this common rotation-related logic for background picture, so that I can inherit all my other ViewControllers from this CommonViewController. The only problem I have is that CommonViewController "requires" that the view it manages has a backgroundPicture: UIView property, which I don't know how to ensure.
If I create a new file CommonViewController together with XIB-file, I can add the backgroundPicture image view in XIB and connect it with code (via regular "control-drag" approach), but apparently this won't work, as there is no guarantee that the views which inherit CommonViewController will have this property. What is the correct way to solve this issue without hacks on iOS in Swift?
Unfortunately I could not find a solution, maybe I've been searching for something wrong. It seems that I somehow need to inherit a CommonViewController for each scene (for each CustomViewController), but also I have to somehow set the top view of each of these controller's to be equal to some CommonView, so that CommonViewController does not crash when I try to access #IBOutlet weak var backgroundPicutre: UIImageView!.
The obvious way would be to define some method or property in the CommonViewController, so that the controllers which inherit it, can implement / override it, but it seems a bit hacky as it still requires copy-pasting in each ViewController which inherits CommonViewController.
How I imagined the solution: I create CustomViewController: CommonViewController, then I create a view controller in my Storyboard and change the "Class" property to "CustomViewController" (in property editor), then I select the view which corresponds to this newly added controller and change the "Class" property to "BackgroundImageView. But I'm not sure if it's the correct way to do (also I doubt thatCustomViewControllerwill properly "connect" itsIBOutletfieldbakcgroundImageViewwith the correspondingUIViewfromBackgroundImageView`, that's why I wanted to ask experts what they think about it.
I think you should define your base controller (CommonViewController) entirely in code, i.e. don't use no xibs / storyboards for the base controller. It doesn't mean you should rid off storyboards / xibs completely. Interface for alll other view controllers except CommonViewController may still be implemented with xibs / storyboards.
In this case CommonViewController implementation may look like this:
import UIKit
class CommonViewController: UIViewController {
// use this property every time you need to
// manipulate backgroundPicture
var backgroundPicture: UIImageView = {
// Replace with your image name
let image = UIImage(named: "BackgroundPicture")!
let imageView = UIImageView()
imageView.image = image
return imageView
}()
override func viewDidLoad() {
// If subclass overrides viewDidLoad()
// it should contain super.viewDidLoad()
super.viewDidLoad()
view.addSubview(backgroundPicture)
// Align backgroundPicture to bounds of superview
// You can remove this code and implement
// your own alignment with frames or Autolayout
backgroundPicture.frame = view.bounds
// Send backgroundPicture to back of the view
// Otherwise backgroundPicture may overlap views added in subclasses
view.sendSubviewToBack(backgroundPicture)
}
override func viewDidLayoutSubviews() {
// If subclass overrides viewDidLayoutSubviews()
// It should contain super.viewDidLayoutSubviews()
super.viewDidLayoutSubvews()
// Align backgroundPicture to bounds of superview
// You can remove this code and implement
// your own alignment with frames or Autolayout
backgroundPicture.frame = view.bounds
}
}

Load a nib file to act as a custom keyboard when a UITextField is tapped

I'm trying to load a .xib file to act as a custom keyboard when a textField is tapped. I'm able to show the .xib file (view) when the textField is tapped but I'm not sure how to communicate the buttons in the .xib file with the a textField in the ViewController.swift.
This is what I have done so far.
Created a single project.
Added a UITextField and created an outlet for it.
#IBOutlet weak var myField: MyTextField!
Created a new .xib file and called it CustomView.xib.
Created a new class and called it CustomView.swift.
Assigned class CustomView.swift to the CustomView.xib file.
Added some UIButtons in the CustomView.xib file. These will be acting as a custom-keyboard, they will show when the textField is tapped and hide when the resignFirstResponder method is called.
In the viewDidLoad method of the ViewController.swift I assigned inputView as follow.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myField: MyTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let myView = NSBundle.mainBundle().loadNibNamed("CustomView", owner: self, options: nil).first as? CustomView
myField.inputView = myView
}
}
Done
When I run the app and tap on the textField the .xib file shows (see picture below), my problem is, how do I communicate the buttons with the textField in the ViewController.swif. In other words what I want is to show the numbers in the inputField when they are tapped.
Any suggestions? Is this how this is usually done.
Here is an image that shows the view shown when the inputField was tapped.
Let's say you have 9 UIButtons, then consider ctrl + dragging an IBAction from all these buttons in storyboard to a single method such as:
- (IBAction)tapped:(id)sender
{
textView.text = [NSString stringWithFormat:#"%#%li", textView.text, (long)((UIButton*)sender).tag];
NSLog(#"%#", textView.text);
}
given that textField is your text field, you could then append each corresponding number from your keypad (that is to say, the buttons) using the tag property of these buttons and the aforementioned method.
You could set the tag number for each single button in storyboard (I.e., 1 - 9, respectively).
I didn't test it using 9 buttons but did with only 2, with tag numbers 1 and 2, respectively for each buttons. The result was displaying fine in UITextField (I.e., 1, 12, 122 and so forth).
Update (Post-comment):
I was able to re-create this using a nib file containing a few buttons and a UITextField in storyboard.
The process proceeds as follows:
1. Create a nib with all the buttons (which you already have done).
2. Create a view; and under "Custom Class", re-name the class to this view (I.e., "Custom Class" -> "Class" -> "view-that-holds-the-buttons").
3. Wire the IBActions (a total of 9 corresponding to the number of your buttons) to one single method as described above.
4. In your other view controller whose view hold the UITextField, load the nib using your existing method.
5. Add this view (from nib) as subview.
The following concerns with communication between the view (that holds the buttons along with the IBAction method) and the controller in which you load the nib:
6. create a delegate (weak) property.
7. Before you add the view (from nib), assign this delegate with the view controller (the control that loads the nib).
8. Create a protocol:
For instance:
protocol keypadProtocol : class
{
func displayKeys(keystrokeObject: AnyObject)
}
Have the view controller that loads the nib conform to this protocol and implement the required method (displayKeys):
//The one that loads the nib, which also holds the UITextField
class mainViewController: UIViewController, keypadProtocol
So, once the buttons are tapped, the IBAction would be called; but instead of displaying it, we send the sender to our delegate, which is the view controller that implements the displayKeys method and holds the UITextField.
The IBAction would be implemented as follows:
#IBAction func tapped(sender: AnyObject)
{
delegate!.displayKeys(sender)
}
displayKeys would be implemented like the following:
func displayKeys(keystrokeObject: AnyObject)
{
textView.text = NSString(format: "%#%li", textView.text! ?? "", (keystrokeObject as! UIButton).tag) as String
}
Declaration of the delegate in the controller where you load the nib file:
weak var delegate : keypadProtocol?
Assigning the delegate from within the view controller where you load the nib:
keyPadView.delegate = self //keyPadView is the nib file loaded
In reply to your second comment:
Assumption:
We have 2 classes.
The first one is a subclass of UIView, which is the xib, that holds the buttons.
Let’s call this „KeypadView“.
The second one is the main view controller, which is associated to the controller that holds
the UITextField in your storyboard.
Let’s call this „MainViewController“.
Step 2:
Firstly, please create a new UIView and name it, for the sake of consistency, „KeypadView“.
Then, click on your .xib file; on the right panel, click on the third tab from the left, which is called „Identity Inspector“; you would see „Custom Class -> Class“, where you would associate this xib to the class you created (you need this class, in order to connect the IBAction for the buttons from the xib file to it). It would be the „KeypadView“, which is a subclass of UIView.
Step 6:
You declare this in the class („KeypadView“) that holds the buttons.
Step 8:
You connect this method (IBAction) to the aforementioned class (I.e., „KeypadView“).
Once you load the xib („KeypadView“) from within the „mainViewController“, set the delegate in „KeypadView“ to self (self is the „MainViewController“):
let keyPadView = NSBundle.mainBundle().loadNibNamed("CustomView", owner: self, options: nil).first as? KeyPadView
keyPadView.delegate = self
self.view.addSubview(keypadView)
//you may need to re-adjust the position of the views; I will leave that to you
In your „KeyPadView“ class, there should be an IBAction that gets called from each of the buttons:
I.e.,
#IBAction func tapped(sender: AnyObject)
{
delegate!.displayKeys(sender)
}
Our delegate is the „mainViewController“, if you recall.
Since displayKeys is implemented in „mainViewController“, the following method would be called therein:
func displayKeys(keystrokeObject: AnyObject)
{
textView.text = NSString(format: "%#%li", textView.text! ?? "", (keystrokeObject as! UIButton).tag) as String
}
„mainViewController“ would then display the keystrokes in its UITextField (I.e., textView).

Short lag when setting label text using storyboards

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.

Swift - Access IBOutlet in other class

I have a UIView with a TableView and a Button (Big Button). The TableView has a custom Cell. In this cell there is an "Add" button. I want to animate the first button when the user makes click on the Add button.
This is my schema:
This is my code:
class ProductsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
#IBOutlet var bigButton: UIButton! <- I WANT TO ANIMATE THAT BUTTON
}
ProductCell class
class ProductCell: UITableViewCell {
#IBAction func addProduct(sender: AnyObject) {
//I WANT TO ACCESS THE BIG BUTTON FROM HERE
}
}
Screen example of my app
I've tried to get the parent controller or the superview to get the IBOutlet but the app is crashing allways
Add block properties to your cells which lets them notify your view controller when they have been clicked. In your view controller block code, you can then access the big button.
See my answer to a similar question. Simply replace the switch example with your button. So replace UISwitch with UIButton.
How can I get index path of cell on switch change event in section based table view
So rather than have the cell try and talk to another cell/button, have the cell notify the controller which can then manage the big button changes.
Although I made a comment about using alternate methods you could also employ a strategy below based on updates to a property stored in the current view controller class. You could just as well use property observation on the ProductsViewController but I assume you'd like to keep OOP focused and reduce the size of your controller.
Subclass the ViewController
One could subclass an existing UIViewController and then create a property in the super class that deals with the value that was changed (row tapped). In that subclass you could then do some animation. Because you would be subclassing you continue to obtain all the benefits and methods defined in your existing controller. In your identity inspector point your Class to the new subclass and create any functional updates to your UI using animation.
class ProductsViewController:... {
var inheritedProperty:UIView = targetView {
willSet {newValue } // is the newValue
didSet {oldValue} //is the old value
}
}
class AnimatedProductsViewController:ProductsViewController {
override var inheritedProperty:UIView {
//do something interesting if the property of super class changed
willSet {newValue } // is the newValue
didSet {oldValue} //is the old value
//you might want to call this method like so
// didSet { animate(newValue) }
}
func animate (view: UIView){
//do animation routine using UIView animation, UIDynamics, etc.
}
}
Property Observation
Whenever the didSelectCell... method is called just set a value to the inheritedProperty. Then add the property observers (see sample code) and react when the property changes (maybe pass a reference to the view you want to animate).
For example: Within the property observer you can just take that view and pass it to your animator function (whatever is going to do the animation). There are many examples on SO of how to animate a view so just search for (UIView animation, UIDynamics, etc).
The normal benefits of separation are encapsulation of functionality and reuse but Swift also guarantees that each set of property observers will fire independently. You'd have to give some more thought to this as to its applicability in this use case.
Do all this things in your viewController
Add target Method to cell's add button in cellForRowAtIndexPath Method
Like This
cell.add.addTarget(self, action: "addProduct:", forControlEvents: UIControlEvents.TouchUpInside)
Define method
func addProduct(button:UIButton)
{
// do button animation here
}

Resources