Catch inconsistent application state with Swift's `guard` statement? - swift

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.

Related

Trigger a segue from a function (without UI element)

I'm very new with programming. Currently, I need to trigger a segue directly after a function is being executed.
This is my code:
func onlineSearch() {
let urlToGoogle = "https://www.google.com/search?q=\(resultingText)"
let urlString = urlToGoogle.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
url = URL(string: urlString!)
performSegue(withIdentifier: K.goToBrowser, sender: nil)
}
}
When I run this, I get this error:
Warning: Attempt to present <MyApp.SFSafariViewController: 0x10153be70> on <MyApp.CameraViewController: 0x101708460> whose view is not in the window hierarchy!
But if I run all the same, but simply trigger the segue from a button, instead of the way I want, it actually works perfectly.
#IBAction func webViewButtonPressed(_ sender: UIButton) { // TEMP
performSegue(withIdentifier: K.goToBrowser, sender: self)
}
P.S.: Nevermind the grotesque unwrapping, it is just working as a placeholder right now.
Reposting my comment as an answer with a code snippet -
If the caller of onlinesearch() may be on anything other than the main thread, you need to wrap the performSegue call in a DispatchQueue.main.async{} block.
DispatchQueue.main.async {
performSegue(withIdentifier: K.goToBrowser, sender: nil)
}
Generally, methods which affect UI (updating controls, performing segues, etc.) need to be on the main thread. Calls from UIKit are already on the main thread (viewDidLoad, draw, events, notifications, timers etc.), but other task completion handlers (and anything run on explicitly on a different queue) may not be.
try using this to open the safari URL
if let url = URL(string: "https://www.google.com/search?q=\(resultingText)") {
UIApplication.shared.open(url)
}
Here's a function if you want to use it anywhere else.
But you should mark Chris Comas answer as the correct one.
func openURL(url: URL) {
UIApplication.shared.open(url, options: [:])
}
}
Just for the sake of knowledge:
let urlGoogle = URL(string: "https://www.google.com")
openURL(url: urlGoogle)
if you want to open the browser inside the app check Swift documentation: https://developer.apple.com/documentation/uikit/uiwebview

Warnings from String interpolation

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)")

Can you both find a first instance of a type in a collection, returning that instance as that concrete type?

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.

How can I check the currentTitle of a UIButton in a switch?

I have a UIButton which changes the title in different situations.
If you press the button the currentTitle should be checked by a Swift Switch (and certain Code executed).
The following isn't working:
#IBAction func button(_ sender: UIButton) {
switch sender.currentTitle {
case "":
//Code
case "OK":
//Code
default:
}
}
Xcode just shows "Expected Pattern" (1st cause) and "Expected Expression" (2nd case)
per the documentation, UIButton.currentTitle is of type String?. (optional type String)
Your switch statement is comparing an optional type to a non-optional type which is the reason you see errors.
Suggest first unwrapping the value before checking it's value with a switch like:
guard let title = sender.currentTitle() else { return }
switch title {
...
}
Every case in a switch statement must have a line of code. If you don't want to do anything you can use break:
#IBAction func button(_ sender: UIButton) {
switch sender.currentTitle {
case "":
//Code
case "OK":
//Code
default:
break
}
}
I solved the problem accidentally by adding questions marks
#IBAction func button(_ sender: UIButton) {
switch sender.currentTitle {
case ""?:
//Code
case "OK"?:
//Code
default:
break
}
}
I don't know why but it works nevertheless thanks for helping me.

Update Swift NSTextfield variable when it's changed

How can I write this so that it updates the variable when the user finishes using the field (for Cocoa?). The aim is to allow the user to specify a custom IP address for the TV's location on the network.
import Cocoa
import Alamofire
class ViewController: NSViewController, NSTextFieldDelegate {
#IBAction func MenuButton(_ sender: NSButtonCell) {
triggerRemoteControl(irccc: "AAAAAQAAAAEAAABgAw==")
}
#IBAction func ReturnButton(_ sender: NSButton) {
triggerRemoteControl(irccc: "AAAAAgAAAJcAAAAjAw==")
}
…
#IBOutlet var IPField: NSTextField! // [A] Set by the user
…
func triggerRemoteControl(irccc: String) {
Alamofire.request(IPField, // [B] Goes here when it's updated.
method: .post,
parameters: ["parameter" : "value"],
encoding: SOAPEncoding(service: "urn:schemas-sony-com:service:IRCC:1",
action: "X_SendIRCC", IRCCC: irccc)).responseString { response in
print(response)
}
}
}
— UPDATE
I tried declaring a variable:
var IPString: String
and then (I set the textField's delegate to ViewController, and placed this function inside):
override func controlTextDidEndEditing(_ obj: Notification){
let IPString = IPField.stringValue
}
Even using the "-> String" and return notation still has it complaining about unused variables. I obviously don't know my Syntax well enough.
Complier also complains about not the ViewController not being initialised.
What you need is to override the func controlTextDidEndEditing(_ obj: Notification) function
You should take a look at:
object (property of obj) - sometimes you would like to know which object sent you the end editing action.
userInfo (property of obj) - contains a "NSTextMovement" key, which allows you to define how the user did end the editing.
override func controlTextDidEndEditing(_ obj: Notification){
let IPString = IPField.stringValue
}
Here, you're creating new constant. What you want is to set this value into your class variable, so you should make IPString = IPField.stringValue
But it's not quite correct, because func controlTextDidEndEditing(_ obj: Notification) could be called from other objects, so first you should check if obj notification contain object which send it with guard, for example.
guard let object = obj.object else {
return
}
Then check if object is your IPField with identity operators
guard object === IPField else {
return
}
And finally you can assign your field value to your IPString var
IPString = object.stringValue
Hope it will help you. Ohh and one advice from my side, you should use lower camel case naming convention for you variables.