I have got two buttons on my today extension. One is dedicated to open MainViewController and the second one need to navigate to second ViewController.
So I made a appURL:
let appURL = NSURL(string: "StarterApplication://")
and then on a first button I call:
self.extensionContext?.open(appURL! as URL, completionHandler:nil)
Which opens app on MainViewController.
How can I open MainViewController and performSegue to my SecondViewController when tapping second button on Today Widget?
I made a second URL scheme for that specific ViewController. I saw in other simmilar topics that it can be done by calling:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
}
in AppDelegate but don't have any idea how to use it.
Use one URL scheme. You can add different paths or arguments for different task.
For example, I've an app that displays multiple items in a today extension. If you tap an item the app is opened.
- (void)_openItem:(SPItem *)item
{
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"myapp://localhost/open_item?itemID=%#", item.itemID]];
if (url != nil)
{
[self.extensionContext openURL:url completionHandler:nil];
}
}
As you've already mentioned in you question, you need to implement - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
In my case it more or less looks like this:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString *,id> *)options
{
BOOL ret;
if ([url.scheme isEqualToString:#"myapp"])
{
if ([url.path isEqual:#"open_item"])
{
#try
{
NSDictionary *query = [url.query queryDictionary];
NSString* itemID = query[#"itemID"];
[self _gotoItemWithID:itemID completionHandler:nil];
}
#catch (NSException *exception) {
}
ret = YES;
}
}
else
{
ret = NO;
}
return ret;
}
As you can see, I first check, if the url scheme. If it's the one I expect, I check the path. By using different paths I'm able to implement different commands the today extension is able to execute. Each command may have different arguments. In case of the "open_item" command, I expect the parameter "itemID".
By returning YES, you tell iOS you were able to handle the URL your app was called with.
In my app [self _gotoItemWithID:itemID completionHandler:nil] does all the need tasks to display the item. In your case you would need a function to display the second view controller.
Edit:
I forgot to mention that queryDictionary is a method in an NSString extension. It takes a string (self) and tries to extract URL parameter and return them as dictionary.
- (NSDictionary*)queryDictionary
{
NSCharacterSet* delimiterSet = [NSCharacterSet characterSetWithCharactersInString:#"&;"];
NSMutableDictionary* pairs = [NSMutableDictionary dictionary];
NSScanner* scanner = [[NSScanner alloc] initWithString:self];
while (![scanner isAtEnd])
{
NSString* pairString;
[scanner scanUpToCharactersFromSet:delimiterSet
intoString:&pairString] ;
[scanner scanCharactersFromSet:delimiterSet intoString:NULL];
NSArray* kvPair = [pairString componentsSeparatedByString:#"="];
if ([kvPair count] == 2)
{
NSString* key = [[kvPair objectAtIndex:0] stringByRemovingPercentEncoding];
NSString* value = [[kvPair objectAtIndex:1] stringByRemovingPercentEncoding];
[pairs setObject:value forKey:key] ;
}
}
return [NSDictionary dictionaryWithDictionary:pairs];
}
I found the solution. The answer is deep linking. Here is my method in appDelegate:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
let urlPath : String = url.path as String!
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if(urlPath == "/Action"){
let ActionView: ActionViewController = mainStoryboard.instantiateViewController(withIdentifier: "ActionViewController") as! ActionViewController
self.window?.rootViewController = ActionView
} else if(urlPath == "/VoiceCommandView") {
let VoiceCommandView: ViewController = mainStoryboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController
self.window?.rootViewController = VoiceCommandView
}
self.window?.makeKeyAndVisible()
return true
And in the TodayExtensionViewController I defined two URL schemes with same host but different URL paths. And made a simple:
self.extensionContext?.open(startAppURL! as URL, completionHandler:nil)
for first button but changed the urlPath for the second button.
Related
I encounter a weird issue with long dynamic link base on FirebaseDynamicLinks (4.0.8):
Have tried refer to similar issues firebase/quickstart-ios/issues/380#issuecomment-343255857 and DynamicLinks.dynamicLinks().handleUniversalLink returns false
My long dynamic link format is alike that :
https://example.page.link/?link=https://app.tdservice/account?to=create&apn=com.testDynamicAndroid.service.app&isi=1234567890&ibi=com.TestDynamiciOS.service
And already confirm my real link is normal by appsearch-validation-tool
However, my implement of handleUniversalLink in AppDelegate without call back, cause the handled return NO....
- (BOOL)application:(UIApplication *)application
continueUserActivity:(nonnull NSUserActivity *)userActivity
restorationHandler:
#if defined(__IPHONE_12_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0)
(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
#else
(nonnull void (^)(NSArray *_Nullable))restorationHandler {
#endif // __IPHONE_12_0
if ([userActivity webpageURL] != nil) {
NSString *incomingURL = [userActivity webpageURL].absoluteString;
NSLog(#"Incoming URL is %#", incomingURL);
BOOL handled = [[FIRDynamicLinks dynamicLinks] handleUniversalLink:userActivity.webpageURL
completion:^(FIRDynamicLink * _Nullable dynamicLink,
NSError * _Nullable error) {
if (error != nil) {
return NSLog(#"Found an error! %#", error.localizedDescription);
}
if (dynamicLink != nil && dynamicLink.url != nil) {
[self handleIncomingDynamicLink:dynamicLink]; // additional declare
} else {
NSLog(#"This's weird. Dynamic link object has no url");
}
}];
if (handled) {
return YES;
} else {
// may do other things with incoming URL
return NO;
}
} else { return NO; }
}
NSDictionary *FIRDLDictionaryFromQuery(NSString *queryString) {
NSArray<NSString *> *keyValuePairs = [queryString componentsSeparatedByString:#"&"];
NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionaryWithCapacity:keyValuePairs.count];
for (NSString *pair in keyValuePairs) {
NSArray *keyValuePair = [pair componentsSeparatedByString:#"="];
if (keyValuePair.count == 2) {
NSString *key = keyValuePair[0];
NSString *value = [keyValuePair[1] stringByRemovingPercentEncoding];
[queryDictionary setObject:value forKey:key];
}
}
}
Then I tracking FIRDynamicLinks and found the root cause.
Dynamic link for iOS with filter char of #"&" and #"=" , according to the keyValuePair.count == 2, my params be dropped who the keyword of [link].
Due to my long dynamic link has two [=] symbol before first [&] symbol, keyValuePair.count is 3.
So that my dynamic link object hasn't link parameter and return nil
I think url have to refer to Manually constructing a Dynamic Link URL , and try to be the same.
Finally, I found to similar symptom Deep Link does not contain valid required params, link value have to percent encoded, solving my confuse and my issue.
But interesting, Android platform without this symptom, the same long dynamic link is workable.
I used the Solution form iOS Share Extension issue when sharing images from Photo library to get Images from the Photo App. This works great in the Simulator, but on the Device I get an error that I can't Access the NSURL provided by the itemProvider:
2018-02-18 12:54:09.448148+0100 MyApp[6281:1653186] [default] [ERROR] Failed to determine whether URL /var/mobile/Media/PhotoData/OutgoingTemp/554581B2-950C-4CFD-AE67-A2342EDEA04D/IMG_2784.JPG (s) is managed by a file provider
Caused by the Statment:
[itemProvider loadItemForTypeIdentifier:itemProvider.registeredTypeIdentifiers.firstObject options:nil completionHandler:^(id<NSSecureCoding> item, NSError *error) {
}
Searching PHAsset for the Item Name is not a good solution as the user have to grand access to the photo library again.
In didSelectPost you must not dismiss the ShareExtension ViewController until you have processed all the items in the inputItems array. The ViewController is dismissed using [super didSelectPost] or by calling the completion method of the extension context.
Here is my solution in code:
- (void)didSelectPost {
__block NSInteger itemCount = ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments.count;
__block NSInteger processedCount = 0;
for (NSItemProvider* itemProvider in ((NSExtensionItem*)self.extensionContext.inputItems[0]).attachments ) {
if([itemProvider hasItemConformingToTypeIdentifier:#"public.jpeg"]) {
NSLog(#"itemprovider = %#", itemProvider);
[itemProvider loadItemForTypeIdentifier:#"public.jpeg" options:nil completionHandler: ^(id<NSSecureCoding> item, NSError *error) {
NSData *imgData;
if([(NSObject*)item isKindOfClass:[NSURL class]]) {
imgData = [NSData dataWithContentsOfURL:(NSURL*)item];
}
if([(NSObject*)item isKindOfClass:[UIImage class]]) {
imgData = UIImagePNGRepresentation((UIImage*)item);
}
NSDictionary *dict = #{
#"imgData" : imgData,
#"name" : self.contentText
};
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:#"group.share.extension1"];
[defaults setObject:dict forKey:#"img"];
[defaults synchronize];
processedCount += 1;
if (processedCount == itemCount)
[super didSelectPost];
}];
}
}
The loadItemForTypeIdentifier or in Swift loadItem method is asynchronous so dismissing the UI must be called as the last thing inside its completionHandler.
For example I have:
override func didSelectPost() {
// This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments.
if let item = self.extensionContext?.inputItems[0] as? NSExtensionItem, let attachments = item.attachments {
for provider in attachments {
if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: {item, error in
// do all you need to do here, i.e.
if let tmpURL = item as? URL {
// etc. etc.
}
// and, at the end, inside the completionHandler, call
// Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context.
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
})
}
}
}
}
I you dismiss the UI via:
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
or via:
super.didSelectPost()
outside of the completionHandler after the async method loadItem you will get all kind of permission errors, further more this errors could be random, sometimes happen and sometimes don't, this is because sometimes your async call to loadItem gets the chance to terminate before the UI is dismissed and sometimes it doesn't.
Just leaving this here, hoping it helps someone. This issue costed me few hours.
I know it's been discussed that you cannot currently (as of xcode 6.3) test localnotifications pushing to the watch, but you can simulate remotenotification by using an .apns file and a dev scheme.
People say they are similar, so it seems like a good idea to use some kind of handling code to pass the data to do what you'd like. So for example in an apns with the following:
{
"aps": {
"alert": {
"body": "bodyText",
"title": "titleText"
},
"category": "myCategory"
},
"WatchKit Simulator Actions": [],
"customKey": "custom value."
}
I have an alert with "body" and "title" in it.
Now, to handle it in the watch's wkInterfaceController, i do the following
override func didReceiveRemoteNotification(remoteNotification: [NSObject : AnyObject], withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {
if let remoteaps:NSDictionary = remoteNotification["aps"] as? NSDictionary{
if let remotealert:NSDictionary = remoteaps["alert"] as? NSDictionary{
handleNotification( remoteAlert );
}
}
completeHandler( .Custom )
}
func handleNotification( alert : AnyObject? ){
if let alert = alert, let remotetitle = alert["title"] as? String{
println( "didReceiveRemoteNotification::remoteNotification.alert \(remotetitle)" )
titleLabel.setText(remotetitle);
}
if let alert = alert, let remotebody = alert["body"] as? String{
//println( "didReceiveRemoteNotification::remoteNotification.alert \(remotetitle)" )
bodyLabel.setText(remotebody);
}
}
Where the handling of the alert is abstracted, but how do I access that alert from localnotification's handler? Is that data in userInfo? somewhere else? Will this work?
override func didReceiveLocalNotification(localNotification: UILocalNotification, withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {
handleNotification( localNotification.userInfo ); //is this the right way to pass this?
completionHandler(.Custom)
}
Also, how do I send out that local notification to match that? Here is the phone side code to set up the notification:
var reminderNotification:UILocalNotification = UILocalNotification();
reminderNotification.category = "myCategory"
reminderNotification.alertTitle = "TitleText"
reminderNotification.alertBody = "BodyText"
reminderNotification.fireDate = NSDate().dateByAddingTimeInterval(60) //notify in one minute
reminderNotification.timeZone = NSTimeZone.systemTimeZone()
UIApplication.sharedApplication().scheduleLocalNotification(reminderNotification)
This should work:
- (void)didReceiveLocalNotification:(UILocalNotification *)localNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler {
// This method is called when a local notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
NSString *reminderId = [localNotification.userInfo valueForKey:kAppLocalNotificationIdKey]; // This is a custom key
[self manageNotification:#{#"title":localNotification.alertTitle,
#"body":localNotification.alertBody}
withId:reminderId];
// After populating your dynamic notification interface call the completion block.
completionHandler(WKUserNotificationInterfaceTypeCustom);
}
- (void)didReceiveRemoteNotification:(NSDictionary *)remoteNotification withCompletion:(void (^)(WKUserNotificationInterfaceType))completionHandler {
// This method is called when a remote notification needs to be presented.
// Implement it if you use a dynamic notification interface.
// Populate your dynamic notification interface as quickly as possible.
NSDictionary *aps = [remoteNotification valueForKey:#"aps"];
if (!IsEmpty(aps)) {
NSDictionary *alert = [aps valueForKey:#"alert"];
if (!IsEmpty(alert)) {
NSString *reminderId = [remoteNotification valueForKey:kAppLocalNotificationIdKey]; // This is a custom key
[self manageNotification:alert withId:reminderId];
}
}
// After populating your dynamic notification interface call the completion block.
completionHandler(WKUserNotificationInterfaceTypeCustom);
}
- (void) manageNotification:(NSDictionary *)notificationData withId:(NSString *)notificationId {
NSString *title = [notificationData valueForKey:#"title"];
NSString *message = [notificationData valueForKey:#"body"];
[self.titleLabel setText:title];
[self.messageLabel setText:message];
}
I am Using cocoaLumberjack logging framework for iOS logging.
For storing logs in a file I used this code.
DDFileLogger* fileLogger = [[DDFileLogger alloc] init];
fileLogger.rollingFrequency = 60 * 60 * 24;
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;
[DDLog addLogger:fileLogger];
DDLogVerbose(#"hello");
NSLog(#"hihihihihi");
I am unable to find where exactly the logfile generated by this code is stored.
Can someone help me with this problem ?
You can download the log files from connected device, or you can send directly from app. Both approaches are described below.
Send log files from app through email, in Swift
Write this in the class where you have a reference to DDFileLogger. I would put this in a custom logger class e.g. MyLogger.swift
var ddFileLogger: DDFileLogger!
var logFileDataArray: [NSData] {
get {
let logFilePaths = ddFileLogger.logFileManager.sortedLogFilePaths() as! [String]
var logFileDataArray = [NSData]()
for logFilePath in logFilePaths {
let fileURL = NSURL(fileURLWithPath: logFilePath)
if let logFileData = try? NSData(contentsOfURL: fileURL, options: NSDataReadingOptions.DataReadingMappedIfSafe) {
// Insert at front to reverse the order, so that oldest logs appear first.
logFileDataArray.insert(logFileData, atIndex: 0)
}
}
return logFileDataArray
}
}
Then, when user taps on a button to indicate that they want to send the logs,
// Required by MFMailComposeViewController
import MessageUI
#IBAction func writeEmailTapped(sender: AnyObject) {
if MFMailComposeViewController.canSendMail() {
let composeVC = MFMailComposeViewController()
composeVC.mailComposeDelegate = self
// Configure the fields of the interface.
composeVC.setToRecipients(["your-email#company.com"])
composeVC.setSubject("Feedback for app")
composeVC.setMessageBody("", isHTML: false)
let attachmentData = NSMutableData()
for logFileData in MyLogger.sharedInstance.logFileDataArray {
attachmentData.appendData(logFileData)
}
composeVC.addAttachmentData(attachmentData, mimeType: "text/plain", fileName: "diagnostic.log")
self.presentViewController(composeVC, animated: true, completion: nil)
} else {
// Tell user about not able to send email directly.
}
}
This results in a compose email pop-up with an attachment file named diagnostic.log, which is all the log files concatenated together.
Special thanks - This is pretty much a Swift translation from the Objective-C version given by the other answer.
Get log file(s) from device directly, through USB cable
If you want to get the log files that your app created while running on device,
Connect your device to your mac
In Xcode, go to Window -> Devices
On top-left in the device list, click on the connected device.
In the main panel, under Installed Apps section, click on the application in which you ran CocoaLumberjack.
At the bottom of the Installed Apps list, click on the gear icon and then Download Container.
In Finder, right click (show menu) on the saved .xcappdata file and select Show Package Contents
Log files are saved in /AppData/Library/Caches/Logs/
Up-vote would be nice if this is helpful to you!
The answers here don't seem to account for the fact that there may be multiple log files. You can use your DDFileLogger instance's logFileManager property to loop through file information. Check out DDFileLogger.h for public methods and properties. The following may be of use:
- (NSString *)logsDirectory;
- (NSArray *)unsortedLogFilePaths;
- (NSArray *)unsortedLogFileNames;
- (NSArray *)unsortedLogFileInfos;
- (NSArray *)sortedLogFilePaths;
- (NSArray *)sortedLogFileNames;
- (NSArray *)sortedLogFileInfos;
Here is my solution for getting log data (and emailing it). Note that the default number of log files is 5 as of this writing.
- (NSMutableArray *)errorLogData {
NSUInteger maximumLogFilesToReturn = MIN([KRLogManager sharedInstance].fileLogger.logFileManager.maximumNumberOfLogFiles, 10);
NSMutableArray *errorLogFiles = [NSMutableArray arrayWithCapacity:maximumLogFilesToReturn];
DDFileLogger *logger = [KRLogManager sharedInstance].fileLogger;
NSArray *sortedLogFileInfos = [logger.logFileManager sortedLogFileInfos];
for (int i = 0; i < MIN(sortedLogFileInfos.count, maximumLogFilesToReturn); i++) {
DDLogFileInfo *logFileInfo = [sortedLogFileInfos objectAtIndex:i];
NSData *fileData = [NSData dataWithContentsOfFile:logFileInfo.filePath];
[errorLogFiles addObject:fileData];
}
return errorLogFiles;
}
- (void)composeEmailWithDebugAttachment {
if ([MFMailComposeViewController canSendMail]) {
MFMailComposeViewController *mailViewController = [[MFMailComposeViewController alloc] init];
mailViewController.mailComposeDelegate = self;
NSMutableData *errorLogData = [NSMutableData data];
for (NSData *errorLogFileData in [self errorLogData]) {
[errorLogData appendData:errorLogFileData];
}
[mailViewController addAttachmentData:errorLogData mimeType:#"text/plain" fileName:#"errorLog.txt"];
[mailViewController setSubject:NSLocalizedString(#"Good Subject", #"")];
[mailViewController setToRecipients:[NSArray arrayWithObject:#"some#email.com"]];
[self presentModalViewController:mailViewController animated:YES];
}
else {
NSString *message = NSLocalizedString(#"Sorry, your issue can't be reported right now. This is most likely because no mail accounts are set up on your mobile device.", #"");
[[[UIAlertView alloc] initWithTitle:nil message:message delegate:nil cancelButtonTitle:NSLocalizedString(#"OK", #"") otherButtonTitles: nil] show];
}
}
If you're using CocoaLumberjack, you have DDFileLogger.h and you can expose the currentLogFileInfo: method that is implemented already:
#interface DDFileLogger : DDAbstractLogger <DDLogger>
...
- (DDLogFileInfo *)currentLogFileInfo;
#end
Then, you can programmatically access the path to the current file with:
// configure logger
DDFileLogger *fileLogger = [DDFileLogger new];
[DDLog addLogger:fileLogger];
[DDLog addLogger:[DDTTYLogger sharedInstance]];
DDLogInfo(#"log file at: %#", [[fileLogger currentLogFileInfo] filePath]);
Which, on my iPhone, printed:
log file at: /var/mobile/Applications/3BE1219F-78BE-491C-B68C-74D6FA0C2EF1/Library/Caches/Logs/log-5D1286.txt
to both the console and the file.
You can control where it is stored, for example, I sometime store it in the iTunes folder for easy retrieval. Use this in the AppDelegate when setting up the fileLogger:
NSString * applicationDocumentsDirectory = [[[[NSFileManager defaultManager]
URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject] path];
DDLogFileManagerDefault *documentsFileManager = [[DDLogFileManagerDefault alloc]
initWithLogsDirectory:applicationDocumentsDirectory];
DDFileLogger *fileLogger = [[DDFileLogger alloc]
initWithLogFileManager:documentsFileManager];
Found this to be the latest:
DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
fileLogger.logFileManager.logsDirectory;//THIS
From the official code:
Default log file manager.
All log files are placed inside the logsDirectory. If a specific
logsDirectory isn't specified, the default directory is used. On
Mac, this is in ~/Library/Logs/. On iPhone, this
is in ~/Library/Caches/Logs.
Log files are named "log-.txt", where uuid is a 6 character hexadecimal consisting of the set [0123456789ABCDEF].
Archived log files are automatically deleted according to the maximumNumberOfLogFiles property.
Got the answer
It is stored in
Library/Appication Support/Iphone Simulator/#version no#/applications/#your application#/documents/logs/log-3hex no>
Send log files from app through email, in Objective-C
Objective-C code:
In you header:
#import <MessageUI/MessageUI.h>
#import "DDLog.h"
#import "DDFileLogger.h"
In your implementation:
- (NSMutableArray *)errorLogData {
DDFileLogger *ddFileLogger = [DDFileLogger new];
NSArray <NSString *> *logFilePaths = [ddFileLogger.logFileManager sortedLogFilePaths];
NSMutableArray <NSData *> *logFileDataArray = [NSMutableArray new];
for (NSString* logFilePath in logFilePaths) {
NSURL *fileUrl = [NSURL fileURLWithPath:logFilePath];
NSData *logFileData = [NSData dataWithContentsOfURL:fileUrl options:NSDataReadingMappedIfSafe error:nil];
if (logFileData) {
[logFileDataArray insertObject:logFileData atIndex:0];
}
}
return logFileDataArray;
}
- (void)composeEmailWithDebugAttachment {
if ([MFMailComposeViewController canSendMail]) {
MFMailComposeViewController *mailViewController = [[MFMailComposeViewController alloc] init];
mailViewController.mailComposeDelegate = self;
NSMutableData *errorLogData = [NSMutableData data];
for (NSData *errorLogFileData in [self errorLogData]) {
[errorLogData appendData:errorLogFileData];
}
[mailViewController addAttachmentData:errorLogData mimeType:#"text/plain" fileName:#"filename.log"];
[mailViewController setSubject:NSLocalizedString(#"LogFile Subject", #"")];
[mailViewController setToRecipients:[NSArray arrayWithObject:#"email#email.com"]];
[self presentViewController:mailViewController animated:YES completion:nil];
} else {
NSString *message = NSLocalizedString(#"Sorry, your issue can't be reported right now. This is most likely because no mail accounts are set up on your mobile device.", #"");
[[[UIAlertView alloc] initWithTitle:nil message:message delegate:nil cancelButtonTitle:NSLocalizedString(#"OK", #"") otherButtonTitles: nil] show];
}
}
- (void)mailComposeController:(MFMailComposeViewController *)mailer didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error {
[self becomeFirstResponder];
[mailer dismissViewControllerAnimated:YES completion:nil];
}
Here is how you could add something in your Log-file:
DDLogError(#"This is an error.");
DDLogWarn(#"This is a warning.");
DDLogInfo(#"This is just a message.");
DDLogVerbose(#"This is a verbose message.");
Don't forget to set your ddLogLevel in your Constants-file.
Constants.h :
extern NSUInteger const ddLogLevel;
Comstants.m :
NSUInteger const ddLogLevel =
#ifdef DEBUG
LOG_LEVEL_VERBOSE;
#else
LOG_LEVEL_ERROR;
#endif
It is very obvious but don't forget to configure CocoaLumberjack in your AppDelegate.m file.
[DDLog addLogger:[DDASLLogger sharedInstance]];
[DDLog addLogger:[DDTTYLogger sharedInstance]];
DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
[fileLogger setMaximumFileSize:(1024 * 1024)];
[fileLogger setRollingFrequency:(3600.0 * 24.0)];
[[fileLogger logFileManager] setMaximumNumberOfLogFiles:7];
[DDLog addLogger:fileLogger];
The best place to paste this code is - (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions; in your AppDelegate.m file.
Also you should add this line of code in your AppDelegate.m header:
#import <CocoaLumberjack/CocoaLumberjack.h>
Hope it will help.
I had to rewrite it a little to be compatible with Swift 4...
var logFileDataArray: [Data] {
let logFilePaths = delegate.fileLogger.logFileManager.sortedLogFilePaths
var logFileDataArray = [Data]()
for logFilePath in logFilePaths {
let fileURL = URL(fileURLWithPath: logFilePath)
if let logFileData =
try? Data(contentsOf: fileURL, options: Data.ReadingOptions.mappedIfSafe) {
logFileDataArray.insert(logFileData, at: 0)
}
}
return logFileDataArray
}
func sendApplicationLog(text: String) {
if MFMailComposeViewController.canSendMail() {
let controller = MFMailComposeViewController()
controller.mailComposeDelegate = self
controller.setToRecipients(["dohan.rene#gmail.com"])
controller.setSubject("Log of motorkari iOS")
controller.setMessageBody(text, isHTML: false)
var attachmentData = Data()
for logFileData in logFileDataArray { attachmentData.append(logFileData) }
controller.addAttachmentData(attachmentData, mimeType: "text/plain",
fileName: "motorkari_ios_application.log")
present(controller, animated: true, completion: nil)
} else {
showMessage("Log cannot be send !")
}
}
I don't know if this might help somebody else... I took the previous answers and passed it to Swift 5. Apart from getting all the paths, it prints the content.
let logFileLogger = DDFileLogger()
print(logFileLogger.logFileManager.logsDirectory)
for path in logFileLogger.logFileManager.sortedLogFilePaths {
do {
let content = try String(contentsOfFile: path, encoding: .utf8)
print(content)
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
I have a commercial app that has a completely legitimate reason to see the SSID of the network it is connected to: If it is connected to a Adhoc network for a 3rd party hardware device it needs to be functioning in a different manner than if it is connected to the internet.
Everything I've seen about getting the SSID tells me I have to use Apple80211, which I understand is a private library. I also read that if I use a private library Apple will not approve the app.
Am I stuck between an Apple and a hard place, or is there something I'm missing here?
As of iOS 7 or 8, you can do this (need Entitlement for iOS 12+ as shown below):
#import SystemConfiguration.CaptiveNetwork;
/** Returns first non-empty SSID network info dictionary.
* #see CNCopyCurrentNetworkInfo */
- (NSDictionary *)fetchSSIDInfo {
NSArray *interfaceNames = CFBridgingRelease(CNCopySupportedInterfaces());
NSLog(#"%s: Supported interfaces: %#", __func__, interfaceNames);
NSDictionary *SSIDInfo;
for (NSString *interfaceName in interfaceNames) {
SSIDInfo = CFBridgingRelease(
CNCopyCurrentNetworkInfo((__bridge CFStringRef)interfaceName));
NSLog(#"%s: %# => %#", __func__, interfaceName, SSIDInfo);
BOOL isNotEmpty = (SSIDInfo.count > 0);
if (isNotEmpty) {
break;
}
}
return SSIDInfo;
}
Example output:
2011-03-04 15:32:00.669 ShowSSID[4857:307] -[ShowSSIDAppDelegate fetchSSIDInfo]: Supported interfaces: (
en0
)
2011-03-04 15:32:00.693 ShowSSID[4857:307] -[ShowSSIDAppDelegate fetchSSIDInfo]: en0 => {
BSSID = "ca:fe:ca:fe:ca:fe";
SSID = XXXX;
SSIDDATA = <01234567 01234567 01234567>;
}
Note that no ifs are supported on the simulator. Test on your device.
iOS 12
You must enable access wifi info from capabilities.
Important
To use this function in iOS 12 and later, enable the Access WiFi Information capability for your app in Xcode. When you enable this capability, Xcode automatically adds the Access WiFi Information entitlement to your entitlements file and App ID. Documentation link
Swift 4.2
func getConnectedWifiInfo() -> [AnyHashable: Any]? {
if let ifs = CFBridgingRetain( CNCopySupportedInterfaces()) as? [String],
let ifName = ifs.first as CFString?,
let info = CFBridgingRetain( CNCopyCurrentNetworkInfo((ifName))) as? [AnyHashable: Any] {
return info
}
return nil
}
UPDATE FOR iOS 10 and up
CNCopySupportedInterfaces is no longer deprecated in iOS 10. (API Reference)
You need to import SystemConfiguration/CaptiveNetwork.h and add SystemConfiguration.framework to your target's Linked Libraries (under build phases).
Here is a code snippet in swift (RikiRiocma's Answer):
import Foundation
import SystemConfiguration.CaptiveNetwork
public class SSID {
class func fetchSSIDInfo() -> String {
var currentSSID = ""
if let interfaces = CNCopySupportedInterfaces() {
for i in 0..<CFArrayGetCount(interfaces) {
let interfaceName: UnsafePointer<Void> = CFArrayGetValueAtIndex(interfaces, i)
let rec = unsafeBitCast(interfaceName, AnyObject.self)
let unsafeInterfaceData = CNCopyCurrentNetworkInfo("\(rec)")
if unsafeInterfaceData != nil {
let interfaceData = unsafeInterfaceData! as Dictionary!
currentSSID = interfaceData["SSID"] as! String
}
}
}
return currentSSID
}
}
(Important: CNCopySupportedInterfaces returns nil on simulator.)
For Objective-c, see Esad's answer here and below
+ (NSString *)GetCurrentWifiHotSpotName {
NSString *wifiName = nil;
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
for (NSString *ifnam in ifs) {
NSDictionary *info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);
if (info[#"SSID"]) {
wifiName = info[#"SSID"];
}
}
return wifiName;
}
UPDATE FOR iOS 9
As of iOS 9 Captive Network is deprecated*. (source)
*No longer deprecated in iOS 10, see above.
It's recommended you use NEHotspotHelper (source)
You will need to email apple at networkextension#apple.com and request entitlements. (source)
Sample Code (Not my code. See Pablo A's answer):
for(NEHotspotNetwork *hotspotNetwork in [NEHotspotHelper supportedNetworkInterfaces]) {
NSString *ssid = hotspotNetwork.SSID;
NSString *bssid = hotspotNetwork.BSSID;
BOOL secure = hotspotNetwork.secure;
BOOL autoJoined = hotspotNetwork.autoJoined;
double signalStrength = hotspotNetwork.signalStrength;
}
Side note: Yup, they deprecated CNCopySupportedInterfaces in iOS 9 and reversed their position in iOS 10. I spoke with an Apple networking engineer and the reversal came after so many people filed Radars and spoke out about the issue on the Apple Developer forums.
Here's the cleaned up ARC version, based on #elsurudo's code:
- (id)fetchSSIDInfo {
NSArray *ifs = (__bridge_transfer NSArray *)CNCopySupportedInterfaces();
NSLog(#"Supported interfaces: %#", ifs);
NSDictionary *info;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer NSDictionary *)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);
NSLog(#"%# => %#", ifnam, info);
if (info && [info count]) { break; }
}
return info;
}
This works for me on the device (not simulator). Make sure you add the systemconfiguration framework.
#import <SystemConfiguration/CaptiveNetwork.h>
+ (NSString *)currentWifiSSID {
// Does not work on the simulator.
NSString *ssid = nil;
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
for (NSString *ifnam in ifs) {
NSDictionary *info = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);
if (info[#"SSID"]) {
ssid = info[#"SSID"];
}
}
return ssid;
}
This code work well in order to get SSID.
#import <SystemConfiguration/CaptiveNetwork.h>
#implementation IODAppDelegate
#synthesize window = _window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CFArrayRef myArray = CNCopySupportedInterfaces();
CFDictionaryRef myDict = CNCopyCurrentNetworkInfo(CFArrayGetValueAtIndex(myArray, 0));
NSLog(#"Connected at:%#",myDict);
NSDictionary *myDictionary = (__bridge_transfer NSDictionary*)myDict;
NSString * BSSID = [myDictionary objectForKey:#"BSSID"];
NSLog(#"bssid is %#",BSSID);
// Override point for customization after application launch.
return YES;
}
And this is the results :
Connected at:{
BSSID = 0;
SSID = "Eqra'aOrange";
SSIDDATA = <45717261 27614f72 616e6765>;
}
If you are running iOS 12 you will need to do an extra step.
I've been struggling to make this code work and finally found this on Apple's site:
"Important
To use this function in iOS 12 and later, enable the Access WiFi Information capability for your app in Xcode. When you enable this capability, Xcode automatically adds the Access WiFi Information entitlement to your entitlements file and App ID."
https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo
See CNCopyCurrentNetworkInfo in CaptiveNetwork: http://developer.apple.com/library/ios/#documentation/SystemConfiguration/Reference/CaptiveNetworkRef/Reference/reference.html.
Here's the short & sweet Swift version.
Remember to link and import the Framework:
import UIKit
import SystemConfiguration.CaptiveNetwork
Define the method:
func fetchSSIDInfo() -> CFDictionary? {
if let
ifs = CNCopySupportedInterfaces().takeUnretainedValue() as? [String],
ifName = ifs.first,
info = CNCopyCurrentNetworkInfo((ifName as CFStringRef))
{
return info.takeUnretainedValue()
}
return nil
}
Call the method when you need it:
if let
ssidInfo = fetchSSIDInfo() as? [String:AnyObject],
ssID = ssidInfo["SSID"] as? String
{
println("SSID: \(ssID)")
} else {
println("SSID not found")
}
As mentioned elsewhere, this only works on your iDevice. When not on WiFi, the method will return nil – hence the optional.
For iOS 13
As from iOS 13 your app also needs Core Location access in order to use the CNCopyCurrentNetworkInfo function unless it configured the current network or has VPN configurations:
So this is what you need (see apple documentation):
- Link the CoreLocation.framework library
- Add location-services as a UIRequiredDeviceCapabilities Key/Value in Info.plist
- Add a NSLocationWhenInUseUsageDescription Key/Value in Info.plist describing why your app requires Core Location
- Add the "Access WiFi Information" entitlement for your app
Now as an Objective-C example, first check if location access has been accepted before reading the network info using CNCopyCurrentNetworkInfo:
- (void)fetchSSIDInfo {
NSString *ssid = NSLocalizedString(#"not_found", nil);
if (#available(iOS 13.0, *)) {
if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) {
NSLog(#"User has explicitly denied authorization for this application, or location services are disabled in Settings.");
} else {
CLLocationManager* cllocation = [[CLLocationManager alloc] init];
if(![CLLocationManager locationServicesEnabled] || [CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined){
[cllocation requestWhenInUseAuthorization];
usleep(500);
return [self fetchSSIDInfo];
}
}
}
NSArray *ifs = (__bridge_transfer id)CNCopySupportedInterfaces();
id info = nil;
for (NSString *ifnam in ifs) {
info = (__bridge_transfer id)CNCopyCurrentNetworkInfo(
(__bridge CFStringRef)ifnam);
NSDictionary *infoDict = (NSDictionary *)info;
for (NSString *key in infoDict.allKeys) {
if ([key isEqualToString:#"SSID"]) {
ssid = [infoDict objectForKey:key];
}
}
}
...
...
}