NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, 9813)

遇到Obj-C 存取 self sign ssl 時會顯示 error:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)

解法:

For me your first example is working fine. I have tested with the following code without problems (it is of course very insecure since it allows any server certificate).

@implementation SessionTest

- (void) startSession
{
    NSURL *url = [NSURL URLWithString:@"https://self-signed.server.url"];

    NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

    NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithURL:url
                                                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                                    if(error == nil)
                                                    {
                                                        NSString * text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
                                                        NSLog(@"Data: %@",text);
                                                    }
                                                    else
                                                    {
                                                        NSLog(@"Error: %@", error);
                                                    }
                                                }];

    [dataTask resume];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}

@end

Update: This is the class interface, the SessionTest class is the NSURLSessionDataDelegate, to start the data download you create a SessionTest object and call the startSession method.

@interface SessionTest : NSObject <NSURLSessionDelegate>

- (void) startSession;

@end

或這個範例:

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:Nil];
...
...
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler{
  if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]){
    if([challenge.protectionSpace.host isEqualToString:@"mydomain.com"]){
      NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
      completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
    }
  }
}

Module compiled with Swift 4.0.3 cannot be imported in Swift 4.1

不知何時升到 xcode 9.3, 別人用 xcode 9.2 寫的 library 會無法 build.

I got new error message:

Module compiled with Swift 4.0.3 cannot be imported in Swift 4.1

 

because AirWatch SDK 18.3 not support xcode 9.3, I try to download and install swift toolchain 4.0.3 from URL:

https://swift.org/download/#releases

Switch toolchain to swift 4.0.3:

 

and I got new error message with crash:

dyld: Library not loaded: @rpath/libswiftAVFoundation.dylib
 Referenced from: /private/var/containers/Bun

dle/Application/90D24C8C-3ADD-46C7-B57E-4DA9B9B80BFE/AirWatchSDK18.3.app/Frameworks/AWSDK.framework/AWSDK
 Reason: Incompatible library version: AWSDK requires version 1.0.0 or later, but libswiftAVFoundation.dylib provides version 0.0.0

 

原因是:

Apple states that you can’t mix and match binaries built with different versions of Swift: https://developer.apple.com/swift/blog/?id=2

I have two choices:

  • 1: Waiting for new SDK support xcode 9.3
  • 2: down-grade to xcode 9.2

 

iOS中使用URL Scheme進行App跳轉

iOS的APP可以注冊自己的URL Scheme,URL Scheme是為方便app之間互相調用而設計的。我們可以通過系統的OpenURL來開啟該app,並可以傳遞一些參數。

例如: line:// 開頭的 sheme 可以用來開啟 LINE App.

 

Apple 的官方教學:

https://developer.apple.com/documentation/uikit/core_app/communicating_with_other_apps_using_custom_urls

 


在info.plist里添加URL types

每一個項目里面都會有一個info.plist配置檔案。找到info.plist,右鍵選擇Add Row,然後選擇URL types。如圖所示︰


URL Identifier是自定義的 URL scheme 的名字,一般采用反轉域名的方法保證該名字的唯一性,比如 com.iOSStrongDemo.www

重點是要在「URL Schemes」 加增加一個 item,一個app 可以註冊多組的 item.

 

 


 

在AppDelegate里面增加 handleOpenURL,當app被成功開啟時,切換到特定頁面。

 

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {

// 接受傳過來的參數
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@”Main” bundle:nil];

UINavigationController *root = [[UINavigationController alloc]initWithRootViewController:[storyboard instantiateViewControllerWithIdentifier:@”MainViewController”]];
self.window.rootViewController= root;

return YES;



}

for retrieve

- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];

NSMutableDictionary *mutableRetrievedDictionary = [[[NSUserDefaults standardUserDefaults] objectForKey:@"DicKey"] mutableCopy];

 // here parse the dictionary and do your work here, when your works is over 

 // remove the key of standardUserDefaults 

 [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"DicKey"];
 [[NSUserDefaults standardUserDefaults] synchronize];
}

