Handling Errors with intent parameters and Dynamic options with Siri - swift

I've an intent parameter set as dynamic from the intent definition.
Let's say that the server where I get information for this option is currently down.
It is not clear how to present to users the fact that the options at the moment cannot be retrieved. The completion field where we should return the options also accepts an Error.
I've filled it with a subclass of Error and I've also implemented the LocalizedError protocol for this class... but when I encounter the error from the shortcut App, Apple is just presenting a pop up message that returns a terrible message not localized (but that includes the right Error name).
Here is the code that I'm using...
func provideCarModelOptions(for intent: CarIntent, with completion: #escaping ([String]?, Error?) -> Void) {
if(somethingGoesWrongWithServers()){
completion([],CarError.ServerDown)
}else{
completion(ReturnListOfModels(), nil)
}
}
And this is how I've implementend the CarError enum
public enum CarError:Error{
case serverDown
case generic
}
extension CarError : LocalizedError{
public var errorDescription: String? {
switch self {
case .serverDown:
return "Server is down"
case .generic:
return "SomethingGoesWrong"
}
}
}
Am I doing anything wrong or Apple is not handling the Errors the right way?

This worked for me to provide localized description:
completion(nil, INIntentError.init(_nsError: NSError(domain: "com.Domain.error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Error Message"])))

Related

XCTest for when iCloud is enabled and disabled

