How to set root view controller in Today extension programmatically - ios

I have a project without storyboard. I've set root view controller in AppDelegate. Now I've added a today extension. I got a new storyboard MainInterface.storyboard with root view controller TodayViewController. How can I delete MainInterface.storyboard and set root view controller programmatically?

Xcode 11.0
Open Info.plist from your Extension and change
<dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
...
</dict>
to:
<dict>
<key>NSExtensionPrincipalClass</key>
<string>NameOfYourExtension.NameOfYourViewController</string>
...
</dict>
Delete MainInterface.storyboard and it should work without any faults.

Using the URL Scheme to achieve your goal, See this code.
Write the following code in TodayViewController (Today extension swift file)
#IBAction func buttonTapped(_ sender: UIButton) {
let url = URL(string: "MyAppTodayExtension://")!
self.extensionContext?.open(url, completionHandler: { (success) in
if (!success) {
print("error: failed to open app from Today Extension")
}
})
}
Add URL Scheme in your info.plist file
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>MyAppTodayExtension</string>
</array>
</dict>
</array>
When you tap in today extension then get call following method in AppDelegate.swift file.
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
//Write code here for navigating to a specific controller OR set root view controller
return true
}

Related

Swift: Unable to get URL from Widget Link

I am attempting to implement a means to open specific pages of my app via iOS 14's widget, but so far my attempt just simply open the app without picking up the full deeplinking URL.
In my main app's SceneDelegate.swift:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
if let item = URLContexts.first {
print(item.url)
}
}
In my widget's EntryView:
struct EntryWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Link(destination: URL(string: "entry://link1")!, label: {
Text("Link 1")
})
Link(destination: URL(string: "entry://link2")!, label: {
Text("Link 2")
})
}
}
}
The above implementation prints "entry://" every single time when I launch my app via the widget. This is regardless whether I tap anywhere on the widget, or on the Text labels, all opens my app and prints "entry://", with the subsequent information (ie link1, link2) missing.
I believe I have setup the info.plist correctly in my main app:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>entry</string>
</array>
</dict>
</array>

iOS video not playing (play button crossed out)

I'm trying to play a video from URL, but when I run the code, all I can see is the play button crossed out.
here is the code:
import UIKit
import AVFoundation
import AVKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
#IBAction func playVideo(_ sender: Any) {
let videoURL = URL(string: "http://techslides.com/demos/sample-videos/small.mp4")
let player = AVPlayer(url: videoURL!)
let playerControler = AVPlayerViewController()
playerControler.player = player
present(playerControler, animated: true) {
player.play()
}
}
Xcode 9.1
Swift 4
Any idea what is wrong?
You need to edit your .plist file to be able to get video by given http URL
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
Add this code to your .plist xml, or simply try to load https: video:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>

Document Creation with UIDocumentBrowserViewController