上面的  code  無法執行,是因為 deprecated. 請改用 openURL:

 

– (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
if ([[url scheme] isEqualToString:@”todolist”]) {
ToDoItem *item = [[ToDoItem alloc] init];
NSString *taskName = [url query];
if (!taskName || ![self isValidTaskString:taskName]) { // must have a task name
return NO;
}
taskName = [taskName stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

item.toDoTask = taskName;
NSString *dateString = [url fragment];
if (!dateString || [dateString isEqualToString:@”today”]) {
item.dateDue = [NSDate date];
} else {
if (![self isValidDateString:dateString]) {
return NO;
}
// format: yyyymmddhhmm (24-hour clock)
NSString *curStr = [dateString substringWithRange:NSMakeRange(0, 4)];
NSInteger yeardigit = [curStr integerValue];
curStr = [dateString substringWithRange:NSMakeRange(4, 2)];
NSInteger monthdigit = [curStr integerValue];
curStr = [dateString substringWithRange:NSMakeRange(6, 2)];
NSInteger daydigit = [curStr integerValue];
curStr = [dateString substringWithRange:NSMakeRange(8, 2)];
NSInteger hourdigit = [curStr integerValue];
curStr = [dateString substringWithRange:NSMakeRange(10, 2)];
NSInteger minutedigit = [curStr integerValue];

NSDateComponents *dateComps = [[NSDateComponents alloc] init];
[dateComps setYear:yeardigit];
[dateComps setMonth:monthdigit];
[dateComps setDay:daydigit];
[dateComps setHour:hourdigit];
[dateComps setMinute:minutedigit];
NSCalendar *calendar = [s[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDate *itemDate = [calendar dateFromComponents:dateComps];
if (!itemDate) {
return NO;
}
item.dateDue = itemDate;
}

[(NSMutableArray *)self.list addObject:item];
return YES;
}
return NO;
}
Be sure to validate the input you get from URLs passed to your app; see Validating Input and Interprocess Communication in Secure Coding Guide to find out how to avoid problems related to URL handling. To learn about URL schemes defined by Apple, see Apple URL Scheme Reference.

 

說明: 上面的 handleOpenURL 和 openURL 在 iOS10中已弃用(deprecated)

請改用:

– (void)openScheme:(NSString *)scheme {

    UIApplication *application = [UIApplication sharedApplication];

    NSURL *URL = [NSURL URLWithString:scheme];

    NSLog(@”openScheme %@”,scheme);

    

    if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) {

        [application openURL:URL options:@{}

           completionHandler:^(BOOL success) {

               NSLog(@”Open %@: %d”,scheme,success);

           }];

    } else {

        BOOL success = [application openURL:URL];

        NSLog(@”Open %@: %d”,scheme,success);

    }

}


英文說明:

  1. Go into your app’s info.plst file.
  2. Add a Row to this and call it “URL types”.
  3. Expand the first item in “URL types” and add a row called “URL identifier”, the value of this string should be the reverse domain for your app e.g. “com.yourcompany.myapp”.
  4. Again, add a row into the first item in “URL types” and call it “URL Schemes”.
  5. Inside “URL Schemes” you can use each item as a different URL you wish to use, so if you wanted to use “myapp://” you would create an item called “myapp”.

Using the URL scheme, you’ve now registered the URL with the app. You can start the application by opening a URL with the custom scheme.

Use UIApplicationDelegate if you want to provide a custom handler for it. All you need to do is provide an implementation for it in your delegate.

Then get it:

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
  if (!url) { 
    return NO; 
  } 
  // Do something with the url here 
}

如果你修改了 info.plist 還是無法套用到 URL types,可能是修改到錯的 info.plist 請到 TARGETS 裡去修改:

Click TARGETS and click info.Then Click URL Type and add or write or set name in URL Schemes Target->Info->URL types->Add url types

 

說明:在info plist 裡加完,會出現在上面的 URL Types(0) 裡裡,在畫面最下面:

 

 

[iOS] install / uninstall app on device through MDM server?

使用下列的範例就可以安裝自家(in-house) 和 app store 上的 app了。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
     <dict>
          <key>CommandUUID</key>
          <string>4424F929-BDD2-4D44-B518-393C0DABD56A</string>
          <key>Command</key>
               <dict>
                    <key>RequestType</key>
                    <string>InstallApplication</string>
                    <key>iTunesStoreID</key>
                    <integer>464656389</integer>
                    <key>ManagementFlags</key>
                    <integer>4</integer>
               </dict>
     </dict>
</plist>

Here is example of RemoveApplication

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
     <dict>
          <key>CommandUUID</key>
          <string>4424F929-BDD2-4D44-B518-393C0DABD56A</string>
          <key>Command</key>
               <dict>
                    <key>RequestType</key>
                    <string>RemoveApplication</string>
                    <key>Identifier</key>
                    <string>com.test.test</string>
               </dict>
     </dict>
</plist>

There is no functionality to whitelist and blacklist apps. However, you can query device for list of application and if you see some blacklisted application you can do some actions.


說明:上面的範例是如何去下載 Apple App Store 上的 App.

 

相關文章:

[iOS] get MDM settings from system dictionary in swift
http://stackoverflow.max-everyday.com/2018/01/ios-get-mdm-settings-from-system-dictionary-in-swift/

MDM推送設定值到 iOS device
http://stackoverflow.max-everyday.com/2017/09/mdm-settings-ios-device/

MDM推送App到 iOS device
http://stackoverflow.max-everyday.com/2017/09/mdm-install-app/

Sample iOS 行動裝置管理伺服器(MDM server)
http://stackoverflow.max-everyday.com/2017/09/ios-mdm-server/

[iOS] 計時器 (Timer)

想要做一個計時器 (Timer),可以怎麼做?使用NSTimer,每隔一秒鐘更新一次秒數,就這麼簡單!

- (void)viewDidLoad
{
    [super viewDidLoad];
    // 設定Timer,每過1秒執行方法
    self.accumulatedTime = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(updateTime:) userInfo:nil repeats:YES];
}
-(void)updateTime:(NSTimer *)timer
{
    self. accumulatedTime++;
    NSLog(@"accumulatedTime:%f",self. accumulatedTime);
}

