How can I do an optional binding in Swift and check for a negative result? Say for example I have an optional view controller that I'd like to lazy load. When it's time to use it, I'd like to check if it's nil, and initialize it if it hasn't been done yet.
I can do something like this:
if let vc = viewController? {
// do something with it
} else {
// initialize it
// do something with it
}
But this is kludgey and inefficient, and requires me to put the "do something with it" code in there twice or bury it in a closure. The obvious way to improve on this from objC experience would be something like this:
if !(let vc = viewController?) {
// initialize it
}
if let vc = viewController? {
// do something with it
}
But this nets you a "Pattern variable binding cannot appear in an expression" error, which is telling me not to put the binding inside the parenthesis and try to evaluate it as an expression, which of course is exactly what I'm trying to do...
Or another way to write that out that actually works is:
if let vc = viewController? {
} else {
// initialize it
}
if let vc = viewController? {
// do something with it
}
But this is... silly... for lack of a better word. There must be a better way!
How can I do an optional binding and check for a negative result as the default? Surely this is a common use case?
you can implicitly cast Optional to boolean value
if !viewController {
viewController = // something
}
let vc = viewController! // you know it must be non-nil
vc.doSomething()
Update: In Xcode6-beta5, Optional no longer conform to LogicValue/BooleanType, you have to check it with nil using == or !=
if viewController == nil {
viewController = // something
}
Would this work for you?
if viewController == nil {
// initialize it
}
// do something with it
One way might be to create a defer statement that handles the actions. We can ensure those actions occur after the creation of our object by checking for nil. If we run into nil, instantiate the object and return. Before returning our deference will occur within the scope of some function.
func recreateAndUse() {
defer {
viewController?.view.addSubview(UIView())
viewController!.beginAppearanceTransition(true, animated: true)
}
guard viewController != nil else {
viewController = UIViewController()
return
}
}
Related
You have a vc (green) and it has a panel (yellow) "holder"
Say you have ten different view controllers...Prices, Sales, Stock, Trucks, Drivers, Palettes, which you are going to put in the yellow area, one at a time. It will dynamically load each VC from storyboard
instantiateViewController(withIdentifier: "PricesID") as! Prices
We will hold the current VC one in current. Here's code that will allow you to "swap between" them...
>>NOTE, THIS IS WRONG. DON'T USE THIS CODE<<
One has to do what Sulthan explains below.
var current: UIViewController? = nil {
willSet {
// recall that the property name ("current") means the "old" one in willSet
if (current != nil) {
current!.willMove(toParentViewController: nil)
current!.view.removeFromSuperview()
current!.removeFromParentViewController()
// "!! point X !!"
}
}
didSet {
// recall that the property name ("current") means the "new" one in didSet
if (current != nil) {
current!.willMove(toParentViewController: self)
holder.addSubview(current!.view)
current!.view.bindEdgesToSuperview()
current!.didMove(toParentViewController: self)
}
}
}
>>>>>>>>IMPORTANT!<<<<<<<<<
Also note, if you do something like this, it is ESSENTIAL to get rid of the yellow view controller when the green page is done. Otherwise current will retain it and the green page will never be released:
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
current = nil
super.dismiss(animated: flag, completion: completion)
}
Continuing, you'd use the current property like this:
func showPrices() {
current = s.instantiateViewController(withIdentifier: "PricesID") as! Prices
}
func showSales() {
current = s.instantiateViewController(withIdentifier: "SalesID") as! Sales
}
But consider this, notice "point X". Normally there you'd be sure to set the view controller you are getting rid of to nil.
blah this, blah that
blah.removeFromParentViewController()
blah = nil
However I (don't think) you can really set current to nil inside the "willSet" code block. And I appreciate it's just about to be set to something (in didSet). But it seems a bit strange. What's missing? Can you even do this sort of thing in a computed property?
Final usable version.....
Using Sulthan's approach, this then works perfectly after considerable testing.
So calling like this
// change yellow area to "Prices"
current = s.instantiateViewController(withIdentifier: "PricesID") as! Prices
// change yellow area to "Stock"
current = s.instantiateViewController(withIdentifier: "StickID") as! Stock
this works well...
var current: UIViewController? = nil { // ESSENTIAL to nil on dismiss
didSet {
guard current != oldValue else { return }
oldValue?.willMove(toParentViewController: nil)
if (current != nil) {
addChildViewController(current!)
holder.addSubview(current!.view)
current!.view.bindEdgesToSuperview()
}
oldValue?.view.removeFromSuperview()
oldValue?.removeFromParentViewController()
if (current != nil) {
current!.didMove(toParentViewController: self)
}
}
// courtesy http://stackoverflow.com/a/41900263/294884
}
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
// ESSENTIAL to nil on dismiss
current = nil
super.dismiss(animated: flag, completion: completion)
}
Let's divide the question into two: (1) Is there a "leak"? and (2) Is this a good idea?
First the "leak". Short answer: no. Even if you don't set current to nil, the view controller it holds obviously doesn't "leak"; when the containing view controller goes out of existence, so does the view controller pointed to by current.
The current view controller does, however, live longer than it needs to. For that reason, this seems a silly thing to do. There is no need for a strong reference current to the child view controller, because it is, after all, your childViewControllers[0] (if you do the child view controller "dance" correctly). You are thus merely duplicating, with your property, what the childViewControllers property already does.
So that brings us to the second question: is what you are doing a good idea? No. I see where you're coming from — you'd like to encapsulate the "dance" for child view controllers. But you are doing the dance incorrectly in any case; you're thus subverting the view controller hierarchy. To encapsulate the "dance", I would say you are much better off doing the dance correctly and supplying functions that perform it, along with a computed read-only property that refers to childViewController[0] if it exists.
Here, I assume we will only ever have one child view controller at a time; I think this does much better the thing you are trying to do:
var current : UIViewController? {
if self.childViewControllers.count > 0 {
return self.childViewControllers[0]
}
return nil
}
func removeChild() {
if let vc = self.current {
vc.willMove(toParentViewController: nil)
vc.view.removeFromSuperview()
vc.removeFromParentViewController()
}
}
func createChild(_ vc:UIViewController) {
self.removeChild() // There Can Be Only One
self.addChildViewController(vc) // *
// ... get vc's view into the interface ...
vc.didMove(toParentViewController: self)
}
I don't think that using didSet is actually wrong. However, the biggest problem is that you are trying to split the code between willSet and didSet because that's not needed at all. You can always use oldValue in didSet:
var current: UIViewController? = nil {
didSet {
guard current != oldValue else {
return
}
oldValue?.willMove(toParentViewController: nil)
if let current = current {
self.addChildViewController(current)
}
//... add current.view to the view hierarchy here...
oldValue?.view.removeFromSuperview()
oldValue?.removeFromParentViewController()
current?.didMove(toParentViewController: self)
}
}
By the way, the order in which the functions are called is important. Therefore I don't advise to split the functionality into remove and add. Otherwise the order of viewDidDisappear and viewDidAppear for both controllers can be surprising.
I am implementing this drop down menu from cocoaPod. It is pretty easy to implement and I got it to work.
https://github.com/PhamBaTho/BTNavigationDropdownMenu
However, as per instruction, I have implemented the following functions in viewDidLoad
self.navigationItem.titleView = menuView
menuView.didSelectItemAtIndexHandler = {[weak self] (indexPath: Int) -> () in
print("Did select item at index: \(indexPath)")
if indexPath == 0 {
print("Closest")
self?.sortByDistance()
} else if indexPath == 1 {
print("Popular")
self?.sortByRatings()
} else if indexPath == 2 {
print("My Posts")
self?.myPosts()
} else {
}
I am abit concerned as Xcode is telling me to put a ? or a ! just after self which was never done in other places of my program. Could someone please advise if this is totally acceptable or is there a better way of doing it? It just seems odd force unwrapping or putting my VC as optional...?
The whole point of {[weak self] ... is that the controller may be released and you don't want this block to strongly capture it and keep it in memory if it has been released by whatever presented it. As such the reference to self may be nil.
So, you definitely don't want to use !, and you should either user ? or and if let check.
When I try to instantiateViewControllerWithIdentifier on an iPhone it is crashing the app, although this works fine on the ios simulator. The code I have used is:
let questionsGameVC: QuestionsGame = self.storyboard?.instantiateViewControllerWithIdentifier("Questions") as! QuestionsGame
The error it is saying is
fatal error: unexpectedly found nil while unwrapping an Optional value
Can someone add anything to what is going wrong?
There are lots of places this could go wrong. I would put a breakpoint at that line and look at the state of the system, but debugging swift is still not in a great state. One alternative is to break apart that line of code and test all the pieces. Something like this:
if let storyboard = self.storyboard {
if let viewController = storyboard.instantiateViewControllerWithIdentifier("Questions") {
if let questionGame = viewController as? QuestionGame {
println("Success!")
} else {
println("Question game is the wrong type in the storyboard")
}
} else {
println("There is no view controller with the identifier Questions")
}
} else {
println("The storyboard is nil")
}
Whatever gets printed at the end should give you a better idea of where the problem is. Most often I have seen misspelled identifiers or situations where the class of the view controller in the storyboard has not been changed from UIViewController to the custom type.
I'm trying to check if the back button on my view controller was pressed but I'm having a hard time detecting this in Swift.
With this code:
if (contains(self.navigationController?.viewControllers, self)) {
println("Back button not pressed")
} else {
self.updateSearchQueryModel()
}
I am getting the error:
Could not find an overload for contains that accepts the supplied arguments.
I did get the result that I wanted in another fashion but I am still confused as to why this error is happening.
Why is this happening? Can I not check if self exists in an array?
Source of original code in Objective C that I couldn't translate to Swift:
Setting action for back button in navigation controller
-(void) viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
// back button was pressed. We know this is true because self is no longer
// in the navigation stack.
}
[super viewWillDisappear:animated];
}
Please do not tell me how to detect that the back button was pressed. I already figured that out here.
Source of solution to objective: Detecting when the 'back' button is pressed on a navbar
If you look at the declaration of the viewControllers property, you notice that it's [AnyObject]! and not [UIViewController]!.
The contains function requires that the sequence element implements the Equatable protocol, which AnyObject doesn't.
The solution is to make an explicit downcast of that array, using optional binding:
if let viewControllers = self.navigationController?.viewControllers as? [UIViewController] {
if (contains(viewControllers, self)) {
println("Back button not pressed")
} else {
self.updateSearchQueryModel()
}
}
I'm new in Swift but try this:
if let controllers = self.navigationController?.viewControllers as? [UIViewController] {
if contains(controllers, self) {
DLog("!")
}
}
You're getting error because you're passing optional as contains()'s first argument
So I have been having fun with default parameter values.
class containerViewController: UIViewController {
var detailView:UIViewController?
override func viewDidLoad(){
super.viewDidLoad()
detailView = anotherViewController()
}
func hideDetailView(vc:UIViewController? = detailView){ // <- THIS LINE
// code
}
}
The line Ive marked produces an error:
'containerViewController.Type' does not have a member named 'detailView'
Ive been reading online, including this question, but I cant seem to figure out how to fix this.
What I want is to be able to use hideDetailView() and if I send in a specific view controller as a parameter to that function, it hides that specific view controller. If I dont send any parameter, it just hides the current view controller that is held in the detailView parameter.
How can I achieve this?
You can use nil for the default value, and check if nil in the body.
func hideDetailView(vc:UIViewController? = nil){ // <- THIS LINE
let vc_ = vc ?? detailView
// code
}
But In this case, you can't distinguish following calls:
// passing `nil` as Optional<UIViewController>
let vc:UIViewController? = nil
container.hideDetailView(vc: vc)
// use default value
container.hideDetailView()
If you don't like that, you can use UIViewController??:
func hideDetailView(vc:UIViewController?? = nil){
let vc_ /*: UIViewController? */ = vc ?? detailView
// code
}