I've encountered a problem I can't solve myself. I have tried the Internet without any luck.
I'm still pretty new to Swift and coding, and right now following a guide helping me create an app.
Unfortunately, as I can understand, the app was written for Swift 3, and is giving me some issues since I'm using Swift 4.
I have to lines that gives me this warning:
String interpolation produces a debug description for an optional value; did you mean to make this explicit?
Use 'String(describing:)' to silence this warning Fix
Provide a default value to avoid this warning Fix
However, when I click one of Xcode's solutions I get another problem.
If I use the first fix, the app crashes and I get the following message:
Thread 1: Fatal error: Unexpected Segue Identifier;
If I use the second fix I have to assign a default value. And I don't know what this should be.
The whole passage of code is as follows.
It's the line starting with guard let selectedMealCell and the last one after default: that is causing the issues.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
switch(segue.identifier ?? "") {
case "AddItem":
os_log("Adding a new meal.", log: OSLog.default, type: .debug)
case "ShowDetail":
guard let mealDetailViewController = segue.destination as? MealViewController else {
fatalError("Unexpected destination: \(segue.destination)")
}
guard let selectedMealCell = sender as? MealTableViewCell else {
fatalError("Unexpected sender: \(sender)")
}
guard let indexPath = tableView.indexPath(for: selectedMealCell) else {
fatalError("The selected cell is not being displayed by the table")
}
let selectedMeal = meals[indexPath.row]
mealDetailViewController.meal = selectedMeal
default:
fatalError("Unexpected Segue Identifier; \(segue.identifier)")
}
}
So, the first suggested fix worked for you. It quieted the compile time warning, although admittedly String(describing:) is a weak solution.
In both cases, you need to unwrap the optional value. For the first case you should use:
guard let selectedMealCell = sender as? MealTableViewCell else {
if let sender = sender {
fatalError("Unexpected sender: \(sender))")
} else {
fatalError("sender is nil")
}
}
and in the second case:
fatalError("Unexpected Segue Identifier; \(segue.identifier ?? "")")
Then you got a runtime error:
"Unexpected Segue Identifier;"
That is telling you that your switch didn't match the first 2 cases and it ran the default case. The crash is caused because your code is explicitly calling fatalError. Your segue.identifier is apparently an empty string.
So your problem is actually in your Storyboard. You need to assign identifiers to your segues. Click on the segue arrows between your view controllers, and assign identifiers "AddItem" and "ShowDetail" to the proper segues. The segue identifier is assigned in the Attributes Inspector on the right in Xcode.
If you are prepared to write an small extension to Optional, it can make the business of inserting the value of an optional variable less painful, and avoid having to write optionalVar ?? "" repeatedly:
Given:
extension Optional: CustomStringConvertible {
public var description: String {
switch self {
case .some(let wrappedValue):
return "\(wrappedValue)"
default:
return "<nil>"
}
}
}
Then you can write:
var optionalWithValue: String? = "Maybe"
var optionalWithoutValue: String?
print("optionalWithValue is \(optionalWithValue.description)")
print("optionalWithoutValue is \(optionalWithoutValue.description)")
which gives:
optionalWithValue is Maybe
optionalWithoutValue is <nil>
You can also write print("value is \(anOptionalVariable)") -- the .description is redundant since print() uses CustomStringConvertible.description anyway -- but although it works you still get the annoying compiler warning.
You can use the following to automatically produce "nil" (or any other String) for nil values and for non-nil values use the description provided by CustomStringConvertible
extension String.StringInterpolation {
mutating func appendInterpolation<T: CustomStringConvertible>(_ value: T?) {
appendInterpolation(value ?? "nil" as CustomStringConvertible)
}
}
For your own types you have to conform to CustomStringConvertible for this to work:
class MyClass: CustomStringConvertible {
var description: String {
return "Whatever you want to print when you use MyClass in a string"
}
}
With this set up, you can simply use your optionals the same way as any other type, without any compiler warnings.
var myClass: MyClass?
myClass = MyClass()
print("myClass is \(myClass)")
Related
I've a function in Swift like so:
#IBAction func doSomething(_ sender: AnyObject) { }
I need the sender to be of type NSMenuItem, so I check it with a guard:
guard let menuItem = sender as? NSMenuItem else { return }
But this will silently let the application continue if there is some serious error in my application logic, resulting in a different object type being passed.
Wouldn't it be better to just crash the application, rather than 'presenting' the user with a mysteriously non-working function?
What is the best way to check and react for these super-basic assumptions?
Note that you can write your #IBActions like this:
#IBAction func doSomething(_ sender: NSMenuItem)
sender does not have to be AnyObject.
In other situations though, if you want to crash with a guard statement, you could do:
guard ... else { fatalError("a message") }
fatalError returns Never, so it can be used in the else clause.
I am trying to tap on a UITableView to get to a new detail view. My original code was based on a tutorial that created a data model to store the values in, which contains a class called Bonus.
In my main UITableView, I had the following for my prepare(for: sender):
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
switch (segue.identifier ?? "") {
case "ShowDetail":
guard let bonusDetailViewController = segue.destination as? BonusSubmissionViewController else {
fatalError("Unexpected destination: \(segue.destination)")
}
guard let selectedBonusCell = sender as? BonusTableViewCell else {
fatalError("Unexpected sender: \(sender!)")
}
guard let indexPath = tableView.indexPath(for: selectedBonusCell) else {
fatalError("The selected cell is not being displayed by the table")
}
let selectedBonus = bonuses[(indexPath.row)]
bonusDetailViewController.bonus = selectedBonus
case "ShowSettings":
os_log("Going to settings.", log: OSLog.default, type: .debug)
default:
fatalError("Unexpected Segue Indentifier; \(segue.identifier!)")
}
}
The above code worked when I was manually loading the bonus data. Now that I am using a JSON file to populate the UITableView (and to get all the source data from), I changed the let selectedBonus to use my new jBonuses[(indexPath.row)] which is how my UITableView is now populated (using a JSON file). When I try to build this, I get an error stating:
Cannot assign value of type 'BonusTableViewController.Bonuses' to type
'Bonus?'
on the code line that says bonusDetailViewController.bonus = selectedBonus
I don't think I need to change the Bonus.swift data model, since the labels and structs weren't changed, but I can't figure out what this error is trying to tell me to do, especially since my UITableView does load correctly and looks the same as it did before I was using the JSON based method.
I hope that makes sense.
Please read the error message carefully and learn to understand it.
Cannot assign value of type 'BonusTableViewController.Bonuses' to type 'Bonus?'
in the line bonusDetailViewController.bonus = selectedBonus means
The type of the left side bonusDetailViewController.bonus is Bonus?
The type of the right side selectedBonus is Bonuses.
so bonusDetailViewController.bonus has to be declared as Bonuses?. In your previous question you were using both types bonus and bonuses. Please consolidate your code to use only one them. Comment out the type you want to drop.
This code bothers me. Below, I'm trying to find the first instance of a specific type of ViewController in a NavigationController's stack. Simple. But when I've found it, I have to then cast it to the type I just looked for, which seems redundant to me.
func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {
guard let foundViewController = viewControllers.first(where: { $0 is T }) as? T else {
return nil
}
self.popToViewController(foundViewController, animated:animated)
return foundViewController
}
Only thing I can think of is this...
func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {
guard let foundViewController = viewControllers.flatMap({ $0 as? T }).first() else {
return nil
}
self.popToViewController(foundViewController, animated:animated)
return foundViewController
}
...but I've repeatedly found using flatMap like this tends to confuse people reading the code, and, as correctly pointed out in the comments below, iterates over the entire collection whereas first doesn't do that.
So is there another way to solve this issue?
You can use case patterns to select the viewControllers of the type you are interested in and pop and return the first one you find:
extension UINavigationController {
func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {
for case let vc as T in viewControllers {
self.popToViewController(vc, animated: animated)
return vc
}
return nil
}
}
Example:
Use a button in OrangeViewController to return to GreenViewController earlier in the stack:
#IBAction func popToGreen(_ sender: UIButton) {
let greenVC = self.navigationController?.popToFirstViewController(
ofType: GreenViewController.self,
animated: true
)
// Modify a property in GreenViewController that
// will be moved into a label in viewWillAppear
greenVC?.labelText = "Returned here from Orange"
}
popToLastViewController(ofType:animated:)
You might also want a function to pop to the most recent viewController of a type. That is easily achieved with a simple modification (adding .reversed()):
func popToLastViewController<T:UIViewController>(ofType type:T.Type, animated: Bool) -> T? {
for case let vc as T in viewControllers.reversed() {
self.popToViewController(vc, animated: animated)
return vc
}
return nil
}
I'm in favor of combining flatMap and lazy to get the behavior of conditionally casting to T, stripping out mismatches, and not enumerating the whole array:
func popToFirstViewController<T:UIViewController>(ofType type:T.Type, animated:Bool) -> T? {
guard let foundViewController = viewControllers.lazy.flatMap({ $0 as? T }).first {
return nil
}
self.popToViewController(foundViewController, animated:animated)
return foundViewController
}
As for "confusing people that read the code:" flatMap is fairly idiomatic Swift, and will be less ambiguous with the upcoming rename to compactMap. If readers in your environment really have trouble, you could always write a small helper (generic or not) that performs the same work under a clearer name.
I am trying to dyanmically cast to a class in Swift. Is this possible? Here is the code I am trying to use:
let stringClass: AnyClass = NSString.self
let anyObject: AnyObject = "foo"
let string = anyObject as! stringClass
The code fails to compile at the cast. Is this possible and if so, why is the right syntax?
Real use case
Here is the real issue. I am attempting to refactor this code:
switch (value) {
case "valueOne":
viewController = storyboard.instantiateViewController(withIdentifier: "foo") as! FirstViewController
case "valueTwo":
viewController = storyboard.instantiateViewController(withIdentifier: "bar") as! SecondViewController
default:
return nil
}
into:
let controllersDictionary: [String: (String, UIViewController.Type)] = [
"valueOne" : ("bar", FirstViewController.self),
"valueTwo" : ("foo", SecondViewController.self)
]
let tuple = controllersDictionary[value]!
let identifier = tuple.0
let cast = tuple.1
let viewController = storyboard.instantiateViewController(withIdentifier: identifier) as! cast
I'm not sure exactly what you're trying to achieve, but here's a working version of your example:
func cast<T>(value: Any, to type: T) -> T? {
return castedValue as? T
}
let inputValue: Any = "this is a test"
let inputType = String.self()
let casted = cast(value: inputValue, to: inputType)
print(casted)
I'm not seeing what the cast at this point is for. You can write:
let controllersDictionary: [String: String] = [
"valueOne" : "bar",
"valueTwo" : "foo"
]
let identifier = controllersDictionary[value]!
let viewController = storyboard.instantiateViewController(withIdentifier: identifier)
The cast does nothing for you in the code that you have shown. viewController is typed as UIViewController, but it is the correct underlying view controller subclass thanks to polymorphism; whatever the class is in the storyboard, that's the class of this instance.
The only time you need to cast down is when you have to message an instance with a message belonging only to the subclass, and you have not shown any such need at this point in your code.
While there are/will be ways to make this kind of thing work, the Swifty solution (IMO) is to have your desired classes adhere to a protocol that defines the shared behavior you're trying to use, or simply use a super class they have in common
This allows the dynamism requried (in most cases at least) while still allowing the compile-time checks that prevent run time errors.
For your example,
protocol Stringable {
func toString() -> String
}
extension String: Stringable {
func toString() -> String {
return self
}
}
let thing = "foo"
let anything: Any = thing
let test: String? = (anything as? Stringable)?.toString()
Note that this requires "Any" rather than "AnyObject" since you need to cast to a protocol
Since you mentioned ViewControllers, I thought this might help:
static func createViewController<T: UIViewController>(storyboard: String, scene: String) -> T? {
return UIStoryboard(name: storyboard, bundle: nil).instantiateViewControllerWithIdentifier(scene) as? T
}
The x as! Y.Type syntax only works when Y is explicitly stated.
So, the compiler wants x as! NSString. The compiler crash is a bug, I suggest that you file a report.
And just think about it for a second, how would that even help you? stringClass is of type AnyClass, and you're force casting an AnyObject which already conforms to AnyClass. You should cast when you have a static type in mind, because casting doesn't really make any sense otherwise.
If you want to check, however, whether your object's class is a subclass of a particular type, you'd use:
x is Y.Type
Its possible so long as you can provide "a hint" to the compiler about the type of... T. So in the example below one must use : String?.
func cast<T>(_ value: Any) -> T? {
return value as? T
}
let inputValue: Any = "this is a test"
let casted: String? = cast(inputValue)
print(casted) // Optional("this is a test")
print(type(of: casted)) // Optional<String>
Why Swift doesn't just allow us to let casted = cast<String>(inputValue) I'll never know.
One annoying scenerio is when your func has no return value. Then its not always straightford to provide the necessary "hint". Lets look at this example...
func asyncCast<T>(_ value: Any, completion: (T?) -> Void) {
completion(value as? T)
}
The following client code DOES NOT COMPILE. It gives a "Generic parameter 'T' could not be inferred" error.
let inputValue: Any = "this is a test"
asyncCast(inputValue) { casted in
print(casted)
print(type(of: casted))
}
But you can solve this by providing a "hint" to compiler as follows:
asyncCast(inputValue) { (casted: String?) in
print(casted) // Optional("this is a test")
print(type(of: casted)) // Optional<String>
}
Problem: Upon converting to Swift 2 I get the following error: "Value of optional type [NSIndexPath]? not unwrapped, did you mean to use "!" or "?"". The issue is that if I use '?', it gives an error saying I should use '!', and if I use '!' it gives an error saying I should use '?'. Thus it creates this nasty little bug loop that seems to be unfixable.
Code:
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
if identifier == Constants.SegueIdentifier {
if let selectedRowIndex = collectionView?.indexPathsForSelectedItems().last as? NSIndexPath {
if let cell = collectionView?.cellForItemAtIndexPath(selectedRowIndex) {
//We check if the selected Card is the one in the middle to open the chat. If it's not, we scroll to the side card selected.
if cell.frame.size.height > cell.bounds.size.height {
return true
} else {
collectionView?.scrollToItemAtIndexPath(selectedRowIndex, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
return false
}
}
}
}
return true
}
I haven't been able to come up with any work arounds since it seems like I need to somehow unwrap it. Has anyone seen this problem?
indexPathsForSelectedItems() returns [NSIndexPath]? (optional), you have to add another question mark for optional chaining and remove as? NSIndexPath as the compiler knows the unwrapped type.
if let selectedRowIndex = collectionView?.indexPathsForSelectedItems()?.last {
Solution:
if let selectedRowIndex = collectionView!.indexPathsForSelectedItems()!.last! as? NSIndexPath
My only concern is type safety but it works in my project as it currently is.