上面是有傳Timer 為參數的寫法,如果沒有要傳 Timer 進去,就拿掉 : 即可,我正使用的中的範例:

@property (weak, nonatomic) NSTimer *checkCameraStatusTimer;
//页面将要进入前台,开启定时器
-(void)viewWillAppear:(BOOL)animated
{
    //开启定时器
    self.checkCameraStatusTimer=[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(checkcamerastates) userInfo:nil repeats:YES];
    [self.checkCameraStatusTimer fire];
}

//页面消失,进入后台不显示该页面,关闭定时器
-(void)viewDidDisappear:(BOOL)animated
{
    //关闭定时器
    [self.checkCameraStatusTimer invalidate];
}

-(void)checkcamerastates
{
// TODO: here..
}

Swift 範例:

[iOS] Swift how to use NSTimer background?
http://stackoverflow.max-everyday.com/2018/01/ios-swift-how-to-use-nstimer-background/

 

NSURLConnection initWithRequest is deprecated

iOS 9.0 會顯示 NSURLConnection connectionWithRequest 已經 deprecated, 所以原本的 code 需要修改成:

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
        {
            // do something with the data 
        }];
[dataTask resume];

NSHTTPURLResponse 預設沒有 status code, 需要轉換一下成為 NSHTTPURLResponse 即可。

 

    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
    NSLog(@"response status code: %ld", (long)[httpResponse statusCode]);