I have a viewMode that determines if iCloud is enabled or disabled with the result being a prompt to the User to login to iCloud or not.
Is there a way to progamatically login/logout to iCloud from an XCTest to reliable test all paths?
Here is my test
func testShowLoginButtonForiCloud() {
let viewModel = OnboardingViewModel()
let expectation = XCTestExpectation(description: "Wait for CKContainer auth check")
var iCloudEnabled: Bool?
viewModel.shouldShowiCloudLogin { result, error in
iCloudEnabled = result
expectation.fulfill()
}
wait(for: [expectation], timeout: 5.0)
XCTAssertNotNil(iCloudEnabled)
XCTAssertFalse(iCloudEnabled!)
}
Here is my ViewModel
typealias Completion = (Bool, Error?) -> Void
final class OnboardingViewModel {
func shouldShowiCloudLogin(completion: #escaping Completion) {
CKContainer.default().accountStatus { (status, error) in
switch status {
case .available :
completion(true, nil)
default :
completion(false, error)
}
}
}
}
Can we programmatically log in to CloudKit for unit testing? This is inadvisable, because even if we could, the tests would be slow & fragile. Instead, treat CloudKit as an architectural boundary. Unit tests can go right up to this boundary. And we can pretend stuff comes back from the boundary. In this way, we can test all paths.
To program this boundary into your code, use a protocol. This protocol will be a slice containing only those CKContainer methods you want. (This is the Interface Segregation Principle in action.) Since CKContainer already implements this method, we can attach it as an empty extension.
protocol CKContainerProtocol {
func accountStatus(completionHandler: #escaping (CKAccountStatus, Error?) -> Void)
}
extension CKContainer: CKContainerProtocol {}
Then add a property to your view model:
var cloudKitContainer: CKContainerProtocol = CKContainer.default()
The default value means your code will continue to use the real CKContainer unless told otherwise. Change your code to call cloudKitContainer instead of CKContainer.default().
Then in test code, you can provide a different implementation of CKContainerProtocol. This will let you do stubbing and mocking. You can confirm that accountStatus() is called exactly once. And you can exercise its closure with different CKAccountStatus values to confirm how your Completion closure is called.

Customizing sandboxed NSSavePanel alert

I am validating urls from NSSavePanel using the delegate's panel(_:validate) method, throwing error in case of invalid url. In such case the NSSavePanel presents an alert, which I want to customize (meaning present some human readable description) depending on the error thrown, keeping the save panel window open and then letting you choose another path.
LocalizedError works just fine when not using App Sandbox but in a sandboxed app the getter for error description is never called and the message in the alert is generic "Operation couldn't be completed. (#yourErrorType)", which I guess is somehow caused by the different inheritance chain for sandboxed NSSavePanels.
I am struggling figuring a way around this - is it possible to customize the alert somehow while still keeping the app sandboxed?
Addendum: Permissions for User Selected File => r/w. Running the following example produces different alerts with/without sandbox.
func runSavePanel()
{
let panel = NSSavePanel()
let delegate = SavePanelDelegate()
panel.delegate = delegate
_ = panel.runModal()
}
class SavePanelDelegate: NSObject, NSOpenSavePanelDelegate {
func panel(_ sender: Any, validate url: URL) throws {
throw CustomError.whatever
}
}
enum CustomError: LocalizedError {
case whatever
var errorDescription: String? {
get {
return "my description"
}
}
}
So, after a bit of further digging I can tell the solution of the riddle finally although I can only guess the reasons why it was made tricky by Apple. Apparently NSError exclusively needs to be used. The customization has to be done in userInfo, say
let userInfo = [NSLocalizedDescriptionKey: "yourLocalizedDescription", NSLocalizedRecoverySuggestionErrorKey: "yourSuggestion"]
throw NSError(domain: "whatever", code: 0, userInfo: userInfo)
etc. By the way subclassing NSError doesn't work, the Sandbox will just happily ignore you :)

Cannot convert value of type '(AccountViewController) -> () -> (AccountViewController)'

Fairly new to Swift so please be easy on me. Working with an inherited codebase.
Getting this error:
Cannot convert value of type '(AccountViewController) -> () -> (AccountViewController)' to expected argument type 'GetUserDelegate?'
This code is in my view controller. This is the only code out of this example that I wrote:
fileprivate var userDataSource = getAPI().getUser(delegate: self)
This code is part of my API definition:
func getUser(delegate: GetUserDelegate?) {
sessionManager.request(ApiRequests.user).validate().responseJSON { (response: Alamofire.DataResponse<Any>) -> Void in
switch response.result {
case .success(let value):
guard let user = Mapper<User>().map(JSONObject: value) else {
delegate?.apiError(code: nil, message: "Cannot decode user")
return
}
delegate?.getUserSuccess(user: user)
case .failure(let err):
delegate?.apiError(code: nil, message: err.localizedDescription)
}
}
}
And here is the protocol:
protocol GetUserDelegate: APIErrorDelegate {
func getUserSuccess(user: User)
}
Now elsewhere in the code I'm seeing similar functions where all they pass in is delegate: self, but this doesn't appear to work and gives me the above error.
My guess is that this is because I am setting this in the class definition directly, rather than in one of the class methods - am I on the right track here? I've done a decent chunk of OOP coding before, but I've never used a delegate design pattern, so I'm not totally understanding implementation here I think.
I guess the error is clear, you should implement such GetUserDelegate inside of your AccountViewController, something like:
class AccountViewController: UIViewController, GetUserDelegate {
func getUserSuccess(user: User) {
// stuff here //
}
}

Swift 3 custom URLProtocol crashes when converting Error to NSError

I've got a rather large body of Swift 3 code for Mac OS 10.11 and up (using Xcode 8.2.1). There are a number of processes, among them a GUI application and a background service. Both of these use a custom URLProtocol, which is implemented in a framework (imported by both application and service). The protocol sometimes may generate instances of an enum that conforms to Error, which it catches and handles appropriately (generally by using the URLProtocolClient to toss them up to the URLSession trying to make the request).
When there's no error, both the app and the service work fine.
When there is an error, the app works fine (well, as expected).
When there is an error, the service crashes.
Wandering through the debugger has shown that this crash is occurring when the Error is automatically converted into an NSError by the runtime. I added this cast explicitly in my code, and sure enough, I get the same crash, now on that line.
I saw Swift 3.1: Crash when custom error is converted to NSError to access its domain property, but it doesn't apply here:
The solution there - extending the Error to a CustomNSError (and LocalizedError for good measure) and implementing the relevant properties - didn't help.
The crash occurs after the domain has been obtained; as far as I can tell that was not a problem for me.
Possibly relevant: that was on iOS, not Mac.
Having already tried the only listed solution to that question, I'm at something of a loss. I've been debugging this for hours without getting anywhere except that it seems to happen somewhere deep in the guts of dyld.
Code:
enum CrowbarProtocolError: Error {
case FailedToCreateSocket;
case PathTooLong;
case NonSocketFile;
case SocketNotFound;
...
case UnrecognizedError(errno: Int32);
}
extension CrowbarProtocolError: LocalizedError {
public var errorDescription: String? {
return "Some localized description"
}
}
extension CrowbarProtocolError: CustomNSError {
public static var errorDomain: String {
return "Some Domain Name"
}
public var errorCode: Int {
return 204 //Should be your custom error code.
}
public var errorUserInfo: [String: Any] {
return ["WTF": "Swift?"];
}
}
...
open class CrowbarUrlProtocol: URLProtocol {
...
override open func startLoading() {
do {
let sockHandle = try CrowbarUrlProtocol.openSocket();
let req = try buildRequestData();
sockHandle.write(req);
NotificationCenter.default.addObserver(
self,
selector: #selector(self.readCompleted),
name: .NSFileHandleReadToEndOfFileCompletion,
object: nil);
sockHandle.readToEndOfFileInBackgroundAndNotify();
} catch let err {
Log.warn("CrowbarUrlProtocol request failed with error \(err)");
// -- In the background service, THIS LINE CRASHES! --
let err2 = err as NSError;
Log.warn("As NSError: \(err2)");
// -- Without the explicit cast, it crashes on this line --
self.client?.urlProtocol(self, didFailWithError: err);
}
}
...
}
One idea I have for solving this is just doing everything (or, as much as possible) using NSErrors, on the grounds that if there's never a need to convert an Error to an NSError, then whatever is causing this crash won't happen. No idea if it'll work but it seems worth a try...
OK, as far as I can tell this is just a bug in Swift's runtime, but I found a work-around: just use NSError for everything involving Obj-C code rather than Swift Errors (to avoid the implicit cast). Since I already was implementing CustomNSError, it was easy to just create a toNSError() function on my Error enum, and use that for the self.client?.urlProtocol(self, didFailWithError: err) lines.
enum CrowbarProtocolError: Error, CustomNSError {
case FailedToCreateSocket;
case PathTooLong;
...
public func asNSError() -> NSError {
return NSError(domain: CrowbarProtocolError.errorDomain,
code: self.errorCode,
userInfo: self.errorUserInfo);
}
}
...
open class CrowbarUrlProtocol: URLProtocol {
...
override open func startLoading() {
do {
let sockHandle = try CrowbarUrlProtocol.openSocket();
let req = try buildRequestData();
sockHandle.write(req);
NotificationCenter.default.addObserver(
self,
selector: #selector(self.readCompleted),
name: .NSFileHandleReadToEndOfFileCompletion,
object: nil);
sockHandle.readToEndOfFileInBackgroundAndNotify();
} catch let err as CrowbarProtocolError {
Log.warn("CrowbarUrlProtocol request failed with error \(err)");
self.client?.urlProtocol(self, didFailWithError: err.asNSError());
}
catch let err {
Log.warn("CrowbarUrlProtocol caught non-CrowbarProtocol Error \(err)");
// This would probably crash the service, but shouldn't ever happen
self.client?.urlProtocol(self, didFailWithError: err);
}
}
...
}

generically injecting a side effect in a SignalProducer

My app has a status area at the top that shows progress information (similar to Xcode and iTunes). I want to update it by injecting side effects into an event stream, using a closure that converts the stream's value into the ProgressUpdate value. I'm using an extension on SignalProducer so any signal producer in my app can update the app's status area (there is a lot more involved to allow for multiple signals at once, but that doesn't affect this problem).
I'm basing it on SignalProducer's on(starting:, started:, ...). It requires the latest swift 3.1 beta to allow the constraint on the error type, but this is straight from a playground.
import ReactiveSwift
struct Rc2Error: Error {
}
struct ProgressUpdate {
let message: String
let value: Double = -1
}
class MacAppStatus {
fileprivate func process(event: (Event<ProgressUpdate, Rc2Error>) -> Void)
{
//update UI based on the event
}
}
extension SignalProducer where Error == Rc2Error {
func updateProgress<Value>(status: MacAppStatus, converter: #escaping (Value) -> ProgressUpdate) -> SignalProducer<Value, Error>
{
return SignalProducer<Value, Error> { observer, compositeDisposable in
self.startWithSignal { signal, disposable in
compositeDisposable += disposable
compositeDisposable += signal
.on(event: { (orignal) in
switch original {
case .completed:
status.process(Event<ProgressUpdate, Rc2Error>.completed)
case .interrupted:
status.process(Event<ProgressUpdate, Rc2Error>.interrupted)
case .failed(let err):
status.process(Event<ProgressUpdate, Rc2Error>.failed(err))
case .value(let val):
status.process(Event<ProgressUpdate, Rc2Error>.value(converter(val)))
}
})
.observe(observer)
}
}
}
}
```
The last line of .observe(observer) produces an error:
error: cannot convert value of type 'Observer<Value, Rc2Error>' to expected argument type 'Observer<_, Rc2Error>'
Any ideas why this conversion fails? Suggestions on a different way to accomplish this?
It looks like it was just bad error reporting from the compiler. The actual problem was that process() should take an Event, not a closure that takes an event. It also needed an empty external parameter name.
Changing the signature to
fileprivate func process(_ event: Event<ProgressUpdate, Rc2Error>)
and fixing the original typo Mike Taverne pointed out fixed it.