The documentation for documentBrowser(_:didRequestDocumentCreationWithHandler:) says, "Create a new document and save it to a temporary location. If you use a UIDocument subclass to create the document, you must close it before calling the importHandler block."
So I created a file URL by taking the URL for the user's temporary directory (FileManager.default.temporaryDirectory) and appending a name and extension (getting a path like "file:///private/var/mobile/Containers/Data/Application/C1DE454D-EA1E-4166-B137-5B43185169D8/tmp/Untitled.uti"). But when I call save(to:for:completionHandler:) passing this URL, the completion handler is never called back. I also tried using url(for:in:appropriateFor:create:) to pass a subdirectory in the user's temporary directory—the completion handler was still never called.
I understand the document browser view controller is managed by a separate process, which has its own read / write permissions. Beyond that though, I'm having a hard time understanding what the problem is. Where can new documents be temporarily saved so that the document browser process can move them?
Update: as of the current betas, I now see an error with domain NSFileProviderInternalErrorDomain and code 1 getting logged: "The reader is not permitted to access the URL." At least that's confirmation of what's happening…
So, to start with, if you're using a custom UTI, it's got to be set up correctly. Mine look like this…
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array>
<string>icon-file-name</string> // Can be excluded, but keep the array
</array>
<key>CFBundleTypeName</key>
<string>Your Document Name</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.custom-uti</string>
</array>
</dict>
</array>
and
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string> // My doc is saved as Data, not a file wrapper
</array>
<key>UTTypeDescription</key>
<string>Your Document Name</string>
<key>UTTypeIdentifier</key>
<string>com.custom-uti</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>doc-extension</string>
</array>
</dict>
</dict>
</array>
Also
<key>UISupportsDocumentBrowser</key>
<true/>
I subclass UIDocument as MyDocument and add the following method to create a new temp document…
static func create(completion: #escaping Result<MyDocument> -> Void) throws {
let targetURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Untitled").appendingPathExtension("doc-extension")
coordinationQueue.async {
let document = MyDocument(fileURL: targetURL)
var error: NSError? = nil
NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: targetURL, error: &error) { url in
document.save(to: url, for: .forCreating) { success in
DispatchQueue.main.async {
if success {
completion(.success(document))
} else {
completion(.failure(MyDocumentError.unableToSaveDocument))
}
}
}
}
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}
Then init and display the DBVC as follows:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
lazy var documentBrowser: UIDocumentBrowserViewController = {
let utiDecs = Bundle.main.object(forInfoDictionaryKey: kUTExportedTypeDeclarationsKey as String) as! [[String: Any]]
let uti = utiDecs.first?[kUTTypeIdentifierKey as String] as! String
let dbvc = UIDocumentBrowserViewController(forOpeningFilesWithContentTypes:[uti])
dbvc.delegate = self
return dbvc
}()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = documentBrowser
window?.makeKeyAndVisible()
return true
}
}
And my delegate methods are as follows:
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Swift.Void) {
do {
try MyDocument.create() { result in
switch result {
case let .success(document):
// .move as I'm moving a temp file, if you're using a template
// this will be .copy
importHandler(document.fileURL, .move)
case let .failure(error):
// Show error
importHandler(nil, .none)
}
}
} catch {
// Show error
importHandler(nil, .none)
}
}
func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) {
let document = MyDocument(fileURL: destinationURL)
document.open { success in
if success {
// Modally present DocumentViewContoller for document
} else {
// Show error
}
}
}
And that's pretty much it. Let me know how you get on!
I had the same issue, but then I realized that the recommended way was to simply copy the package/folder from the Bundle, like so:
func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: #escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) {
if let url = Bundle.main.url(forResource: "Your Already Created Package", withExtension: "your-package-extension") {
importHandler(url, .copy)
} else {
importHandler(nil, .none)
}
}
To clarify, this package is just a folder that you've created and plopped into Xcode.
This approach makes sense, if you think about it, for a few reasons:
Apple File System (AFS). The move towards AFS means copying is (almost) free.
Permissions. Copying from the Bundle is always permissible, and the user is specifying the location to copy to.
New document browser paradigm. Since we're using the new UIDocumentBrowserViewController paradigm (which is due to iOS11 and the new Files app), it is even handling the naming (c.f. Apple's Pages) and moving and arranging of files. We don't have to worry about which thread to run things on either.
So. Simpler, easier, and probably better. I can't think of a reason to manually create all the files (or use the temp folder, etc).
Test on the device, not in the Simulator. The minute I switched to testing on the device, everything just started working correctly. (NOTE: It may be that the Simulator failures occur only for Sierra users like myself.)

Export UIDocument with custom file package UTI