You cannot just return the data (because the NSURLSessionDataTask runs asynchronously). You probably want to employ your own completion block pattern, similar to the completionHandler of the dataTaskWithRequest method.

So, you would add your own block parameter to your method, that you’ll invoke from inside the dataTaskWithRequest method’s completionHandler:

+ (NSURLSessionDataTask *)postCall:(NSDictionary *)parameters fromURL:(NSString *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler {

    // create your request here ...

    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (completionHandler)
            completionHandler(data, response, error);
    }];

    [dataTask resume];

    return dataTask;
}

Note, I think it’s good to return the NSURLSessionDataTask reference so (a) the caller can make sure the data task was successfully created; and (b) you have the NSURLSessionTask reference that you can use to cancel the task in case, at some future date, you want to be able to cancel the request for some reason (e.g. the user dismisses the view controller from which the request was issued).

Anyway, you’d then invoke this with:

NSURLSessionTask *task = [MyClass postCall:parameters fromURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    // put whatever code you want to perform when the asynchronous data task completes
}];

if (!task) {
    // handle failure to create task any way you want
}

You ask:

Is my way good or it will cause me problems in the future? I don’t have [an] image to download and I don’t have anything to upload, I just have to send [some] simple string data and receive [simple] string data. Or it will be better to but that code in each function independently?

If you’re receiving simple string data back, I’d suggest composing your response in JSON format, and then having the completion block in postCall use NSJSONSerialization to extract the response. Using JSON makes it easier for the app to differentiate between successful response and a variety of server related problems that might also return string responses.

So, let’s say you modified your server code to return a response like so:

{"response":"some text"}

Then you could modify postCall to parse that response like so:

+ (NSURLSessionDataTask *)postCall:(NSDictionary *)parameters fromURL:(NSString *)url completionHandler:(void (^)(NSString *responseString, NSError *error))completionHandler {

    // create your request here ...

    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (completionHandler) {
            if (error) {
                completionHandler(nil, error);
            } else {
                NSError *parseError = nil;
                NSDictionary *responseDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError];

                completionHandler(responseDictionary[@"response"], parseError);
            }
        }
    }];

    [dataTask resume];

    return dataTask;
}

In terms of your underlying question, whether a method like postCall makes sense, yes, I think it makes perfect sense to put the details of creating the request in a single method. My minor reservation in your implementation was your decision to make it a class method rather than an instance method. You’re currently creating a new NSURLSession for each request. I’d suggest making postCall an instance method (of a singleton if you want) and then saving the session as a class property, which you set once and then re-use on subsequent queries.

關於 iOS 不受支持的 URL 編碼問題

之前使用的副程式,遇到 iOS 9 已被停用,xcode 提示我請改用 stringByAddingPercentEncodingWithAllowedCharacters。

 

修改前副程式:

- (NSString*)escapePath:(NSString*)url
{
 CFStringEncoding encoding = CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding);
 NSString *escapedPath =
 (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
 (CFStringRef)url,
 NULL,
 (CFStringRef)@":?=,!$&'()*+;[]@#~",
 encoding);

修改後副程式:

- (NSString*)escapePath:(NSString*)url
{
    NSString *charactersToEscape = @"[email protected]#$^&%*+,:;='\"`<>()[]{}/\\| ";
    NSCharacterSet *allowedCharacters = [[NSCharacterSet characterSetWithCharactersInString:charactersToEscape] invertedSet];
    NSString *encodedUrl = [url stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacters];

 

 

Open PDF Documents in swift

使用下面的範例,就可以開啟  PDF 來 preview 也可以 OpenIn 到其他 app 裡。

Obj-C 的範例:
http://stackoverflow.max-everyday.com/2017/05/open-pdf-documents-in-ios/

Swift 範例:

class ViewController: UIViewController, UIDocumentInteractionControllerDelegate {

    var docController:UIDocumentInteractionController!


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        if let fileURL = NSBundle.mainBundle().pathForResource("SamplePDF1", ofType: "pdf") { // Use if let to unwrap to fileURL variable if file exists
            docController = UIDocumentInteractionController(URL: NSURL(fileURLWithPath: fileURL))

            docController.name = NSURL(fileURLWithPath: fileURL).lastPathComponent

            docController.delegate = self

            docController.presentPreviewAnimated(true)
        }


    }

    func documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController) -> UIViewController {
        return self
    }
     func documentInteractionControllerDidEndPreview(controller: UIDocumentInteractionController) {
        docController = nil
    }