I'm trying to export my UIDocument subclass with a UIDocumentPickerViewController. The subclass writes data to a FileWrapper and its UTI conforms to com.apple.package.
But the presented document picker shows "Documents in iCloud Drive are not available because the iCloud Drive setting is disabled."
The document is successfully written to the cache, as I can see from the exported container package.
When I change the document subclass and custom UTI to conform to a single file (e.g. public.plain-text), the document picker works fine and I can export the file. So the problem seems to be with the Document Type or Exported UTI.
Am I doing something wrong or is this a bug?
Info.plist
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Custom Doc</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.zxzxlch.documentsandbox.customdoc</string>
</array>
<key>LSTypeIsPackage</key>
<true/>
</dict>
</array>
...
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
</array>
<key>UTTypeDescription</key>
<string>Custom Doc File</string>
<key>UTTypeIdentifier</key>
<string>com.zxzxlch.documentsandbox.customdoc</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>zzz</string>
</array>
</dict>
</dict>
</array>
CustomDocument.swift
private let textFilename = "contents.txt"
class CustomDocument: UIDocument {
var content = "Test"
override func load(fromContents contents: Any, ofType typeName: String?) throws {
guard let topFileWrapper = contents as? FileWrapper,
let textData = topFileWrapper.fileWrappers?[textFilename]?.regularFileContents else {
return
}
content = String(data: textData, encoding: .utf8)!
}
override func contents(forType typeName: String) throws -> Any {
let textFileWrapper = FileWrapper(regularFileWithContents: content.data(using: .utf8)!)
textFileWrapper.preferredFilename = textFilename
return FileWrapper(directoryWithFileWrappers: [textFilename: textFileWrapper])
}
}
ViewController.swift
func exportDocument() {
// Write to cache
let cachesDir = FileManager.default.urls(for: FileManager.SearchPathDirectory.cachesDirectory, in: .allDomainsMask).first!
let dataDir = cachesDir.appendingPathComponent("export", isDirectory: true)
try! FileManager.default.createDirectory(at: dataDir, withIntermediateDirectories: true, attributes: nil)
let fileURL = dataDir.appendingPathComponent("cookie").appendingPathExtension("zzz")
let archive = CustomDocument(fileURL: fileURL)
archive.content = "Cookie cat"
archive.save(to: archive.fileURL, for: .forCreating) { success in
guard success else {
let alertController = UIAlertController.notice(title: "Cannot export data", message: nil)
self.present(alertController, animated: true, completion: nil)
return
}
let documentPicker = UIDocumentPickerViewController(url: archive.fileURL, in: .exportToService)
documentPicker.delegate = self
self.present(documentPicker, animated: true, completion: nil)
}
}
This solves my problem: make the UTI also conform to public.composite-content, i.e.
<key>UTTypeConformsTo</key>
<array>
<string>com.apple.package</string>
<string>public.composite-content</string>
</array>
I'm not sure why though.

iOS swift streaming app does not play music in background mode

My app is running fine but as soon the screen safe is on or the doing something else on the iphone the stream stops. I activated the background modes "is playing audio" but it does not helps.
This is my ViewController.swift
import UIKit
import MediaPlayer
class ViewController: UIViewController {
let player: MPMoviePlayerViewController = MPMoviePlayerViewController(contentURL: NSURL(string: "http://url to my stream"))
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
player.moviePlayer.movieSourceType = .Streaming
self.presentViewController(player, animated: true, completion: nil)
loadAddressURL()
}
func stop() {
player.moviePlayer.stop()
}
#IBAction func Hitplay(sender: AnyObject) {
player.moviePlayer.play()
}
#IBAction func Hitpause(sender: AnyObject) {
player.moviePlayer.stop()
}
#IBOutlet var Nowplay: UIWebView!
var URLPath = "http://url to on air now"
func loadAddressURL() {
let requestURL = NSURL (string:URLPath)
let request = NSURLRequest (URL: requestURL!)
Nowplay.loadRequest(request)
}
}
and here is my info.plist
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>com.product name.$(PRODUCT_NAME:rfc1034identifier)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
Setting the audio background mode is correct, but I think you also need to set the audio category for the audio session.
Try adding this to your app delegate's didFinishLaunchingWithOptions:
var activeError: NSError? = nil
AVAudioSession.sharedInstance().setActive(true, error: &activeError)
if let actError = activeError {
NSLog("Error setting audio active: \(actError.code)")
}
var categoryError: NSError? = nil
AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, error: &categoryError)
if let catError = categoryError {
NSLog("Error setting audio category: \(catError.code)")
}

Resources