Sharing text or image with UIActivityViewController in Swift

使用 swift 分享「文字」和「圖片」 在 iOS app.

source code:


import UIKit
class ViewController: UIViewController {

    // share text
    @IBAction func shareTextButton(_ sender: UIButton) {

        // text to share
        let text = "This is some text that I want to share."

        // set up activity view controller
        let textToShare = [ text ]
        let activityViewController = UIActivityViewController(activityItems: textToShare, applicationActivities: nil)
        activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash

        // exclude some activity types from the list (optional)
        activityViewController.excludedActivityTypes = [ UIActivityType.airDrop, UIActivityType.postToFacebook ]

        // present the view controller
        self.present(activityViewController, animated: true, completion: nil)

    }

    // share image
    @IBAction func shareImageButton(_ sender: UIButton) {

        // image to share
        let image = UIImage(named: "Image")

        // set up activity view controller
        let imageToShare = [ image! ]
        let activityViewController = UIActivityViewController(activityItems: imageToShare, applicationActivities: nil)
        activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash

        // exclude some activity types from the list (optional)
        activityViewController.excludedActivityTypes = [ UIActivityType.airDrop, UIActivityType.postToFacebook ]

        // present the view controller
        self.present(activityViewController, animated: true, completion: nil)
    }

}

執行畫面:

左文字,右圖片。

iOS本地服务器-GCDWebServer支持后台运行

遇到的问题:

GCDWebServer不支持长链接,只要App退到后台,connect自动就stop,网页请求无法获得响应。

解决办法:

1)开启支持后台模式:将GCDWebServer.m中的GCDWebServerOption_AutomaticallySuspendInBackground设置为NO;

修改為:

_suspendInBackground = [_GetOption(_options, GCDWebServerOption_AutomaticallySuspendInBackground, @NO) boolValue];

2)打开Background Modes;

結果還是不行。


修改為 @NO 的 xcode console log:

[ERROR] Failed accepting IPv4 socket: Bad file descriptor (9)
[DEBUG] Did open connection on socket 6
[DEBUG] Connection received 363 bytes on socket 6
[DEBUG] Connection on socket 6 preflighting request "GET /" with 363 bytes body
[DEBUG] Connection on socket 6 processing request "GET /" with 363 bytes body
[DEBUG] Did connect
[DEBUG] Did start background task
[DEBUG] Connection sent 182 bytes on socket 6
[ERROR] Error while writing to socket 6: Broken pipe (32)
[DEBUG] Did close connection on socket 6
[VERBOSE] [192.168.31.237:8080] 192.168.31.237:56991 200 "GET /" (363 | 182)
[DEBUG] Did disconnect
[DEBUG] Did end background task

修改為 @YES 的 console log 一切換到背景,web server網路就斷了:

[DEBUG] Did open IPv4 listening socket 4
[DEBUG] Did open IPv6 listening socket 5
[INFO] GCDWebServer started on port 8080 and reachable at http://192.168.31.237:8080/
2018-03-19 17:09:23.227 TkWebServer[3414:1442070] Visit http://192.168.31.237:8080/ in your web browser
[DEBUG] Did enter background
[DEBUG] Did close IPv6 listening socket 5
[DEBUG] Did close IPv4 listening socket 4
[INFO] GCDWebServer stopped